@fonderie/admin 0.3.0 → 0.4.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
@@ -88,6 +88,26 @@ new AdminModule({
88
88
  });
89
89
  ```
90
90
 
91
+ ## The admin log
92
+
93
+ ```ts
94
+ import { AdminModule } from '@fonderie/admin';
95
+ import { getMigrationsPath } from '@fonderie/admin/migrations';
96
+
97
+ app.register(new AdminModule({ adminToken, store })); // run the migrations first
98
+ ```
99
+
100
+ With a store, every request served by this surface is a row in
101
+ `fonderie_admin_log` — actor (the `X-Actor` header, else `admin-token`),
102
+ method, path, route, module, status, duration, request id, client IP —
103
+ **including the ones the token guard refused.** The log middleware runs
104
+ before the guard, and a failed write never fails the request it describes.
105
+ Without a store nothing is recorded and the manifest says so
106
+ (`admin.log: false`).
107
+
108
+ `GET /_admin/activity/admin-log?limit=50&before=<cursor>` reads it, newest
109
+ first; `next` is the cursor for the following page.
110
+
91
111
  ## Composed routes
92
112
 
93
113
  Bricks that implement `describeAdmin()` have their admin routes mounted here,
package/brain/outcomes.md CHANGED
@@ -7,10 +7,31 @@ rows it seeds, routes it registers. Generated from the migration SQL and
7
7
  route tables in source — trust this file instead of reading `dist/` or
8
8
  downloading tarballs.
9
9
 
10
+ ## Database tables (after all migrations)
11
+
12
+ ### `fonderie_admin_log`
13
+
14
+ ```sql
15
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
16
+ at TIMESTAMPTZ NOT NULL DEFAULT now()
17
+ actor TEXT NOT NULL
18
+ method TEXT NOT NULL
19
+ path TEXT NOT NULL
20
+ route TEXT NOT NULL
21
+ module TEXT NOT NULL
22
+ status INTEGER NOT NULL
23
+ duration_ms INTEGER NOT NULL
24
+ request_id TEXT
25
+ client_ip TEXT
26
+ ```
27
+
28
+ Raw SQL ships in `node_modules/@fonderie/admin/dist/migrations/sql/` — read it there if you must; never download tarballs.
29
+
10
30
  ## HTTP routes registered
11
31
 
12
32
  | Method | Path | Middleware chain (auth / validation / handler) |
13
33
  |---|---|---|
14
34
  | GET | `/_admin` | `async () => setApiResponse( HTTP.OK, 'ADMIN_ATTENTION', 'What needs attention', attention(app, await doctor()), )` |
35
+ | GET | `/_admin/activity/admin-log` | `async (ctx) => { const q = new URL(ctx.request.url).searchParams; const limit = Number(q.get('limit')) || undefined; const before = q.get('before') ?? undefined; const page = await readAdminLog(store, { ...(limit ? { limit } : {}), ...(before ? { before } : {}), }); return setApiResponse(HTTP.OK, 'ADMIN_LOG', 'Admin activity', page); }` |
15
36
  | 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 }), )` |
37
+ | GET | `/_admin/manifest` | `async () => setApiResponse( HTTP.OK, 'ADMIN_MANIFEST', 'Deployment manifest', buildManifest(app, { version: this.version, log: Boolean(store) }), )` |
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## @fonderie/admin
6
6
 
7
+ Subpath exports: `@fonderie/admin/migrations`
8
+
7
9
  ```ts
8
10
  new AdminModule(options?: IAdminOptions): AdminModule
9
11
  .name: "@fonderie/admin"
@@ -18,7 +20,7 @@ const DEFAULT_ADMIN_PATH: "/_admin"
18
20
 
19
21
  const DEFAULT_CHECK_TIMEOUT_MS: 10000
20
22
 
21
- function buildManifest(app: IFonderieApp, admin: { version: string; }): IAdminManifest
23
+ function buildManifest(app: IFonderieApp, admin: { version: string; log: boolean; }): IAdminManifest
22
24
 
23
25
  function runDoctor(checks: INamedCheck[], timeoutMs: number): Promise<IAdminDoctorReport>
24
26
 
@@ -26,11 +28,20 @@ function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[]
26
28
 
27
29
  function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention
28
30
 
31
+ function adminLog(store: IStoreAdapter, route: string, module: string): Middleware
32
+
33
+ function readAdminLog(store: IStoreAdapter, opts?: { limit?: number; before?: string; }): Promise<IAdminLogPage>
34
+
35
+ const DEFAULT_ACTOR: "admin-token"
36
+
37
+ const MAX_PAGE: 200
38
+
29
39
  interface IAdminOptions {
30
40
  adminToken?: string;
31
41
  path?: string;
32
42
  checks?: IAdminCheck[];
33
43
  checkTimeoutMs?: number;
44
+ store?: IStoreAdapter;
34
45
  }
35
46
 
36
47
  interface IAdminManifest {
@@ -38,6 +49,7 @@ interface IAdminManifest {
38
49
  env: string;
39
50
  admin: {
40
51
  version: string;
52
+ log: boolean;
41
53
  };
42
54
  modules: IAdminModuleEntry[];
43
55
  readiness: IReadinessReport;
@@ -74,4 +86,23 @@ interface IAdminAttentionItem {
74
86
  severity: 'error' | 'advice';
75
87
  message: string;
76
88
  }
89
+
90
+ interface IAdminLogEntry {
91
+ id: string;
92
+ at: string;
93
+ actor: string;
94
+ method: string;
95
+ path: string;
96
+ route: string;
97
+ module: string;
98
+ status: number;
99
+ durationMs: number;
100
+ requestId: string | null;
101
+ clientIp: string | null;
102
+ }
103
+
104
+ interface IAdminLogPage {
105
+ entries: IAdminLogEntry[];
106
+ next: string | null;
107
+ }
77
108
  ```
package/dist/index.cjs CHANGED
@@ -22,11 +22,15 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ADMIN_VERSION: () => ADMIN_VERSION,
24
24
  AdminModule: () => AdminModule,
25
+ DEFAULT_ACTOR: () => DEFAULT_ACTOR,
25
26
  DEFAULT_ADMIN_PATH: () => DEFAULT_ADMIN_PATH,
