'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. The six MEASURED fields (version, template count + categories, * MCP tool names, transport identity triple, premium list, language count) * derive from the runtime modules the product itself uses (APP_TEMPLATES, * mcp-core TOOLS, mcp.js MCP_RUNTIME, PREMIUM_FEATURES, i18n, version.js) — * nothing measured is hand-typed, so those cannot drift BY CONSTRUCTION. * `product`, `description`, `api.restMount` and `mcp.endpoint` are STATIC * registration/product metadata (visible in app.js) — labeled as such in * openapi.yaml. This is the * * 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); };