@fonderie/admin 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/brain/outcomes.md +1 -1
- package/brain/signatures.md +4 -1
- package/dist/index.cjs +53 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +53 -22
- package/dist/index.js.map +1 -1
- package/dist/ui/app.global.js +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,6 +141,38 @@ frontend and would rather not serve the bundle.
|
|
|
141
141
|
- The script path is derived from the request, so the page works under a
|
|
142
142
|
`basePath`, under a moved `path`, and with or without a trailing slash.
|
|
143
143
|
|
|
144
|
+
## Binding the surface to a hostname
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
new AdminModule({ adminToken, ui: true, host: 'admin.example.com' })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The surface then answers only for requests addressed to that hostname; every
|
|
151
|
+
other host gets the same `404` an unmounted surface gives — not a `403`,
|
|
152
|
+
which would confirm both that it exists and that you found the wrong door.
|
|
153
|
+
Takes a list (`['admin.example.com', 'localhost:3000']`), matches
|
|
154
|
+
case-insensitively, and ignores the port unless you configured one.
|
|
155
|
+
|
|
156
|
+
**What this is for.** A platform that gives every deployment a permanent URL
|
|
157
|
+
of its own — `project-abc123.vercel.app` — serves your admin surface there
|
|
158
|
+
too, walking straight around anything you put in front of your custom
|
|
159
|
+
domain. Binding closes that, while the rest of the app keeps answering on
|
|
160
|
+
both. Point a second domain at the same deployment and you can put SSO, a
|
|
161
|
+
VPN or an IP allowlist in front of *that hostname only*, without running a
|
|
162
|
+
second service.
|
|
163
|
+
|
|
164
|
+
**⚠️ What this is not.** `Host` is set by the client, so binding is a hook
|
|
165
|
+
for a boundary, not a boundary. Someone who can reach your origin directly
|
|
166
|
+
can send the right `Host` and pass the check — only the edge can stop that
|
|
167
|
+
(a WAF rule on the admin path, or an origin locked to the proxy). Treat it
|
|
168
|
+
the way `@fonderie/core` treats `trustProxy`: useful, and dangerous to
|
|
169
|
+
mistake for authentication. The controls that actually hold are the token,
|
|
170
|
+
its scopes, and the log.
|
|
171
|
+
|
|
172
|
+
A refused request is still logged — the caller learns nothing from a 404,
|
|
173
|
+
and you learn that someone went looking. `GET /_admin/manifest` reports the
|
|
174
|
+
binding as `admin.host`, so the Configuration page can show it.
|
|
175
|
+
|
|
144
176
|
## Scoped tokens
|
|
145
177
|
|
|
146
178
|
The `adminToken` you configure is the **root**: every scope, and the only
|
package/brain/outcomes.md
CHANGED
|
@@ -52,5 +52,5 @@ Raw SQL ships in `node_modules/@fonderie/admin/dist/migrations/sql/` — read it
|
|
|
52
52
|
| GET | `/_admin/activity/admin-log` | `async (ctx) => { const q = new URL(ctx.request.url).searchParams; const limit = Number(q.get('limit')) || undefined; const before = q.get('before') ?? undefined; const page = await readAdminLog(store, { ...(limit ? { limit } : {}), ...(before ? { before } : {}), }); return setApiResponse(HTTP.OK, 'ADMIN_LOG', 'Admin activity', page); }` |
|
|
53
53
|
| GET | `/_admin/config` | `async () => setApiResponse( HTTP.OK, 'ADMIN_CONFIG', 'Declared vs held', configReport(app, this.options.env ?? []), )` |
|
|
54
54
|
| GET | `/_admin/doctor` | `async () => setApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor())` |
|
|
55
|
-
| GET | `/_admin/manifest` | `async () => setApiResponse( HTTP.OK, 'ADMIN_MANIFEST', 'Deployment manifest', buildManifest(app, { version: this.version, log: Boolean(store) }), )` |
|
|
55
|
+
| GET | `/_admin/manifest` | `async () => setApiResponse( HTTP.OK, 'ADMIN_MANIFEST', 'Deployment manifest', buildManifest(app, { version: this.version, log: Boolean(store), host: this.hosts }), )` |
|
|
56
56
|
| GET | `/_admin/routes` | `async () => setApiResponse(HTTP.OK, 'ADMIN_ROUTES', 'Exposed routes', routesReport(app, this.name))` |
|
package/brain/signatures.md
CHANGED
|
@@ -11,6 +11,7 @@ new AdminModule(options?: IAdminOptions): AdminModule
|
|
|
11
11
|
.name: "@fonderie/admin"
|
|
12
12
|
.version: string
|
|
13
13
|
.path: string
|
|
14
|
+
.hosts: string[] | null
|
|
14
15
|
.install(app: IFonderieApp): void
|
|
15
16
|
.checkReadiness(): IReadinessProblem[]
|
|
16
17
|
|
|
@@ -20,7 +21,7 @@ const DEFAULT_ADMIN_PATH: "/_admin"
|
|
|
20
21
|
|
|
21
22
|
const DEFAULT_CHECK_TIMEOUT_MS: 10000
|
|
22
23
|
|
|
23
|
-
function buildManifest(app: IFonderieApp, admin: { version: string; log: boolean; }): IAdminManifest
|
|
24
|
+
function buildManifest(app: IFonderieApp, admin: { version: string; log: boolean; host: string[] | null; }): IAdminManifest
|
|
24
25
|
|
|
25
26
|
function runDoctor(checks: INamedCheck[], timeoutMs: number): Promise<IAdminDoctorReport>
|
|
26
27
|
|
|
@@ -64,6 +65,7 @@ interface IAdminOptions {
|
|
|
64
65
|
checks?: IAdminCheck[];
|
|
65
66
|
checkTimeoutMs?: number;
|
|
66
67
|
ui?: boolean;
|
|
68
|
+
host?: string | string[];
|
|
67
69
|
store?: IStoreAdapter;
|
|
68
70
|
env?: string[];
|
|
69
71
|
}
|
|
@@ -74,6 +76,7 @@ interface IAdminManifest {
|
|
|
74
76
|
admin: {
|
|
75
77
|
version: string;
|
|
76
78
|
log: boolean;
|
|
79
|
+
host: string[] | null;
|
|
77
80
|
};
|
|
78
81
|
modules: IAdminModuleEntry[];
|
|
79
82
|
readiness: IReadinessReport;
|
package/dist/index.cjs
CHANGED
|
@@ -384,22 +384,37 @@ var uiHtml = (scriptPath) => `<!doctype html>
|
|
|
384
384
|
// src/module.ts
|
|
385
385
|
var DEFAULT_ADMIN_PATH = "/_admin";
|
|
386
386
|
var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
387
|
-
var ADMIN_VERSION = "0.
|
|
387
|
+
var ADMIN_VERSION = "0.8.1";
|
|
388
|
+
function normalizeHostPattern(host) {
|
|
389
|
+
return host.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/.*$/, "").toLowerCase();
|
|
390
|
+
}
|
|
391
|
+
function requireAdminHost(hosts) {
|
|
392
|
+
const allowed = new Set(hosts.map(normalizeHostPattern).filter(Boolean));
|
|
393
|
+
return (ctx, next) => {
|
|
394
|
+
const url = new URL(ctx.request.url);
|
|
395
|
+
const ok = allowed.has(url.host.toLowerCase()) || allowed.has(url.hostname.toLowerCase());
|
|
396
|
+
return ok ? next() : Promise.resolve((0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Not found"));
|
|
397
|
+
};
|
|
398
|
+
}
|
|
388
399
|
var AdminModule = class {
|
|
389
400
|
constructor(options = {}) {
|
|
390
401
|
this.options = options;
|
|
391
402
|
this.path = (0, import_core3.normalizeMountPath)(options.path ?? DEFAULT_ADMIN_PATH);
|
|
403
|
+
this.hosts = options.host === void 0 ? null : [options.host].flat();
|
|
392
404
|
}
|
|
393
405
|
options;
|
|
394
406
|
name = "@fonderie/admin";
|
|
395
407
|
version = ADMIN_VERSION;
|
|
396
408
|
path;
|
|
409
|
+
// null = answer on any hostname.
|
|
410
|
+
hosts;
|
|
397
411
|
install(app) {
|
|
398
412
|
const token = this.options.adminToken;
|
|
399
413
|
if (!token) return;
|
|
400
414
|
app.reserve(this.path);
|
|
401
415
|
const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
|
|
402
416
|
const store = this.options.store;
|
|
417
|
+
const hostGuard = this.hosts ? requireAdminHost(this.hosts) : null;
|
|
403
418
|
const checks = collectChecks(app, this.options.checks ?? []);
|
|
404
419
|
const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
405
420
|
const doctor = () => runDoctor(checks, timeoutMs);
|
|
@@ -421,7 +436,7 @@ var AdminModule = class {
|
|
|
421
436
|
import_core3.HTTP.OK,
|
|
422
437
|
"ADMIN_MANIFEST",
|
|
423
438
|
"Deployment manifest",
|
|
424
|
-
buildManifest(app, { version: this.version, log: Boolean(store) })
|
|
439
|
+
buildManifest(app, { version: this.version, log: Boolean(store), host: this.hosts })
|
|
425
440
|
)
|
|
426
441
|
],
|
|
427
442
|
[
|
|
@@ -513,7 +528,14 @@ var AdminModule = class {
|
|
|
513
528
|
}
|
|
514
529
|
mounted.set(key, module2);
|
|
515
530
|
const guard = requireAdminScope(token, store, needed ?? scopeFor(method, path));
|
|
516
|
-
const chain =
|
|
531
|
+
const chain = [
|
|
532
|
+
// Logged first so a wrong-host attempt still leaves a row: the
|
|
533
|
+
// caller learns nothing from a 404, the operator learns something.
|
|
534
|
+
...store ? [adminLog(store, path, module2)] : [],
|
|
535
|
+
...hostGuard ? [hostGuard] : [],
|
|
536
|
+
guard,
|
|
537
|
+
...handlers
|
|
538
|
+
];
|
|
517
539
|
app.addRoute(method, path, ...chain);
|
|
518
540
|
};
|
|
519
541
|
if (this.options.ui) {
|
|
@@ -529,25 +551,34 @@ var AdminModule = class {
|
|
|
529
551
|
}
|
|
530
552
|
return js || null;
|
|
531
553
|
};
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
554
|
+
const ui = (h) => hostGuard ? [hostGuard, h] : [h];
|
|
555
|
+
app.addRoute(
|
|
556
|
+
"GET",
|
|
557
|
+
at("/_admin/ui"),
|
|
558
|
+
...ui(async (ctx) => {
|
|
559
|
+
const here = (0, import_core3.normalizeRequestPath)(new URL(ctx.request.url).pathname);
|
|
560
|
+
return new Response(uiHtml(`${here}/app.js`), {
|
|
561
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }
|
|
562
|
+
});
|
|
563
|
+
})
|
|
564
|
+
);
|
|
565
|
+
app.addRoute(
|
|
566
|
+
"GET",
|
|
567
|
+
scriptPath,
|
|
568
|
+
...ui(async () => {
|
|
569
|
+
const body = loadJs();
|
|
570
|
+
return body ? new Response(body, {
|
|
571
|
+
headers: {
|
|
572
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
573
|
+
"cache-control": "public, max-age=300"
|
|
574
|
+
}
|
|
575
|
+
}) : (0, import_core3.setApiResponse)(
|
|
576
|
+
import_core3.HTTP.SERVICE_UNAVAILABLE,
|
|
577
|
+
"UI_NOT_BUILT",
|
|
578
|
+
"This copy of @fonderie/admin has no built dashboard (dist/ui). Install the published package, or run its build."
|
|
579
|
+
);
|
|
580
|
+
})
|
|
581
|
+
);
|
|
551
582
|
}
|
|
552
583
|
for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
|
|
553
584
|
for (const [method, path, handlers] of rootOnly)
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../../node_modules/tsup/assets/cjs_shims.js","../src/module.ts","../src/doctor.ts","../src/log.ts","../src/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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,IAAM,mBAAmB,MACvB,OAAO,aAAa,cAChB,IAAI,IAAI,QAAQ,UAAU,EAAE,EAAE,OAC7B,SAAS,iBAAiB,SAAS,cAAc,QAAQ,YAAY,MAAM,WAC1E,SAAS,cAAc,MACvB,IAAI,IAAI,WAAW,SAAS,OAAO,EAAE;AAEtC,IAAM,gBAAgC,iCAAiB;;;ACZ9D,qBAA6B;AAE7B,IAAAA,eAA+E;AAC/E,yBAA6C;;;ACOtC,SAAS,cAAc,KAAmB,KAAmC;AACnF,QAAM,OAAO,oBAAI,IAAoB;AACrC,QAAM,MAAqB,CAAC;AAC5B,QAAM,MAAM,CAACC,SAAgB,UAA6B;AACzD,UAAM,QAAQ,KAAK,IAAI,MAAM,IAAI;AACjC,QAAI;AACH,YAAM,IAAI;AAAA,QACT,cAAcA,OAAM,wBAAwB,MAAM,IAAI,MAAM,KAAK;AAAA,MAClE;AACD,SAAK,IAAI,MAAM,MAAMA,OAAM;AAC3B,QAAI,KAAK,EAAE,GAAG,OAAO,QAAAA,QAAO,CAAC;AAAA,EAC9B;AACA,aAAW,EAAE,QAAAA,SAAQ,YAAY,KAAK,IAAI,kBAAkB,GAAG;AAC9D,eAAW,KAAK,YAAY,UAAU,CAAC,EAAG,KAAIA,SAAQ,CAAC;AAAA,EACxD;AACA,aAAW,KAAK,IAAK,KAAI,eAAe,CAAC;AACzC,SAAO;AACR;AAEA,eAAe,OAAO,OAAoB,WAA+C;AACxF,QAAM,UAAU,KAAK,IAAI;AACzB,MAAI;AACJ,QAAM,UAAU,IAAI,QAA2B,CAAC,YAAY;AAC3D,YAAQ;AAAA,MACP,MAAM,QAAQ,EAAE,IAAI,OAAO,UAAU,CAAC,mBAAmB,SAAS,KAAK,EAAE,CAAC;AAAA,MAC1E;AAAA,IACD;AAAA,EACD,CAAC;AACD,MAAI;AACJ,MAAI;AACH,aAAS,MAAM,QAAQ,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC;AAAA,EACnD,SAAS,KAAK;AACb,aAAS;AAAA,MACR,IAAI;AAAA,MACJ,UAAU,CAAC,gBAAgB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,IAC9E;AAAA,EACD,UAAE;AACD,QAAI,MAAO,cAAa,KAAK;AAAA,EAC9B;AACA,SAAO,EAAE,MAAM,MAAM,MAAM,QAAQ,MAAM,QAAQ,GAAG,QAAQ,YAAY,KAAK,IAAI,IAAI,QAAQ;AAC9F;AAEA,eAAsB,UACrB,QACA,WAC8B;AAC9B,QAAM,UAAU,MAAM,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC;AACzE,SAAO,EAAE,cAAa,oBAAI,KAAK,GAAE,YAAY,GAAG,IAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,EAAE,GAAG,QAAQ,QAAQ;AACjG;AAIO,SAAS,UAAU,KAAmB,QAA6C;AACzF,QAAM,QAAkC,CAAC;AACzC,aAAW,KAAK,IAAI,yBAAyB,EAAE,UAAU;AACxD,UAAM,KAAK;AAAA,MACV,QAAQ,EAAE;AAAA,MACV,UAAU,EAAE,aAAa,UAAU,UAAU;AAAA,MAC7C,SAAS,EAAE;AAAA,IACZ,CAAC;AAAA,EACF;AACA,aAAW,KAAK,OAAO,QAAQ;AAC9B,QAAI,EAAE,QAAS;AACf,UAAM,WAAW,EAAE,KAAK,WAAW;AACnC,QAAI,CAAC,EAAE,MAAM,EAAE,SAAS,WAAW;AAClC,YAAM,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,SAAS,SAAS,CAAC;AAC3D,eAAW,KAAK,EAAE,SAAU,OAAM,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,SAAS,EAAE,CAAC;AAAA,EAChF;AACA,SAAO,EAAE,aAAa,OAAO,aAAa,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,MAAM;AACjG;;;AC/EA,kBAAqC;AAM9B,IAAM,gBAAgB;AAItB,SAAS,SAAS,OAAsB,OAAeC,SAA4B;AACzF,SAAO,OAAO,KAAuB,SAAS;AAC7C,UAAM,UAAU,KAAK,IAAI;AACzB,UAAM,MAAM,MAAM,KAAK;AACvB,UAAM,MAAM;AAAA;AAAA,MAEX,OACC,IAAI,QAAQ,QAAQ,IAAI,SAAS,MAChC,OAAO,IAAI,KAAK,gBAAgB,MAAM,WACpC,SAAS,IAAI,KAAK,gBAAgB,CAAC,KACnC;AAAA,MACJ,QAAQ,IAAI,QAAQ;AAAA;AAAA;AAAA,MAGpB,UAAM,kCAAqB,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,QAAQ;AAAA,MAC5D;AAAA,MACA,QAAAA;AAAA,MACA,QAAQ,IAAI;AAAA,MACZ,YAAY,KAAK,IAAI,IAAI;AAAA,MACzB,WAAW,OAAO,IAAI,KAAK,WAAW,MAAM,WAAW,IAAI,KAAK,WAAW,IAAI;AAAA,MAC/E,UAAU,IAAI,KAAK,YAAY;AAAA,IAChC;AACA,QAAI;AACH,YAAM,MAAM;AAAA,QACX;AAAA;AAAA;AAAA,QAGA;AAAA,UACC,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,QACL;AAAA,MACD;AAAA,IACD,SAAS,KAAK;AACb,cAAQ;AAAA,QACP;AAAA,QACA,eAAe,QAAQ,IAAI,UAAU;AAAA,MACtC;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;AAEO,IAAM,WAAW;AAGxB,eAAsB,aACrB,OACA,OAA4C,CAAC,GACpB;AACzB,QAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,SAAS,IAAI,CAAC,GAAG,QAAQ;AAC9D,QAAM,SAAS,KAAK,SAAS,aAAa,KAAK,MAAM,IAAI;AACzD,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,CAAC,QAAQ,MAAM,MAAM,QAAQ,MAAM,MAAM,QAAQ,CAAC;AAAA,EACnD;AACA,QAAM,OAAO,KAAK,MAAM,GAAG,KAAK;AAChC,QAAM,OAAO,KAAK,KAAK,SAAS,CAAC;AACjC,SAAO;AAAA,IACN,SAAS;AAAA,IACT,MAAM,KAAK,SAAS,SAAS,OAAO,aAAa,IAAI,IAAI;AAAA,EAC1D;AACD;AAEA,SAAS,aAAa,GAA2B;AAChD,SAAO,OAAO,KAAK,GAAG,IAAI,KAAK,EAAE,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,MAAM,EAAE,SAAS,WAAW;AAC3F;AAEA,SAAS,aAAa,GAA8C;AACnE,QAAM,CAAC,IAAI,EAAE,IAAI,OAAO,KAAK,GAAG,WAAW,EAAE,SAAS,MAAM,EAAE,MAAM,GAAG;AACvE,SAAO,MAAM,KAAK,EAAE,IAAI,GAAG,IAAI;AAChC;;;ACxFO,SAAS,cACf,KACA,OACiB;AACjB,QAAM,SAAS,IAAI,eAAe;AAClC,QAAM,WAAW,oBAAI,IAAiC;AACtD,aAAW,KAAK,OAAO,UAAU,UAAU;AAC1C,UAAM,OAAO,SAAS,IAAI,EAAE,MAAM,KAAK,CAAC;AACxC,SAAK,KAAK,CAAC;AACX,aAAS,IAAI,EAAE,QAAQ,IAAI;AAAA,EAC5B;AACA,QAAM,aAAa,IAAI,IAAI,IAAI,kBAAkB,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AACvE,QAAM,UAA+B,OAAO,QAAQ,IAAI,CAAC,EAAE,MAAM,QAAQ,MAAM;AAC9E,UAAM,WAAW,SAAS,IAAI,IAAI,KAAK,CAAC;AACxC,WAAO;AAAA,MACN;AAAA,MACA,SAAS,WAAW;AAAA,MACpB,WAAW,EAAE,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,SAAS;AAAA,MACzE,gBAAgB,WAAW,IAAI,IAAI;AAAA,IACpC;AAAA,EACD,CAAC;AACD,SAAO;AAAA,IACN,aAAa,OAAO;AAAA,IACpB,KAAK,OAAO;AAAA,IACZ;AAAA,IACA;AAAA,IACA,WAAW,OAAO;AAAA,IAClB,QAAQ,IAAI,OAAO;AAAA,EACpB;AACD;;;ACxBO,SAAS,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,aACf,KACA,aACA,QACqB;AACrB,QAAM,SAAS,IAAI,eAAe;AAClC,QAAM,gBAAgB,OAAO,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,WAAW,WAAW;AACtF,QAAM,eAAe,IAAI;AAAA,IACxB,IACE,OAAO,EACP,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,eAAe,kBAAkB,EAAE,QAAQ,EAAE,IAAI,CAAC,EACzF,IAAI,CAAC,MAAM,EAAE,MAAgB;AAAA,EAChC;AACA,SAAO;AAAA,IACN,aAAa,OAAO;AAAA,IACpB,OAAO,EAAE,IAAI,CAAC,cAAc,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,UAAU,cAAc;AAAA,IACzF,QAAQ,OAAO,QACb,IAAI,CAAC,EAAE,KAAK,OAAO,EAAE,QAAQ,MAAM,KAAK,aAAa,IAAI,IAAI,EAAE,EAAE,EACjE,OAAO,CAAC,MAAM,EAAE,GAAG;AAAA,IACrB;AAAA,EACD;AACD;AAEA,SAAS,kBAAkB,QAAgB,MAAuB;AACjE,MAAI,4CAA4C,KAAK,IAAI,EAAG,QAAO;AACnE,MAAI,gBAAgB,KAAK,IAAI,KAAK,WAAW,MAAO,QAAO;AAC3D,SAAO,4BAA4B,KAAK,IAAI;AAC7C;;;ACxEA,yBAAwC;AAExC,IAAAC,eAAwD;AAMjD,IAAM,SAAgC,CAAC,QAAQ,SAAS,SAAS;AAIjE,SAAS,SAAS,QAAgB,MAA0B;AAClE,MAAI,kBAAkB,KAAK,IAAI,EAAG,QAAO;AACzC,SAAO,OAAO,YAAY,MAAM,QAAQ,SAAS;AAClD;AAGO,SAAS,OAAO,MAA6B,QAA6B;AAChF,MAAI,KAAK,SAAS,SAAS,EAAG,QAAO;AACrC,MAAI,WAAW,UAAW,QAAO;AACjC,MAAI,KAAK,SAAS,OAAO,EAAG,QAAO;AACnC,SAAO,WAAW,UAAU,KAAK,SAAS,MAAM;AACjD;AAEO,IAAM,YAAY,CAAC,cACzB,+BAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAChD,IAAM,YAAY,MAAc,WAAO,gCAAY,EAAE,EAAE,SAAS,WAAW,CAAC;AAE5E,IAAM,UAAU;AAAA;AAGhB,eAAsB,WAAW,OAAoD;AACpF,SAAO,MAAM;AAAA,IACZ,UAAU,OAAO;AAAA,EAClB;AACD;AAEA,eAAsB,WACrB,OACA,OACwD;AACxD,QAAM,QAAQ,UAAU;AACxB,QAAM,YAAY,MAAM,gBACrB,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,gBAAgB,KAAU,IACtD;AACH,QAAM,CAAC,MAAM,IAAI,MAAM,MAAM;AAAA,IAC5B;AAAA,2CACyC,OAAO;AAAA,IAChD,CAAC,MAAM,MAAM,UAAU,KAAK,GAAG,MAAM,QAAQ,MAAM,WAAW,SAAS;AAAA,EACxE;AACA,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,sCAAsC;AACnE,SAAO,EAAE,OAAO,OAAO;AACxB;AAEA,eAAsB,YAAY,OAAsB,IAA8B;AACrF,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA,IACA,CAAC,EAAE;AAAA,EACJ;AACA,SAAO,KAAK,SAAS;AACtB;AAEA,eAAe,SAAS,OAAsB,OAAkD;AAC/F,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB,UAAU,OAAO;AAAA;AAAA,IAEjB,CAAC,UAAU,KAAK,CAAC;AAAA,EAClB;AACA,SAAO,OAAO;AACf;AAEA,IAAM,SAAS,CAAC,QAAkC;AACjD,QAAM,IAAI,IAAI,QAAQ,QAAQ,IAAI,eAAe,KAAK;AACtD,SAAO,EAAE,WAAW,SAAS,IAAI,EAAE,MAAM,CAAC,IAAI;AAC/C;AACA,IAAM,eAAe,UACpB,6BAAe,kBAAK,cAAc,gBAAgB,gCAAgC;AAM5E,SAAS,kBACf,WACA,OACA,QACa;AACb,SAAO,OAAO,KAAK,SAAS;AAC3B,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,CAAC,MAAO,QAAO,aAAa;AAChC,YAAI,gCAAkB,OAAO,SAAS,EAAG,QAAO,KAAK;AACrD,QAAI,WAAW,UAAU,CAAC,MAAO,QAAO,aAAa;AACrD,UAAM,SAAS,MAAM,SAAS,OAAO,KAAK;AAC1C,QAAI,CAAC,OAAQ,QAAO,aAAa;AACjC,QAAI,CAAC,OAAO,OAAO,QAAQ,MAAM,GAAG;AACnC,iBAAO,6BAAe,kBAAK,WAAW,aAAa,yBAAyB,MAAM,SAAS;AAAA,IAC5F;AACA,QAAI,KAAK,gBAAgB,IAAI,OAAO;AACpC,SAAK,MACH,MAAM,uEAAuE,CAAC,OAAO,EAAE,CAAC,EACxF,MAAM,MAAM,MAAS;AACvB,WAAO,KAAK;AAAA,EACb;AACD;AAGO,IAAM,mBAAmC;AAAA,EAC/C,UAAU,OAAgB;AACzB,UAAM,OAAO,CAAC,MAAc,aAAqB;AAAA,MAChD,SAAS;AAAA,MACT,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAC,EAAE;AAAA,IAC9C;AACA,UAAM,IAAK,SAAS,CAAC;AACrB,UAAM,OAAO,OAAO,EAAE,MAAM,MAAM,WAAW,EAAE,MAAM,EAAE,KAAK,IAAI;AAChE,QAAI,CAAC,QAAQ,KAAK,SAAS,IAAK,QAAO,KAAK,QAAQ,qCAAgC;AACpF,UAAM,SAAS,EAAE,QAAQ;AACzB,QACC,CAAC,MAAM,QAAQ,MAAM,KACrB,OAAO,WAAW,KAClB,CAAC,OAAO,MAAM,CAAC,MAAM,OAAO,SAAS,CAAe,CAAC;AAErD,aAAO,KAAK,UAAU,sCAAsC,OAAO,KAAK,IAAI,CAAC,EAAE;AAChF,UAAM,OAAO,EAAE,eAAe;AAC9B,QACC,SAAS,WACR,CAAC,OAAO,UAAU,IAAI,KAAM,OAAkB,KAAM,OAAkB;AAEvE,aAAO,KAAK,iBAAiB,qDAAqD;AACnF,WAAO;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,QACL;AAAA,QACA,QAAQ,CAAC,GAAG,IAAI,IAAI,MAAsB,CAAC;AAAA,QAC3C,GAAI,SAAS,SAAY,EAAE,eAAe,KAAe,IAAI,CAAC;AAAA,MAC/D;AAAA,IACD;AAAA,EACD;AACD;;;ACvIO,IAAM,SAAS,CAAC,eAA+B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAWvC,UAAU;AAAA;AAAA;AAAA;;;ANOlB,IAAM,qBAAqB;AAC3B,IAAM,2BAA2B;AAGjC,IAAM,gBAAwB;AAE9B,IAAM,cAAN,MAA6C;AAAA,EAKnD,YAAoB,UAAyB,CAAC,GAAG;AAA7B;AACnB,SAAK,WAAO,iCAAmB,QAAQ,QAAQ,kBAAkB;AAAA,EAClE;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;AAErB,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,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,UAAU,KAAK,MAAM,OAAO,CAAC;AAAA,QAC9B;AAAA,MACF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC;AAAA,UACC,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,cAAc,KAAK,EAAE,SAAS,KAAK,SAAS,KAAK,QAAQ,KAAK,EAAE,CAAC;AAAA,QAClE;AAAA,MACF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC,6BAAe,kBAAK,IAAI,gBAAgB,yBAAyB,MAAM,OAAO,CAAC;AAAA,MACjF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC;AAAA,UACC,kBAAK;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,6BAAe,kBAAK,IAAI,gBAAgB,kBAAkB,aAAa,KAAK,KAAK,IAAI,CAAC;AAAA,MACxF;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA,gBACC;AAAA,UACC,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,aAAa,KAAK,KAAK,MAAM,QAAQ,MAAM,WAAW,KAAK,IAAI,IAAI;AAAA,QACpE;AAAA,MACF;AAAA,IACD;AACA,QAAI,OAAO;AACV,UAAI,KAAK;AAAA,QACR;AAAA,QACA;AAAA,QACA,OAAO,QAAQ;AACd,gBAAM,IAAI,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE;AACnC,gBAAM,QAAQ,OAAO,EAAE,IAAI,OAAO,CAAC,KAAK;AACxC,gBAAM,SAAS,EAAE,IAAI,QAAQ,KAAK;AAClC,gBAAM,OAAO,MAAM,aAAa,OAAO;AAAA,YACtC,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,YACzB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,UAC5B,CAAC;AACD,qBAAO,6BAAe,kBAAK,IAAI,aAAa,kBAAkB,IAAI;AAAA,QACnE;AAAA,MACD,CAAC;AAAA,IACF;AAEA,UAAM,WAAkD,QACrD;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,cACC,6BAAS,gBAAgB;AAAA,UACzB,OAAO,QAAQ;AACd,kBAAM,OAAO,IAAI,KAAK,MAAM;AAK5B,kBAAM,YAAY,IAAI,QAAQ,QAAQ,IAAI,SAAS,KAAK;AACxD,kBAAM,EAAE,OAAO,WAAW,OAAO,IAAI,MAAM,WAAW,OAAO;AAAA,cAC5D,GAAG;AAAA,cACH;AAAA,YACD,CAAC;AAED,uBAAO,6BAAe,kBAAK,SAAS,gBAAgB,kCAA6B;AAAA,cAChF,OAAO;AAAA,cACP,GAAG;AAAA,YACJ,CAAC;AAAA,UACF;AAAA,QACD;AAAA,MACD;AAAA,MACA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,UACC,OAAO,QAAQ;AACd,kBAAM,KAAK,MAAM,YAAY,OAAO,IAAI,KAAK,SAAS,IAAI,KAAK,EAAE;AACjE,mBAAO,SACJ,6BAAe,kBAAK,IAAI,iBAAiB,eAAe,QACxD,6BAAe,kBAAK,WAAW,aAAa,oBAAoB;AAAA,UACpE;AAAA,QACD;AAAA,MACD;AAAA,IACD,IACC,CAAC;AACJ,UAAM,UAAU,oBAAI,IAAoB;AACxC,UAAM,QAAQ,CACbC,SACA,QACA,MACA,UACA,WACU;AACV,YAAM,MAAM,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI;AAC3C,YAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,UAAI,OAAO;AACV,cAAM,IAAI;AAAA,UACT,cAAcA,OAAM,oBAAoB,GAAG,KAAK,KAAK;AAAA,QACtD;AAAA,MACD;AACA,cAAQ,IAAI,KAAKA,OAAM;AAGvB,YAAM,QAAQ,kBAAkB,OAAO,OAAO,UAAU,SAAS,QAAQ,IAAI,CAAC;AAC9E,YAAM,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;AAQA,QAAI,KAAK,QAAQ,IAAI;AACpB,YAAM,aAAa,GAAG,mBAAmB;AAKzC,UAAI,KAAoB;AACxB,YAAM,SAAS,MAAqB;AACnC,YAAI,OAAO,MAAM;AAChB,cAAI;AACH,qBAAK,6BAAa,IAAI,IAAI,sBAAsB,aAAe,GAAG,MAAM;AAAA,UACzE,QAAQ;AACP,iBAAK;AAAA,UACN;AAAA,QACD;AACA,eAAO,MAAM;AAAA,MACd;AACA,UAAI,SAAS,OAAO,GAAG,YAAY,GAAG,OAAO,QAAQ;AAKpD,cAAM,WAAO,mCAAqB,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,QAAQ;AACnE,eAAO,IAAI,SAAS,OAAO,GAAG,IAAI,SAAS,GAAG;AAAA,UAC7C,SAAS,EAAE,gBAAgB,4BAA4B,iBAAiB,WAAW;AAAA,QACpF,CAAC;AAAA,MACF,CAAC;AACD,UAAI,SAAS,OAAO,YAAY,YAAY;AAC3C,cAAM,OAAO,OAAO;AACpB,eAAO,OACJ,IAAI,SAAS,MAAM;AAAA,UACnB,SAAS;AAAA,YACR,gBAAgB;AAAA,YAChB,iBAAiB;AAAA,UAClB;AAAA,QACD,CAAC,QACA;AAAA,UACA,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACH,CAAC;AAAA,IACF;AAEA,eAAW,CAAC,QAAQ,MAAM,OAAO,KAAK,IAAK,OAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC;AACvF,eAAW,CAAC,QAAQ,MAAM,QAAQ,KAAK;AACtC,YAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG,UAAU,MAAM;AACpD,eAAW,EAAE,QAAAA,SAAQ,YAAY,KAAK,IAAI,kBAAkB,GAAG;AAC9D,iBAAW,KAAK,YAAY,UAAU,CAAC;AACtC,cAAMA,SAAQ,EAAE,QAAQ,KAAK,OAAO,EAAE,MAAM,EAAE,QAAQ;AAAA,IACxD;AAAA,EACD;AAAA,EAEA,iBAAsC;AACrC,eAAO,uCAAmB,KAAK,QAAQ,YAAY,EAAE,QAAQ,KAAK,KAAK,CAAC;AAAA,EACzE;AACD;","names":["import_core","module","module","import_core","module"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../../node_modules/tsup/assets/cjs_shims.js","../src/module.ts","../src/doctor.ts","../src/log.ts","../src/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\n// 'https://admin.example.com/' and 'admin.example.com' name the same host, so\n// accept both. A scheme here is the likely mistake — this option sits beside\n// ones that are full URLs — and left unnormalized it matches nothing, so the\n// surface 404s everywhere and looks exactly like it was never mounted.\nfunction normalizeHostPattern(host: string): string {\n\treturn host\n\t\t.trim()\n\t\t.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, '')\n\t\t.replace(/\\/.*$/, '')\n\t\t.toLowerCase();\n}\n\n// A request addressed to a hostname this surface does not answer for gets the\n// same 404 an unmounted surface gives — indistinguishable on purpose. A 403\n// would confirm both that the surface exists and that you found the wrong door.\nfunction requireAdminHost(hosts: readonly string[]): Middleware {\n\tconst allowed = new Set(hosts.map(normalizeHostPattern).filter(Boolean));\n\treturn (ctx, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\t// Match with or without the port: a configured 'admin.example.com' should\n\t\t// not stop working behind a non-default port, and a configured\n\t\t// 'localhost:3000' should still be exact. Ports are not a boundary here.\n\t\tconst ok = allowed.has(url.host.toLowerCase()) || allowed.has(url.hostname.toLowerCase());\n\t\treturn ok ? next() : Promise.resolve(setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found'));\n\t};\n}\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\t// null = answer on any hostname.\n\treadonly hosts: string[] | null;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = normalizeMountPath(options.path ?? DEFAULT_ADMIN_PATH);\n\t\tthis.hosts = options.host === undefined ? null : [options.host].flat();\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst token = this.options.adminToken;\n\t\tif (!token) return;\n\n\t\tapp.reserve(this.path);\n\t\t// Declared at the default path so the route table reads literally; re-based when configured.\n\t\tconst at = (p: string): string => this.path + p.slice(DEFAULT_ADMIN_PATH.length);\n\n\t\tconst store = this.options.store;\n\t\t// First in every chain: a surface bound to another hostname must look\n\t\t// unmounted here, before anything reads a token or a body.\n\t\tconst hostGuard = this.hosts ? requireAdminHost(this.hosts) : null;\n\t\t// Collected once at boot: a duplicate name is refused here, not at request time.\n\t\tconst checks = collectChecks(app, this.options.checks ?? []);\n\t\tconst timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;\n\t\tconst doctor = () => runDoctor(checks, timeoutMs);\n\n\t\tconst own: Array<[string, string, Middleware]> = [\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_ATTENTION',\n\t\t\t\t\t\t'What needs attention',\n\t\t\t\t\t\tattention(app, await doctor()),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/manifest',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_MANIFEST',\n\t\t\t\t\t\t'Deployment manifest',\n\t\t\t\t\t\tbuildManifest(app, { version: this.version, log: Boolean(store), host: this.hosts }),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/doctor',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor()),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/config',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_CONFIG',\n\t\t\t\t\t\t'Declared vs held',\n\t\t\t\t\t\tconfigReport(app, this.options.env ?? []),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/routes',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_ROUTES', 'Exposed routes', routesReport(app, this.name)),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/access/tokens',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_TOKENS',\n\t\t\t\t\t\t'Admin tokens',\n\t\t\t\t\t\ttokensReport(app, this.name, store ? await listTokens(store) : null),\n\t\t\t\t\t),\n\t\t\t],\n\t\t];\n\t\tif (store) {\n\t\t\town.push([\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/activity/admin-log',\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst q = new URL(ctx.request.url).searchParams;\n\t\t\t\t\tconst limit = Number(q.get('limit')) || undefined;\n\t\t\t\t\tconst before = q.get('before') ?? undefined;\n\t\t\t\t\tconst page = await readAdminLog(store, {\n\t\t\t\t\t\t...(limit ? { limit } : {}),\n\t\t\t\t\t\t...(before ? { before } : {}),\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'ADMIN_LOG', 'Admin activity', page);\n\t\t\t\t},\n\t\t\t]);\n\t\t}\n\t\t// Issuing and revoking are root-only: a scoped token can never mint one.\n\t\tconst rootOnly: Array<[string, string, Middleware[]]> = store\n\t\t\t? [\n\t\t\t\t\t[\n\t\t\t\t\t\t'POST',\n\t\t\t\t\t\t'/_admin/access/tokens',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tvalidate(issueTokenSchema),\n\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\tconst body = ctx.meta['body'] as {\n\t\t\t\t\t\t\t\t\tname: string;\n\t\t\t\t\t\t\t\t\tscopes: AdminScope[];\n\t\t\t\t\t\t\t\t\texpiresInDays?: number;\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\tconst createdBy = ctx.request.headers.get('x-actor') || 'admin-token';\n\t\t\t\t\t\t\t\tconst { token: plaintext, record } = await issueToken(store, {\n\t\t\t\t\t\t\t\t\t...body,\n\t\t\t\t\t\t\t\t\tcreatedBy,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t// The plaintext is returned once and never stored.\n\t\t\t\t\t\t\t\treturn setApiResponse(HTTP.CREATED, 'TOKEN_ISSUED', 'Token issued — shown once', {\n\t\t\t\t\t\t\t\t\ttoken: plaintext,\n\t\t\t\t\t\t\t\t\t...record,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t\t[\n\t\t\t\t\t\t'DELETE',\n\t\t\t\t\t\t'/_admin/access/tokens/:id',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tasync (ctx) => {\n\t\t\t\t\t\t\t\tconst ok = await revokeToken(store, ctx.meta.params?.['id'] ?? '');\n\t\t\t\t\t\t\t\treturn ok\n\t\t\t\t\t\t\t\t\t? setApiResponse(HTTP.OK, 'TOKEN_REVOKED', 'Token revoked')\n\t\t\t\t\t\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such live token');\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t]\n\t\t\t: [];\n\t\tconst mounted = new Map<string, string>();\n\t\tconst mount = (\n\t\t\tmodule: string,\n\t\t\tmethod: string,\n\t\t\tpath: string,\n\t\t\thandlers: Middleware[],\n\t\t\tneeded?: AdminScope | 'root',\n\t\t): void => {\n\t\t\tconst key = `${method.toUpperCase()} ${path}`;\n\t\t\tconst prior = mounted.get(key);\n\t\t\tif (prior) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[fonderie] ${module} cannot describe ${key}: ${prior} already describes it`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tmounted.set(key, module);\n\t\t\t// The scope comes from the route itself. The log sits before the guard:\n\t\t\t// a refused request is a row too.\n\t\t\tconst guard = requireAdminScope(token, store, needed ?? scopeFor(method, path));\n\t\t\tconst chain: Middleware[] = [\n\t\t\t\t// Logged first so a wrong-host attempt still leaves a row: the\n\t\t\t\t// caller learns nothing from a 404, the operator learns something.\n\t\t\t\t...(store ? [adminLog(store, path, module)] : []),\n\t\t\t\t...(hostGuard ? [hostGuard] : []),\n\t\t\t\tguard,\n\t\t\t\t...handlers,\n\t\t\t];\n\t\t\tapp.addRoute(method, path, ...chain);\n\t\t};\n\n\t\t// The served dashboard. Deliberately OUTSIDE the guard: a browser\n\t\t// navigating to a page cannot send an Authorization header, and neither\n\t\t// file carries data — the HTML is a shell and the script is the same\n\t\t// bundle npm serves. The page asks for a token and sends it itself, so\n\t\t// every request that reads anything is guarded and logged as usual.\n\t\t// These two are not logged: they carry no token to attribute.\n\t\tif (this.options.ui) {\n\t\t\tconst scriptPath = at('/_admin/ui/app.js');\n\t\t\t// Read on first request, not at boot: the bundle sits beside the BUILT\n\t\t\t// module (tsup names its IIFE output <entry>.global.js), so running from\n\t\t\t// source — tests, a linked checkout — has none, and that must not stop\n\t\t\t// an app from booting over a dashboard it may never open.\n\t\t\tlet js: string | null = null;\n\t\t\tconst loadJs = (): string | null => {\n\t\t\t\tif (js === null) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tjs = readFileSync(new URL('./ui/app.global.js', import.meta.url), 'utf8');\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tjs = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn js || null;\n\t\t\t};\n\t\t\tconst ui = (h: Middleware): Middleware[] => (hostGuard ? [hostGuard, h] : [h]);\n\t\t\tapp.addRoute(\n\t\t\t\t'GET',\n\t\t\t\tat('/_admin/ui'),\n\t\t\t\t...ui(async (ctx) => {\n\t\t\t\t\t// From the request, not from config: this is the only place that\n\t\t\t\t\t// knows basePath, a moved path AND a trailing slash at once. The\n\t\t\t\t\t// router's own normalizer, so the href and the routing cannot\n\t\t\t\t\t// disagree about what this path is.\n\t\t\t\t\tconst here = normalizeRequestPath(new URL(ctx.request.url).pathname);\n\t\t\t\t\treturn new Response(uiHtml(`${here}/app.js`), {\n\t\t\t\t\t\theaders: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },\n\t\t\t\t\t});\n\t\t\t\t}),\n\t\t\t);\n\t\t\tapp.addRoute(\n\t\t\t\t'GET',\n\t\t\t\tscriptPath,\n\t\t\t\t...ui(async () => {\n\t\t\t\t\tconst body = loadJs();\n\t\t\t\t\treturn body\n\t\t\t\t\t\t? new Response(body, {\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t'content-type': 'text/javascript; charset=utf-8',\n\t\t\t\t\t\t\t\t\t'cache-control': 'public, max-age=300',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: setApiResponse(\n\t\t\t\t\t\t\t\tHTTP.SERVICE_UNAVAILABLE,\n\t\t\t\t\t\t\t\t'UI_NOT_BUILT',\n\t\t\t\t\t\t\t\t'This copy of @fonderie/admin has no built dashboard (dist/ui). Install the published package, or run its build.',\n\t\t\t\t\t\t\t);\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tfor (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);\n\t\tfor (const [method, path, handlers] of rootOnly)\n\t\t\tmount(this.name, method, at(path), handlers, 'root');\n\t\tfor (const { 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; host: string[] | null },\n): IAdminManifest {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems) {\n\t\tconst list = byModule.get(p.module) ?? [];\n\t\tlist.push(p);\n\t\tbyModule.set(p.module, list);\n\t}\n\tconst describing = new Set(app.adminDescriptions().map((d) => d.module));\n\tconst modules: IAdminModuleEntry[] = report.modules.map(({ name, version }) => {\n\t\tconst problems = byModule.get(name) ?? [];\n\t\treturn {\n\t\t\tname,\n\t\t\tversion: version ?? null,\n\t\t\treadiness: { ok: !problems.some((p) => p.severity === 'error'), problems },\n\t\t\tdescribesAdmin: describing.has(name),\n\t\t};\n\t});\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tenv: report.env,\n\t\tadmin,\n\t\tmodules,\n\t\treadiness: report.readiness,\n\t\troutes: app.routes(),\n\t};\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type {\n\tIAdminConfigReport,\n\tIAdminRoutesReport,\n\tIAdminTokenRecord,\n\tIAdminTokensReport,\n} from './types';\n\nexport function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems)\n\t\tbyModule.set(p.module, [...(byModule.get(p.module) ?? []), p]);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\treadiness: report.readiness,\n\t\tmodules: report.modules.map(({ name }) => ({ name, problems: byModule.get(name) ?? [] })),\n\t\tenv: env.map((name) => ({\n\t\t\tname,\n\t\t\tset: process.env[name] !== undefined && process.env[name] !== '',\n\t\t})),\n\t};\n}\n\nconst PROBES = new Set(['/healthz', '/readyz', '/metrics']);\n\nexport function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport {\n\treturn {\n\t\tgeneratedAt: new Date().toISOString(),\n\t\troutes: app.routes().map((r) => ({\n\t\t\t...r,\n\t\t\tguard:\n\t\t\t\tr.module === adminModule\n\t\t\t\t\t? 'admin'\n\t\t\t\t\t: PROBES.has(r.path) && r.module === '@fonderie/core'\n\t\t\t\t\t\t? 'probe'\n\t\t\t\t\t\t: 'app',\n\t\t})),\n\t};\n}\n\n// A brick's own adminToken shows as a readiness problem only when weak, so\n// \"set at all\" has to be asked; a described module that also registers a\n// legacy route under /admin, /plans or /billing/wallet/grant has one.\nexport function tokensReport(\n\tapp: IFonderieApp,\n\tadminModule: string,\n\tissued: IAdminTokenRecord[] | null,\n): IAdminTokensReport {\n\tconst report = app.securityReport();\n\tconst adminProblems = report.readiness.problems.filter((p) => p.module === adminModule);\n\tconst legacyOwners = new Set(\n\t\tapp\n\t\t\t.routes()\n\t\t\t.filter((r) => r.module && r.module !== adminModule && isLegacyAdminPath(r.method, r.path))\n\t\t\t.map((r) => r.module as string),\n\t);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tadmin: { ok: !adminProblems.some((p) => p.severity === 'error'), problems: adminProblems },\n\t\tlegacy: report.modules\n\t\t\t.map(({ name }) => ({ module: name, set: legacyOwners.has(name) }))\n\t\t\t.filter((e) => e.set),\n\t\tissued,\n\t};\n}\n\nfunction isLegacyAdminPath(method: string, path: string): boolean {\n\tif (/\\/admin\\/(config|secrets|templates)(\\/|$)/.test(path)) return true;\n\tif (/\\/plans(\\/|$)/.test(path) && method !== 'GET') return true;\n\treturn /\\/billing\\/wallet\\/grant$/.test(path);\n}\n","import { createHash, randomBytes } from 'node:crypto';\nimport type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { HTTP, constantTimeEqual, setApiResponse } from '@fonderie/core';\nimport type { IRequestSchema } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { AdminScope, IAdminTokenRecord } from './types';\n\nexport const SCOPES: readonly AdminScope[] = ['read', 'write', 'secrets'];\n\n// Derived from the route, never annotated: the plaintext secret read is the\n// one thing that needs `secrets`; any other mutation needs `write`.\nexport function scopeFor(method: string, path: string): AdminScope {\n\tif (/\\/secrets(\\/|$)/.test(path)) return 'secrets';\n\treturn method.toUpperCase() === 'GET' ? 'read' : 'write';\n}\n\n// `write` implies `read`; `secrets` implies both.\nexport function grants(held: readonly AdminScope[], needed: AdminScope): boolean {\n\tif (held.includes('secrets')) return true;\n\tif (needed === 'secrets') return false;\n\tif (held.includes('write')) return true;\n\treturn needed === 'read' && held.includes('read');\n}\n\nexport const hashToken = (token: string): string =>\n\tcreateHash('sha256').update(token).digest('hex');\nconst mintToken = (): string => `fad_${randomBytes(24).toString('base64url')}`;\n\nconst COLUMNS = `id, name, scopes, created_by AS \"createdBy\", created_at AS \"createdAt\",\n expires_at AS \"expiresAt\", revoked_at AS \"revokedAt\", last_used_at AS \"lastUsedAt\"`;\n\nexport async function listTokens(store: IStoreAdapter): Promise<IAdminTokenRecord[]> {\n\treturn store.query<IAdminTokenRecord>(\n\t\t`SELECT ${COLUMNS} FROM fonderie_admin_tokens ORDER BY created_at DESC`,\n\t);\n}\n\nexport async function issueToken(\n\tstore: IStoreAdapter,\n\tinput: { name: string; scopes: AdminScope[]; expiresInDays?: number; createdBy: string },\n): Promise<{ token: string; record: IAdminTokenRecord }> {\n\tconst token = mintToken();\n\tconst expiresAt = input.expiresInDays\n\t\t? new Date(Date.now() + input.expiresInDays * 86_400_000)\n\t\t: null;\n\tconst [record] = await store.query<IAdminTokenRecord>(\n\t\t`INSERT INTO fonderie_admin_tokens (name, token_hash, scopes, created_by, expires_at)\n\t\t VALUES ($1, $2, $3, $4, $5) RETURNING ${COLUMNS}`,\n\t\t[input.name, hashToken(token), input.scopes, input.createdBy, expiresAt],\n\t);\n\tif (!record) throw new Error('[admin] token insert returned no row');\n\treturn { token, record };\n}\n\nexport async function revokeToken(store: IStoreAdapter, id: string): Promise<boolean> {\n\tconst rows = await store.query<{ id: string }>(\n\t\t`UPDATE fonderie_admin_tokens SET revoked_at = now() WHERE id = $1 AND revoked_at IS NULL RETURNING id`,\n\t\t[id],\n\t);\n\treturn rows.length > 0;\n}\n\nasync function findLive(store: IStoreAdapter, token: string): Promise<IAdminTokenRecord | null> {\n\tconst [row] = await store.query<IAdminTokenRecord>(\n\t\t`SELECT ${COLUMNS} FROM fonderie_admin_tokens\n\t\t WHERE token_hash = $1 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())`,\n\t\t[hashToken(token)],\n\t);\n\treturn row ?? null;\n}\n\nconst bearer = (ctx: IFonderieContext): string => {\n\tconst h = ctx.request.headers.get('authorization') ?? '';\n\treturn h.startsWith('Bearer ') ? h.slice(7) : '';\n};\nconst UNAUTHORIZED = () =>\n\tsetApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token');\n\n// The bootstrap token is the root: every scope, and the only credential that\n// may issue or revoke tokens. Issued tokens carry their scopes; a valid token\n// short of the route's scope is 403, an unknown, revoked or expired one is\n// the same 401 as a missing one. The token's name becomes the log's actor.\nexport function requireAdminScope(\n\tbootstrap: string,\n\tstore: IStoreAdapter | undefined,\n\tneeded: AdminScope | 'root',\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst token = bearer(ctx);\n\t\tif (!token) return UNAUTHORIZED();\n\t\tif (constantTimeEqual(token, bootstrap)) return next();\n\t\tif (needed === 'root' || !store) return UNAUTHORIZED();\n\t\tconst record = await findLive(store, token);\n\t\tif (!record) return UNAUTHORIZED();\n\t\tif (!grants(record.scopes, needed)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', `This token lacks the '${needed}' scope`);\n\t\t}\n\t\tctx.meta['adminTokenName'] = record.name;\n\t\tvoid store\n\t\t\t.query(`UPDATE fonderie_admin_tokens SET last_used_at = now() WHERE id = $1`, [record.id])\n\t\t\t.catch(() => undefined);\n\t\treturn next();\n\t};\n}\n\n// { name: 1–100 chars, scopes: non-empty subset of SCOPES, expiresInDays?: 1–3650 }\nexport const issueTokenSchema: IRequestSchema = {\n\tsafeParse(input: unknown) {\n\t\tconst fail = (path: string, message: string) => ({\n\t\t\tsuccess: false as const,\n\t\t\terror: { issues: [{ path: [path], message }] },\n\t\t});\n\t\tconst b = (input ?? {}) as Record<string, unknown>;\n\t\tconst name = typeof b['name'] === 'string' ? b['name'].trim() : '';\n\t\tif (!name || name.length > 100) return fail('name', 'name is required (1–100 chars)');\n\t\tconst scopes = b['scopes'];\n\t\tif (\n\t\t\t!Array.isArray(scopes) ||\n\t\t\tscopes.length === 0 ||\n\t\t\t!scopes.every((s) => SCOPES.includes(s as AdminScope))\n\t\t)\n\t\t\treturn fail('scopes', `scopes must be a non-empty list of ${SCOPES.join(', ')}`);\n\t\tconst days = b['expiresInDays'];\n\t\tif (\n\t\t\tdays !== undefined &&\n\t\t\t(!Number.isInteger(days) || (days as number) < 1 || (days as number) > 3650)\n\t\t)\n\t\t\treturn fail('expiresInDays', 'expiresInDays must be an integer between 1 and 3650');\n\t\treturn {\n\t\t\tsuccess: true as const,\n\t\t\tdata: {\n\t\t\t\tname,\n\t\t\t\tscopes: [...new Set(scopes as AdminScope[])],\n\t\t\t\t...(days !== undefined ? { expiresInDays: days as number } : {}),\n\t\t\t},\n\t\t};\n\t},\n};\n","// The served shell's document. Inlined as a string so it ships in dist with no\n// copy step. The script path is passed in, not relative: the surface can be\n// mounted under any basePath or path, and a trailing slash must not break it.\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
|
@@ -7,6 +7,27 @@ interface IAdminOptions {
|
|
|
7
7
|
checks?: IAdminCheck[];
|
|
8
8
|
checkTimeoutMs?: number;
|
|
9
9
|
ui?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Answer only for requests addressed to this hostname (or one of these);
|
|
12
|
+
* anything else gets the same 404 an unmounted surface gives. Port optional:
|
|
13
|
+
* 'admin.example.com' also matches 'admin.example.com:8443'.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ NOT an access control. The `Host` header is set by the client, so this
|
|
16
|
+
* is only worth anything when something upstream decides the hostname and
|
|
17
|
+
* your origin is not reachable around it. Same footgun as `trustProxy` in
|
|
18
|
+
* @fonderie/core's client-ip middleware.
|
|
19
|
+
*
|
|
20
|
+
* What it DOES buy, concretely: on a platform that gives every deployment a
|
|
21
|
+
* permanent URL of its own (`project-abc.vercel.app`), that URL answers
|
|
22
|
+
* whatever your custom domain answers — walking straight around anything you
|
|
23
|
+
* put in front of the domain. Binding the surface to the domain closes that
|
|
24
|
+
* hole while the rest of the app keeps serving on both.
|
|
25
|
+
*
|
|
26
|
+
* What it does NOT buy: protection from someone sending the right `Host` to
|
|
27
|
+
* your origin directly. Only the edge can stop that — a WAF/firewall rule on
|
|
28
|
+
* the admin path, or an origin locked to the proxy.
|
|
29
|
+
*/
|
|
30
|
+
host?: string | string[];
|
|
10
31
|
store?: IStoreAdapter;
|
|
11
32
|
env?: string[];
|
|
12
33
|
}
|
|
@@ -22,6 +43,7 @@ interface IAdminManifest {
|
|
|
22
43
|
admin: {
|
|
23
44
|
version: string;
|
|
24
45
|
log: boolean;
|
|
46
|
+
host: string[] | null;
|
|
25
47
|
};
|
|
26
48
|
modules: IAdminModuleEntry[];
|
|
27
49
|
readiness: IReadinessReport;
|
|
@@ -118,6 +140,7 @@ declare class AdminModule implements IFonderieModule {
|
|
|
118
140
|
readonly name = "@fonderie/admin";
|
|
119
141
|
readonly version: string;
|
|
120
142
|
readonly path: string;
|
|
143
|
+
readonly hosts: string[] | null;
|
|
121
144
|
constructor(options?: IAdminOptions);
|
|
122
145
|
install(app: IFonderieApp): void;
|
|
123
146
|
checkReadiness(): IReadinessProblem[];
|
|
@@ -126,6 +149,7 @@ declare class AdminModule implements IFonderieModule {
|
|
|
126
149
|
declare function buildManifest(app: IFonderieApp, admin: {
|
|
127
150
|
version: string;
|
|
128
151
|
log: boolean;
|
|
152
|
+
host: string[] | null;
|
|
129
153
|
}): IAdminManifest;
|
|
130
154
|
|
|
131
155
|
interface INamedCheck extends IAdminCheck {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,27 @@ interface IAdminOptions {
|
|
|
7
7
|
checks?: IAdminCheck[];
|
|
8
8
|
checkTimeoutMs?: number;
|
|
9
9
|
ui?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Answer only for requests addressed to this hostname (or one of these);
|
|
12
|
+
* anything else gets the same 404 an unmounted surface gives. Port optional:
|
|
13
|
+
* 'admin.example.com' also matches 'admin.example.com:8443'.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ NOT an access control. The `Host` header is set by the client, so this
|
|
16
|
+
* is only worth anything when something upstream decides the hostname and
|
|
17
|
+
* your origin is not reachable around it. Same footgun as `trustProxy` in
|
|
18
|
+
* @fonderie/core's client-ip middleware.
|
|
19
|
+
*
|
|
20
|
+
* What it DOES buy, concretely: on a platform that gives every deployment a
|
|
21
|
+
* permanent URL of its own (`project-abc.vercel.app`), that URL answers
|
|
22
|
+
* whatever your custom domain answers — walking straight around anything you
|
|
23
|
+
* put in front of the domain. Binding the surface to the domain closes that
|
|
24
|
+
* hole while the rest of the app keeps serving on both.
|
|
25
|
+
*
|
|
26
|
+
* What it does NOT buy: protection from someone sending the right `Host` to
|
|
27
|
+
* your origin directly. Only the edge can stop that — a WAF/firewall rule on
|
|
28
|
+
* the admin path, or an origin locked to the proxy.
|
|
29
|
+
*/
|
|
30
|
+
host?: string | string[];
|
|
10
31
|
store?: IStoreAdapter;
|
|
11
32
|
env?: string[];
|
|
12
33
|
}
|
|
@@ -22,6 +43,7 @@ interface IAdminManifest {
|
|
|
22
43
|
admin: {
|
|
23
44
|
version: string;
|
|
24
45
|
log: boolean;
|
|
46
|
+
host: string[] | null;
|
|
25
47
|
};
|
|
26
48
|
modules: IAdminModuleEntry[];
|
|
27
49
|
readiness: IReadinessReport;
|
|
@@ -118,6 +140,7 @@ declare class AdminModule implements IFonderieModule {
|
|
|
118
140
|
readonly name = "@fonderie/admin";
|
|
119
141
|
readonly version: string;
|
|
120
142
|
readonly path: string;
|
|
143
|
+
readonly hosts: string[] | null;
|
|
121
144
|
constructor(options?: IAdminOptions);
|
|
122
145
|
install(app: IFonderieApp): void;
|
|
123
146
|
checkReadiness(): IReadinessProblem[];
|
|
@@ -126,6 +149,7 @@ declare class AdminModule implements IFonderieModule {
|
|
|
126
149
|
declare function buildManifest(app: IFonderieApp, admin: {
|
|
127
150
|
version: string;
|
|
128
151
|
log: boolean;
|
|
152
|
+
host: string[] | null;
|
|
129
153
|
}): IAdminManifest;
|
|
130
154
|
|
|
131
155
|
interface INamedCheck extends IAdminCheck {
|
package/dist/index.js
CHANGED
|
@@ -332,22 +332,37 @@ var uiHtml = (scriptPath) => `<!doctype html>
|
|
|
332
332
|
// src/module.ts
|
|
333
333
|
var DEFAULT_ADMIN_PATH = "/_admin";
|
|
334
334
|
var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
335
|
-
var ADMIN_VERSION = "0.
|
|
335
|
+
var ADMIN_VERSION = "0.8.1";
|
|
336
|
+
function normalizeHostPattern(host) {
|
|
337
|
+
return host.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/.*$/, "").toLowerCase();
|
|
338
|
+
}
|
|
339
|
+
function requireAdminHost(hosts) {
|
|
340
|
+
const allowed = new Set(hosts.map(normalizeHostPattern).filter(Boolean));
|
|
341
|
+
return (ctx, next) => {
|
|
342
|
+
const url = new URL(ctx.request.url);
|
|
343
|
+
const ok = allowed.has(url.host.toLowerCase()) || allowed.has(url.hostname.toLowerCase());
|
|
344
|
+
return ok ? next() : Promise.resolve(setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Not found"));
|
|
345
|
+
};
|
|
346
|
+
}
|
|
336
347
|
var AdminModule = class {
|
|
337
348
|
constructor(options = {}) {
|
|
338
349
|
this.options = options;
|
|
339
350
|
this.path = normalizeMountPath(options.path ?? DEFAULT_ADMIN_PATH);
|
|
351
|
+
this.hosts = options.host === void 0 ? null : [options.host].flat();
|
|
340
352
|
}
|
|
341
353
|
options;
|
|
342
354
|
name = "@fonderie/admin";
|
|
343
355
|
version = ADMIN_VERSION;
|
|
344
356
|
path;
|
|
357
|
+
// null = answer on any hostname.
|
|
358
|
+
hosts;
|
|
345
359
|
install(app) {
|
|
346
360
|
const token = this.options.adminToken;
|
|
347
361
|
if (!token) return;
|
|
348
362
|
app.reserve(this.path);
|
|
349
363
|
const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
|
|
350
364
|
const store = this.options.store;
|
|
365
|
+
const hostGuard = this.hosts ? requireAdminHost(this.hosts) : null;
|
|
351
366
|
const checks = collectChecks(app, this.options.checks ?? []);
|
|
352
367
|
const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
353
368
|
const doctor = () => runDoctor(checks, timeoutMs);
|
|
@@ -369,7 +384,7 @@ var AdminModule = class {
|
|
|
369
384
|
HTTP2.OK,
|
|
370
385
|
"ADMIN_MANIFEST",
|
|
371
386
|
"Deployment manifest",
|
|
372
|
-
buildManifest(app, { version: this.version, log: Boolean(store) })
|
|
387
|
+
buildManifest(app, { version: this.version, log: Boolean(store), host: this.hosts })
|
|
373
388
|
)
|
|
374
389
|
],
|
|
375
390
|
[
|
|
@@ -461,7 +476,14 @@ var AdminModule = class {
|
|
|
461
476
|
}
|
|
462
477
|
mounted.set(key, module);
|
|
463
478
|
const guard = requireAdminScope(token, store, needed ?? scopeFor(method, path));
|
|
464
|
-
const chain =
|
|
479
|
+
const chain = [
|
|
480
|
+
// Logged first so a wrong-host attempt still leaves a row: the
|
|
481
|
+
// caller learns nothing from a 404, the operator learns something.
|
|
482
|
+
...store ? [adminLog(store, path, module)] : [],
|
|
483
|
+
...hostGuard ? [hostGuard] : [],
|
|
484
|
+
guard,
|
|
485
|
+
...handlers
|
|
486
|
+
];
|
|
465
487
|
app.addRoute(method, path, ...chain);
|
|
466
488
|
};
|
|
467
489
|
if (this.options.ui) {
|
|
@@ -477,25 +499,34 @@ var AdminModule = class {
|
|
|
477
499
|
}
|
|
478
500
|
return js || null;
|
|
479
501
|
};
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
502
|
+
const ui = (h) => hostGuard ? [hostGuard, h] : [h];
|
|
503
|
+
app.addRoute(
|
|
504
|
+
"GET",
|
|
505
|
+
at("/_admin/ui"),
|
|
506
|
+
...ui(async (ctx) => {
|
|
507
|
+
const here = normalizeRequestPath2(new URL(ctx.request.url).pathname);
|
|
508
|
+
return new Response(uiHtml(`${here}/app.js`), {
|
|
509
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }
|
|
510
|
+
});
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
app.addRoute(
|
|
514
|
+
"GET",
|
|
515
|
+
scriptPath,
|
|
516
|
+
...ui(async () => {
|
|
517
|
+
const body = loadJs();
|
|
518
|
+
return body ? new Response(body, {
|
|
519
|
+
headers: {
|
|
520
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
521
|
+
"cache-control": "public, max-age=300"
|
|
522
|
+
}
|
|
523
|
+
}) : setApiResponse2(
|
|
524
|
+
HTTP2.SERVICE_UNAVAILABLE,
|
|
525
|
+
"UI_NOT_BUILT",
|
|
526
|
+
"This copy of @fonderie/admin has no built dashboard (dist/ui). Install the published package, or run its build."
|
|
527
|
+
);
|
|
528
|
+
})
|
|
529
|
+
);
|
|
499
530
|
}
|
|
500
531
|
for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
|
|
501
532
|
for (const [method, path, handlers] of rootOnly)
|