@fonderie/admin 0.2.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 +42 -0
- package/brain/outcomes.md +2 -0
- package/brain/signatures.md +34 -0
- package/dist/index.cjs +94 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +89 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -46,6 +46,48 @@ curl -H "Authorization: Bearer $ADMIN_TOKEN" https://api.example.com/_admin/mani
|
|
|
46
46
|
problems are shown here in production — unlike `/readyz`, which is public —
|
|
47
47
|
because the operator is the audience.
|
|
48
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
|
+
|
|
49
91
|
## Composed routes
|
|
50
92
|
|
|
51
93
|
Bricks that implement `describeAdmin()` have their admin routes mounted here,
|
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` | `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())` |
|
|
14
16
|
| GET | `/_admin/manifest` | `async () => setApiResponse( HTTP.OK, 'ADMIN_MANIFEST', 'Deployment manifest', buildManifest(app, { version: this.version }), )` |
|
package/brain/signatures.md
CHANGED
|
@@ -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 {
|
|
@@ -40,4 +50,28 @@ interface IAdminModuleEntry {
|
|
|
40
50
|
readiness: IReadinessReport;
|
|
41
51
|
describesAdmin: boolean;
|
|
42
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;
|
|
76
|
+
}
|
|
43
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
|
-
|
|
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();
|
|
@@ -62,7 +130,8 @@ function buildManifest(app, admin) {
|
|
|
62
130
|
|
|
63
131
|
// src/module.ts
|
|
64
132
|
var DEFAULT_ADMIN_PATH = "/_admin";
|
|
65
|
-
var
|
|
133
|
+
var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
134
|
+
var ADMIN_VERSION = "0.3.0";
|
|
66
135
|
var AdminModule = class {
|
|
67
136
|
constructor(options = {}) {
|
|
68
137
|
this.options = options;
|
|
@@ -78,7 +147,20 @@ var AdminModule = class {
|
|
|
78
147
|
app.reserve(this.path);
|
|
79
148
|
const guard = (0, import_middlewares.requireAdminToken)(token);
|
|
80
149
|
const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
|
|
150
|
+
const checks = collectChecks(app, this.options.checks ?? []);
|
|
151
|
+
const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
152
|
+
const doctor = () => runDoctor(checks, timeoutMs);
|
|
81
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
|
+
],
|
|
82
164
|
[
|
|
83
165
|
"GET",
|
|
84
166
|
"/_admin/manifest",
|
|
@@ -88,6 +170,11 @@ var AdminModule = class {
|
|
|
88
170
|
"Deployment manifest",
|
|
89
171
|
buildManifest(app, { version: this.version })
|
|
90
172
|
)
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
"GET",
|
|
176
|
+
"/_admin/doctor",
|
|
177
|
+
async () => (0, import_core.setApiResponse)(import_core.HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
|
|
91
178
|
]
|
|
92
179
|
];
|
|
93
180
|
const mounted = /* @__PURE__ */ new Map();
|
|
@@ -117,6 +204,10 @@ var AdminModule = class {
|
|
|
117
204
|
ADMIN_VERSION,
|
|
118
205
|
AdminModule,
|
|
119
206
|
DEFAULT_ADMIN_PATH,
|
|
120
|
-
|
|
207
|
+
DEFAULT_CHECK_TIMEOUT_MS,
|
|
208
|
+
attention,
|
|
209
|
+
buildManifest,
|
|
210
|
+
collectChecks,
|
|
211
|
+
runDoctor
|
|
121
212
|
});
|
|
122
213
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -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\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 own: 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\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];\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 { 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"]}
|
|
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,8 +1,10 @@
|
|
|
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;
|
|
@@ -20,8 +22,29 @@ interface IAdminManifest {
|
|
|
20
22
|
readiness: IReadinessReport;
|
|
21
23
|
routes: IRouteEntry[];
|
|
22
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
|
+
}
|
|
23
45
|
|
|
24
46
|
declare const DEFAULT_ADMIN_PATH = "/_admin";
|
|
47
|
+
declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
|
|
25
48
|
declare const ADMIN_VERSION: string;
|
|
26
49
|
declare class AdminModule implements IFonderieModule {
|
|
27
50
|
private options;
|
|
@@ -37,4 +60,11 @@ declare function buildManifest(app: IFonderieApp, admin: {
|
|
|
37
60
|
version: string;
|
|
38
61
|
}): IAdminManifest;
|
|
39
62
|
|
|
40
|
-
|
|
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,8 +1,10 @@
|
|
|
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;
|
|
@@ -20,8 +22,29 @@ interface IAdminManifest {
|
|
|
20
22
|
readiness: IReadinessReport;
|
|
21
23
|
routes: IRouteEntry[];
|
|
22
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
|
+
}
|
|
23
45
|
|
|
24
46
|
declare const DEFAULT_ADMIN_PATH = "/_admin";
|
|
47
|
+
declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
|
|
25
48
|
declare const ADMIN_VERSION: string;
|
|
26
49
|
declare class AdminModule implements IFonderieModule {
|
|
27
50
|
private options;
|
|
@@ -37,4 +60,11 @@ declare function buildManifest(app: IFonderieApp, admin: {
|
|
|
37
60
|
version: string;
|
|
38
61
|
}): IAdminManifest;
|
|
39
62
|
|
|
40
|
-
|
|
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();
|
|
@@ -33,7 +97,8 @@ function buildManifest(app, admin) {
|
|
|
33
97
|
|
|
34
98
|
// src/module.ts
|
|
35
99
|
var DEFAULT_ADMIN_PATH = "/_admin";
|
|
36
|
-
var
|
|
100
|
+
var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
101
|
+
var ADMIN_VERSION = "0.3.0";
|
|
37
102
|
var AdminModule = class {
|
|
38
103
|
constructor(options = {}) {
|
|
39
104
|
this.options = options;
|
|
@@ -49,7 +114,20 @@ var AdminModule = class {
|
|
|
49
114
|
app.reserve(this.path);
|
|
50
115
|
const guard = requireAdminToken(token);
|
|
51
116
|
const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
|
|
117
|
+
const checks = collectChecks(app, this.options.checks ?? []);
|
|
118
|
+
const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
119
|
+
const doctor = () => runDoctor(checks, timeoutMs);
|
|
52
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
|
+
],
|
|
53
131
|
[
|
|
54
132
|
"GET",
|
|
55
133
|
"/_admin/manifest",
|
|
@@ -59,6 +137,11 @@ var AdminModule = class {
|
|
|
59
137
|
"Deployment manifest",
|
|
60
138
|
buildManifest(app, { version: this.version })
|
|
61
139
|
)
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"GET",
|
|
143
|
+
"/_admin/doctor",
|
|
144
|
+
async () => setApiResponse(HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
|
|
62
145
|
]
|
|
63
146
|
];
|
|
64
147
|
const mounted = /* @__PURE__ */ new Map();
|
|
@@ -87,6 +170,10 @@ export {
|
|
|
87
170
|
ADMIN_VERSION,
|
|
88
171
|
AdminModule,
|
|
89
172
|
DEFAULT_ADMIN_PATH,
|
|
90
|
-
|
|
173
|
+
DEFAULT_CHECK_TIMEOUT_MS,
|
|
174
|
+
attention,
|
|
175
|
+
buildManifest,
|
|
176
|
+
collectChecks,
|
|
177
|
+
runDoctor
|
|
91
178
|
};
|
|
92
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\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 own: 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\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];\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 { 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":";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,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;;;ADvBO,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;AAErC,UAAM,KAAK,CAAC,MAAsB,KAAK,OAAO,EAAE,MAAM,mBAAmB,MAAM;AAE/E,UAAM,MAA2C;AAAA,MAChD;AAAA,QACC;AAAA,QACA;AAAA,QACA,YACC;AAAA,UACC,KAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,cAAc,KAAK,EAAE,SAAS,KAAK,QAAQ,CAAC;AAAA,QAC7C;AAAA,MACF;AAAA,IACD;AACA,UAAM,UAAU,oBAAI,IAAoB;AACxC,UAAM,QAAQ,CAAC,QAAgB,QAAgB,MAAc,aAAiC;AAC7F,YAAM,MAAM,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI;AAC3C,YAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,UAAI,OAAO;AACV,cAAM,IAAI;AAAA,UACT,cAAc,MAAM,oBAAoB,GAAG,KAAK,KAAK;AAAA,QACtD;AAAA,MACD;AACA,cAAQ,IAAI,KAAK,MAAM;AACvB,UAAI,SAAS,QAAQ,MAAM,OAAO,GAAG,QAAQ;AAAA,IAC9C;AAEA,eAAW,CAAC,QAAQ,MAAM,OAAO,KAAK,IAAK,OAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC;AACvF,eAAW,EAAE,QAAQ,YAAY,KAAK,IAAI,kBAAkB,GAAG;AAC9D,iBAAW,KAAK,YAAY,UAAU,CAAC;AACtC,cAAM,QAAQ,EAAE,QAAQ,KAAK,OAAO,EAAE,MAAM,EAAE,QAAQ;AAAA,IACxD;AAAA,EACD;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.
|
|
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.
|
|
40
|
+
"@fonderie/core": "^0.19.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@fonderie/core": "../core",
|