@fonderie/admin 0.1.0 → 0.3.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
@@ -41,9 +41,69 @@ curl -H "Authorization: Bearer $ADMIN_TOKEN" https://api.example.com/_admin/mani
41
41
  }
42
42
  ```
43
43
 
44
- `version` is `null` until a brick reports one (`IFonderieModule.version`).
45
- Readiness problems are shown here in production — unlike `/readyz`, which is
46
- public — because the operator is the audience.
44
+ `version` is `null` until a brick reports one (`IFonderieModule.version`);
45
+ `describesAdmin` says whether the brick offers routes below. Readiness
46
+ problems are shown here in production — unlike `/readyz`, which is public —
47
+ because the operator is the audience.
48
+
49
+ ## `GET /_admin/doctor` and `GET /_admin`
50
+
51
+ The five reconciliation checks from `docs/OPERATIONS.md` — and any the app
52
+ adds — with a permanent address. Each check reads the other side of a copy
53
+ (the provider, DNS, the database), so `/doctor` is on demand, not cached.
54
+
55
+ ```json
56
+ { "generatedAt": "…", "ok": false, "checks": [
57
+ { "name": "billing.subscription-drift", "module": "@fonderie/billing", "ok": false,
58
+ "findings": ["workspace:ws_1 (sub_x): status ours=active theirs=canceled [over-granting]"], "durationMs": 412 },
59
+ { "name": "billing.webhook-registration", "module": "@fonderie/billing", "ok": true,
60
+ "findings": [], "skipped": "config.publicUrl is not set", "durationMs": 0 },
61
+ { "name": "courier.sender-dns", "module": "@fonderie/courier", "ok": true,
62
+ "findings": ["DMARC p=none — monitoring only"], "durationMs": 88 }
63
+ ] }
64
+ ```
65
+
66
+ - `ok` is false only for a hard failure. Findings on a passing check are
67
+ advice and never turn a healthy deployment red.
68
+ - A check that throws becomes a finding; one that exceeds `checkTimeoutMs`
69
+ (default 10 s) is reported as timed out. The doctor itself never throws.
70
+ - `skipped` says why a check could not run — a provider that cannot be asked,
71
+ a `publicUrl` that is not set — instead of guessing.
72
+
73
+ `GET /_admin` is the attention page: readiness problems as reported, failed
74
+ checks as errors, advice as advice. `ok: true, items: []` is green.
75
+
76
+ Checks a module cannot own belong to the app:
77
+
78
+ ```ts
79
+ new AdminModule({
80
+ adminToken,
81
+ checks: [{
82
+ name: 'app.migrations',
83
+ run: async () => {
84
+ const pending = await runner.pending();
85
+ return { ok: pending.length === 0, findings: pending.map((f) => `pending: ${f}`) };
86
+ },
87
+ }],
88
+ });
89
+ ```
90
+
91
+ ## Composed routes
92
+
93
+ Bricks that implement `describeAdmin()` have their admin routes mounted here,
94
+ behind this module's token, whatever their own `adminToken` is set to. Two
95
+ bricks describing the same route fail boot, naming both.
96
+
97
+ | Under `/_admin` | From | Legacy standalone path (deprecated) |
98
+ |---|---|---|
99
+ | `GET\|PUT\|DELETE /config[/:key]`, `…/revisions`, `…/rollback` | `@fonderie/config` | `/admin/config…` |
100
+ | `GET\|PUT\|DELETE /secrets[/:key]`, `…/revisions`, `…/rollback`, `POST …/reveal` | `@fonderie/config` | `/admin/secrets…` |
101
+ | `GET\|PUT\|DELETE /templates[/:type]`, `…/revisions`, `…/rollback` | `@fonderie/courier` | `/admin/templates…` |
102
+ | `POST /plans`, `PUT\|DELETE /plans/:planId` | `@fonderie/billing` | `/plans…` |
103
+ | `POST /wallet/grant` (with `config.wallet`) | `@fonderie/billing` | `/billing/wallet/grant` |
104
+
105
+ The legacy paths keep working with each brick's own token and are removed in
106
+ a later major; `docs/ADMIN-BRICK-DESIGN.md` §9.
47
107
 
48
108
  ## Why it's a brick
49
109
 
package/brain/outcomes.md CHANGED
@@ -11,4 +11,6 @@ downloading tarballs.
11
11
 
12
12
  | Method | Path | Middleware chain (auth / validation / handler) |
13
13
  |---|---|---|
14
- | GET | `/_admin/manifest` | `g(async () => setApiResponse( HTTP.OK, 'ADMIN_MANIFEST', 'Deployment manifest', buildManifest(app, { version: this.version }), ), )` |
14
+ | GET | `/_admin` | `async () => setApiResponse( HTTP.OK, 'ADMIN_ATTENTION', 'What needs attention', attention(app, await doctor()), )` |
15
+ | GET | `/_admin/doctor` | `async () => setApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor())` |
16
+ | GET | `/_admin/manifest` | `async () => setApiResponse( HTTP.OK, 'ADMIN_MANIFEST', 'Deployment manifest', buildManifest(app, { version: this.version }), )` |
@@ -16,11 +16,21 @@ const ADMIN_VERSION: string
16
16
 
17
17
  const DEFAULT_ADMIN_PATH: "/_admin"
18
18
 
19
+ const DEFAULT_CHECK_TIMEOUT_MS: 10000
20
+
19
21
  function buildManifest(app: IFonderieApp, admin: { version: string; }): IAdminManifest
20
22
 
23
+ function runDoctor(checks: INamedCheck[], timeoutMs: number): Promise<IAdminDoctorReport>
24
+
25
+ function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[]
26
+
27
+ function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention
28
+
21
29
  interface IAdminOptions {
22
30
  adminToken?: string;
23
31
  path?: string;
32
+ checks?: IAdminCheck[];
33
+ checkTimeoutMs?: number;
24
34
  }
25
35
 
26
36
  interface IAdminManifest {
@@ -38,5 +48,30 @@ interface IAdminModuleEntry {
38
48
  name: string;
39
49
  version: string | null;
40
50
  readiness: IReadinessReport;
51
+ describesAdmin: boolean;
52
+ }
53
+
54
+ interface IAdminCheckResult extends IAdminCheckReport {
55
+ name: string;
56
+ module: string;
57
+ durationMs: number;
58
+ }
59
+
60
+ interface IAdminDoctorReport {
61
+ generatedAt: string;
62
+ ok: boolean;
63
+ checks: IAdminCheckResult[];
64
+ }
65
+
66
+ interface IAdminAttention {
67
+ generatedAt: string;
68
+ ok: boolean;
69
+ items: IAdminAttentionItem[];
70
+ }
71
+
72
+ interface IAdminAttentionItem {
73
+ source: string;
74
+ severity: 'error' | 'advice';
75
+ message: string;
41
76
  }
42
77
  ```
