@fonderie/admin 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ui/app.global.js +7 -7
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -572,7 +572,7 @@ var uiHtml = (scriptPath) => `<!doctype html>
|
|
|
572
572
|
// src/module.ts
|
|
573
573
|
var DEFAULT_ADMIN_PATH = "/_admin";
|
|
574
574
|
var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
575
|
-
var ADMIN_VERSION = "1.0.
|
|
575
|
+
var ADMIN_VERSION = "1.0.2";
|
|
576
576
|
function normalizeHostPattern(host) {
|
|
577
577
|
return host.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/.*$/, "").toLowerCase();
|
|
578
578
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../../node_modules/tsup/assets/cjs_shims.js","../src/module.ts","../src/doctor.ts","../src/log.ts","../src/migrate.ts","../src/manifest.ts","../src/pages.ts","../src/tokens.ts","../src/ui/html.ts"],"sourcesContent":["export { AdminModule, ADMIN_VERSION, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS } from './module';\nexport { buildManifest } from './manifest';\nexport { runDoctor, collectChecks, attention } from './doctor';\nexport { adminLog, readAdminLog, DEFAULT_ACTOR, MAX_PAGE } from './log';\nexport { environmentReport, routesReport, tokensReport } from './pages';\nexport { migrationsReport, applyModuleMigrations, applyMigrationsSchema } from './migrate';\nexport {\n\trequireAdminScope,\n\tscopeFor,\n\tgrants,\n\tissueToken,\n\trevokeToken,\n\tlistTokens,\n\thashToken,\n\tSCOPES,\n} from './tokens';\nexport type {\n\tIAdminOptions,\n\tIAdminManifest,\n\tIAdminModuleEntry,\n\tIAdminCheckResult,\n\tIAdminDoctorReport,\n\tIAdminAttention,\n\tIAdminAttentionItem,\n\tIAdminLogEntry,\n\tIAdminLogPage,\n\tIAdminEnvEntry,\n\tIAdminEnvironmentReport,\n\tAdminRouteGuard,\n\tIAdminRouteEntry,\n\tIAdminRoutesReport,\n\tIAdminTokenEntry,\n\tIAdminTokensReport,\n\tAdminScope,\n\tIAdminTokenRecord,\n\tIMigrationSet,\n\tIAdminPendingMigration,\n\tIAdminMigrationModule,\n\tIAdminMigrationsReport,\n} from './types';\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import { readFileSync } from 'node:fs';\nimport type { IFonderieApp, IFonderieModule, IReadinessProblem, Middleware } from '@fonderie/core';\nimport { HTTP, normalizeMountPath, normalizeRequestPath, setApiResponse } from '@fonderie/core';\nimport { validate, validateAdminToken } from '@fonderie/core/middlewares';\n\nimport { attention, collectChecks, runDoctor } from './doctor';\nimport { adminLog, readAdminLog } from './log';\nimport { applyMigrationsSchema, applyModuleMigrations, migrationsReport } from './migrate';\nimport { buildManifest } from './manifest';\nimport { environmentReport, routesReport, tokensReport } from './pages';\nimport {\n\tissueToken,\n\tissueTokenSchema,\n\tlistTokens,\n\trequireAdminScope,\n\trevokeToken,\n\tscopeFor,\n} from './tokens';\nimport { uiHtml } from './ui/html';\nimport type { AdminScope } from './types';\nimport type { IAdminOptions } from './types';\n\nexport const DEFAULT_ADMIN_PATH = '/_admin';\nexport const DEFAULT_CHECK_TIMEOUT_MS = 10_000;\n\n// Replaced at build time (tsup env); the fallback is what tests see.\n// Baked in at build time by tsup.base — see its note on why a module reports\n// its version at all.\nexport const ADMIN_VERSION: string = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\n\n// 'https://admin.example.com/' and 'admin.example.com' name the same host, so\n// accept both. A scheme here is the likely mistake — this option sits beside\n// ones that are full URLs — and left unnormalized it matches nothing, so the\n// surface 404s everywhere and looks exactly like it was never mounted.\nfunction normalizeHostPattern(host: string): string {\n\treturn host\n\t\t.trim()\n\t\t.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, '')\n\t\t.replace(/\\/.*$/, '')\n\t\t.toLowerCase();\n}\n\n// A request addressed to a hostname this surface does not answer for gets the\n// same 404 an unmounted surface gives — indistinguishable on purpose. A 403\n// would confirm both that the surface exists and that you found the wrong door.\nfunction requireAdminHost(hosts: readonly string[]): Middleware {\n\tconst allowed = new Set(hosts.map(normalizeHostPattern).filter(Boolean));\n\treturn (ctx, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\t// Match with or without the port: a configured 'admin.example.com' should\n\t\t// not stop working behind a non-default port, and a configured\n\t\t// 'localhost:3000' should still be exact. Ports are not a boundary here.\n\t\tconst ok = allowed.has(url.host.toLowerCase()) || allowed.has(url.hostname.toLowerCase());\n\t\treturn ok ? next() : Promise.resolve(setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found'));\n\t};\n}\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\t// null = answer on any hostname.\n\treadonly hosts: string[] | null;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = normalizeMountPath(options.path ?? DEFAULT_ADMIN_PATH);\n\t\tthis.hosts = options.host === undefined ? null : [options.host].flat();\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst token = this.options.adminToken;\n\t\tif (!token) return;\n\n\t\tapp.reserve(this.path);\n\t\t// Declared at the default path so the route table reads literally; re-based when configured.\n\t\tconst at = (p: string): string => this.path + p.slice(DEFAULT_ADMIN_PATH.length);\n\n\t\tconst store = this.options.store;\n\t\t// First in every chain: a surface bound to another hostname must look\n\t\t// unmounted here, before anything reads a token or a body.\n\t\tconst hostGuard = this.hosts ? requireAdminHost(this.hosts) : null;\n\t\t// Collected once at boot: a duplicate name is refused here, not at request time.\n\t\tconst checks = collectChecks(app, this.options.checks ?? []);\n\t\tconst timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;\n\t\tconst doctor = () => runDoctor(checks, timeoutMs);\n\n\t\tconst own: Array<[string, string, Middleware]> = [\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_ATTENTION',\n\t\t\t\t\t\t'What needs attention',\n\t\t\t\t\t\tattention(app, await doctor()),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/manifest',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_MANIFEST',\n\t\t\t\t\t\t'Deployment manifest',\n\t\t\t\t\t\tbuildManifest(app, { version: this.version, log: Boolean(store), host: this.hosts }),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/doctor',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor()),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t// NOT '/_admin/config'. This reports readiness problems and which\n\t\t\t\t// DECLARED ENV VARS are set — a deployment report, not configuration.\n\t\t\t\t// It held the name anyway, which blocked @fonderie/config from ever\n\t\t\t\t// mounting beside the console (both described GET /_admin/config and\n\t\t\t\t// boot failed), and the misnomer is what made the shell probe\n\t\t\t\t// '/config' to detect the config brick — a probe that was true on\n\t\t\t\t// every deployment, built a client against admin's own report and\n\t\t\t\t// crashed the page with \"a.map is not a function\".\n\t\t\t\t'/_admin/environment',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_ENVIRONMENT',\n\t\t\t\t\t\t'Declared vs held',\n\t\t\t\t\t\tenvironmentReport(app, this.options.env ?? []),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/routes',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_ROUTES', 'Exposed routes', routesReport(app, this.name)),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/access/tokens',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_TOKENS',\n\t\t\t\t\t\t'Admin tokens',\n\t\t\t\t\t\ttokensReport(app, this.name, store ? await listTokens(store) : null),\n\t\t\t\t\t),\n\t\t\t],\n\t\t];\n\t\tif (store) {\n\t\t\town.push([\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/activity/admin-log',\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst q = new URL(ctx.request.url).searchParams;\n\t\t\t\t\tconst limit = Number(q.get('limit')) || undefined;\n\t\t\t\t\tconst before = q.get('before') ?? undefined;\n\t\t\t\t\tconst page = await readAdminLog(store, {\n\t\t\t\t\t\t...(limit ? { limit } : {}),\n\t\t\t\t\t\t...(before ? { before } : {}),\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'ADMIN_LOG', 'Admin activity', page);\n\t\t\t\t},\n\t\t\t]);\n\t\t}\n\t\t// Issuing and revoking are root-only: a scoped token can never mint one.\n\t\tconst rootOnly: Array<[string, string, Middleware[]]> = store\n\t\t\t? [\n\t\t\t\t\t[\n\t\t\t\t\t\t'POST',\n\t\t\t\t\t\t'/_admin/access/tokens',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tvalidate(issueTokenSchema),\n\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\tconst body = ctx.meta['body'] as {\n\t\t\t\t\t\t\t\t\tname: string;\n\t\t\t\t\t\t\t\t\tscopes: AdminScope[];\n\t\t\t\t\t\t\t\t\texpiresInDays?: number;\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\tconst createdBy = ctx.request.headers.get('x-actor') || 'admin-token';\n\t\t\t\t\t\t\t\tconst { token: plaintext, record } = await issueToken(store, {\n\t\t\t\t\t\t\t\t\t...body,\n\t\t\t\t\t\t\t\t\tcreatedBy,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t// The plaintext is returned once and never stored.\n\t\t\t\t\t\t\t\treturn setApiResponse(HTTP.CREATED, 'TOKEN_ISSUED', 'Token issued — shown once', {\n\t\t\t\t\t\t\t\t\ttoken: plaintext,\n\t\t\t\t\t\t\t\t\t...record,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t\t[\n\t\t\t\t\t\t'DELETE',\n\t\t\t\t\t\t'/_admin/access/tokens/:id',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\tconst ok = await revokeToken(store, ctx.meta.params?.['id'] ?? '');\n\t\t\t\t\t\t\t\treturn ok\n\t\t\t\t\t\t\t\t\t? setApiResponse(HTTP.OK, 'TOKEN_REVOKED', 'Token revoked')\n\t\t\t\t\t\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such live token');\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t]\n\t\t\t: [];\n\t\t// Reporting AND applying pending migrations. Registered only when the app\n\t\t// hands over its migration sequence — this surface must never infer that\n\t\t// order (see IAdminOptions.migrations), so without it there is nothing\n\t\t// honest to say and the routes do not exist.\n\t\t//\n\t\t// Scope is derived, not annotated: scopeFor() gives GET ⇒ read, POST ⇒\n\t\t// write. Deliberately NOT 'root' like the token routes — those are root\n\t\t// because a scoped token minting a token is privilege escalation.\n\t\t// Applying migrations that provably delete nothing is not.\n\t\tconst migrationSets = this.options.migrations;\n\t\tconst scoped: Array<[string, string, Middleware[]]> =\n\t\t\tstore && migrationSets\n\t\t\t\t? [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'GET',\n\t\t\t\t\t\t\t'/_admin/migrations',\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\tasync () =>\n\t\t\t\t\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t\t\t\t\t'MIGRATIONS',\n\t\t\t\t\t\t\t\t\t\t'Pending migrations by module',\n\t\t\t\t\t\t\t\t\t\tawait migrationsReport(store, migrationSets),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t],\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'POST',\n\t\t\t\t\t\t\t'/_admin/migrations/:module/apply',\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\tvalidate(applyMigrationsSchema),\n\t\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\t\tconst { expect } = ctx.meta['body'] as { expect: string[] };\n\t\t\t\t\t\t\t\t\tconst name = ctx.meta.params?.['module'] ?? '';\n\t\t\t\t\t\t\t\t\tconst out = await applyModuleMigrations(store, migrationSets, name, expect);\n\n\t\t\t\t\t\t\t\t\tif (out.ok) {\n\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t\t\t\t\t\tout.reason,\n\t\t\t\t\t\t\t\t\t\t\tout.reason === 'MIGRATIONS_APPLIED'\n\t\t\t\t\t\t\t\t\t\t\t\t? `Applied. ${out.module.pending.length} still pending in ${name}.`\n\t\t\t\t\t\t\t\t\t\t\t\t: `${name} is already up to date`,\n\t\t\t\t\t\t\t\t\t\t\tout.module,\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tswitch (out.reason) {\n\t\t\t\t\t\t\t\t\t\tcase 'NOT_FOUND':\n\t\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\t\tHTTP.NOT_FOUND,\n\t\t\t\t\t\t\t\t\t\t\t\t'NOT_FOUND',\n\t\t\t\t\t\t\t\t\t\t\t\t`No migration set named \"${name}\"`,\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\tcase 'MIGRATIONS_OUT_OF_ORDER':\n\t\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t\t\t\t\t\tout.reason,\n\t\t\t\t\t\t\t\t\t\t\t\t`Apply \"${out.blockedBy}\" first — it runs before \"${name}\" and is behind.`,\n\t\t\t\t\t\t\t\t\t\t\t\t{ blockedBy: out.blockedBy },\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\tcase 'MIGRATIONS_CHANGED':\n\t\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t\t\t\t\t\tout.reason,\n\t\t\t\t\t\t\t\t\t\t\t\t'What is pending changed since you looked. Refresh and read it again.',\n\t\t\t\t\t\t\t\t\t\t\t\t{ expected: out.expected, actual: out.actual },\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t\t\t\t\t\t\tout.reason,\n\t\t\t\t\t\t\t\t\t\t\t\t'A pending migration deletes data. No down-migration brings it back — ' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'apply it through CI or `npm run migrate`, not from here.',\n\t\t\t\t\t\t\t\t\t\t\t\t{ files: out.files },\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t],\n\t\t\t\t\t]\n\t\t\t\t: [];\n\t\tconst mounted = new Map<string, string>();\n\t\tconst mount = (\n\t\t\tmodule: string,\n\t\t\tmethod: string,\n\t\t\tpath: string,\n\t\t\thandlers: Middleware[],\n\t\t\tneeded?: AdminScope | 'root',\n\t\t): void => {\n\t\t\tconst key = `${method.toUpperCase()} ${path}`;\n\t\t\tconst prior = mounted.get(key);\n\t\t\tif (prior) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[fonderie] ${module} cannot describe ${key}: ${prior} already describes it`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tmounted.set(key, module);\n\t\t\t// The scope comes from the route itself. The log sits before the guard:\n\t\t\t// a refused request is a row too.\n\t\t\tconst guard = requireAdminScope(token, store, needed ?? scopeFor(method, path));\n\t\t\tconst chain: Middleware[] = [\n\t\t\t\t// Logged first so a wrong-host attempt still leaves a row: the\n\t\t\t\t// caller learns nothing from a 404, the operator learns something.\n\t\t\t\t...(store ? [adminLog(store, path, module)] : []),\n\t\t\t\t...(hostGuard ? [hostGuard] : []),\n\t\t\t\tguard,\n\t\t\t\t...handlers,\n\t\t\t];\n\t\t\tapp.addRoute(method, path, ...chain);\n\t\t};\n\n\t\t// The served dashboard. Deliberately OUTSIDE the guard: a browser\n\t\t// navigating to a page cannot send an Authorization header, and neither\n\t\t// file carries data — the HTML is a shell and the script is the same\n\t\t// bundle npm serves. The page asks for a token and sends it itself, so\n\t\t// every request that reads anything is guarded and logged as usual.\n\t\t// These two are not logged: they carry no token to attribute.\n\t\tif (this.options.ui) {\n\t\t\tconst scriptPath = at('/_admin/ui/app.js');\n\t\t\t// Read on first request, not at boot: the bundle sits beside the BUILT\n\t\t\t// module (tsup names its IIFE output <entry>.global.js), so running from\n\t\t\t// source — tests, a linked checkout — has none, and that must not stop\n\t\t\t// an app from booting over a dashboard it may never open.\n\t\t\tlet js: string | null = null;\n\t\t\tconst loadJs = (): string | null => {\n\t\t\t\tif (js === null) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tjs = readFileSync(new URL('./ui/app.global.js', import.meta.url), 'utf8');\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tjs = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn js || null;\n\t\t\t};\n\t\t\tconst ui = (h: Middleware): Middleware[] => (hostGuard ? [hostGuard, h] : [h]);\n\t\t\tapp.addRoute(\n\t\t\t\t'GET',\n\t\t\t\tat('/_admin/ui'),\n\t\t\t\t...ui(async (ctx) => {\n\t\t\t\t\t// From the request, not from config: this is the only place that\n\t\t\t\t\t// knows basePath, a moved path AND a trailing slash at once. The\n\t\t\t\t\t// router's own normalizer, so the href and the routing cannot\n\t\t\t\t\t// disagree about what this path is.\n\t\t\t\t\tconst here = normalizeRequestPath(new URL(ctx.request.url).pathname);\n\t\t\t\t\treturn new Response(uiHtml(`${here}/app.js`), {\n\t\t\t\t\t\theaders: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },\n\t\t\t\t\t});\n\t\t\t\t}),\n\t\t\t);\n\t\t\tapp.addRoute(\n\t\t\t\t'GET',\n\t\t\t\tscriptPath,\n\t\t\t\t...ui(async () => {\n\t\t\t\t\tconst body = loadJs();\n\t\t\t\t\treturn body\n\t\t\t\t\t\t? new Response(body, {\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t'content-type': 'text/javascript; charset=utf-8',\n\t\t\t\t\t\t\t\t\t'cache-control': 'public, max-age=300',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: setApiResponse(\n\t\t\t\t\t\t\t\tHTTP.SERVICE_UNAVAILABLE,\n\t\t\t\t\t\t\t\t'UI_NOT_BUILT',\n\t\t\t\t\t\t\t\t'This copy of @fonderie/admin has no built dashboard (dist/ui). Install the published package, or run its build.',\n\t\t\t\t\t\t\t);\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tfor (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);\n\t\tfor (const [method, path, handlers] of rootOnly)\n\t\t\tmount(this.name, method, at(path), handlers, 'root');\n\t\tfor (const [method, path, handlers] of scoped) mount(this.name, method, at(path), handlers);\n\t\tfor (const { module, description } of app.adminDescriptions()) {\n\t\t\tfor (const r of description.routes ?? [])\n\t\t\t\tmount(module, r.method, this.path + r.path, r.handlers);\n\t\t}\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IAdminCheck, IAdminCheckReport, IFonderieApp } from '@fonderie/core';\n\nimport type { IAdminAttention, IAdminCheckResult, IAdminDoctorReport } from './types';\n\nexport interface INamedCheck extends IAdminCheck {\n\tmodule: string;\n}\n\n// Module checks first (sorted by module), then the app's own. A name used\n// twice would make a report ambiguous, so it is refused up front.\nexport function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[] {\n\tconst seen = new Map<string, string>();\n\tconst out: INamedCheck[] = [];\n\tconst add = (module: string, check: IAdminCheck): void => {\n\t\tconst prior = seen.get(check.name);\n\t\tif (prior)\n\t\t\tthrow new Error(\n\t\t\t\t`[fonderie] ${module} cannot offer check \"${check.name}\": ${prior} already does`,\n\t\t\t);\n\t\tseen.set(check.name, module);\n\t\tout.push({ ...check, module });\n\t};\n\tfor (const { module, description } of app.adminDescriptions()) {\n\t\tfor (const c of description.checks ?? []) add(module, c);\n\t}\n\tfor (const c of own) add('application', c);\n\treturn out;\n}\n\nasync function runOne(check: INamedCheck, timeoutMs: number): Promise<IAdminCheckResult> {\n\tconst started = Date.now();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<IAdminCheckReport>((resolve) => {\n\t\ttimer = setTimeout(\n\t\t\t() => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet report: IAdminCheckReport;\n\ttry {\n\t\treport = await Promise.race([check.run(), timeout]);\n\t} catch (err) {\n\t\treport = {\n\t\t\tok: false,\n\t\t\tfindings: [`check threw: ${err instanceof Error ? err.message : String(err)}`],\n\t\t};\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n\treturn { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };\n}\n\nexport async function runDoctor(\n\tchecks: INamedCheck[],\n\ttimeoutMs: number,\n): Promise<IAdminDoctorReport> {\n\tconst results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));\n\treturn { generatedAt: new Date().toISOString(), ok: results.every((r) => r.ok), checks: results };\n}\n\n// What needs the operator today: readiness problems as reported, failed\n// checks as errors, findings on a passing check as advice. Empty = green.\nexport function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention {\n\tconst items: IAdminAttention['items'] = [];\n\tfor (const p of app.checkProductionReadiness().problems) {\n\t\titems.push({\n\t\t\tsource: p.module,\n\t\t\tseverity: p.severity === 'error' ? 'error' : 'advice',\n\t\t\tmessage: p.message,\n\t\t});\n\t}\n\tfor (const c of doctor.checks) {\n\t\tif (c.skipped) continue;\n\t\tconst severity = c.ok ? 'advice' : 'error';\n\t\tif (!c.ok && c.findings.length === 0)\n\t\t\titems.push({ source: c.name, severity, message: 'failed' });\n\t\tfor (const f of c.findings) items.push({ source: c.name, severity, message: f });\n\t}\n\treturn { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === 'error'), items };\n}\n","import { normalizeRequestPath } from '@fonderie/core';\nimport type { IFonderieContext, Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAdminLogEntry, IAdminLogPage } from './types';\n\nexport const DEFAULT_ACTOR = 'admin-token';\n\n// Sits BEFORE the token guard, so a refused request is a row too. A failed\n// write must not fail the request it describes.\nexport function adminLog(store: IStoreAdapter, route: string, module: string): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst started = Date.now();\n\t\tconst res = await next();\n\t\tconst row = {\n\t\t\t// A scoped token names itself; X-Actor refines it; the root token is 'admin-token'.\n\t\t\tactor:\n\t\t\t\tctx.request.headers.get('x-actor') ||\n\t\t\t\t(typeof ctx.meta['adminTokenName'] === 'string'\n\t\t\t\t\t? `token:${ctx.meta['adminTokenName']}`\n\t\t\t\t\t: DEFAULT_ACTOR),\n\t\t\tmethod: ctx.request.method,\n\t\t\t// Normalized, so '/x' and '/x/' are one row for one route — the same\n\t\t\t// path the router matched and `route` below records the pattern of.\n\t\t\tpath: normalizeRequestPath(new URL(ctx.request.url).pathname),\n\t\t\troute,\n\t\t\tmodule,\n\t\t\tstatus: res.status,\n\t\t\tdurationMs: Date.now() - started,\n\t\t\trequestId: typeof ctx.meta['requestId'] === 'string' ? ctx.meta['requestId'] : null,\n\t\t\tclientIp: ctx.meta.clientIp ?? null,\n\t\t};\n\t\ttry {\n\t\t\tawait store.query(\n\t\t\t\t`INSERT INTO fonderie_admin_log\n\t\t\t\t (actor, method, path, route, module, status, duration_ms, request_id, client_ip)\n\t\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,\n\t\t\t\t[\n\t\t\t\t\trow.actor,\n\t\t\t\t\trow.method,\n\t\t\t\t\trow.path,\n\t\t\t\t\trow.route,\n\t\t\t\t\trow.module,\n\t\t\t\t\trow.status,\n\t\t\t\t\trow.durationMs,\n\t\t\t\t\trow.requestId,\n\t\t\t\t\trow.clientIp,\n\t\t\t\t],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error(\n\t\t\t\t'[admin] could not write the admin log:',\n\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t);\n\t\t}\n\t\treturn res;\n\t};\n}\n\nexport const MAX_PAGE = 200;\n\n// Newest first, keyset on (at, id). `before` is the last entry's cursor.\nexport async function readAdminLog(\n\tstore: IStoreAdapter,\n\topts: { limit?: number; before?: string } = {},\n): Promise<IAdminLogPage> {\n\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);\n\tconst cursor = opts.before ? decodeCursor(opts.before) : null;\n\tconst rows = await store.query<IAdminLogEntry>(\n\t\t`SELECT id, at, actor, method, path, route, module, status,\n\t\t duration_ms AS \"durationMs\", request_id AS \"requestId\", client_ip AS \"clientIp\"\n\t\t FROM fonderie_admin_log\n\t\t WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))\n\t\t ORDER BY at DESC, id DESC\n\t\t LIMIT $3`,\n\t\t[cursor?.at ?? null, cursor?.id ?? null, limit + 1],\n\t);\n\tconst page = rows.slice(0, limit);\n\tconst last = page[page.length - 1];\n\treturn {\n\t\tentries: page,\n\t\tnext: rows.length > limit && last ? encodeCursor(last) : null,\n\t};\n}\n\nfunction encodeCursor(e: IAdminLogEntry): string {\n\treturn Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, 'utf8').toString('base64url');\n}\n\nfunction decodeCursor(c: string): { at: string; id: string } | null {\n\tconst [at, id] = Buffer.from(c, 'base64url').toString('utf8').split('|');\n\treturn at && id ? { at, id } : null;\n}\n","import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { IRequestSchema } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport { classifyMigration, InternalMigrationRunner } from '@fonderie/store';\n\nimport type { IAdminMigrationModule, IAdminMigrationsReport, IMigrationSet } from './types';\n\n/**\n * `expect` is the exact pending list the operator was shown, in order.\n *\n * It is not ceremony. Between rendering the page and clicking the button a\n * deploy can change what is pending — and the thing that changed is precisely\n * the thing nobody reviewed. Sending it back makes the server refuse rather\n * than apply a set the operator never saw. Same shape as versionedWrite's\n * if_version, and it also gives this POST the body schema audit:validation\n * requires.\n */\nexport const applyMigrationsSchema: IRequestSchema = {\n\tsafeParse(input: unknown) {\n\t\tconst fail = (path: string, message: string) => ({\n\t\t\tsuccess: false as const,\n\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t});\n\t\tconst b = (input ?? {}) as Record<string, unknown>;\n\t\tconst expect = b['expect'];\n\t\tif (!Array.isArray(expect) || !expect.every((f) => typeof f === 'string'))\n\t\t\treturn fail('expect', 'expect must be the list of pending filenames you were shown');\n\t\t// An empty list is legitimate: it asserts \"I was told there was nothing\n\t\t// to do\", and the handler answers UP_TO_DATE or CHANGED accordingly.\n\t\treturn { success: true as const, data: { expect: expect as string[] } };\n\t},\n};\n\n// Applying is the APP's order, never ours. The sets arrive already interleaved\n// — brick dirs and the app's own, in the sequence its applier runs them — from\n// the one constant both the applier and this reporter read. See LeadEasyGen's\n// db/migrations/steps.ts: \"auth owns fonderie_users, which the app migration\n// extends, and media's assets reference storage's blobs.\"\n\n// Has this database EVER been migrated? pending() cannot answer it: it catches\n// its own read failure and returns every file, so a virgin database, a database\n// belonging to another app, and an unreachable one are the same answer. Asked\n// directly, because \"61 pending\" means something completely different depending\n// on which of those it is.\nasync function hasMigrationHistory(store: IStoreAdapter): Promise<boolean> {\n\ttry {\n\t\tconst rows = await store.query<{ n: number }>(\n\t\t\t'SELECT count(*)::int AS n FROM fonderie_migrations',\n\t\t);\n\t\treturn (rows[0]?.n ?? 0) > 0;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction classifyPending(dir: string, files: readonly string[]) {\n\treturn files.map((file) => {\n\t\tconst { impact, destructive } = classifyMigration(readFileSync(join(dir, file), 'utf8'));\n\t\treturn { file, impact, destructive };\n\t});\n}\n\n/**\n * What every module is waiting on, with each pending file's impact.\n *\n * `blockedBy` names the first EARLIER module that is behind. Order is\n * load-bearing and declared by the app, so a module cannot be applied while\n * something it may depend on is unapplied — the panel offers one button at a\n * time and the operator walks the list in the declared order.\n */\nexport async function migrationsReport(\n\tstore: IStoreAdapter,\n\tsets: ReadonlyArray<IMigrationSet>,\n): Promise<IAdminMigrationsReport> {\n\tconst everApplied = await hasMigrationHistory(store);\n\n\tconst modules: IAdminMigrationModule[] = [];\n\tlet firstBehind: string | null = null;\n\n\tfor (const [name, dir] of sets) {\n\t\tconst pending = await new InternalMigrationRunner(store, dir).pending();\n\t\tconst files = classifyPending(dir, pending);\n\t\tmodules.push({\n\t\t\tname,\n\t\t\tpending: files,\n\t\t\t// Only a module BEFORE this one blocks it. The first module with\n\t\t\t// pending work blocks every module after it, and nothing blocks it.\n\t\t\tblockedBy: firstBehind,\n\t\t\t// A file that deletes data on a database with nothing in it yet is not\n\t\t\t// destroying anything. Same rule the CLI's --check applies.\n\t\t\tappliable: files.length > 0 && firstBehind === null && (!everApplied || files.every((f) => f.impact === 'additive')),\n\t\t});\n\t\tif (files.length > 0 && firstBehind === null) firstBehind = name;\n\t}\n\n\treturn { everApplied, modules };\n}\n\nexport type ApplyOutcome =\n\t| { ok: true; reason: 'MIGRATIONS_APPLIED' | 'MIGRATIONS_UP_TO_DATE'; module: IAdminMigrationModule }\n\t| { ok: false; reason: 'NOT_FOUND' }\n\t| { ok: false; reason: 'MIGRATIONS_OUT_OF_ORDER'; blockedBy: string }\n\t| { ok: false; reason: 'MIGRATIONS_CHANGED'; expected: string[]; actual: string[] }\n\t| { ok: false; reason: 'MIGRATION_DESTRUCTIVE'; files: IAdminMigrationModule['pending'] };\n\n/**\n * Apply one module's pending migrations — all of them, or none.\n *\n * There is no partial apply, deliberately: files run in lexicographic order, so\n * an additive migration sorting AFTER a destructive one may depend on it.\n * Cherry-picking around a DROP would apply 901 against a schema that never saw\n * 900. Refusing the whole module at the first destructive file is the only safe\n * reading, and happens to be the only one MigrationRunner.run() supports.\n *\n * `expect` is the file list the operator was looking at. Between the render and\n * the click a deploy can change what is pending; without this the button would\n * apply something nobody reviewed.\n */\nexport async function applyModuleMigrations(\n\tstore: IStoreAdapter,\n\tsets: ReadonlyArray<IMigrationSet>,\n\tmodule: string,\n\texpect: readonly string[],\n): Promise<ApplyOutcome> {\n\tconst set = sets.find(([name]) => name === module);\n\tif (!set) return { ok: false, reason: 'NOT_FOUND' };\n\n\t// Recomputed here rather than trusted from a previous call: this is the\n\t// check that decides whether to write.\n\tconst before = await migrationsReport(store, sets);\n\tconst target = before.modules.find((m) => m.name === module);\n\tif (!target) return { ok: false, reason: 'NOT_FOUND' };\n\n\tif (target.blockedBy) {\n\t\treturn { ok: false, reason: 'MIGRATIONS_OUT_OF_ORDER', blockedBy: target.blockedBy };\n\t}\n\n\tconst actual = target.pending.map((p) => p.file);\n\tconst same = actual.length === expect.length && actual.every((f, i) => f === expect[i]);\n\tif (!same) {\n\t\treturn { ok: false, reason: 'MIGRATIONS_CHANGED', expected: [...expect], actual };\n\t}\n\n\tif (actual.length === 0) {\n\t\treturn { ok: true, reason: 'MIGRATIONS_UP_TO_DATE', module: target };\n\t}\n\n\tconst destructive = target.pending.filter((p) => p.impact === 'destructive');\n\tif (destructive.length > 0 && before.everApplied) {\n\t\treturn { ok: false, reason: 'MIGRATION_DESTRUCTIVE', files: destructive };\n\t}\n\n\tawait new InternalMigrationRunner(store, set[1]).run();\n\n\t// Re-READ, never assume. run() returns void, and a request that dies\n\t// mid-apply still committed every file it finished — each one is its own\n\t// transaction. What the database says now is the only honest answer.\n\tconst after = await migrationsReport(store, sets);\n\tconst now = after.modules.find((m) => m.name === module);\n\treturn {\n\t\tok: true,\n\t\treason: 'MIGRATIONS_APPLIED',\n\t\tmodule: now ?? { name: module, pending: [], blockedBy: null, appliable: false },\n\t};\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminManifest, IAdminModuleEntry } from './types';\n\nexport function buildManifest(\n\tapp: IFonderieApp,\n\tadmin: { version: string; log: boolean; host: string[] | null },\n): IAdminManifest {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems) {\n\t\tconst list = byModule.get(p.module) ?? [];\n\t\tlist.push(p);\n\t\tbyModule.set(p.module, list);\n\t}\n\tconst describing = new Set(app.adminDescriptions().map((d) => d.module));\n\tconst modules: IAdminModuleEntry[] = report.modules.map(({ name, version }) => {\n\t\tconst problems = byModule.get(name) ?? [];\n\t\treturn {\n\t\t\tname,\n\t\t\tversion: version ?? null,\n\t\t\treadiness: { ok: !problems.some((p) => p.severity === 'error'), problems },\n\t\t\tdescribesAdmin: describing.has(name),\n\t\t};\n\t});\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tenv: report.env,\n\t\tadmin,\n\t\tmodules,\n\t\treadiness: report.readiness,\n\t\troutes: app.routes(),\n\t};\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type {\n\tIAdminEnvironmentReport,\n\tIAdminRoutesReport,\n\tIAdminTokenRecord,\n\tIAdminTokensReport,\n} from './types';\n\nexport function environmentReport(app: IFonderieApp, env: string[]): IAdminEnvironmentReport {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems)\n\t\tbyModule.set(p.module, [...(byModule.get(p.module) ?? []), p]);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\treadiness: report.readiness,\n\t\tmodules: report.modules.map(({ name }) => ({ name, problems: byModule.get(name) ?? [] })),\n\t\tenv: env.map((name) => ({\n\t\t\tname,\n\t\t\tset: process.env[name] !== undefined && process.env[name] !== '',\n\t\t})),\n\t};\n}\n\nconst PROBES = new Set(['/healthz', '/readyz', '/metrics']);\n\nexport function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport {\n\treturn {\n\t\tgeneratedAt: new Date().toISOString(),\n\t\troutes: app.routes().map((r) => ({\n\t\t\t...r,\n\t\t\tguard:\n\t\t\t\tr.module === adminModule\n\t\t\t\t\t? 'admin'\n\t\t\t\t\t: PROBES.has(r.path) && r.module === '@fonderie/core'\n\t\t\t\t\t\t? 'probe'\n\t\t\t\t\t\t: 'app',\n\t\t})),\n\t};\n}\n\n// A brick's own adminToken shows as a readiness problem only when weak, so\n// \"set at all\" has to be asked; a described module that also registers a\n// legacy route under /admin, /plans or /billing/wallet/grant has one.\nexport function tokensReport(\n\tapp: IFonderieApp,\n\tadminModule: string,\n\tissued: IAdminTokenRecord[] | null,\n): IAdminTokensReport {\n\tconst report = app.securityReport();\n\tconst adminProblems = report.readiness.problems.filter((p) => p.module === adminModule);\n\tconst legacyOwners = new Set(\n\t\tapp\n\t\t\t.routes()\n\t\t\t.filter((r) => r.module && r.module !== adminModule && isLegacyAdminPath(r.method, r.path))\n\t\t\t.map((r) => r.module as string),\n\t);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tadmin: { ok: !adminProblems.some((p) => p.severity === 'error'), problems: adminProblems },\n\t\tlegacy: report.modules\n\t\t\t.map(({ name }) => ({ module: name, set: legacyOwners.has(name) }))\n\t\t\t.filter((e) => e.set),\n\t\tissued,\n\t};\n}\n\nfunction isLegacyAdminPath(method: string, path: string): boolean {\n\tif (/\\/admin\\/(config|secrets|templates)(\\/|$)/.test(path)) return true;\n\tif (/\\/plans(\\/|$)/.test(path) && method !== 'GET') return true;\n\treturn /\\/billing\\/wallet\\/grant$/.test(path);\n}\n","import { createHash, randomBytes } from 'node:crypto';\nimport type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { HTTP, constantTimeEqual, setApiResponse } from '@fonderie/core';\nimport type { IRequestSchema } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { AdminScope, IAdminTokenRecord } from './types';\n\nexport const SCOPES: readonly AdminScope[] = ['read', 'write', 'secrets'];\n\n// Derived from the route, never annotated: the plaintext secret read is the\n// one thing that needs `secrets`; any other mutation needs `write`.\nexport function scopeFor(method: string, path: string): AdminScope {\n\tif (/\\/secrets(\\/|$)/.test(path)) return 'secrets';\n\treturn method.toUpperCase() === 'GET' ? 'read' : 'write';\n}\n\n// `write` implies `read`; `secrets` implies both.\nexport function grants(held: readonly AdminScope[], needed: AdminScope): boolean {\n\tif (held.includes('secrets')) return true;\n\tif (needed === 'secrets') return false;\n\tif (held.includes('write')) return true;\n\treturn needed === 'read' && held.includes('read');\n}\n\nexport const hashToken = (token: string): string =>\n\tcreateHash('sha256').update(token).digest('hex');\nconst mintToken = (): string => `fad_${randomBytes(24).toString('base64url')}`;\n\nconst COLUMNS = `id, name, scopes, created_by AS \"createdBy\", created_at AS \"createdAt\",\n expires_at AS \"expiresAt\", revoked_at AS \"revokedAt\", last_used_at AS \"lastUsedAt\"`;\n\nexport async function listTokens(store: IStoreAdapter): Promise<IAdminTokenRecord[]> {\n\treturn store.query<IAdminTokenRecord>(\n\t\t`SELECT ${COLUMNS} FROM fonderie_admin_tokens ORDER BY created_at DESC`,\n\t);\n}\n\nexport async function issueToken(\n\tstore: IStoreAdapter,\n\tinput: { name: string; scopes: AdminScope[]; expiresInDays?: number; createdBy: string },\n): Promise<{ token: string; record: IAdminTokenRecord }> {\n\tconst token = mintToken();\n\tconst expiresAt = input.expiresInDays\n\t\t? new Date(Date.now() + input.expiresInDays * 86_400_000)\n\t\t: null;\n\tconst [record] = await store.query<IAdminTokenRecord>(\n\t\t`INSERT INTO fonderie_admin_tokens (name, token_hash, scopes, created_by, expires_at)\n\t\t VALUES ($1, $2, $3, $4, $5) RETURNING ${COLUMNS}`,\n\t\t[input.name, hashToken(token), input.scopes, input.createdBy, expiresAt],\n\t);\n\tif (!record) throw new Error('[admin] token insert returned no row');\n\treturn { token, record };\n}\n\nexport async function revokeToken(store: IStoreAdapter, id: string): Promise<boolean> {\n\tconst rows = await store.query<{ id: string }>(\n\t\t`UPDATE fonderie_admin_tokens SET revoked_at = now() WHERE id = $1 AND revoked_at IS NULL RETURNING id`,\n\t\t[id],\n\t);\n\treturn rows.length > 0;\n}\n\nasync function findLive(store: IStoreAdapter, token: string): Promise<IAdminTokenRecord | null> {\n\tconst [row] = await store.query<IAdminTokenRecord>(\n\t\t`SELECT ${COLUMNS} FROM fonderie_admin_tokens\n\t\t WHERE token_hash = $1 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())`,\n\t\t[hashToken(token)],\n\t);\n\treturn row ?? null;\n}\n\nconst bearer = (ctx: IFonderieContext): string => {\n\tconst h = ctx.request.headers.get('authorization') ?? '';\n\treturn h.startsWith('Bearer ') ? h.slice(7) : '';\n};\nconst UNAUTHORIZED = () =>\n\tsetApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token');\n\n// The bootstrap token is the root: every scope, and the only credential that\n// may issue or revoke tokens. Issued tokens carry their scopes; a valid token\n// short of the route's scope is 403, an unknown, revoked or expired one is\n// the same 401 as a missing one. The token's name becomes the log's actor.\nexport function requireAdminScope(\n\tbootstrap: string,\n\tstore: IStoreAdapter | undefined,\n\tneeded: AdminScope | 'root',\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst token = bearer(ctx);\n\t\tif (!token) return UNAUTHORIZED();\n\t\tif (constantTimeEqual(token, bootstrap)) return next();\n\t\tif (needed === 'root' || !store) return UNAUTHORIZED();\n\t\tconst record = await findLive(store, token);\n\t\tif (!record) return UNAUTHORIZED();\n\t\tif (!grants(record.scopes, needed)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', `This token lacks the '${needed}' scope`);\n\t\t}\n\t\tctx.meta['adminTokenName'] = record.name;\n\t\tvoid store\n\t\t\t.query(`UPDATE fonderie_admin_tokens SET last_used_at = now() WHERE id = $1`, [record.id])\n\t\t\t.catch(() => undefined);\n\t\treturn next();\n\t};\n}\n\n// { name: 1–100 chars, scopes: non-empty subset of SCOPES, expiresInDays?: 1–3650 }\nexport const issueTokenSchema: IRequestSchema = {\n\tsafeParse(input: unknown) {\n\t\tconst fail = (path: string, message: string) => ({\n\t\t\tsuccess: false as const,\n\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t});\n\t\tconst b = (input ?? {}) as Record<string, unknown>;\n\t\tconst name = typeof b['name'] === 'string' ? b['name'].trim() : '';\n\t\tif (!name || name.length > 100) return fail('name', 'name is required (1–100 chars)');\n\t\tconst scopes = b['scopes'];\n\t\tif (\n\t\t\t!Array.isArray(scopes) ||\n\t\t\tscopes.length === 0 ||\n\t\t\t!scopes.every((s) => SCOPES.includes(s as AdminScope))\n\t\t)\n\t\t\treturn fail('scopes', `scopes must be a non-empty list of ${SCOPES.join(', ')}`);\n\t\tconst days = b['expiresInDays'];\n\t\tif (\n\t\t\tdays !== undefined &&\n\t\t\t(!Number.isInteger(days) || (days as number) < 1 || (days as number) > 3650)\n\t\t)\n\t\t\treturn fail('expiresInDays', 'expiresInDays must be an integer between 1 and 3650');\n\t\treturn {\n\t\t\tsuccess: true as const,\n\t\t\tdata: {\n\t\t\t\tname,\n\t\t\t\tscopes: [...new Set(scopes as AdminScope[])],\n\t\t\t\t...(days !== undefined ? { expiresInDays: days as number } : {}),\n\t\t\t},\n\t\t};\n\t},\n};\n","// The served shell's document. Inlined as a string so it ships in dist with no\n// copy step. The script path is passed in, not relative: the surface can be\n// mounted under any basePath or path, and a trailing slash must not break it.\n//\n// The `:root` block is the organisation's design tokens (organization/ui/base.css),\n// carried here rather than imported: this file is a string in dist, the admin\n// package has no CSS pipeline, and an operator console must not depend on a\n// stylesheet it might fail to fetch. Screens read these through\n// `var(--fonderie-*)` in their inline styles, so the palette lives in exactly\n// one place and both light and dark follow from it.\n//\n// Namespaced `--fonderie-*` deliberately. The dashboard can be mounted inside a\n// consumer's own page, and bare `--color-text` would collide with theirs —\n// silently, and only in their app.\n//\n// NO WEBFONT. The tokens name Inter first and fall through to the system stack,\n// but nothing is fetched: an admin console should not announce its existence to\n// a third party, and it has to work on an air-gapped deploy.\n// The dark palette, applied from two places: when the OS asks for it, and when\n// the operator asks for it explicitly. Declared once so the two cannot drift.\nconst DARK = `\n--fonderie-accent:#00d294;\n--fonderie-accent-strong:#00e6a2;\n--fonderie-warning:#f5a623;\n--fonderie-danger:#f33;\n--fonderie-bg:#0a0a0a;\n--fonderie-surface:#111;\n--fonderie-surface-alt:#1a1a1a;\n--fonderie-border:#27272a;\n--fonderie-border-light:#1c1c1e;\n--fonderie-text:#ededed;\n--fonderie-text-muted:#a1a1aa;\n--fonderie-topbar:#0a0a0a;\n--fonderie-shadow-card:0 2px 3px 0 rgba(0,0,0,.3);\n`;\n\nconst TOKENS = `\n:root{\n--fonderie-accent:#00d294;\n--fonderie-accent-strong:#009767;\n--fonderie-warning:#f5a623;\n--fonderie-danger:#e00;\n--fonderie-bg:#fafafa;\n--fonderie-surface:#fff;\n--fonderie-surface-alt:#fafafa;\n--fonderie-border:#e0e0e0;\n--fonderie-border-light:#f5f5f5;\n--fonderie-text:#171717;\n--fonderie-text-muted:#5c5c5c;\n--fonderie-topbar:#000;\n--fonderie-font:Inter,\"Inter Fallback\",-apple-system,BlinkMacSystemFont,\"Helvetica Neue\",system-ui,sans-serif;\n--fonderie-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;\n--fonderie-tracking-display:-0.05em;\n--fonderie-tracking-body:-0.02em;\n--fonderie-radius:4px;\n--fonderie-radius-lg:8px;\n--fonderie-shadow-card:0 2px 3px 0 rgba(0,0,0,.05);\n}\n/* OS says dark, and the operator has not overridden it. */\n@media (prefers-color-scheme:dark){:root:not([data-theme=\"light\"]){${DARK}}}\n/* The operator chose dark, whatever the OS says. Without this rule \"Dark\" on a\n light machine did nothing — the console could only ever follow the system. */\n:root[data-theme=\"dark\"]{${DARK}}\nhtml,body{margin:0;padding:0}\nbody{\nbackground:var(--fonderie-bg);\ncolor:var(--fonderie-text);\nfont-family:var(--fonderie-font);\nfont-size:14px;\nline-height:1.7;\nletter-spacing:var(--fonderie-tracking-body);\n-webkit-font-smoothing:antialiased;\n-moz-osx-font-smoothing:grayscale;\n}\nh1,h2,h3{letter-spacing:var(--fonderie-tracking-display);font-weight:600;line-height:1.25}\ncode,pre{font-family:var(--fonderie-mono)}\n/* Focus must stay visible on a console that performs irreversible actions. */\n:focus-visible{outline:2px solid var(--fonderie-accent);outline-offset:2px}\n\n/* Theme switcher — copied from the organisation UI (theme.css .theme-switch),\n with --color-* mapped to --fonderie-*. It lives here rather than in inline\n styles because the selected pill is expressed with :has(input:checked): the\n radio is the state, so nothing in React has to style the label by hand. */\n.theme-switch{\ndisplay:inline-flex;\nalign-items:center;\ngap:0;\nborder:1px solid var(--fonderie-border);\nborder-radius:9999px;\npadding:3px;\nmargin:0;\nbackground:var(--fonderie-bg);\n}\n.theme-switch__option{\ndisplay:inline-flex;\nalign-items:center;\ngap:4px;\npadding:4px 10px;\nborder-radius:9999px;\ncursor:pointer;\nfont-size:12px;\ncolor:var(--fonderie-text-muted);\ntransition:background .15s,color .15s;\nwhite-space:nowrap;\n}\n.theme-switch__option input{position:absolute;opacity:0;width:0;height:0;pointer-events:none}\n.theme-switch__option:has(input:checked){\nbackground:var(--fonderie-surface);\ncolor:var(--fonderie-text);\nbox-shadow:0 1px 2px rgba(0,0,0,.06);\n}\n/* :has() is wide but not universal. Without it every option reads as unselected\n and the control looks broken, so keyboard focus alone must still show where\n you are — this is the fallback that keeps it usable. */\n.theme-switch__option:focus-within{color:var(--fonderie-text)}\n.theme-switch__option svg{flex-shrink:0}\n.theme-switch__option span{line-height:1}\n`;\n\n// Applies a STORED theme before the first paint. Without it an operator who\n// chose light on a dark machine gets a dark flash on every load, which is the\n// specific thing a console cannot look like.\n//\n// \"System\" is the ABSENCE of the attribute, so this script does nothing in that\n// case and the media query above handles it — which also means system-mode\n// follows the OS live, with no listener. The organisation's own switcher\n// resolves system eagerly to a concrete value and writes it, so a machine that\n// flips to dark at sunset keeps the old theme until reload; this does not.\n//\n// Degrades correctly: if localStorage throws, or a consumer's CSP blocks the\n// inline script, no attribute is set and the console follows the system — the\n// default either way.\nconst BOOT = `(function(){try{var t=localStorage.getItem('fonderie.admin.theme');` +\n\t`if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t)}catch(e){}})()`;\n\nexport const uiHtml = (scriptPath: string): string => `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex, nofollow\">\n<meta name=\"color-scheme\" content=\"light dark\">\n<title>Admin</title>\n<style>${TOKENS}</style>\n<script>${BOOT}</script>\n</head>\n<body>\n<div id=\"root\"></div>\n<script src=\"${scriptPath}\" defer></script>\n</body>\n</html>\n`;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,IAAM,mBAAmB,MACvB,OAAO,aAAa,cAChB,IAAI,IAAI,QAAQ,UAAU,EAAE,EAAE,OAC7B,SAAS,iBAAiB,SAAS,cAAc,QAAQ,YAAY,MAAM,WAC1E,SAAS,cAAc,MACvB,IAAI,IAAI,WAAW,SAAS,OAAO,EAAE;AAEtC,IAAM,gBAAgC,iCAAiB;;;ACZ9D,IAAAA,kBAA6B;AAE7B,IAAAC,eAA+E;AAC/E,yBAA6C;;;ACOtC,SAAS,cAAc,KAAmB,KAAmC;AACnF,QAAM,OAAO,oBAAI,IAAoB;AACrC,QAAM,MAAqB,CAAC;AAC5B,QAAM,MAAM,CAACC,SAAgB,UAA6B;AACzD,UAAM,QAAQ,KAAK,IAAI,MAAM,IAAI;AACjC,QAAI;AACH,YAAM,IAAI;AAAA,QACT,cAAcA,OAAM,wBAAwB,MAAM,IAAI,MAAM,KAAK;AAAA,MAClE;AACD,SAAK,IAAI,MAAM,MAAMA,OAAM;AAC3B,QAAI,KAAK,EAAE,GAAG,OAAO,QAAAA,QAAO,CAAC;AAAA,EAC9B;AACA,aAAW,EAAE,QAAAA,SAAQ,YAAY,KAAK,IAAI,kBAAkB,GAAG;AAC9D,eAAW,KAAK,YAAY,UAAU,CAAC,EAAG,KAAIA,SAAQ,CAAC;AAAA,EACxD;AACA,aAAW,KAAK,IAAK,KAAI,eAAe,CAAC;AACzC,SAAO;AACR;AAEA,eAAe,OAAO,OAAoB,WAA+C;AACxF,QAAM,UAAU,KAAK,IAAI;AACzB,MAAI;AACJ,QAAM,UAAU,IAAI,QAA2B,CAAC,YAAY;AAC3D,YAAQ;AAAA,MACP,MAAM,QAAQ,EAAE,IAAI,OAAO,UAAU,CAAC,mBAAmB,SAAS,KAAK,EAAE,CAAC;AAAA,MAC1E;AAAA,IACD;AAAA,EACD,CAAC;AACD,MAAI;AACJ,MAAI;AACH,aAAS,MAAM,QAAQ,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC;AAAA,EACnD,SAAS,KAAK;AACb,aAAS;AAAA,MACR,IAAI;AAAA,MACJ,UAAU,CAAC,gBAAgB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,IAC9E;AAAA,EACD,UAAE;AACD,QAAI,MAAO,cAAa,KAAK;AAAA,EAC9B;AACA,SAAO,EAAE,MAAM,MAAM,MAAM,QAAQ,MAAM,QAAQ,GAAG,QAAQ,YAAY,KAAK,IAAI,IAAI,QAAQ;AAC9F;AAEA,eAAsB,UACrB,QACA,WAC8B;AAC9B,QAAM,UAAU,MAAM,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC;AACzE,SAAO,EAAE,cAAa,oBAAI,KAAK,GAAE,YAAY,GAAG,IAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,EAAE,GAAG,QAAQ,QAAQ;AACjG;AAIO,SAAS,UAAU,KAAmB,QAA6C;AACzF,QAAM,QAAkC,CAAC;AACzC,aAAW,KAAK,IAAI,yBAAyB,EAAE,UAAU;AACxD,UAAM,KAAK;AAAA,MACV,QAAQ,EAAE;AAAA,MACV,UAAU,EAAE,aAAa,UAAU,UAAU;AAAA,MAC7C,SAAS,EAAE;AAAA,IACZ,CAAC;AAAA,EACF;AACA,aAAW,KAAK,OAAO,QAAQ;AAC9B,QAAI,EAAE,QAAS;AACf,UAAM,WAAW,EAAE,KAAK,WAAW;AACnC,QAAI,CAAC,EAAE,MAAM,EAAE,SAAS,WAAW;AAClC,YAAM,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,SAAS,SAAS,CAAC;AAC3D,eAAW,KAAK,EAAE,SAAU,OAAM,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,SAAS,EAAE,CAAC;AAAA,EAChF;AACA,SAAO,EAAE,aAAa,OAAO,aAAa,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,MAAM;AACjG;;;AC/EA,kBAAqC;AAM9B,IAAM,gBAAgB;AAItB,SAAS,SAAS,OAAsB,OAAeC,SAA4B;AACzF,SAAO,OAAO,KAAuB,SAAS;AAC7C,UAAM,UAAU,KAAK,IAAI;AACzB,UAAM,MAAM,MAAM,KAAK;AACvB,UAAM,MAAM;AAAA;AAAA,MAEX,OACC,IAAI,QAAQ,QAAQ,IAAI,SAAS,MAChC,OAAO,IAAI,KAAK,gBAAgB,MAAM,WACpC,SAAS,IAAI,KAAK,gBAAgB,CAAC,KACnC;AAAA,MACJ,QAAQ,IAAI,QAAQ;AAAA;AAAA;AAAA,MAGpB,UAAM,kCAAqB,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,QAAQ;AAAA,MAC5D;AAAA,MACA,QAAAA;AAAA,MACA,QAAQ,IAAI;AAAA,MACZ,YAAY,KAAK,IAAI,IAAI;AAAA,MACzB,WAAW,OAAO,IAAI,KAAK,WAAW,MAAM,WAAW,IAAI,KAAK,WAAW,IAAI;AAAA,MAC/E,UAAU,IAAI,KAAK,YAAY;AAAA,IAChC;AACA,QAAI;AACH,YAAM,MAAM;AAAA,QACX;AAAA;AAAA;AAAA,QAGA;AAAA,UACC,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,QACL;AAAA,MACD;AAAA,IACD,SAAS,KAAK;AACb,cAAQ;AAAA,QACP;AAAA,QACA,eAAe,QAAQ,IAAI,UAAU;AAAA,MACtC;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;AAEO,IAAM,WAAW;AAGxB,eAAsB,aACrB,OACA,OAA4C,CAAC,GACpB;AACzB,QAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,SAAS,IAAI,CAAC,GAAG,QAAQ;AAC9D,QAAM,SAAS,KAAK,SAAS,aAAa,KAAK,MAAM,IAAI;AACzD,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,CAAC,QAAQ,MAAM,MAAM,QAAQ,MAAM,MAAM,QAAQ,CAAC;AAAA,EACnD;AACA,QAAM,OAAO,KAAK,MAAM,GAAG,KAAK;AAChC,QAAM,OAAO,KAAK,KAAK,SAAS,CAAC;AACjC,SAAO;AAAA,IACN,SAAS;AAAA,IACT,MAAM,KAAK,SAAS,SAAS,OAAO,aAAa,IAAI,IAAI;AAAA,EAC1D;AACD;AAEA,SAAS,aAAa,GAA2B;AAChD,SAAO,OAAO,KAAK,GAAG,IAAI,KAAK,EAAE,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,MAAM,EAAE,SAAS,WAAW;AAC3F;AAEA,SAAS,aAAa,GAA8C;AACnE,QAAM,CAAC,IAAI,EAAE,IAAI,OAAO,KAAK,GAAG,WAAW,EAAE,SAAS,MAAM,EAAE,MAAM,GAAG;AACvE,SAAO,MAAM,KAAK,EAAE,IAAI,GAAG,IAAI;AAChC;;;AC5FA,qBAA6B;AAC7B,uBAAqB;AAGrB,mBAA2D;AAcpD,IAAM,wBAAwC;AAAA,EACpD,UAAU,OAAgB;AACzB,UAAM,OAAO,CAAC,MAAc,aAAqB;AAAA,MAChD,SAAS;AAAA,MACT,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAC,EAAE;AAAA,IAC9C;AACA,UAAM,IAAK,SAAS,CAAC;AACrB,UAAM,SAAS,EAAE,QAAQ;AACzB,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,CAAC,OAAO,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AACvE,aAAO,KAAK,UAAU,6DAA6D;AAGpF,WAAO,EAAE,SAAS,MAAe,MAAM,EAAE,OAA2B,EAAE;AAAA,EACvE;AACD;AAaA,eAAe,oBAAoB,OAAwC;AAC1E,MAAI;AACH,UAAM,OAAO,MAAM,MAAM;AAAA,MACxB;AAAA,IACD;AACA,YAAQ,KAAK,CAAC,GAAG,KAAK,KAAK;AAAA,EAC5B,QAAQ;AACP,WAAO;AAAA,EACR;AACD;AAEA,SAAS,gBAAgB,KAAa,OAA0B;AAC/D,SAAO,MAAM,IAAI,CAAC,SAAS;AAC1B,UAAM,EAAE,QAAQ,YAAY,QAAI,oCAAkB,iCAAa,uBAAK,KAAK,IAAI,GAAG,MAAM,CAAC;AACvF,WAAO,EAAE,MAAM,QAAQ,YAAY;AAAA,EACpC,CAAC;AACF;AAUA,eAAsB,iBACrB,OACA,MACkC;AAClC,QAAM,cAAc,MAAM,oBAAoB,KAAK;AAEnD,QAAM,UAAmC,CAAC;AAC1C,MAAI,cAA6B;AAEjC,aAAW,CAAC,MAAM,GAAG,KAAK,MAAM;AAC/B,UAAM,UAAU,MAAM,IAAI,qCAAwB,OAAO,GAAG,EAAE,QAAQ;AACtE,UAAM,QAAQ,gBAAgB,KAAK,OAAO;AAC1C,YAAQ,KAAK;AAAA,MACZ;AAAA,MACA,SAAS;AAAA;AAAA;AAAA,MAGT,WAAW;AAAA;AAAA;AAAA,MAGX,WAAW,MAAM,SAAS,KAAK,gBAAgB,SAAS,CAAC,eAAe,MAAM,MAAM,CAAC,MAAM,EAAE,WAAW,UAAU;AAAA,IACnH,CAAC;AACD,QAAI,MAAM,SAAS,KAAK,gBAAgB,KAAM,eAAc;AAAA,EAC7D;AAEA,SAAO,EAAE,aAAa,QAAQ;AAC/B;AAsBA,eAAsB,sBACrB,OACA,MACAC,SACA,QACwB;AACxB,QAAM,MAAM,KAAK,KAAK,CAAC,CAAC,IAAI,MAAM,SAASA,OAAM;AACjD,MAAI,CAAC,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,YAAY;AAIlD,QAAM,SAAS,MAAM,iBAAiB,OAAO,IAAI;AACjD,QAAM,SAAS,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,SAASA,OAAM;AAC3D,MAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,OAAO,QAAQ,YAAY;AAErD,MAAI,OAAO,WAAW;AACrB,WAAO,EAAE,IAAI,OAAO,QAAQ,2BAA2B,WAAW,OAAO,UAAU;AAAA,EACpF;AAEA,QAAM,SAAS,OAAO,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI;AAC/C,QAAM,OAAO,OAAO,WAAW,OAAO,UAAU,OAAO,MAAM,CAAC,GAAG,MAAM,MAAM,OAAO,CAAC,CAAC;AACtF,MAAI,CAAC,MAAM;AACV,WAAO,EAAE,IAAI,OAAO,QAAQ,sBAAsB,UAAU,CAAC,GAAG,MAAM,GAAG,OAAO;AAAA,EACjF;AAEA,MAAI,OAAO,WAAW,GAAG;AACxB,WAAO,EAAE,IAAI,MAAM,QAAQ,yBAAyB,QAAQ,OAAO;AAAA,EACpE;AAEA,QAAM,cAAc,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,WAAW,aAAa;AAC3E,MAAI,YAAY,SAAS,KAAK,OAAO,aAAa;AACjD,WAAO,EAAE,IAAI,OAAO,QAAQ,yBAAyB,OAAO,YAAY;AAAA,EACzE;AAEA,QAAM,IAAI,qCAAwB,OAAO,IAAI,CAAC,CAAC,EAAE,IAAI;AAKrD,QAAM,QAAQ,MAAM,iBAAiB,OAAO,IAAI;AAChD,QAAM,MAAM,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,SAASA,OAAM;AACvD,SAAO;AAAA,IACN,IAAI;AAAA,IACJ,QAAQ;AAAA,IACR,QAAQ,OAAO,EAAE,MAAMA,SAAQ,SAAS,CAAC,GAAG,WAAW,MAAM,WAAW,MAAM;AAAA,EAC/E;AACD;;;ACjKO,SAAS,cACf,KACA,OACiB;AACjB,QAAM,SAAS,IAAI,eAAe;AAClC,QAAM,WAAW,oBAAI,IAAiC;AACtD,aAAW,KAAK,OAAO,UAAU,UAAU;AAC1C,UAAM,OAAO,SAAS,IAAI,EAAE,MAAM,KAAK,CAAC;AACxC,SAAK,KAAK,CAAC;AACX,aAAS,IAAI,EAAE,QAAQ,IAAI;AAAA,EAC5B;AACA,QAAM,aAAa,IAAI,IAAI,IAAI,kBAAkB,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AACvE,QAAM,UAA+B,OAAO,QAAQ,IAAI,CAAC,EAAE,MAAM,QAAQ,MAAM;AAC9E,UAAM,WAAW,SAAS,IAAI,IAAI,KAAK,CAAC;AACxC,WAAO;AAAA,MACN;AAAA,MACA,SAAS,WAAW;AAAA,MACpB,WAAW,EAAE,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,SAAS;AAAA,MACzE,gBAAgB,WAAW,IAAI,IAAI;AAAA,IACpC;AAAA,EACD,CAAC;AACD,SAAO;AAAA,IACN,aAAa,OAAO;AAAA,IACpB,KAAK,OAAO;AAAA,IACZ;AAAA,IACA;AAAA,IACA,WAAW,OAAO;AAAA,IAClB,QAAQ,IAAI,OAAO;AAAA,EACpB;AACD;;;ACxBO,SAAS,kBAAkB,KAAmB,KAAwC;AAC5F,QAAM,SAAS,IAAI,eAAe;AAClC,QAAM,WAAW,oBAAI,IAAiC;AACtD,aAAW,KAAK,OAAO,UAAU;AAChC,aAAS,IAAI,EAAE,QAAQ,CAAC,GAAI,SAAS,IAAI,EAAE,MAAM,KAAK,CAAC,GAAI,CAAC,CAAC;AAC9D,SAAO;AAAA,IACN,aAAa,OAAO;AAAA,IACpB,WAAW,OAAO;AAAA,IAClB,SAAS,OAAO,QAAQ,IAAI,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,SAAS,IAAI,IAAI,KAAK,CAAC,EAAE,EAAE;AAAA,IACxF,KAAK,IAAI,IAAI,CAAC,UAAU;AAAA,MACvB;AAAA,MACA,KAAK,QAAQ,IAAI,IAAI,MAAM,UAAa,QAAQ,IAAI,IAAI,MAAM;AAAA,IAC/D,EAAE;AAAA,EACH;AACD;AAEA,IAAM,SAAS,oBAAI,IAAI,CAAC,YAAY,WAAW,UAAU,CAAC;AAEnD,SAAS,aAAa,KAAmB,aAAyC;AACxF,SAAO;AAAA,IACN,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,IAAI,OAAO,EAAE,IAAI,CAAC,OAAO;AAAA,MAChC,GAAG;AAAA,MACH,OACC,EAAE,WAAW,cACV,UACA,OAAO,IAAI,EAAE,IAAI,KAAK,EAAE,WAAW,mBAClC,UACA;AAAA,IACN,EAAE;AAAA,EACH;AACD;AAKO,SAAS,aACf,KACA,aACA,QACqB;AACrB,QAAM,SAAS,IAAI,eAAe;AAClC,QAAM,gBAAgB,OAAO,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,WAAW,WAAW;AACtF,QAAM,eAAe,IAAI;AAAA,IACxB,IACE,OAAO,EACP,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,eAAe,kBAAkB,EAAE,QAAQ,EAAE,IAAI,CAAC,EACzF,IAAI,CAAC,MAAM,EAAE,MAAgB;AAAA,EAChC;AACA,SAAO;AAAA,IACN,aAAa,OAAO;AAAA,IACpB,OAAO,EAAE,IAAI,CAAC,cAAc,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,UAAU,cAAc;AAAA,IACzF,QAAQ,OAAO,QACb,IAAI,CAAC,EAAE,KAAK,OAAO,EAAE,QAAQ,MAAM,KAAK,aAAa,IAAI,IAAI,EAAE,EAAE,EACjE,OAAO,CAAC,MAAM,EAAE,GAAG;AAAA,IACrB;AAAA,EACD;AACD;AAEA,SAAS,kBAAkB,QAAgB,MAAuB;AACjE,MAAI,4CAA4C,KAAK,IAAI,EAAG,QAAO;AACnE,MAAI,gBAAgB,KAAK,IAAI,KAAK,WAAW,MAAO,QAAO;AAC3D,SAAO,4BAA4B,KAAK,IAAI;AAC7C;;;ACxEA,yBAAwC;AAExC,IAAAC,eAAwD;AAMjD,IAAM,SAAgC,CAAC,QAAQ,SAAS,SAAS;AAIjE,SAAS,SAAS,QAAgB,MAA0B;AAClE,MAAI,kBAAkB,KAAK,IAAI,EAAG,QAAO;AACzC,SAAO,OAAO,YAAY,MAAM,QAAQ,SAAS;AAClD;AAGO,SAAS,OAAO,MAA6B,QAA6B;AAChF,MAAI,KAAK,SAAS,SAAS,EAAG,QAAO;AACrC,MAAI,WAAW,UAAW,QAAO;AACjC,MAAI,KAAK,SAAS,OAAO,EAAG,QAAO;AACnC,SAAO,WAAW,UAAU,KAAK,SAAS,MAAM;AACjD;AAEO,IAAM,YAAY,CAAC,cACzB,+BAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAChD,IAAM,YAAY,MAAc,WAAO,gCAAY,EAAE,EAAE,SAAS,WAAW,CAAC;AAE5E,IAAM,UAAU;AAAA;AAGhB,eAAsB,WAAW,OAAoD;AACpF,SAAO,MAAM;AAAA,IACZ,UAAU,OAAO;AAAA,EAClB;AACD;AAEA,eAAsB,WACrB,OACA,OACwD;AACxD,QAAM,QAAQ,UAAU;AACxB,QAAM,YAAY,MAAM,gBACrB,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,gBAAgB,KAAU,IACtD;AACH,QAAM,CAAC,MAAM,IAAI,MAAM,MAAM;AAAA,IAC5B;AAAA,2CACyC,OAAO;AAAA,IAChD,CAAC,MAAM,MAAM,UAAU,KAAK,GAAG,MAAM,QAAQ,MAAM,WAAW,SAAS;AAAA,EACxE;AACA,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,sCAAsC;AACnE,SAAO,EAAE,OAAO,OAAO;AACxB;AAEA,eAAsB,YAAY,OAAsB,IAA8B;AACrF,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA,IACA,CAAC,EAAE;AAAA,EACJ;AACA,SAAO,KAAK,SAAS;AACtB;AAEA,eAAe,SAAS,OAAsB,OAAkD;AAC/F,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB,UAAU,OAAO;AAAA;AAAA,IAEjB,CAAC,UAAU,KAAK,CAAC;AAAA,EAClB;AACA,SAAO,OAAO;AACf;AAEA,IAAM,SAAS,CAAC,QAAkC;AACjD,QAAM,IAAI,IAAI,QAAQ,QAAQ,IAAI,eAAe,KAAK;AACtD,SAAO,EAAE,WAAW,SAAS,IAAI,EAAE,MAAM,CAAC,IAAI;AAC/C;AACA,IAAM,eAAe,UACpB,6BAAe,kBAAK,cAAc,gBAAgB,gCAAgC;AAM5E,SAAS,kBACf,WACA,OACA,QACa;AACb,SAAO,OAAO,KAAK,SAAS;AAC3B,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,CAAC,MAAO,QAAO,aAAa;AAChC,YAAI,gCAAkB,OAAO,SAAS,EAAG,QAAO,KAAK;AACrD,QAAI,WAAW,UAAU,CAAC,MAAO,QAAO,aAAa;AACrD,UAAM,SAAS,MAAM,SAAS,OAAO,KAAK;AAC1C,QAAI,CAAC,OAAQ,QAAO,aAAa;AACjC,QAAI,CAAC,OAAO,OAAO,QAAQ,MAAM,GAAG;AACnC,iBAAO,6BAAe,kBAAK,WAAW,aAAa,yBAAyB,MAAM,SAAS;AAAA,IAC5F;AACA,QAAI,KAAK,gBAAgB,IAAI,OAAO;AACpC,SAAK,MACH,MAAM,uEAAuE,CAAC,OAAO,EAAE,CAAC,EACxF,MAAM,MAAM,MAAS;AACvB,WAAO,KAAK;AAAA,EACb;AACD;AAGO,IAAM,mBAAmC;AAAA,EAC/C,UAAU,OAAgB;AACzB,UAAM,OAAO,CAAC,MAAc,aAAqB;AAAA,MAChD,SAAS;AAAA,MACT,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAC,EAAE;AAAA,IAC9C;AACA,UAAM,IAAK,SAAS,CAAC;AACrB,UAAM,OAAO,OAAO,EAAE,MAAM,MAAM,WAAW,EAAE,MAAM,EAAE,KAAK,IAAI;AAChE,QAAI,CAAC,QAAQ,KAAK,SAAS,IAAK,QAAO,KAAK,QAAQ,qCAAgC;AACpF,UAAM,SAAS,EAAE,QAAQ;AACzB,QACC,CAAC,MAAM,QAAQ,MAAM,KACrB,OAAO,WAAW,KAClB,CAAC,OAAO,MAAM,CAAC,MAAM,OAAO,SAAS,CAAe,CAAC;AAErD,aAAO,KAAK,UAAU,sCAAsC,OAAO,KAAK,IAAI,CAAC,EAAE;AAChF,UAAM,OAAO,EAAE,eAAe;AAC9B,QACC,SAAS,WACR,CAAC,OAAO,UAAU,IAAI,KAAM,OAAkB,KAAM,OAAkB;AAEvE,aAAO,KAAK,iBAAiB,qDAAqD;AACnF,WAAO;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,QACL;AAAA,QACA,QAAQ,CAAC,GAAG,IAAI,IAAI,MAAsB,CAAC;AAAA,QAC3C,GAAI,SAAS,SAAY,EAAE,eAAe,KAAe,IAAI,CAAC;AAAA,MAC/D;AAAA,IACD;AAAA,EACD;AACD;;;ACtHA,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBb,IAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qEAuBsD,IAAI;AAAA;AAAA;AAAA,2BAG9C,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsE/B,IAAM,OAAO;AAGN,IAAM,SAAS,CAAC,eAA+B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAQ7C,MAAM;AAAA,UACL,IAAI;AAAA;AAAA;AAAA;AAAA,eAIC,UAAU;AAAA;AAAA;AAAA;;;AP9HlB,IAAM,qBAAqB;AAC3B,IAAM,2BAA2B;AAKjC,IAAM,gBAAwB;AAMrC,SAAS,qBAAqB,MAAsB;AACnD,SAAO,KACL,KAAK,EACL,QAAQ,4BAA4B,EAAE,EACtC,QAAQ,SAAS,EAAE,EACnB,YAAY;AACf;AAKA,SAAS,iBAAiB,OAAsC;AAC/D,QAAM,UAAU,IAAI,IAAI,MAAM,IAAI,oBAAoB,EAAE,OAAO,OAAO,CAAC;AACvE,SAAO,CAAC,KAAK,SAAS;AACrB,UAAM,MAAM,IAAI,IAAI,IAAI,QAAQ,GAAG;AAInC,UAAM,KAAK,QAAQ,IAAI,IAAI,KAAK,YAAY,CAAC,KAAK,QAAQ,IAAI,IAAI,SAAS,YAAY,CAAC;AACxF,WAAO,KAAK,KAAK,IAAI,QAAQ,YAAQ,6BAAe,kBAAK,WAAW,aAAa,WAAW,CAAC;AAAA,EAC9F;AACD;AAEO,IAAM,cAAN,MAA6C;AAAA,EAOnD,YAAoB,UAAyB,CAAC,GAAG;AAA7B;AACnB,SAAK,WAAO,iCAAmB,QAAQ,QAAQ,kBAAkB;AACjE,SAAK,QAAQ,QAAQ,SAAS,SAAY,OAAO,CAAC,QAAQ,IAAI,EAAE,KAAK;AAAA,EACtE;AAAA,EAHoB;AAAA,EANX,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA;AAAA,EAEA;AAAA,EAOT,QAAQ,KAAyB;AAChC,UAAM,QAAQ,KAAK,QAAQ;AAC3B,QAAI,CAAC,MAAO;AAEZ,QAAI,QAAQ,KAAK,IAAI;AAErB,UAAM,KAAK,CAAC,MAAsB,KAAK,OAAO,EAAE,MAAM,mBAAmB,MAAM;AAE/E,UAAM,QAAQ,KAAK,QAAQ;AAG3B,UAAM,YAAY,KAAK,QAAQ,iBAAiB,KAAK,KAAK,IAAI;AAE9D,UAAM,SAAS,cAAc,KAAK,KAAK,QAAQ,UAAU,CAAC,CAAC;AAC3D,UAAM,YAAY,KAAK,QAAQ,kBAAkB;AACjD,UAAM,SAAS,MAAM,UAAU,QAAQ,SAAS;AAEhD,UAAM,MAA2C;AAAA,MAChD;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC;AAAA,UACC,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,UAAU,KAAK,MAAM,OAAO,CAAC;AAAA,QAC9B;AAAA,MACF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC;AAAA,UACC,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,cAAc,KAAK,EAAE,SAAS,KAAK,SAAS,KAAK,QAAQ,KAAK,GAAG,MAAM,KAAK,MAAM,CAAC;AAAA,QACpF;AAAA,MACF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC,6BAAe,kBAAK,IAAI,gBAAgB,yBAAyB,MAAM,OAAO,CAAC;AAAA,MACjF;AAAA,MACA;AAAA,QACC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QASA;AAAA,QACA,gBACC;AAAA,UACC,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,kBAAkB,KAAK,KAAK,QAAQ,OAAO,CAAC,CAAC;AAAA,QAC9C;AAAA,MACF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC,6BAAe,kBAAK,IAAI,gBAAgB,kBAAkB,aAAa,KAAK,KAAK,IAAI,CAAC;AAAA,MACxF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC;AAAA,UACC,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,aAAa,KAAK,KAAK,MAAM,QAAQ,MAAM,WAAW,KAAK,IAAI,IAAI;AAAA,QACpE;AAAA,MACF;AAAA,IACD;AACA,QAAI,OAAO;AACV,UAAI,KAAK;AAAA,QACR;AAAA,QACA;AAAA,QACA,OAAO,QAAQ;AACd,gBAAM,IAAI,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE;AACnC,gBAAM,QAAQ,OAAO,EAAE,IAAI,OAAO,CAAC,KAAK;AACxC,gBAAM,SAAS,EAAE,IAAI,QAAQ,KAAK;AAClC,gBAAM,OAAO,MAAM,aAAa,OAAO;AAAA,YACtC,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,YACzB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,UAC5B,CAAC;AACD,qBAAO,6BAAe,kBAAK,IAAI,aAAa,kBAAkB,IAAI;AAAA,QACnE;AAAA,MACD,CAAC;AAAA,IACF;AAEA,UAAM,WAAkD,QACrD;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,cACC,6BAAS,gBAAgB;AAAA,UACzB,OAAO,QAAQ;AACd,kBAAM,OAAO,IAAI,KAAK,MAAM;AAK5B,kBAAM,YAAY,IAAI,QAAQ,QAAQ,IAAI,SAAS,KAAK;AACxD,kBAAM,EAAE,OAAO,WAAW,OAAO,IAAI,MAAM,WAAW,OAAO;AAAA,cAC5D,GAAG;AAAA,cACH;AAAA,YACD,CAAC;AAED,uBAAO,6BAAe,kBAAK,SAAS,gBAAgB,kCAA6B;AAAA,cAChF,OAAO;AAAA,cACP,GAAG;AAAA,YACJ,CAAC;AAAA,UACF;AAAA,QACD;AAAA,MACD;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,UACC,OAAO,QAAQ;AACd,kBAAM,KAAK,MAAM,YAAY,OAAO,IAAI,KAAK,SAAS,IAAI,KAAK,EAAE;AACjE,mBAAO,SACJ,6BAAe,kBAAK,IAAI,iBAAiB,eAAe,QACxD,6BAAe,kBAAK,WAAW,aAAa,oBAAoB;AAAA,UACpE;AAAA,QACD;AAAA,MACD;AAAA,IACD,IACC,CAAC;AAUJ,UAAM,gBAAgB,KAAK,QAAQ;AACnC,UAAM,SACL,SAAS,gBACN;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,UACC,gBACC;AAAA,YACC,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,YACA,MAAM,iBAAiB,OAAO,aAAa;AAAA,UAC5C;AAAA,QACF;AAAA,MACD;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,cACC,6BAAS,qBAAqB;AAAA,UAC9B,OAAO,QAAQ;AACd,kBAAM,EAAE,OAAO,IAAI,IAAI,KAAK,MAAM;AAClC,kBAAM,OAAO,IAAI,KAAK,SAAS,QAAQ,KAAK;AAC5C,kBAAM,MAAM,MAAM,sBAAsB,OAAO,eAAe,MAAM,MAAM;AAE1E,gBAAI,IAAI,IAAI;AACX,yBAAO;AAAA,gBACN,kBAAK;AAAA,gBACL,IAAI;AAAA,gBACJ,IAAI,WAAW,uBACZ,YAAY,IAAI,OAAO,QAAQ,MAAM,qBAAqB,IAAI,MAC9D,GAAG,IAAI;AAAA,gBACV,IAAI;AAAA,cACL;AAAA,YACD;AACA,oBAAQ,IAAI,QAAQ;AAAA,cACnB,KAAK;AACJ,2BAAO;AAAA,kBACN,kBAAK;AAAA,kBACL;AAAA,kBACA,2BAA2B,IAAI;AAAA,gBAChC;AAAA,cACD,KAAK;AACJ,2BAAO;AAAA,kBACN,kBAAK;AAAA,kBACL,IAAI;AAAA,kBACJ,UAAU,IAAI,SAAS,kCAA6B,IAAI;AAAA,kBACxD,EAAE,WAAW,IAAI,UAAU;AAAA,gBAC5B;AAAA,cACD,KAAK;AACJ,2BAAO;AAAA,kBACN,kBAAK;AAAA,kBACL,IAAI;AAAA,kBACJ;AAAA,kBACA,EAAE,UAAU,IAAI,UAAU,QAAQ,IAAI,OAAO;AAAA,gBAC9C;AAAA,cACD;AACC,2BAAO;AAAA,kBACN,kBAAK;AAAA,kBACL,IAAI;AAAA,kBACJ;AAAA,kBAEA,EAAE,OAAO,IAAI,MAAM;AAAA,gBACpB;AAAA,YACF;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAAA,IACD,IACC,CAAC;AACL,UAAM,UAAU,oBAAI,IAAoB;AACxC,UAAM,QAAQ,CACbC,SACA,QACA,MACA,UACA,WACU;AACV,YAAM,MAAM,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI;AAC3C,YAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,UAAI,OAAO;AACV,cAAM,IAAI;AAAA,UACT,cAAcA,OAAM,oBAAoB,GAAG,KAAK,KAAK;AAAA,QACtD;AAAA,MACD;AACA,cAAQ,IAAI,KAAKA,OAAM;AAGvB,YAAM,QAAQ,kBAAkB,OAAO,OAAO,UAAU,SAAS,QAAQ,IAAI,CAAC;AAC9E,YAAM,QAAsB;AAAA;AAAA;AAAA,QAG3B,GAAI,QAAQ,CAAC,SAAS,OAAO,MAAMA,OAAM,CAAC,IAAI,CAAC;AAAA,QAC/C,GAAI,YAAY,CAAC,SAAS,IAAI,CAAC;AAAA,QAC/B;AAAA,QACA,GAAG;AAAA,MACJ;AACA,UAAI,SAAS,QAAQ,MAAM,GAAG,KAAK;AAAA,IACpC;AAQA,QAAI,KAAK,QAAQ,IAAI;AACpB,YAAM,aAAa,GAAG,mBAAmB;AAKzC,UAAI,KAAoB;AACxB,YAAM,SAAS,MAAqB;AACnC,YAAI,OAAO,MAAM;AAChB,cAAI;AACH,qBAAK,8BAAa,IAAI,IAAI,sBAAsB,aAAe,GAAG,MAAM;AAAA,UACzE,QAAQ;AACP,iBAAK;AAAA,UACN;AAAA,QACD;AACA,eAAO,MAAM;AAAA,MACd;AACA,YAAM,KAAK,CAAC,MAAiC,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AAC5E,UAAI;AAAA,QACH;AAAA,QACA,GAAG,YAAY;AAAA,QACf,GAAG,GAAG,OAAO,QAAQ;AAKpB,gBAAM,WAAO,mCAAqB,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,QAAQ;AACnE,iBAAO,IAAI,SAAS,OAAO,GAAG,IAAI,SAAS,GAAG;AAAA,YAC7C,SAAS,EAAE,gBAAgB,4BAA4B,iBAAiB,WAAW;AAAA,UACpF,CAAC;AAAA,QACF,CAAC;AAAA,MACF;AACA,UAAI;AAAA,QACH;AAAA,QACA;AAAA,QACA,GAAG,GAAG,YAAY;AACjB,gBAAM,OAAO,OAAO;AACpB,iBAAO,OACJ,IAAI,SAAS,MAAM;AAAA,YACnB,SAAS;AAAA,cACR,gBAAgB;AAAA,cAChB,iBAAiB;AAAA,YAClB;AAAA,UACD,CAAC,QACA;AAAA,YACA,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACH,CAAC;AAAA,MACF;AAAA,IACD;AAEA,eAAW,CAAC,QAAQ,MAAM,OAAO,KAAK,IAAK,OAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC;AACvF,eAAW,CAAC,QAAQ,MAAM,QAAQ,KAAK;AACtC,YAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG,UAAU,MAAM;AACpD,eAAW,CAAC,QAAQ,MAAM,QAAQ,KAAK,OAAQ,OAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG,QAAQ;AAC1F,eAAW,EAAE,QAAAA,SAAQ,YAAY,KAAK,IAAI,kBAAkB,GAAG;AAC9D,iBAAW,KAAK,YAAY,UAAU,CAAC;AACtC,cAAMA,SAAQ,EAAE,QAAQ,KAAK,OAAO,EAAE,MAAM,EAAE,QAAQ;AAAA,IACxD;AAAA,EACD;AAAA,EAEA,iBAAsC;AACrC,eAAO,uCAAmB,KAAK,QAAQ,YAAY,EAAE,QAAQ,KAAK,KAAK,CAAC;AAAA,EACzE;AACD;","names":["import_node_fs","import_core","module","module","module","import_core","module"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../../node_modules/tsup/assets/cjs_shims.js","../src/module.ts","../src/doctor.ts","../src/log.ts","../src/migrate.ts","../src/manifest.ts","../src/pages.ts","../src/tokens.ts","../src/ui/html.ts"],"sourcesContent":["export { AdminModule, ADMIN_VERSION, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS } from './module';\nexport { buildManifest } from './manifest';\nexport { runDoctor, collectChecks, attention } from './doctor';\nexport { adminLog, readAdminLog, DEFAULT_ACTOR, MAX_PAGE } from './log';\nexport { environmentReport, routesReport, tokensReport } from './pages';\nexport { migrationsReport, applyModuleMigrations, applyMigrationsSchema } from './migrate';\nexport {\n\trequireAdminScope,\n\tscopeFor,\n\tgrants,\n\tissueToken,\n\trevokeToken,\n\tlistTokens,\n\thashToken,\n\tSCOPES,\n} from './tokens';\nexport type {\n\tIAdminOptions,\n\tIAdminManifest,\n\tIAdminModuleEntry,\n\tIAdminCheckResult,\n\tIAdminDoctorReport,\n\tIAdminAttention,\n\tIAdminAttentionItem,\n\tIAdminLogEntry,\n\tIAdminLogPage,\n\tIAdminEnvEntry,\n\tIAdminEnvironmentReport,\n\tAdminRouteGuard,\n\tIAdminRouteEntry,\n\tIAdminRoutesReport,\n\tIAdminTokenEntry,\n\tIAdminTokensReport,\n\tAdminScope,\n\tIAdminTokenRecord,\n\tIMigrationSet,\n\tIAdminPendingMigration,\n\tIAdminMigrationModule,\n\tIAdminMigrationsReport,\n} from './types';\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import { readFileSync } from 'node:fs';\nimport type { IFonderieApp, IFonderieModule, IReadinessProblem, Middleware } from '@fonderie/core';\nimport { HTTP, normalizeMountPath, normalizeRequestPath, setApiResponse } from '@fonderie/core';\nimport { validate, validateAdminToken } from '@fonderie/core/middlewares';\n\nimport { attention, collectChecks, runDoctor } from './doctor';\nimport { adminLog, readAdminLog } from './log';\nimport { applyMigrationsSchema, applyModuleMigrations, migrationsReport } from './migrate';\nimport { buildManifest } from './manifest';\nimport { environmentReport, routesReport, tokensReport } from './pages';\nimport {\n\tissueToken,\n\tissueTokenSchema,\n\tlistTokens,\n\trequireAdminScope,\n\trevokeToken,\n\tscopeFor,\n} from './tokens';\nimport { uiHtml } from './ui/html';\nimport type { AdminScope } from './types';\nimport type { IAdminOptions } from './types';\n\nexport const DEFAULT_ADMIN_PATH = '/_admin';\nexport const DEFAULT_CHECK_TIMEOUT_MS = 10_000;\n\n// Replaced at build time (tsup env); the fallback is what tests see.\n// Baked in at build time by tsup.base — see its note on why a module reports\n// its version at all.\nexport const ADMIN_VERSION: string = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\n\n// 'https://admin.example.com/' and 'admin.example.com' name the same host, so\n// accept both. A scheme here is the likely mistake — this option sits beside\n// ones that are full URLs — and left unnormalized it matches nothing, so the\n// surface 404s everywhere and looks exactly like it was never mounted.\nfunction normalizeHostPattern(host: string): string {\n\treturn host\n\t\t.trim()\n\t\t.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, '')\n\t\t.replace(/\\/.*$/, '')\n\t\t.toLowerCase();\n}\n\n// A request addressed to a hostname this surface does not answer for gets the\n// same 404 an unmounted surface gives — indistinguishable on purpose. A 403\n// would confirm both that the surface exists and that you found the wrong door.\nfunction requireAdminHost(hosts: readonly string[]): Middleware {\n\tconst allowed = new Set(hosts.map(normalizeHostPattern).filter(Boolean));\n\treturn (ctx, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\t// Match with or without the port: a configured 'admin.example.com' should\n\t\t// not stop working behind a non-default port, and a configured\n\t\t// 'localhost:3000' should still be exact. Ports are not a boundary here.\n\t\tconst ok = allowed.has(url.host.toLowerCase()) || allowed.has(url.hostname.toLowerCase());\n\t\treturn ok ? next() : Promise.resolve(setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found'));\n\t};\n}\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\t// null = answer on any hostname.\n\treadonly hosts: string[] | null;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = normalizeMountPath(options.path ?? DEFAULT_ADMIN_PATH);\n\t\tthis.hosts = options.host === undefined ? null : [options.host].flat();\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst token = this.options.adminToken;\n\t\tif (!token) return;\n\n\t\tapp.reserve(this.path);\n\t\t// Declared at the default path so the route table reads literally; re-based when configured.\n\t\tconst at = (p: string): string => this.path + p.slice(DEFAULT_ADMIN_PATH.length);\n\n\t\tconst store = this.options.store;\n\t\t// First in every chain: a surface bound to another hostname must look\n\t\t// unmounted here, before anything reads a token or a body.\n\t\tconst hostGuard = this.hosts ? requireAdminHost(this.hosts) : null;\n\t\t// Collected once at boot: a duplicate name is refused here, not at request time.\n\t\tconst checks = collectChecks(app, this.options.checks ?? []);\n\t\tconst timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;\n\t\tconst doctor = () => runDoctor(checks, timeoutMs);\n\n\t\tconst own: Array<[string, string, Middleware]> = [\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_ATTENTION',\n\t\t\t\t\t\t'What needs attention',\n\t\t\t\t\t\tattention(app, await doctor()),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/manifest',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_MANIFEST',\n\t\t\t\t\t\t'Deployment manifest',\n\t\t\t\t\t\tbuildManifest(app, { version: this.version, log: Boolean(store), host: this.hosts }),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/doctor',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor()),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t// NOT '/_admin/config'. This reports readiness problems and which\n\t\t\t\t// DECLARED ENV VARS are set — a deployment report, not configuration.\n\t\t\t\t// It held the name anyway, which blocked @fonderie/config from ever\n\t\t\t\t// mounting beside the console (both described GET /_admin/config and\n\t\t\t\t// boot failed), and the misnomer is what made the shell probe\n\t\t\t\t// '/config' to detect the config brick — a probe that was true on\n\t\t\t\t// every deployment, built a client against admin's own report and\n\t\t\t\t// crashed the page with \"a.map is not a function\".\n\t\t\t\t'/_admin/environment',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_ENVIRONMENT',\n\t\t\t\t\t\t'Declared vs held',\n\t\t\t\t\t\tenvironmentReport(app, this.options.env ?? []),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/routes',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_ROUTES', 'Exposed routes', routesReport(app, this.name)),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/access/tokens',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_TOKENS',\n\t\t\t\t\t\t'Admin tokens',\n\t\t\t\t\t\ttokensReport(app, this.name, store ? await listTokens(store) : null),\n\t\t\t\t\t),\n\t\t\t],\n\t\t];\n\t\tif (store) {\n\t\t\town.push([\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/activity/admin-log',\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst q = new URL(ctx.request.url).searchParams;\n\t\t\t\t\tconst limit = Number(q.get('limit')) || undefined;\n\t\t\t\t\tconst before = q.get('before') ?? undefined;\n\t\t\t\t\tconst page = await readAdminLog(store, {\n\t\t\t\t\t\t...(limit ? { limit } : {}),\n\t\t\t\t\t\t...(before ? { before } : {}),\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'ADMIN_LOG', 'Admin activity', page);\n\t\t\t\t},\n\t\t\t]);\n\t\t}\n\t\t// Issuing and revoking are root-only: a scoped token can never mint one.\n\t\tconst rootOnly: Array<[string, string, Middleware[]]> = store\n\t\t\t? [\n\t\t\t\t\t[\n\t\t\t\t\t\t'POST',\n\t\t\t\t\t\t'/_admin/access/tokens',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tvalidate(issueTokenSchema),\n\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\tconst body = ctx.meta['body'] as {\n\t\t\t\t\t\t\t\t\tname: string;\n\t\t\t\t\t\t\t\t\tscopes: AdminScope[];\n\t\t\t\t\t\t\t\t\texpiresInDays?: number;\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\tconst createdBy = ctx.request.headers.get('x-actor') || 'admin-token';\n\t\t\t\t\t\t\t\tconst { token: plaintext, record } = await issueToken(store, {\n\t\t\t\t\t\t\t\t\t...body,\n\t\t\t\t\t\t\t\t\tcreatedBy,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t// The plaintext is returned once and never stored.\n\t\t\t\t\t\t\t\treturn setApiResponse(HTTP.CREATED, 'TOKEN_ISSUED', 'Token issued — shown once', {\n\t\t\t\t\t\t\t\t\ttoken: plaintext,\n\t\t\t\t\t\t\t\t\t...record,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t\t[\n\t\t\t\t\t\t'DELETE',\n\t\t\t\t\t\t'/_admin/access/tokens/:id',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\tconst ok = await revokeToken(store, ctx.meta.params?.['id'] ?? '');\n\t\t\t\t\t\t\t\treturn ok\n\t\t\t\t\t\t\t\t\t? setApiResponse(HTTP.OK, 'TOKEN_REVOKED', 'Token revoked')\n\t\t\t\t\t\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such live token');\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t]\n\t\t\t: [];\n\t\t// Reporting AND applying pending migrations. Registered only when the app\n\t\t// hands over its migration sequence — this surface must never infer that\n\t\t// order (see IAdminOptions.migrations), so without it there is nothing\n\t\t// honest to say and the routes do not exist.\n\t\t//\n\t\t// Scope is derived, not annotated: scopeFor() gives GET ⇒ read, POST ⇒\n\t\t// write. Deliberately NOT 'root' like the token routes — those are root\n\t\t// because a scoped token minting a token is privilege escalation.\n\t\t// Applying migrations that provably delete nothing is not.\n\t\tconst migrationSets = this.options.migrations;\n\t\tconst scoped: Array<[string, string, Middleware[]]> =\n\t\t\tstore && migrationSets\n\t\t\t\t? [\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'GET',\n\t\t\t\t\t\t\t'/_admin/migrations',\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\tasync () =>\n\t\t\t\t\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t\t\t\t\t'MIGRATIONS',\n\t\t\t\t\t\t\t\t\t\t'Pending migrations by module',\n\t\t\t\t\t\t\t\t\t\tawait migrationsReport(store, migrationSets),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t],\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'POST',\n\t\t\t\t\t\t\t'/_admin/migrations/:module/apply',\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\tvalidate(applyMigrationsSchema),\n\t\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\t\tconst { expect } = ctx.meta['body'] as { expect: string[] };\n\t\t\t\t\t\t\t\t\tconst name = ctx.meta.params?.['module'] ?? '';\n\t\t\t\t\t\t\t\t\tconst out = await applyModuleMigrations(store, migrationSets, name, expect);\n\n\t\t\t\t\t\t\t\t\tif (out.ok) {\n\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t\t\t\t\t\tout.reason,\n\t\t\t\t\t\t\t\t\t\t\tout.reason === 'MIGRATIONS_APPLIED'\n\t\t\t\t\t\t\t\t\t\t\t\t? `Applied. ${out.module.pending.length} still pending in ${name}.`\n\t\t\t\t\t\t\t\t\t\t\t\t: `${name} is already up to date`,\n\t\t\t\t\t\t\t\t\t\t\tout.module,\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tswitch (out.reason) {\n\t\t\t\t\t\t\t\t\t\tcase 'NOT_FOUND':\n\t\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\t\tHTTP.NOT_FOUND,\n\t\t\t\t\t\t\t\t\t\t\t\t'NOT_FOUND',\n\t\t\t\t\t\t\t\t\t\t\t\t`No migration set named \"${name}\"`,\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\tcase 'MIGRATIONS_OUT_OF_ORDER':\n\t\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t\t\t\t\t\tout.reason,\n\t\t\t\t\t\t\t\t\t\t\t\t`Apply \"${out.blockedBy}\" first — it runs before \"${name}\" and is behind.`,\n\t\t\t\t\t\t\t\t\t\t\t\t{ blockedBy: out.blockedBy },\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\tcase 'MIGRATIONS_CHANGED':\n\t\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t\t\t\t\t\tout.reason,\n\t\t\t\t\t\t\t\t\t\t\t\t'What is pending changed since you looked. Refresh and read it again.',\n\t\t\t\t\t\t\t\t\t\t\t\t{ expected: out.expected, actual: out.actual },\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t\t\t\t\t\t\tout.reason,\n\t\t\t\t\t\t\t\t\t\t\t\t'A pending migration deletes data. No down-migration brings it back — ' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'apply it through CI or `npm run migrate`, not from here.',\n\t\t\t\t\t\t\t\t\t\t\t\t{ files: out.files },\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t],\n\t\t\t\t\t]\n\t\t\t\t: [];\n\t\tconst mounted = new Map<string, string>();\n\t\tconst mount = (\n\t\t\tmodule: string,\n\t\t\tmethod: string,\n\t\t\tpath: string,\n\t\t\thandlers: Middleware[],\n\t\t\tneeded?: AdminScope | 'root',\n\t\t): void => {\n\t\t\tconst key = `${method.toUpperCase()} ${path}`;\n\t\t\tconst prior = mounted.get(key);\n\t\t\tif (prior) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[fonderie] ${module} cannot describe ${key}: ${prior} already describes it`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tmounted.set(key, module);\n\t\t\t// The scope comes from the route itself. The log sits before the guard:\n\t\t\t// a refused request is a row too.\n\t\t\tconst guard = requireAdminScope(token, store, needed ?? scopeFor(method, path));\n\t\t\tconst chain: Middleware[] = [\n\t\t\t\t// Logged first so a wrong-host attempt still leaves a row: the\n\t\t\t\t// caller learns nothing from a 404, the operator learns something.\n\t\t\t\t...(store ? [adminLog(store, path, module)] : []),\n\t\t\t\t...(hostGuard ? [hostGuard] : []),\n\t\t\t\tguard,\n\t\t\t\t...handlers,\n\t\t\t];\n\t\t\tapp.addRoute(method, path, ...chain);\n\t\t};\n\n\t\t// The served dashboard. Deliberately OUTSIDE the guard: a browser\n\t\t// navigating to a page cannot send an Authorization header, and neither\n\t\t// file carries data — the HTML is a shell and the script is the same\n\t\t// bundle npm serves. The page asks for a token and sends it itself, so\n\t\t// every request that reads anything is guarded and logged as usual.\n\t\t// These two are not logged: they carry no token to attribute.\n\t\tif (this.options.ui) {\n\t\t\tconst scriptPath = at('/_admin/ui/app.js');\n\t\t\t// Read on first request, not at boot: the bundle sits beside the BUILT\n\t\t\t// module (tsup names its IIFE output <entry>.global.js), so running from\n\t\t\t// source — tests, a linked checkout — has none, and that must not stop\n\t\t\t// an app from booting over a dashboard it may never open.\n\t\t\tlet js: string | null = null;\n\t\t\tconst loadJs = (): string | null => {\n\t\t\t\tif (js === null) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tjs = readFileSync(new URL('./ui/app.global.js', import.meta.url), 'utf8');\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tjs = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn js || null;\n\t\t\t};\n\t\t\tconst ui = (h: Middleware): Middleware[] => (hostGuard ? [hostGuard, h] : [h]);\n\t\t\tapp.addRoute(\n\t\t\t\t'GET',\n\t\t\t\tat('/_admin/ui'),\n\t\t\t\t...ui(async (ctx) => {\n\t\t\t\t\t// From the request, not from config: this is the only place that\n\t\t\t\t\t// knows basePath, a moved path AND a trailing slash at once. The\n\t\t\t\t\t// router's own normalizer, so the href and the routing cannot\n\t\t\t\t\t// disagree about what this path is.\n\t\t\t\t\tconst here = normalizeRequestPath(new URL(ctx.request.url).pathname);\n\t\t\t\t\treturn new Response(uiHtml(`${here}/app.js`), {\n\t\t\t\t\t\theaders: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },\n\t\t\t\t\t});\n\t\t\t\t}),\n\t\t\t);\n\t\t\tapp.addRoute(\n\t\t\t\t'GET',\n\t\t\t\tscriptPath,\n\t\t\t\t...ui(async () => {\n\t\t\t\t\tconst body = loadJs();\n\t\t\t\t\treturn body\n\t\t\t\t\t\t? new Response(body, {\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t'content-type': 'text/javascript; charset=utf-8',\n\t\t\t\t\t\t\t\t\t'cache-control': 'public, max-age=300',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: setApiResponse(\n\t\t\t\t\t\t\t\tHTTP.SERVICE_UNAVAILABLE,\n\t\t\t\t\t\t\t\t'UI_NOT_BUILT',\n\t\t\t\t\t\t\t\t'This copy of @fonderie/admin has no built dashboard (dist/ui). Install the published package, or run its build.',\n\t\t\t\t\t\t\t);\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tfor (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);\n\t\tfor (const [method, path, handlers] of rootOnly)\n\t\t\tmount(this.name, method, at(path), handlers, 'root');\n\t\tfor (const [method, path, handlers] of scoped) mount(this.name, method, at(path), handlers);\n\t\tfor (const { module, description } of app.adminDescriptions()) {\n\t\t\tfor (const r of description.routes ?? [])\n\t\t\t\tmount(module, r.method, this.path + r.path, r.handlers);\n\t\t}\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IAdminCheck, IAdminCheckReport, IFonderieApp } from '@fonderie/core';\n\nimport type { IAdminAttention, IAdminCheckResult, IAdminDoctorReport } from './types';\n\nexport interface INamedCheck extends IAdminCheck {\n\tmodule: string;\n}\n\n// Module checks first (sorted by module), then the app's own. A name used\n// twice would make a report ambiguous, so it is refused up front.\nexport function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[] {\n\tconst seen = new Map<string, string>();\n\tconst out: INamedCheck[] = [];\n\tconst add = (module: string, check: IAdminCheck): void => {\n\t\tconst prior = seen.get(check.name);\n\t\tif (prior)\n\t\t\tthrow new Error(\n\t\t\t\t`[fonderie] ${module} cannot offer check \"${check.name}\": ${prior} already does`,\n\t\t\t);\n\t\tseen.set(check.name, module);\n\t\tout.push({ ...check, module });\n\t};\n\tfor (const { module, description } of app.adminDescriptions()) {\n\t\tfor (const c of description.checks ?? []) add(module, c);\n\t}\n\tfor (const c of own) add('application', c);\n\treturn out;\n}\n\nasync function runOne(check: INamedCheck, timeoutMs: number): Promise<IAdminCheckResult> {\n\tconst started = Date.now();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<IAdminCheckReport>((resolve) => {\n\t\ttimer = setTimeout(\n\t\t\t() => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet report: IAdminCheckReport;\n\ttry {\n\t\treport = await Promise.race([check.run(), timeout]);\n\t} catch (err) {\n\t\treport = {\n\t\t\tok: false,\n\t\t\tfindings: [`check threw: ${err instanceof Error ? err.message : String(err)}`],\n\t\t};\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n\treturn { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };\n}\n\nexport async function runDoctor(\n\tchecks: INamedCheck[],\n\ttimeoutMs: number,\n): Promise<IAdminDoctorReport> {\n\tconst results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));\n\treturn { generatedAt: new Date().toISOString(), ok: results.every((r) => r.ok), checks: results };\n}\n\n// What needs the operator today: readiness problems as reported, failed\n// checks as errors, findings on a passing check as advice. Empty = green.\nexport function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention {\n\tconst items: IAdminAttention['items'] = [];\n\tfor (const p of app.checkProductionReadiness().problems) {\n\t\titems.push({\n\t\t\tsource: p.module,\n\t\t\tseverity: p.severity === 'error' ? 'error' : 'advice',\n\t\t\tmessage: p.message,\n\t\t});\n\t}\n\tfor (const c of doctor.checks) {\n\t\tif (c.skipped) continue;\n\t\tconst severity = c.ok ? 'advice' : 'error';\n\t\tif (!c.ok && c.findings.length === 0)\n\t\t\titems.push({ source: c.name, severity, message: 'failed' });\n\t\tfor (const f of c.findings) items.push({ source: c.name, severity, message: f });\n\t}\n\treturn { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === 'error'), items };\n}\n","import { normalizeRequestPath } from '@fonderie/core';\nimport type { IFonderieContext, Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAdminLogEntry, IAdminLogPage } from './types';\n\nexport const DEFAULT_ACTOR = 'admin-token';\n\n// Sits BEFORE the token guard, so a refused request is a row too. A failed\n// write must not fail the request it describes.\nexport function adminLog(store: IStoreAdapter, route: string, module: string): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst started = Date.now();\n\t\tconst res = await next();\n\t\tconst row = {\n\t\t\t// A scoped token names itself; X-Actor refines it; the root token is 'admin-token'.\n\t\t\tactor:\n\t\t\t\tctx.request.headers.get('x-actor') ||\n\t\t\t\t(typeof ctx.meta['adminTokenName'] === 'string'\n\t\t\t\t\t? `token:${ctx.meta['adminTokenName']}`\n\t\t\t\t\t: DEFAULT_ACTOR),\n\t\t\tmethod: ctx.request.method,\n\t\t\t// Normalized, so '/x' and '/x/' are one row for one route — the same\n\t\t\t// path the router matched and `route` below records the pattern of.\n\t\t\tpath: normalizeRequestPath(new URL(ctx.request.url).pathname),\n\t\t\troute,\n\t\t\tmodule,\n\t\t\tstatus: res.status,\n\t\t\tdurationMs: Date.now() - started,\n\t\t\trequestId: typeof ctx.meta['requestId'] === 'string' ? ctx.meta['requestId'] : null,\n\t\t\tclientIp: ctx.meta.clientIp ?? null,\n\t\t};\n\t\ttry {\n\t\t\tawait store.query(\n\t\t\t\t`INSERT INTO fonderie_admin_log\n\t\t\t\t (actor, method, path, route, module, status, duration_ms, request_id, client_ip)\n\t\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,\n\t\t\t\t[\n\t\t\t\t\trow.actor,\n\t\t\t\t\trow.method,\n\t\t\t\t\trow.path,\n\t\t\t\t\trow.route,\n\t\t\t\t\trow.module,\n\t\t\t\t\trow.status,\n\t\t\t\t\trow.durationMs,\n\t\t\t\t\trow.requestId,\n\t\t\t\t\trow.clientIp,\n\t\t\t\t],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error(\n\t\t\t\t'[admin] could not write the admin log:',\n\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t);\n\t\t}\n\t\treturn res;\n\t};\n}\n\nexport const MAX_PAGE = 200;\n\n// Newest first, keyset on (at, id). `before` is the last entry's cursor.\nexport async function readAdminLog(\n\tstore: IStoreAdapter,\n\topts: { limit?: number; before?: string } = {},\n): Promise<IAdminLogPage> {\n\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);\n\tconst cursor = opts.before ? decodeCursor(opts.before) : null;\n\tconst rows = await store.query<IAdminLogEntry>(\n\t\t`SELECT id, at, actor, method, path, route, module, status,\n\t\t duration_ms AS \"durationMs\", request_id AS \"requestId\", client_ip AS \"clientIp\"\n\t\t FROM fonderie_admin_log\n\t\t WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))\n\t\t ORDER BY at DESC, id DESC\n\t\t LIMIT $3`,\n\t\t[cursor?.at ?? null, cursor?.id ?? null, limit + 1],\n\t);\n\tconst page = rows.slice(0, limit);\n\tconst last = page[page.length - 1];\n\treturn {\n\t\tentries: page,\n\t\tnext: rows.length > limit && last ? encodeCursor(last) : null,\n\t};\n}\n\nfunction encodeCursor(e: IAdminLogEntry): string {\n\treturn Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, 'utf8').toString('base64url');\n}\n\nfunction decodeCursor(c: string): { at: string; id: string } | null {\n\tconst [at, id] = Buffer.from(c, 'base64url').toString('utf8').split('|');\n\treturn at && id ? { at, id } : null;\n}\n","import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { IRequestSchema } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport { classifyMigration, InternalMigrationRunner } from '@fonderie/store';\n\nimport type { IAdminMigrationModule, IAdminMigrationsReport, IMigrationSet } from './types';\n\n/**\n * `expect` is the exact pending list the operator was shown, in order.\n *\n * It is not ceremony. Between rendering the page and clicking the button a\n * deploy can change what is pending — and the thing that changed is precisely\n * the thing nobody reviewed. Sending it back makes the server refuse rather\n * than apply a set the operator never saw. Same shape as versionedWrite's\n * if_version, and it also gives this POST the body schema audit:validation\n * requires.\n */\nexport const applyMigrationsSchema: IRequestSchema = {\n\tsafeParse(input: unknown) {\n\t\tconst fail = (path: string, message: string) => ({\n\t\t\tsuccess: false as const,\n\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t});\n\t\tconst b = (input ?? {}) as Record<string, unknown>;\n\t\tconst expect = b['expect'];\n\t\tif (!Array.isArray(expect) || !expect.every((f) => typeof f === 'string'))\n\t\t\treturn fail('expect', 'expect must be the list of pending filenames you were shown');\n\t\t// An empty list is legitimate: it asserts \"I was told there was nothing\n\t\t// to do\", and the handler answers UP_TO_DATE or CHANGED accordingly.\n\t\treturn { success: true as const, data: { expect: expect as string[] } };\n\t},\n};\n\n// Applying is the APP's order, never ours. The sets arrive already interleaved\n// — brick dirs and the app's own, in the sequence its applier runs them — from\n// the one constant both the applier and this reporter read. See the reference app's\n// db/migrations/steps.ts: \"auth owns fonderie_users, which the app migration\n// extends, and media's assets reference storage's blobs.\"\n\n// Has this database EVER been migrated? pending() cannot answer it: it catches\n// its own read failure and returns every file, so a virgin database, a database\n// belonging to another app, and an unreachable one are the same answer. Asked\n// directly, because \"61 pending\" means something completely different depending\n// on which of those it is.\nasync function hasMigrationHistory(store: IStoreAdapter): Promise<boolean> {\n\ttry {\n\t\tconst rows = await store.query<{ n: number }>(\n\t\t\t'SELECT count(*)::int AS n FROM fonderie_migrations',\n\t\t);\n\t\treturn (rows[0]?.n ?? 0) > 0;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction classifyPending(dir: string, files: readonly string[]) {\n\treturn files.map((file) => {\n\t\tconst { impact, destructive } = classifyMigration(readFileSync(join(dir, file), 'utf8'));\n\t\treturn { file, impact, destructive };\n\t});\n}\n\n/**\n * What every module is waiting on, with each pending file's impact.\n *\n * `blockedBy` names the first EARLIER module that is behind. Order is\n * load-bearing and declared by the app, so a module cannot be applied while\n * something it may depend on is unapplied — the panel offers one button at a\n * time and the operator walks the list in the declared order.\n */\nexport async function migrationsReport(\n\tstore: IStoreAdapter,\n\tsets: ReadonlyArray<IMigrationSet>,\n): Promise<IAdminMigrationsReport> {\n\tconst everApplied = await hasMigrationHistory(store);\n\n\tconst modules: IAdminMigrationModule[] = [];\n\tlet firstBehind: string | null = null;\n\n\tfor (const [name, dir] of sets) {\n\t\tconst pending = await new InternalMigrationRunner(store, dir).pending();\n\t\tconst files = classifyPending(dir, pending);\n\t\tmodules.push({\n\t\t\tname,\n\t\t\tpending: files,\n\t\t\t// Only a module BEFORE this one blocks it. The first module with\n\t\t\t// pending work blocks every module after it, and nothing blocks it.\n\t\t\tblockedBy: firstBehind,\n\t\t\t// A file that deletes data on a database with nothing in it yet is not\n\t\t\t// destroying anything. Same rule the CLI's --check applies.\n\t\t\tappliable: files.length > 0 && firstBehind === null && (!everApplied || files.every((f) => f.impact === 'additive')),\n\t\t});\n\t\tif (files.length > 0 && firstBehind === null) firstBehind = name;\n\t}\n\n\treturn { everApplied, modules };\n}\n\nexport type ApplyOutcome =\n\t| { ok: true; reason: 'MIGRATIONS_APPLIED' | 'MIGRATIONS_UP_TO_DATE'; module: IAdminMigrationModule }\n\t| { ok: false; reason: 'NOT_FOUND' }\n\t| { ok: false; reason: 'MIGRATIONS_OUT_OF_ORDER'; blockedBy: string }\n\t| { ok: false; reason: 'MIGRATIONS_CHANGED'; expected: string[]; actual: string[] }\n\t| { ok: false; reason: 'MIGRATION_DESTRUCTIVE'; files: IAdminMigrationModule['pending'] };\n\n/**\n * Apply one module's pending migrations — all of them, or none.\n *\n * There is no partial apply, deliberately: files run in lexicographic order, so\n * an additive migration sorting AFTER a destructive one may depend on it.\n * Cherry-picking around a DROP would apply 901 against a schema that never saw\n * 900. Refusing the whole module at the first destructive file is the only safe\n * reading, and happens to be the only one MigrationRunner.run() supports.\n *\n * `expect` is the file list the operator was looking at. Between the render and\n * the click a deploy can change what is pending; without this the button would\n * apply something nobody reviewed.\n */\nexport async function applyModuleMigrations(\n\tstore: IStoreAdapter,\n\tsets: ReadonlyArray<IMigrationSet>,\n\tmodule: string,\n\texpect: readonly string[],\n): Promise<ApplyOutcome> {\n\tconst set = sets.find(([name]) => name === module);\n\tif (!set) return { ok: false, reason: 'NOT_FOUND' };\n\n\t// Recomputed here rather than trusted from a previous call: this is the\n\t// check that decides whether to write.\n\tconst before = await migrationsReport(store, sets);\n\tconst target = before.modules.find((m) => m.name === module);\n\tif (!target) return { ok: false, reason: 'NOT_FOUND' };\n\n\tif (target.blockedBy) {\n\t\treturn { ok: false, reason: 'MIGRATIONS_OUT_OF_ORDER', blockedBy: target.blockedBy };\n\t}\n\n\tconst actual = target.pending.map((p) => p.file);\n\tconst same = actual.length === expect.length && actual.every((f, i) => f === expect[i]);\n\tif (!same) {\n\t\treturn { ok: false, reason: 'MIGRATIONS_CHANGED', expected: [...expect], actual };\n\t}\n\n\tif (actual.length === 0) {\n\t\treturn { ok: true, reason: 'MIGRATIONS_UP_TO_DATE', module: target };\n\t}\n\n\tconst destructive = target.pending.filter((p) => p.impact === 'destructive');\n\tif (destructive.length > 0 && before.everApplied) {\n\t\treturn { ok: false, reason: 'MIGRATION_DESTRUCTIVE', files: destructive };\n\t}\n\n\tawait new InternalMigrationRunner(store, set[1]).run();\n\n\t// Re-READ, never assume. run() returns void, and a request that dies\n\t// mid-apply still committed every file it finished — each one is its own\n\t// transaction. What the database says now is the only honest answer.\n\tconst after = await migrationsReport(store, sets);\n\tconst now = after.modules.find((m) => m.name === module);\n\treturn {\n\t\tok: true,\n\t\treason: 'MIGRATIONS_APPLIED',\n\t\tmodule: now ?? { name: module, pending: [], blockedBy: null, appliable: false },\n\t};\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminManifest, IAdminModuleEntry } from './types';\n\nexport function buildManifest(\n\tapp: IFonderieApp,\n\tadmin: { version: string; log: boolean; host: string[] | null },\n): IAdminManifest {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems) {\n\t\tconst list = byModule.get(p.module) ?? [];\n\t\tlist.push(p);\n\t\tbyModule.set(p.module, list);\n\t}\n\tconst describing = new Set(app.adminDescriptions().map((d) => d.module));\n\tconst modules: IAdminModuleEntry[] = report.modules.map(({ name, version }) => {\n\t\tconst problems = byModule.get(name) ?? [];\n\t\treturn {\n\t\t\tname,\n\t\t\tversion: version ?? null,\n\t\t\treadiness: { ok: !problems.some((p) => p.severity === 'error'), problems },\n\t\t\tdescribesAdmin: describing.has(name),\n\t\t};\n\t});\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tenv: report.env,\n\t\tadmin,\n\t\tmodules,\n\t\treadiness: report.readiness,\n\t\troutes: app.routes(),\n\t};\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type {\n\tIAdminEnvironmentReport,\n\tIAdminRoutesReport,\n\tIAdminTokenRecord,\n\tIAdminTokensReport,\n} from './types';\n\nexport function environmentReport(app: IFonderieApp, env: string[]): IAdminEnvironmentReport {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems)\n\t\tbyModule.set(p.module, [...(byModule.get(p.module) ?? []), p]);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\treadiness: report.readiness,\n\t\tmodules: report.modules.map(({ name }) => ({ name, problems: byModule.get(name) ?? [] })),\n\t\tenv: env.map((name) => ({\n\t\t\tname,\n\t\t\tset: process.env[name] !== undefined && process.env[name] !== '',\n\t\t})),\n\t};\n}\n\nconst PROBES = new Set(['/healthz', '/readyz', '/metrics']);\n\nexport function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport {\n\treturn {\n\t\tgeneratedAt: new Date().toISOString(),\n\t\troutes: app.routes().map((r) => ({\n\t\t\t...r,\n\t\t\tguard:\n\t\t\t\tr.module === adminModule\n\t\t\t\t\t? 'admin'\n\t\t\t\t\t: PROBES.has(r.path) && r.module === '@fonderie/core'\n\t\t\t\t\t\t? 'probe'\n\t\t\t\t\t\t: 'app',\n\t\t})),\n\t};\n}\n\n// A brick's own adminToken shows as a readiness problem only when weak, so\n// \"set at all\" has to be asked; a described module that also registers a\n// legacy route under /admin, /plans or /billing/wallet/grant has one.\nexport function tokensReport(\n\tapp: IFonderieApp,\n\tadminModule: string,\n\tissued: IAdminTokenRecord[] | null,\n): IAdminTokensReport {\n\tconst report = app.securityReport();\n\tconst adminProblems = report.readiness.problems.filter((p) => p.module === adminModule);\n\tconst legacyOwners = new Set(\n\t\tapp\n\t\t\t.routes()\n\t\t\t.filter((r) => r.module && r.module !== adminModule && isLegacyAdminPath(r.method, r.path))\n\t\t\t.map((r) => r.module as string),\n\t);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tadmin: { ok: !adminProblems.some((p) => p.severity === 'error'), problems: adminProblems },\n\t\tlegacy: report.modules\n\t\t\t.map(({ name }) => ({ module: name, set: legacyOwners.has(name) }))\n\t\t\t.filter((e) => e.set),\n\t\tissued,\n\t};\n}\n\nfunction isLegacyAdminPath(method: string, path: string): boolean {\n\tif (/\\/admin\\/(config|secrets|templates)(\\/|$)/.test(path)) return true;\n\tif (/\\/plans(\\/|$)/.test(path) && method !== 'GET') return true;\n\treturn /\\/billing\\/wallet\\/grant$/.test(path);\n}\n","import { createHash, randomBytes } from 'node:crypto';\nimport type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { HTTP, constantTimeEqual, setApiResponse } from '@fonderie/core';\nimport type { IRequestSchema } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { AdminScope, IAdminTokenRecord } from './types';\n\nexport const SCOPES: readonly AdminScope[] = ['read', 'write', 'secrets'];\n\n// Derived from the route, never annotated: the plaintext secret read is the\n// one thing that needs `secrets`; any other mutation needs `write`.\nexport function scopeFor(method: string, path: string): AdminScope {\n\tif (/\\/secrets(\\/|$)/.test(path)) return 'secrets';\n\treturn method.toUpperCase() === 'GET' ? 'read' : 'write';\n}\n\n// `write` implies `read`; `secrets` implies both.\nexport function grants(held: readonly AdminScope[], needed: AdminScope): boolean {\n\tif (held.includes('secrets')) return true;\n\tif (needed === 'secrets') return false;\n\tif (held.includes('write')) return true;\n\treturn needed === 'read' && held.includes('read');\n}\n\nexport const hashToken = (token: string): string =>\n\tcreateHash('sha256').update(token).digest('hex');\nconst mintToken = (): string => `fad_${randomBytes(24).toString('base64url')}`;\n\nconst COLUMNS = `id, name, scopes, created_by AS \"createdBy\", created_at AS \"createdAt\",\n expires_at AS \"expiresAt\", revoked_at AS \"revokedAt\", last_used_at AS \"lastUsedAt\"`;\n\nexport async function listTokens(store: IStoreAdapter): Promise<IAdminTokenRecord[]> {\n\treturn store.query<IAdminTokenRecord>(\n\t\t`SELECT ${COLUMNS} FROM fonderie_admin_tokens ORDER BY created_at DESC`,\n\t);\n}\n\nexport async function issueToken(\n\tstore: IStoreAdapter,\n\tinput: { name: string; scopes: AdminScope[]; expiresInDays?: number; createdBy: string },\n): Promise<{ token: string; record: IAdminTokenRecord }> {\n\tconst token = mintToken();\n\tconst expiresAt = input.expiresInDays\n\t\t? new Date(Date.now() + input.expiresInDays * 86_400_000)\n\t\t: null;\n\tconst [record] = await store.query<IAdminTokenRecord>(\n\t\t`INSERT INTO fonderie_admin_tokens (name, token_hash, scopes, created_by, expires_at)\n\t\t VALUES ($1, $2, $3, $4, $5) RETURNING ${COLUMNS}`,\n\t\t[input.name, hashToken(token), input.scopes, input.createdBy, expiresAt],\n\t);\n\tif (!record) throw new Error('[admin] token insert returned no row');\n\treturn { token, record };\n}\n\nexport async function revokeToken(store: IStoreAdapter, id: string): Promise<boolean> {\n\tconst rows = await store.query<{ id: string }>(\n\t\t`UPDATE fonderie_admin_tokens SET revoked_at = now() WHERE id = $1 AND revoked_at IS NULL RETURNING id`,\n\t\t[id],\n\t);\n\treturn rows.length > 0;\n}\n\nasync function findLive(store: IStoreAdapter, token: string): Promise<IAdminTokenRecord | null> {\n\tconst [row] = await store.query<IAdminTokenRecord>(\n\t\t`SELECT ${COLUMNS} FROM fonderie_admin_tokens\n\t\t WHERE token_hash = $1 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())`,\n\t\t[hashToken(token)],\n\t);\n\treturn row ?? null;\n}\n\nconst bearer = (ctx: IFonderieContext): string => {\n\tconst h = ctx.request.headers.get('authorization') ?? '';\n\treturn h.startsWith('Bearer ') ? h.slice(7) : '';\n};\nconst UNAUTHORIZED = () =>\n\tsetApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token');\n\n// The bootstrap token is the root: every scope, and the only credential that\n// may issue or revoke tokens. Issued tokens carry their scopes; a valid token\n// short of the route's scope is 403, an unknown, revoked or expired one is\n// the same 401 as a missing one. The token's name becomes the log's actor.\nexport function requireAdminScope(\n\tbootstrap: string,\n\tstore: IStoreAdapter | undefined,\n\tneeded: AdminScope | 'root',\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst token = bearer(ctx);\n\t\tif (!token) return UNAUTHORIZED();\n\t\tif (constantTimeEqual(token, bootstrap)) return next();\n\t\tif (needed === 'root' || !store) return UNAUTHORIZED();\n\t\tconst record = await findLive(store, token);\n\t\tif (!record) return UNAUTHORIZED();\n\t\tif (!grants(record.scopes, needed)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', `This token lacks the '${needed}' scope`);\n\t\t}\n\t\tctx.meta['adminTokenName'] = record.name;\n\t\tvoid store\n\t\t\t.query(`UPDATE fonderie_admin_tokens SET last_used_at = now() WHERE id = $1`, [record.id])\n\t\t\t.catch(() => undefined);\n\t\treturn next();\n\t};\n}\n\n// { name: 1–100 chars, scopes: non-empty subset of SCOPES, expiresInDays?: 1–3650 }\nexport const issueTokenSchema: IRequestSchema = {\n\tsafeParse(input: unknown) {\n\t\tconst fail = (path: string, message: string) => ({\n\t\t\tsuccess: false as const,\n\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t});\n\t\tconst b = (input ?? {}) as Record<string, unknown>;\n\t\tconst name = typeof b['name'] === 'string' ? b['name'].trim() : '';\n\t\tif (!name || name.length > 100) return fail('name', 'name is required (1–100 chars)');\n\t\tconst scopes = b['scopes'];\n\t\tif (\n\t\t\t!Array.isArray(scopes) ||\n\t\t\tscopes.length === 0 ||\n\t\t\t!scopes.every((s) => SCOPES.includes(s as AdminScope))\n\t\t)\n\t\t\treturn fail('scopes', `scopes must be a non-empty list of ${SCOPES.join(', ')}`);\n\t\tconst days = b['expiresInDays'];\n\t\tif (\n\t\t\tdays !== undefined &&\n\t\t\t(!Number.isInteger(days) || (days as number) < 1 || (days as number) > 3650)\n\t\t)\n\t\t\treturn fail('expiresInDays', 'expiresInDays must be an integer between 1 and 3650');\n\t\treturn {\n\t\t\tsuccess: true as const,\n\t\t\tdata: {\n\t\t\t\tname,\n\t\t\t\tscopes: [...new Set(scopes as AdminScope[])],\n\t\t\t\t...(days !== undefined ? { expiresInDays: days as number } : {}),\n\t\t\t},\n\t\t};\n\t},\n};\n","// The served shell's document. Inlined as a string so it ships in dist with no\n// copy step. The script path is passed in, not relative: the surface can be\n// mounted under any basePath or path, and a trailing slash must not break it.\n//\n// The `:root` block is the organisation's design tokens (organization/ui/base.css),\n// carried here rather than imported: this file is a string in dist, the admin\n// package has no CSS pipeline, and an operator console must not depend on a\n// stylesheet it might fail to fetch. Screens read these through\n// `var(--fonderie-*)` in their inline styles, so the palette lives in exactly\n// one place and both light and dark follow from it.\n//\n// Namespaced `--fonderie-*` deliberately. The dashboard can be mounted inside a\n// consumer's own page, and bare `--color-text` would collide with theirs —\n// silently, and only in their app.\n//\n// NO WEBFONT. The tokens name Inter first and fall through to the system stack,\n// but nothing is fetched: an admin console should not announce its existence to\n// a third party, and it has to work on an air-gapped deploy.\n// The dark palette, applied from two places: when the OS asks for it, and when\n// the operator asks for it explicitly. Declared once so the two cannot drift.\nconst DARK = `\n--fonderie-accent:#00d294;\n--fonderie-accent-strong:#00e6a2;\n--fonderie-warning:#f5a623;\n--fonderie-danger:#f33;\n--fonderie-bg:#0a0a0a;\n--fonderie-surface:#111;\n--fonderie-surface-alt:#1a1a1a;\n--fonderie-border:#27272a;\n--fonderie-border-light:#1c1c1e;\n--fonderie-text:#ededed;\n--fonderie-text-muted:#a1a1aa;\n--fonderie-topbar:#0a0a0a;\n--fonderie-shadow-card:0 2px 3px 0 rgba(0,0,0,.3);\n`;\n\nconst TOKENS = `\n:root{\n--fonderie-accent:#00d294;\n--fonderie-accent-strong:#009767;\n--fonderie-warning:#f5a623;\n--fonderie-danger:#e00;\n--fonderie-bg:#fafafa;\n--fonderie-surface:#fff;\n--fonderie-surface-alt:#fafafa;\n--fonderie-border:#e0e0e0;\n--fonderie-border-light:#f5f5f5;\n--fonderie-text:#171717;\n--fonderie-text-muted:#5c5c5c;\n--fonderie-topbar:#000;\n--fonderie-font:Inter,\"Inter Fallback\",-apple-system,BlinkMacSystemFont,\"Helvetica Neue\",system-ui,sans-serif;\n--fonderie-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;\n--fonderie-tracking-display:-0.05em;\n--fonderie-tracking-body:-0.02em;\n--fonderie-radius:4px;\n--fonderie-radius-lg:8px;\n--fonderie-shadow-card:0 2px 3px 0 rgba(0,0,0,.05);\n}\n/* OS says dark, and the operator has not overridden it. */\n@media (prefers-color-scheme:dark){:root:not([data-theme=\"light\"]){${DARK}}}\n/* The operator chose dark, whatever the OS says. Without this rule \"Dark\" on a\n light machine did nothing — the console could only ever follow the system. */\n:root[data-theme=\"dark\"]{${DARK}}\nhtml,body{margin:0;padding:0}\nbody{\nbackground:var(--fonderie-bg);\ncolor:var(--fonderie-text);\nfont-family:var(--fonderie-font);\nfont-size:14px;\nline-height:1.7;\nletter-spacing:var(--fonderie-tracking-body);\n-webkit-font-smoothing:antialiased;\n-moz-osx-font-smoothing:grayscale;\n}\nh1,h2,h3{letter-spacing:var(--fonderie-tracking-display);font-weight:600;line-height:1.25}\ncode,pre{font-family:var(--fonderie-mono)}\n/* Focus must stay visible on a console that performs irreversible actions. */\n:focus-visible{outline:2px solid var(--fonderie-accent);outline-offset:2px}\n\n/* Theme switcher — copied from the organisation UI (theme.css .theme-switch),\n with --color-* mapped to --fonderie-*. It lives here rather than in inline\n styles because the selected pill is expressed with :has(input:checked): the\n radio is the state, so nothing in React has to style the label by hand. */\n.theme-switch{\ndisplay:inline-flex;\nalign-items:center;\ngap:0;\nborder:1px solid var(--fonderie-border);\nborder-radius:9999px;\npadding:3px;\nmargin:0;\nbackground:var(--fonderie-bg);\n}\n.theme-switch__option{\ndisplay:inline-flex;\nalign-items:center;\ngap:4px;\npadding:4px 10px;\nborder-radius:9999px;\ncursor:pointer;\nfont-size:12px;\ncolor:var(--fonderie-text-muted);\ntransition:background .15s,color .15s;\nwhite-space:nowrap;\n}\n.theme-switch__option input{position:absolute;opacity:0;width:0;height:0;pointer-events:none}\n.theme-switch__option:has(input:checked){\nbackground:var(--fonderie-surface);\ncolor:var(--fonderie-text);\nbox-shadow:0 1px 2px rgba(0,0,0,.06);\n}\n/* :has() is wide but not universal. Without it every option reads as unselected\n and the control looks broken, so keyboard focus alone must still show where\n you are — this is the fallback that keeps it usable. */\n.theme-switch__option:focus-within{color:var(--fonderie-text)}\n.theme-switch__option svg{flex-shrink:0}\n.theme-switch__option span{line-height:1}\n`;\n\n// Applies a STORED theme before the first paint. Without it an operator who\n// chose light on a dark machine gets a dark flash on every load, which is the\n// specific thing a console cannot look like.\n//\n// \"System\" is the ABSENCE of the attribute, so this script does nothing in that\n// case and the media query above handles it — which also means system-mode\n// follows the OS live, with no listener. The organisation's own switcher\n// resolves system eagerly to a concrete value and writes it, so a machine that\n// flips to dark at sunset keeps the old theme until reload; this does not.\n//\n// Degrades correctly: if localStorage throws, or a consumer's CSP blocks the\n// inline script, no attribute is set and the console follows the system — the\n// default either way.\nconst BOOT = `(function(){try{var t=localStorage.getItem('fonderie.admin.theme');` +\n\t`if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t)}catch(e){}})()`;\n\nexport const uiHtml = (scriptPath: string): string => `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex, nofollow\">\n<meta name=\"color-scheme\" content=\"light dark\">\n<title>Admin</title>\n<style>${TOKENS}</style>\n<script>${BOOT}</script>\n</head>\n<body>\n<div id=\"root\"></div>\n<script src=\"${scriptPath}\" defer></script>\n</body>\n</html>\n`;\n"],"mappings":"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","names":["import_node_fs","import_core","module","module","module","import_core","module"]}
|
package/dist/index.js
CHANGED
|
@@ -517,7 +517,7 @@ var uiHtml = (scriptPath) => `<!doctype html>
|
|
|
517
517
|
// src/module.ts
|
|
518
518
|
var DEFAULT_ADMIN_PATH = "/_admin";
|
|
519
519
|
var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
520
|
-
var ADMIN_VERSION = "1.0.
|
|
520
|
+
var ADMIN_VERSION = "1.0.2";
|
|
521
521
|
function normalizeHostPattern(host) {
|
|
522
522
|
return host.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/.*$/, "").toLowerCase();
|
|
523
523
|
}
|