@fonderie/admin 0.5.0 → 0.7.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.
- package/README.md +56 -0
- package/brain/outcomes.md +17 -1
- package/brain/signatures.md +32 -1
- package/dist/index.cjs +229 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +218 -20
- package/dist/index.js.map +1 -1
- package/dist/migrations/sql/002_admin_tokens.sql +14 -0
- package/dist/ui/app.global.js +66 -0
- package/package.json +13 -5
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/module.ts","../src/doctor.ts","../src/log.ts","../src/manifest.ts","../src/pages.ts"],"sourcesContent":["export { AdminModule, ADMIN_VERSION, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS } from './module';\nexport { buildManifest } from './manifest';\nexport { runDoctor, collectChecks, attention } from './doctor';\nexport { adminLog, readAdminLog, DEFAULT_ACTOR, MAX_PAGE } from './log';\nexport { configReport, routesReport, tokensReport } from './pages';\nexport type {\n\tIAdminOptions,\n\tIAdminManifest,\n\tIAdminModuleEntry,\n\tIAdminCheckResult,\n\tIAdminDoctorReport,\n\tIAdminAttention,\n\tIAdminAttentionItem,\n\tIAdminLogEntry,\n\tIAdminLogPage,\n\tIAdminEnvEntry,\n\tIAdminConfigReport,\n\tAdminRouteGuard,\n\tIAdminRouteEntry,\n\tIAdminRoutesReport,\n\tIAdminTokenEntry,\n\tIAdminTokensReport,\n} from './types';\n","import type { IFonderieApp, IFonderieModule, IReadinessProblem, Middleware } from '@fonderie/core';\nimport { HTTP, setApiResponse } from '@fonderie/core';\nimport { requireAdminToken, validateAdminToken } from '@fonderie/core/middlewares';\n\nimport { attention, collectChecks, runDoctor } from './doctor';\nimport { adminLog, readAdminLog } from './log';\nimport { buildManifest } from './manifest';\nimport { configReport, routesReport, tokensReport } from './pages';\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.\nexport const ADMIN_VERSION: string = process.env['FONDERIE_ADMIN_VERSION'] ?? '0.0.0-dev';\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = (options.path ?? DEFAULT_ADMIN_PATH).replace(/\\/+$/, '');\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\tconst guard = requireAdminToken(token);\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// 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) }),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/doctor',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor()),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/config',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_CONFIG',\n\t\t\t\t\t\t'Declared vs held',\n\t\t\t\t\t\tconfigReport(app, this.options.env ?? []),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/routes',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_ROUTES', 'Exposed routes', routesReport(app, this.name)),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/access/tokens',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_TOKENS', 'Admin tokens', tokensReport(app, this.name)),\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\tconst mounted = new Map<string, string>();\n\t\tconst mount = (module: string, method: string, path: string, handlers: Middleware[]): 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 log sits before the guard: a refused request is a row too.\n\t\t\tconst chain = store\n\t\t\t\t? [adminLog(store, path, module), guard, ...handlers]\n\t\t\t\t: [guard, ...handlers];\n\t\t\tapp.addRoute(method, path, ...chain);\n\t\t};\n\n\t\tfor (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);\n\t\tfor (const { module, description } of app.adminDescriptions()) {\n\t\t\tfor (const r of description.routes ?? [])\n\t\t\t\tmount(module, r.method, this.path + r.path, r.handlers);\n\t\t}\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IAdminCheck, IAdminCheckReport, IFonderieApp } from '@fonderie/core';\n\nimport type { IAdminAttention, IAdminCheckResult, IAdminDoctorReport } from './types';\n\nexport interface INamedCheck extends IAdminCheck {\n\tmodule: string;\n}\n\n// Module checks first (sorted by module), then the app's own. A name used\n// twice would make a report ambiguous, so it is refused up front.\nexport function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[] {\n\tconst seen = new Map<string, string>();\n\tconst out: INamedCheck[] = [];\n\tconst add = (module: string, check: IAdminCheck): void => {\n\t\tconst prior = seen.get(check.name);\n\t\tif (prior)\n\t\t\tthrow new Error(\n\t\t\t\t`[fonderie] ${module} cannot offer check \"${check.name}\": ${prior} already does`,\n\t\t\t);\n\t\tseen.set(check.name, module);\n\t\tout.push({ ...check, module });\n\t};\n\tfor (const { module, description } of app.adminDescriptions()) {\n\t\tfor (const c of description.checks ?? []) add(module, c);\n\t}\n\tfor (const c of own) add('application', c);\n\treturn out;\n}\n\nasync function runOne(check: INamedCheck, timeoutMs: number): Promise<IAdminCheckResult> {\n\tconst started = Date.now();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<IAdminCheckReport>((resolve) => {\n\t\ttimer = setTimeout(\n\t\t\t() => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet report: IAdminCheckReport;\n\ttry {\n\t\treport = await Promise.race([check.run(), timeout]);\n\t} catch (err) {\n\t\treport = {\n\t\t\tok: false,\n\t\t\tfindings: [`check threw: ${err instanceof Error ? err.message : String(err)}`],\n\t\t};\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n\treturn { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };\n}\n\nexport async function runDoctor(\n\tchecks: INamedCheck[],\n\ttimeoutMs: number,\n): Promise<IAdminDoctorReport> {\n\tconst results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));\n\treturn { generatedAt: new Date().toISOString(), ok: results.every((r) => r.ok), checks: results };\n}\n\n// What needs the operator today: readiness problems as reported, failed\n// checks as errors, findings on a passing check as advice. Empty = green.\nexport function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention {\n\tconst items: IAdminAttention['items'] = [];\n\tfor (const p of app.checkProductionReadiness().problems) {\n\t\titems.push({\n\t\t\tsource: p.module,\n\t\t\tseverity: p.severity === 'error' ? 'error' : 'advice',\n\t\t\tmessage: p.message,\n\t\t});\n\t}\n\tfor (const c of doctor.checks) {\n\t\tif (c.skipped) continue;\n\t\tconst severity = c.ok ? 'advice' : 'error';\n\t\tif (!c.ok && c.findings.length === 0)\n\t\t\titems.push({ source: c.name, severity, message: 'failed' });\n\t\tfor (const f of c.findings) items.push({ source: c.name, severity, message: f });\n\t}\n\treturn { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === 'error'), items };\n}\n","import 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\tactor: ctx.request.headers.get('x-actor') || DEFAULT_ACTOR,\n\t\t\tmethod: ctx.request.method,\n\t\t\tpath: 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 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 },\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 { IAdminConfigReport, IAdminRoutesReport, IAdminTokensReport } from './types';\n\nexport function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems)\n\t\tbyModule.set(p.module, [...(byModule.get(p.module) ?? []), p]);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\treadiness: report.readiness,\n\t\tmodules: report.modules.map(({ name }) => ({ name, problems: byModule.get(name) ?? [] })),\n\t\tenv: env.map((name) => ({\n\t\t\tname,\n\t\t\tset: process.env[name] !== undefined && process.env[name] !== '',\n\t\t})),\n\t};\n}\n\nconst PROBES = new Set(['/healthz', '/readyz', '/metrics']);\n\nexport function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport {\n\treturn {\n\t\tgeneratedAt: new Date().toISOString(),\n\t\troutes: app.routes().map((r) => ({\n\t\t\t...r,\n\t\t\tguard:\n\t\t\t\tr.module === adminModule\n\t\t\t\t\t? 'admin'\n\t\t\t\t\t: PROBES.has(r.path) && r.module === '@fonderie/core'\n\t\t\t\t\t\t? 'probe'\n\t\t\t\t\t\t: 'app',\n\t\t})),\n\t};\n}\n\n// A brick's own adminToken shows as a readiness problem only when weak, so\n// \"set at all\" has to be asked; a described module that also registers a\n// legacy route under /admin, /plans or /billing/wallet/grant has one.\nexport function tokensReport(app: IFonderieApp, adminModule: string): 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};\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,kBAAqC;AACrC,yBAAsD;;;ACQ/C,SAAS,cAAc,KAAmB,KAAmC;AACnF,QAAM,OAAO,oBAAI,IAAoB;AACrC,QAAM,MAAqB,CAAC;AAC5B,QAAM,MAAM,CAACA,SAAgB,UAA6B;AACzD,UAAM,QAAQ,KAAK,IAAI,MAAM,IAAI;AACjC,QAAI;AACH,YAAM,IAAI;AAAA,QACT,cAAcA,OAAM,wBAAwB,MAAM,IAAI,MAAM,KAAK;AAAA,MAClE;AACD,SAAK,IAAI,MAAM,MAAMA,OAAM;AAC3B,QAAI,KAAK,EAAE,GAAG,OAAO,QAAAA,QAAO,CAAC;AAAA,EAC9B;AACA,aAAW,EAAE,QAAAA,SAAQ,YAAY,KAAK,IAAI,kBAAkB,GAAG;AAC9D,eAAW,KAAK,YAAY,UAAU,CAAC,EAAG,KAAIA,SAAQ,CAAC;AAAA,EACxD;AACA,aAAW,KAAK,IAAK,KAAI,eAAe,CAAC;AACzC,SAAO;AACR;AAEA,eAAe,OAAO,OAAoB,WAA+C;AACxF,QAAM,UAAU,KAAK,IAAI;AACzB,MAAI;AACJ,QAAM,UAAU,IAAI,QAA2B,CAAC,YAAY;AAC3D,YAAQ;AAAA,MACP,MAAM,QAAQ,EAAE,IAAI,OAAO,UAAU,CAAC,mBAAmB,SAAS,KAAK,EAAE,CAAC;AAAA,MAC1E;AAAA,IACD;AAAA,EACD,CAAC;AACD,MAAI;AACJ,MAAI;AACH,aAAS,MAAM,QAAQ,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC;AAAA,EACnD,SAAS,KAAK;AACb,aAAS;AAAA,MACR,IAAI;AAAA,MACJ,UAAU,CAAC,gBAAgB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,IAC9E;AAAA,EACD,UAAE;AACD,QAAI,MAAO,cAAa,KAAK;AAAA,EAC9B;AACA,SAAO,EAAE,MAAM,MAAM,MAAM,QAAQ,MAAM,QAAQ,GAAG,QAAQ,YAAY,KAAK,IAAI,IAAI,QAAQ;AAC9F;AAEA,eAAsB,UACrB,QACA,WAC8B;AAC9B,QAAM,UAAU,MAAM,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC;AACzE,SAAO,EAAE,cAAa,oBAAI,KAAK,GAAE,YAAY,GAAG,IAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,EAAE,GAAG,QAAQ,QAAQ;AACjG;AAIO,SAAS,UAAU,KAAmB,QAA6C;AACzF,QAAM,QAAkC,CAAC;AACzC,aAAW,KAAK,IAAI,yBAAyB,EAAE,UAAU;AACxD,UAAM,KAAK;AAAA,MACV,QAAQ,EAAE;AAAA,MACV,UAAU,EAAE,aAAa,UAAU,UAAU;AAAA,MAC7C,SAAS,EAAE;AAAA,IACZ,CAAC;AAAA,EACF;AACA,aAAW,KAAK,OAAO,QAAQ;AAC9B,QAAI,EAAE,QAAS;AACf,UAAM,WAAW,EAAE,KAAK,WAAW;AACnC,QAAI,CAAC,EAAE,MAAM,EAAE,SAAS,WAAW;AAClC,YAAM,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,SAAS,SAAS,CAAC;AAC3D,eAAW,KAAK,EAAE,SAAU,OAAM,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,SAAS,EAAE,CAAC;AAAA,EAChF;AACA,SAAO,EAAE,aAAa,OAAO,aAAa,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,MAAM;AACjG;;;AC1EO,IAAM,gBAAgB;AAItB,SAAS,SAAS,OAAsB,OAAeC,SAA4B;AACzF,SAAO,OAAO,KAAuB,SAAS;AAC7C,UAAM,UAAU,KAAK,IAAI;AACzB,UAAM,MAAM,MAAM,KAAK;AACvB,UAAM,MAAM;AAAA,MACX,OAAO,IAAI,QAAQ,QAAQ,IAAI,SAAS,KAAK;AAAA,MAC7C,QAAQ,IAAI,QAAQ;AAAA,MACpB,MAAM,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE;AAAA,MAC/B;AAAA,MACA,QAAAA;AAAA,MACA,QAAQ,IAAI;AAAA,MACZ,YAAY,KAAK,IAAI,IAAI;AAAA,MACzB,WAAW,OAAO,IAAI,KAAK,WAAW,MAAM,WAAW,IAAI,KAAK,WAAW,IAAI;AAAA,MAC/E,UAAU,IAAI,KAAK,YAAY;AAAA,IAChC;AACA,QAAI;AACH,YAAM,MAAM;AAAA,QACX;AAAA;AAAA;AAAA,QAGA;AAAA,UACC,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,QACL;AAAA,MACD;AAAA,IACD,SAAS,KAAK;AACb,cAAQ;AAAA,QACP;AAAA,QACA,eAAe,QAAQ,IAAI,UAAU;AAAA,MACtC;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;AAEO,IAAM,WAAW;AAGxB,eAAsB,aACrB,OACA,OAA4C,CAAC,GACpB;AACzB,QAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,SAAS,IAAI,CAAC,GAAG,QAAQ;AAC9D,QAAM,SAAS,KAAK,SAAS,aAAa,KAAK,MAAM,IAAI;AACzD,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,CAAC,QAAQ,MAAM,MAAM,QAAQ,MAAM,MAAM,QAAQ,CAAC;AAAA,EACnD;AACA,QAAM,OAAO,KAAK,MAAM,GAAG,KAAK;AAChC,QAAM,OAAO,KAAK,KAAK,SAAS,CAAC;AACjC,SAAO;AAAA,IACN,SAAS;AAAA,IACT,MAAM,KAAK,SAAS,SAAS,OAAO,aAAa,IAAI,IAAI;AAAA,EAC1D;AACD;AAEA,SAAS,aAAa,GAA2B;AAChD,SAAO,OAAO,KAAK,GAAG,IAAI,KAAK,EAAE,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,MAAM,EAAE,SAAS,WAAW;AAC3F;AAEA,SAAS,aAAa,GAA8C;AACnE,QAAM,CAAC,IAAI,EAAE,IAAI,OAAO,KAAK,GAAG,WAAW,EAAE,SAAS,MAAM,EAAE,MAAM,GAAG;AACvE,SAAO,MAAM,KAAK,EAAE,IAAI,GAAG,IAAI;AAChC;;;AChFO,SAAS,cACf,KACA,OACiB;AACjB,QAAM,SAAS,IAAI,eAAe;AAClC,QAAM,WAAW,oBAAI,IAAiC;AACtD,aAAW,KAAK,OAAO,UAAU,UAAU;AAC1C,UAAM,OAAO,SAAS,IAAI,EAAE,MAAM,KAAK,CAAC;AACxC,SAAK,KAAK,CAAC;AACX,aAAS,IAAI,EAAE,QAAQ,IAAI;AAAA,EAC5B;AACA,QAAM,aAAa,IAAI,IAAI,IAAI,kBAAkB,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AACvE,QAAM,UAA+B,OAAO,QAAQ,IAAI,CAAC,EAAE,MAAM,QAAQ,MAAM;AAC9E,UAAM,WAAW,SAAS,IAAI,IAAI,KAAK,CAAC;AACxC,WAAO;AAAA,MACN;AAAA,MACA,SAAS,WAAW;AAAA,MACpB,WAAW,EAAE,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,SAAS;AAAA,MACzE,gBAAgB,WAAW,IAAI,IAAI;AAAA,IACpC;AAAA,EACD,CAAC;AACD,SAAO;AAAA,IACN,aAAa,OAAO;AAAA,IACpB,KAAK,OAAO;AAAA,IACZ;AAAA,IACA;AAAA,IACA,WAAW,OAAO;AAAA,IAClB,QAAQ,IAAI,OAAO;AAAA,EACpB;AACD;;;AC7BO,SAAS,aAAa,KAAmB,KAAmC;AAClF,QAAM,SAAS,IAAI,eAAe;AAClC,QAAM,WAAW,oBAAI,IAAiC;AACtD,aAAW,KAAK,OAAO,UAAU;AAChC,aAAS,IAAI,EAAE,QAAQ,CAAC,GAAI,SAAS,IAAI,EAAE,MAAM,KAAK,CAAC,GAAI,CAAC,CAAC;AAC9D,SAAO;AAAA,IACN,aAAa,OAAO;AAAA,IACpB,WAAW,OAAO;AAAA,IAClB,SAAS,OAAO,QAAQ,IAAI,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,SAAS,IAAI,IAAI,KAAK,CAAC,EAAE,EAAE;AAAA,IACxF,KAAK,IAAI,IAAI,CAAC,UAAU;AAAA,MACvB;AAAA,MACA,KAAK,QAAQ,IAAI,IAAI,MAAM,UAAa,QAAQ,IAAI,IAAI,MAAM;AAAA,IAC/D,EAAE;AAAA,EACH;AACD;AAEA,IAAM,SAAS,oBAAI,IAAI,CAAC,YAAY,WAAW,UAAU,CAAC;AAEnD,SAAS,aAAa,KAAmB,aAAyC;AACxF,SAAO;AAAA,IACN,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,IAAI,OAAO,EAAE,IAAI,CAAC,OAAO;AAAA,MAChC,GAAG;AAAA,MACH,OACC,EAAE,WAAW,cACV,UACA,OAAO,IAAI,EAAE,IAAI,KAAK,EAAE,WAAW,mBAClC,UACA;AAAA,IACN,EAAE;AAAA,EACH;AACD;AAKO,SAAS,aAAa,KAAmB,aAAyC;AACxF,QAAM,SAAS,IAAI,eAAe;AAClC,QAAM,gBAAgB,OAAO,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,WAAW,WAAW;AACtF,QAAM,eAAe,IAAI;AAAA,IACxB,IACE,OAAO,EACP,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,eAAe,kBAAkB,EAAE,QAAQ,EAAE,IAAI,CAAC,EACzF,IAAI,CAAC,MAAM,EAAE,MAAgB;AAAA,EAChC;AACA,SAAO;AAAA,IACN,aAAa,OAAO;AAAA,IACpB,OAAO,EAAE,IAAI,CAAC,cAAc,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,UAAU,cAAc;AAAA,IACzF,QAAQ,OAAO,QACb,IAAI,CAAC,EAAE,KAAK,OAAO,EAAE,QAAQ,MAAM,KAAK,aAAa,IAAI,IAAI,EAAE,EAAE,EACjE,OAAO,CAAC,MAAM,EAAE,GAAG;AAAA,EACtB;AACD;AAEA,SAAS,kBAAkB,QAAgB,MAAuB;AACjE,MAAI,4CAA4C,KAAK,IAAI,EAAG,QAAO;AACnE,MAAI,gBAAgB,KAAK,IAAI,KAAK,WAAW,MAAO,QAAO;AAC3D,SAAO,4BAA4B,KAAK,IAAI;AAC7C;;;AJpDO,IAAM,qBAAqB;AAC3B,IAAM,2BAA2B;AAGjC,IAAM,gBAAwB;AAE9B,IAAM,cAAN,MAA6C;AAAA,EAKnD,YAAoB,UAAyB,CAAC,GAAG;AAA7B;AACnB,SAAK,QAAQ,QAAQ,QAAQ,oBAAoB,QAAQ,QAAQ,EAAE;AAAA,EACpE;AAAA,EAFoB;AAAA,EAJX,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA,EAMT,QAAQ,KAAyB;AAChC,UAAM,QAAQ,KAAK,QAAQ;AAC3B,QAAI,CAAC,MAAO;AAEZ,QAAI,QAAQ,KAAK,IAAI;AACrB,UAAM,YAAQ,sCAAkB,KAAK;AAErC,UAAM,KAAK,CAAC,MAAsB,KAAK,OAAO,EAAE,MAAM,mBAAmB,MAAM;AAE/E,UAAM,QAAQ,KAAK,QAAQ;AAE3B,UAAM,SAAS,cAAc,KAAK,KAAK,QAAQ,UAAU,CAAC,CAAC;AAC3D,UAAM,YAAY,KAAK,QAAQ,kBAAkB;AACjD,UAAM,SAAS,MAAM,UAAU,QAAQ,SAAS;AAEhD,UAAM,MAA2C;AAAA,MAChD;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC;AAAA,UACC,iBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,UAAU,KAAK,MAAM,OAAO,CAAC;AAAA,QAC9B;AAAA,MACF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC;AAAA,UACC,iBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,cAAc,KAAK,EAAE,SAAS,KAAK,SAAS,KAAK,QAAQ,KAAK,EAAE,CAAC;AAAA,QAClE;AAAA,MACF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC,4BAAe,iBAAK,IAAI,gBAAgB,yBAAyB,MAAM,OAAO,CAAC;AAAA,MACjF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC;AAAA,UACC,iBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,aAAa,KAAK,KAAK,QAAQ,OAAO,CAAC,CAAC;AAAA,QACzC;AAAA,MACF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC,4BAAe,iBAAK,IAAI,gBAAgB,kBAAkB,aAAa,KAAK,KAAK,IAAI,CAAC;AAAA,MACxF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC,4BAAe,iBAAK,IAAI,gBAAgB,gBAAgB,aAAa,KAAK,KAAK,IAAI,CAAC;AAAA,MACtF;AAAA,IACD;AACA,QAAI,OAAO;AACV,UAAI,KAAK;AAAA,QACR;AAAA,QACA;AAAA,QACA,OAAO,QAAQ;AACd,gBAAM,IAAI,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE;AACnC,gBAAM,QAAQ,OAAO,EAAE,IAAI,OAAO,CAAC,KAAK;AACxC,gBAAM,SAAS,EAAE,IAAI,QAAQ,KAAK;AAClC,gBAAM,OAAO,MAAM,aAAa,OAAO;AAAA,YACtC,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,YACzB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,UAC5B,CAAC;AACD,qBAAO,4BAAe,iBAAK,IAAI,aAAa,kBAAkB,IAAI;AAAA,QACnE;AAAA,MACD,CAAC;AAAA,IACF;AACA,UAAM,UAAU,oBAAI,IAAoB;AACxC,UAAM,QAAQ,CAACC,SAAgB,QAAgB,MAAc,aAAiC;AAC7F,YAAM,MAAM,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI;AAC3C,YAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,UAAI,OAAO;AACV,cAAM,IAAI;AAAA,UACT,cAAcA,OAAM,oBAAoB,GAAG,KAAK,KAAK;AAAA,QACtD;AAAA,MACD;AACA,cAAQ,IAAI,KAAKA,OAAM;AAEvB,YAAM,QAAQ,QACX,CAAC,SAAS,OAAO,MAAMA,OAAM,GAAG,OAAO,GAAG,QAAQ,IAClD,CAAC,OAAO,GAAG,QAAQ;AACtB,UAAI,SAAS,QAAQ,MAAM,GAAG,KAAK;AAAA,IACpC;AAEA,eAAW,CAAC,QAAQ,MAAM,OAAO,KAAK,IAAK,OAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC;AACvF,eAAW,EAAE,QAAAA,SAAQ,YAAY,KAAK,IAAI,kBAAkB,GAAG;AAC9D,iBAAW,KAAK,YAAY,UAAU,CAAC;AACtC,cAAMA,SAAQ,EAAE,QAAQ,KAAK,OAAO,EAAE,MAAM,EAAE,QAAQ;AAAA,IACxD;AAAA,EACD;AAAA,EAEA,iBAAsC;AACrC,eAAO,uCAAmB,KAAK,QAAQ,YAAY,EAAE,QAAQ,KAAK,KAAK,CAAC;AAAA,EACzE;AACD;","names":["module","module","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/manifest.ts","../src/pages.ts","../src/tokens.ts","../src/ui/html.ts"],"sourcesContent":["export { AdminModule, ADMIN_VERSION, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS } from './module';\nexport { buildManifest } from './manifest';\nexport { runDoctor, collectChecks, attention } from './doctor';\nexport { adminLog, readAdminLog, DEFAULT_ACTOR, MAX_PAGE } from './log';\nexport { configReport, routesReport, tokensReport } from './pages';\nexport {\n\trequireAdminScope,\n\tscopeFor,\n\tgrants,\n\tissueToken,\n\trevokeToken,\n\tlistTokens,\n\thashToken,\n\tSCOPES,\n} from './tokens';\nexport type {\n\tIAdminOptions,\n\tIAdminManifest,\n\tIAdminModuleEntry,\n\tIAdminCheckResult,\n\tIAdminDoctorReport,\n\tIAdminAttention,\n\tIAdminAttentionItem,\n\tIAdminLogEntry,\n\tIAdminLogPage,\n\tIAdminEnvEntry,\n\tIAdminConfigReport,\n\tAdminRouteGuard,\n\tIAdminRouteEntry,\n\tIAdminRoutesReport,\n\tIAdminTokenEntry,\n\tIAdminTokensReport,\n\tAdminScope,\n\tIAdminTokenRecord,\n} 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 { buildManifest } from './manifest';\nimport { configReport, routesReport, tokensReport } from './pages';\nimport {\n\tissueToken,\n\tissueTokenSchema,\n\tlistTokens,\n\trequireAdminScope,\n\trevokeToken,\n\tscopeFor,\n} from './tokens';\nimport { uiHtml } from './ui/html';\nimport type { AdminScope } from './types';\nimport type { IAdminOptions } from './types';\n\nexport const DEFAULT_ADMIN_PATH = '/_admin';\nexport const DEFAULT_CHECK_TIMEOUT_MS = 10_000;\n\n// Replaced at build time (tsup env); the fallback is what tests see.\nexport const ADMIN_VERSION: string = process.env['FONDERIE_ADMIN_VERSION'] ?? '0.0.0-dev';\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = normalizeMountPath(options.path ?? DEFAULT_ADMIN_PATH);\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// 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) }),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/doctor',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor()),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/config',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_CONFIG',\n\t\t\t\t\t\t'Declared vs held',\n\t\t\t\t\t\tconfigReport(app, this.options.env ?? []),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/routes',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_ROUTES', 'Exposed routes', routesReport(app, this.name)),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/access/tokens',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_TOKENS',\n\t\t\t\t\t\t'Admin tokens',\n\t\t\t\t\t\ttokensReport(app, this.name, store ? await listTokens(store) : null),\n\t\t\t\t\t),\n\t\t\t],\n\t\t];\n\t\tif (store) {\n\t\t\town.push([\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/activity/admin-log',\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst q = new URL(ctx.request.url).searchParams;\n\t\t\t\t\tconst limit = Number(q.get('limit')) || undefined;\n\t\t\t\t\tconst before = q.get('before') ?? undefined;\n\t\t\t\t\tconst page = await readAdminLog(store, {\n\t\t\t\t\t\t...(limit ? { limit } : {}),\n\t\t\t\t\t\t...(before ? { before } : {}),\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'ADMIN_LOG', 'Admin activity', page);\n\t\t\t\t},\n\t\t\t]);\n\t\t}\n\t\t// Issuing and revoking are root-only: a scoped token can never mint one.\n\t\tconst rootOnly: Array<[string, string, Middleware[]]> = store\n\t\t\t? [\n\t\t\t\t\t[\n\t\t\t\t\t\t'POST',\n\t\t\t\t\t\t'/_admin/access/tokens',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tvalidate(issueTokenSchema),\n\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\tconst body = ctx.meta['body'] as {\n\t\t\t\t\t\t\t\t\tname: string;\n\t\t\t\t\t\t\t\t\tscopes: AdminScope[];\n\t\t\t\t\t\t\t\t\texpiresInDays?: number;\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\tconst createdBy = ctx.request.headers.get('x-actor') || 'admin-token';\n\t\t\t\t\t\t\t\tconst { token: plaintext, record } = await issueToken(store, {\n\t\t\t\t\t\t\t\t\t...body,\n\t\t\t\t\t\t\t\t\tcreatedBy,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t// The plaintext is returned once and never stored.\n\t\t\t\t\t\t\t\treturn setApiResponse(HTTP.CREATED, 'TOKEN_ISSUED', 'Token issued — shown once', {\n\t\t\t\t\t\t\t\t\ttoken: plaintext,\n\t\t\t\t\t\t\t\t\t...record,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t\t[\n\t\t\t\t\t\t'DELETE',\n\t\t\t\t\t\t'/_admin/access/tokens/:id',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\tconst ok = await revokeToken(store, ctx.meta.params?.['id'] ?? '');\n\t\t\t\t\t\t\t\treturn ok\n\t\t\t\t\t\t\t\t\t? setApiResponse(HTTP.OK, 'TOKEN_REVOKED', 'Token revoked')\n\t\t\t\t\t\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such live token');\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t]\n\t\t\t: [];\n\t\tconst mounted = new Map<string, string>();\n\t\tconst mount = (\n\t\t\tmodule: string,\n\t\t\tmethod: string,\n\t\t\tpath: string,\n\t\t\thandlers: Middleware[],\n\t\t\tneeded?: AdminScope | 'root',\n\t\t): void => {\n\t\t\tconst key = `${method.toUpperCase()} ${path}`;\n\t\t\tconst prior = mounted.get(key);\n\t\t\tif (prior) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[fonderie] ${module} cannot describe ${key}: ${prior} already describes it`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tmounted.set(key, module);\n\t\t\t// The scope comes from the route itself. The log sits before the guard:\n\t\t\t// a refused request is a row too.\n\t\t\tconst guard = requireAdminScope(token, store, needed ?? scopeFor(method, path));\n\t\t\tconst chain = store\n\t\t\t\t? [adminLog(store, path, module), guard, ...handlers]\n\t\t\t\t: [guard, ...handlers];\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\tapp.addRoute('GET', at('/_admin/ui'), async (ctx) => {\n\t\t\t\t// From the request, not from config: this is the only place that\n\t\t\t\t// knows basePath, a moved path AND a trailing slash at once. The\n\t\t\t\t// router's own normalizer, so the href and the routing cannot\n\t\t\t\t// disagree about what this path is.\n\t\t\t\tconst here = normalizeRequestPath(new URL(ctx.request.url).pathname);\n\t\t\t\treturn new Response(uiHtml(`${here}/app.js`), {\n\t\t\t\t\theaders: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },\n\t\t\t\t});\n\t\t\t});\n\t\t\tapp.addRoute('GET', scriptPath, async () => {\n\t\t\t\tconst body = loadJs();\n\t\t\t\treturn body\n\t\t\t\t\t? new Response(body, {\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t'content-type': 'text/javascript; charset=utf-8',\n\t\t\t\t\t\t\t\t'cache-control': 'public, max-age=300',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t: setApiResponse(\n\t\t\t\t\t\t\tHTTP.SERVICE_UNAVAILABLE,\n\t\t\t\t\t\t\t'UI_NOT_BUILT',\n\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);\n\t\t\t});\n\t\t}\n\n\t\tfor (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);\n\t\tfor (const [method, path, handlers] of rootOnly)\n\t\t\tmount(this.name, method, at(path), handlers, 'root');\n\t\tfor (const { module, description } of app.adminDescriptions()) {\n\t\t\tfor (const r of description.routes ?? [])\n\t\t\t\tmount(module, r.method, this.path + r.path, r.handlers);\n\t\t}\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IAdminCheck, IAdminCheckReport, IFonderieApp } from '@fonderie/core';\n\nimport type { IAdminAttention, IAdminCheckResult, IAdminDoctorReport } from './types';\n\nexport interface INamedCheck extends IAdminCheck {\n\tmodule: string;\n}\n\n// Module checks first (sorted by module), then the app's own. A name used\n// twice would make a report ambiguous, so it is refused up front.\nexport function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[] {\n\tconst seen = new Map<string, string>();\n\tconst out: INamedCheck[] = [];\n\tconst add = (module: string, check: IAdminCheck): void => {\n\t\tconst prior = seen.get(check.name);\n\t\tif (prior)\n\t\t\tthrow new Error(\n\t\t\t\t`[fonderie] ${module} cannot offer check \"${check.name}\": ${prior} already does`,\n\t\t\t);\n\t\tseen.set(check.name, module);\n\t\tout.push({ ...check, module });\n\t};\n\tfor (const { module, description } of app.adminDescriptions()) {\n\t\tfor (const c of description.checks ?? []) add(module, c);\n\t}\n\tfor (const c of own) add('application', c);\n\treturn out;\n}\n\nasync function runOne(check: INamedCheck, timeoutMs: number): Promise<IAdminCheckResult> {\n\tconst started = Date.now();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<IAdminCheckReport>((resolve) => {\n\t\ttimer = setTimeout(\n\t\t\t() => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet report: IAdminCheckReport;\n\ttry {\n\t\treport = await Promise.race([check.run(), timeout]);\n\t} catch (err) {\n\t\treport = {\n\t\t\tok: false,\n\t\t\tfindings: [`check threw: ${err instanceof Error ? err.message : String(err)}`],\n\t\t};\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n\treturn { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };\n}\n\nexport async function runDoctor(\n\tchecks: INamedCheck[],\n\ttimeoutMs: number,\n): Promise<IAdminDoctorReport> {\n\tconst results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));\n\treturn { generatedAt: new Date().toISOString(), ok: results.every((r) => r.ok), checks: results };\n}\n\n// What needs the operator today: readiness problems as reported, failed\n// checks as errors, findings on a passing check as advice. Empty = green.\nexport function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention {\n\tconst items: IAdminAttention['items'] = [];\n\tfor (const p of app.checkProductionReadiness().problems) {\n\t\titems.push({\n\t\t\tsource: p.module,\n\t\t\tseverity: p.severity === 'error' ? 'error' : 'advice',\n\t\t\tmessage: p.message,\n\t\t});\n\t}\n\tfor (const c of doctor.checks) {\n\t\tif (c.skipped) continue;\n\t\tconst severity = c.ok ? 'advice' : 'error';\n\t\tif (!c.ok && c.findings.length === 0)\n\t\t\titems.push({ source: c.name, severity, message: 'failed' });\n\t\tfor (const f of c.findings) items.push({ source: c.name, severity, message: f });\n\t}\n\treturn { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === 'error'), items };\n}\n","import { normalizeRequestPath } from '@fonderie/core';\nimport type { IFonderieContext, Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAdminLogEntry, IAdminLogPage } from './types';\n\nexport const DEFAULT_ACTOR = 'admin-token';\n\n// Sits BEFORE the token guard, so a refused request is a row too. A failed\n// write must not fail the request it describes.\nexport function adminLog(store: IStoreAdapter, route: string, module: string): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst started = Date.now();\n\t\tconst res = await next();\n\t\tconst row = {\n\t\t\t// A scoped token names itself; X-Actor refines it; the root token is 'admin-token'.\n\t\t\tactor:\n\t\t\t\tctx.request.headers.get('x-actor') ||\n\t\t\t\t(typeof ctx.meta['adminTokenName'] === 'string'\n\t\t\t\t\t? `token:${ctx.meta['adminTokenName']}`\n\t\t\t\t\t: DEFAULT_ACTOR),\n\t\t\tmethod: ctx.request.method,\n\t\t\t// Normalized, so '/x' and '/x/' are one row for one route — the same\n\t\t\t// path the router matched and `route` below records the pattern of.\n\t\t\tpath: normalizeRequestPath(new URL(ctx.request.url).pathname),\n\t\t\troute,\n\t\t\tmodule,\n\t\t\tstatus: res.status,\n\t\t\tdurationMs: Date.now() - started,\n\t\t\trequestId: typeof ctx.meta['requestId'] === 'string' ? ctx.meta['requestId'] : null,\n\t\t\tclientIp: ctx.meta.clientIp ?? null,\n\t\t};\n\t\ttry {\n\t\t\tawait store.query(\n\t\t\t\t`INSERT INTO fonderie_admin_log\n\t\t\t\t (actor, method, path, route, module, status, duration_ms, request_id, client_ip)\n\t\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,\n\t\t\t\t[\n\t\t\t\t\trow.actor,\n\t\t\t\t\trow.method,\n\t\t\t\t\trow.path,\n\t\t\t\t\trow.route,\n\t\t\t\t\trow.module,\n\t\t\t\t\trow.status,\n\t\t\t\t\trow.durationMs,\n\t\t\t\t\trow.requestId,\n\t\t\t\t\trow.clientIp,\n\t\t\t\t],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error(\n\t\t\t\t'[admin] could not write the admin log:',\n\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t);\n\t\t}\n\t\treturn res;\n\t};\n}\n\nexport const MAX_PAGE = 200;\n\n// Newest first, keyset on (at, id). `before` is the last entry's cursor.\nexport async function readAdminLog(\n\tstore: IStoreAdapter,\n\topts: { limit?: number; before?: string } = {},\n): Promise<IAdminLogPage> {\n\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);\n\tconst cursor = opts.before ? decodeCursor(opts.before) : null;\n\tconst rows = await store.query<IAdminLogEntry>(\n\t\t`SELECT id, at, actor, method, path, route, module, status,\n\t\t duration_ms AS \"durationMs\", request_id AS \"requestId\", client_ip AS \"clientIp\"\n\t\t FROM fonderie_admin_log\n\t\t WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))\n\t\t ORDER BY at DESC, id DESC\n\t\t LIMIT $3`,\n\t\t[cursor?.at ?? null, cursor?.id ?? null, limit + 1],\n\t);\n\tconst page = rows.slice(0, limit);\n\tconst last = page[page.length - 1];\n\treturn {\n\t\tentries: page,\n\t\tnext: rows.length > limit && last ? encodeCursor(last) : null,\n\t};\n}\n\nfunction encodeCursor(e: IAdminLogEntry): string {\n\treturn Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, 'utf8').toString('base64url');\n}\n\nfunction decodeCursor(c: string): { at: string; id: string } | null {\n\tconst [at, id] = Buffer.from(c, 'base64url').toString('utf8').split('|');\n\treturn at && id ? { at, id } : null;\n}\n","import 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 },\n): IAdminManifest {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems) {\n\t\tconst list = byModule.get(p.module) ?? [];\n\t\tlist.push(p);\n\t\tbyModule.set(p.module, list);\n\t}\n\tconst describing = new Set(app.adminDescriptions().map((d) => d.module));\n\tconst modules: IAdminModuleEntry[] = report.modules.map(({ name, version }) => {\n\t\tconst problems = byModule.get(name) ?? [];\n\t\treturn {\n\t\t\tname,\n\t\t\tversion: version ?? null,\n\t\t\treadiness: { ok: !problems.some((p) => p.severity === 'error'), problems },\n\t\t\tdescribesAdmin: describing.has(name),\n\t\t};\n\t});\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tenv: report.env,\n\t\tadmin,\n\t\tmodules,\n\t\treadiness: report.readiness,\n\t\troutes: app.routes(),\n\t};\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type {\n\tIAdminConfigReport,\n\tIAdminRoutesReport,\n\tIAdminTokenRecord,\n\tIAdminTokensReport,\n} from './types';\n\nexport function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems)\n\t\tbyModule.set(p.module, [...(byModule.get(p.module) ?? []), p]);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\treadiness: report.readiness,\n\t\tmodules: report.modules.map(({ name }) => ({ name, problems: byModule.get(name) ?? [] })),\n\t\tenv: env.map((name) => ({\n\t\t\tname,\n\t\t\tset: process.env[name] !== undefined && process.env[name] !== '',\n\t\t})),\n\t};\n}\n\nconst PROBES = new Set(['/healthz', '/readyz', '/metrics']);\n\nexport function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport {\n\treturn {\n\t\tgeneratedAt: new Date().toISOString(),\n\t\troutes: app.routes().map((r) => ({\n\t\t\t...r,\n\t\t\tguard:\n\t\t\t\tr.module === adminModule\n\t\t\t\t\t? 'admin'\n\t\t\t\t\t: PROBES.has(r.path) && r.module === '@fonderie/core'\n\t\t\t\t\t\t? 'probe'\n\t\t\t\t\t\t: 'app',\n\t\t})),\n\t};\n}\n\n// A brick's own adminToken shows as a readiness problem only when weak, so\n// \"set at all\" has to be asked; a described module that also registers a\n// legacy route under /admin, /plans or /billing/wallet/grant has one.\nexport function tokensReport(\n\tapp: IFonderieApp,\n\tadminModule: string,\n\tissued: IAdminTokenRecord[] | null,\n): IAdminTokensReport {\n\tconst report = app.securityReport();\n\tconst adminProblems = report.readiness.problems.filter((p) => p.module === adminModule);\n\tconst legacyOwners = new Set(\n\t\tapp\n\t\t\t.routes()\n\t\t\t.filter((r) => r.module && r.module !== adminModule && isLegacyAdminPath(r.method, r.path))\n\t\t\t.map((r) => r.module as string),\n\t);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tadmin: { ok: !adminProblems.some((p) => p.severity === 'error'), problems: adminProblems },\n\t\tlegacy: report.modules\n\t\t\t.map(({ name }) => ({ module: name, set: legacyOwners.has(name) }))\n\t\t\t.filter((e) => e.set),\n\t\tissued,\n\t};\n}\n\nfunction isLegacyAdminPath(method: string, path: string): boolean {\n\tif (/\\/admin\\/(config|secrets|templates)(\\/|$)/.test(path)) return true;\n\tif (/\\/plans(\\/|$)/.test(path) && method !== 'GET') return true;\n\treturn /\\/billing\\/wallet\\/grant$/.test(path);\n}\n","import { createHash, randomBytes } from 'node:crypto';\nimport type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { HTTP, constantTimeEqual, setApiResponse } from '@fonderie/core';\nimport type { IRequestSchema } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { AdminScope, IAdminTokenRecord } from './types';\n\nexport const SCOPES: readonly AdminScope[] = ['read', 'write', 'secrets'];\n\n// Derived from the route, never annotated: the plaintext secret read is the\n// one thing that needs `secrets`; any other mutation needs `write`.\nexport function scopeFor(method: string, path: string): AdminScope {\n\tif (/\\/secrets(\\/|$)/.test(path)) return 'secrets';\n\treturn method.toUpperCase() === 'GET' ? 'read' : 'write';\n}\n\n// `write` implies `read`; `secrets` implies both.\nexport function grants(held: readonly AdminScope[], needed: AdminScope): boolean {\n\tif (held.includes('secrets')) return true;\n\tif (needed === 'secrets') return false;\n\tif (held.includes('write')) return true;\n\treturn needed === 'read' && held.includes('read');\n}\n\nexport const hashToken = (token: string): string =>\n\tcreateHash('sha256').update(token).digest('hex');\nconst mintToken = (): string => `fad_${randomBytes(24).toString('base64url')}`;\n\nconst COLUMNS = `id, name, scopes, created_by AS \"createdBy\", created_at AS \"createdAt\",\n expires_at AS \"expiresAt\", revoked_at AS \"revokedAt\", last_used_at AS \"lastUsedAt\"`;\n\nexport async function listTokens(store: IStoreAdapter): Promise<IAdminTokenRecord[]> {\n\treturn store.query<IAdminTokenRecord>(\n\t\t`SELECT ${COLUMNS} FROM fonderie_admin_tokens ORDER BY created_at DESC`,\n\t);\n}\n\nexport async function issueToken(\n\tstore: IStoreAdapter,\n\tinput: { name: string; scopes: AdminScope[]; expiresInDays?: number; createdBy: string },\n): Promise<{ token: string; record: IAdminTokenRecord }> {\n\tconst token = mintToken();\n\tconst expiresAt = input.expiresInDays\n\t\t? new Date(Date.now() + input.expiresInDays * 86_400_000)\n\t\t: null;\n\tconst [record] = await store.query<IAdminTokenRecord>(\n\t\t`INSERT INTO fonderie_admin_tokens (name, token_hash, scopes, created_by, expires_at)\n\t\t VALUES ($1, $2, $3, $4, $5) RETURNING ${COLUMNS}`,\n\t\t[input.name, hashToken(token), input.scopes, input.createdBy, expiresAt],\n\t);\n\tif (!record) throw new Error('[admin] token insert returned no row');\n\treturn { token, record };\n}\n\nexport async function revokeToken(store: IStoreAdapter, id: string): Promise<boolean> {\n\tconst rows = await store.query<{ id: string }>(\n\t\t`UPDATE fonderie_admin_tokens SET revoked_at = now() WHERE id = $1 AND revoked_at IS NULL RETURNING id`,\n\t\t[id],\n\t);\n\treturn rows.length > 0;\n}\n\nasync function findLive(store: IStoreAdapter, token: string): Promise<IAdminTokenRecord | null> {\n\tconst [row] = await store.query<IAdminTokenRecord>(\n\t\t`SELECT ${COLUMNS} FROM fonderie_admin_tokens\n\t\t WHERE token_hash = $1 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())`,\n\t\t[hashToken(token)],\n\t);\n\treturn row ?? null;\n}\n\nconst bearer = (ctx: IFonderieContext): string => {\n\tconst h = ctx.request.headers.get('authorization') ?? '';\n\treturn h.startsWith('Bearer ') ? h.slice(7) : '';\n};\nconst UNAUTHORIZED = () =>\n\tsetApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token');\n\n// The bootstrap token is the root: every scope, and the only credential that\n// may issue or revoke tokens. Issued tokens carry their scopes; a valid token\n// short of the route's scope is 403, an unknown, revoked or expired one is\n// the same 401 as a missing one. The token's name becomes the log's actor.\nexport function requireAdminScope(\n\tbootstrap: string,\n\tstore: IStoreAdapter | undefined,\n\tneeded: AdminScope | 'root',\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst token = bearer(ctx);\n\t\tif (!token) return UNAUTHORIZED();\n\t\tif (constantTimeEqual(token, bootstrap)) return next();\n\t\tif (needed === 'root' || !store) return UNAUTHORIZED();\n\t\tconst record = await findLive(store, token);\n\t\tif (!record) return UNAUTHORIZED();\n\t\tif (!grants(record.scopes, needed)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', `This token lacks the '${needed}' scope`);\n\t\t}\n\t\tctx.meta['adminTokenName'] = record.name;\n\t\tvoid store\n\t\t\t.query(`UPDATE fonderie_admin_tokens SET last_used_at = now() WHERE id = $1`, [record.id])\n\t\t\t.catch(() => undefined);\n\t\treturn next();\n\t};\n}\n\n// { name: 1–100 chars, scopes: non-empty subset of SCOPES, expiresInDays?: 1–3650 }\nexport const issueTokenSchema: IRequestSchema = {\n\tsafeParse(input: unknown) {\n\t\tconst fail = (path: string, message: string) => ({\n\t\t\tsuccess: false as const,\n\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t});\n\t\tconst b = (input ?? {}) as Record<string, unknown>;\n\t\tconst name = typeof b['name'] === 'string' ? b['name'].trim() : '';\n\t\tif (!name || name.length > 100) return fail('name', 'name is required (1–100 chars)');\n\t\tconst scopes = b['scopes'];\n\t\tif (\n\t\t\t!Array.isArray(scopes) ||\n\t\t\tscopes.length === 0 ||\n\t\t\t!scopes.every((s) => SCOPES.includes(s as AdminScope))\n\t\t)\n\t\t\treturn fail('scopes', `scopes must be a non-empty list of ${SCOPES.join(', ')}`);\n\t\tconst days = b['expiresInDays'];\n\t\tif (\n\t\t\tdays !== undefined &&\n\t\t\t(!Number.isInteger(days) || (days as number) < 1 || (days as number) > 3650)\n\t\t)\n\t\t\treturn fail('expiresInDays', 'expiresInDays must be an integer between 1 and 3650');\n\t\treturn {\n\t\t\tsuccess: true as const,\n\t\t\tdata: {\n\t\t\t\tname,\n\t\t\t\tscopes: [...new Set(scopes as AdminScope[])],\n\t\t\t\t...(days !== undefined ? { expiresInDays: days as number } : {}),\n\t\t\t},\n\t\t};\n\t},\n};\n","// The served shell's document. Inlined as a string so it ships in dist with no\n// copy step. The script path is passed in, not relative: the surface can be\n// mounted under any basePath or path, and a trailing slash must not break it.\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<title>Admin</title>\n<style>html,body{margin:0;background:#fff;color:#111}</style>\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_core","module","module","import_core","module"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -6,6 +6,7 @@ interface IAdminOptions {
|
|
|
6
6
|
path?: string;
|
|
7
7
|
checks?: IAdminCheck[];
|
|
8
8
|
checkTimeoutMs?: number;
|
|
9
|
+
ui?: boolean;
|
|
9
10
|
store?: IStoreAdapter;
|
|
10
11
|
env?: string[];
|
|
11
12
|
}
|
|
@@ -88,6 +89,17 @@ interface IAdminTokenEntry {
|
|
|
88
89
|
module: string;
|
|
89
90
|
set: boolean;
|
|
90
91
|
}
|
|
92
|
+
type AdminScope = 'read' | 'write' | 'secrets';
|
|
93
|
+
interface IAdminTokenRecord {
|
|
94
|
+
id: string;
|
|
95
|
+
name: string;
|
|
96
|
+
scopes: AdminScope[];
|
|
97
|
+
createdBy: string;
|
|
98
|
+
createdAt: string;
|
|
99
|
+
expiresAt: string | null;
|
|
100
|
+
revokedAt: string | null;
|
|
101
|
+
lastUsedAt: string | null;
|
|
102
|
+
}
|
|
91
103
|
interface IAdminTokensReport {
|
|
92
104
|
generatedAt: string;
|
|
93
105
|
admin: {
|
|
@@ -95,6 +107,7 @@ interface IAdminTokensReport {
|
|
|
95
107
|
problems: IReadinessReport['problems'];
|
|
96
108
|
};
|
|
97
109
|
legacy: IAdminTokenEntry[];
|
|
110
|
+
issued: IAdminTokenRecord[] | null;
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
declare const DEFAULT_ADMIN_PATH = "/_admin";
|
|
@@ -132,6 +145,23 @@ declare function readAdminLog(store: IStoreAdapter, opts?: {
|
|
|
132
145
|
|
|
133
146
|
declare function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport;
|
|
134
147
|
declare function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport;
|
|
135
|
-
declare function tokensReport(app: IFonderieApp, adminModule: string): IAdminTokensReport;
|
|
148
|
+
declare function tokensReport(app: IFonderieApp, adminModule: string, issued: IAdminTokenRecord[] | null): IAdminTokensReport;
|
|
149
|
+
|
|
150
|
+
declare const SCOPES: readonly AdminScope[];
|
|
151
|
+
declare function scopeFor(method: string, path: string): AdminScope;
|
|
152
|
+
declare function grants(held: readonly AdminScope[], needed: AdminScope): boolean;
|
|
153
|
+
declare const hashToken: (token: string) => string;
|
|
154
|
+
declare function listTokens(store: IStoreAdapter): Promise<IAdminTokenRecord[]>;
|
|
155
|
+
declare function issueToken(store: IStoreAdapter, input: {
|
|
156
|
+
name: string;
|
|
157
|
+
scopes: AdminScope[];
|
|
158
|
+
expiresInDays?: number;
|
|
159
|
+
createdBy: string;
|
|
160
|
+
}): Promise<{
|
|
161
|
+
token: string;
|
|
162
|
+
record: IAdminTokenRecord;
|
|
163
|
+
}>;
|
|
164
|
+
declare function revokeToken(store: IStoreAdapter, id: string): Promise<boolean>;
|
|
165
|
+
declare function requireAdminScope(bootstrap: string, store: IStoreAdapter | undefined, needed: AdminScope | 'root'): Middleware;
|
|
136
166
|
|
|
137
|
-
export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokensReport, MAX_PAGE, adminLog, attention, buildManifest, collectChecks, configReport, readAdminLog, routesReport, runDoctor, tokensReport };
|
|
167
|
+
export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, MAX_PAGE, SCOPES, adminLog, attention, buildManifest, collectChecks, configReport, grants, hashToken, issueToken, listTokens, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ interface IAdminOptions {
|
|
|
6
6
|
path?: string;
|
|
7
7
|
checks?: IAdminCheck[];
|
|
8
8
|
checkTimeoutMs?: number;
|
|
9
|
+
ui?: boolean;
|
|
9
10
|
store?: IStoreAdapter;
|
|
10
11
|
env?: string[];
|
|
11
12
|
}
|
|
@@ -88,6 +89,17 @@ interface IAdminTokenEntry {
|
|
|
88
89
|
module: string;
|
|
89
90
|
set: boolean;
|
|
90
91
|
}
|
|
92
|
+
type AdminScope = 'read' | 'write' | 'secrets';
|
|
93
|
+
interface IAdminTokenRecord {
|
|
94
|
+
id: string;
|
|
95
|
+
name: string;
|
|
96
|
+
scopes: AdminScope[];
|
|
97
|
+
createdBy: string;
|
|
98
|
+
createdAt: string;
|
|
99
|
+
expiresAt: string | null;
|
|
100
|
+
revokedAt: string | null;
|
|
101
|
+
lastUsedAt: string | null;
|
|
102
|
+
}
|
|
91
103
|
interface IAdminTokensReport {
|
|
92
104
|
generatedAt: string;
|
|
93
105
|
admin: {
|
|
@@ -95,6 +107,7 @@ interface IAdminTokensReport {
|
|
|
95
107
|
problems: IReadinessReport['problems'];
|
|
96
108
|
};
|
|
97
109
|
legacy: IAdminTokenEntry[];
|
|
110
|
+
issued: IAdminTokenRecord[] | null;
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
declare const DEFAULT_ADMIN_PATH = "/_admin";
|
|
@@ -132,6 +145,23 @@ declare function readAdminLog(store: IStoreAdapter, opts?: {
|
|
|
132
145
|
|
|
133
146
|
declare function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport;
|
|
134
147
|
declare function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport;
|
|
135
|
-
declare function tokensReport(app: IFonderieApp, adminModule: string): IAdminTokensReport;
|
|
148
|
+
declare function tokensReport(app: IFonderieApp, adminModule: string, issued: IAdminTokenRecord[] | null): IAdminTokensReport;
|
|
149
|
+
|
|
150
|
+
declare const SCOPES: readonly AdminScope[];
|
|
151
|
+
declare function scopeFor(method: string, path: string): AdminScope;
|
|
152
|
+
declare function grants(held: readonly AdminScope[], needed: AdminScope): boolean;
|
|
153
|
+
declare const hashToken: (token: string) => string;
|
|
154
|
+
declare function listTokens(store: IStoreAdapter): Promise<IAdminTokenRecord[]>;
|
|
155
|
+
declare function issueToken(store: IStoreAdapter, input: {
|
|
156
|
+
name: string;
|
|
157
|
+
scopes: AdminScope[];
|
|
158
|
+
expiresInDays?: number;
|
|
159
|
+
createdBy: string;
|
|
160
|
+
}): Promise<{
|
|
161
|
+
token: string;
|
|
162
|
+
record: IAdminTokenRecord;
|
|
163
|
+
}>;
|
|
164
|
+
declare function revokeToken(store: IStoreAdapter, id: string): Promise<boolean>;
|
|
165
|
+
declare function requireAdminScope(bootstrap: string, store: IStoreAdapter | undefined, needed: AdminScope | 'root'): Middleware;
|
|
136
166
|
|
|
137
|
-
export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokensReport, MAX_PAGE, adminLog, attention, buildManifest, collectChecks, configReport, readAdminLog, routesReport, runDoctor, tokensReport };
|
|
167
|
+
export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, type AdminScope, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokenRecord, type IAdminTokensReport, MAX_PAGE, SCOPES, adminLog, attention, buildManifest, collectChecks, configReport, grants, hashToken, issueToken, listTokens, readAdminLog, requireAdminScope, revokeToken, routesReport, runDoctor, scopeFor, tokensReport };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/module.ts
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { HTTP as HTTP2, normalizeMountPath, normalizeRequestPath as normalizeRequestPath2, setApiResponse as setApiResponse2 } from "@fonderie/core";
|
|
4
|
+
import { validate, validateAdminToken } from "@fonderie/core/middlewares";
|
|
4
5
|
|
|
5
6
|
// src/doctor.ts
|
|
6
7
|
function collectChecks(app, own) {
|
|
@@ -67,15 +68,19 @@ function attention(app, doctor) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
// src/log.ts
|
|
71
|
+
import { normalizeRequestPath } from "@fonderie/core";
|
|
70
72
|
var DEFAULT_ACTOR = "admin-token";
|
|
71
73
|
function adminLog(store, route, module) {
|
|
72
74
|
return async (ctx, next) => {
|
|
73
75
|
const started = Date.now();
|
|
74
76
|
const res = await next();
|
|
75
77
|
const row = {
|
|
76
|
-
|
|
78
|
+
// A scoped token names itself; X-Actor refines it; the root token is 'admin-token'.
|
|
79
|
+
actor: ctx.request.headers.get("x-actor") || (typeof ctx.meta["adminTokenName"] === "string" ? `token:${ctx.meta["adminTokenName"]}` : DEFAULT_ACTOR),
|
|
77
80
|
method: ctx.request.method,
|
|
78
|
-
|
|
81
|
+
// Normalized, so '/x' and '/x/' are one row for one route — the same
|
|
82
|
+
// path the router matched and `route` below records the pattern of.
|
|
83
|
+
path: normalizeRequestPath(new URL(ctx.request.url).pathname),
|
|
79
84
|
route,
|
|
80
85
|
module,
|
|
81
86
|
status: res.status,
|
|
@@ -192,7 +197,7 @@ function routesReport(app, adminModule) {
|
|
|
192
197
|
}))
|
|
193
198
|
};
|
|
194
199
|
}
|
|
195
|
-
function tokensReport(app, adminModule) {
|
|
200
|
+
function tokensReport(app, adminModule, issued) {
|
|
196
201
|
const report = app.securityReport();
|
|
197
202
|
const adminProblems = report.readiness.problems.filter((p) => p.module === adminModule);
|
|
198
203
|
const legacyOwners = new Set(
|
|
@@ -201,7 +206,8 @@ function tokensReport(app, adminModule) {
|
|
|
201
206
|
return {
|
|
202
207
|
generatedAt: report.generatedAt,
|
|
203
208
|
admin: { ok: !adminProblems.some((p) => p.severity === "error"), problems: adminProblems },
|
|
204
|
-
legacy: report.modules.map(({ name }) => ({ module: name, set: legacyOwners.has(name) })).filter((e) => e.set)
|
|
209
|
+
legacy: report.modules.map(({ name }) => ({ module: name, set: legacyOwners.has(name) })).filter((e) => e.set),
|
|
210
|
+
issued
|
|
205
211
|
};
|
|
206
212
|
}
|
|
207
213
|
function isLegacyAdminPath(method, path) {
|
|
@@ -210,14 +216,127 @@ function isLegacyAdminPath(method, path) {
|
|
|
210
216
|
return /\/billing\/wallet\/grant$/.test(path);
|
|
211
217
|
}
|
|
212
218
|
|
|
219
|
+
// src/tokens.ts
|
|
220
|
+
import { createHash, randomBytes } from "crypto";
|
|
221
|
+
import { HTTP, constantTimeEqual, setApiResponse } from "@fonderie/core";
|
|
222
|
+
var SCOPES = ["read", "write", "secrets"];
|
|
223
|
+
function scopeFor(method, path) {
|
|
224
|
+
if (/\/secrets(\/|$)/.test(path)) return "secrets";
|
|
225
|
+
return method.toUpperCase() === "GET" ? "read" : "write";
|
|
226
|
+
}
|
|
227
|
+
function grants(held, needed) {
|
|
228
|
+
if (held.includes("secrets")) return true;
|
|
229
|
+
if (needed === "secrets") return false;
|
|
230
|
+
if (held.includes("write")) return true;
|
|
231
|
+
return needed === "read" && held.includes("read");
|
|
232
|
+
}
|
|
233
|
+
var hashToken = (token) => createHash("sha256").update(token).digest("hex");
|
|
234
|
+
var mintToken = () => `fad_${randomBytes(24).toString("base64url")}`;
|
|
235
|
+
var COLUMNS = `id, name, scopes, created_by AS "createdBy", created_at AS "createdAt",
|
|
236
|
+
expires_at AS "expiresAt", revoked_at AS "revokedAt", last_used_at AS "lastUsedAt"`;
|
|
237
|
+
async function listTokens(store) {
|
|
238
|
+
return store.query(
|
|
239
|
+
`SELECT ${COLUMNS} FROM fonderie_admin_tokens ORDER BY created_at DESC`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
async function issueToken(store, input) {
|
|
243
|
+
const token = mintToken();
|
|
244
|
+
const expiresAt = input.expiresInDays ? new Date(Date.now() + input.expiresInDays * 864e5) : null;
|
|
245
|
+
const [record] = await store.query(
|
|
246
|
+
`INSERT INTO fonderie_admin_tokens (name, token_hash, scopes, created_by, expires_at)
|
|
247
|
+
VALUES ($1, $2, $3, $4, $5) RETURNING ${COLUMNS}`,
|
|
248
|
+
[input.name, hashToken(token), input.scopes, input.createdBy, expiresAt]
|
|
249
|
+
);
|
|
250
|
+
if (!record) throw new Error("[admin] token insert returned no row");
|
|
251
|
+
return { token, record };
|
|
252
|
+
}
|
|
253
|
+
async function revokeToken(store, id) {
|
|
254
|
+
const rows = await store.query(
|
|
255
|
+
`UPDATE fonderie_admin_tokens SET revoked_at = now() WHERE id = $1 AND revoked_at IS NULL RETURNING id`,
|
|
256
|
+
[id]
|
|
257
|
+
);
|
|
258
|
+
return rows.length > 0;
|
|
259
|
+
}
|
|
260
|
+
async function findLive(store, token) {
|
|
261
|
+
const [row] = await store.query(
|
|
262
|
+
`SELECT ${COLUMNS} FROM fonderie_admin_tokens
|
|
263
|
+
WHERE token_hash = $1 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())`,
|
|
264
|
+
[hashToken(token)]
|
|
265
|
+
);
|
|
266
|
+
return row ?? null;
|
|
267
|
+
}
|
|
268
|
+
var bearer = (ctx) => {
|
|
269
|
+
const h = ctx.request.headers.get("authorization") ?? "";
|
|
270
|
+
return h.startsWith("Bearer ") ? h.slice(7) : "";
|
|
271
|
+
};
|
|
272
|
+
var UNAUTHORIZED = () => setApiResponse(HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Missing or invalid admin token");
|
|
273
|
+
function requireAdminScope(bootstrap, store, needed) {
|
|
274
|
+
return async (ctx, next) => {
|
|
275
|
+
const token = bearer(ctx);
|
|
276
|
+
if (!token) return UNAUTHORIZED();
|
|
277
|
+
if (constantTimeEqual(token, bootstrap)) return next();
|
|
278
|
+
if (needed === "root" || !store) return UNAUTHORIZED();
|
|
279
|
+
const record = await findLive(store, token);
|
|
280
|
+
if (!record) return UNAUTHORIZED();
|
|
281
|
+
if (!grants(record.scopes, needed)) {
|
|
282
|
+
return setApiResponse(HTTP.FORBIDDEN, "FORBIDDEN", `This token lacks the '${needed}' scope`);
|
|
283
|
+
}
|
|
284
|
+
ctx.meta["adminTokenName"] = record.name;
|
|
285
|
+
void store.query(`UPDATE fonderie_admin_tokens SET last_used_at = now() WHERE id = $1`, [record.id]).catch(() => void 0);
|
|
286
|
+
return next();
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
var issueTokenSchema = {
|
|
290
|
+
safeParse(input) {
|
|
291
|
+
const fail = (path, message) => ({
|
|
292
|
+
success: false,
|
|
293
|
+
error: { issues: [{ path: [path], message }] }
|
|
294
|
+
});
|
|
295
|
+
const b = input ?? {};
|
|
296
|
+
const name = typeof b["name"] === "string" ? b["name"].trim() : "";
|
|
297
|
+
if (!name || name.length > 100) return fail("name", "name is required (1\u2013100 chars)");
|
|
298
|
+
const scopes = b["scopes"];
|
|
299
|
+
if (!Array.isArray(scopes) || scopes.length === 0 || !scopes.every((s) => SCOPES.includes(s)))
|
|
300
|
+
return fail("scopes", `scopes must be a non-empty list of ${SCOPES.join(", ")}`);
|
|
301
|
+
const days = b["expiresInDays"];
|
|
302
|
+
if (days !== void 0 && (!Number.isInteger(days) || days < 1 || days > 3650))
|
|
303
|
+
return fail("expiresInDays", "expiresInDays must be an integer between 1 and 3650");
|
|
304
|
+
return {
|
|
305
|
+
success: true,
|
|
306
|
+
data: {
|
|
307
|
+
name,
|
|
308
|
+
scopes: [...new Set(scopes)],
|
|
309
|
+
...days !== void 0 ? { expiresInDays: days } : {}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/ui/html.ts
|
|
316
|
+
var uiHtml = (scriptPath) => `<!doctype html>
|
|
317
|
+
<html lang="en">
|
|
318
|
+
<head>
|
|
319
|
+
<meta charset="utf-8">
|
|
320
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
321
|
+
<meta name="robots" content="noindex, nofollow">
|
|
322
|
+
<title>Admin</title>
|
|
323
|
+
<style>html,body{margin:0;background:#fff;color:#111}</style>
|
|
324
|
+
</head>
|
|
325
|
+
<body>
|
|
326
|
+
<div id="root"></div>
|
|
327
|
+
<script src="${scriptPath}" defer></script>
|
|
328
|
+
</body>
|
|
329
|
+
</html>
|
|
330
|
+
`;
|
|
331
|
+
|
|
213
332
|
// src/module.ts
|
|
214
333
|
var DEFAULT_ADMIN_PATH = "/_admin";
|
|
215
334
|
var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
216
|
-
var ADMIN_VERSION = "0.
|
|
335
|
+
var ADMIN_VERSION = "0.7.0";
|
|
217
336
|
var AdminModule = class {
|
|
218
337
|
constructor(options = {}) {
|
|
219
338
|
this.options = options;
|
|
220
|
-
this.path = (options.path ?? DEFAULT_ADMIN_PATH)
|
|
339
|
+
this.path = normalizeMountPath(options.path ?? DEFAULT_ADMIN_PATH);
|
|
221
340
|
}
|
|
222
341
|
options;
|
|
223
342
|
name = "@fonderie/admin";
|
|
@@ -227,7 +346,6 @@ var AdminModule = class {
|
|
|
227
346
|
const token = this.options.adminToken;
|
|
228
347
|
if (!token) return;
|
|
229
348
|
app.reserve(this.path);
|
|
230
|
-
const guard = requireAdminToken(token);
|
|
231
349
|
const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
|
|
232
350
|
const store = this.options.store;
|
|
233
351
|
const checks = collectChecks(app, this.options.checks ?? []);
|
|
@@ -237,8 +355,8 @@ var AdminModule = class {
|
|
|
237
355
|
[
|
|
238
356
|
"GET",
|
|
239
357
|
"/_admin",
|
|
240
|
-
async () =>
|
|
241
|
-
|
|
358
|
+
async () => setApiResponse2(
|
|
359
|
+
HTTP2.OK,
|
|
242
360
|
"ADMIN_ATTENTION",
|
|
243
361
|
"What needs attention",
|
|
244
362
|
attention(app, await doctor())
|
|
@@ -247,8 +365,8 @@ var AdminModule = class {
|
|
|
247
365
|
[
|
|
248
366
|
"GET",
|
|
249
367
|
"/_admin/manifest",
|
|
250
|
-
async () =>
|
|
251
|
-
|
|
368
|
+
async () => setApiResponse2(
|
|
369
|
+
HTTP2.OK,
|
|
252
370
|
"ADMIN_MANIFEST",
|
|
253
371
|
"Deployment manifest",
|
|
254
372
|
buildManifest(app, { version: this.version, log: Boolean(store) })
|
|
@@ -257,13 +375,13 @@ var AdminModule = class {
|
|
|
257
375
|
[
|
|
258
376
|
"GET",
|
|
259
377
|
"/_admin/doctor",
|
|
260
|
-
async () =>
|
|
378
|
+
async () => setApiResponse2(HTTP2.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
|
|
261
379
|
],
|
|
262
380
|
[
|
|
263
381
|
"GET",
|
|
264
382
|
"/_admin/config",
|
|
265
|
-
async () =>
|
|
266
|
-
|
|
383
|
+
async () => setApiResponse2(
|
|
384
|
+
HTTP2.OK,
|
|
267
385
|
"ADMIN_CONFIG",
|
|
268
386
|
"Declared vs held",
|
|
269
387
|
configReport(app, this.options.env ?? [])
|
|
@@ -272,12 +390,17 @@ var AdminModule = class {
|
|
|
272
390
|
[
|
|
273
391
|
"GET",
|
|
274
392
|
"/_admin/routes",
|
|
275
|
-
async () =>
|
|
393
|
+
async () => setApiResponse2(HTTP2.OK, "ADMIN_ROUTES", "Exposed routes", routesReport(app, this.name))
|
|
276
394
|
],
|
|
277
395
|
[
|
|
278
396
|
"GET",
|
|
279
397
|
"/_admin/access/tokens",
|
|
280
|
-
async () =>
|
|
398
|
+
async () => setApiResponse2(
|
|
399
|
+
HTTP2.OK,
|
|
400
|
+
"ADMIN_TOKENS",
|
|
401
|
+
"Admin tokens",
|
|
402
|
+
tokensReport(app, this.name, store ? await listTokens(store) : null)
|
|
403
|
+
)
|
|
281
404
|
]
|
|
282
405
|
];
|
|
283
406
|
if (store) {
|
|
@@ -292,12 +415,43 @@ var AdminModule = class {
|
|
|
292
415
|
...limit ? { limit } : {},
|
|
293
416
|
...before ? { before } : {}
|
|
294
417
|
});
|
|
295
|
-
return
|
|
418
|
+
return setApiResponse2(HTTP2.OK, "ADMIN_LOG", "Admin activity", page);
|
|
296
419
|
}
|
|
297
420
|
]);
|
|
298
421
|
}
|
|
422
|
+
const rootOnly = store ? [
|
|
423
|
+
[
|
|
424
|
+
"POST",
|
|
425
|
+
"/_admin/access/tokens",
|
|
426
|
+
[
|
|
427
|
+
validate(issueTokenSchema),
|
|
428
|
+
async (ctx) => {
|
|
429
|
+
const body = ctx.meta["body"];
|
|
430
|
+
const createdBy = ctx.request.headers.get("x-actor") || "admin-token";
|
|
431
|
+
const { token: plaintext, record } = await issueToken(store, {
|
|
432
|
+
...body,
|
|
433
|
+
createdBy
|
|
434
|
+
});
|
|
435
|
+
return setApiResponse2(HTTP2.CREATED, "TOKEN_ISSUED", "Token issued \u2014 shown once", {
|
|
436
|
+
token: plaintext,
|
|
437
|
+
...record
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
]
|
|
441
|
+
],
|
|
442
|
+
[
|
|
443
|
+
"DELETE",
|
|
444
|
+
"/_admin/access/tokens/:id",
|
|
445
|
+
[
|
|
446
|
+
async (ctx) => {
|
|
447
|
+
const ok = await revokeToken(store, ctx.meta.params?.["id"] ?? "");
|
|
448
|
+
return ok ? setApiResponse2(HTTP2.OK, "TOKEN_REVOKED", "Token revoked") : setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "No such live token");
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
]
|
|
452
|
+
] : [];
|
|
299
453
|
const mounted = /* @__PURE__ */ new Map();
|
|
300
|
-
const mount = (module, method, path, handlers) => {
|
|
454
|
+
const mount = (module, method, path, handlers, needed) => {
|
|
301
455
|
const key = `${method.toUpperCase()} ${path}`;
|
|
302
456
|
const prior = mounted.get(key);
|
|
303
457
|
if (prior) {
|
|
@@ -306,10 +460,46 @@ var AdminModule = class {
|
|
|
306
460
|
);
|
|
307
461
|
}
|
|
308
462
|
mounted.set(key, module);
|
|
463
|
+
const guard = requireAdminScope(token, store, needed ?? scopeFor(method, path));
|
|
309
464
|
const chain = store ? [adminLog(store, path, module), guard, ...handlers] : [guard, ...handlers];
|
|
310
465
|
app.addRoute(method, path, ...chain);
|
|
311
466
|
};
|
|
467
|
+
if (this.options.ui) {
|
|
468
|
+
const scriptPath = at("/_admin/ui/app.js");
|
|
469
|
+
let js = null;
|
|
470
|
+
const loadJs = () => {
|
|
471
|
+
if (js === null) {
|
|
472
|
+
try {
|
|
473
|
+
js = readFileSync(new URL("./ui/app.global.js", import.meta.url), "utf8");
|
|
474
|
+
} catch {
|
|
475
|
+
js = "";
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return js || null;
|
|
479
|
+
};
|
|
480
|
+
app.addRoute("GET", at("/_admin/ui"), async (ctx) => {
|
|
481
|
+
const here = normalizeRequestPath2(new URL(ctx.request.url).pathname);
|
|
482
|
+
return new Response(uiHtml(`${here}/app.js`), {
|
|
483
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
app.addRoute("GET", scriptPath, async () => {
|
|
487
|
+
const body = loadJs();
|
|
488
|
+
return body ? new Response(body, {
|
|
489
|
+
headers: {
|
|
490
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
491
|
+
"cache-control": "public, max-age=300"
|
|
492
|
+
}
|
|
493
|
+
}) : setApiResponse2(
|
|
494
|
+
HTTP2.SERVICE_UNAVAILABLE,
|
|
495
|
+
"UI_NOT_BUILT",
|
|
496
|
+
"This copy of @fonderie/admin has no built dashboard (dist/ui). Install the published package, or run its build."
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
312
500
|
for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
|
|
501
|
+
for (const [method, path, handlers] of rootOnly)
|
|
502
|
+
mount(this.name, method, at(path), handlers, "root");
|
|
313
503
|
for (const { module, description } of app.adminDescriptions()) {
|
|
314
504
|
for (const r of description.routes ?? [])
|
|
315
505
|
mount(module, r.method, this.path + r.path, r.handlers);
|
|
@@ -326,14 +516,22 @@ export {
|
|
|
326
516
|
DEFAULT_ADMIN_PATH,
|
|
327
517
|
DEFAULT_CHECK_TIMEOUT_MS,
|
|
328
518
|
MAX_PAGE,
|
|
519
|
+
SCOPES,
|
|
329
520
|
adminLog,
|
|
330
521
|
attention,
|
|
331
522
|
buildManifest,
|
|
332
523
|
collectChecks,
|
|
333
524
|
configReport,
|
|
525
|
+
grants,
|
|
526
|
+
hashToken,
|
|
527
|
+
issueToken,
|
|
528
|
+
listTokens,
|
|
334
529
|
readAdminLog,
|
|
530
|
+
requireAdminScope,
|
|
531
|
+
revokeToken,
|
|
335
532
|
routesReport,
|
|
336
533
|
runDoctor,
|
|
534
|
+
scopeFor,
|
|
337
535
|
tokensReport
|
|
338
536
|
};
|
|
339
537
|
//# sourceMappingURL=index.js.map
|