26
27
  DEFAULT_CHECK_TIMEOUT_MS: () => DEFAULT_CHECK_TIMEOUT_MS,
28
+ MAX_PAGE: () => MAX_PAGE,
29
+ adminLog: () => adminLog,
27
30
  attention: () => attention,
28
31
  buildManifest: () => buildManifest,
29
32
  collectChecks: () => collectChecks,
33
+ readAdminLog: () => readAdminLog,
30
34
  runDoctor: () => runDoctor
31
35
  });
32
36
  module.exports = __toCommonJS(index_exports);
@@ -99,6 +103,77 @@ function attention(app, doctor) {
99
103
  return { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === "error"), items };
100
104
  }
101
105
 
106
+ // src/log.ts
107
+ var DEFAULT_ACTOR = "admin-token";
108
+ function adminLog(store, route, module2) {
109
+ return async (ctx, next) => {
110
+ const started = Date.now();
111
+ const res = await next();
112
+ const row = {
113
+ actor: ctx.request.headers.get("x-actor") || DEFAULT_ACTOR,
114
+ method: ctx.request.method,
115
+ path: new URL(ctx.request.url).pathname,
116
+ route,
117
+ module: module2,
118
+ status: res.status,
119
+ durationMs: Date.now() - started,
120
+ requestId: typeof ctx.meta["requestId"] === "string" ? ctx.meta["requestId"] : null,
121
+ clientIp: ctx.meta.clientIp ?? null
122
+ };
123
+ try {
124
+ await store.query(
125
+ `INSERT INTO fonderie_admin_log
126
+ (actor, method, path, route, module, status, duration_ms, request_id, client_ip)
127
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
128
+ [
129
+ row.actor,
130
+ row.method,
131
+ row.path,
132
+ row.route,
133
+ row.module,
134
+ row.status,
135
+ row.durationMs,
136
+ row.requestId,
137
+ row.clientIp
138
+ ]
139
+ );
140
+ } catch (err) {
141
+ console.error(
142
+ "[admin] could not write the admin log:",
143
+ err instanceof Error ? err.message : err
144
+ );
145
+ }
146
+ return res;
147
+ };
148
+ }
149
+ var MAX_PAGE = 200;
150
+ async function readAdminLog(store, opts = {}) {
151
+ const limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);
152
+ const cursor = opts.before ? decodeCursor(opts.before) : null;
153
+ const rows = await store.query(
154
+ `SELECT id, at, actor, method, path, route, module, status,
155
+ duration_ms AS "durationMs", request_id AS "requestId", client_ip AS "clientIp"
156
+ FROM fonderie_admin_log
157
+ WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))
158
+ ORDER BY at DESC, id DESC
159
+ LIMIT $3`,
160
+ [cursor?.at ?? null, cursor?.id ?? null, limit + 1]
161
+ );
162
+ const page = rows.slice(0, limit);
163
+ const last = page[page.length - 1];
164
+ return {
165
+ entries: page,
166
+ next: rows.length > limit && last ? encodeCursor(last) : null
167
+ };
168
+ }
169
+ function encodeCursor(e) {
170
+ return Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, "utf8").toString("base64url");
171
+ }
172
+ function decodeCursor(c) {
173
+ const [at, id] = Buffer.from(c, "base64url").toString("utf8").split("|");
174
+ return at && id ? { at, id } : null;
175
+ }
176
+
102
177
  // src/manifest.ts
103
178
  function buildManifest(app, admin) {
104
179
  const report = app.securityReport();
@@ -131,7 +206,7 @@ function buildManifest(app, admin) {
131
206
  // src/module.ts
132
207
  var DEFAULT_ADMIN_PATH = "/_admin";
133
208
  var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
134
- var ADMIN_VERSION = "0.3.0";
209
+ var ADMIN_VERSION = "0.4.0";
135
210
  var AdminModule = class {
136
211
  constructor(options = {}) {
137
212
  this.options = options;
@@ -147,6 +222,7 @@ var AdminModule = class {
147
222
  app.reserve(this.path);
148
223
  const guard = (0, import_middlewares.requireAdminToken)(token);
149
224
  const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
225
+ const store = this.options.store;
150
226
  const checks = collectChecks(app, this.options.checks ?? []);
151
227
  const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
152
228
  const doctor = () => runDoctor(checks, timeoutMs);
@@ -168,7 +244,7 @@ var AdminModule = class {
168
244
  import_core.HTTP.OK,
169
245
  "ADMIN_MANIFEST",
170
246
  "Deployment manifest",
171
- buildManifest(app, { version: this.version })
247
+ buildManifest(app, { version: this.version, log: Boolean(store) })
172
248
  )
173
249
  ],
174
250
  [
@@ -177,6 +253,22 @@ var AdminModule = class {
177
253
  async () => (0, import_core.setApiResponse)(import_core.HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
178
254
  ]
179
255
  ];
256
+ if (store) {
257
+ own.push([
258
+ "GET",
259
+ "/_admin/activity/admin-log",
260
+ async (ctx) => {
261
+ const q = new URL(ctx.request.url).searchParams;
262
+ const limit = Number(q.get("limit")) || void 0;
263
+ const before = q.get("before") ?? void 0;
264
+ const page = await readAdminLog(store, {
265
+ ...limit ? { limit } : {},
266
+ ...before ? { before } : {}
267
+ });
268
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "ADMIN_LOG", "Admin activity", page);
269
+ }
270
+ ]);
271
+ }
180
272
  const mounted = /* @__PURE__ */ new Map();
181
273
  const mount = (module2, method, path, handlers) => {
182
274
  const key = `${method.toUpperCase()} ${path}`;
@@ -187,7 +279,8 @@ var AdminModule = class {
187
279
  );
188
280
  }
189
281
  mounted.set(key, module2);
190
- app.addRoute(method, path, guard, ...handlers);
282
+ const chain = store ? [adminLog(store, path, module2), guard, ...handlers] : [guard, ...handlers];
283
+ app.addRoute(method, path, ...chain);
191
284
  };
192
285
  for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
193
286
  for (const { module: module2, description } of app.adminDescriptions()) {
@@ -203,11 +296,15 @@ var AdminModule = class {
203
296
  0 && (module.exports = {
204
297
  ADMIN_VERSION,
205
298
  AdminModule,
299
+ DEFAULT_ACTOR,
206
300
  DEFAULT_ADMIN_PATH,
207
301
  DEFAULT_CHECK_TIMEOUT_MS,
302
+ MAX_PAGE,
303
+ adminLog,
208
304
  attention,
209
305
  buildManifest,
210
306
  collectChecks,
307
+ readAdminLog,
211
308
  runDoctor
212
309
  });
213
310
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
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"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/module.ts","../src/doctor.ts","../src/log.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 { adminLog, readAdminLog, DEFAULT_ACTOR, MAX_PAGE } from './log';\nexport type {\n\tIAdminOptions,\n\tIAdminManifest,\n\tIAdminModuleEntry,\n\tIAdminCheckResult,\n\tIAdminDoctorReport,\n\tIAdminAttention,\n\tIAdminAttentionItem,\n\tIAdminLogEntry,\n\tIAdminLogPage,\n} from './types';\n","import type { IFonderieApp, IFonderieModule, IReadinessProblem, Middleware } from '@fonderie/core';\nimport { HTTP, setApiResponse } from '@fonderie/core';\nimport { requireAdminToken, validateAdminToken } from '@fonderie/core/middlewares';\n\nimport { attention, collectChecks, runDoctor } from './doctor';\nimport { adminLog, readAdminLog } from './log';\nimport { buildManifest } from './manifest';\nimport type { IAdminOptions } from './types';\n\nexport const DEFAULT_ADMIN_PATH = '/_admin';\nexport const DEFAULT_CHECK_TIMEOUT_MS = 10_000;\n\n// Replaced at build time (tsup env); the fallback is what tests see.\nexport const ADMIN_VERSION: string = process.env['FONDERIE_ADMIN_VERSION'] ?? '0.0.0-dev';\n\nexport class AdminModule implements IFonderieModule {\n\treadonly name = '@fonderie/admin';\n\treadonly version = ADMIN_VERSION;\n\treadonly path: string;\n\n\tconstructor(private options: IAdminOptions = {}) {\n\t\tthis.path = (options.path ?? DEFAULT_ADMIN_PATH).replace(/\\/+$/, '');\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst token = this.options.adminToken;\n\t\tif (!token) return;\n\n\t\tapp.reserve(this.path);\n\t\tconst guard = requireAdminToken(token);\n\t\t// Declared at the default path so the route table reads literally; re-based when configured.\n\t\tconst at = (p: string): string => this.path + p.slice(DEFAULT_ADMIN_PATH.length);\n\n\t\tconst store = this.options.store;\n\t\t// Collected once at boot: a duplicate name is refused here, not at request time.\n\t\tconst checks = collectChecks(app, this.options.checks ?? []);\n\t\tconst timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;\n\t\tconst doctor = () => runDoctor(checks, timeoutMs);\n\n\t\tconst own: Array<[string, string, Middleware]> = [\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_ATTENTION',\n\t\t\t\t\t\t'What needs attention',\n\t\t\t\t\t\tattention(app, await doctor()),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/manifest',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_MANIFEST',\n\t\t\t\t\t\t'Deployment manifest',\n\t\t\t\t\t\tbuildManifest(app, { version: this.version, log: Boolean(store) }),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/doctor',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor()),\n\t\t\t],\n\t\t];\n\t\tif (store) {\n\t\t\town.push([\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/activity/admin-log',\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst q = new URL(ctx.request.url).searchParams;\n\t\t\t\t\tconst limit = Number(q.get('limit')) || undefined;\n\t\t\t\t\tconst before = q.get('before') ?? undefined;\n\t\t\t\t\tconst page = await readAdminLog(store, {\n\t\t\t\t\t\t...(limit ? { limit } : {}),\n\t\t\t\t\t\t...(before ? { before } : {}),\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'ADMIN_LOG', 'Admin activity', page);\n\t\t\t\t},\n\t\t\t]);\n\t\t}\n\t\tconst mounted = new Map<string, string>();\n\t\tconst mount = (module: string, method: string, path: string, handlers: Middleware[]): void => {\n\t\t\tconst key = `${method.toUpperCase()} ${path}`;\n\t\t\tconst prior = mounted.get(key);\n\t\t\tif (prior) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[fonderie] ${module} cannot describe ${key}: ${prior} already describes it`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tmounted.set(key, module);\n\t\t\t// The log sits before the guard: a refused request is a row too.\n\t\t\tconst chain = store\n\t\t\t\t? [adminLog(store, path, module), guard, ...handlers]\n\t\t\t\t: [guard, ...handlers];\n\t\t\tapp.addRoute(method, path, ...chain);\n\t\t};\n\n\t\tfor (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);\n\t\tfor (const { module, description } of app.adminDescriptions()) {\n\t\t\tfor (const r of description.routes ?? [])\n\t\t\t\tmount(module, r.method, this.path + r.path, r.handlers);\n\t\t}\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IAdminCheck, IAdminCheckReport, IFonderieApp } from '@fonderie/core';\n\nimport type { IAdminAttention, IAdminCheckResult, IAdminDoctorReport } from './types';\n\nexport interface INamedCheck extends IAdminCheck {\n\tmodule: string;\n}\n\n// Module checks first (sorted by module), then the app's own. A name used\n// twice would make a report ambiguous, so it is refused up front.\nexport function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[] {\n\tconst seen = new Map<string, string>();\n\tconst out: INamedCheck[] = [];\n\tconst add = (module: string, check: IAdminCheck): void => {\n\t\tconst prior = seen.get(check.name);\n\t\tif (prior)\n\t\t\tthrow new Error(\n\t\t\t\t`[fonderie] ${module} cannot offer check \"${check.name}\": ${prior} already does`,\n\t\t\t);\n\t\tseen.set(check.name, module);\n\t\tout.push({ ...check, module });\n\t};\n\tfor (const { module, description } of app.adminDescriptions()) {\n\t\tfor (const c of description.checks ?? []) add(module, c);\n\t}\n\tfor (const c of own) add('application', c);\n\treturn out;\n}\n\nasync function runOne(check: INamedCheck, timeoutMs: number): Promise<IAdminCheckResult> {\n\tconst started = Date.now();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<IAdminCheckReport>((resolve) => {\n\t\ttimer = setTimeout(\n\t\t\t() => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet report: IAdminCheckReport;\n\ttry {\n\t\treport = await Promise.race([check.run(), timeout]);\n\t} catch (err) {\n\t\treport = {\n\t\t\tok: false,\n\t\t\tfindings: [`check threw: ${err instanceof Error ? err.message : String(err)}`],\n\t\t};\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n\treturn { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };\n}\n\nexport async function runDoctor(\n\tchecks: INamedCheck[],\n\ttimeoutMs: number,\n): Promise<IAdminDoctorReport> {\n\tconst results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));\n\treturn { generatedAt: new Date().toISOString(), ok: results.every((r) => r.ok), checks: results };\n}\n\n// What needs the operator today: readiness problems as reported, failed\n// checks as errors, findings on a passing check as advice. Empty = green.\nexport function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention {\n\tconst items: IAdminAttention['items'] = [];\n\tfor (const p of app.checkProductionReadiness().problems) {\n\t\titems.push({\n\t\t\tsource: p.module,\n\t\t\tseverity: p.severity === 'error' ? 'error' : 'advice',\n\t\t\tmessage: p.message,\n\t\t});\n\t}\n\tfor (const c of doctor.checks) {\n\t\tif (c.skipped) continue;\n\t\tconst severity = c.ok ? 'advice' : 'error';\n\t\tif (!c.ok && c.findings.length === 0)\n\t\t\titems.push({ source: c.name, severity, message: 'failed' });\n\t\tfor (const f of c.findings) items.push({ source: c.name, severity, message: f });\n\t}\n\treturn { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === 'error'), items };\n}\n","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAdminLogEntry, IAdminLogPage } from './types';\n\nexport const DEFAULT_ACTOR = 'admin-token';\n\n// Sits BEFORE the token guard, so a refused request is a row too. A failed\n// write must not fail the request it describes.\nexport function adminLog(store: IStoreAdapter, route: string, module: string): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst started = Date.now();\n\t\tconst res = await next();\n\t\tconst row = {\n\t\t\tactor: ctx.request.headers.get('x-actor') || DEFAULT_ACTOR,\n\t\t\tmethod: ctx.request.method,\n\t\t\tpath: new URL(ctx.request.url).pathname,\n\t\t\troute,\n\t\t\tmodule,\n\t\t\tstatus: res.status,\n\t\t\tdurationMs: Date.now() - started,\n\t\t\trequestId: typeof ctx.meta['requestId'] === 'string' ? ctx.meta['requestId'] : null,\n\t\t\tclientIp: ctx.meta.clientIp ?? null,\n\t\t};\n\t\ttry {\n\t\t\tawait store.query(\n\t\t\t\t`INSERT INTO fonderie_admin_log\n\t\t\t\t (actor, method, path, route, module, status, duration_ms, request_id, client_ip)\n\t\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,\n\t\t\t\t[\n\t\t\t\t\trow.actor,\n\t\t\t\t\trow.method,\n\t\t\t\t\trow.path,\n\t\t\t\t\trow.route,\n\t\t\t\t\trow.module,\n\t\t\t\t\trow.status,\n\t\t\t\t\trow.durationMs,\n\t\t\t\t\trow.requestId,\n\t\t\t\t\trow.clientIp,\n\t\t\t\t],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error(\n\t\t\t\t'[admin] could not write the admin log:',\n\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t);\n\t\t}\n\t\treturn res;\n\t};\n}\n\nexport const MAX_PAGE = 200;\n\n// Newest first, keyset on (at, id). `before` is the last entry's cursor.\nexport async function readAdminLog(\n\tstore: IStoreAdapter,\n\topts: { limit?: number; before?: string } = {},\n): Promise<IAdminLogPage> {\n\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);\n\tconst cursor = opts.before ? decodeCursor(opts.before) : null;\n\tconst rows = await store.query<IAdminLogEntry>(\n\t\t`SELECT id, at, actor, method, path, route, module, status,\n\t\t duration_ms AS \"durationMs\", request_id AS \"requestId\", client_ip AS \"clientIp\"\n\t\t FROM fonderie_admin_log\n\t\t WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))\n\t\t ORDER BY at DESC, id DESC\n\t\t LIMIT $3`,\n\t\t[cursor?.at ?? null, cursor?.id ?? null, limit + 1],\n\t);\n\tconst page = rows.slice(0, limit);\n\tconst last = page[page.length - 1];\n\treturn {\n\t\tentries: page,\n\t\tnext: rows.length > limit && last ? encodeCursor(last) : null,\n\t};\n}\n\nfunction encodeCursor(e: IAdminLogEntry): string {\n\treturn Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, 'utf8').toString('base64url');\n}\n\nfunction decodeCursor(c: string): { at: string; id: string } | null {\n\tconst [at, id] = Buffer.from(c, 'base64url').toString('utf8').split('|');\n\treturn at && id ? { at, id } : null;\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminManifest, IAdminModuleEntry } from './types';\n\nexport function buildManifest(\n\tapp: IFonderieApp,\n\tadmin: { version: string; log: boolean },\n): IAdminManifest {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems) {\n\t\tconst list = byModule.get(p.module) ?? [];\n\t\tlist.push(p);\n\t\tbyModule.set(p.module, list);\n\t}\n\tconst describing = new Set(app.adminDescriptions().map((d) => d.module));\n\tconst modules: IAdminModuleEntry[] = report.modules.map(({ name, version }) => {\n\t\tconst problems = byModule.get(name) ?? [];\n\t\treturn {\n\t\t\tname,\n\t\t\tversion: version ?? null,\n\t\t\treadiness: { ok: !problems.some((p) => p.severity === 'error'), problems },\n\t\t\tdescribesAdmin: describing.has(name),\n\t\t};\n\t});\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tenv: report.env,\n\t\tadmin,\n\t\tmodules,\n\t\treadiness: report.readiness,\n\t\troutes: app.routes(),\n\t};\n}\n"],"mappings":"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","names":["module","module","module"]}
package/dist/index.d.cts CHANGED
@@ -1,10 +1,12 @@
1
- import { IAdminCheck, IAdminCheckReport, IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';
1
+ import { IAdminCheck, IAdminCheckReport, IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem, Middleware } from '@fonderie/core';
2
+ import { IStoreAdapter } from '@fonderie/store';
2
3
 
3
4
  interface IAdminOptions {
4
5
  adminToken?: string;
5
6
  path?: string;
6
7
  checks?: IAdminCheck[];
7
8
  checkTimeoutMs?: number;
9
+ store?: IStoreAdapter;
8
10
  }
9
11
  interface IAdminModuleEntry {
10
12
  name: string;
@@ -17,6 +19,7 @@ interface IAdminManifest {
17
19
  env: string;
18
20
  admin: {
19
21
  version: string;
22
+ log: boolean;
20
23
  };
21
24
  modules: IAdminModuleEntry[];
22
25
  readiness: IReadinessReport;
@@ -42,6 +45,23 @@ interface IAdminAttention {
42
45
  ok: boolean;
43
46
  items: IAdminAttentionItem[];
44
47
  }
48
+ interface IAdminLogEntry {
49
+ id: string;
50
+ at: string;
51
+ actor: string;
52
+ method: string;
53
+ path: string;
54
+ route: string;
55
+ module: string;
56
+ status: number;
57
+ durationMs: number;
58
+ requestId: string | null;
59
+ clientIp: string | null;
60
+ }
61
+ interface IAdminLogPage {
62
+ entries: IAdminLogEntry[];
63
+ next: string | null;
64
+ }
45
65
 
46
66
  declare const DEFAULT_ADMIN_PATH = "/_admin";
47
67
  declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
@@ -58,6 +78,7 @@ declare class AdminModule implements IFonderieModule {
58
78
 
59
79
  declare function buildManifest(app: IFonderieApp, admin: {
60
80
  version: string;
81
+ log: boolean;
61
82
  }): IAdminManifest;
62
83
 
63
84
  interface INamedCheck extends IAdminCheck {
@@ -67,4 +88,12 @@ declare function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedChe
67
88
  declare function runDoctor(checks: INamedCheck[], timeoutMs: number): Promise<IAdminDoctorReport>;
68
89
  declare function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention;
69
90
 
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 };
91
+ declare const DEFAULT_ACTOR = "admin-token";
92
+ declare function adminLog(store: IStoreAdapter, route: string, module: string): Middleware;
93
+ declare const MAX_PAGE = 200;
94
+ declare function readAdminLog(store: IStoreAdapter, opts?: {
95
+ limit?: number;
96
+ before?: string;
97
+ }): Promise<IAdminLogPage>;
98
+
99
+ export { ADMIN_VERSION, AdminModule, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminDoctorReport, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, MAX_PAGE, adminLog, attention, buildManifest, collectChecks, readAdminLog, runDoctor };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { IAdminCheck, IAdminCheckReport, IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';
1
+ import { IAdminCheck, IAdminCheckReport, IReadinessReport, IRouteEntry, IFonderieModule, IFonderieApp, IReadinessProblem, Middleware } from '@fonderie/core';
2
+ import { IStoreAdapter } from '@fonderie/store';
2
3
 
3
4
  interface IAdminOptions {
4
5
  adminToken?: string;
5
6
  path?: string;
6
7
  checks?: IAdminCheck[];
7
8
  checkTimeoutMs?: number;
9
+ store?: IStoreAdapter;
8
10
  }
9
11
  interface IAdminModuleEntry {
10
12
  name: string;
@@ -17,6 +19,7 @@ interface IAdminManifest {
17
19
  env: string;
18
20
  admin: {
19
21
  version: string;
22
+ log: boolean;
20
23
  };
21
24
  modules: IAdminModuleEntry[];
22
25
  readiness: IReadinessReport;
@@ -42,6 +45,23 @@ interface IAdminAttention {
42
45
  ok: boolean;
43
46
  items: IAdminAttentionItem[];
44
47
  }
48
+ interface IAdminLogEntry {
49
+ id: string;
50
+ at: string;
51
+ actor: string;
52
+ method: string;
53
+ path: string;
54
+ route: string;
55
+ module: string;
56
+ status: number;
57
+ durationMs: number;
58
+ requestId: string | null;
59
+ clientIp: string | null;
60
+ }
61
+ interface IAdminLogPage {
62
+ entries: IAdminLogEntry[];
63
+ next: string | null;
64
+ }
45
65
 
46
66
  declare const DEFAULT_ADMIN_PATH = "/_admin";
47
67
  declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
@@ -58,6 +78,7 @@ declare class AdminModule implements IFonderieModule {
58
78
 
59
79
  declare function buildManifest(app: IFonderieApp, admin: {
60
80
  version: string;
81
+ log: boolean;
61
82
  }): IAdminManifest;
62
83
 
63
84
  interface INamedCheck extends IAdminCheck {
@@ -67,4 +88,12 @@ declare function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedChe
67
88
  declare function runDoctor(checks: INamedCheck[], timeoutMs: number): Promise<IAdminDoctorReport>;
68
89
  declare function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention;
69
90
 
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 };
91
+ declare const DEFAULT_ACTOR = "admin-token";
92
+ declare function adminLog(store: IStoreAdapter, route: string, module: string): Middleware;
93
+ declare const MAX_PAGE = 200;
94
+ declare function readAdminLog(store: IStoreAdapter, opts?: {
95
+ limit?: number;
96
+ before?: string;
97
+ }): Promise<IAdminLogPage>;
98
+
99
+ export { ADMIN_VERSION, AdminModule, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminDoctorReport, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, MAX_PAGE, adminLog, attention, buildManifest, collectChecks, readAdminLog, runDoctor };
package/dist/index.js CHANGED
@@ -66,6 +66,77 @@ function attention(app, doctor) {
66
66
  return { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === "error"), items };
67
67
  }
68
68
 
69
+ // src/log.ts
70
+ var DEFAULT_ACTOR = "admin-token";
71
+ function adminLog(store, route, module) {
72
+ return async (ctx, next) => {
73
+ const started = Date.now();
74
+ const res = await next();
75
+ const row = {
76
+ actor: ctx.request.headers.get("x-actor") || DEFAULT_ACTOR,
77
+ method: ctx.request.method,
78
+ path: new URL(ctx.request.url).pathname,
79
+ route,
80
+ module,
81
+ status: res.status,
82
+ durationMs: Date.now() - started,
83
+ requestId: typeof ctx.meta["requestId"] === "string" ? ctx.meta["requestId"] : null,
84
+ clientIp: ctx.meta.clientIp ?? null
85
+ };
86
+ try {
87
+ await store.query(
88
+ `INSERT INTO fonderie_admin_log
89
+ (actor, method, path, route, module, status, duration_ms, request_id, client_ip)
90
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
91
+ [
92
+ row.actor,
93
+ row.method,
94
+ row.path,
95
+ row.route,
96
+ row.module,
97
+ row.status,
98
+ row.durationMs,
99
+ row.requestId,
100
+ row.clientIp
101
+ ]
102
+ );
103
+ } catch (err) {
104
+ console.error(
105
+ "[admin] could not write the admin log:",
106
+ err instanceof Error ? err.message : err
107
+ );
108
+ }
109
+ return res;
110
+ };
111
+ }
112
+ var MAX_PAGE = 200;
113
+ async function readAdminLog(store, opts = {}) {
114
+ const limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);
115
+ const cursor = opts.before ? decodeCursor(opts.before) : null;
116
+ const rows = await store.query(
117
+ `SELECT id, at, actor, method, path, route, module, status,
118
+ duration_ms AS "durationMs", request_id AS "requestId", client_ip AS "clientIp"
119
+ FROM fonderie_admin_log
120
+ WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))
121
+ ORDER BY at DESC, id DESC
122
+ LIMIT $3`,
123
+ [cursor?.at ?? null, cursor?.id ?? null, limit + 1]
124
+ );
125
+ const page = rows.slice(0, limit);
126
+ const last = page[page.length - 1];
127
+ return {
128
+ entries: page,
129
+ next: rows.length > limit && last ? encodeCursor(last) : null
130
+ };
131
+ }
132
+ function encodeCursor(e) {
133
+ return Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, "utf8").toString("base64url");
134
+ }
135
+ function decodeCursor(c) {
136
+ const [at, id] = Buffer.from(c, "base64url").toString("utf8").split("|");
137
+ return at && id ? { at, id } : null;
138
+ }
139
+
69
140
  // src/manifest.ts
70
141
  function buildManifest(app, admin) {
71
142
  const report = app.securityReport();
@@ -98,7 +169,7 @@ function buildManifest(app, admin) {
98
169
  // src/module.ts
99
170
  var DEFAULT_ADMIN_PATH = "/_admin";
100
171
  var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
101
- var ADMIN_VERSION = "0.3.0";
172
+ var ADMIN_VERSION = "0.4.0";
102
173
  var AdminModule = class {
103
174
  constructor(options = {}) {
104
175
  this.options = options;
@@ -114,6 +185,7 @@ var AdminModule = class {
114
185
  app.reserve(this.path);
115
186
  const guard = requireAdminToken(token);
116
187
  const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
188
+ const store = this.options.store;
117
189
  const checks = collectChecks(app, this.options.checks ?? []);
118
190
  const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
119
191
  const doctor = () => runDoctor(checks, timeoutMs);
@@ -135,7 +207,7 @@ var AdminModule = class {
135
207
  HTTP.OK,
136
208
  "ADMIN_MANIFEST",
137
209
  "Deployment manifest",
138
- buildManifest(app, { version: this.version })
210
+ buildManifest(app, { version: this.version, log: Boolean(store) })
139
211
  )
140
212
  ],
141
213
  [
@@ -144,6 +216,22 @@ var AdminModule = class {
144
216
  async () => setApiResponse(HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
145
217
  ]
146
218
  ];
219
+ if (store) {
220
+ own.push([
221
+ "GET",
222
+ "/_admin/activity/admin-log",
223
+ async (ctx) => {
224
+ const q = new URL(ctx.request.url).searchParams;
225
+ const limit = Number(q.get("limit")) || void 0;
226
+ const before = q.get("before") ?? void 0;
227
+ const page = await readAdminLog(store, {
228
+ ...limit ? { limit } : {},
229
+ ...before ? { before } : {}
230
+ });
231
+ return setApiResponse(HTTP.OK, "ADMIN_LOG", "Admin activity", page);
232
+ }
233
+ ]);
234
+ }
147
235
  const mounted = /* @__PURE__ */ new Map();
148
236
  const mount = (module, method, path, handlers) => {
149
237
  const key = `${method.toUpperCase()} ${path}`;
@@ -154,7 +242,8 @@ var AdminModule = class {
154
242
  );
155
243
  }
156
244
  mounted.set(key, module);
157
- app.addRoute(method, path, guard, ...handlers);
245
+ const chain = store ? [adminLog(store, path, module), guard, ...handlers] : [guard, ...handlers];
246
+ app.addRoute(method, path, ...chain);
158
247
  };
159
248
  for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
160
249
  for (const { module, description } of app.adminDescriptions()) {
@@ -169,11 +258,15 @@ var AdminModule = class {
169
258
  export {
170
259
  ADMIN_VERSION,
171
260
  AdminModule,
261
+ DEFAULT_ACTOR,
172
262
  DEFAULT_ADMIN_PATH,
173
263
  DEFAULT_CHECK_TIMEOUT_MS,
264
+ MAX_PAGE,
265
+ adminLog,
174
266
  attention,
175
267
  buildManifest,
176
268
  collectChecks,
269
+ readAdminLog,
177
270
  runDoctor
178
271
  };
179
272
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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":[]}
1
+ {"version":3,"sources":["../src/module.ts","../src/doctor.ts","../src/log.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 { adminLog, readAdminLog } from './log';\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\tconst store = this.options.store;\n\t\t// Collected once at boot: a duplicate name is refused here, not at request time.\n\t\tconst checks = collectChecks(app, this.options.checks ?? []);\n\t\tconst timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;\n\t\tconst doctor = () => runDoctor(checks, timeoutMs);\n\n\t\tconst own: Array<[string, string, Middleware]> = [\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_ATTENTION',\n\t\t\t\t\t\t'What needs attention',\n\t\t\t\t\t\tattention(app, await doctor()),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/manifest',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_MANIFEST',\n\t\t\t\t\t\t'Deployment manifest',\n\t\t\t\t\t\tbuildManifest(app, { version: this.version, log: Boolean(store) }),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/doctor',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_DOCTOR', 'Reconciliation checks', await doctor()),\n\t\t\t],\n\t\t];\n\t\tif (store) {\n\t\t\town.push([\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/activity/admin-log',\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst q = new URL(ctx.request.url).searchParams;\n\t\t\t\t\tconst limit = Number(q.get('limit')) || undefined;\n\t\t\t\t\tconst before = q.get('before') ?? undefined;\n\t\t\t\t\tconst page = await readAdminLog(store, {\n\t\t\t\t\t\t...(limit ? { limit } : {}),\n\t\t\t\t\t\t...(before ? { before } : {}),\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'ADMIN_LOG', 'Admin activity', page);\n\t\t\t\t},\n\t\t\t]);\n\t\t}\n\t\tconst mounted = new Map<string, string>();\n\t\tconst mount = (module: string, method: string, path: string, handlers: Middleware[]): void => {\n\t\t\tconst key = `${method.toUpperCase()} ${path}`;\n\t\t\tconst prior = mounted.get(key);\n\t\t\tif (prior) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[fonderie] ${module} cannot describe ${key}: ${prior} already describes it`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tmounted.set(key, module);\n\t\t\t// The log sits before the guard: a refused request is a row too.\n\t\t\tconst chain = store\n\t\t\t\t? [adminLog(store, path, module), guard, ...handlers]\n\t\t\t\t: [guard, ...handlers];\n\t\t\tapp.addRoute(method, path, ...chain);\n\t\t};\n\n\t\tfor (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);\n\t\tfor (const { module, description } of app.adminDescriptions()) {\n\t\t\tfor (const r of description.routes ?? [])\n\t\t\t\tmount(module, r.method, this.path + r.path, r.handlers);\n\t\t}\n\t}\n\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn validateAdminToken(this.options.adminToken, { module: this.name });\n\t}\n}\n","import type { IAdminCheck, IAdminCheckReport, IFonderieApp } from '@fonderie/core';\n\nimport type { IAdminAttention, IAdminCheckResult, IAdminDoctorReport } from './types';\n\nexport interface INamedCheck extends IAdminCheck {\n\tmodule: string;\n}\n\n// Module checks first (sorted by module), then the app's own. A name used\n// twice would make a report ambiguous, so it is refused up front.\nexport function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedCheck[] {\n\tconst seen = new Map<string, string>();\n\tconst out: INamedCheck[] = [];\n\tconst add = (module: string, check: IAdminCheck): void => {\n\t\tconst prior = seen.get(check.name);\n\t\tif (prior)\n\t\t\tthrow new Error(\n\t\t\t\t`[fonderie] ${module} cannot offer check \"${check.name}\": ${prior} already does`,\n\t\t\t);\n\t\tseen.set(check.name, module);\n\t\tout.push({ ...check, module });\n\t};\n\tfor (const { module, description } of app.adminDescriptions()) {\n\t\tfor (const c of description.checks ?? []) add(module, c);\n\t}\n\tfor (const c of own) add('application', c);\n\treturn out;\n}\n\nasync function runOne(check: INamedCheck, timeoutMs: number): Promise<IAdminCheckResult> {\n\tconst started = Date.now();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<IAdminCheckReport>((resolve) => {\n\t\ttimer = setTimeout(\n\t\t\t() => resolve({ ok: false, findings: [`timed out after ${timeoutMs} ms`] }),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet report: IAdminCheckReport;\n\ttry {\n\t\treport = await Promise.race([check.run(), timeout]);\n\t} catch (err) {\n\t\treport = {\n\t\t\tok: false,\n\t\t\tfindings: [`check threw: ${err instanceof Error ? err.message : String(err)}`],\n\t\t};\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n\treturn { name: check.name, module: check.module, ...report, durationMs: Date.now() - started };\n}\n\nexport async function runDoctor(\n\tchecks: INamedCheck[],\n\ttimeoutMs: number,\n): Promise<IAdminDoctorReport> {\n\tconst results = await Promise.all(checks.map((c) => runOne(c, timeoutMs)));\n\treturn { generatedAt: new Date().toISOString(), ok: results.every((r) => r.ok), checks: results };\n}\n\n// What needs the operator today: readiness problems as reported, failed\n// checks as errors, findings on a passing check as advice. Empty = green.\nexport function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention {\n\tconst items: IAdminAttention['items'] = [];\n\tfor (const p of app.checkProductionReadiness().problems) {\n\t\titems.push({\n\t\t\tsource: p.module,\n\t\t\tseverity: p.severity === 'error' ? 'error' : 'advice',\n\t\t\tmessage: p.message,\n\t\t});\n\t}\n\tfor (const c of doctor.checks) {\n\t\tif (c.skipped) continue;\n\t\tconst severity = c.ok ? 'advice' : 'error';\n\t\tif (!c.ok && c.findings.length === 0)\n\t\t\titems.push({ source: c.name, severity, message: 'failed' });\n\t\tfor (const f of c.findings) items.push({ source: c.name, severity, message: f });\n\t}\n\treturn { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === 'error'), items };\n}\n","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAdminLogEntry, IAdminLogPage } from './types';\n\nexport const DEFAULT_ACTOR = 'admin-token';\n\n// Sits BEFORE the token guard, so a refused request is a row too. A failed\n// write must not fail the request it describes.\nexport function adminLog(store: IStoreAdapter, route: string, module: string): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst started = Date.now();\n\t\tconst res = await next();\n\t\tconst row = {\n\t\t\tactor: ctx.request.headers.get('x-actor') || DEFAULT_ACTOR,\n\t\t\tmethod: ctx.request.method,\n\t\t\tpath: new URL(ctx.request.url).pathname,\n\t\t\troute,\n\t\t\tmodule,\n\t\t\tstatus: res.status,\n\t\t\tdurationMs: Date.now() - started,\n\t\t\trequestId: typeof ctx.meta['requestId'] === 'string' ? ctx.meta['requestId'] : null,\n\t\t\tclientIp: ctx.meta.clientIp ?? null,\n\t\t};\n\t\ttry {\n\t\t\tawait store.query(\n\t\t\t\t`INSERT INTO fonderie_admin_log\n\t\t\t\t (actor, method, path, route, module, status, duration_ms, request_id, client_ip)\n\t\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,\n\t\t\t\t[\n\t\t\t\t\trow.actor,\n\t\t\t\t\trow.method,\n\t\t\t\t\trow.path,\n\t\t\t\t\trow.route,\n\t\t\t\t\trow.module,\n\t\t\t\t\trow.status,\n\t\t\t\t\trow.durationMs,\n\t\t\t\t\trow.requestId,\n\t\t\t\t\trow.clientIp,\n\t\t\t\t],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error(\n\t\t\t\t'[admin] could not write the admin log:',\n\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t);\n\t\t}\n\t\treturn res;\n\t};\n}\n\nexport const MAX_PAGE = 200;\n\n// Newest first, keyset on (at, id). `before` is the last entry's cursor.\nexport async function readAdminLog(\n\tstore: IStoreAdapter,\n\topts: { limit?: number; before?: string } = {},\n): Promise<IAdminLogPage> {\n\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);\n\tconst cursor = opts.before ? decodeCursor(opts.before) : null;\n\tconst rows = await store.query<IAdminLogEntry>(\n\t\t`SELECT id, at, actor, method, path, route, module, status,\n\t\t duration_ms AS \"durationMs\", request_id AS \"requestId\", client_ip AS \"clientIp\"\n\t\t FROM fonderie_admin_log\n\t\t WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))\n\t\t ORDER BY at DESC, id DESC\n\t\t LIMIT $3`,\n\t\t[cursor?.at ?? null, cursor?.id ?? null, limit + 1],\n\t);\n\tconst page = rows.slice(0, limit);\n\tconst last = page[page.length - 1];\n\treturn {\n\t\tentries: page,\n\t\tnext: rows.length > limit && last ? encodeCursor(last) : null,\n\t};\n}\n\nfunction encodeCursor(e: IAdminLogEntry): string {\n\treturn Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, 'utf8').toString('base64url');\n}\n\nfunction decodeCursor(c: string): { at: string; id: string } | null {\n\tconst [at, id] = Buffer.from(c, 'base64url').toString('utf8').split('|');\n\treturn at && id ? { at, id } : null;\n}\n","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminManifest, IAdminModuleEntry } from './types';\n\nexport function buildManifest(\n\tapp: IFonderieApp,\n\tadmin: { version: string; log: boolean },\n): IAdminManifest {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems) {\n\t\tconst list = byModule.get(p.module) ?? [];\n\t\tlist.push(p);\n\t\tbyModule.set(p.module, list);\n\t}\n\tconst describing = new Set(app.adminDescriptions().map((d) => d.module));\n\tconst modules: IAdminModuleEntry[] = report.modules.map(({ name, version }) => {\n\t\tconst problems = byModule.get(name) ?? [];\n\t\treturn {\n\t\t\tname,\n\t\t\tversion: version ?? null,\n\t\t\treadiness: { ok: !problems.some((p) => p.severity === 'error'), problems },\n\t\t\tdescribesAdmin: describing.has(name),\n\t\t};\n\t});\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tenv: report.env,\n\t\tadmin,\n\t\tmodules,\n\t\treadiness: report.readiness,\n\t\troutes: app.routes(),\n\t};\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,3 @@
1
+ declare const getMigrationsPath: () => string;
2
+
3
+ export { getMigrationsPath };
@@ -0,0 +1,7 @@
1
+ // src/migrations/index.ts
2
+ import { createMigrationsPath } from "@fonderie/store";
3
+ var getMigrationsPath = () => createMigrationsPath(import.meta.url);
4
+ export {
5
+ getMigrationsPath
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/migrations/index.ts"],"sourcesContent":["import { createMigrationsPath } from '@fonderie/store';\n\nexport const getMigrationsPath = (): string => createMigrationsPath(import.meta.url);\n"],"mappings":";AAAA,SAAS,4BAA4B;AAE9B,IAAM,oBAAoB,MAAc,qBAAqB,YAAY,GAAG;","names":[]}
@@ -0,0 +1,18 @@
1
+ -- fonderie_admin_log: every request served by the admin surface, including
2
+ -- the ones the token guard refused. Append-only; the operator reads it.
3
+ CREATE TABLE IF NOT EXISTS fonderie_admin_log (
4
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
5
+ at TIMESTAMPTZ NOT NULL DEFAULT now(),
6
+ actor TEXT NOT NULL, -- X-Actor header, or 'admin-token'
7
+ method TEXT NOT NULL,
8
+ path TEXT NOT NULL, -- request pathname, no query string
9
+ route TEXT NOT NULL, -- the pattern, relative to basePath: /_admin/secrets/:key/reveal
10
+ module TEXT NOT NULL, -- who described the route
11
+ status INTEGER NOT NULL,
12
+ duration_ms INTEGER NOT NULL,
13
+ request_id TEXT,
14
+ client_ip TEXT
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_fonderie_admin_log_at
18
+ ON fonderie_admin_log (at DESC, id DESC);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonderie/admin",
3
- "version": "0.3.0",
3
+ "version": "0.4.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": [
@@ -22,13 +22,17 @@
22
22
  "types": "./dist/index.d.ts",
23
23
  "import": "./dist/index.js",
24
24
  "require": "./dist/index.cjs"
25
+ },
26
+ "./migrations": {
27
+ "types": "./dist/migrations/index.d.ts",
28
+ "import": "./dist/migrations/index.js"
25
29
  }
26
30
  },
27
31
  "main": "./dist/index.cjs",
28
32
  "module": "./dist/index.js",
29
33
  "types": "./dist/index.d.ts",
30
34
  "scripts": {
31
- "build": "tsup",
35
+ "build": "tsup && tsup --config tsup.migrations.ts",
32
36
  "dev": "tsup --watch",
33
37
  "typecheck": "tsc --noEmit",
34
38
  "test": "tsx --test src/__tests__/*.test.ts",
@@ -37,10 +41,17 @@
37
41
  "check": "biome check --write src"
38
42
  },
39
43
  "peerDependencies": {
40
- "@fonderie/core": "^0.19.0"
44
+ "@fonderie/core": "^0.19.0",
45
+ "@fonderie/store": "^0.5.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "@fonderie/store": {
49
+ "optional": true
50
+ }
41
51
  },
42
52
  "devDependencies": {
43
53
  "@fonderie/core": "../core",
54
+ "@fonderie/store": "../store",
44
55
  "@types/node": "^26.5.1",
45
56
  "tsup": "^8.5.1",
46
57
  "tsx": "^4.23.13",