@fonderie/admin 0.10.0 → 1.0.1

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/brain/outcomes.md CHANGED
@@ -50,8 +50,8 @@ Raw SQL ships in `node_modules/@fonderie/admin/dist/migrations/sql/` — read it
50
50
  | POST | `/_admin/access/tokens` | `[ validate(issueTokenSchema), async (ctx) => { const body = ctx.meta['body'] as { name: string; scopes: AdminScope[]; expiresInDays?: number; }; const createdBy = ctx.request.headers.get('x-actor') || 'admin-token'; const { token: plaintext, record } = await issueToken(store, { ...body, createdBy, }); // The plaintext is returned once and never stored. return setApiResponse(HTTP.CREATED, 'TOKEN_ISSUED', 'Token issued — shown once', { token: plaintext, ...record, }); }, ]` |
51
51
  | DELETE | `/_admin/access/tokens/:id` | `[ async (ctx) => { const ok = await revokeToken(store, ctx.meta.params?.['id'] ?? ''); return ok ? setApiResponse(HTTP.OK, 'TOKEN_REVOKED', 'Token revoked') : setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such live token'); }, ]` |
52
52
  | GET | `/_admin/activity/admin-log` | `async (ctx) => { const q = new URL(ctx.request.url).searchParams; const limit = Number(q.get('limit')) || undefined; const before = q.get('before') ?? undefined; const page = await readAdminLog(store, { ...(limit ? { limit } : {}), ...(before ? { before } : {}), }); return setApiResponse(HTTP.OK, 'ADMIN_LOG', 'Admin activity', page); }` |
53
- | GET | `/_admin/config` | `async () => setApiResponse( HTTP.OK, 'ADMIN_CONFIG', 'Declared vs held', configReport(app, this.options.env ?? []), )` |
54
53
  | GET | `/_admin/doctor` | `async () => setApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor())` |
54
+ | GET | `/_admin/environment` | `async () => setApiResponse( HTTP.OK, 'ADMIN_ENVIRONMENT', 'Declared vs held', environmentReport(app, this.options.env ?? []), )` |
55
55
  | GET | `/_admin/manifest` | `async () => setApiResponse( HTTP.OK, 'ADMIN_MANIFEST', 'Deployment manifest', buildManifest(app, { version: this.version, log: Boolean(store), host: this.hosts }), )` |
56
56
  | GET | `/_admin/migrations` | `[ async () => setApiResponse( HTTP.OK, 'MIGRATIONS', 'Pending migrations by module', await migrationsReport(store, migrationSets), ), ]` |
57
57
  | POST | `/_admin/migrations/:module/apply` | `[ validate(applyMigrationsSchema), async (ctx) => { const { expect } = ctx.meta['body'] as { expect: string[] }; const name = ctx.meta.params?.['module'] ?? ''; const out = await applyModuleMigrations(store, migrationSets, name, expect); if (out.ok) { return setApiResponse( HTTP.OK, out.reason, out.reason === 'MIGRATIONS_APPLIED' ? `Applied. ${out.module.pending.length} still pending in ${name}.` : `${name} is already up to date`, out.module, ); } switch (out.reason) { case 'NOT_FOUND': return setApiResponse( HTTP.NOT_FOUND, 'NOT_FOUND', `No migration set named "${name}"`, ); case 'MIGRATIONS_OUT_OF_ORDER': return setApiResponse( HTTP.CONFLICT, out.reason, `Apply "${out.blockedBy}" first — it runs before "${name}" and is behind.`, { blockedBy: out.blockedBy }, ); case 'MIGRATIONS_CHANGED': return setApiResponse( HTTP.CONFLICT, out.reason, 'What is pending changed since you looked. Refresh and read it again.', { expected: out.expected, actual: out.actual }, ); default: return setApiResponse( HTTP.UNPROCESSABLE, out.reason, 'A pending migration deletes data. No down-migration brings it back — ' + 'apply it through CI or `npm run migrate`, not from here.', { files: out.files }, ); } }, ]` |
@@ -37,7 +37,7 @@ const DEFAULT_ACTOR: "admin-token"
37
37
 
38
38
  const MAX_PAGE: 200
39
39
 
40
- function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport
40
+ function environmentReport(app: IFonderieApp, env: string[]): IAdminEnvironmentReport
41
41
 
42
42
  function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport
43
43
 
@@ -145,7 +145,7 @@ interface IAdminEnvEntry {
145
145
  set: boolean;
146
146
  }
147
147
 
