Files
dashcaddy/dashcaddy-api/routes/capabilities.js
T
Hermes 9ed7fe7c5d [pending-judge] feat(DC-165 rev2): product/site uniformity — fleet gate proven at composition root, single-source capabilities manifest
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.
2026-10-09 02:25:35 -07:00

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); };