package/dist/index.cjs CHANGED
@@ -23,7 +23,11 @@ __export(index_exports, {
23
23
  ADMIN_VERSION: () => ADMIN_VERSION,
24
24
  AdminModule: () => AdminModule,
25
25
  DEFAULT_ADMIN_PATH: () => DEFAULT_ADMIN_PATH,
26
- buildManifest: () => buildManifest
26
+ DEFAULT_CHECK_TIMEOUT_MS: () => DEFAULT_CHECK_TIMEOUT_MS,
27
+ attention: () => attention,
28
+ buildManifest: () => buildManifest,
29
+ collectChecks: () => collectChecks,
30
+ runDoctor: () => runDoctor
27
31
  });
28
32
  module.exports = __toCommonJS(index_exports);
29
33
 
@@ -31,6 +35,70 @@ module.exports = __toCommonJS(index_exports);
31
35
  var import_core = require("@fonderie/core");
32
36
  var import_middlewares = require("@fonderie/core/middlewares");
33
37
 
38
+ // src/doctor.ts
39
+ function collectChecks(app, own) {
40
+ const seen = /* @__PURE__ */ new Map();
41
+ const out = [];
42
+ const add = (module2, check) => {
43
+ const prior = seen.get(check.name);
44
+ if (prior)
45
+ throw new Error(
46
+ `[fonderie] ${module2} cannot offer check "${check.name}": ${prior} already does`
47
+ );
48
+ seen.set(check.name, module2);
49
+ out.push({ ...check, module: module2 });
50
+ };
51
+ for (const { module: module2, description } of app.adminDescriptions()) {
52
+ for (const c of description.checks ?? []) add(module2, c);
53
+ }
54
+ for (const c of own) add("application", c);
55
+ return out;
56
+ }
57
+ async function runOne(check, timeoutMs) {
58
+ const started = Date.now();
59
+ let timer;
60
+ const timeout = new Promise((resolve) => {
61
+ timer = setTimeout(
62
+ () => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),
63
+ timeoutMs
64
+ );
65
+ });
66
+ let report;
67
+ try {
68
+ report = await Promise.race([check.run(), timeout]);
69
+ } catch (err) {
70
+ report = {
71
+ ok: false,
72
+ findings: [`check threw: ${err instanceof Error ? err.message : String(err)}`]
73
+ };
74
+ } finally {
75
+ if (timer) clearTimeout(timer);
76
+ }
77
+ return { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };
78
+ }
79
+ async function runDoctor(checks, timeoutMs) {
80
+ const results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));
81
+ return { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ok: results.every((r) => r.ok), checks: results };
82
+ }
83
+ function attention(app, doctor) {
84
+ const items = [];
85
+ for (const p of app.checkProductionReadiness().problems) {
86
+ items.push({
87
+ source: p.module,
88
+ severity: p.severity === "error" ? "error" : "advice",
89
+ message: p.message
90
+ });
91
+ }
92
+ for (const c of doctor.checks) {
93
+ if (c.skipped) continue;
94
+ const severity = c.ok ? "advice" : "error";
95
+ if (!c.ok && c.findings.length === 0)
96
+ items.push({ source: c.name, severity, message: "failed" });
97
+ for (const f of c.findings) items.push({ source: c.name, severity, message: f });
98
+ }
99
+ return { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === "error"), items };
100
+ }
101
+
34
102
  // src/manifest.ts