148
- interface IAdminConfigReport {
148
+ interface IAdminEnvironmentReport {
149
149
  generatedAt: string;
150
150
  readiness: IReadinessReport;
151
151
  modules: Array<{
package/dist/index.cjs CHANGED
@@ -33,7 +33,7 @@ __export(index_exports, {
33
33
  attention: () => attention,
34
34
  buildManifest: () => buildManifest,
35
35
  collectChecks: () => collectChecks,
36
- configReport: () => configReport,
36
+ environmentReport: () => environmentReport,
37
37
  grants: () => grants,
38
38
  hashToken: () => hashToken,
39
39
  issueToken: () => issueToken,
@@ -312,7 +312,7 @@ function buildManifest(app, admin) {
312
312
  }
313
313
 
314
314
  // src/pages.ts
315
- function configReport(app, env) {
315
+ function environmentReport(app, env) {
316
316
  const report = app.securityReport();
317
317
  const byModule = /* @__PURE__ */ new Map();
318
318
  for (const p of report.readiness.problems)
@@ -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 = "0.10.0";
575
+ var ADMIN_VERSION = "1.0.1";
576
576
  function normalizeHostPattern(host) {
577
577
  return host.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/.*$/, "").toLowerCase();
578
578
  }
@@ -634,12 +634,20 @@ var AdminModule = class {
634
634
  ],
635
635
  [
636
636
  "GET",
637
- "/_admin/config",
637
+ // NOT '/_admin/config'. This reports readiness problems and which
638
+ // DECLARED ENV VARS are set — a deployment report, not configuration.
639
+ // It held the name anyway, which blocked @fonderie/config from ever
640
+ // mounting beside the console (both described GET /_admin/config and
641
+ // boot failed), and the misnomer is what made the shell probe
642
+ // '/config' to detect the config brick — a probe that was true on
643
+ // every deployment, built a client against admin's own report and
644
+ // crashed the page with "a.map is not a function".
645
+ "/_admin/environment",
638
646
  async () => (0, import_core3.setApiResponse)(
639
647
  import_core3.HTTP.OK,
640
- "ADMIN_CONFIG",
648
+ "ADMIN_ENVIRONMENT",
641
649
  "Declared vs held",
642
- configReport(app, this.options.env ?? [])
650
+ environmentReport(app, this.options.env ?? [])
643
651
  )
644
652
  ],
645
653
  [
@@ -860,7 +868,7 @@ var AdminModule = class {
860
868
  attention,
861
869
  buildManifest,
862
870
  collectChecks,
863
- configReport,
871
+ environmentReport,
864
872
  grants,
865
873
  hashToken,
866
874
  issueToken,
@@ -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 { configReport, 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\tIAdminConfigReport,\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 { configReport, 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'/_admin/config',\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_CONFIG',\n\t\t\t\t\t\t'Declared vs held',\n\t\t\t\t\t\tconfigReport(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\tIAdminConfigReport,\n\tIAdminRoutesReport,\n\tIAdminTokenRecord,\n\tIAdminTokensReport,\n} from './types';\n\nexport function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport {\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"]}
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":"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","names":["import_node_fs","import_core","module","module","module","import_core","module"]}
package/dist/index.d.cts CHANGED
@@ -102,7 +102,7 @@ interface IAdminEnvEntry {
102
102
  name: string;
103
103
  set: boolean;
104
104
  }
105
- interface IAdminConfigReport {
105
+ interface IAdminEnvironmentReport {
106
106
  generatedAt: string;
107
107
  readiness: IReadinessReport;
108
108
  modules: Array<{
@@ -195,7 +195,7 @@ declare function readAdminLog(store: IStoreAdapter, opts?: {
195
195
  before?: string;
196
196
  }): Promise<IAdminLogPage>;
197
197
 
198
- declare function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport;
198
+ declare function environmentReport(app: IFonderieApp, env: string[]): IAdminEnvironmentReport;
199
199
  declare function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport;
200
200
  declare function tokensReport(app: IFonderieApp, adminModule: string, issued: IAdminTokenRecord[] | null): IAdminTokensReport;
201
201
 
@@ -272,4 +272,4 @@ declare function issueToken(store: IStoreAdapter, input: {
272
272
  declare function revokeToken(store: IStoreAdapter, id: string): Promise<boolean>;
273
273
  declare function requireAdminScope(bootstrap: string, store: IStoreAdapter | undefined, needed: AdminScope | 'root'): Middleware;
274
274
 
275
- export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminMigrationModule, type IAdminMigrationsReport, type IAdminModuleEntry, type IAdminOptions, type IAdminPendingMigration, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, type IMigrationSet, MAX_PAGE, SCOPES, adminLog, applyMigrationsSchema, applyModuleMigrations, attention, buildManifest, collectChecks, configReport, grants, hashToken, issueToken, listTokens, migrationsReport, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
275
+ export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminEnvironmentReport, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminMigrationModule, type IAdminMigrationsReport, type IAdminModuleEntry, type IAdminOptions, type IAdminPendingMigration, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, type IMigrationSet, MAX_PAGE, SCOPES, adminLog, applyMigrationsSchema, applyModuleMigrations, attention, buildManifest, collectChecks, environmentReport, grants, hashToken, issueToken, listTokens, migrationsReport, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
package/dist/index.d.ts CHANGED
@@ -102,7 +102,7 @@ interface IAdminEnvEntry {
102
102
  name: string;
103
103
  set: boolean;
104
104
  }
105
- interface IAdminConfigReport {
105
+ interface IAdminEnvironmentReport {
106
106
  generatedAt: string;
107
107
  readiness: IReadinessReport;
108
108
  modules: Array<{
@@ -195,7 +195,7 @@ declare function readAdminLog(store: IStoreAdapter, opts?: {
195
195
  before?: string;
196
196
  }): Promise<IAdminLogPage>;
197
197
 
198
- declare function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport;
198
+ declare function environmentReport(app: IFonderieApp, env: string[]): IAdminEnvironmentReport;
199
199
  declare function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport;
200
200
  declare function tokensReport(app: IFonderieApp, adminModule: string, issued: IAdminTokenRecord[] | null): IAdminTokensReport;
201
201
 
@@ -272,4 +272,4 @@ declare function issueToken(store: IStoreAdapter, input: {
272
272
  declare function revokeToken(store: IStoreAdapter, id: string): Promise<boolean>;
273
273
  declare function requireAdminScope(bootstrap: string, store: IStoreAdapter | undefined, needed: AdminScope | 'root'): Middleware;
274
274
 
275
- export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminMigrationModule, type IAdminMigrationsReport, type IAdminModuleEntry, type IAdminOptions, type IAdminPendingMigration, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, type IMigrationSet, MAX_PAGE, SCOPES, adminLog, applyMigrationsSchema, applyModuleMigrations, attention, buildManifest, collectChecks, configReport, grants, hashToken, issueToken, listTokens, migrationsReport, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
275
+ export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminEnvironmentReport, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminMigrationModule, type IAdminMigrationsReport, type IAdminModuleEntry, type IAdminOptions, type IAdminPendingMigration, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, type IMigrationSet, MAX_PAGE, SCOPES, adminLog, applyMigrationsSchema, applyModuleMigrations, attention, buildManifest, collectChecks, environmentReport, grants, hashToken, issueToken, listTokens, migrationsReport, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
package/dist/index.js CHANGED
@@ -257,7 +257,7 @@ function buildManifest(app, admin) {
257
257
  }
258
258
 
259
259
  // src/pages.ts
260
- function configReport(app, env) {
260
+ function environmentReport(app, env) {
261
261
  const report = app.securityReport();
262
262
  const byModule = /* @__PURE__ */ new Map();
263
263
  for (const p of report.readiness.problems)
@@ -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 = "0.10.0";
520
+ var ADMIN_VERSION = "1.0.1";
521
521
  function normalizeHostPattern(host) {
522
522
  return host.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/.*$/, "").toLowerCase();
523
523
  }
@@ -579,12 +579,20 @@ var AdminModule = class {
579
579
  ],
580
580
  [
581
581
  "GET",
582
- "/_admin/config",
582
+ // NOT '/_admin/config'. This reports readiness problems and which
583
+ // DECLARED ENV VARS are set — a deployment report, not configuration.
584
+ // It held the name anyway, which blocked @fonderie/config from ever
585
+ // mounting beside the console (both described GET /_admin/config and
586
+ // boot failed), and the misnomer is what made the shell probe
587
+ // '/config' to detect the config brick — a probe that was true on
588
+ // every deployment, built a client against admin's own report and
589
+ // crashed the page with "a.map is not a function".
590
+ "/_admin/environment",
583
591
  async () => setApiResponse2(
584
592
  HTTP2.OK,
585
- "ADMIN_CONFIG",
593
+ "ADMIN_ENVIRONMENT",
586
594
  "Declared vs held",
587
- configReport(app, this.options.env ?? [])
595
+ environmentReport(app, this.options.env ?? [])
588
596
  )
589
597
  ],
590
598
  [
@@ -804,7 +812,7 @@ export {
804
812
  attention,
805
813
  buildManifest,
806
814
  collectChecks,
807
- configReport,
815
+ environmentReport,
808
816
  grants,
809
817
  hashToken,
810
818
  issueToken,