@fonderie/admin 0.6.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 CHANGED
@@ -111,6 +111,36 @@ fonderie admin log --limit 20 # who did what
111
111
 
112
112
  `FONDERIE_ADMIN_PREFIX` when the surface was moved off `/_admin`.
113
113
 
114
+ ## The served dashboard
115
+
116
+ ```ts
117
+ app.register(new AdminModule({ adminToken, store, ui: true }));
118
+ ```
119
+
120
+ `GET /_admin/ui` then serves the React shell — the same
121
+ `@fonderie/react-admin-screens` you would otherwise mount yourself, compiled
122
+ into one file (82 KB gzipped) and shipped in this package. No frontend build,
123
+ no extra dependency: React and the screens are devDependencies here, bundled
124
+ at publish time.
125
+
126
+ Off by default, because most deployments reach the surface through their own
127
+ frontend and would rather not serve the bundle.
128
+
129
+ - **The two static routes are unguarded, deliberately.** A browser navigating
130
+ to a page cannot send an `Authorization` header, and neither file carries
131
+ data — the HTML is an empty shell and the script is the same code npm
132
+ serves. The page asks for a token and sends it itself, so every request that
133
+ *reads* anything is guarded and logged as usual. The two asset fetches are
134
+ not logged: they carry no token to attribute.
135
+ - **The token lives in `sessionStorage`** — this tab only, never on the
136
+ server. Give it a `read` token (below) and the dashboard cannot reveal a
137
+ secret or change anything.
138
+ - **The page shows only what is mounted.** It reads the manifest first and
139
+ builds a client per brick that actually described routes, so a deployment
140
+ without billing has no Money pages rather than pages that 404.
141
+ - The script path is derived from the request, so the page works under a
142
+ `basePath`, under a moved `path`, and with or without a trailing slash.
143
+
114
144
  ## Scoped tokens
115
145
 
116
146
  The `adminToken` you configure is the **root**: every scope, and the only
@@ -63,6 +63,7 @@ interface IAdminOptions {
63
63
  path?: string;
64
64
  checks?: IAdminCheck[];
65
65
  checkTimeoutMs?: number;
66
+ ui?: boolean;
66
67
  store?: IStoreAdapter;
67
68
  env?: string[];
68
69
  }
package/dist/index.cjs CHANGED
@@ -46,8 +46,13 @@ __export(index_exports, {
46
46
  });
47
47
  module.exports = __toCommonJS(index_exports);
48
48
 
49
+ // ../../node_modules/tsup/assets/cjs_shims.js
50
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
51
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
52
+
49
53
  // src/module.ts
50
- var import_core2 = require("@fonderie/core");
54
+ var import_node_fs = require("fs");
55
+ var import_core3 = require("@fonderie/core");
51
56
  var import_middlewares = require("@fonderie/core/middlewares");
52
57
 
53
58
  // src/doctor.ts
@@ -115,6 +120,7 @@ function attention(app, doctor) {
115
120
  }
116
121
 
117
122
  // src/log.ts
123
+ var import_core = require("@fonderie/core");
118
124
  var DEFAULT_ACTOR = "admin-token";
