@fonderie/admin 1.4.0 → 1.5.0

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.
@@ -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/operators/http.ts","../src/operators/crypto.ts","../src/operators/schemas.ts","../src/operators/service.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 { secretBox } from './operators/crypto';\nimport { needsStepUp, operatorAdminRoutes, sessionRoutes } from './operators/http';\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\t// Operator accounts ride on the store: sessions, lockouts and links are rows.\n\t\tconst operatorsOn = Boolean(store) && this.options.operators !== false;\n\t\tconst operatorDeps =\n\t\t\tstore && operatorsOn\n\t\t\t\t? {\n\t\t\t\t\t\tstore,\n\t\t\t\t\t\tbox: secretBox(this.options.operatorKey),\n\t\t\t\t\t\trootToken: token,\n\t\t\t\t\t\t// From the request, so a basePath in front of the surface is kept.\n\t\t\t\t\t\tuiPath: (ctx: { request: Request }) => {\n\t\t\t\t\t\t\tconst p = new URL(ctx.request.url).pathname;\n\t\t\t\t\t\t\tconst i = p.indexOf(this.path);\n\t\t\t\t\t\t\treturn `${i >= 0 ? p.slice(0, i + this.path.length) : this.path}/ui`;\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: null;\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 need = needed ?? scopeFor(method, path);\n\t\t\tconst guard = requireAdminScope(token, store, need, {\n\t\t\t\tstepUp: needsStepUp(method, path, need),\n\t\t\t\toperators: operatorsOn,\n\t\t\t});\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\tif (operatorDeps && store) {\n\t\t\t// The sign-in routes sit OUTSIDE the guard — they are how a session that\n\t\t\t// passes it comes to exist — but inside the host guard and the log.\n\t\t\tfor (const [method, path, handlers] of sessionRoutes(operatorDeps)) {\n\t\t\t\tconst full = at(path);\n\t\t\t\tmounted.set(`${method} ${full}`, this.name);\n\t\t\t\tapp.addRoute(\n\t\t\t\t\tmethod,\n\t\t\t\t\tfull,\n\t\t\t\t\tadminLog(store, full, this.name),\n\t\t\t\t\t...(hostGuard ? [hostGuard] : []),\n\t\t\t\t\t...handlers,\n\t\t\t\t);\n\t\t\t}\n\t\t\tfor (const [method, path, handlers, needed] of operatorAdminRoutes(operatorDeps))\n\t\t\t\tmount(this.name, method, at(path), handlers, needed);\n\t\t}\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\tconst problems = validateAdminToken(this.options.adminToken, { module: this.name });\n\t\tconst operatorsOn = Boolean(this.options.store) && this.options.operators !== false;\n\t\tif (\n\t\t\toperatorsOn &&\n\t\t\tthis.options.operatorKey &&\n\t\t\t!/^[0-9a-fA-F]{64}$/.test(this.options.operatorKey)\n\t\t) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: this.name,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'operatorKey must be 64 hex characters (`openssl rand -hex 32`)',\n\t\t\t});\n\t\t}\n\t\t// No key is a supported setup, not a problem: authenticator secrets are\n\t\t// stored as-is (as django-otp and many SaaS do), and a leaked one alone\n\t\t// opens nothing — the operator's scrypt-hashed password is still needed.\n\t\t// operatorKey is optional hardening against a database leak.\n\t\treturn problems;\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\t// An operator session names itself and cannot be overridden: X-Actor\n\t\t\t// is a client-supplied label, fine for a token, not for a person.\n\t\t\tactor:\n\t\t\t\t(typeof ctx.meta['adminOperator'] === 'string'\n\t\t\t\t\t? `operator:${ctx.meta['adminOperator']}`\n\t\t\t\t\t: null) ||\n\t\t\t\t(typeof ctx.meta['adminActor'] === 'string' ? ctx.meta['adminActor'] : null) ||\n\t\t\t\tctx.request.headers.get('x-actor') ||\n\t\t\t\t(typeof ctx.meta['adminTokenName'] === 'string'\n\t\t\t\t\t? `token:${ctx.meta['adminTokenName']}`\n\t\t\t\t\t: DEFAULT_ACTOR),\n\t\t\tmethod: ctx.request.method,\n\t\t\t// Normalized, so '/x' and '/x/' are one row for one route — the same\n\t\t\t// path the router matched and `route` below records the pattern of.\n\t\t\tpath: normalizeRequestPath(new URL(ctx.request.url).pathname),\n\t\t\troute,\n\t\t\tmodule,\n\t\t\tstatus: res.status,\n\t\t\tdurationMs: Date.now() - started,\n\t\t\trequestId: typeof ctx.meta['requestId'] === 'string' ? ctx.meta['requestId'] : null,\n\t\t\tclientIp: ctx.meta.clientIp ?? null,\n\t\t};\n\t\ttry {\n\t\t\tawait store.query(\n\t\t\t\t`INSERT INTO fonderie_admin_log\n\t\t\t\t (actor, method, path, route, module, status, duration_ms, request_id, client_ip)\n\t\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,\n\t\t\t\t[\n\t\t\t\t\trow.actor,\n\t\t\t\t\trow.method,\n\t\t\t\t\trow.path,\n\t\t\t\t\trow.route,\n\t\t\t\t\trow.module,\n\t\t\t\t\trow.status,\n\t\t\t\t\trow.durationMs,\n\t\t\t\t\trow.requestId,\n\t\t\t\t\trow.clientIp,\n\t\t\t\t],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error(\n\t\t\t\t'[admin] could not write the admin log:',\n\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t);\n\t\t}\n\t\treturn res;\n\t};\n}\n\nexport const MAX_PAGE = 200;\n\n// Newest first, keyset on (at, id). `before` is the last entry's cursor.\nexport async function readAdminLog(\n\tstore: IStoreAdapter,\n\topts: { limit?: number; before?: string } = {},\n): Promise<IAdminLogPage> {\n\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);\n\tconst cursor = opts.before ? decodeCursor(opts.before) : null;\n\tconst rows = await store.query<IAdminLogEntry>(\n\t\t`SELECT id, at, actor, method, path, route, module, status,\n\t\t duration_ms AS \"durationMs\", request_id AS \"requestId\", client_ip AS \"clientIp\"\n\t\t FROM fonderie_admin_log\n\t\t WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))\n\t\t ORDER BY at DESC, id DESC\n\t\t LIMIT $3`,\n\t\t[cursor?.at ?? null, cursor?.id ?? null, limit + 1],\n\t);\n\tconst page = rows.slice(0, limit);\n\tconst last = page[page.length - 1];\n\treturn {\n\t\tentries: page,\n\t\tnext: rows.length > limit && last ? encodeCursor(last) : null,\n\t};\n}\n\nfunction encodeCursor(e: IAdminLogEntry): string {\n\treturn Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, 'utf8').toString('base64url');\n}\n\nfunction decodeCursor(c: string): { at: string; id: string } | null {\n\tconst [at, id] = Buffer.from(c, 'base64url').toString('utf8').split('|');\n\treturn at && id ? { at, id } : null;\n}\n","import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { IRequestSchema } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport { classifyMigration, InternalMigrationRunner } from '@fonderie/store';\n\nimport type { IAdminMigrationModule, IAdminMigrationsReport, IMigrationSet } from './types';\n\n/**\n * `expect` is the exact pending list the operator was shown, in order.\n *\n * It is not ceremony. Between rendering the page and clicking the button a\n * deploy can change what is pending — and the thing that changed is precisely\n * the thing nobody reviewed. Sending it back makes the server refuse rather\n * than apply a set the operator never saw. Same shape as versionedWrite's\n * if_version, and it also gives this POST the body schema audit:validation\n * requires.\n */\nexport const applyMigrationsSchema: IRequestSchema = {\n\tsafeParse(input: unknown) {\n\t\tconst fail = (path: string, message: string) => ({\n\t\t\tsuccess: false as const,\n\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t});\n\t\tconst b = (input ?? {}) as Record<string, unknown>;\n\t\tconst expect = b['expect'];\n\t\tif (!Array.isArray(expect) || !expect.every((f) => typeof f === 'string'))\n\t\t\treturn fail('expect', 'expect must be the list of pending filenames you were shown');\n\t\t// An empty list is legitimate: it asserts \"I was told there was nothing\n\t\t// to do\", and the handler answers UP_TO_DATE or CHANGED accordingly.\n\t\treturn { success: true as const, data: { expect: expect as string[] } };\n\t},\n};\n\n// Applying is the APP's order, never ours. The sets arrive already interleaved\n// — brick dirs and the app's own, in the sequence its applier runs them — from\n// the one constant both the applier and this reporter read. See the reference app's\n// db/migrations/steps.ts: \"auth owns fonderie_users, which the app migration\n// extends, and media's assets reference storage's blobs.\"\n\n// Has this database EVER been migrated? pending() cannot answer it: it catches\n// its own read failure and returns every file, so a virgin database, a database\n// belonging to another app, and an unreachable one are the same answer. Asked\n// directly, because \"61 pending\" means something completely different depending\n// on which of those it is.\nasync function hasMigrationHistory(store: IStoreAdapter): Promise<boolean> {\n\ttry {\n\t\tconst rows = await store.query<{ n: number }>(\n\t\t\t'SELECT count(*)::int AS n FROM fonderie_migrations',\n\t\t);\n\t\treturn (rows[0]?.n ?? 0) > 0;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction classifyPending(dir: string, files: readonly string[]) {\n\treturn files.map((file) => {\n\t\tconst { impact, destructive } = classifyMigration(readFileSync(join(dir, file), 'utf8'));\n\t\treturn { file, impact, destructive };\n\t});\n}\n\n/**\n * What every module is waiting on, with each pending file's impact.\n *\n * `blockedBy` names the first EARLIER module that is behind. Order is\n * load-bearing and declared by the app, so a module cannot be applied while\n * something it may depend on is unapplied — the panel offers one button at a\n * time and the operator walks the list in the declared order.\n */\nexport async function migrationsReport(\n\tstore: IStoreAdapter,\n\tsets: ReadonlyArray<IMigrationSet>,\n): Promise<IAdminMigrationsReport> {\n\tconst everApplied = await hasMigrationHistory(store);\n\n\tconst modules: IAdminMigrationModule[] = [];\n\tlet firstBehind: string | null = null;\n\n\tfor (const [name, dir] of sets) {\n\t\tconst pending = await new InternalMigrationRunner(store, dir).pending();\n\t\tconst files = classifyPending(dir, pending);\n\t\tmodules.push({\n\t\t\tname,\n\t\t\tpending: files,\n\t\t\t// Only a module BEFORE this one blocks it. The first module with\n\t\t\t// pending work blocks every module after it, and nothing blocks it.\n\t\t\tblockedBy: firstBehind,\n\t\t\t// A file that deletes data on a database with nothing in it yet is not\n\t\t\t// destroying anything. Same rule the CLI's --check applies.\n\t\t\tappliable:\n\t\t\t\tfiles.length > 0 &&\n\t\t\t\tfirstBehind === null &&\n\t\t\t\t(!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| {\n\t\t\tok: true;\n\t\t\treason: 'MIGRATIONS_APPLIED' | 'MIGRATIONS_UP_TO_DATE';\n\t\t\tmodule: IAdminMigrationModule;\n\t }\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 { authorizeOperator } from './operators/http';\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.\n//\n// Without a Bearer header, an operator's session cookie is the other way in\n// (see operators/http.ts): same scopes, plus a fresh authenticator code for\n// the dangerous actions `stepUp` marks.\nexport function requireAdminScope(\n\tbootstrap: string,\n\tstore: IStoreAdapter | undefined,\n\tneeded: AdminScope | 'root',\n\topts: { stepUp?: boolean; operators?: boolean } = {},\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst token = bearer(ctx);\n\t\tif (!token) {\n\t\t\tif (!store || !opts.operators) return UNAUTHORIZED();\n\t\t\tconst verdict = await authorizeOperator(store, ctx, needed, opts.stepUp ?? false);\n\t\t\tif (verdict === null) return UNAUTHORIZED();\n\t\t\treturn verdict === 'ok' ? next() : verdict;\n\t\t}\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","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { HTTP, constantTimeEqual, setApiResponse } from '@fonderie/core';\nimport { validate } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { SCOPES, grants } from '../tokens';\nimport type { AdminScope } from '../types';\nimport { type ISecretBox, passwordProblem, totpUri } from './crypto';\nimport {\n\tclaimSchema,\n\tenrollmentSchema,\n\tfactorSchema,\n\tinspectLinkSchema,\n\tinviteSchema,\n\tloginSchema,\n\tredeemLinkSchema,\n\tupdateOperatorSchema,\n} from './schemas';\nimport {\n\tABSOLUTE_MS,\n\tEMAIL_RE,\n\tINVITE_HOURS,\n\ttype IOperatorRow,\n\tPENDING_MS,\n\tRECOVERY_HOURS,\n\ttype SessionStage,\n\tcheckPassword,\n\tcheckSecondFactor,\n\tclaimFirstOperator,\n\tconfirmEnrollment,\n\tcreateLink,\n\tcreateSession,\n\tdeleteOperatorSessions,\n\tdeleteSession,\n\tenrollmentSecret,\n\tfindLink,\n\tfindOperator,\n\tlistOperators,\n\tlockMinutes,\n\tmarkStepUp,\n\tnormalizeEmail,\n\toperatorCount,\n\tpublicOperator,\n\treadSession,\n\tredeemLink,\n\tstepUpFresh,\n} from './service';\n\n// ── the cookie ─────────────────────────────────────────────────────────────\n// HttpOnly (script cannot read it), SameSite=Strict (never sent from another\n// site), Secure everywhere except plain-http localhost. Over HTTPS it takes\n// the __Host- prefix, which the browser enforces: Secure, Path=/, no Domain —\n// so a sibling subdomain cannot set or shadow it.\n\nconst LOCAL = new Set(['localhost', '127.0.0.1', '[::1]']);\nconst isSecure = (ctx: IFonderieContext): boolean => {\n\tconst u = new URL(ctx.request.url);\n\treturn !(u.protocol === 'http:' && LOCAL.has(u.hostname));\n};\nconst cookieName = (ctx: IFonderieContext) =>\n\tisSecure(ctx) ? '__Host-fonderie_admin' : 'fonderie_admin';\n\nexport function readCookie(ctx: IFonderieContext): string {\n\tconst header = ctx.request.headers.get('cookie') ?? '';\n\tconst want = cookieName(ctx);\n\tfor (const part of header.split(';')) {\n\t\tconst i = part.indexOf('=');\n\t\tif (i > 0 && part.slice(0, i).trim() === want)\n\t\t\treturn decodeURIComponent(part.slice(i + 1).trim());\n\t}\n\treturn '';\n}\n\nfunction withCookie(\n\tres: Response,\n\tctx: IFonderieContext,\n\tvalue: string,\n\tmaxAgeMs: number,\n): Response {\n\tconst attrs = [\n\t\t`${cookieName(ctx)}=${value}`,\n\t\t'Path=/',\n\t\t'HttpOnly',\n\t\t'SameSite=Strict',\n\t\t`Max-Age=${Math.floor(maxAgeMs / 1000)}`,\n\t];\n\tif (isSecure(ctx)) attrs.push('Secure');\n\tres.headers.append('set-cookie', attrs.join('; '));\n\tres.headers.set('cache-control', 'no-store');\n\treturn res;\n}\nconst clearCookie = (res: Response, ctx: IFonderieContext) => withCookie(res, ctx, '', 0);\n\n// ── CSRF ───────────────────────────────────────────────────────────────────\n// SameSite=Strict already keeps the cookie off cross-SITE requests. A sibling\n// subdomain is the same site, so a cookie-authenticated write must also come\n// from THIS origin: browsers send Origin (or Sec-Fetch-Site) on every\n// non-GET fetch. A request with neither is not from a browser page.\nexport function sameOrigin(ctx: IFonderieContext): boolean {\n\tconst origin = ctx.request.headers.get('origin');\n\tconst host = ctx.request.headers.get('host') ?? new URL(ctx.request.url).host;\n\tif (origin) {\n\t\ttry {\n\t\t\treturn new URL(origin).host === host;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\tconst site = ctx.request.headers.get('sec-fetch-site');\n\treturn site === null || site === 'same-origin';\n}\n\n// ── per-IP attempt limit ───────────────────────────────────────────────────\n// Best effort, in memory: per instance on serverless. The durable brake is the\n// per-account lockout in the database; this one stops one address from\n// spraying many accounts.\nconst WINDOW_MS = 10 * 60_000;\nconst PER_IP = 30;\nconst attempts = new Map<string, number[]>();\nfunction ipLimited(ctx: IFonderieContext): boolean {\n\tconst ip = ctx.meta.clientIp ?? 'unknown';\n\tconst now = Date.now();\n\tconst recent = (attempts.get(ip) ?? []).filter((t) => now - t < WINDOW_MS);\n\trecent.push(now);\n\tattempts.set(ip, recent);\n\tif (attempts.size > 10_000) attempts.clear();\n\treturn recent.length > PER_IP;\n}\n/** Test hook: forget every address. */\nexport const resetAttemptLimits = (): void => attempts.clear();\n\n// ── helpers ────────────────────────────────────────────────────────────────\n\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\nconst str = (v: unknown): string => (typeof v === 'string' ? v : '');\nconst bearer = (ctx: IFonderieContext): string => {\n\tconst h = ctx.request.headers.get('authorization') ?? '';\n\treturn h.startsWith('Bearer ') ? h.slice(7) : '';\n};\n\nconst BAD_CREDENTIALS = () =>\n\tsetApiResponse(\n\t\tHTTP.UNAUTHORIZED,\n\t\t'INVALID_CREDENTIALS',\n\t\t'That email and password do not match an operator.',\n\t);\nconst BAD_CODE = () =>\n\tsetApiResponse(\n\t\tHTTP.UNAUTHORIZED,\n\t\t'INVALID_CODE',\n\t\t'That code did not work. Codes change every 30 seconds.',\n\t);\nconst LOCKED = (op: IOperatorRow) =>\n\tsetApiResponse(\n\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t'LOCKED',\n\t\t`Too many attempts. Try again in ${lockMinutes(op)} minute(s).`,\n\t);\nconst TOO_FAST = () =>\n\tsetApiResponse(\n\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t'RATE_LIMITED',\n\t\t'Too many attempts from this address. Wait a few minutes.',\n\t);\nconst WRONG_STAGE = () =>\n\tsetApiResponse(HTTP.UNAUTHORIZED, 'NO_SESSION', 'Start again from sign-in.');\nconst CROSS_ORIGIN = () =>\n\tsetApiResponse(HTTP.FORBIDDEN, 'CROSS_ORIGIN', 'This request did not come from the admin page.');\n\nconst stateOf = (stage: SessionStage) =>\n\tstage === 'active' ? 'signed-in' : stage === 'enroll' ? 'needs-enrollment' : 'needs-2fa';\n\nexport interface IOperatorDeps {\n\tstore: IStoreAdapter;\n\tbox: ISecretBox;\n\trootToken: string;\n\t/** Where the console lives, for the links it hands out: '<prefix>/ui'. */\n\tuiPath: (ctx: IFonderieContext) => string;\n}\n\nasync function startSession(\n\tdeps: IOperatorDeps,\n\tctx: IFonderieContext,\n\top: IOperatorRow,\n\tstage: SessionStage,\n\tpayload: Record<string, unknown> = {},\n) {\n\tctx.meta['adminOperator'] = op.email;\n\tconst id = await createSession(deps.store, ctx, op.id, stage);\n\tconst res = setApiResponse(HTTP.OK, 'ADMIN_SESSION', 'Session', {\n\t\tstate: stateOf(stage),\n\t\toperator: publicOperator(op),\n\t\t...payload,\n\t});\n\treturn withCookie(res, ctx, id, stage === 'active' ? ABSOLUTE_MS : PENDING_MS);\n}\n\n/** A pending (or active) session of the given stage, from the cookie. */\nasync function sessionAt(deps: IOperatorDeps, ctx: IFonderieContext, stage: SessionStage) {\n\tconst cookie = readCookie(ctx);\n\tconst found = await readSession(deps.store, cookie);\n\treturn found && found.session.stage === stage ? { ...found, cookie } : null;\n}\n\n// ── the public sign-in routes ─────────────────────────────────────────────\n// Outside the admin guard: they are how a guard-passing session comes to\n// exist. Every write is same-origin-checked and attempt-limited.\n\nexport function sessionRoutes(deps: IOperatorDeps): Array<[string, string, Middleware[]]> {\n\tconst { store, box } = deps;\n\t// Every sign-in write: same-origin, attempt-limited, attributed to 'anonymous'\n\t// in the log until a session names someone.\n\tconst check: Middleware = async (ctx, next) => {\n\t\tctx.meta['adminActor'] = 'anonymous';\n\t\tif (ctx.request.method !== 'GET' && !sameOrigin(ctx)) return CROSS_ORIGIN();\n\t\tif (ctx.request.method !== 'GET' && ipLimited(ctx)) return TOO_FAST();\n\t\treturn next();\n\t};\n\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/session',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst found = await readSession(store, readCookie(ctx));\n\t\t\t\t\tconst claimable = (await operatorCount(store)) === 0;\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'ADMIN_SESSION', 'Session', {\n\t\t\t\t\t\tstate: found ? stateOf(found.session.stage) : 'signed-out',\n\t\t\t\t\t\toperator: found ? publicOperator(found.op) : null,\n\t\t\t\t\t\tclaimable,\n\t\t\t\t\t\tstepUpFresh: found ? stepUpFresh(found.session) : false,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t// The one-time claim. Needs the root token AND an empty operator table;\n\t\t\t// after the first operator exists it is permanently refused.\n\t\t\t'POST',\n\t\t\t'/_admin/session/claim',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(claimSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tif (!constantTimeEqual(bearer(ctx), deps.rootToken)) {\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.UNAUTHORIZED,\n\t\t\t\t\t\t\t'UNAUTHORIZED',\n\t\t\t\t\t\t\t'Claiming needs the root admin token.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst email = normalizeEmail(str(b['email']));\n\t\t\t\t\tif (!EMAIL_RE.test(email))\n\t\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'Enter a valid email address.');\n\t\t\t\t\tconst problem = passwordProblem(b['password']);\n\t\t\t\t\tif (problem) return setApiResponse(HTTP.UNPROCESSABLE, 'WEAK_PASSWORD', problem);\n\t\t\t\t\tconst op = await claimFirstOperator(store, {\n\t\t\t\t\t\temail,\n\t\t\t\t\t\tname: str(b['name']) || undefined,\n\t\t\t\t\t\tpassword: str(b['password']),\n\t\t\t\t\t});\n\t\t\t\t\tif (!op)\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t'ALREADY_CLAIMED',\n\t\t\t\t\t\t\t'This console already has operators. Ask one for an invite.',\n\t\t\t\t\t\t);\n\t\t\t\t\treturn startSession(deps, ctx, op, 'enroll');\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/session/login',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(loginSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst result = await checkPassword(store, str(b['email']), str(b['password']));\n\t\t\t\t\tif (result.op && 'locked' in result && result.locked) return LOCKED(result.op);\n\t\t\t\t\tif (!result.ok || !result.op) return BAD_CREDENTIALS();\n\t\t\t\t\treturn startSession(\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\tresult.op,\n\t\t\t\t\t\tresult.op.totpConfirmedAt ? 'password' : 'enroll',\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/session/enrollment',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst s = await sessionAt(deps, ctx, 'enroll');\n\t\t\t\t\tif (!s) return WRONG_STAGE();\n\t\t\t\t\tconst secret = await enrollmentSecret(store, box, s.op);\n\t\t\t\t\tconst issuer = `Admin · ${new URL(ctx.request.url).hostname}`;\n\t\t\t\t\tconst res = setApiResponse(HTTP.OK, 'ENROLLMENT', 'Scan this with an authenticator app', {\n\t\t\t\t\t\tsecret,\n\t\t\t\t\t\turi: totpUri(issuer, s.op.email, secret),\n\t\t\t\t\t\taccount: s.op.email,\n\t\t\t\t\t\tissuer,\n\t\t\t\t\t});\n\t\t\t\t\tres.headers.set('cache-control', 'no-store');\n\t\t\t\t\treturn res;\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/session/enrollment',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(enrollmentSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst s = await sessionAt(deps, ctx, 'enroll');\n\t\t\t\t\tif (!s) return WRONG_STAGE();\n\t\t\t\t\tconst codes = await confirmEnrollment(store, box, s.op, str(body(ctx)['code']));\n\t\t\t\t\tif (!codes) {\n\t\t\t\t\t\tconst fresh = await findOperator(store, { id: s.op.id });\n\t\t\t\t\t\treturn fresh && lockMinutes(fresh) ? LOCKED(fresh) : BAD_CODE();\n\t\t\t\t\t}\n\t\t\t\t\t// A new session id at the privilege change: a pending id that leaked\n\t\t\t\t\t// never becomes a signed-in one.\n\t\t\t\t\tawait deleteSession(store, s.cookie);\n\t\t\t\t\tconst op = (await findOperator(store, { id: s.op.id })) ?? s.op;\n\t\t\t\t\treturn startSession(deps, ctx, op, 'active', { backupCodes: codes });\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/session/verify',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(factorSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst s = await sessionAt(deps, ctx, 'password');\n\t\t\t\t\tif (!s) return WRONG_STAGE();\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst r = await checkSecondFactor(store, box, s.op, {\n\t\t\t\t\t\tcode: b['code'],\n\t\t\t\t\t\tbackupCode: b['backupCode'],\n\t\t\t\t\t});\n\t\t\t\t\tif (!r.ok) {\n\t\t\t\t\t\tconst fresh = await findOperator(store, { id: s.op.id });\n\t\t\t\t\t\treturn fresh && lockMinutes(fresh) ? LOCKED(fresh) : BAD_CODE();\n\t\t\t\t\t}\n\t\t\t\t\tawait deleteSession(store, s.cookie);\n\t\t\t\t\tawait store.query(\n\t\t\t\t\t\t`UPDATE fonderie_admin_operators SET last_login_at = now() WHERE id = $1`,\n\t\t\t\t\t\t[s.op.id],\n\t\t\t\t\t);\n\t\t\t\t\treturn startSession(\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\ts.op,\n\t\t\t\t\t\t'active',\n\t\t\t\t\t\tr.via === 'backup' ? { backupCodesLeft: r.backupLeft } : {},\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t// A fresh code for the next five minutes of dangerous actions.\n\t\t\t'POST',\n\t\t\t'/_admin/session/step-up',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(factorSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst s = await sessionAt(deps, ctx, 'active');\n\t\t\t\t\tif (!s) return WRONG_STAGE();\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst r = await checkSecondFactor(store, box, s.op, {\n\t\t\t\t\t\tcode: b['code'],\n\t\t\t\t\t\tbackupCode: b['backupCode'],\n\t\t\t\t\t});\n\t\t\t\t\tif (!r.ok) return BAD_CODE();\n\t\t\t\t\tawait markStepUp(store, s.session.idHash);\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'STEPPED_UP', 'Confirmed for five minutes', {\n\t\t\t\t\t\tstepUpFresh: true,\n\t\t\t\t\t\t...(r.via === 'backup' ? { backupCodesLeft: r.backupLeft } : {}),\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'DELETE',\n\t\t\t'/_admin/session',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst cookie = readCookie(ctx);\n\t\t\t\t\tif (cookie) await deleteSession(store, cookie);\n\t\t\t\t\treturn clearCookie(setApiResponse(HTTP.OK, 'SIGNED_OUT', 'Signed out'), ctx);\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t// What a link is for, before the person sets a password. POST so the\n\t\t\t// token never sits in a URL a proxy logs.\n\t\t\t'POST',\n\t\t\t'/_admin/session/link/inspect',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(inspectLinkSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst link = await findLink(store, str(body(ctx)['token']));\n\t\t\t\t\treturn link\n\t\t\t\t\t\t? setApiResponse(HTTP.OK, 'LINK', 'Link', { kind: link.kind, email: link.email })\n\t\t\t\t\t\t: setApiResponse(\n\t\t\t\t\t\t\t\tHTTP.NOT_FOUND,\n\t\t\t\t\t\t\t\t'INVALID_LINK',\n\t\t\t\t\t\t\t\t'This link has expired or was already used. Ask for a new one.',\n\t\t\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/session/link',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(redeemLinkSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst problem = passwordProblem(b['password']);\n\t\t\t\t\tif (problem) return setApiResponse(HTTP.UNPROCESSABLE, 'WEAK_PASSWORD', problem);\n\t\t\t\t\tconst r = await redeemLink(store, str(b['token']), {\n\t\t\t\t\t\tpassword: str(b['password']),\n\t\t\t\t\t\tname: str(b['name']) || undefined,\n\t\t\t\t\t});\n\t\t\t\t\tif ('error' in r) {\n\t\t\t\t\t\treturn r.error === 'ALREADY_OPERATOR'\n\t\t\t\t\t\t\t? setApiResponse(\n\t\t\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t\t\t'ALREADY_OPERATOR',\n\t\t\t\t\t\t\t\t\t'That email is already an operator. Sign in instead.',\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: setApiResponse(\n\t\t\t\t\t\t\t\t\tHTTP.NOT_FOUND,\n\t\t\t\t\t\t\t\t\t'INVALID_LINK',\n\t\t\t\t\t\t\t\t\t'This link has expired or was already used. Ask for a new one.',\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn startSession(deps, ctx, r.op, 'enroll');\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t];\n}\n\n// ── guard integration ──────────────────────────────────────────────────────\n\n/**\n * Dangerous for a PERSON: needs a code from the last five minutes. Reads never\n * do; revealing a secret, minting a token or link, applying a migration and\n * deleting anything do.\n */\nexport function needsStepUp(method: string, path: string, needed: AdminScope | 'root'): boolean {\n\tconst m = method.toUpperCase();\n\tif (m === 'GET' || m === 'HEAD') return false;\n\treturn (\n\t\tneeded === 'root' ||\n\t\tneeded === 'secrets' ||\n\t\tm === 'DELETE' ||\n\t\t/\\/migrations\\/[^/]+\\/apply$/.test(path)\n\t);\n}\n\n/**\n * The cookie path of the admin guard. Returns null when the request carries\n * no operator session at all (the caller then answers 401 as before), or the\n * Response to send, or 'ok' with the operator recorded for the log.\n */\nexport async function authorizeOperator(\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tneeded: AdminScope | 'root',\n\tstepUp: boolean,\n): Promise<Response | 'ok' | null> {\n\tconst found = await readSession(store, readCookie(ctx));\n\tif (!found || found.session.stage !== 'active') return null;\n\tconst method = ctx.request.method.toUpperCase();\n\tif (method !== 'GET' && method !== 'HEAD' && !sameOrigin(ctx)) return CROSS_ORIGIN();\n\t// For a person, 'root' (tokens, operators) means the highest scope.\n\tconst scope: AdminScope = needed === 'root' ? 'secrets' : needed;\n\tif (!grants(found.op.scopes, scope)) {\n\t\treturn setApiResponse(\n\t\t\tHTTP.FORBIDDEN,\n\t\t\t'FORBIDDEN',\n\t\t\t`Your operator account lacks the '${scope}' scope`,\n\t\t);\n\t}\n\tctx.meta['adminOperator'] = found.op.email;\n\tif (stepUp && !stepUpFresh(found.session)) {\n\t\treturn setApiResponse(\n\t\t\tHTTP.FORBIDDEN,\n\t\t\t'STEP_UP_REQUIRED',\n\t\t\t'Enter a code from your authenticator app to continue.',\n\t\t);\n\t}\n\treturn 'ok';\n}\n\n// ── managing operators (behind the guard, root-level) ──────────────────────\n\nconst actorOf = (ctx: IFonderieContext): string =>\n\ttypeof ctx.meta['adminOperator'] === 'string'\n\t\t? `operator:${ctx.meta['adminOperator']}`\n\t\t: 'root-token';\n\nconst validScopes = (v: unknown): AdminScope[] | null =>\n\tArray.isArray(v) && v.length > 0 && v.every((s) => SCOPES.includes(s as AdminScope))\n\t\t? [...new Set(v as AdminScope[])]\n\t\t: null;\n\nexport function operatorAdminRoutes(\n\tdeps: IOperatorDeps,\n): Array<[string, string, Middleware[], 'read' | 'root']> {\n\tconst { store } = deps;\n\tconst link = (ctx: IFonderieContext, token: string) => `${deps.uiPath(ctx)}#/link/${token}`;\n\tconst idOf = (ctx: IFonderieContext) => ctx.meta.params?.['id'] ?? '';\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/access/operators',\n\t\t\t[async () => setApiResponse(HTTP.OK, 'OPERATORS', 'Operators', await listOperators(store))],\n\t\t\t'read',\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/access/operators/invites',\n\t\t\t[\n\t\t\t\tvalidate(inviteSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst email = normalizeEmail(str(b['email']));\n\t\t\t\t\tif (!EMAIL_RE.test(email))\n\t\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'Enter a valid email address.');\n\t\t\t\t\tconst scopes = validScopes(b['scopes']);\n\t\t\t\t\tif (!scopes)\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t\t'INVALID',\n\t\t\t\t\t\t\t`scopes must be a non-empty list of ${SCOPES.join(', ')}`,\n\t\t\t\t\t\t);\n\t\t\t\t\tif (await findOperator(store, { email }))\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t'ALREADY_OPERATOR',\n\t\t\t\t\t\t\t'That email is already an operator.',\n\t\t\t\t\t\t);\n\t\t\t\t\tconst hours = Number.isInteger(b['expiresInHours'])\n\t\t\t\t\t\t? Math.min(Math.max(b['expiresInHours'] as number, 1), 336)\n\t\t\t\t\t\t: INVITE_HOURS;\n\t\t\t\t\tconst l = await createLink(store, {\n\t\t\t\t\t\tkind: 'invite',\n\t\t\t\t\t\temail,\n\t\t\t\t\t\tscopes,\n\t\t\t\t\t\tcreatedBy: actorOf(ctx),\n\t\t\t\t\t\thours,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.CREATED, 'INVITE_CREATED', 'Invite link — shown once', {\n\t\t\t\t\t\tid: l.id,\n\t\t\t\t\t\temail,\n\t\t\t\t\t\tscopes,\n\t\t\t\t\t\texpiresAt: l.expiresAt,\n\t\t\t\t\t\ttoken: l.token,\n\t\t\t\t\t\turl: link(ctx, l.token),\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t],\n\t\t\t'root',\n\t\t],\n\t\t[\n\t\t\t// Lost password or lost authenticator: a single-use link that sets a new\n\t\t\t// password and makes them enroll again. Signs them out everywhere.\n\t\t\t'POST',\n\t\t\t'/_admin/access/operators/:id/recovery',\n\t\t\t[\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst op = await findOperator(store, { id: idOf(ctx) });\n\t\t\t\t\tif (!op) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such operator');\n\t\t\t\t\tawait deleteOperatorSessions(store, op.id);\n\t\t\t\t\tconst l = await createLink(store, {\n\t\t\t\t\t\tkind: 'recovery',\n\t\t\t\t\t\temail: op.email,\n\t\t\t\t\t\toperatorId: op.id,\n\t\t\t\t\t\tcreatedBy: actorOf(ctx),\n\t\t\t\t\t\thours: RECOVERY_HOURS,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.CREATED, 'RECOVERY_CREATED', 'Recovery link — shown once', {\n\t\t\t\t\t\tid: l.id,\n\t\t\t\t\t\temail: op.email,\n\t\t\t\t\t\texpiresAt: l.expiresAt,\n\t\t\t\t\t\ttoken: l.token,\n\t\t\t\t\t\turl: link(ctx, l.token),\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t],\n\t\t\t'root',\n\t\t],\n\t\t[\n\t\t\t'PUT',\n\t\t\t'/_admin/access/operators/:id',\n\t\t\t[\n\t\t\t\tvalidate(updateOperatorSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst op = await findOperator(store, { id: idOf(ctx) });\n\t\t\t\t\tif (!op) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such operator');\n\t\t\t\t\tconst scopes = b['scopes'] === undefined ? op.scopes : validScopes(b['scopes']);\n\t\t\t\t\tif (!scopes)\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t\t'INVALID',\n\t\t\t\t\t\t\t`scopes must be a non-empty list of ${SCOPES.join(', ')}`,\n\t\t\t\t\t\t);\n\t\t\t\t\tif (\n\t\t\t\t\t\tctx.meta['adminOperator'] === op.email &&\n\t\t\t\t\t\t!scopes.includes('secrets') &&\n\t\t\t\t\t\top.scopes.includes('secrets')\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t'SELF_DEMOTION',\n\t\t\t\t\t\t\t'You cannot remove your own highest scope. Ask another operator.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst disabled = b['disabled'];\n\t\t\t\t\tif (disabled === true && ctx.meta['adminOperator'] === op.email) {\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t'SELF_DISABLE',\n\t\t\t\t\t\t\t'You cannot disable yourself. Ask another operator.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst [row] = await store.query<IOperatorRow>(\n\t\t\t\t\t\t`UPDATE fonderie_admin_operators\n\t\t\t\t\t\t SET scopes = $2, name = coalesce($3, name),\n\t\t\t\t\t\t disabled_at = CASE WHEN $4::boolean IS NULL THEN disabled_at WHEN $4 THEN coalesce(disabled_at, now()) ELSE NULL END\n\t\t\t\t\t\t WHERE id = $1\n\t\t\t\t\t\t RETURNING id, email, name, password_hash AS \"passwordHash\", scopes, totp_secret AS \"totpSecret\",\n\t\t\t\t\t\t totp_confirmed_at AS \"totpConfirmedAt\", totp_last_step AS \"totpLastStep\", backup_codes AS \"backupCodes\",\n\t\t\t\t\t\t failed_attempts AS \"failedAttempts\", locked_until AS \"lockedUntil\", created_by AS \"createdBy\",\n\t\t\t\t\t\t created_at AS \"createdAt\", last_login_at AS \"lastLoginAt\", disabled_at AS \"disabledAt\"`,\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\top.id,\n\t\t\t\t\t\t\tscopes,\n\t\t\t\t\t\t\ttypeof b['name'] === 'string' ? b['name'] : null,\n\t\t\t\t\t\t\ttypeof disabled === 'boolean' ? disabled : null,\n\t\t\t\t\t\t],\n\t\t\t\t\t);\n\t\t\t\t\tif (!row) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such operator');\n\t\t\t\t\t// Any change of rights ends their sessions: new rights apply at next sign-in.\n\t\t\t\t\tawait deleteOperatorSessions(store, op.id);\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'OPERATOR_UPDATED',\n\t\t\t\t\t\t'Operator updated',\n\t\t\t\t\t\tpublicOperator(row),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t],\n\t\t\t'root',\n\t\t],\n\t\t[\n\t\t\t'DELETE',\n\t\t\t'/_admin/access/operators/links/:id',\n\t\t\t[\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst rows = await store.query(\n\t\t\t\t\t\t`UPDATE fonderie_admin_invites SET used_at = now() WHERE id = $1 AND used_at IS NULL RETURNING id`,\n\t\t\t\t\t\t[idOf(ctx)],\n\t\t\t\t\t);\n\t\t\t\t\treturn rows.length\n\t\t\t\t\t\t? setApiResponse(HTTP.OK, 'LINK_REVOKED', 'Link revoked')\n\t\t\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such live link');\n\t\t\t\t},\n\t\t\t],\n\t\t\t'root',\n\t\t],\n\t];\n}\n","import {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\tcreateHash,\n\tcreateHmac,\n\trandomBytes,\n\tscrypt as scryptCb,\n\ttimingSafeEqual,\n} from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\n// Everything an operator sign-in needs, on node:crypto alone. @fonderie/admin\n// takes no auth dependency: the operator surface must work in an app that\n// never installed @fonderie/auth, and must not share its user tables.\n\n// ── TOTP (RFC 6238, SHA-1, 6 digits, 30 s) ─────────────────────────────────\n// The algorithm @fonderie/auth ships, plus a REPLAY GUARD it lacks: the\n// matched time-step is returned so the caller can refuse it next time. A code\n// read over a shoulder is otherwise good for up to 90 seconds.\n\nconst STEP = 30;\nconst DRIFT = 1;\nconst DIGITS = 6;\nconst B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n\nexport function base32Encode(buf: Buffer): string {\n\tlet bits = 0;\n\tlet value = 0;\n\tlet out = '';\n\tfor (const byte of buf) {\n\t\tvalue = (value << 8) | byte;\n\t\tbits += 8;\n\t\twhile (bits >= 5) {\n\t\t\tout += B32[(value >>> (bits - 5)) & 31];\n\t\t\tbits -= 5;\n\t\t}\n\t}\n\tif (bits > 0) out += B32[(value << (5 - bits)) & 31];\n\treturn out;\n}\n\nexport function base32Decode(input: string): Buffer {\n\tlet bits = 0;\n\tlet value = 0;\n\tconst out: number[] = [];\n\tfor (const ch of input.toUpperCase().replace(/[=\\s]/g, '')) {\n\t\tconst i = B32.indexOf(ch);\n\t\tif (i === -1) continue;\n\t\tvalue = (value << 5) | i;\n\t\tbits += 5;\n\t\tif (bits >= 8) {\n\t\t\tout.push((value >>> (bits - 8)) & 255);\n\t\t\tbits -= 8;\n\t\t}\n\t}\n\treturn Buffer.from(out);\n}\n\nexport function hotp(secret: string, counter: number): string {\n\tconst msg = Buffer.alloc(8);\n\tmsg.writeBigUInt64BE(BigInt(counter));\n\tconst mac = createHmac('sha1', base32Decode(secret)).update(msg).digest();\n\tconst o = (mac[19] ?? 0) & 15;\n\tconst bin =\n\t\t(((mac[o] ?? 0) & 0x7f) << 24) |\n\t\t(((mac[o + 1] ?? 0) & 255) << 16) |\n\t\t(((mac[o + 2] ?? 0) & 255) << 8) |\n\t\t((mac[o + 3] ?? 0) & 255);\n\treturn String(bin % 10 ** DIGITS).padStart(DIGITS, '0');\n}\n\nexport const totpCounter = (now = Date.now()): number => Math.floor(now / 1000 / STEP);\n\n/**\n * The time-step the code matched, or null. A step at or below `lastUsed` is\n * refused even when the code is right — that is the replay guard.\n */\nexport function verifyTotp(\n\tsecret: string,\n\tcode: string,\n\tlastUsed: number | null,\n\tnow = Date.now(),\n): number | null {\n\tconst clean = code.replace(/\\s/g, '');\n\tif (!/^\\d{6}$/.test(clean)) return null;\n\tconst t = totpCounter(now);\n\tfor (let d = -DRIFT; d <= DRIFT; d++) {\n\t\tconst step = t + d;\n\t\tif (lastUsed !== null && step <= lastUsed) continue;\n\t\tif (constantTimeEqual(hotp(secret, step), clean)) return step;\n\t}\n\treturn null;\n}\n\nexport const newTotpSecret = (): string => base32Encode(randomBytes(20));\n\nexport function totpUri(issuer: string, account: string, secret: string): string {\n\tconst q = new URLSearchParams({\n\t\tsecret,\n\t\tissuer,\n\t\talgorithm: 'SHA1',\n\t\tdigits: String(DIGITS),\n\t\tperiod: String(STEP),\n\t});\n\treturn `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(account)}?${q}`;\n}\n\n// ── Backup codes ───────────────────────────────────────────────────────────\n// Ten single-use codes, shown once, stored as SHA-256. 10 base32 characters\n// (50 bits) each — long enough that the per-account lockout, not the code\n// space, is what stops guessing. Grouped for reading: ABCDE-FGHIJ.\n\nexport function newBackupCodes(count = 10): string[] {\n\treturn Array.from({ length: count }, () => {\n\t\tconst s = base32Encode(randomBytes(7)).slice(0, 10);\n\t\treturn `${s.slice(0, 5)}-${s.slice(5)}`;\n\t});\n}\n\nexport const normalizeBackupCode = (code: string): string =>\n\tcode.toUpperCase().replace(/[^A-Z2-7]/g, '');\n\nexport const sha256 = (s: string): string => createHash('sha256').update(s).digest('hex');\n\n// ── Passwords: scrypt ──────────────────────────────────────────────────────\n// Stored as `scrypt$N$r$p$salt$hash` so parameters can be raised later and\n// old hashes still verify. N=2^15, r=8 — the OWASP floor for scrypt.\n\nconst N = 2 ** 15;\nconst R = 8;\nconst P = 1;\nconst KEYLEN = 32;\n\nconst scrypt = (password: string, salt: Buffer, n: number, r: number, p: number) =>\n\tnew Promise<Buffer>((resolve, reject) =>\n\t\tscryptCb(\n\t\t\tpassword.normalize('NFKC'),\n\t\t\tsalt,\n\t\t\tKEYLEN,\n\t\t\t{ N: n, r, p, maxmem: 128 * n * r * 2 },\n\t\t\t(err, key) => (err ? reject(err) : resolve(key)),\n\t\t),\n\t);\n\nexport async function hashPassword(password: string): Promise<string> {\n\tconst salt = randomBytes(16);\n\tconst key = await scrypt(password, salt, N, R, P);\n\treturn `scrypt$${N}$${R}$${P}$${salt.toString('base64')}$${key.toString('base64')}`;\n}\n\nexport async function verifyPassword(password: string, stored: string): Promise<boolean> {\n\tconst [kind, n, r, p, salt, hash] = stored.split('$');\n\tif (kind !== 'scrypt' || !n || !r || !p || !salt || !hash) return false;\n\tconst expected = Buffer.from(hash, 'base64');\n\tconst key = await scrypt(password, Buffer.from(salt, 'base64'), Number(n), Number(r), Number(p));\n\treturn key.length === expected.length && timingSafeEqual(key, expected);\n}\n\n// A hash of nothing, verified against when the email is unknown, so a wrong\n// address costs the same time as a wrong password and the response timing\n// does not reveal who is an operator.\nlet dummy: Promise<string> | null = null;\nexport const dummyHash = (): Promise<string> => {\n\tdummy ??= hashPassword(randomBytes(16).toString('hex'));\n\treturn dummy;\n};\n\n// 12 characters minimum and not trivially guessable. Deliberately no\n// composition rules (NIST 800-63B): length is what matters, and the second\n// factor is mandatory anyway.\nexport function passwordProblem(password: unknown): string | null {\n\tif (typeof password !== 'string' || password.length < 12) return 'Use at least 12 characters.';\n\tif (password.length > 256) return 'Use at most 256 characters.';\n\tif (/^(.)\\1+$/.test(password)) return 'That password is too easy to guess.';\n\treturn null;\n}\n\n// ── TOTP secret at rest ────────────────────────────────────────────────────\n// A TOTP secret must stay reversible, so it is encrypted, not hashed, with the\n// app's operatorKey (64 hex). Without a key it is stored as-is and readiness\n// says so. Same `v1:iv:tag:data` shape as @fonderie/auth's MFA cipher.\n\nconst PREFIX = 'op.v1:';\n\nexport interface ISecretBox {\n\tseal(plain: string): string;\n\topen(stored: string): string;\n}\n\nexport function secretBox(keyHex: string | undefined): ISecretBox {\n\tif (!keyHex) return { seal: (p) => p, open: (s) => s };\n\tconst key = Buffer.from(keyHex, 'hex');\n\tif (key.length !== 32)\n\t\tthrow new Error('[admin] operatorKey must be 32 bytes — 64 hex chars (`openssl rand -hex 32`)');\n\treturn {\n\t\tseal(plain) {\n\t\t\tconst iv = randomBytes(12);\n\t\t\tconst c = createCipheriv('aes-256-gcm', key, iv);\n\t\t\tconst data = Buffer.concat([c.update(plain, 'utf8'), c.final()]);\n\t\t\treturn `${PREFIX}${iv.toString('hex')}:${c.getAuthTag().toString('hex')}:${data.toString('hex')}`;\n\t\t},\n\t\topen(stored) {\n\t\t\tif (!stored.startsWith(PREFIX)) return stored;\n\t\t\tconst [iv, tag, data] = stored.slice(PREFIX.length).split(':');\n\t\t\tif (!iv || !tag || !data) throw new Error('[admin] malformed operator secret');\n\t\t\tconst d = createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));\n\t\t\td.setAuthTag(Buffer.from(tag, 'hex'));\n\t\t\treturn Buffer.concat([d.update(Buffer.from(data, 'hex')), d.final()]).toString('utf8');\n\t\t},\n\t};\n}\n\n/** An opaque random credential: a session id or an invite token. */\nexport const newOpaqueToken = (prefix: string): string =>\n\t`${prefix}_${randomBytes(32).toString('base64url')}`;\n","import type { IRequestSchema } from '@fonderie/core/middlewares';\n\nimport { SCOPES } from '../tokens';\n\n// Envelope validation for the operator routes: types, lengths, unknown keys\n// stripped. The judgement calls that deserve their own message (password\n// strength, email shape) stay in the handlers, which answer with a specific\n// reason rather than a generic INVALID_PARAMETER.\n\ntype Field =\n\t| { type: 'string'; required?: boolean; max: number }\n\t| { type: 'boolean'; required?: boolean }\n\t| { type: 'integer'; required?: boolean; min: number; max: number }\n\t| { type: 'scopes'; required?: boolean };\n\nfunction schema(fields: Record<string, Field>, opts: { oneOf?: string[] } = {}): IRequestSchema {\n\treturn {\n\t\tsafeParse(input: unknown) {\n\t\t\tconst fail = (path: string, message: string) => ({\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t\t});\n\t\t\tconst b = (input && typeof input === 'object' ? input : {}) as Record<string, unknown>;\n\t\t\tconst out: Record<string, unknown> = {};\n\t\t\tfor (const [key, f] of Object.entries(fields)) {\n\t\t\t\tconst v = b[key];\n\t\t\t\tif (v === undefined || v === null) {\n\t\t\t\t\tif (f.required) return fail(key, `${key} is required`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (f.type === 'string') {\n\t\t\t\t\tif (typeof v !== 'string' || v.length === 0 || v.length > f.max)\n\t\t\t\t\t\treturn fail(key, `${key} must be a string of 1–${f.max} characters`);\n\t\t\t\t} else if (f.type === 'boolean') {\n\t\t\t\t\tif (typeof v !== 'boolean') return fail(key, `${key} must be true or false`);\n\t\t\t\t} else if (f.type === 'integer') {\n\t\t\t\t\tif (!Number.isInteger(v) || (v as number) < f.min || (v as number) > f.max)\n\t\t\t\t\t\treturn fail(key, `${key} must be an integer ${f.min}–${f.max}`);\n\t\t\t\t} else if (!Array.isArray(v) || v.length === 0 || !v.every((s) => SCOPES.includes(s))) {\n\t\t\t\t\treturn fail(key, `${key} must be a non-empty list of ${SCOPES.join(', ')}`);\n\t\t\t\t}\n\t\t\t\tout[key] = v;\n\t\t\t}\n\t\t\tif (opts.oneOf && !opts.oneOf.some((k) => out[k] !== undefined)) {\n\t\t\t\treturn fail(opts.oneOf[0] ?? 'body', `one of ${opts.oneOf.join(', ')} is required`);\n\t\t\t}\n\t\t\treturn { success: true as const, data: out };\n\t\t},\n\t};\n}\n\nconst email = { type: 'string', required: true, max: 320 } as const;\nconst password = { type: 'string', required: true, max: 256 } as const;\nconst name = { type: 'string', max: 200 } as const;\nconst token = { type: 'string', required: true, max: 200 } as const;\n\nexport const claimSchema = schema({ email, password, name });\nexport const loginSchema = schema({ email, password });\nexport const enrollmentSchema = schema({ code: { type: 'string', required: true, max: 12 } });\nexport const factorSchema = schema(\n\t{ code: { type: 'string', max: 12 }, backupCode: { type: 'string', max: 32 } },\n\t{ oneOf: ['code', 'backupCode'] },\n);\nexport const inspectLinkSchema = schema({ token });\nexport const redeemLinkSchema = schema({ token, password, name });\nexport const inviteSchema = schema({\n\temail,\n\tscopes: { type: 'scopes', required: true },\n\texpiresInHours: { type: 'integer', min: 1, max: 336 },\n});\nexport const updateOperatorSchema = schema({\n\tscopes: { type: 'scopes' },\n\tname,\n\tdisabled: { type: 'boolean' },\n});\n","import type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { AdminScope } from '../types';\nimport {\n\ttype ISecretBox,\n\tdummyHash,\n\thashPassword,\n\tnewBackupCodes,\n\tnewOpaqueToken,\n\tnewTotpSecret,\n\tnormalizeBackupCode,\n\tsha256,\n\tverifyPassword,\n\tverifyTotp,\n} from './crypto';\n\n// Session policy. Short on purpose: this is the surface that can reveal every\n// secret the app holds.\nexport const IDLE_MS = 30 * 60_000; // signed out after 30 minutes untouched\nexport const ABSOLUTE_MS = 12 * 3_600_000; // and after 12 hours regardless\nexport const PENDING_MS = 10 * 60_000; // password checked, code not yet: 10 minutes\nexport const STEP_UP_MS = 5 * 60_000; // a fresh code covers dangerous actions for 5 minutes\nexport const INVITE_HOURS = 72;\nexport const RECOVERY_HOURS = 24;\nexport const MAX_FAILURES = 5; // then locked, doubling from 1 minute, capped at an hour\n\nexport type SessionStage = 'password' | 'enroll' | 'active';\n\nexport interface IOperatorRow {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\tpasswordHash: string;\n\tscopes: AdminScope[];\n\ttotpSecret: string | null;\n\ttotpConfirmedAt: string | null;\n\ttotpLastStep: string | null; // BIGINT arrives as a string\n\tbackupCodes: string[];\n\tfailedAttempts: number;\n\tlockedUntil: string | null;\n\tcreatedBy: string;\n\tcreatedAt: string;\n\tlastLoginAt: string | null;\n\tdisabledAt: string | null;\n}\n\nexport interface ISessionRow {\n\tidHash: string;\n\toperatorId: string;\n\tstage: SessionStage;\n\tcreatedAt: string;\n\tlastSeenAt: string;\n\texpiresAt: string;\n\tstepUpAt: string | null;\n}\n\nconst OP_COLS = `id, email, name, password_hash AS \"passwordHash\", scopes, totp_secret AS \"totpSecret\",\n totp_confirmed_at AS \"totpConfirmedAt\", totp_last_step AS \"totpLastStep\", backup_codes AS \"backupCodes\",\n failed_attempts AS \"failedAttempts\", locked_until AS \"lockedUntil\", created_by AS \"createdBy\",\n created_at AS \"createdAt\", last_login_at AS \"lastLoginAt\", disabled_at AS \"disabledAt\"`;\n\nconst SESSION_COLS = `id_hash AS \"idHash\", operator_id AS \"operatorId\", stage, created_at AS \"createdAt\",\n last_seen_at AS \"lastSeenAt\", expires_at AS \"expiresAt\", step_up_at AS \"stepUpAt\"`;\n\nexport const normalizeEmail = (e: string): string => e.trim().toLowerCase();\nexport const EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n// ── operators ──────────────────────────────────────────────────────────────\n\nexport async function operatorCount(store: IStoreAdapter): Promise<number> {\n\tconst [row] = await store.query<{ n: string }>(\n\t\t`SELECT count(*)::text AS n FROM fonderie_admin_operators`,\n\t);\n\treturn Number(row?.n ?? 0);\n}\n\nexport async function findOperator(\n\tstore: IStoreAdapter,\n\tby: { id?: string; email?: string },\n): Promise<IOperatorRow | null> {\n\tconst [row] = by.id\n\t\t? await store.query<IOperatorRow>(\n\t\t\t\t`SELECT ${OP_COLS} FROM fonderie_admin_operators WHERE id = $1`,\n\t\t\t\t[by.id],\n\t\t\t)\n\t\t: await store.query<IOperatorRow>(\n\t\t\t\t`SELECT ${OP_COLS} FROM fonderie_admin_operators WHERE email = $1`,\n\t\t\t\t[normalizeEmail(by.email ?? '')],\n\t\t\t);\n\treturn row ?? null;\n}\n\n/**\n * The one-time claim: creates the first operator, with every scope, only while\n * none exists. Serialized with an advisory lock so two simultaneous claims\n * cannot both succeed.\n */\nexport async function claimFirstOperator(\n\tstore: IStoreAdapter,\n\tinput: { email: string; name?: string | undefined; password: string },\n): Promise<IOperatorRow | null> {\n\tconst passwordHash = await hashPassword(input.password);\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(`SELECT pg_advisory_xact_lock(hashtext('fonderie_admin_claim'))`);\n\t\tconst [row] = await tx.query<IOperatorRow>(\n\t\t\t`INSERT INTO fonderie_admin_operators (email, name, password_hash, scopes, created_by)\n\t\t\t SELECT $1, $2, $3, $4, 'claim'\n\t\t\t WHERE NOT EXISTS (SELECT 1 FROM fonderie_admin_operators)\n\t\t\t RETURNING ${OP_COLS}`,\n\t\t\t[normalizeEmail(input.email), input.name ?? null, passwordHash, ['read', 'write', 'secrets']],\n\t\t);\n\t\treturn row ?? null;\n\t});\n}\n\nexport async function recordFailure(store: IStoreAdapter, id: string): Promise<void> {\n\t// Past the limit, lock for 2^(n-limit) minutes, capped at an hour.\n\tawait store.query(\n\t\t`UPDATE fonderie_admin_operators\n\t\t SET failed_attempts = failed_attempts + 1,\n\t\t locked_until = CASE WHEN failed_attempts + 1 >= $2\n\t\t THEN now() + make_interval(mins => LEAST(60, power(2, failed_attempts + 1 - $2)::int))\n\t\t ELSE locked_until END\n\t\t WHERE id = $1`,\n\t\t[id, MAX_FAILURES],\n\t);\n}\n\nexport const isLocked = (op: IOperatorRow): boolean =>\n\top.lockedUntil !== null && new Date(op.lockedUntil).getTime() > Date.now();\n\nexport function lockMinutes(op: IOperatorRow): number {\n\treturn op.lockedUntil\n\t\t? Math.max(1, Math.ceil((new Date(op.lockedUntil).getTime() - Date.now()) / 60_000))\n\t\t: 0;\n}\n\n/** Password check that costs the same whether or not the email exists. */\nexport async function checkPassword(store: IStoreAdapter, email: string, password: string) {\n\tconst op = await findOperator(store, { email });\n\tif (!op) {\n\t\tawait verifyPassword(password, await dummyHash());\n\t\treturn { op: null, ok: false } as const;\n\t}\n\tif (op.disabledAt) {\n\t\tawait verifyPassword(password, await dummyHash());\n\t\treturn { op: null, ok: false } as const;\n\t}\n\tif (isLocked(op)) return { op, ok: false, locked: true } as const;\n\tconst ok = await verifyPassword(password, op.passwordHash);\n\tif (!ok) await recordFailure(store, op.id);\n\treturn { op, ok } as const;\n}\n\n/** A time-step or a backup code. Consumes the backup code; records the step. */\nexport async function checkSecondFactor(\n\tstore: IStoreAdapter,\n\tbox: ISecretBox,\n\top: IOperatorRow,\n\tinput: { code?: unknown; backupCode?: unknown },\n): Promise<{ ok: boolean; via?: 'totp' | 'backup'; backupLeft?: number }> {\n\tif (isLocked(op)) return { ok: false };\n\tif (typeof input.code === 'string' && op.totpSecret) {\n\t\tconst last = op.totpLastStep === null ? null : Number(op.totpLastStep);\n\t\tconst step = verifyTotp(box.open(op.totpSecret), input.code, last);\n\t\tif (step !== null) {\n\t\t\t// Conditional on the step still being newer: two requests racing with\n\t\t\t// the same code cannot both pass.\n\t\t\tconst rows = await store.query(\n\t\t\t\t`UPDATE fonderie_admin_operators SET totp_last_step = $2, failed_attempts = 0, locked_until = NULL\n\t\t\t\t WHERE id = $1 AND (totp_last_step IS NULL OR totp_last_step < $2) RETURNING id`,\n\t\t\t\t[op.id, step],\n\t\t\t);\n\t\t\tif (rows.length) return { ok: true, via: 'totp' };\n\t\t}\n\t} else if (typeof input.backupCode === 'string') {\n\t\tconst h = sha256(normalizeBackupCode(input.backupCode));\n\t\tconst [row] = await store.query<{ left: number | null }>(\n\t\t\t`UPDATE fonderie_admin_operators\n\t\t\t SET backup_codes = array_remove(backup_codes, $2), failed_attempts = 0, locked_until = NULL\n\t\t\t WHERE id = $1 AND $2 = ANY(backup_codes)\n\t\t\t RETURNING coalesce(array_length(backup_codes, 1), 0) AS left`,\n\t\t\t[op.id, h],\n\t\t);\n\t\tif (row) return { ok: true, via: 'backup', backupLeft: Number(row.left ?? 0) };\n\t}\n\tawait recordFailure(store, op.id);\n\treturn { ok: false };\n}\n\n/** The secret to show while enrolling. Created once, kept until confirmed. */\nexport async function enrollmentSecret(\n\tstore: IStoreAdapter,\n\tbox: ISecretBox,\n\top: IOperatorRow,\n): Promise<string> {\n\tif (op.totpSecret && !op.totpConfirmedAt) return box.open(op.totpSecret);\n\tconst secret = newTotpSecret();\n\tawait store.query(\n\t\t`UPDATE fonderie_admin_operators SET totp_secret = $2 WHERE id = $1 AND totp_confirmed_at IS NULL`,\n\t\t[op.id, box.seal(secret)],\n\t);\n\treturn secret;\n}\n\nexport async function confirmEnrollment(\n\tstore: IStoreAdapter,\n\tbox: ISecretBox,\n\top: IOperatorRow,\n\tcode: string,\n): Promise<string[] | null> {\n\tif (!op.totpSecret || op.totpConfirmedAt || isLocked(op)) return null;\n\tconst step = verifyTotp(box.open(op.totpSecret), code, null);\n\tif (step === null) {\n\t\tawait recordFailure(store, op.id);\n\t\treturn null;\n\t}\n\tconst codes = newBackupCodes();\n\tawait store.query(\n\t\t`UPDATE fonderie_admin_operators\n\t\t SET totp_confirmed_at = now(), totp_last_step = $2, backup_codes = $3,\n\t\t failed_attempts = 0, locked_until = NULL, last_login_at = now()\n\t\t WHERE id = $1`,\n\t\t[op.id, step, codes.map((c) => sha256(normalizeBackupCode(c)))],\n\t);\n\treturn codes;\n}\n\nexport async function listOperators(store: IStoreAdapter) {\n\tconst operators = await store.query<IOperatorRow>(\n\t\t`SELECT ${OP_COLS} FROM fonderie_admin_operators ORDER BY created_at`,\n\t);\n\tconst links = await store.query<{\n\t\tid: string;\n\t\tkind: string;\n\t\temail: string;\n\t\tscopes: AdminScope[];\n\t\tcreatedBy: string;\n\t\tcreatedAt: string;\n\t\texpiresAt: string;\n\t}>(\n\t\t`SELECT id, kind, email, scopes, created_by AS \"createdBy\", created_at AS \"createdAt\", expires_at AS \"expiresAt\"\n\t\t FROM fonderie_admin_invites WHERE used_at IS NULL AND expires_at > now() ORDER BY created_at DESC`,\n\t);\n\treturn { operators: operators.map(publicOperator), links };\n}\n\n/** What the API ever says about an operator: never a hash, secret or code. */\nexport function publicOperator(op: IOperatorRow) {\n\treturn {\n\t\tid: op.id,\n\t\temail: op.email,\n\t\tname: op.name,\n\t\tscopes: op.scopes,\n\t\tenrolled: op.totpConfirmedAt !== null,\n\t\tbackupCodesLeft: op.backupCodes.length,\n\t\tlocked: isLocked(op),\n\t\tcreatedBy: op.createdBy,\n\t\tcreatedAt: op.createdAt,\n\t\tlastLoginAt: op.lastLoginAt,\n\t\tdisabledAt: op.disabledAt,\n\t};\n}\n\n// ── links (invite / recovery) ──────────────────────────────────────────────\n\nexport async function createLink(\n\tstore: IStoreAdapter,\n\tinput: {\n\t\tkind: 'invite' | 'recovery';\n\t\temail: string;\n\t\tscopes?: AdminScope[];\n\t\toperatorId?: string;\n\t\tcreatedBy: string;\n\t\thours: number;\n\t},\n): Promise<{ id: string; token: string; expiresAt: string }> {\n\tconst token = newOpaqueToken(input.kind === 'invite' ? 'fai' : 'far');\n\tif (input.operatorId) {\n\t\t// A new recovery link retires any earlier one for the same operator.\n\t\tawait store.query(\n\t\t\t`UPDATE fonderie_admin_invites SET used_at = now() WHERE operator_id = $1 AND used_at IS NULL`,\n\t\t\t[input.operatorId],\n\t\t);\n\t}\n\tconst [row] = await store.query<{ id: string; expiresAt: string }>(\n\t\t`INSERT INTO fonderie_admin_invites (kind, token_hash, email, scopes, operator_id, created_by, expires_at)\n\t\t VALUES ($1, $2, $3, $4, $5, $6, now() + make_interval(hours => $7)) RETURNING id, expires_at AS \"expiresAt\"`,\n\t\t[\n\t\t\tinput.kind,\n\t\t\tsha256(token),\n\t\t\tnormalizeEmail(input.email),\n\t\t\tinput.scopes ?? [],\n\t\t\tinput.operatorId ?? null,\n\t\t\tinput.createdBy,\n\t\t\tinput.hours,\n\t\t],\n\t);\n\tif (!row) throw new Error('[admin] link insert returned no row');\n\treturn { id: row.id, token, expiresAt: row.expiresAt };\n}\n\nexport async function findLink(store: IStoreAdapter, token: string) {\n\tconst [row] = await store.query<{\n\t\tid: string;\n\t\tkind: 'invite' | 'recovery';\n\t\temail: string;\n\t\tscopes: AdminScope[];\n\t\toperatorId: string | null;\n\t\tcreatedBy: string;\n\t}>(\n\t\t`SELECT id, kind, email, scopes, operator_id AS \"operatorId\", created_by AS \"createdBy\"\n\t\t FROM fonderie_admin_invites WHERE token_hash = $1 AND used_at IS NULL AND expires_at > now()`,\n\t\t[sha256(token)],\n\t);\n\treturn row ?? null;\n}\n\n/**\n * Redeem a link: an invite creates the operator, a recovery resets password and\n * second factor. Single-use, atomically. Returns the operator, or a reason.\n */\nexport async function redeemLink(\n\tstore: IStoreAdapter,\n\ttoken: string,\n\tinput: { password: string; name?: string | undefined },\n): Promise<{ op: IOperatorRow } | { error: 'INVALID_LINK' | 'ALREADY_OPERATOR' }> {\n\tconst passwordHash = await hashPassword(input.password);\n\treturn store\n\t\t.transaction(async (tx) => {\n\t\t\tconst [link] = await tx.query<{\n\t\t\t\tkind: string;\n\t\t\t\temail: string;\n\t\t\t\tscopes: AdminScope[];\n\t\t\t\toperatorId: string | null;\n\t\t\t\tcreatedBy: string;\n\t\t\t}>(\n\t\t\t\t`UPDATE fonderie_admin_invites SET used_at = now()\n\t\t\t WHERE token_hash = $1 AND used_at IS NULL AND expires_at > now()\n\t\t\t RETURNING kind, email, scopes, operator_id AS \"operatorId\", created_by AS \"createdBy\"`,\n\t\t\t\t[sha256(token)],\n\t\t\t);\n\t\t\tif (!link) return { error: 'INVALID_LINK' as const };\n\t\t\tif (link.kind === 'invite') {\n\t\t\t\tconst [op] = await tx.query<IOperatorRow>(\n\t\t\t\t\t`INSERT INTO fonderie_admin_operators (email, name, password_hash, scopes, created_by)\n\t\t\t\t VALUES ($1, $2, $3, $4, $5) ON CONFLICT (email) DO NOTHING RETURNING ${OP_COLS}`,\n\t\t\t\t\t[link.email, input.name ?? null, passwordHash, link.scopes, link.createdBy],\n\t\t\t\t);\n\t\t\t\tif (!op)\n\t\t\t\t\tthrow Object.assign(new Error('already an operator'), { code: 'ALREADY_OPERATOR' });\n\t\t\t\treturn { op };\n\t\t\t}\n\t\t\tconst [op] = await tx.query<IOperatorRow>(\n\t\t\t\t`UPDATE fonderie_admin_operators\n\t\t\t SET password_hash = $2, totp_secret = NULL, totp_confirmed_at = NULL, totp_last_step = NULL,\n\t\t\t backup_codes = '{}', failed_attempts = 0, locked_until = NULL\n\t\t\t WHERE id = $1 AND disabled_at IS NULL RETURNING ${OP_COLS}`,\n\t\t\t\t[link.operatorId, passwordHash],\n\t\t\t);\n\t\t\tif (!op) return { error: 'INVALID_LINK' as const };\n\t\t\tawait tx.query(`DELETE FROM fonderie_admin_sessions WHERE operator_id = $1`, [op.id]);\n\t\t\treturn { op };\n\t\t})\n\t\t.catch((err: unknown) => {\n\t\t\tif ((err as { code?: string }).code === 'ALREADY_OPERATOR')\n\t\t\t\treturn { error: 'ALREADY_OPERATOR' as const };\n\t\t\tthrow err;\n\t\t});\n}\n\n// ── sessions ───────────────────────────────────────────────────────────────\n\nexport async function createSession(\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\toperatorId: string,\n\tstage: SessionStage,\n): Promise<string> {\n\tconst id = newOpaqueToken('fas');\n\tconst ttl = stage === 'active' ? ABSOLUTE_MS : PENDING_MS;\n\tawait store.query(\n\t\t`INSERT INTO fonderie_admin_sessions (id_hash, operator_id, stage, expires_at, client_ip, user_agent)\n\t\t VALUES ($1, $2, $3, now() + make_interval(secs => $4), $5, $6)`,\n\t\t[\n\t\t\tsha256(id),\n\t\t\toperatorId,\n\t\t\tstage,\n\t\t\tttl / 1000,\n\t\t\tctx.meta.clientIp ?? null,\n\t\t\tctx.request.headers.get('user-agent')?.slice(0, 300) ?? null,\n\t\t],\n\t);\n\treturn id;\n}\n\nexport async function deleteSession(store: IStoreAdapter, id: string): Promise<void> {\n\tawait store.query(`DELETE FROM fonderie_admin_sessions WHERE id_hash = $1`, [sha256(id)]);\n}\n\nexport async function deleteOperatorSessions(\n\tstore: IStoreAdapter,\n\toperatorId: string,\n): Promise<void> {\n\tawait store.query(`DELETE FROM fonderie_admin_sessions WHERE operator_id = $1`, [operatorId]);\n}\n\n/** The session behind a cookie value, if it is still alive (absolute and idle). */\nexport async function readSession(\n\tstore: IStoreAdapter,\n\tid: string,\n): Promise<{ session: ISessionRow; op: IOperatorRow } | null> {\n\tif (!id) return null;\n\tconst [session] = await store.query<ISessionRow>(\n\t\t`SELECT ${SESSION_COLS} FROM fonderie_admin_sessions WHERE id_hash = $1 AND expires_at > now()`,\n\t\t[sha256(id)],\n\t);\n\tif (!session) return null;\n\tif (session.stage === 'active' && Date.now() - new Date(session.lastSeenAt).getTime() > IDLE_MS) {\n\t\tawait store.query(`DELETE FROM fonderie_admin_sessions WHERE id_hash = $1`, [session.idHash]);\n\t\treturn null;\n\t}\n\tconst op = await findOperator(store, { id: session.operatorId });\n\tif (!op || op.disabledAt) return null;\n\t// Touch at most once a minute: idle tracking without a write per request.\n\tif (Date.now() - new Date(session.lastSeenAt).getTime() > 60_000) {\n\t\tvoid store\n\t\t\t.query(`UPDATE fonderie_admin_sessions SET last_seen_at = now() WHERE id_hash = $1`, [\n\t\t\t\tsession.idHash,\n\t\t\t])\n\t\t\t.catch(() => undefined);\n\t}\n\treturn { session, op };\n}\n\nexport const stepUpFresh = (s: ISessionRow): boolean =>\n\ts.stepUpAt !== null && Date.now() - new Date(s.stepUpAt).getTime() < STEP_UP_MS;\n\nexport async function markStepUp(store: IStoreAdapter, idHash: string): Promise<void> {\n\tawait store.query(`UPDATE fonderie_admin_sessions SET step_up_at = now() WHERE id_hash = $1`, [\n\t\tidHash,\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/* ── Console chrome. Pseudo-selectors the inline-styled screens cannot carry.\n Each one only refines: embedded without this shell, a screen loses hover\n states and keeps working. ── */\nbutton{transition:background-color .12s,border-color .12s,color .12s,opacity .12s}\nbutton:disabled{opacity:.5;cursor:default}\n.fonderie-admin-main button:not(:disabled):hover{opacity:.82}\n.fonderie-admin-main button:not(:disabled):active{opacity:.7}\n.fonderie-admin-nav-item:hover{background:color-mix(in srgb,var(--fonderie-text) 5%,transparent)!important;color:var(--fonderie-text)!important}\n.fonderie-admin-nav-item[aria-current=\"page\"]:hover{background:color-mix(in srgb,var(--fonderie-accent) 16%,transparent)!important}\n/* Tables and lists are framed cards; the frame is the last row's rule. */\n.fonderie-admin-main tbody tr:last-child>td{border-bottom:none!important}\n.fonderie-admin-main ul>li:last-child{border-bottom:none!important}\n.fonderie-admin-main tbody tr:hover>td{background:color-mix(in srgb,var(--fonderie-text) 2.5%,transparent)}\n/* Inputs carry their border inline, so focus is a ring, not a border change. */\ninput:focus-visible,select:focus-visible,textarea:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--fonderie-accent) 30%,transparent)}\ninput::placeholder,textarea::placeholder{color:var(--fonderie-text-muted);opacity:.75}\n/* In the sidebar footer the switcher spans the column. */\nnav .theme-switch{display:flex;width:100%;box-sizing:border-box}\nnav .theme-switch__option{flex:1;justify-content:center;padding:5px 6px}\n@media (max-width:820px){.fonderie-admin-main>div{padding:20px 16px 48px!important}}\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 =\n\t`(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","import_middlewares","module","module","name","module","name","name","import_node_crypto","import_core","import_core","import_core","password","scryptCb","email","password","token","op","email","token","token","bearer","name","token","body","name","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/operators/http.ts","../src/operators/crypto.ts","../src/operators/schemas.ts","../src/operators/service.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 { secretBox } from './operators/crypto';\nimport { needsStepUp, operatorAdminRoutes, sessionRoutes } from './operators/http';\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\t// Operator accounts ride on the store: sessions, lockouts and links are rows.\n\t\tconst operatorsOn = Boolean(store) && this.options.operators !== false;\n\t\tconst operatorDeps =\n\t\t\tstore && operatorsOn\n\t\t\t\t? {\n\t\t\t\t\t\tstore,\n\t\t\t\t\t\tbox: secretBox(this.options.operatorKey),\n\t\t\t\t\t\trootToken: token,\n\t\t\t\t\t\t// From the request, so a basePath in front of the surface is kept.\n\t\t\t\t\t\tuiPath: (ctx: { request: Request }) => {\n\t\t\t\t\t\t\tconst p = new URL(ctx.request.url).pathname;\n\t\t\t\t\t\t\tconst i = p.indexOf(this.path);\n\t\t\t\t\t\t\treturn `${i >= 0 ? p.slice(0, i + this.path.length) : this.path}/ui`;\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: null;\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 need = needed ?? scopeFor(method, path);\n\t\t\tconst guard = requireAdminScope(token, store, need, {\n\t\t\t\tstepUp: needsStepUp(method, path, need),\n\t\t\t\toperators: operatorsOn,\n\t\t\t});\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\t// The version in the URL: the script is cached for minutes, so a\n\t\t\t\t\t// page loaded after a deploy must not reuse the previous bundle.\n\t\t\t\t\treturn new Response(uiHtml(`${here}/app.js?v=${encodeURIComponent(this.version)}`), {\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\tif (operatorDeps && store) {\n\t\t\t// The sign-in routes sit OUTSIDE the guard — they are how a session that\n\t\t\t// passes it comes to exist — but inside the host guard and the log.\n\t\t\tfor (const [method, path, handlers] of sessionRoutes(operatorDeps)) {\n\t\t\t\tconst full = at(path);\n\t\t\t\tmounted.set(`${method} ${full}`, this.name);\n\t\t\t\tapp.addRoute(\n\t\t\t\t\tmethod,\n\t\t\t\t\tfull,\n\t\t\t\t\tadminLog(store, full, this.name),\n\t\t\t\t\t...(hostGuard ? [hostGuard] : []),\n\t\t\t\t\t...handlers,\n\t\t\t\t);\n\t\t\t}\n\t\t\tfor (const [method, path, handlers, needed] of operatorAdminRoutes(operatorDeps))\n\t\t\t\tmount(this.name, method, at(path), handlers, needed);\n\t\t}\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\tconst problems = validateAdminToken(this.options.adminToken, { module: this.name });\n\t\tconst operatorsOn = Boolean(this.options.store) && this.options.operators !== false;\n\t\tif (\n\t\t\toperatorsOn &&\n\t\t\tthis.options.operatorKey &&\n\t\t\t!/^[0-9a-fA-F]{64}$/.test(this.options.operatorKey)\n\t\t) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: this.name,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'operatorKey must be 64 hex characters (`openssl rand -hex 32`)',\n\t\t\t});\n\t\t}\n\t\t// No key is a supported setup, not a problem: authenticator secrets are\n\t\t// stored as-is (as django-otp and many SaaS do), and a leaked one alone\n\t\t// opens nothing — the operator's scrypt-hashed password is still needed.\n\t\t// operatorKey is optional hardening against a database leak.\n\t\treturn problems;\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\t// An operator session names itself and cannot be overridden: X-Actor\n\t\t\t// is a client-supplied label, fine for a token, not for a person.\n\t\t\tactor:\n\t\t\t\t(typeof ctx.meta['adminOperator'] === 'string'\n\t\t\t\t\t? `operator:${ctx.meta['adminOperator']}`\n\t\t\t\t\t: null) ||\n\t\t\t\t(typeof ctx.meta['adminActor'] === 'string' ? ctx.meta['adminActor'] : null) ||\n\t\t\t\tctx.request.headers.get('x-actor') ||\n\t\t\t\t(typeof ctx.meta['adminTokenName'] === 'string'\n\t\t\t\t\t? `token:${ctx.meta['adminTokenName']}`\n\t\t\t\t\t: DEFAULT_ACTOR),\n\t\t\tmethod: ctx.request.method,\n\t\t\t// Normalized, so '/x' and '/x/' are one row for one route — the same\n\t\t\t// path the router matched and `route` below records the pattern of.\n\t\t\tpath: normalizeRequestPath(new URL(ctx.request.url).pathname),\n\t\t\troute,\n\t\t\tmodule,\n\t\t\tstatus: res.status,\n\t\t\tdurationMs: Date.now() - started,\n\t\t\trequestId: typeof ctx.meta['requestId'] === 'string' ? ctx.meta['requestId'] : null,\n\t\t\tclientIp: ctx.meta.clientIp ?? null,\n\t\t};\n\t\ttry {\n\t\t\tawait store.query(\n\t\t\t\t`INSERT INTO fonderie_admin_log\n\t\t\t\t (actor, method, path, route, module, status, duration_ms, request_id, client_ip)\n\t\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,\n\t\t\t\t[\n\t\t\t\t\trow.actor,\n\t\t\t\t\trow.method,\n\t\t\t\t\trow.path,\n\t\t\t\t\trow.route,\n\t\t\t\t\trow.module,\n\t\t\t\t\trow.status,\n\t\t\t\t\trow.durationMs,\n\t\t\t\t\trow.requestId,\n\t\t\t\t\trow.clientIp,\n\t\t\t\t],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error(\n\t\t\t\t'[admin] could not write the admin log:',\n\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t);\n\t\t}\n\t\treturn res;\n\t};\n}\n\nexport const MAX_PAGE = 200;\n\n// Newest first, keyset on (at, id). `before` is the last entry's cursor.\nexport async function readAdminLog(\n\tstore: IStoreAdapter,\n\topts: { limit?: number; before?: string } = {},\n): Promise<IAdminLogPage> {\n\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);\n\tconst cursor = opts.before ? decodeCursor(opts.before) : null;\n\tconst rows = await store.query<IAdminLogEntry>(\n\t\t`SELECT id, at, actor, method, path, route, module, status,\n\t\t duration_ms AS \"durationMs\", request_id AS \"requestId\", client_ip AS \"clientIp\"\n\t\t FROM fonderie_admin_log\n\t\t WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))\n\t\t ORDER BY at DESC, id DESC\n\t\t LIMIT $3`,\n\t\t[cursor?.at ?? null, cursor?.id ?? null, limit + 1],\n\t);\n\tconst page = rows.slice(0, limit);\n\tconst last = page[page.length - 1];\n\treturn {\n\t\tentries: page,\n\t\tnext: rows.length > limit && last ? encodeCursor(last) : null,\n\t};\n}\n\nfunction encodeCursor(e: IAdminLogEntry): string {\n\treturn Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, 'utf8').toString('base64url');\n}\n\nfunction decodeCursor(c: string): { at: string; id: string } | null {\n\tconst [at, id] = Buffer.from(c, 'base64url').toString('utf8').split('|');\n\treturn at && id ? { at, id } : null;\n}\n","import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { IRequestSchema } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport { classifyMigration, InternalMigrationRunner } from '@fonderie/store';\n\nimport type { IAdminMigrationModule, IAdminMigrationsReport, IMigrationSet } from './types';\n\n/**\n * `expect` is the exact pending list the operator was shown, in order.\n *\n * It is not ceremony. Between rendering the page and clicking the button a\n * deploy can change what is pending — and the thing that changed is precisely\n * the thing nobody reviewed. Sending it back makes the server refuse rather\n * than apply a set the operator never saw. Same shape as versionedWrite's\n * if_version, and it also gives this POST the body schema audit:validation\n * requires.\n */\nexport const applyMigrationsSchema: IRequestSchema = {\n\tsafeParse(input: unknown) {\n\t\tconst fail = (path: string, message: string) => ({\n\t\t\tsuccess: false as const,\n\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t});\n\t\tconst b = (input ?? {}) as Record<string, unknown>;\n\t\tconst expect = b['expect'];\n\t\tif (!Array.isArray(expect) || !expect.every((f) => typeof f === 'string'))\n\t\t\treturn fail('expect', 'expect must be the list of pending filenames you were shown');\n\t\t// An empty list is legitimate: it asserts \"I was told there was nothing\n\t\t// to do\", and the handler answers UP_TO_DATE or CHANGED accordingly.\n\t\treturn { success: true as const, data: { expect: expect as string[] } };\n\t},\n};\n\n// Applying is the APP's order, never ours. The sets arrive already interleaved\n// — brick dirs and the app's own, in the sequence its applier runs them — from\n// the one constant both the applier and this reporter read. See the reference app's\n// db/migrations/steps.ts: \"auth owns fonderie_users, which the app migration\n// extends, and media's assets reference storage's blobs.\"\n\n// Has this database EVER been migrated? pending() cannot answer it: it catches\n// its own read failure and returns every file, so a virgin database, a database\n// belonging to another app, and an unreachable one are the same answer. Asked\n// directly, because \"61 pending\" means something completely different depending\n// on which of those it is.\nasync function hasMigrationHistory(store: IStoreAdapter): Promise<boolean> {\n\ttry {\n\t\tconst rows = await store.query<{ n: number }>(\n\t\t\t'SELECT count(*)::int AS n FROM fonderie_migrations',\n\t\t);\n\t\treturn (rows[0]?.n ?? 0) > 0;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction classifyPending(dir: string, files: readonly string[]) {\n\treturn files.map((file) => {\n\t\tconst { impact, destructive } = classifyMigration(readFileSync(join(dir, file), 'utf8'));\n\t\treturn { file, impact, destructive };\n\t});\n}\n\n/**\n * What every module is waiting on, with each pending file's impact.\n *\n * `blockedBy` names the first EARLIER module that is behind. Order is\n * load-bearing and declared by the app, so a module cannot be applied while\n * something it may depend on is unapplied — the panel offers one button at a\n * time and the operator walks the list in the declared order.\n */\nexport async function migrationsReport(\n\tstore: IStoreAdapter,\n\tsets: ReadonlyArray<IMigrationSet>,\n): Promise<IAdminMigrationsReport> {\n\tconst everApplied = await hasMigrationHistory(store);\n\n\tconst modules: IAdminMigrationModule[] = [];\n\tlet firstBehind: string | null = null;\n\n\tfor (const [name, dir] of sets) {\n\t\tconst pending = await new InternalMigrationRunner(store, dir).pending();\n\t\tconst files = classifyPending(dir, pending);\n\t\tmodules.push({\n\t\t\tname,\n\t\t\tpending: files,\n\t\t\t// Only a module BEFORE this one blocks it. The first module with\n\t\t\t// pending work blocks every module after it, and nothing blocks it.\n\t\t\tblockedBy: firstBehind,\n\t\t\t// A file that deletes data on a database with nothing in it yet is not\n\t\t\t// destroying anything. Same rule the CLI's --check applies.\n\t\t\tappliable:\n\t\t\t\tfiles.length > 0 &&\n\t\t\t\tfirstBehind === null &&\n\t\t\t\t(!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| {\n\t\t\tok: true;\n\t\t\treason: 'MIGRATIONS_APPLIED' | 'MIGRATIONS_UP_TO_DATE';\n\t\t\tmodule: IAdminMigrationModule;\n\t }\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 { authorizeOperator } from './operators/http';\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.\n//\n// Without a Bearer header, an operator's session cookie is the other way in\n// (see operators/http.ts): same scopes, plus a fresh authenticator code for\n// the dangerous actions `stepUp` marks.\nexport function requireAdminScope(\n\tbootstrap: string,\n\tstore: IStoreAdapter | undefined,\n\tneeded: AdminScope | 'root',\n\topts: { stepUp?: boolean; operators?: boolean } = {},\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst token = bearer(ctx);\n\t\tif (!token) {\n\t\t\tif (!store || !opts.operators) return UNAUTHORIZED();\n\t\t\tconst verdict = await authorizeOperator(store, ctx, needed, opts.stepUp ?? false);\n\t\t\tif (verdict === null) return UNAUTHORIZED();\n\t\t\treturn verdict === 'ok' ? next() : verdict;\n\t\t}\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","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { HTTP, constantTimeEqual, setApiResponse } from '@fonderie/core';\nimport { validate } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { SCOPES, grants } from '../tokens';\nimport type { AdminScope } from '../types';\nimport { type ISecretBox, passwordProblem, totpUri } from './crypto';\nimport {\n\tclaimSchema,\n\tenrollmentSchema,\n\tfactorSchema,\n\tinspectLinkSchema,\n\tinviteSchema,\n\tloginSchema,\n\tredeemLinkSchema,\n\tupdateOperatorSchema,\n} from './schemas';\nimport {\n\tABSOLUTE_MS,\n\tEMAIL_RE,\n\tINVITE_HOURS,\n\ttype IOperatorRow,\n\tPENDING_MS,\n\tRECOVERY_HOURS,\n\ttype SessionStage,\n\tcheckPassword,\n\tcheckSecondFactor,\n\tclaimFirstOperator,\n\tconfirmEnrollment,\n\tcreateLink,\n\tcreateSession,\n\tdeleteOperatorSessions,\n\tdeleteSession,\n\tenrollmentSecret,\n\tfindLink,\n\tfindOperator,\n\tlistOperators,\n\tlockMinutes,\n\tmarkStepUp,\n\tnormalizeEmail,\n\toperatorCount,\n\tpublicOperator,\n\treadSession,\n\tredeemLink,\n\tstepUpFresh,\n} from './service';\n\n// ── the cookie ─────────────────────────────────────────────────────────────\n// HttpOnly (script cannot read it), SameSite=Strict (never sent from another\n// site), Secure everywhere except plain-http localhost. Over HTTPS it takes\n// the __Host- prefix, which the browser enforces: Secure, Path=/, no Domain —\n// so a sibling subdomain cannot set or shadow it.\n\nconst LOCAL = new Set(['localhost', '127.0.0.1', '[::1]']);\nconst isSecure = (ctx: IFonderieContext): boolean => {\n\tconst u = new URL(ctx.request.url);\n\treturn !(u.protocol === 'http:' && LOCAL.has(u.hostname));\n};\nconst cookieName = (ctx: IFonderieContext) =>\n\tisSecure(ctx) ? '__Host-fonderie_admin' : 'fonderie_admin';\n\nexport function readCookie(ctx: IFonderieContext): string {\n\tconst header = ctx.request.headers.get('cookie') ?? '';\n\tconst want = cookieName(ctx);\n\tfor (const part of header.split(';')) {\n\t\tconst i = part.indexOf('=');\n\t\tif (i > 0 && part.slice(0, i).trim() === want)\n\t\t\treturn decodeURIComponent(part.slice(i + 1).trim());\n\t}\n\treturn '';\n}\n\nfunction withCookie(\n\tres: Response,\n\tctx: IFonderieContext,\n\tvalue: string,\n\tmaxAgeMs: number,\n): Response {\n\tconst attrs = [\n\t\t`${cookieName(ctx)}=${value}`,\n\t\t'Path=/',\n\t\t'HttpOnly',\n\t\t'SameSite=Strict',\n\t\t`Max-Age=${Math.floor(maxAgeMs / 1000)}`,\n\t];\n\tif (isSecure(ctx)) attrs.push('Secure');\n\tres.headers.append('set-cookie', attrs.join('; '));\n\tres.headers.set('cache-control', 'no-store');\n\treturn res;\n}\nconst clearCookie = (res: Response, ctx: IFonderieContext) => withCookie(res, ctx, '', 0);\n\n// ── CSRF ───────────────────────────────────────────────────────────────────\n// SameSite=Strict already keeps the cookie off cross-SITE requests. A sibling\n// subdomain is the same site, so a cookie-authenticated write must also come\n// from THIS origin: browsers send Origin (or Sec-Fetch-Site) on every\n// non-GET fetch. A request with neither is not from a browser page.\nexport function sameOrigin(ctx: IFonderieContext): boolean {\n\tconst origin = ctx.request.headers.get('origin');\n\tconst host = ctx.request.headers.get('host') ?? new URL(ctx.request.url).host;\n\tif (origin) {\n\t\ttry {\n\t\t\treturn new URL(origin).host === host;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\tconst site = ctx.request.headers.get('sec-fetch-site');\n\treturn site === null || site === 'same-origin';\n}\n\n// ── per-IP attempt limit ───────────────────────────────────────────────────\n// Best effort, in memory: per instance on serverless. The durable brake is the\n// per-account lockout in the database; this one stops one address from\n// spraying many accounts.\nconst WINDOW_MS = 10 * 60_000;\nconst PER_IP = 30;\nconst attempts = new Map<string, number[]>();\nfunction ipLimited(ctx: IFonderieContext): boolean {\n\tconst ip = ctx.meta.clientIp ?? 'unknown';\n\tconst now = Date.now();\n\tconst recent = (attempts.get(ip) ?? []).filter((t) => now - t < WINDOW_MS);\n\trecent.push(now);\n\tattempts.set(ip, recent);\n\tif (attempts.size > 10_000) attempts.clear();\n\treturn recent.length > PER_IP;\n}\n/** Test hook: forget every address. */\nexport const resetAttemptLimits = (): void => attempts.clear();\n\n// ── helpers ────────────────────────────────────────────────────────────────\n\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\nconst str = (v: unknown): string => (typeof v === 'string' ? v : '');\nconst bearer = (ctx: IFonderieContext): string => {\n\tconst h = ctx.request.headers.get('authorization') ?? '';\n\treturn h.startsWith('Bearer ') ? h.slice(7) : '';\n};\n\nconst BAD_CREDENTIALS = () =>\n\tsetApiResponse(\n\t\tHTTP.UNAUTHORIZED,\n\t\t'INVALID_CREDENTIALS',\n\t\t'That email and password do not match an operator.',\n\t);\nconst BAD_CODE = () =>\n\tsetApiResponse(\n\t\tHTTP.UNAUTHORIZED,\n\t\t'INVALID_CODE',\n\t\t'That code did not work. Codes change every 30 seconds.',\n\t);\nconst LOCKED = (op: IOperatorRow) =>\n\tsetApiResponse(\n\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t'LOCKED',\n\t\t`Too many attempts. Try again in ${lockMinutes(op)} minute(s).`,\n\t);\nconst TOO_FAST = () =>\n\tsetApiResponse(\n\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t'RATE_LIMITED',\n\t\t'Too many attempts from this address. Wait a few minutes.',\n\t);\nconst WRONG_STAGE = () =>\n\tsetApiResponse(HTTP.UNAUTHORIZED, 'NO_SESSION', 'Start again from sign-in.');\nconst CROSS_ORIGIN = () =>\n\tsetApiResponse(HTTP.FORBIDDEN, 'CROSS_ORIGIN', 'This request did not come from the admin page.');\n\nconst stateOf = (stage: SessionStage) =>\n\tstage === 'active' ? 'signed-in' : stage === 'enroll' ? 'needs-enrollment' : 'needs-2fa';\n\nexport interface IOperatorDeps {\n\tstore: IStoreAdapter;\n\tbox: ISecretBox;\n\trootToken: string;\n\t/** Where the console lives, for the links it hands out: '<prefix>/ui'. */\n\tuiPath: (ctx: IFonderieContext) => string;\n}\n\nasync function startSession(\n\tdeps: IOperatorDeps,\n\tctx: IFonderieContext,\n\top: IOperatorRow,\n\tstage: SessionStage,\n\tpayload: Record<string, unknown> = {},\n) {\n\tctx.meta['adminOperator'] = op.email;\n\tconst id = await createSession(deps.store, ctx, op.id, stage);\n\tconst res = setApiResponse(HTTP.OK, 'ADMIN_SESSION', 'Session', {\n\t\tstate: stateOf(stage),\n\t\toperator: publicOperator(op),\n\t\t...payload,\n\t});\n\treturn withCookie(res, ctx, id, stage === 'active' ? ABSOLUTE_MS : PENDING_MS);\n}\n\n/** A pending (or active) session of the given stage, from the cookie. */\nasync function sessionAt(deps: IOperatorDeps, ctx: IFonderieContext, stage: SessionStage) {\n\tconst cookie = readCookie(ctx);\n\tconst found = await readSession(deps.store, cookie);\n\treturn found && found.session.stage === stage ? { ...found, cookie } : null;\n}\n\n// ── the public sign-in routes ─────────────────────────────────────────────\n// Outside the admin guard: they are how a guard-passing session comes to\n// exist. Every write is same-origin-checked and attempt-limited.\n\nexport function sessionRoutes(deps: IOperatorDeps): Array<[string, string, Middleware[]]> {\n\tconst { store, box } = deps;\n\t// Every sign-in write: same-origin, attempt-limited, attributed to 'anonymous'\n\t// in the log until a session names someone.\n\tconst check: Middleware = async (ctx, next) => {\n\t\tctx.meta['adminActor'] = 'anonymous';\n\t\tif (ctx.request.method !== 'GET' && !sameOrigin(ctx)) return CROSS_ORIGIN();\n\t\tif (ctx.request.method !== 'GET' && ipLimited(ctx)) return TOO_FAST();\n\t\treturn next();\n\t};\n\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/session',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst found = await readSession(store, readCookie(ctx));\n\t\t\t\t\tconst claimable = (await operatorCount(store)) === 0;\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'ADMIN_SESSION', 'Session', {\n\t\t\t\t\t\tstate: found ? stateOf(found.session.stage) : 'signed-out',\n\t\t\t\t\t\toperator: found ? publicOperator(found.op) : null,\n\t\t\t\t\t\tclaimable,\n\t\t\t\t\t\tstepUpFresh: found ? stepUpFresh(found.session) : false,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t// The one-time claim. Needs the root token AND an empty operator table;\n\t\t\t// after the first operator exists it is permanently refused.\n\t\t\t'POST',\n\t\t\t'/_admin/session/claim',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(claimSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tif (!constantTimeEqual(bearer(ctx), deps.rootToken)) {\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.UNAUTHORIZED,\n\t\t\t\t\t\t\t'UNAUTHORIZED',\n\t\t\t\t\t\t\t'Claiming needs the root admin token.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst email = normalizeEmail(str(b['email']));\n\t\t\t\t\tif (!EMAIL_RE.test(email))\n\t\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'Enter a valid email address.');\n\t\t\t\t\tconst problem = passwordProblem(b['password']);\n\t\t\t\t\tif (problem) return setApiResponse(HTTP.UNPROCESSABLE, 'WEAK_PASSWORD', problem);\n\t\t\t\t\tconst op = await claimFirstOperator(store, {\n\t\t\t\t\t\temail,\n\t\t\t\t\t\tname: str(b['name']) || undefined,\n\t\t\t\t\t\tpassword: str(b['password']),\n\t\t\t\t\t});\n\t\t\t\t\tif (!op)\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t'ALREADY_CLAIMED',\n\t\t\t\t\t\t\t'This console already has operators. Ask one for an invite.',\n\t\t\t\t\t\t);\n\t\t\t\t\treturn startSession(deps, ctx, op, 'enroll');\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/session/login',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(loginSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst result = await checkPassword(store, str(b['email']), str(b['password']));\n\t\t\t\t\tif (result.op && 'locked' in result && result.locked) return LOCKED(result.op);\n\t\t\t\t\tif (!result.ok || !result.op) return BAD_CREDENTIALS();\n\t\t\t\t\treturn startSession(\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\tresult.op,\n\t\t\t\t\t\tresult.op.totpConfirmedAt ? 'password' : 'enroll',\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/session/enrollment',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst s = await sessionAt(deps, ctx, 'enroll');\n\t\t\t\t\tif (!s) return WRONG_STAGE();\n\t\t\t\t\tconst secret = await enrollmentSecret(store, box, s.op);\n\t\t\t\t\tconst issuer = `Admin · ${new URL(ctx.request.url).hostname}`;\n\t\t\t\t\tconst res = setApiResponse(HTTP.OK, 'ENROLLMENT', 'Scan this with an authenticator app', {\n\t\t\t\t\t\tsecret,\n\t\t\t\t\t\turi: totpUri(issuer, s.op.email, secret),\n\t\t\t\t\t\taccount: s.op.email,\n\t\t\t\t\t\tissuer,\n\t\t\t\t\t});\n\t\t\t\t\tres.headers.set('cache-control', 'no-store');\n\t\t\t\t\treturn res;\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/session/enrollment',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(enrollmentSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst s = await sessionAt(deps, ctx, 'enroll');\n\t\t\t\t\tif (!s) return WRONG_STAGE();\n\t\t\t\t\tconst codes = await confirmEnrollment(store, box, s.op, str(body(ctx)['code']));\n\t\t\t\t\tif (!codes) {\n\t\t\t\t\t\tconst fresh = await findOperator(store, { id: s.op.id });\n\t\t\t\t\t\treturn fresh && lockMinutes(fresh) ? LOCKED(fresh) : BAD_CODE();\n\t\t\t\t\t}\n\t\t\t\t\t// A new session id at the privilege change: a pending id that leaked\n\t\t\t\t\t// never becomes a signed-in one.\n\t\t\t\t\tawait deleteSession(store, s.cookie);\n\t\t\t\t\tconst op = (await findOperator(store, { id: s.op.id })) ?? s.op;\n\t\t\t\t\treturn startSession(deps, ctx, op, 'active', { backupCodes: codes });\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/session/verify',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(factorSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst s = await sessionAt(deps, ctx, 'password');\n\t\t\t\t\tif (!s) return WRONG_STAGE();\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst r = await checkSecondFactor(store, box, s.op, {\n\t\t\t\t\t\tcode: b['code'],\n\t\t\t\t\t\tbackupCode: b['backupCode'],\n\t\t\t\t\t});\n\t\t\t\t\tif (!r.ok) {\n\t\t\t\t\t\tconst fresh = await findOperator(store, { id: s.op.id });\n\t\t\t\t\t\treturn fresh && lockMinutes(fresh) ? LOCKED(fresh) : BAD_CODE();\n\t\t\t\t\t}\n\t\t\t\t\tawait deleteSession(store, s.cookie);\n\t\t\t\t\tawait store.query(\n\t\t\t\t\t\t`UPDATE fonderie_admin_operators SET last_login_at = now() WHERE id = $1`,\n\t\t\t\t\t\t[s.op.id],\n\t\t\t\t\t);\n\t\t\t\t\treturn startSession(\n\t\t\t\t\t\tdeps,\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\ts.op,\n\t\t\t\t\t\t'active',\n\t\t\t\t\t\tr.via === 'backup' ? { backupCodesLeft: r.backupLeft } : {},\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t// A fresh code for the next five minutes of dangerous actions.\n\t\t\t'POST',\n\t\t\t'/_admin/session/step-up',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(factorSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst s = await sessionAt(deps, ctx, 'active');\n\t\t\t\t\tif (!s) return WRONG_STAGE();\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst r = await checkSecondFactor(store, box, s.op, {\n\t\t\t\t\t\tcode: b['code'],\n\t\t\t\t\t\tbackupCode: b['backupCode'],\n\t\t\t\t\t});\n\t\t\t\t\tif (!r.ok) return BAD_CODE();\n\t\t\t\t\tawait markStepUp(store, s.session.idHash);\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'STEPPED_UP', 'Confirmed for five minutes', {\n\t\t\t\t\t\tstepUpFresh: true,\n\t\t\t\t\t\t...(r.via === 'backup' ? { backupCodesLeft: r.backupLeft } : {}),\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'DELETE',\n\t\t\t'/_admin/session',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst cookie = readCookie(ctx);\n\t\t\t\t\tif (cookie) await deleteSession(store, cookie);\n\t\t\t\t\treturn clearCookie(setApiResponse(HTTP.OK, 'SIGNED_OUT', 'Signed out'), ctx);\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t// What a link is for, before the person sets a password. POST so the\n\t\t\t// token never sits in a URL a proxy logs.\n\t\t\t'POST',\n\t\t\t'/_admin/session/link/inspect',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(inspectLinkSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst link = await findLink(store, str(body(ctx)['token']));\n\t\t\t\t\treturn link\n\t\t\t\t\t\t? setApiResponse(HTTP.OK, 'LINK', 'Link', { kind: link.kind, email: link.email })\n\t\t\t\t\t\t: setApiResponse(\n\t\t\t\t\t\t\t\tHTTP.NOT_FOUND,\n\t\t\t\t\t\t\t\t'INVALID_LINK',\n\t\t\t\t\t\t\t\t'This link has expired or was already used. Ask for a new one.',\n\t\t\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/session/link',\n\t\t\t[\n\t\t\t\tcheck,\n\t\t\t\tvalidate(redeemLinkSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst problem = passwordProblem(b['password']);\n\t\t\t\t\tif (problem) return setApiResponse(HTTP.UNPROCESSABLE, 'WEAK_PASSWORD', problem);\n\t\t\t\t\tconst r = await redeemLink(store, str(b['token']), {\n\t\t\t\t\t\tpassword: str(b['password']),\n\t\t\t\t\t\tname: str(b['name']) || undefined,\n\t\t\t\t\t});\n\t\t\t\t\tif ('error' in r) {\n\t\t\t\t\t\treturn r.error === 'ALREADY_OPERATOR'\n\t\t\t\t\t\t\t? setApiResponse(\n\t\t\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t\t\t'ALREADY_OPERATOR',\n\t\t\t\t\t\t\t\t\t'That email is already an operator. Sign in instead.',\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: setApiResponse(\n\t\t\t\t\t\t\t\t\tHTTP.NOT_FOUND,\n\t\t\t\t\t\t\t\t\t'INVALID_LINK',\n\t\t\t\t\t\t\t\t\t'This link has expired or was already used. Ask for a new one.',\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn startSession(deps, ctx, r.op, 'enroll');\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t];\n}\n\n// ── guard integration ──────────────────────────────────────────────────────\n\n/**\n * Dangerous for a PERSON: needs a code from the last five minutes. Reads never\n * do; revealing a secret, minting a token or link, applying a migration and\n * deleting anything do.\n */\nexport function needsStepUp(method: string, path: string, needed: AdminScope | 'root'): boolean {\n\tconst m = method.toUpperCase();\n\tif (m === 'GET' || m === 'HEAD') return false;\n\treturn (\n\t\tneeded === 'root' ||\n\t\tneeded === 'secrets' ||\n\t\tm === 'DELETE' ||\n\t\t/\\/migrations\\/[^/]+\\/apply$/.test(path)\n\t);\n}\n\n/**\n * The cookie path of the admin guard. Returns null when the request carries\n * no operator session at all (the caller then answers 401 as before), or the\n * Response to send, or 'ok' with the operator recorded for the log.\n */\nexport async function authorizeOperator(\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tneeded: AdminScope | 'root',\n\tstepUp: boolean,\n): Promise<Response | 'ok' | null> {\n\tconst found = await readSession(store, readCookie(ctx));\n\tif (!found || found.session.stage !== 'active') return null;\n\tconst method = ctx.request.method.toUpperCase();\n\tif (method !== 'GET' && method !== 'HEAD' && !sameOrigin(ctx)) return CROSS_ORIGIN();\n\t// For a person, 'root' (tokens, operators) means the highest scope.\n\tconst scope: AdminScope = needed === 'root' ? 'secrets' : needed;\n\tif (!grants(found.op.scopes, scope)) {\n\t\treturn setApiResponse(\n\t\t\tHTTP.FORBIDDEN,\n\t\t\t'FORBIDDEN',\n\t\t\t`Your operator account lacks the '${scope}' scope`,\n\t\t);\n\t}\n\tctx.meta['adminOperator'] = found.op.email;\n\tif (stepUp && !stepUpFresh(found.session)) {\n\t\treturn setApiResponse(\n\t\t\tHTTP.FORBIDDEN,\n\t\t\t'STEP_UP_REQUIRED',\n\t\t\t'Enter a code from your authenticator app to continue.',\n\t\t);\n\t}\n\treturn 'ok';\n}\n\n// ── managing operators (behind the guard, root-level) ──────────────────────\n\nconst actorOf = (ctx: IFonderieContext): string =>\n\ttypeof ctx.meta['adminOperator'] === 'string'\n\t\t? `operator:${ctx.meta['adminOperator']}`\n\t\t: 'root-token';\n\nconst validScopes = (v: unknown): AdminScope[] | null =>\n\tArray.isArray(v) && v.length > 0 && v.every((s) => SCOPES.includes(s as AdminScope))\n\t\t? [...new Set(v as AdminScope[])]\n\t\t: null;\n\nexport function operatorAdminRoutes(\n\tdeps: IOperatorDeps,\n): Array<[string, string, Middleware[], 'read' | 'root']> {\n\tconst { store } = deps;\n\tconst link = (ctx: IFonderieContext, token: string) => `${deps.uiPath(ctx)}#/link/${token}`;\n\tconst idOf = (ctx: IFonderieContext) => ctx.meta.params?.['id'] ?? '';\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/access/operators',\n\t\t\t[async () => setApiResponse(HTTP.OK, 'OPERATORS', 'Operators', await listOperators(store))],\n\t\t\t'read',\n\t\t],\n\t\t[\n\t\t\t'POST',\n\t\t\t'/_admin/access/operators/invites',\n\t\t\t[\n\t\t\t\tvalidate(inviteSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst email = normalizeEmail(str(b['email']));\n\t\t\t\t\tif (!EMAIL_RE.test(email))\n\t\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'Enter a valid email address.');\n\t\t\t\t\tconst scopes = validScopes(b['scopes']);\n\t\t\t\t\tif (!scopes)\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t\t'INVALID',\n\t\t\t\t\t\t\t`scopes must be a non-empty list of ${SCOPES.join(', ')}`,\n\t\t\t\t\t\t);\n\t\t\t\t\tif (await findOperator(store, { email }))\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t'ALREADY_OPERATOR',\n\t\t\t\t\t\t\t'That email is already an operator.',\n\t\t\t\t\t\t);\n\t\t\t\t\tconst hours = Number.isInteger(b['expiresInHours'])\n\t\t\t\t\t\t? Math.min(Math.max(b['expiresInHours'] as number, 1), 336)\n\t\t\t\t\t\t: INVITE_HOURS;\n\t\t\t\t\tconst l = await createLink(store, {\n\t\t\t\t\t\tkind: 'invite',\n\t\t\t\t\t\temail,\n\t\t\t\t\t\tscopes,\n\t\t\t\t\t\tcreatedBy: actorOf(ctx),\n\t\t\t\t\t\thours,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.CREATED, 'INVITE_CREATED', 'Invite link — shown once', {\n\t\t\t\t\t\tid: l.id,\n\t\t\t\t\t\temail,\n\t\t\t\t\t\tscopes,\n\t\t\t\t\t\texpiresAt: l.expiresAt,\n\t\t\t\t\t\ttoken: l.token,\n\t\t\t\t\t\turl: link(ctx, l.token),\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t],\n\t\t\t'root',\n\t\t],\n\t\t[\n\t\t\t// Lost password or lost authenticator: a single-use link that sets a new\n\t\t\t// password and makes them enroll again. Signs them out everywhere.\n\t\t\t'POST',\n\t\t\t'/_admin/access/operators/:id/recovery',\n\t\t\t[\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst op = await findOperator(store, { id: idOf(ctx) });\n\t\t\t\t\tif (!op) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such operator');\n\t\t\t\t\tawait deleteOperatorSessions(store, op.id);\n\t\t\t\t\tconst l = await createLink(store, {\n\t\t\t\t\t\tkind: 'recovery',\n\t\t\t\t\t\temail: op.email,\n\t\t\t\t\t\toperatorId: op.id,\n\t\t\t\t\t\tcreatedBy: actorOf(ctx),\n\t\t\t\t\t\thours: RECOVERY_HOURS,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.CREATED, 'RECOVERY_CREATED', 'Recovery link — shown once', {\n\t\t\t\t\t\tid: l.id,\n\t\t\t\t\t\temail: op.email,\n\t\t\t\t\t\texpiresAt: l.expiresAt,\n\t\t\t\t\t\ttoken: l.token,\n\t\t\t\t\t\turl: link(ctx, l.token),\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t],\n\t\t\t'root',\n\t\t],\n\t\t[\n\t\t\t'PUT',\n\t\t\t'/_admin/access/operators/:id',\n\t\t\t[\n\t\t\t\tvalidate(updateOperatorSchema),\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst b = body(ctx);\n\t\t\t\t\tconst op = await findOperator(store, { id: idOf(ctx) });\n\t\t\t\t\tif (!op) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such operator');\n\t\t\t\t\tconst scopes = b['scopes'] === undefined ? op.scopes : validScopes(b['scopes']);\n\t\t\t\t\tif (!scopes)\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t\t'INVALID',\n\t\t\t\t\t\t\t`scopes must be a non-empty list of ${SCOPES.join(', ')}`,\n\t\t\t\t\t\t);\n\t\t\t\t\tif (\n\t\t\t\t\t\tctx.meta['adminOperator'] === op.email &&\n\t\t\t\t\t\t!scopes.includes('secrets') &&\n\t\t\t\t\t\top.scopes.includes('secrets')\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t'SELF_DEMOTION',\n\t\t\t\t\t\t\t'You cannot remove your own highest scope. Ask another operator.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst disabled = b['disabled'];\n\t\t\t\t\tif (disabled === true && ctx.meta['adminOperator'] === op.email) {\n\t\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t\t\t'SELF_DISABLE',\n\t\t\t\t\t\t\t'You cannot disable yourself. Ask another operator.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst [row] = await store.query<IOperatorRow>(\n\t\t\t\t\t\t`UPDATE fonderie_admin_operators\n\t\t\t\t\t\t SET scopes = $2, name = coalesce($3, name),\n\t\t\t\t\t\t disabled_at = CASE WHEN $4::boolean IS NULL THEN disabled_at WHEN $4 THEN coalesce(disabled_at, now()) ELSE NULL END\n\t\t\t\t\t\t WHERE id = $1\n\t\t\t\t\t\t RETURNING id, email, name, password_hash AS \"passwordHash\", scopes, totp_secret AS \"totpSecret\",\n\t\t\t\t\t\t totp_confirmed_at AS \"totpConfirmedAt\", totp_last_step AS \"totpLastStep\", backup_codes AS \"backupCodes\",\n\t\t\t\t\t\t failed_attempts AS \"failedAttempts\", locked_until AS \"lockedUntil\", created_by AS \"createdBy\",\n\t\t\t\t\t\t created_at AS \"createdAt\", last_login_at AS \"lastLoginAt\", disabled_at AS \"disabledAt\"`,\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\top.id,\n\t\t\t\t\t\t\tscopes,\n\t\t\t\t\t\t\ttypeof b['name'] === 'string' ? b['name'] : null,\n\t\t\t\t\t\t\ttypeof disabled === 'boolean' ? disabled : null,\n\t\t\t\t\t\t],\n\t\t\t\t\t);\n\t\t\t\t\tif (!row) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such operator');\n\t\t\t\t\t// Any change of rights ends their sessions: new rights apply at next sign-in.\n\t\t\t\t\tawait deleteOperatorSessions(store, op.id);\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'OPERATOR_UPDATED',\n\t\t\t\t\t\t'Operator updated',\n\t\t\t\t\t\tpublicOperator(row),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t],\n\t\t\t'root',\n\t\t],\n\t\t[\n\t\t\t'DELETE',\n\t\t\t'/_admin/access/operators/links/:id',\n\t\t\t[\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst rows = await store.query(\n\t\t\t\t\t\t`UPDATE fonderie_admin_invites SET used_at = now() WHERE id = $1 AND used_at IS NULL RETURNING id`,\n\t\t\t\t\t\t[idOf(ctx)],\n\t\t\t\t\t);\n\t\t\t\t\treturn rows.length\n\t\t\t\t\t\t? setApiResponse(HTTP.OK, 'LINK_REVOKED', 'Link revoked')\n\t\t\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such live link');\n\t\t\t\t},\n\t\t\t],\n\t\t\t'root',\n\t\t],\n\t];\n}\n","import {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\tcreateHash,\n\tcreateHmac,\n\trandomBytes,\n\tscrypt as scryptCb,\n\ttimingSafeEqual,\n} from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\n// Everything an operator sign-in needs, on node:crypto alone. @fonderie/admin\n// takes no auth dependency: the operator surface must work in an app that\n// never installed @fonderie/auth, and must not share its user tables.\n\n// ── TOTP (RFC 6238, SHA-1, 6 digits, 30 s) ─────────────────────────────────\n// The algorithm @fonderie/auth ships, plus a REPLAY GUARD it lacks: the\n// matched time-step is returned so the caller can refuse it next time. A code\n// read over a shoulder is otherwise good for up to 90 seconds.\n\nconst STEP = 30;\nconst DRIFT = 1;\nconst DIGITS = 6;\nconst B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n\nexport function base32Encode(buf: Buffer): string {\n\tlet bits = 0;\n\tlet value = 0;\n\tlet out = '';\n\tfor (const byte of buf) {\n\t\tvalue = (value << 8) | byte;\n\t\tbits += 8;\n\t\twhile (bits >= 5) {\n\t\t\tout += B32[(value >>> (bits - 5)) & 31];\n\t\t\tbits -= 5;\n\t\t}\n\t}\n\tif (bits > 0) out += B32[(value << (5 - bits)) & 31];\n\treturn out;\n}\n\nexport function base32Decode(input: string): Buffer {\n\tlet bits = 0;\n\tlet value = 0;\n\tconst out: number[] = [];\n\tfor (const ch of input.toUpperCase().replace(/[=\\s]/g, '')) {\n\t\tconst i = B32.indexOf(ch);\n\t\tif (i === -1) continue;\n\t\tvalue = (value << 5) | i;\n\t\tbits += 5;\n\t\tif (bits >= 8) {\n\t\t\tout.push((value >>> (bits - 8)) & 255);\n\t\t\tbits -= 8;\n\t\t}\n\t}\n\treturn Buffer.from(out);\n}\n\nexport function hotp(secret: string, counter: number): string {\n\tconst msg = Buffer.alloc(8);\n\tmsg.writeBigUInt64BE(BigInt(counter));\n\tconst mac = createHmac('sha1', base32Decode(secret)).update(msg).digest();\n\tconst o = (mac[19] ?? 0) & 15;\n\tconst bin =\n\t\t(((mac[o] ?? 0) & 0x7f) << 24) |\n\t\t(((mac[o + 1] ?? 0) & 255) << 16) |\n\t\t(((mac[o + 2] ?? 0) & 255) << 8) |\n\t\t((mac[o + 3] ?? 0) & 255);\n\treturn String(bin % 10 ** DIGITS).padStart(DIGITS, '0');\n}\n\nexport const totpCounter = (now = Date.now()): number => Math.floor(now / 1000 / STEP);\n\n/**\n * The time-step the code matched, or null. A step at or below `lastUsed` is\n * refused even when the code is right — that is the replay guard.\n */\nexport function verifyTotp(\n\tsecret: string,\n\tcode: string,\n\tlastUsed: number | null,\n\tnow = Date.now(),\n): number | null {\n\tconst clean = code.replace(/\\s/g, '');\n\tif (!/^\\d{6}$/.test(clean)) return null;\n\tconst t = totpCounter(now);\n\tfor (let d = -DRIFT; d <= DRIFT; d++) {\n\t\tconst step = t + d;\n\t\tif (lastUsed !== null && step <= lastUsed) continue;\n\t\tif (constantTimeEqual(hotp(secret, step), clean)) return step;\n\t}\n\treturn null;\n}\n\nexport const newTotpSecret = (): string => base32Encode(randomBytes(20));\n\nexport function totpUri(issuer: string, account: string, secret: string): string {\n\tconst q = new URLSearchParams({\n\t\tsecret,\n\t\tissuer,\n\t\talgorithm: 'SHA1',\n\t\tdigits: String(DIGITS),\n\t\tperiod: String(STEP),\n\t});\n\treturn `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(account)}?${q}`;\n}\n\n// ── Backup codes ───────────────────────────────────────────────────────────\n// Ten single-use codes, shown once, stored as SHA-256. 10 base32 characters\n// (50 bits) each — long enough that the per-account lockout, not the code\n// space, is what stops guessing. Grouped for reading: ABCDE-FGHIJ.\n\nexport function newBackupCodes(count = 10): string[] {\n\treturn Array.from({ length: count }, () => {\n\t\tconst s = base32Encode(randomBytes(7)).slice(0, 10);\n\t\treturn `${s.slice(0, 5)}-${s.slice(5)}`;\n\t});\n}\n\nexport const normalizeBackupCode = (code: string): string =>\n\tcode.toUpperCase().replace(/[^A-Z2-7]/g, '');\n\nexport const sha256 = (s: string): string => createHash('sha256').update(s).digest('hex');\n\n// ── Passwords: scrypt ──────────────────────────────────────────────────────\n// Stored as `scrypt$N$r$p$salt$hash` so parameters can be raised later and\n// old hashes still verify. N=2^15, r=8 — the OWASP floor for scrypt.\n\nconst N = 2 ** 15;\nconst R = 8;\nconst P = 1;\nconst KEYLEN = 32;\n\nconst scrypt = (password: string, salt: Buffer, n: number, r: number, p: number) =>\n\tnew Promise<Buffer>((resolve, reject) =>\n\t\tscryptCb(\n\t\t\tpassword.normalize('NFKC'),\n\t\t\tsalt,\n\t\t\tKEYLEN,\n\t\t\t{ N: n, r, p, maxmem: 128 * n * r * 2 },\n\t\t\t(err, key) => (err ? reject(err) : resolve(key)),\n\t\t),\n\t);\n\nexport async function hashPassword(password: string): Promise<string> {\n\tconst salt = randomBytes(16);\n\tconst key = await scrypt(password, salt, N, R, P);\n\treturn `scrypt$${N}$${R}$${P}$${salt.toString('base64')}$${key.toString('base64')}`;\n}\n\nexport async function verifyPassword(password: string, stored: string): Promise<boolean> {\n\tconst [kind, n, r, p, salt, hash] = stored.split('$');\n\tif (kind !== 'scrypt' || !n || !r || !p || !salt || !hash) return false;\n\tconst expected = Buffer.from(hash, 'base64');\n\tconst key = await scrypt(password, Buffer.from(salt, 'base64'), Number(n), Number(r), Number(p));\n\treturn key.length === expected.length && timingSafeEqual(key, expected);\n}\n\n// A hash of nothing, verified against when the email is unknown, so a wrong\n// address costs the same time as a wrong password and the response timing\n// does not reveal who is an operator.\nlet dummy: Promise<string> | null = null;\nexport const dummyHash = (): Promise<string> => {\n\tdummy ??= hashPassword(randomBytes(16).toString('hex'));\n\treturn dummy;\n};\n\n// 12 characters minimum and not trivially guessable. Deliberately no\n// composition rules (NIST 800-63B): length is what matters, and the second\n// factor is mandatory anyway.\nexport function passwordProblem(password: unknown): string | null {\n\tif (typeof password !== 'string' || password.length < 12) return 'Use at least 12 characters.';\n\tif (password.length > 256) return 'Use at most 256 characters.';\n\tif (/^(.)\\1+$/.test(password)) return 'That password is too easy to guess.';\n\treturn null;\n}\n\n// ── TOTP secret at rest ────────────────────────────────────────────────────\n// A TOTP secret must stay reversible, so it is encrypted, not hashed, with the\n// app's operatorKey (64 hex). Without a key it is stored as-is and readiness\n// says so. Same `v1:iv:tag:data` shape as @fonderie/auth's MFA cipher.\n\nconst PREFIX = 'op.v1:';\n\nexport interface ISecretBox {\n\tseal(plain: string): string;\n\topen(stored: string): string;\n}\n\nexport function secretBox(keyHex: string | undefined): ISecretBox {\n\tif (!keyHex) return { seal: (p) => p, open: (s) => s };\n\tconst key = Buffer.from(keyHex, 'hex');\n\tif (key.length !== 32)\n\t\tthrow new Error('[admin] operatorKey must be 32 bytes — 64 hex chars (`openssl rand -hex 32`)');\n\treturn {\n\t\tseal(plain) {\n\t\t\tconst iv = randomBytes(12);\n\t\t\tconst c = createCipheriv('aes-256-gcm', key, iv);\n\t\t\tconst data = Buffer.concat([c.update(plain, 'utf8'), c.final()]);\n\t\t\treturn `${PREFIX}${iv.toString('hex')}:${c.getAuthTag().toString('hex')}:${data.toString('hex')}`;\n\t\t},\n\t\topen(stored) {\n\t\t\tif (!stored.startsWith(PREFIX)) return stored;\n\t\t\tconst [iv, tag, data] = stored.slice(PREFIX.length).split(':');\n\t\t\tif (!iv || !tag || !data) throw new Error('[admin] malformed operator secret');\n\t\t\tconst d = createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));\n\t\t\td.setAuthTag(Buffer.from(tag, 'hex'));\n\t\t\treturn Buffer.concat([d.update(Buffer.from(data, 'hex')), d.final()]).toString('utf8');\n\t\t},\n\t};\n}\n\n/** An opaque random credential: a session id or an invite token. */\nexport const newOpaqueToken = (prefix: string): string =>\n\t`${prefix}_${randomBytes(32).toString('base64url')}`;\n","import type { IRequestSchema } from '@fonderie/core/middlewares';\n\nimport { SCOPES } from '../tokens';\n\n// Envelope validation for the operator routes: types, lengths, unknown keys\n// stripped. The judgement calls that deserve their own message (password\n// strength, email shape) stay in the handlers, which answer with a specific\n// reason rather than a generic INVALID_PARAMETER.\n\ntype Field =\n\t| { type: 'string'; required?: boolean; max: number }\n\t| { type: 'boolean'; required?: boolean }\n\t| { type: 'integer'; required?: boolean; min: number; max: number }\n\t| { type: 'scopes'; required?: boolean };\n\nfunction schema(fields: Record<string, Field>, opts: { oneOf?: string[] } = {}): IRequestSchema {\n\treturn {\n\t\tsafeParse(input: unknown) {\n\t\t\tconst fail = (path: string, message: string) => ({\n\t\t\t\tsuccess: false as const,\n\t\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t\t});\n\t\t\tconst b = (input && typeof input === 'object' ? input : {}) as Record<string, unknown>;\n\t\t\tconst out: Record<string, unknown> = {};\n\t\t\tfor (const [key, f] of Object.entries(fields)) {\n\t\t\t\tconst v = b[key];\n\t\t\t\tif (v === undefined || v === null) {\n\t\t\t\t\tif (f.required) return fail(key, `${key} is required`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (f.type === 'string') {\n\t\t\t\t\tif (typeof v !== 'string' || v.length === 0 || v.length > f.max)\n\t\t\t\t\t\treturn fail(key, `${key} must be a string of 1–${f.max} characters`);\n\t\t\t\t} else if (f.type === 'boolean') {\n\t\t\t\t\tif (typeof v !== 'boolean') return fail(key, `${key} must be true or false`);\n\t\t\t\t} else if (f.type === 'integer') {\n\t\t\t\t\tif (!Number.isInteger(v) || (v as number) < f.min || (v as number) > f.max)\n\t\t\t\t\t\treturn fail(key, `${key} must be an integer ${f.min}–${f.max}`);\n\t\t\t\t} else if (!Array.isArray(v) || v.length === 0 || !v.every((s) => SCOPES.includes(s))) {\n\t\t\t\t\treturn fail(key, `${key} must be a non-empty list of ${SCOPES.join(', ')}`);\n\t\t\t\t}\n\t\t\t\tout[key] = v;\n\t\t\t}\n\t\t\tif (opts.oneOf && !opts.oneOf.some((k) => out[k] !== undefined)) {\n\t\t\t\treturn fail(opts.oneOf[0] ?? 'body', `one of ${opts.oneOf.join(', ')} is required`);\n\t\t\t}\n\t\t\treturn { success: true as const, data: out };\n\t\t},\n\t};\n}\n\nconst email = { type: 'string', required: true, max: 320 } as const;\nconst password = { type: 'string', required: true, max: 256 } as const;\nconst name = { type: 'string', max: 200 } as const;\nconst token = { type: 'string', required: true, max: 200 } as const;\n\nexport const claimSchema = schema({ email, password, name });\nexport const loginSchema = schema({ email, password });\nexport const enrollmentSchema = schema({ code: { type: 'string', required: true, max: 12 } });\nexport const factorSchema = schema(\n\t{ code: { type: 'string', max: 12 }, backupCode: { type: 'string', max: 32 } },\n\t{ oneOf: ['code', 'backupCode'] },\n);\nexport const inspectLinkSchema = schema({ token });\nexport const redeemLinkSchema = schema({ token, password, name });\nexport const inviteSchema = schema({\n\temail,\n\tscopes: { type: 'scopes', required: true },\n\texpiresInHours: { type: 'integer', min: 1, max: 336 },\n});\nexport const updateOperatorSchema = schema({\n\tscopes: { type: 'scopes' },\n\tname,\n\tdisabled: { type: 'boolean' },\n});\n","import type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { AdminScope } from '../types';\nimport {\n\ttype ISecretBox,\n\tdummyHash,\n\thashPassword,\n\tnewBackupCodes,\n\tnewOpaqueToken,\n\tnewTotpSecret,\n\tnormalizeBackupCode,\n\tsha256,\n\tverifyPassword,\n\tverifyTotp,\n} from './crypto';\n\n// Session policy. Short on purpose: this is the surface that can reveal every\n// secret the app holds.\nexport const IDLE_MS = 30 * 60_000; // signed out after 30 minutes untouched\nexport const ABSOLUTE_MS = 12 * 3_600_000; // and after 12 hours regardless\nexport const PENDING_MS = 10 * 60_000; // password checked, code not yet: 10 minutes\nexport const STEP_UP_MS = 5 * 60_000; // a fresh code covers dangerous actions for 5 minutes\nexport const INVITE_HOURS = 72;\nexport const RECOVERY_HOURS = 24;\nexport const MAX_FAILURES = 5; // then locked, doubling from 1 minute, capped at an hour\n\nexport type SessionStage = 'password' | 'enroll' | 'active';\n\nexport interface IOperatorRow {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\tpasswordHash: string;\n\tscopes: AdminScope[];\n\ttotpSecret: string | null;\n\ttotpConfirmedAt: string | null;\n\ttotpLastStep: string | null; // BIGINT arrives as a string\n\tbackupCodes: string[];\n\tfailedAttempts: number;\n\tlockedUntil: string | null;\n\tcreatedBy: string;\n\tcreatedAt: string;\n\tlastLoginAt: string | null;\n\tdisabledAt: string | null;\n}\n\nexport interface ISessionRow {\n\tidHash: string;\n\toperatorId: string;\n\tstage: SessionStage;\n\tcreatedAt: string;\n\tlastSeenAt: string;\n\texpiresAt: string;\n\tstepUpAt: string | null;\n}\n\nconst OP_COLS = `id, email, name, password_hash AS \"passwordHash\", scopes, totp_secret AS \"totpSecret\",\n totp_confirmed_at AS \"totpConfirmedAt\", totp_last_step AS \"totpLastStep\", backup_codes AS \"backupCodes\",\n failed_attempts AS \"failedAttempts\", locked_until AS \"lockedUntil\", created_by AS \"createdBy\",\n created_at AS \"createdAt\", last_login_at AS \"lastLoginAt\", disabled_at AS \"disabledAt\"`;\n\nconst SESSION_COLS = `id_hash AS \"idHash\", operator_id AS \"operatorId\", stage, created_at AS \"createdAt\",\n last_seen_at AS \"lastSeenAt\", expires_at AS \"expiresAt\", step_up_at AS \"stepUpAt\"`;\n\nexport const normalizeEmail = (e: string): string => e.trim().toLowerCase();\nexport const EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n// ── operators ──────────────────────────────────────────────────────────────\n\nexport async function operatorCount(store: IStoreAdapter): Promise<number> {\n\tconst [row] = await store.query<{ n: string }>(\n\t\t`SELECT count(*)::text AS n FROM fonderie_admin_operators`,\n\t);\n\treturn Number(row?.n ?? 0);\n}\n\nexport async function findOperator(\n\tstore: IStoreAdapter,\n\tby: { id?: string; email?: string },\n): Promise<IOperatorRow | null> {\n\tconst [row] = by.id\n\t\t? await store.query<IOperatorRow>(\n\t\t\t\t`SELECT ${OP_COLS} FROM fonderie_admin_operators WHERE id = $1`,\n\t\t\t\t[by.id],\n\t\t\t)\n\t\t: await store.query<IOperatorRow>(\n\t\t\t\t`SELECT ${OP_COLS} FROM fonderie_admin_operators WHERE email = $1`,\n\t\t\t\t[normalizeEmail(by.email ?? '')],\n\t\t\t);\n\treturn row ?? null;\n}\n\n/**\n * The one-time claim: creates the first operator, with every scope, only while\n * none exists. Serialized with an advisory lock so two simultaneous claims\n * cannot both succeed.\n */\nexport async function claimFirstOperator(\n\tstore: IStoreAdapter,\n\tinput: { email: string; name?: string | undefined; password: string },\n): Promise<IOperatorRow | null> {\n\tconst passwordHash = await hashPassword(input.password);\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(`SELECT pg_advisory_xact_lock(hashtext('fonderie_admin_claim'))`);\n\t\tconst [row] = await tx.query<IOperatorRow>(\n\t\t\t`INSERT INTO fonderie_admin_operators (email, name, password_hash, scopes, created_by)\n\t\t\t SELECT $1, $2, $3, $4, 'claim'\n\t\t\t WHERE NOT EXISTS (SELECT 1 FROM fonderie_admin_operators)\n\t\t\t RETURNING ${OP_COLS}`,\n\t\t\t[normalizeEmail(input.email), input.name ?? null, passwordHash, ['read', 'write', 'secrets']],\n\t\t);\n\t\treturn row ?? null;\n\t});\n}\n\nexport async function recordFailure(store: IStoreAdapter, id: string): Promise<void> {\n\t// Past the limit, lock for 2^(n-limit) minutes, capped at an hour.\n\tawait store.query(\n\t\t`UPDATE fonderie_admin_operators\n\t\t SET failed_attempts = failed_attempts + 1,\n\t\t locked_until = CASE WHEN failed_attempts + 1 >= $2\n\t\t THEN now() + make_interval(mins => LEAST(60, power(2, failed_attempts + 1 - $2)::int))\n\t\t ELSE locked_until END\n\t\t WHERE id = $1`,\n\t\t[id, MAX_FAILURES],\n\t);\n}\n\nexport const isLocked = (op: IOperatorRow): boolean =>\n\top.lockedUntil !== null && new Date(op.lockedUntil).getTime() > Date.now();\n\nexport function lockMinutes(op: IOperatorRow): number {\n\treturn op.lockedUntil\n\t\t? Math.max(1, Math.ceil((new Date(op.lockedUntil).getTime() - Date.now()) / 60_000))\n\t\t: 0;\n}\n\n/** Password check that costs the same whether or not the email exists. */\nexport async function checkPassword(store: IStoreAdapter, email: string, password: string) {\n\tconst op = await findOperator(store, { email });\n\tif (!op) {\n\t\tawait verifyPassword(password, await dummyHash());\n\t\treturn { op: null, ok: false } as const;\n\t}\n\tif (op.disabledAt) {\n\t\tawait verifyPassword(password, await dummyHash());\n\t\treturn { op: null, ok: false } as const;\n\t}\n\tif (isLocked(op)) return { op, ok: false, locked: true } as const;\n\tconst ok = await verifyPassword(password, op.passwordHash);\n\tif (!ok) await recordFailure(store, op.id);\n\treturn { op, ok } as const;\n}\n\n/** A time-step or a backup code. Consumes the backup code; records the step. */\nexport async function checkSecondFactor(\n\tstore: IStoreAdapter,\n\tbox: ISecretBox,\n\top: IOperatorRow,\n\tinput: { code?: unknown; backupCode?: unknown },\n): Promise<{ ok: boolean; via?: 'totp' | 'backup'; backupLeft?: number }> {\n\tif (isLocked(op)) return { ok: false };\n\tif (typeof input.code === 'string' && op.totpSecret) {\n\t\tconst last = op.totpLastStep === null ? null : Number(op.totpLastStep);\n\t\tconst step = verifyTotp(box.open(op.totpSecret), input.code, last);\n\t\tif (step !== null) {\n\t\t\t// Conditional on the step still being newer: two requests racing with\n\t\t\t// the same code cannot both pass.\n\t\t\tconst rows = await store.query(\n\t\t\t\t`UPDATE fonderie_admin_operators SET totp_last_step = $2, failed_attempts = 0, locked_until = NULL\n\t\t\t\t WHERE id = $1 AND (totp_last_step IS NULL OR totp_last_step < $2) RETURNING id`,\n\t\t\t\t[op.id, step],\n\t\t\t);\n\t\t\tif (rows.length) return { ok: true, via: 'totp' };\n\t\t}\n\t} else if (typeof input.backupCode === 'string') {\n\t\tconst h = sha256(normalizeBackupCode(input.backupCode));\n\t\tconst [row] = await store.query<{ left: number | null }>(\n\t\t\t`UPDATE fonderie_admin_operators\n\t\t\t SET backup_codes = array_remove(backup_codes, $2), failed_attempts = 0, locked_until = NULL\n\t\t\t WHERE id = $1 AND $2 = ANY(backup_codes)\n\t\t\t RETURNING coalesce(array_length(backup_codes, 1), 0) AS left`,\n\t\t\t[op.id, h],\n\t\t);\n\t\tif (row) return { ok: true, via: 'backup', backupLeft: Number(row.left ?? 0) };\n\t}\n\tawait recordFailure(store, op.id);\n\treturn { ok: false };\n}\n\n/** The secret to show while enrolling. Created once, kept until confirmed. */\nexport async function enrollmentSecret(\n\tstore: IStoreAdapter,\n\tbox: ISecretBox,\n\top: IOperatorRow,\n): Promise<string> {\n\tif (op.totpSecret && !op.totpConfirmedAt) return box.open(op.totpSecret);\n\tconst secret = newTotpSecret();\n\tawait store.query(\n\t\t`UPDATE fonderie_admin_operators SET totp_secret = $2 WHERE id = $1 AND totp_confirmed_at IS NULL`,\n\t\t[op.id, box.seal(secret)],\n\t);\n\treturn secret;\n}\n\nexport async function confirmEnrollment(\n\tstore: IStoreAdapter,\n\tbox: ISecretBox,\n\top: IOperatorRow,\n\tcode: string,\n): Promise<string[] | null> {\n\tif (!op.totpSecret || op.totpConfirmedAt || isLocked(op)) return null;\n\tconst step = verifyTotp(box.open(op.totpSecret), code, null);\n\tif (step === null) {\n\t\tawait recordFailure(store, op.id);\n\t\treturn null;\n\t}\n\tconst codes = newBackupCodes();\n\tawait store.query(\n\t\t`UPDATE fonderie_admin_operators\n\t\t SET totp_confirmed_at = now(), totp_last_step = $2, backup_codes = $3,\n\t\t failed_attempts = 0, locked_until = NULL, last_login_at = now()\n\t\t WHERE id = $1`,\n\t\t[op.id, step, codes.map((c) => sha256(normalizeBackupCode(c)))],\n\t);\n\treturn codes;\n}\n\nexport async function listOperators(store: IStoreAdapter) {\n\tconst operators = await store.query<IOperatorRow>(\n\t\t`SELECT ${OP_COLS} FROM fonderie_admin_operators ORDER BY created_at`,\n\t);\n\tconst links = await store.query<{\n\t\tid: string;\n\t\tkind: string;\n\t\temail: string;\n\t\tscopes: AdminScope[];\n\t\tcreatedBy: string;\n\t\tcreatedAt: string;\n\t\texpiresAt: string;\n\t}>(\n\t\t`SELECT id, kind, email, scopes, created_by AS \"createdBy\", created_at AS \"createdAt\", expires_at AS \"expiresAt\"\n\t\t FROM fonderie_admin_invites WHERE used_at IS NULL AND expires_at > now() ORDER BY created_at DESC`,\n\t);\n\treturn { operators: operators.map(publicOperator), links };\n}\n\n/** What the API ever says about an operator: never a hash, secret or code. */\nexport function publicOperator(op: IOperatorRow) {\n\treturn {\n\t\tid: op.id,\n\t\temail: op.email,\n\t\tname: op.name,\n\t\tscopes: op.scopes,\n\t\tenrolled: op.totpConfirmedAt !== null,\n\t\tbackupCodesLeft: op.backupCodes.length,\n\t\tlocked: isLocked(op),\n\t\tcreatedBy: op.createdBy,\n\t\tcreatedAt: op.createdAt,\n\t\tlastLoginAt: op.lastLoginAt,\n\t\tdisabledAt: op.disabledAt,\n\t};\n}\n\n// ── links (invite / recovery) ──────────────────────────────────────────────\n\nexport async function createLink(\n\tstore: IStoreAdapter,\n\tinput: {\n\t\tkind: 'invite' | 'recovery';\n\t\temail: string;\n\t\tscopes?: AdminScope[];\n\t\toperatorId?: string;\n\t\tcreatedBy: string;\n\t\thours: number;\n\t},\n): Promise<{ id: string; token: string; expiresAt: string }> {\n\tconst token = newOpaqueToken(input.kind === 'invite' ? 'fai' : 'far');\n\tif (input.operatorId) {\n\t\t// A new recovery link retires any earlier one for the same operator.\n\t\tawait store.query(\n\t\t\t`UPDATE fonderie_admin_invites SET used_at = now() WHERE operator_id = $1 AND used_at IS NULL`,\n\t\t\t[input.operatorId],\n\t\t);\n\t}\n\tconst [row] = await store.query<{ id: string; expiresAt: string }>(\n\t\t`INSERT INTO fonderie_admin_invites (kind, token_hash, email, scopes, operator_id, created_by, expires_at)\n\t\t VALUES ($1, $2, $3, $4, $5, $6, now() + make_interval(hours => $7)) RETURNING id, expires_at AS \"expiresAt\"`,\n\t\t[\n\t\t\tinput.kind,\n\t\t\tsha256(token),\n\t\t\tnormalizeEmail(input.email),\n\t\t\tinput.scopes ?? [],\n\t\t\tinput.operatorId ?? null,\n\t\t\tinput.createdBy,\n\t\t\tinput.hours,\n\t\t],\n\t);\n\tif (!row) throw new Error('[admin] link insert returned no row');\n\treturn { id: row.id, token, expiresAt: row.expiresAt };\n}\n\nexport async function findLink(store: IStoreAdapter, token: string) {\n\tconst [row] = await store.query<{\n\t\tid: string;\n\t\tkind: 'invite' | 'recovery';\n\t\temail: string;\n\t\tscopes: AdminScope[];\n\t\toperatorId: string | null;\n\t\tcreatedBy: string;\n\t}>(\n\t\t`SELECT id, kind, email, scopes, operator_id AS \"operatorId\", created_by AS \"createdBy\"\n\t\t FROM fonderie_admin_invites WHERE token_hash = $1 AND used_at IS NULL AND expires_at > now()`,\n\t\t[sha256(token)],\n\t);\n\treturn row ?? null;\n}\n\n/**\n * Redeem a link: an invite creates the operator, a recovery resets password and\n * second factor. Single-use, atomically. Returns the operator, or a reason.\n */\nexport async function redeemLink(\n\tstore: IStoreAdapter,\n\ttoken: string,\n\tinput: { password: string; name?: string | undefined },\n): Promise<{ op: IOperatorRow } | { error: 'INVALID_LINK' | 'ALREADY_OPERATOR' }> {\n\tconst passwordHash = await hashPassword(input.password);\n\treturn store\n\t\t.transaction(async (tx) => {\n\t\t\tconst [link] = await tx.query<{\n\t\t\t\tkind: string;\n\t\t\t\temail: string;\n\t\t\t\tscopes: AdminScope[];\n\t\t\t\toperatorId: string | null;\n\t\t\t\tcreatedBy: string;\n\t\t\t}>(\n\t\t\t\t`UPDATE fonderie_admin_invites SET used_at = now()\n\t\t\t WHERE token_hash = $1 AND used_at IS NULL AND expires_at > now()\n\t\t\t RETURNING kind, email, scopes, operator_id AS \"operatorId\", created_by AS \"createdBy\"`,\n\t\t\t\t[sha256(token)],\n\t\t\t);\n\t\t\tif (!link) return { error: 'INVALID_LINK' as const };\n\t\t\tif (link.kind === 'invite') {\n\t\t\t\tconst [op] = await tx.query<IOperatorRow>(\n\t\t\t\t\t`INSERT INTO fonderie_admin_operators (email, name, password_hash, scopes, created_by)\n\t\t\t\t VALUES ($1, $2, $3, $4, $5) ON CONFLICT (email) DO NOTHING RETURNING ${OP_COLS}`,\n\t\t\t\t\t[link.email, input.name ?? null, passwordHash, link.scopes, link.createdBy],\n\t\t\t\t);\n\t\t\t\tif (!op)\n\t\t\t\t\tthrow Object.assign(new Error('already an operator'), { code: 'ALREADY_OPERATOR' });\n\t\t\t\treturn { op };\n\t\t\t}\n\t\t\tconst [op] = await tx.query<IOperatorRow>(\n\t\t\t\t`UPDATE fonderie_admin_operators\n\t\t\t SET password_hash = $2, totp_secret = NULL, totp_confirmed_at = NULL, totp_last_step = NULL,\n\t\t\t backup_codes = '{}', failed_attempts = 0, locked_until = NULL\n\t\t\t WHERE id = $1 AND disabled_at IS NULL RETURNING ${OP_COLS}`,\n\t\t\t\t[link.operatorId, passwordHash],\n\t\t\t);\n\t\t\tif (!op) return { error: 'INVALID_LINK' as const };\n\t\t\tawait tx.query(`DELETE FROM fonderie_admin_sessions WHERE operator_id = $1`, [op.id]);\n\t\t\treturn { op };\n\t\t})\n\t\t.catch((err: unknown) => {\n\t\t\tif ((err as { code?: string }).code === 'ALREADY_OPERATOR')\n\t\t\t\treturn { error: 'ALREADY_OPERATOR' as const };\n\t\t\tthrow err;\n\t\t});\n}\n\n// ── sessions ───────────────────────────────────────────────────────────────\n\nexport async function createSession(\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\toperatorId: string,\n\tstage: SessionStage,\n): Promise<string> {\n\tconst id = newOpaqueToken('fas');\n\tconst ttl = stage === 'active' ? ABSOLUTE_MS : PENDING_MS;\n\tawait store.query(\n\t\t`INSERT INTO fonderie_admin_sessions (id_hash, operator_id, stage, expires_at, client_ip, user_agent)\n\t\t VALUES ($1, $2, $3, now() + make_interval(secs => $4), $5, $6)`,\n\t\t[\n\t\t\tsha256(id),\n\t\t\toperatorId,\n\t\t\tstage,\n\t\t\tttl / 1000,\n\t\t\tctx.meta.clientIp ?? null,\n\t\t\tctx.request.headers.get('user-agent')?.slice(0, 300) ?? null,\n\t\t],\n\t);\n\treturn id;\n}\n\nexport async function deleteSession(store: IStoreAdapter, id: string): Promise<void> {\n\tawait store.query(`DELETE FROM fonderie_admin_sessions WHERE id_hash = $1`, [sha256(id)]);\n}\n\nexport async function deleteOperatorSessions(\n\tstore: IStoreAdapter,\n\toperatorId: string,\n): Promise<void> {\n\tawait store.query(`DELETE FROM fonderie_admin_sessions WHERE operator_id = $1`, [operatorId]);\n}\n\n/** The session behind a cookie value, if it is still alive (absolute and idle). */\nexport async function readSession(\n\tstore: IStoreAdapter,\n\tid: string,\n): Promise<{ session: ISessionRow; op: IOperatorRow } | null> {\n\tif (!id) return null;\n\tconst [session] = await store.query<ISessionRow>(\n\t\t`SELECT ${SESSION_COLS} FROM fonderie_admin_sessions WHERE id_hash = $1 AND expires_at > now()`,\n\t\t[sha256(id)],\n\t);\n\tif (!session) return null;\n\tif (session.stage === 'active' && Date.now() - new Date(session.lastSeenAt).getTime() > IDLE_MS) {\n\t\tawait store.query(`DELETE FROM fonderie_admin_sessions WHERE id_hash = $1`, [session.idHash]);\n\t\treturn null;\n\t}\n\tconst op = await findOperator(store, { id: session.operatorId });\n\tif (!op || op.disabledAt) return null;\n\t// Touch at most once a minute: idle tracking without a write per request.\n\tif (Date.now() - new Date(session.lastSeenAt).getTime() > 60_000) {\n\t\tvoid store\n\t\t\t.query(`UPDATE fonderie_admin_sessions SET last_seen_at = now() WHERE id_hash = $1`, [\n\t\t\t\tsession.idHash,\n\t\t\t])\n\t\t\t.catch(() => undefined);\n\t}\n\treturn { session, op };\n}\n\nexport const stepUpFresh = (s: ISessionRow): boolean =>\n\ts.stepUpAt !== null && Date.now() - new Date(s.stepUpAt).getTime() < STEP_UP_MS;\n\nexport async function markStepUp(store: IStoreAdapter, idHash: string): Promise<void> {\n\tawait store.query(`UPDATE fonderie_admin_sessions SET step_up_at = now() WHERE id_hash = $1`, [\n\t\tidHash,\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/* ── Console chrome. Pseudo-selectors the inline-styled screens cannot carry.\n Each one only refines: embedded without this shell, a screen loses hover\n states and keeps working. ── */\nbutton{transition:background-color .12s,border-color .12s,color .12s,opacity .12s}\nbutton:disabled{opacity:.5;cursor:default}\n.fonderie-admin-main button:not(:disabled):hover{opacity:.82}\n.fonderie-admin-main button:not(:disabled):active{opacity:.7}\n.fonderie-admin-nav-item:hover{background:color-mix(in srgb,var(--fonderie-text) 5%,transparent)!important;color:var(--fonderie-text)!important}\n.fonderie-admin-nav-item[aria-current=\"page\"]:hover{background:color-mix(in srgb,var(--fonderie-accent) 16%,transparent)!important}\n/* Tables and lists are framed cards; the frame is the last row's rule. */\n.fonderie-admin-main tbody tr:last-child>td{border-bottom:none!important}\n.fonderie-admin-main ul>li:last-child{border-bottom:none!important}\n.fonderie-admin-main tbody tr:hover>td{background:color-mix(in srgb,var(--fonderie-text) 2.5%,transparent)}\n/* Inputs carry their border inline, so focus is a ring, not a border change. */\ninput:focus-visible,select:focus-visible,textarea:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--fonderie-accent) 30%,transparent)}\ninput::placeholder,textarea::placeholder{color:var(--fonderie-text-muted);opacity:.75}\n/* In the sidebar footer the switcher spans the column. */\nnav .theme-switch{display:flex;width:100%;box-sizing:border-box}\nnav .theme-switch__option{flex:1;justify-content:center;padding:5px 6px}\n@media (max-width:820px){.fonderie-admin-main>div{padding:20px 16px 48px!important}}\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 =\n\t`(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","import_middlewares","module","module","name","module","name","name","import_node_crypto","import_core","import_core","import_core","password","scryptCb","email","password","token","op","email","token","token","bearer","name","token","body","name","module"]}