35
103
  function buildManifest(app, admin) {
36
104
  const report = app.securityReport();
@@ -40,12 +108,14 @@ function buildManifest(app, admin) {
40
108
  list.push(p);
41
109
  byModule.set(p.module, list);
42
110
  }
111
+ const describing = new Set(app.adminDescriptions().map((d) => d.module));
43
112
  const modules = report.modules.map(({ name, version }) => {
44
113
  const problems = byModule.get(name) ?? [];
45
114
  return {
46
115
  name,
47
116
  version: version ?? null,
48
- readiness: { ok: !problems.some((p) => p.severity === "error"), problems }
117
+ readiness: { ok: !problems.some((p) => p.severity === "error"), problems },
118
+ describesAdmin: describing.has(name)
49
119
  };
50
120
  });
51
121
  return {
@@ -60,7 +130,8 @@ function buildManifest(app, admin) {
60
130
 
61
131
  // src/module.ts
62
132
  var DEFAULT_ADMIN_PATH = "/_admin";
63
- var ADMIN_VERSION = "0.1.0";
133
+ var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
134
+ var ADMIN_VERSION = "0.3.0";
64
135
  var AdminModule = class {
65
136
  constructor(options = {}) {
66
137
  this.options = options;
@@ -75,23 +146,54 @@ var AdminModule = class {
75
146
  if (!token) return;
76
147
  app.reserve(this.path);
77
148
  const guard = (0, import_middlewares.requireAdminToken)(token);
78
- const g = (h) => (ctx, next) => guard(ctx, () => h(ctx, next));
79
149
  const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
80
- const routes = [
150
+ const checks = collectChecks(app, this.options.checks ?? []);
151
+ const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
152
+ const doctor = () => runDoctor(checks, timeoutMs);
153
+ const own = [
154
+ [
155
+ "GET",
156
+ "/_admin",
157
+ async () => (0, import_core.setApiResponse)(
158
+ import_core.HTTP.OK,
159
+ "ADMIN_ATTENTION",
160
+ "What needs attention",
161
+ attention(app, await doctor())
162
+ )
163
+ ],
81
164
  [
82
165
  "GET",
83
166
  "/_admin/manifest",
84
- g(
85
- async () => (0, import_core.setApiResponse)(
86
- import_core.HTTP.OK,
87
- "ADMIN_MANIFEST",
88
- "Deployment manifest",
89
- buildManifest(app, { version: this.version })
90
- )
167
+ async () => (0, import_core.setApiResponse)(
168
+ import_core.HTTP.OK,
169
+ "ADMIN_MANIFEST",
170
+ "Deployment manifest",
171
+ buildManifest(app, { version: this.version })
91
172
  )
173
+ ],
174
+ [
175
+ "GET",
176
+ "/_admin/doctor",
177
+ async () => (0, import_core.setApiResponse)(import_core.HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
92
178
  ]
93
179
  ];
94
- for (const [method, path, handler] of routes) app.addRoute(method, at(path), handler);
180
+ const mounted = /* @__PURE__ */ new Map();
181
+ const mount = (module2, method, path, handlers) => {
182
+ const key = `${method.toUpperCase()} ${path}`;
183
+ const prior = mounted.get(key);
184
+ if (prior) {
185
+ throw new Error(
186
+ `[fonderie] ${module2} cannot describe ${key}: ${prior} already describes it`
187
+ );
188
+ }
189
+ mounted.set(key, module2);
190
+ app.addRoute(method, path, guard, ...handlers);
191
+ };
192
+ for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
193
+ for (const { module: module2, description } of app.adminDescriptions()) {
194
+ for (const r of description.routes ?? [])
195
+ mount(module2, r.method, this.path + r.path, r.handlers);
196
+ }
95
197
  }
96
198
  checkReadiness() {
97
199
  return (0, import_middlewares.validateAdminToken)(this.options.adminToken, { module: this.name });
@@ -102,6 +204,10 @@ var AdminModule = class {
102
204
  ADMIN_VERSION,
103
205
  AdminModule,
104
206
  DEFAULT_ADMIN_PATH,
105
- buildManifest
207
+ DEFAULT_CHECK_TIMEOUT_MS,
208
+ attention,
209
+ buildManifest,
210
+ collectChecks,
211
+ runDoctor
106
212
  });
107
213
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/module.ts","../src/manifest.ts"],"sourcesContent":["export { AdminModule, ADMIN_VERSION, DEFAULT_ADMIN_PATH } from './module';\nexport { buildManifest } from './manifest';\nexport type { IAdminOptions, IAdminManifest, IAdminModuleEntry } from './types';\n","import type { IFonderieApp, IFonderieModule, IReadinessProblem, Middleware } from '@fonderie/core';\nimport { HTTP, setApiResponse } from '@fonderie/core';\nimport { requireAdminToken, validateAdminToken } from '@fonderie/core/middlewares';\n\nimport { buildManifest } from './manifest';\nimport type { IAdminOptions } from './types';\n\nexport const DEFAULT_ADMIN_PATH = '/_admin';\n\n// Replaced at build time (tsup env); the fallback is what tests see.\nexport const ADMIN_VERSION: string = process.env['FONDERIE_ADMIN_VERSION'] ?? '0.0.0-dev';\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = (options.path ?? DEFAULT_ADMIN_PATH).replace(/\\/+$/, '');\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst token = this.options.adminToken;\n\t\tif (!token) return;\n\n\t\tapp.reserve(this.path);\n\t\tconst guard = requireAdminToken(token);\n\t\tconst g =\n\t\t\t(h: Middleware): Middleware =>\n\t\t\t(ctx, next) =>\n\t\t\t\tguard(ctx, () => h(ctx, next));\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 routes: Array<[string, string, Middleware]> = [\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/manifest',\n\t\t\t\tg(async () =>\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 }),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t],\n\t\t];\n\t\tfor (const [method, path, handler] of routes) app.addRoute(method, at(path), handler);\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminManifest, IAdminModuleEntry } from './types';\n\nexport function buildManifest(app: IFonderieApp, admin: { version: string }): 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 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};\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,kBAAqC;AACrC,yBAAsD;;;ACE/C,SAAS,cAAc,KAAmB,OAA4C;AAC5F,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,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,IAC1E;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;;;ADrBO,IAAM,qBAAqB;AAG3B,IAAM,gBAAwB;AAE9B,IAAM,cAAN,MAA6C;AAAA,EAKnD,YAAoB,UAAyB,CAAC,GAAG;AAA7B;AACnB,SAAK,QAAQ,QAAQ,QAAQ,oBAAoB,QAAQ,QAAQ,EAAE;AAAA,EACpE;AAAA,EAFoB;AAAA,EAJX,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA,EAMT,QAAQ,KAAyB;AAChC,UAAM,QAAQ,KAAK,QAAQ;AAC3B,QAAI,CAAC,MAAO;AAEZ,QAAI,QAAQ,KAAK,IAAI;AACrB,UAAM,YAAQ,sCAAkB,KAAK;AACrC,UAAM,IACL,CAAC,MACD,CAAC,KAAK,SACL,MAAM,KAAK,MAAM,EAAE,KAAK,IAAI,CAAC;AAE/B,UAAM,KAAK,CAAC,MAAsB,KAAK,OAAO,EAAE,MAAM,mBAAmB,MAAM;AAE/E,UAAM,SAA8C;AAAA,MACnD;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,UAAE,gBACD;AAAA,YACC,iBAAK;AAAA,YACL;AAAA,YACA;AAAA,YACA,cAAc,KAAK,EAAE,SAAS,KAAK,QAAQ,CAAC;AAAA,UAC7C;AAAA,QACD;AAAA,MACD;AAAA,IACD;AACA,eAAW,CAAC,QAAQ,MAAM,OAAO,KAAK,OAAQ,KAAI,SAAS,QAAQ,GAAG,IAAI,GAAG,OAAO;AAAA,EACrF;AAAA,EAEA,iBAAsC;AACrC,eAAO,uCAAmB,KAAK,QAAQ,YAAY,EAAE,QAAQ,KAAK,KAAK,CAAC;AAAA,EACzE;AACD;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/module.ts","../src/doctor.ts","../src/manifest.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 type {\n\tIAdminOptions,\n\tIAdminManifest,\n\tIAdminModuleEntry,\n\tIAdminCheckResult,\n\tIAdminDoctorReport,\n\tIAdminAttention,\n\tIAdminAttentionItem,\n} from './types';\n","import type { IFonderieApp, IFonderieModule, IReadinessProblem, Middleware } from '@fonderie/core';\nimport { HTTP, setApiResponse } from '@fonderie/core';\nimport { requireAdminToken, validateAdminToken } from '@fonderie/core/middlewares';\n\nimport { attention, collectChecks, runDoctor } from './doctor';\nimport { buildManifest } from './manifest';\nimport type { IAdminOptions } from './types';\n\nexport const DEFAULT_ADMIN_PATH = '/_admin';\nexport const DEFAULT_CHECK_TIMEOUT_MS = 10_000;\n\n// Replaced at build time (tsup env); the fallback is what tests see.\nexport const ADMIN_VERSION: string = process.env['FONDERIE_ADMIN_VERSION'] ?? '0.0.0-dev';\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = (options.path ?? DEFAULT_ADMIN_PATH).replace(/\\/+$/, '');\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst token = this.options.adminToken;\n\t\tif (!token) return;\n\n\t\tapp.reserve(this.path);\n\t\tconst guard = requireAdminToken(token);\n\t\t// Declared at the default path so the route table reads literally; re-based when configured.\n\t\tconst at = (p: string): string => this.path + p.slice(DEFAULT_ADMIN_PATH.length);\n\n\t\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 }),\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];\n\t\tconst mounted = new Map<string, string>();\n\t\tconst mount = (module: string, method: string, path: string, handlers: Middleware[]): void => {\n\t\t\tconst key = `${method.toUpperCase()} ${path}`;\n\t\t\tconst prior = mounted.get(key);\n\t\t\tif (prior) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[fonderie] ${module} cannot describe ${key}: ${prior} already describes it`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tmounted.set(key, module);\n\t\t\tapp.addRoute(method, path, guard, ...handlers);\n\t\t};\n\n\t\tfor (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);\n\t\tfor (const { module, description } of app.adminDescriptions()) {\n\t\t\tfor (const r of description.routes ?? [])\n\t\t\t\tmount(module, r.method, this.path + r.path, r.handlers);\n\t\t}\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IAdminCheck, IAdminCheckReport, IFonderieApp } from '@fonderie/core';\n\nimport type { IAdminAttention, IAdminCheckResult, IAdminDoctorReport } from './types';\n\nexport interface INamedCheck extends IAdminCheck {\n\tmodule: string;\n}\n\n// Module checks first (sorted by module), then the app's own. A name used\n// twice would make a report ambiguous, so it is refused up front.\nexport function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[] {\n\tconst seen = new Map<string, string>();\n\tconst out: INamedCheck[] = [];\n\tconst add = (module: string, check: IAdminCheck): void => {\n\t\tconst prior = seen.get(check.name);\n\t\tif (prior)\n\t\t\tthrow new Error(\n\t\t\t\t`[fonderie] ${module} cannot offer check \"${check.name}\": ${prior} already does`,\n\t\t\t);\n\t\tseen.set(check.name, module);\n\t\tout.push({ ...check, module });\n\t};\n\tfor (const { module, description } of app.adminDescriptions()) {\n\t\tfor (const c of description.checks ?? []) add(module, c);\n\t}\n\tfor (const c of own) add('application', c);\n\treturn out;\n}\n\nasync function runOne(check: INamedCheck, timeoutMs: number): Promise<IAdminCheckResult> {\n\tconst started = Date.now();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<IAdminCheckReport>((resolve) => {\n\t\ttimer = setTimeout(\n\t\t\t() => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet report: IAdminCheckReport;\n\ttry {\n\t\treport = await Promise.race([check.run(), timeout]);\n\t} catch (err) {\n\t\treport = {\n\t\t\tok: false,\n\t\t\tfindings: [`check threw: ${err instanceof Error ? err.message : String(err)}`],\n\t\t};\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n\treturn { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };\n}\n\nexport async function runDoctor(\n\tchecks: INamedCheck[],\n\ttimeoutMs: number,\n): Promise<IAdminDoctorReport> {\n\tconst results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));\n\treturn { generatedAt: new Date().toISOString(), ok: results.every((r) => r.ok), checks: results };\n}\n\n// What needs the operator today: readiness problems as reported, failed\n// checks as errors, findings on a passing check as advice. Empty = green.\nexport function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention {\n\tconst items: IAdminAttention['items'] = [];\n\tfor (const p of app.checkProductionReadiness().problems) {\n\t\titems.push({\n\t\t\tsource: p.module,\n\t\t\tseverity: p.severity === 'error' ? 'error' : 'advice',\n\t\t\tmessage: p.message,\n\t\t});\n\t}\n\tfor (const c of doctor.checks) {\n\t\tif (c.skipped) continue;\n\t\tconst severity = c.ok ? 'advice' : 'error';\n\t\tif (!c.ok && c.findings.length === 0)\n\t\t\titems.push({ source: c.name, severity, message: 'failed' });\n\t\tfor (const f of c.findings) items.push({ source: c.name, severity, message: f });\n\t}\n\treturn { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === 'error'), items };\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminManifest, IAdminModuleEntry } from './types';\n\nexport function buildManifest(app: IFonderieApp, admin: { version: string }): 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"],"mappings":"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","names":["module","module"]}
package/dist/index.d.cts CHANGED
@@ -1,13 +1,16 @@
1
- import { IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';
1
+ import { IAdminCheck, IAdminCheckReport, IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';
2
2
 
3
3
  interface IAdminOptions {
4
4
  adminToken?: string;
5
5
  path?: string;
6
+ checks?: IAdminCheck[];
7
+ checkTimeoutMs?: number;
6
8
  }
7
9
  interface IAdminModuleEntry {
8
10
  name: string;
9
11
  version: string | null;
10
12
  readiness: IReadinessReport;
13
+ describesAdmin: boolean;
11
14
  }
12
15
  interface IAdminManifest {
13
16
  generatedAt: string;
@@ -19,8 +22,29 @@ interface IAdminManifest {
19
22
  readiness: IReadinessReport;
20
23
  routes: IRouteEntry[];
21
24
  }
25
+ interface IAdminCheckResult extends IAdminCheckReport {
26
+ name: string;
27
+ module: string;
28
+ durationMs: number;
29
+ }
30
+ interface IAdminDoctorReport {
31
+ generatedAt: string;
32
+ ok: boolean;
33
+ checks: IAdminCheckResult[];
34
+ }
35
+ interface IAdminAttentionItem {
36
+ source: string;
37
+ severity: 'error' | 'advice';
38
+ message: string;
39
+ }
40
+ interface IAdminAttention {
41
+ generatedAt: string;
42
+ ok: boolean;
43
+ items: IAdminAttentionItem[];
44
+ }
22
45
 
23
46
  declare const DEFAULT_ADMIN_PATH = "/_admin";
47
+ declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
24
48
  declare const ADMIN_VERSION: string;
25
49
  declare class AdminModule implements IFonderieModule {
26
50
  private options;
@@ -36,4 +60,11 @@ declare function buildManifest(app: IFonderieApp, admin: {
36
60
  version: string;
37
61
  }): IAdminManifest;
38
62
 
39
- export { ADMIN_VERSION, AdminModule, DEFAULT_ADMIN_PATH, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, buildManifest };
63
+ interface INamedCheck extends IAdminCheck {
64
+ module: string;
65
+ }
66
+ declare function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[];
67
+ declare function runDoctor(checks: INamedCheck[], timeoutMs: number): Promise<IAdminDoctorReport>;
68
+ declare function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention;
69
+
70
+ export { ADMIN_VERSION, AdminModule, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminDoctorReport, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, attention, buildManifest, collectChecks, runDoctor };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,16 @@
1
- import { IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';
1
+ import { IAdminCheck, IAdminCheckReport, IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';
2
2
 
3
3
  interface IAdminOptions {
4
4
  adminToken?: string;
5
5
  path?: string;
6
+ checks?: IAdminCheck[];
7
+ checkTimeoutMs?: number;
6
8
  }
7
9
  interface IAdminModuleEntry {
8
10
  name: string;
9
11
  version: string | null;
10
12
  readiness: IReadinessReport;
13
+ describesAdmin: boolean;
11
14
  }
12
15
  interface IAdminManifest {
13
16
  generatedAt: string;
@@ -19,8 +22,29 @@ interface IAdminManifest {
19
22
  readiness: IReadinessReport;
20
23
  routes: IRouteEntry[];
21
24
  }
25
+ interface IAdminCheckResult extends IAdminCheckReport {
26
+ name: string;
27
+ module: string;
28
+ durationMs: number;
29
+ }
30
+ interface IAdminDoctorReport {
31
+ generatedAt: string;
32
+ ok: boolean;
33
+ checks: IAdminCheckResult[];
34
+ }
35
+ interface IAdminAttentionItem {
36
+ source: string;
37
+ severity: 'error' | 'advice';
38
+ message: string;
39
+ }
40
+ interface IAdminAttention {
41
+ generatedAt: string;
42
+ ok: boolean;
43
+ items: IAdminAttentionItem[];
44
+ }
22
45
 
23
46
  declare const DEFAULT_ADMIN_PATH = "/_admin";
47
+ declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
24
48
  declare const ADMIN_VERSION: string;
25
49
  declare class AdminModule implements IFonderieModule {
26
50
  private options;
@@ -36,4 +60,11 @@ declare function buildManifest(app: IFonderieApp, admin: {
36
60
  version: string;
37
61
  }): IAdminManifest;
38
62
 
39
- export { ADMIN_VERSION, AdminModule, DEFAULT_ADMIN_PATH, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, buildManifest };
63
+ interface INamedCheck extends IAdminCheck {
64
+ module: string;
65
+ }
66
+ declare function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[];
67
+ declare function runDoctor(checks: INamedCheck[], timeoutMs: number): Promise<IAdminDoctorReport>;
68
+ declare function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention;
69
+
70
+ export { ADMIN_VERSION, AdminModule, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminDoctorReport, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, attention, buildManifest, collectChecks, runDoctor };
package/dist/index.js CHANGED
@@ -2,6 +2,70 @@
2
2
  import { HTTP, setApiResponse } from "@fonderie/core";
3
3
  import { requireAdminToken, validateAdminToken } from "@fonderie/core/middlewares";
4
4
 
5
+ // src/doctor.ts
6
+ function collectChecks(app, own) {
7
+ const seen = /* @__PURE__ */ new Map();
8
+ const out = [];
9
+ const add = (module, check) => {
10
+ const prior = seen.get(check.name);
11
+ if (prior)
12
+ throw new Error(
13
+ `[fonderie] ${module} cannot offer check "${check.name}": ${prior} already does`
14
+ );
15
+ seen.set(check.name, module);
16
+ out.push({ ...check, module });
17
+ };
18
+ for (const { module, description } of app.adminDescriptions()) {
19
+ for (const c of description.checks ?? []) add(module, c);
20
+ }
21
+ for (const c of own) add("application", c);
22
+ return out;
23
+ }
24
+ async function runOne(check, timeoutMs) {
25
+ const started = Date.now();
26
+ let timer;
27
+ const timeout = new Promise((resolve) => {
28
+ timer = setTimeout(
29
+ () => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),
30
+ timeoutMs
31
+ );
32
+ });
33
+ let report;
34
+ try {
35
+ report = await Promise.race([check.run(), timeout]);
36
+ } catch (err) {
37
+ report = {
38
+ ok: false,
39
+ findings: [`check threw: ${err instanceof Error ? err.message : String(err)}`]
40
+ };
41
+ } finally {
42
+ if (timer) clearTimeout(timer);
43
+ }
44
+ return { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };
45
+ }
46
+ async function runDoctor(checks, timeoutMs) {
47
+ const results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));
48
+ return { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ok: results.every((r) => r.ok), checks: results };
49
+ }
50
+ function attention(app, doctor) {
51
+ const items = [];
52
+ for (const p of app.checkProductionReadiness().problems) {
53
+ items.push({
54
+ source: p.module,
55
+ severity: p.severity === "error" ? "error" : "advice",
56
+ message: p.message
57
+ });
58
+ }
59
+ for (const c of doctor.checks) {
60
+ if (c.skipped) continue;
61
+ const severity = c.ok ? "advice" : "error";
62
+ if (!c.ok && c.findings.length === 0)
63
+ items.push({ source: c.name, severity, message: "failed" });
64
+ for (const f of c.findings) items.push({ source: c.name, severity, message: f });
65
+ }
66
+ return { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === "error"), items };
67
+ }
68
+
5
69
  // src/manifest.ts
6
70
  function buildManifest(app, admin) {
7
71
  const report = app.securityReport();
@@ -11,12 +75,14 @@ function buildManifest(app, admin) {
11
75
  list.push(p);
12
76
  byModule.set(p.module, list);
13
77
  }
78
+ const describing = new Set(app.adminDescriptions().map((d) => d.module));
14
79
  const modules = report.modules.map(({ name, version }) => {
15
80
  const problems = byModule.get(name) ?? [];
16
81
  return {
17
82
  name,
18
83
  version: version ?? null,
19
- readiness: { ok: !problems.some((p) => p.severity === "error"), problems }
84
+ readiness: { ok: !problems.some((p) => p.severity === "error"), problems },
85
+ describesAdmin: describing.has(name)
20
86
  };
21
87
  });
22
88
  return {
@@ -31,7 +97,8 @@ function buildManifest(app, admin) {
31
97
 
32
98
  // src/module.ts
33
99
  var DEFAULT_ADMIN_PATH = "/_admin";
34
- var ADMIN_VERSION = "0.1.0";
100
+ var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
101
+ var ADMIN_VERSION = "0.3.0";
35
102
  var AdminModule = class {
36
103
  constructor(options = {}) {
37
104
  this.options = options;
@@ -46,23 +113,54 @@ var AdminModule = class {
46
113
  if (!token) return;
47
114
  app.reserve(this.path);
48
115
  const guard = requireAdminToken(token);
49
- const g = (h) => (ctx, next) => guard(ctx, () => h(ctx, next));
50
116
  const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
51
- const routes = [
117
+ const checks = collectChecks(app, this.options.checks ?? []);
118
+ const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
119
+ const doctor = () => runDoctor(checks, timeoutMs);
120
+ const own = [
121
+ [
122
+ "GET",
123
+ "/_admin",
124
+ async () => setApiResponse(
125
+ HTTP.OK,
126
+ "ADMIN_ATTENTION",
127
+ "What needs attention",
128
+ attention(app, await doctor())
129
+ )
130
+ ],
52
131
  [
53
132
  "GET",
54
133
  "/_admin/manifest",
55
- g(
56
- async () => setApiResponse(
57
- HTTP.OK,
58
- "ADMIN_MANIFEST",
59
- "Deployment manifest",
60
- buildManifest(app, { version: this.version })
61
- )
134
+ async () => setApiResponse(
135
+ HTTP.OK,
136
+ "ADMIN_MANIFEST",
137
+ "Deployment manifest",
138
+ buildManifest(app, { version: this.version })
62
139
  )
140
+ ],
141
+ [
142
+ "GET",
143
+ "/_admin/doctor",
144
+ async () => setApiResponse(HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
63
145
  ]
64
146
  ];
65
- for (const [method, path, handler] of routes) app.addRoute(method, at(path), handler);
147
+ const mounted = /* @__PURE__ */ new Map();
148
+ const mount = (module, method, path, handlers) => {
149
+ const key = `${method.toUpperCase()} ${path}`;
150
+ const prior = mounted.get(key);
151
+ if (prior) {
152
+ throw new Error(
153
+ `[fonderie] ${module} cannot describe ${key}: ${prior} already describes it`
154
+ );
155
+ }
156
+ mounted.set(key, module);
157
+ app.addRoute(method, path, guard, ...handlers);
158
+ };
159
+ for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
160
+ for (const { module, description } of app.adminDescriptions()) {
161
+ for (const r of description.routes ?? [])
162
+ mount(module, r.method, this.path + r.path, r.handlers);
163
+ }
66
164
  }
67
165
  checkReadiness() {
68
166
  return validateAdminToken(this.options.adminToken, { module: this.name });
@@ -72,6 +170,10 @@ export {
72
170
  ADMIN_VERSION,
73
171
  AdminModule,
74
172
  DEFAULT_ADMIN_PATH,
75
- buildManifest
173
+ DEFAULT_CHECK_TIMEOUT_MS,
174
+ attention,
175
+ buildManifest,
176
+ collectChecks,
177
+ runDoctor
76
178
  };
77
179
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/module.ts","../src/manifest.ts"],"sourcesContent":["import type { IFonderieApp, IFonderieModule, IReadinessProblem, Middleware } from '@fonderie/core';\nimport { HTTP, setApiResponse } from '@fonderie/core';\nimport { requireAdminToken, validateAdminToken } from '@fonderie/core/middlewares';\n\nimport { buildManifest } from './manifest';\nimport type { IAdminOptions } from './types';\n\nexport const DEFAULT_ADMIN_PATH = '/_admin';\n\n// Replaced at build time (tsup env); the fallback is what tests see.\nexport const ADMIN_VERSION: string = process.env['FONDERIE_ADMIN_VERSION'] ?? '0.0.0-dev';\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = (options.path ?? DEFAULT_ADMIN_PATH).replace(/\\/+$/, '');\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst token = this.options.adminToken;\n\t\tif (!token) return;\n\n\t\tapp.reserve(this.path);\n\t\tconst guard = requireAdminToken(token);\n\t\tconst g =\n\t\t\t(h: Middleware): Middleware =>\n\t\t\t(ctx, next) =>\n\t\t\t\tguard(ctx, () => h(ctx, next));\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 routes: Array<[string, string, Middleware]> = [\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/manifest',\n\t\t\t\tg(async () =>\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 }),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t],\n\t\t];\n\t\tfor (const [method, path, handler] of routes) app.addRoute(method, at(path), handler);\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminManifest, IAdminModuleEntry } from './types';\n\nexport function buildManifest(app: IFonderieApp, admin: { version: string }): 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 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};\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"],"mappings":";AACA,SAAS,MAAM,sBAAsB;AACrC,SAAS,mBAAmB,0BAA0B;;;ACE/C,SAAS,cAAc,KAAmB,OAA4C;AAC5F,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,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,IAC1E;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;;;ADrBO,IAAM,qBAAqB;AAG3B,IAAM,gBAAwB;AAE9B,IAAM,cAAN,MAA6C;AAAA,EAKnD,YAAoB,UAAyB,CAAC,GAAG;AAA7B;AACnB,SAAK,QAAQ,QAAQ,QAAQ,oBAAoB,QAAQ,QAAQ,EAAE;AAAA,EACpE;AAAA,EAFoB;AAAA,EAJX,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA,EAMT,QAAQ,KAAyB;AAChC,UAAM,QAAQ,KAAK,QAAQ;AAC3B,QAAI,CAAC,MAAO;AAEZ,QAAI,QAAQ,KAAK,IAAI;AACrB,UAAM,QAAQ,kBAAkB,KAAK;AACrC,UAAM,IACL,CAAC,MACD,CAAC,KAAK,SACL,MAAM,KAAK,MAAM,EAAE,KAAK,IAAI,CAAC;AAE/B,UAAM,KAAK,CAAC,MAAsB,KAAK,OAAO,EAAE,MAAM,mBAAmB,MAAM;AAE/E,UAAM,SAA8C;AAAA,MACnD;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,UAAE,YACD;AAAA,YACC,KAAK;AAAA,YACL;AAAA,YACA;AAAA,YACA,cAAc,KAAK,EAAE,SAAS,KAAK,QAAQ,CAAC;AAAA,UAC7C;AAAA,QACD;AAAA,MACD;AAAA,IACD;AACA,eAAW,CAAC,QAAQ,MAAM,OAAO,KAAK,OAAQ,KAAI,SAAS,QAAQ,GAAG,IAAI,GAAG,OAAO;AAAA,EACrF;AAAA,EAEA,iBAAsC;AACrC,WAAO,mBAAmB,KAAK,QAAQ,YAAY,EAAE,QAAQ,KAAK,KAAK,CAAC;AAAA,EACzE;AACD;","names":[]}
1
+ {"version":3,"sources":["../src/module.ts","../src/doctor.ts","../src/manifest.ts"],"sourcesContent":["import type { IFonderieApp, IFonderieModule, IReadinessProblem, Middleware } from '@fonderie/core';\nimport { HTTP, setApiResponse } from '@fonderie/core';\nimport { requireAdminToken, validateAdminToken } from '@fonderie/core/middlewares';\n\nimport { attention, collectChecks, runDoctor } from './doctor';\nimport { buildManifest } from './manifest';\nimport type { IAdminOptions } from './types';\n\nexport const DEFAULT_ADMIN_PATH = '/_admin';\nexport const DEFAULT_CHECK_TIMEOUT_MS = 10_000;\n\n// Replaced at build time (tsup env); the fallback is what tests see.\nexport const ADMIN_VERSION: string = process.env['FONDERIE_ADMIN_VERSION'] ?? '0.0.0-dev';\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = (options.path ?? DEFAULT_ADMIN_PATH).replace(/\\/+$/, '');\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst token = this.options.adminToken;\n\t\tif (!token) return;\n\n\t\tapp.reserve(this.path);\n\t\tconst guard = requireAdminToken(token);\n\t\t// Declared at the default path so the route table reads literally; re-based when configured.\n\t\tconst at = (p: string): string => this.path + p.slice(DEFAULT_ADMIN_PATH.length);\n\n\t\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 }),\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];\n\t\tconst mounted = new Map<string, string>();\n\t\tconst mount = (module: string, method: string, path: string, handlers: Middleware[]): void => {\n\t\t\tconst key = `${method.toUpperCase()} ${path}`;\n\t\t\tconst prior = mounted.get(key);\n\t\t\tif (prior) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[fonderie] ${module} cannot describe ${key}: ${prior} already describes it`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tmounted.set(key, module);\n\t\t\tapp.addRoute(method, path, guard, ...handlers);\n\t\t};\n\n\t\tfor (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);\n\t\tfor (const { module, description } of app.adminDescriptions()) {\n\t\t\tfor (const r of description.routes ?? [])\n\t\t\t\tmount(module, r.method, this.path + r.path, r.handlers);\n\t\t}\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IAdminCheck, IAdminCheckReport, IFonderieApp } from '@fonderie/core';\n\nimport type { IAdminAttention, IAdminCheckResult, IAdminDoctorReport } from './types';\n\nexport interface INamedCheck extends IAdminCheck {\n\tmodule: string;\n}\n\n// Module checks first (sorted by module), then the app's own. A name used\n// twice would make a report ambiguous, so it is refused up front.\nexport function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[] {\n\tconst seen = new Map<string, string>();\n\tconst out: INamedCheck[] = [];\n\tconst add = (module: string, check: IAdminCheck): void => {\n\t\tconst prior = seen.get(check.name);\n\t\tif (prior)\n\t\t\tthrow new Error(\n\t\t\t\t`[fonderie] ${module} cannot offer check \"${check.name}\": ${prior} already does`,\n\t\t\t);\n\t\tseen.set(check.name, module);\n\t\tout.push({ ...check, module });\n\t};\n\tfor (const { module, description } of app.adminDescriptions()) {\n\t\tfor (const c of description.checks ?? []) add(module, c);\n\t}\n\tfor (const c of own) add('application', c);\n\treturn out;\n}\n\nasync function runOne(check: INamedCheck, timeoutMs: number): Promise<IAdminCheckResult> {\n\tconst started = Date.now();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<IAdminCheckReport>((resolve) => {\n\t\ttimer = setTimeout(\n\t\t\t() => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet report: IAdminCheckReport;\n\ttry {\n\t\treport = await Promise.race([check.run(), timeout]);\n\t} catch (err) {\n\t\treport = {\n\t\t\tok: false,\n\t\t\tfindings: [`check threw: ${err instanceof Error ? err.message : String(err)}`],\n\t\t};\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n\treturn { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };\n}\n\nexport async function runDoctor(\n\tchecks: INamedCheck[],\n\ttimeoutMs: number,\n): Promise<IAdminDoctorReport> {\n\tconst results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));\n\treturn { generatedAt: new Date().toISOString(), ok: results.every((r) => r.ok), checks: results };\n}\n\n// What needs the operator today: readiness problems as reported, failed\n// checks as errors, findings on a passing check as advice. Empty = green.\nexport function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention {\n\tconst items: IAdminAttention['items'] = [];\n\tfor (const p of app.checkProductionReadiness().problems) {\n\t\titems.push({\n\t\t\tsource: p.module,\n\t\t\tseverity: p.severity === 'error' ? 'error' : 'advice',\n\t\t\tmessage: p.message,\n\t\t});\n\t}\n\tfor (const c of doctor.checks) {\n\t\tif (c.skipped) continue;\n\t\tconst severity = c.ok ? 'advice' : 'error';\n\t\tif (!c.ok && c.findings.length === 0)\n\t\t\titems.push({ source: c.name, severity, message: 'failed' });\n\t\tfor (const f of c.findings) items.push({ source: c.name, severity, message: f });\n\t}\n\treturn { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === 'error'), items };\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminManifest, IAdminModuleEntry } from './types';\n\nexport function buildManifest(app: IFonderieApp, admin: { version: string }): 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"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonderie/admin",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "fonderie": { "stability": "experimental" },
5
5
  "description": "The operator's surface for a Fonderie deployment. Owns one reserved path (default /_admin) behind one admin token, and answers what the founder cannot see after deploy: which bricks are installed, at what version, whether they are ready, and every route that is exposed. The run-time counterpart to the SDK's brain — read by the human through a panel and by a model through curl.",
6
6
  "keywords": [
@@ -37,7 +37,7 @@
37
37
  "check": "biome check --write src"
38
38
  },
39
39
  "peerDependencies": {
40
- "@fonderie/core": "^0.17.0"
40
+ "@fonderie/core": "^0.19.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@fonderie/core": "../core",