119
125
  function adminLog(store, route, module2) {
120
126
  return async (ctx, next) => {
@@ -124,7 +130,9 @@ function adminLog(store, route, module2) {
124
130
  // A scoped token names itself; X-Actor refines it; the root token is 'admin-token'.
125
131
  actor: ctx.request.headers.get("x-actor") || (typeof ctx.meta["adminTokenName"] === "string" ? `token:${ctx.meta["adminTokenName"]}` : DEFAULT_ACTOR),
126
132
  method: ctx.request.method,
127
- path: new URL(ctx.request.url).pathname,
133
+ // Normalized, so '/x' and '/x/' are one row for one route — the same
134
+ // path the router matched and `route` below records the pattern of.
135
+ path: (0, import_core.normalizeRequestPath)(new URL(ctx.request.url).pathname),
128
136
  route,
129
137
  module: module2,
130
138
  status: res.status,
@@ -262,7 +270,7 @@ function isLegacyAdminPath(method, path) {
262
270
 
263
271
  // src/tokens.ts
264
272
  var import_node_crypto = require("crypto");
265
- var import_core = require("@fonderie/core");
273
+ var import_core2 = require("@fonderie/core");
266
274
  var SCOPES = ["read", "write", "secrets"];
267
275
  function scopeFor(method, path) {
268
276
  if (/\/secrets(\/|$)/.test(path)) return "secrets";
@@ -313,17 +321,17 @@ var bearer = (ctx) => {
313
321
  const h = ctx.request.headers.get("authorization") ?? "";
314
322
  return h.startsWith("Bearer ") ? h.slice(7) : "";
315
323
  };
316
- var UNAUTHORIZED = () => (0, import_core.setApiResponse)(import_core.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Missing or invalid admin token");
324
+ var UNAUTHORIZED = () => (0, import_core2.setApiResponse)(import_core2.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Missing or invalid admin token");
317
325
  function requireAdminScope(bootstrap, store, needed) {
318
326
  return async (ctx, next) => {
319
327
  const token = bearer(ctx);
320
328
  if (!token) return UNAUTHORIZED();
321
- if ((0, import_core.constantTimeEqual)(token, bootstrap)) return next();
329
+ if ((0, import_core2.constantTimeEqual)(token, bootstrap)) return next();
322
330
  if (needed === "root" || !store) return UNAUTHORIZED();
323
331
  const record = await findLive(store, token);
324
332
  if (!record) return UNAUTHORIZED();
325
333
  if (!grants(record.scopes, needed)) {
326
- return (0, import_core.setApiResponse)(import_core.HTTP.FORBIDDEN, "FORBIDDEN", `This token lacks the '${needed}' scope`);
334
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.FORBIDDEN, "FORBIDDEN", `This token lacks the '${needed}' scope`);
327
335
  }
328
336
  ctx.meta["adminTokenName"] = record.name;
329
337
  void store.query(`UPDATE fonderie_admin_tokens SET last_used_at = now() WHERE id = $1`, [record.id]).catch(() => void 0);
@@ -356,14 +364,31 @@ var issueTokenSchema = {
356
364
  }
357
365
  };
358
366
 
367
+ // src/ui/html.ts
368
+ var uiHtml = (scriptPath) => `<!doctype html>
369
+ <html lang="en">
370
+ <head>
371
+ <meta charset="utf-8">
372
+ <meta name="viewport" content="width=device-width, initial-scale=1">
373
+ <meta name="robots" content="noindex, nofollow">
374
+ <title>Admin</title>
375
+ <style>html,body{margin:0;background:#fff;color:#111}</style>
376
+ </head>
377
+ <body>
378
+ <div id="root"></div>
379
+ <script src="${scriptPath}" defer></script>
380
+ </body>
381
+ </html>
382
+ `;
383
+
359
384
  // src/module.ts
360
385
  var DEFAULT_ADMIN_PATH = "/_admin";
361
386
  var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
362
- var ADMIN_VERSION = "0.6.0";
387
+ var ADMIN_VERSION = "0.7.0";
363
388
  var AdminModule = class {
364
389
  constructor(options = {}) {
365
390
  this.options = options;
366
- this.path = (options.path ?? DEFAULT_ADMIN_PATH).replace(/\/+$/, "");
391
+ this.path = (0, import_core3.normalizeMountPath)(options.path ?? DEFAULT_ADMIN_PATH);
367
392
  }
368
393
  options;
369
394
  name = "@fonderie/admin";
@@ -382,8 +407,8 @@ var AdminModule = class {
382
407
  [
383
408
  "GET",
384
409
  "/_admin",
385
- async () => (0, import_core2.setApiResponse)(
386
- import_core2.HTTP.OK,
410
+ async () => (0, import_core3.setApiResponse)(
411
+ import_core3.HTTP.OK,
387
412
  "ADMIN_ATTENTION",
388
413
  "What needs attention",
389
414
  attention(app, await doctor())
@@ -392,8 +417,8 @@ var AdminModule = class {
392
417
  [
393
418
  "GET",
394
419
  "/_admin/manifest",
395
- async () => (0, import_core2.setApiResponse)(
396
- import_core2.HTTP.OK,
420
+ async () => (0, import_core3.setApiResponse)(
421
+ import_core3.HTTP.OK,
397
422
  "ADMIN_MANIFEST",
398
423
  "Deployment manifest",
399
424
  buildManifest(app, { version: this.version, log: Boolean(store) })
@@ -402,13 +427,13 @@ var AdminModule = class {
402
427
  [
403
428
  "GET",
404
429
  "/_admin/doctor",
405
- async () => (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
430
+ async () => (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
406
431
  ],
407
432
  [
408
433
  "GET",
409
434
  "/_admin/config",
410
- async () => (0, import_core2.setApiResponse)(
411
- import_core2.HTTP.OK,
435
+ async () => (0, import_core3.setApiResponse)(
436
+ import_core3.HTTP.OK,
412
437
  "ADMIN_CONFIG",
413
438
  "Declared vs held",
414
439
  configReport(app, this.options.env ?? [])
@@ -417,13 +442,13 @@ var AdminModule = class {
417
442
  [
418
443
  "GET",
419
444
  "/_admin/routes",
420
- async () => (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "ADMIN_ROUTES", "Exposed routes", routesReport(app, this.name))
445
+ async () => (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ADMIN_ROUTES", "Exposed routes", routesReport(app, this.name))
421
446
  ],
422
447
  [
423
448
  "GET",
424
449
  "/_admin/access/tokens",
425
- async () => (0, import_core2.setApiResponse)(
426
- import_core2.HTTP.OK,
450
+ async () => (0, import_core3.setApiResponse)(
451
+ import_core3.HTTP.OK,
427
452
  "ADMIN_TOKENS",
428
453
  "Admin tokens",
429
454
  tokensReport(app, this.name, store ? await listTokens(store) : null)
@@ -442,7 +467,7 @@ var AdminModule = class {
442
467
  ...limit ? { limit } : {},
443
468
  ...before ? { before } : {}
444
469
  });
445
- return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "ADMIN_LOG", "Admin activity", page);
470
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ADMIN_LOG", "Admin activity", page);
446
471
  }
447
472
  ]);
448
473
  }
@@ -459,7 +484,7 @@ var AdminModule = class {
459
484
  ...body,
460
485
  createdBy
461
486
  });
462
- return (0, import_core2.setApiResponse)(import_core2.HTTP.CREATED, "TOKEN_ISSUED", "Token issued \u2014 shown once", {
487
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.CREATED, "TOKEN_ISSUED", "Token issued \u2014 shown once", {
463
488
  token: plaintext,
464
489
  ...record
465
490
  });
@@ -472,7 +497,7 @@ var AdminModule = class {
472
497
  [
473
498
  async (ctx) => {
474
499
  const ok = await revokeToken(store, ctx.meta.params?.["id"] ?? "");
475
- return ok ? (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "TOKEN_REVOKED", "Token revoked") : (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "No such live token");
500
+ return ok ? (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "TOKEN_REVOKED", "Token revoked") : (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "No such live token");
476
501
  }
477
502
  ]
478
503
  ]
@@ -491,6 +516,39 @@ var AdminModule = class {
491
516
  const chain = store ? [adminLog(store, path, module2), guard, ...handlers] : [guard, ...handlers];
492
517
  app.addRoute(method, path, ...chain);
493
518
  };
519
+ if (this.options.ui) {
520
+ const scriptPath = at("/_admin/ui/app.js");
521
+ let js = null;
522
+ const loadJs = () => {
523
+ if (js === null) {
524
+ try {
525
+ js = (0, import_node_fs.readFileSync)(new URL("./ui/app.global.js", importMetaUrl), "utf8");
526
+ } catch {
527
+ js = "";
528
+ }
529
+ }
530
+ return js || null;
531
+ };
532
+ app.addRoute("GET", at("/_admin/ui"), async (ctx) => {
533
+ const here = (0, import_core3.normalizeRequestPath)(new URL(ctx.request.url).pathname);
534
+ return new Response(uiHtml(`${here}/app.js`), {
535
+ headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }
536
+ });
537
+ });
538
+ app.addRoute("GET", scriptPath, async () => {
539
+ const body = loadJs();
540
+ return body ? new Response(body, {
541
+ headers: {
542
+ "content-type": "text/javascript; charset=utf-8",
543
+ "cache-control": "public, max-age=300"
544
+ }
545
+ }) : (0, import_core3.setApiResponse)(
546
+ import_core3.HTTP.SERVICE_UNAVAILABLE,
547
+ "UI_NOT_BUILT",
548
+ "This copy of @fonderie/admin has no built dashboard (dist/ui). Install the published package, or run its build."
549
+ );
550
+ });
551
+ }
494
552
  for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
495
553
  for (const [method, path, handlers] of rootOnly)
496
554
  mount(this.name, method, at(path), handlers, "root");
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/module.ts","../src/doctor.ts","../src/log.ts","../src/manifest.ts","../src/pages.ts","../src/tokens.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","import type { IFonderieApp, IFonderieModule, IReadinessProblem, Middleware } from '@fonderie/core';\nimport { HTTP, 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 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 = (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\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\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 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\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 {\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"],"mappings":"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","names":["import_core","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
  }
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
  }
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/module.ts
2
- import { HTTP as HTTP2, setApiResponse as setApiResponse2 } from "@fonderie/core";
2
+ import { readFileSync } from "fs";
3
+ import { HTTP as HTTP2, normalizeMountPath, normalizeRequestPath as normalizeRequestPath2, setApiResponse as setApiResponse2 } from "@fonderie/core";
3
4
  import { validate, validateAdminToken } from "@fonderie/core/middlewares";
4
5
 
5
6
  // src/doctor.ts
@@ -67,6 +68,7 @@ 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) => {
@@ -76,7 +78,9 @@ function adminLog(store, route, module) {
76
78
  // A scoped token names itself; X-Actor refines it; the root token is 'admin-token'.
77
79
  actor: ctx.request.headers.get("x-actor") || (typeof ctx.meta["adminTokenName"] === "string" ? `token:${ctx.meta["adminTokenName"]}` : DEFAULT_ACTOR),
78
80
  method: ctx.request.method,
79
- path: new URL(ctx.request.url).pathname,
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),
80
84
  route,
81
85
  module,
82
86
  status: res.status,
@@ -308,14 +312,31 @@ var issueTokenSchema = {
308
312
  }
309
313
  };
310
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
+
311
332
  // src/module.ts
312
333
  var DEFAULT_ADMIN_PATH = "/_admin";
313
334
  var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
314
- var ADMIN_VERSION = "0.6.0";
335
+ var ADMIN_VERSION = "0.7.0";
315
336
  var AdminModule = class {
316
337
  constructor(options = {}) {
317
338
  this.options = options;
318
- this.path = (options.path ?? DEFAULT_ADMIN_PATH).replace(/\/+$/, "");
339
+ this.path = normalizeMountPath(options.path ?? DEFAULT_ADMIN_PATH);
319
340
  }
320
341
  options;
321
342
  name = "@fonderie/admin";
@@ -443,6 +464,39 @@ var AdminModule = class {
443
464
  const chain = store ? [adminLog(store, path, module), guard, ...handlers] : [guard, ...handlers];
444
465
  app.addRoute(method, path, ...chain);
445
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
+ }
446
500
  for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
447
501
  for (const [method, path, handlers] of rootOnly)
448
502
  mount(this.name, method, at(path), handlers, "root");