[pending-judge] feat(DC-165): product/site uniformity — fleet gate, complete premium list, LLM-readable capabilities endpoint
Site-vs-product audit (2026-10-09, owner decision: "advertise as Premium")
reconciles the entitlement triangle: marketing copy <-> PREMIUM_FEATURES
<-> license-server grant list.
- Fleet: the pricing page has always sold Multi-Host Fleet Management as
Premium; code never gated it. Added fleet to PREMIUM_FEATURES and mounted
requirePremium('fleet') at the ROUTER level (app.js) so every current AND
future /fleet/* sub-route inherits the gate (fail-closed for new endpoints).
License-server side: plans.js grants fleet for new activations; existing
premium license records backfilled (ops, DNS-license-server).
- Workflows + auto-backup: these were requirePremium-gated in routes since
forever but absent from PREMIUM_FEATURES — the offline path could never
grant them and the site never advertised them. Listed them (descriptions
taken from bundled-workflows.js + backups/schedule schema fields) so the
single source of truth now covers every gated feature; offline migration
(below) grants them to existing activations; server grant list updated.
- Offline activation forward-migration: load() now refreshes an OFFLINE
activation's features snapshot to the current PREMIUM_FEATURES set when
entries are missing (exactly what re-activating the same code today would
store). ONLINE activations are never touched — the license server stays
authoritative across the network boundary; this is the creator's local
build-to-build coherence rule, not an entitlement widening over an
external service. Persisted via credential store; in-memory state is
correct even if persistence fails (warn, not fail).
- GET /api/v1/capabilities (public, like /version): machine-readable ground
truth of the RUNNING build — version, template count + categories, MCP
tool names, protocol revisions, premium features, language count. Every
number derived from the same modules the product uses (no drift by
construction). Registered in PUBLIC_ROUTES (exact:true, GET), added to
public-routes-drift.test.js directMounts, guarded like /version.
No secrets, no host data (tested).
- 8 new executed tests: fleet 403-on-free / pass-on-premium through the REAL
requirePremium middleware (duck-typed LicenseManager instance, prototype
preserved), hasFeature fallback, offline migration (persisted + in-memory),
online NOT migrated (valid bounded cache stub incl. lastOnlineValidatedAt),
capabilities payload == module-derived values (78 templates/17 cats/31
langs/fleet present), no-secrets scan of the payload.
Tests: dc165 8/8; license-manager 76; tier-enforcement 21; public-routes-
drift + composition-root unchanged green (119 combined on the targeted run).
This commit is contained in:
7 files changed
+334
-1
No files matched your search
@@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* DC-165: Public capability manifest — one machine-readable ground truth
|
||||
* about what THIS running DashCaddy build actually does.
|
||||
*
|
||||
* Purpose (Sami 2026-10-09: "It should be LLM readable"): the marketing
|
||||
* site, llms.txt, docs and the live product must never disagree. Static
|
||||
* prose drifts (77 vs 78 templates proved it). This endpoint lets any AI
|
||||
* assistant — or the site build itself — read the truth from the running
|
||||
* product: exact version, template count + categories, MCP tool list,
|
||||
* premium feature list, protocol revisions. Every number is derived from
|
||||
* the same modules the product uses at runtime; nothing is hand-typed, so
|
||||
* it cannot drift by construction.
|
||||
*
|
||||
* Public by design (like /version and /health): no secrets, no usage data,
|
||||
* no hostnames — only static capability facts of the installed build.
|
||||
* Registered in PUBLIC_ROUTES (middleware.js, exact:true, GET) and guarded
|
||||
* by public-routes-drift.test.js like every other public route.
|
||||
*
|
||||
* buildRouter() is attached as a property so the drift test's router-walk
|
||||
* sees the route (routes/version.js convention).
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const { ok } = require('../src/utils/responses');
|
||||
const versionRoute = require('./version');
|
||||
|
||||
function buildRouter(deps = {}) {
|
||||
const router = express.Router();
|
||||
const asyncHandler = deps.asyncHandler
|
||||
|| ((fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next));
|
||||
|
||||
router.get('/capabilities', asyncHandler(async (req, res) => {
|
||||
const { APP_TEMPLATES } = require('../src/docker/app-templates');
|
||||
const templates = APP_TEMPLATES || {};
|
||||
const templateIds = Object.keys(templates);
|
||||
const categories = {};
|
||||
for (const id of templateIds) {
|
||||
const c = (templates[id] && templates[id].category) || 'uncategorized';
|
||||
categories[c] = (categories[c] || 0) + 1;
|
||||
}
|
||||
|
||||
const { TOOLS } = require('../src/mcp/mcp-core');
|
||||
const tools = (TOOLS || []).map((t) => t.name).sort();
|
||||
|
||||
const { PREMIUM_FEATURES } = require('../src/managers/license-manager');
|
||||
const premiumFeatures = Object.keys(PREMIUM_FEATURES || {}).sort();
|
||||
|
||||
const i18n = require('../src/utilities/i18n');
|
||||
let languageCount = null;
|
||||
try { languageCount = i18n.getSupportedLanguages().length; } catch (_) { /* optional surface */ }
|
||||
|
||||
ok(res, {
|
||||
product: 'dashcaddy',
|
||||
version: versionRoute.getVersion(),
|
||||
description: 'Self-hosting dashboard: one-click Docker app deployment with automatic SSL, DNS and reverse proxy; AI intent router; MCP control plane.',
|
||||
templates: { count: templateIds.length, categories },
|
||||
mcp: {
|
||||
transport: 'streamable-http',
|
||||
protocolVersions: ['2025-03-10', '2024-11-05'],
|
||||
endpoint: '/api/v1/mcp',
|
||||
tools
|
||||
},
|
||||
premiumFeatures,
|
||||
api: { restMount: '/api/v1' },
|
||||
i18n: { languageCount }
|
||||
});
|
||||
}));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = { buildRouter };
|
||||
module.exports.default = function factory(ctx) { return buildRouter(ctx); };
|
||||
Reference in new issue
Block a user