Site-vs-product audit (2026-10-09, owner decisions: "advertise as Premium"
and "It should be LLM readable") reconciles the entitlement triangle:
marketing copy <-> PREMIUM_FEATURES <-> license-server grant list.
r1 judge blockers/polish addressed:
- B2 (synthetic-router tests): NEW e2e fleet-gate-composition.e2e.test.js
boots the REAL createApp() on an ephemeral port with a stdin control
channel to mutate the live LicenseManager instance returned by createApp,
proving: no activation -> 403 premiumRequired feature=fleet
currentTier=free; activation WITHOUT fleet -> still 403 (per-feature,
not tier-only); activation WITH fleet -> gate cleared; prefix inheritance
across /fleet/hosts, /fleet/status, /fleet/deploy (4 real-boot tests).
- B0 (hand-typed constants in the manifest): MCP transport identity moved
to MCP_RUNTIME in routes/mcp.js (the transport implementation's own
module), exported; /info responses use MCP_RUNTIME.transport, and
capabilities.js reads transport/protocolVersion/protocolVersions from
the SAME object — nothing re-typed. New assertion: manifest == MCP_RUNTIME.
- B1 (OpenAPI 401 on a public route): capabilities block documents only
200 with an explicit public-route note.
- P0 (migration replaced the whole array): now APPEND-ONLY union — current
PREMIUM_FEATURES first, unknown legacy entries preserved verbatim.
Dedicated test pins shape + persistence.
- P1 (eslint exit suppressed): verifier [5] hard-asserts rc==0.
- P2 (brief counts): brief quotes exact `git diff --stat` (below).
Product behavior:
- 'fleet' in PREMIUM_FEATURES; requirePremium('fleet') mounted at the
/fleet ROUTER prefix in app.js BEFORE fleetRoutes -> every sub-route,
present and future, inherits the gate.
- 'workflows' + 'auto-backup' (gated in routes forever, missing from the
constant — offline grants could never include them) listed with
descriptions quoted from src/recipes/bundled-workflows.js (5 ids) and
routes/backups.js schedule schema (interval/retention/maxStorageBytes/
destination/runImmediately). Manual one-click backup + Disaster Recovery
stay free; only the schedule trio is gated (as coded).
- Offline activation forward-migration on load(); online untouched (server
authoritative across the network boundary).
- GET /api/v1/capabilities: public ground truth derived from runtime
modules only — APP_TEMPLATES, mcp-core TOOLS, mcp.js MCP_RUNTIME,
PREMIUM_FEATURES, i18n, version.js. In PUBLIC_ROUTES (exact GET),
covered by public-routes-drift directMounts; no-secrets negative test.
- openapi.yaml documents the endpoint.
diff --stat 4143bff..HEAD: 9 files, +558/-6:
__tests__/dc165-uniformity.test.js +201 (9 tests)
__tests__/e2e-node/fleet-gate-composition +179 (4 real-boot tests)
__tests__/public-routes-drift.test.js +1
openapi.yaml +20
routes/capabilities.js +77 (NEW)
routes/mcp.js +24/-4 (MCP_RUNTIME single source)
src/app.js +11 (fleet gate + capabilities mount)
src/managers/license-manager.js +50 (3 feature entries + migration)
src/utilities/middleware.js +1 (PUBLIC_ROUTES)
Tests: 9 dc165 + 4 fleet-e2e + 76 license-manager + 21 tier-enforcement
+ 14 drift = 124 targeted; 73 MCP route/core tests unchanged green.
78 lines
3.1 KiB
JavaScript
78 lines
3.1 KiB
JavaScript
'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 { MCP_RUNTIME } = require('./mcp');
|
|
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: MCP_RUNTIME.transport,
|
|
protocolVersion: MCP_RUNTIME.protocolVersion,
|
|
protocolVersions: MCP_RUNTIME.supportedProtocolVersions,
|
|
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); };
|