@fonderie/admin 0.3.0 → 0.5.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 +43 -0
- package/brain/outcomes.md +25 -1
- package/brain/signatures.md +79 -1
- package/dist/index.cjs +172 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -2
- package/dist/index.d.ts +69 -2
- package/dist/index.js +164 -4
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +7 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/sql/001_admin_log.sql +18 -0
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -88,6 +88,49 @@ new AdminModule({
|
|
|
88
88
|
});
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
## `GET /_admin/config`, `/_admin/routes`, `/_admin/access/tokens`
|
|
92
|
+
|
|
93
|
+
- **config** — readiness problems per module, and the presence (never the
|
|
94
|
+
value) of each environment variable the deployment reads. Bricks never
|
|
95
|
+
read `process.env` — config is injected — so the app names them:
|
|
96
|
+
`new AdminModule({ adminToken, env: ['DATABASE_URL', 'STRIPE_SECRET_KEY'] })`.
|
|
97
|
+
- **routes** — every route in the table with a guard class: `admin` (behind
|
|
98
|
+
this token), `probe` (core's `/healthz` `/readyz` `/metrics`), `app`
|
|
99
|
+
(everything else — whether it needs a session is not claimed here).
|
|
100
|
+
- **access/tokens** — the admin token's readiness verdict, and which bricks
|
|
101
|
+
still register a legacy standalone surface with their own token.
|
|
102
|
+
|
|
103
|
+
## From the terminal
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
export FONDERIE_ADMIN_URL=https://api.example.com FONDERIE_ADMIN_TOKEN=…
|
|
107
|
+
fonderie admin attention # what needs you today
|
|
108
|
+
fonderie admin doctor # every reconciliation check
|
|
109
|
+
fonderie admin log --limit 20 # who did what
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`FONDERIE_ADMIN_PREFIX` when the surface was moved off `/_admin`.
|
|
113
|
+
|
|
114
|
+
## The admin log
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { AdminModule } from '@fonderie/admin';
|
|
118
|
+
import { getMigrationsPath } from '@fonderie/admin/migrations';
|
|
119
|
+
|
|
120
|
+
app.register(new AdminModule({ adminToken, store })); // run the migrations first
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
With a store, every request served by this surface is a row in
|
|
124
|
+
`fonderie_admin_log` — actor (the `X-Actor` header, else `admin-token`),
|
|
125
|
+
method, path, route, module, status, duration, request id, client IP —
|
|
126
|
+
**including the ones the token guard refused.** The log middleware runs
|
|
127
|
+
before the guard, and a failed write never fails the request it describes.
|
|
128
|
+
Without a store nothing is recorded and the manifest says so
|
|
129
|
+
(`admin.log: false`).
|
|
130
|
+
|
|
131
|
+
`GET /_admin/activity/admin-log?limit=50&before=<cursor>` reads it, newest
|
|
132
|
+
first; `next` is the cursor for the following page.
|
|
133
|
+
|
|
91
134
|
## Composed routes
|
|
92
135
|
|
|
93
136
|
Bricks that implement `describeAdmin()` have their admin routes mounted here,
|
package/brain/outcomes.md
CHANGED
|
@@ -7,10 +7,34 @@ 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/access/tokens` | `async () => setApiResponse(HTTP.OK, 'ADMIN_TOKENS', 'Admin tokens', tokensReport(app, this.name))` |
|
|
36
|
+
| 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); }` |
|
|
37
|
+
| GET | `/_admin/config` | `async () => setApiResponse( HTTP.OK, 'ADMIN_CONFIG', 'Declared vs held', configReport(app, this.options.env ?? []), )` |
|
|
15
38
|
| 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 }), )` |
|
|
39
|
+
| GET | `/_admin/manifest` | `async () => setApiResponse( HTTP.OK, 'ADMIN_MANIFEST', 'Deployment manifest', buildManifest(app, { version: this.version, log: Boolean(store) }), )` |
|
|
40
|
+
| GET | `/_admin/routes` | `async () => setApiResponse(HTTP.OK, 'ADMIN_ROUTES', 'Exposed routes', routesReport(app, this.name))` |
|
package/brain/signatures.md
CHANGED
|
@@ -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,27 @@ 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
|
+
|
|
39
|
+
function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport
|
|
40
|
+
|
|
41
|
+
function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport
|
|
42
|
+
|
|
43
|
+
function tokensReport(app: IFonderieApp, adminModule: string): IAdminTokensReport
|
|
44
|
+
|
|
29
45
|
interface IAdminOptions {
|
|
30
46
|
adminToken?: string;
|
|
31
47
|
path?: string;
|
|
32
48
|
checks?: IAdminCheck[];
|
|
33
49
|
checkTimeoutMs?: number;
|
|
50
|
+
store?: IStoreAdapter;
|
|
51
|
+
env?: string[];
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
interface IAdminManifest {
|
|
@@ -38,6 +56,7 @@ interface IAdminManifest {
|
|
|
38
56
|
env: string;
|
|
39
57
|
admin: {
|
|
40
58
|
version: string;
|
|
59
|
+
log: boolean;
|
|
41
60
|
};
|
|
42
61
|
modules: IAdminModuleEntry[];
|
|
43
62
|
readiness: IReadinessReport;
|
|
@@ -74,4 +93,63 @@ interface IAdminAttentionItem {
|
|
|
74
93
|
severity: 'error' | 'advice';
|
|
75
94
|
message: string;
|
|
76
95
|
}
|
|
96
|
+
|
|
97
|
+
interface IAdminLogEntry {
|
|
98
|
+
id: string;
|
|
99
|
+
at: string;
|
|
100
|
+
actor: string;
|
|
101
|
+
method: string;
|
|
102
|
+
path: string;
|
|
103
|
+
route: string;
|
|
104
|
+
module: string;
|
|
105
|
+
status: number;
|
|
106
|
+
durationMs: number;
|
|
107
|
+
requestId: string | null;
|
|
108
|
+
clientIp: string | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface IAdminLogPage {
|
|
112
|
+
entries: IAdminLogEntry[];
|
|
113
|
+
next: string | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface IAdminEnvEntry {
|
|
117
|
+
name: string;
|
|
118
|
+
set: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface IAdminConfigReport {
|
|
122
|
+
generatedAt: string;
|
|
123
|
+
readiness: IReadinessReport;
|
|
124
|
+
modules: Array<{
|
|
125
|
+
name: string;
|
|
126
|
+
problems: IReadinessReport['problems'];
|
|
127
|
+
}>;
|
|
128
|
+
env: IAdminEnvEntry[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type AdminRouteGuard = 'admin' | 'probe' | 'app';
|
|
132
|
+
|
|
133
|
+
interface IAdminRouteEntry extends IRouteEntry {
|
|
134
|
+
guard: AdminRouteGuard;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface IAdminRoutesReport {
|
|
138
|
+
generatedAt: string;
|
|
139
|
+
routes: IAdminRouteEntry[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface IAdminTokenEntry {
|
|
143
|
+
module: string;
|
|
144
|
+
set: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface IAdminTokensReport {
|
|
148
|
+
generatedAt: string;
|
|
149
|
+
admin: {
|
|
150
|
+
ok: boolean;
|
|
151
|
+
problems: IReadinessReport['problems'];
|
|
152
|
+
};
|
|
153
|
+
legacy: IAdminTokenEntry[];
|
|
154
|
+
}
|
|
77
155
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -22,12 +22,19 @@ 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,
|
|
30
|
-
|
|
33
|
+
configReport: () => configReport,
|
|
34
|
+
readAdminLog: () => readAdminLog,
|
|
35
|
+
routesReport: () => routesReport,
|
|
36
|
+
runDoctor: () => runDoctor,
|
|
37
|
+
tokensReport: () => tokensReport
|
|
31
38
|
});
|
|
32
39
|
module.exports = __toCommonJS(index_exports);
|
|
33
40
|
|
|
@@ -99,6 +106,77 @@ function attention(app, doctor) {
|
|
|
99
106
|
return { generatedAt: doctor.generatedAt, ok: !items.some((i) => i.severity === "error"), items };
|
|
100
107
|
}
|
|
101
108
|
|
|
109
|
+
// src/log.ts
|
|
110
|
+
var DEFAULT_ACTOR = "admin-token";
|
|
111
|
+
function adminLog(store, route, module2) {
|
|
112
|
+
return async (ctx, next) => {
|
|
113
|
+
const started = Date.now();
|
|
114
|
+
const res = await next();
|
|
115
|
+
const row = {
|
|
116
|
+
actor: ctx.request.headers.get("x-actor") || DEFAULT_ACTOR,
|
|
117
|
+
method: ctx.request.method,
|
|
118
|
+
path: new URL(ctx.request.url).pathname,
|
|
119
|
+
route,
|
|
120
|
+
module: module2,
|
|
121
|
+
status: res.status,
|
|
122
|
+
durationMs: Date.now() - started,
|
|
123
|
+
requestId: typeof ctx.meta["requestId"] === "string" ? ctx.meta["requestId"] : null,
|
|
124
|
+
clientIp: ctx.meta.clientIp ?? null
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
await store.query(
|
|
128
|
+
`INSERT INTO fonderie_admin_log
|
|
129
|
+
(actor, method, path, route, module, status, duration_ms, request_id, client_ip)
|
|
130
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
|
131
|
+
[
|
|
132
|
+
row.actor,
|
|
133
|
+
row.method,
|
|
134
|
+
row.path,
|
|
135
|
+
row.route,
|
|
136
|
+
row.module,
|
|
137
|
+
row.status,
|
|
138
|
+
row.durationMs,
|
|
139
|
+
row.requestId,
|
|
140
|
+
row.clientIp
|
|
141
|
+
]
|
|
142
|
+
);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error(
|
|
145
|
+
"[admin] could not write the admin log:",
|
|
146
|
+
err instanceof Error ? err.message : err
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return res;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
var MAX_PAGE = 200;
|
|
153
|
+
async function readAdminLog(store, opts = {}) {
|
|
154
|
+
const limit = Math.min(Math.max(opts.limit ?? 50, 1), MAX_PAGE);
|
|
155
|
+
const cursor = opts.before ? decodeCursor(opts.before) : null;
|
|
156
|
+
const rows = await store.query(
|
|
157
|
+
`SELECT id, at, actor, method, path, route, module, status,
|
|
158
|
+
duration_ms AS "durationMs", request_id AS "requestId", client_ip AS "clientIp"
|
|
159
|
+
FROM fonderie_admin_log
|
|
160
|
+
WHERE ($1::timestamptz IS NULL OR (at, id) < ($1::timestamptz, $2::uuid))
|
|
161
|
+
ORDER BY at DESC, id DESC
|
|
162
|
+
LIMIT $3`,
|
|
163
|
+
[cursor?.at ?? null, cursor?.id ?? null, limit + 1]
|
|
164
|
+
);
|
|
165
|
+
const page = rows.slice(0, limit);
|
|
166
|
+
const last = page[page.length - 1];
|
|
167
|
+
return {
|
|
168
|
+
entries: page,
|
|
169
|
+
next: rows.length > limit && last ? encodeCursor(last) : null
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function encodeCursor(e) {
|
|
173
|
+
return Buffer.from(`${new Date(e.at).toISOString()}|${e.id}`, "utf8").toString("base64url");
|
|
174
|
+
}
|
|
175
|
+
function decodeCursor(c) {
|
|
176
|
+
const [at, id] = Buffer.from(c, "base64url").toString("utf8").split("|");
|
|
177
|
+
return at && id ? { at, id } : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
102
180
|
// src/manifest.ts
|
|
103
181
|
function buildManifest(app, admin) {
|
|
104
182
|
const report = app.securityReport();
|
|
@@ -128,10 +206,54 @@ function buildManifest(app, admin) {
|
|
|
128
206
|
};
|
|
129
207
|
}
|
|
130
208
|
|
|
209
|
+
// src/pages.ts
|
|
210
|
+
function configReport(app, env) {
|
|
211
|
+
const report = app.securityReport();
|
|
212
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const p of report.readiness.problems)
|
|
214
|
+
byModule.set(p.module, [...byModule.get(p.module) ?? [], p]);
|
|
215
|
+
return {
|
|
216
|
+
generatedAt: report.generatedAt,
|
|
217
|
+
readiness: report.readiness,
|
|
218
|
+
modules: report.modules.map(({ name }) => ({ name, problems: byModule.get(name) ?? [] })),
|
|
219
|
+
env: env.map((name) => ({
|
|
220
|
+
name,
|
|
221
|
+
set: process.env[name] !== void 0 && process.env[name] !== ""
|
|
222
|
+
}))
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
var PROBES = /* @__PURE__ */ new Set(["/healthz", "/readyz", "/metrics"]);
|
|
226
|
+
function routesReport(app, adminModule) {
|
|
227
|
+
return {
|
|
228
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
229
|
+
routes: app.routes().map((r) => ({
|
|
230
|
+
...r,
|
|
231
|
+
guard: r.module === adminModule ? "admin" : PROBES.has(r.path) && r.module === "@fonderie/core" ? "probe" : "app"
|
|
232
|
+
}))
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function tokensReport(app, adminModule) {
|
|
236
|
+
const report = app.securityReport();
|
|
237
|
+
const adminProblems = report.readiness.problems.filter((p) => p.module === adminModule);
|
|
238
|
+
const legacyOwners = new Set(
|
|
239
|
+
app.routes().filter((r) => r.module && r.module !== adminModule && isLegacyAdminPath(r.method, r.path)).map((r) => r.module)
|
|
240
|
+
);
|
|
241
|
+
return {
|
|
242
|
+
generatedAt: report.generatedAt,
|
|
243
|
+
admin: { ok: !adminProblems.some((p) => p.severity === "error"), problems: adminProblems },
|
|
244
|
+
legacy: report.modules.map(({ name }) => ({ module: name, set: legacyOwners.has(name) })).filter((e) => e.set)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function isLegacyAdminPath(method, path) {
|
|
248
|
+
if (/\/admin\/(config|secrets|templates)(\/|$)/.test(path)) return true;
|
|
249
|
+
if (/\/plans(\/|$)/.test(path) && method !== "GET") return true;
|
|
250
|
+
return /\/billing\/wallet\/grant$/.test(path);
|
|
251
|
+
}
|
|
252
|
+
|
|
131
253
|
// src/module.ts
|
|
132
254
|
var DEFAULT_ADMIN_PATH = "/_admin";
|
|
133
255
|
var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
134
|
-
var ADMIN_VERSION = "0.
|
|
256
|
+
var ADMIN_VERSION = "0.5.0";
|
|
135
257
|
var AdminModule = class {
|
|
136
258
|
constructor(options = {}) {
|
|
137
259
|
this.options = options;
|
|
@@ -147,6 +269,7 @@ var AdminModule = class {
|
|
|
147
269
|
app.reserve(this.path);
|
|
148
270
|
const guard = (0, import_middlewares.requireAdminToken)(token);
|
|
149
271
|
const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
|
|
272
|
+
const store = this.options.store;
|
|
150
273
|
const checks = collectChecks(app, this.options.checks ?? []);
|
|
151
274
|
const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
152
275
|
const doctor = () => runDoctor(checks, timeoutMs);
|
|
@@ -168,15 +291,51 @@ var AdminModule = class {
|
|
|
168
291
|
import_core.HTTP.OK,
|
|
169
292
|
"ADMIN_MANIFEST",
|
|
170
293
|
"Deployment manifest",
|
|
171
|
-
buildManifest(app, { version: this.version })
|
|
294
|
+
buildManifest(app, { version: this.version, log: Boolean(store) })
|
|
172
295
|
)
|
|
173
296
|
],
|
|
174
297
|
[
|
|
175
298
|
"GET",
|
|
176
299
|
"/_admin/doctor",
|
|
177
300
|
async () => (0, import_core.setApiResponse)(import_core.HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
|
|
301
|
+
],
|
|
302
|
+
[
|
|
303
|
+
"GET",
|
|
304
|
+
"/_admin/config",
|
|
305
|
+
async () => (0, import_core.setApiResponse)(
|
|
306
|
+
import_core.HTTP.OK,
|
|
307
|
+
"ADMIN_CONFIG",
|
|
308
|
+
"Declared vs held",
|
|
309
|
+
configReport(app, this.options.env ?? [])
|
|
310
|
+
)
|
|
311
|
+
],
|
|
312
|
+
[
|
|
313
|
+
"GET",
|
|
314
|
+
"/_admin/routes",
|
|
315
|
+
async () => (0, import_core.setApiResponse)(import_core.HTTP.OK, "ADMIN_ROUTES", "Exposed routes", routesReport(app, this.name))
|
|
316
|
+
],
|
|
317
|
+
[
|
|
318
|
+
"GET",
|
|
319
|
+
"/_admin/access/tokens",
|
|
320
|
+
async () => (0, import_core.setApiResponse)(import_core.HTTP.OK, "ADMIN_TOKENS", "Admin tokens", tokensReport(app, this.name))
|
|
178
321
|
]
|
|
179
322
|
];
|
|
323
|
+
if (store) {
|
|
324
|
+
own.push([
|
|
325
|
+
"GET",
|
|
326
|
+
"/_admin/activity/admin-log",
|
|
327
|
+
async (ctx) => {
|
|
328
|
+
const q = new URL(ctx.request.url).searchParams;
|
|
329
|
+
const limit = Number(q.get("limit")) || void 0;
|
|
330
|
+
const before = q.get("before") ?? void 0;
|
|
331
|
+
const page = await readAdminLog(store, {
|
|
332
|
+
...limit ? { limit } : {},
|
|
333
|
+
...before ? { before } : {}
|
|
334
|
+
});
|
|
335
|
+
return (0, import_core.setApiResponse)(import_core.HTTP.OK, "ADMIN_LOG", "Admin activity", page);
|
|
336
|
+
}
|
|
337
|
+
]);
|
|
338
|
+
}
|
|
180
339
|
const mounted = /* @__PURE__ */ new Map();
|
|
181
340
|
const mount = (module2, method, path, handlers) => {
|
|
182
341
|
const key = `${method.toUpperCase()} ${path}`;
|
|
@@ -187,7 +346,8 @@ var AdminModule = class {
|
|
|
187
346
|
);
|
|
188
347
|
}
|
|
189
348
|
mounted.set(key, module2);
|
|
190
|
-
|
|
349
|
+
const chain = store ? [adminLog(store, path, module2), guard, ...handlers] : [guard, ...handlers];
|
|
350
|
+
app.addRoute(method, path, ...chain);
|
|
191
351
|
};
|
|
192
352
|
for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
|
|
193
353
|
for (const { module: module2, description } of app.adminDescriptions()) {
|
|
@@ -203,11 +363,18 @@ var AdminModule = class {
|
|
|
203
363
|
0 && (module.exports = {
|
|
204
364
|
ADMIN_VERSION,
|
|
205
365
|
AdminModule,
|
|
366
|
+
DEFAULT_ACTOR,
|
|
206
367
|
DEFAULT_ADMIN_PATH,
|
|
207
368
|
DEFAULT_CHECK_TIMEOUT_MS,
|
|
369
|
+
MAX_PAGE,
|
|
370
|
+
adminLog,
|
|
208
371
|
attention,
|
|
209
372
|
buildManifest,
|
|
210
373
|
collectChecks,
|
|
211
|
-
|
|
374
|
+
configReport,
|
|
375
|
+
readAdminLog,
|
|
376
|
+
routesReport,
|
|
377
|
+
runDoctor,
|
|
378
|
+
tokensReport
|
|
212
379
|
});
|
|
213
380
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -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","../src/pages.ts"],"sourcesContent":["export { AdminModule, ADMIN_VERSION, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS } from './module';\nexport { buildManifest } from './manifest';\nexport { runDoctor, collectChecks, attention } from './doctor';\nexport { adminLog, readAdminLog, DEFAULT_ACTOR, MAX_PAGE } from './log';\nexport { configReport, routesReport, tokensReport } from './pages';\nexport type {\n\tIAdminOptions,\n\tIAdminManifest,\n\tIAdminModuleEntry,\n\tIAdminCheckResult,\n\tIAdminDoctorReport,\n\tIAdminAttention,\n\tIAdminAttentionItem,\n\tIAdminLogEntry,\n\tIAdminLogPage,\n\tIAdminEnvEntry,\n\tIAdminConfigReport,\n\tAdminRouteGuard,\n\tIAdminRouteEntry,\n\tIAdminRoutesReport,\n\tIAdminTokenEntry,\n\tIAdminTokensReport,\n} 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 { configReport, routesReport, tokensReport } from './pages';\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\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/config',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_CONFIG',\n\t\t\t\t\t\t'Declared vs held',\n\t\t\t\t\t\tconfigReport(app, this.options.env ?? []),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/routes',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_ROUTES', 'Exposed routes', routesReport(app, this.name)),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/access/tokens',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_TOKENS', 'Admin tokens', tokensReport(app, this.name)),\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","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminConfigReport, IAdminRoutesReport, IAdminTokensReport } from './types';\n\nexport function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems)\n\t\tbyModule.set(p.module, [...(byModule.get(p.module) ?? []), p]);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\treadiness: report.readiness,\n\t\tmodules: report.modules.map(({ name }) => ({ name, problems: byModule.get(name) ?? [] })),\n\t\tenv: env.map((name) => ({\n\t\t\tname,\n\t\t\tset: process.env[name] !== undefined && process.env[name] !== '',\n\t\t})),\n\t};\n}\n\nconst PROBES = new Set(['/healthz', '/readyz', '/metrics']);\n\nexport function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport {\n\treturn {\n\t\tgeneratedAt: new Date().toISOString(),\n\t\troutes: app.routes().map((r) => ({\n\t\t\t...r,\n\t\t\tguard:\n\t\t\t\tr.module === adminModule\n\t\t\t\t\t? 'admin'\n\t\t\t\t\t: PROBES.has(r.path) && r.module === '@fonderie/core'\n\t\t\t\t\t\t? 'probe'\n\t\t\t\t\t\t: 'app',\n\t\t})),\n\t};\n}\n\n// A brick's own adminToken shows as a readiness problem only when weak, so\n// \"set at all\" has to be asked; a described module that also registers a\n// legacy route under /admin, /plans or /billing/wallet/grant has one.\nexport function tokensReport(app: IFonderieApp, adminModule: string): IAdminTokensReport {\n\tconst report = app.securityReport();\n\tconst adminProblems = report.readiness.problems.filter((p) => p.module === adminModule);\n\tconst legacyOwners = new Set(\n\t\tapp\n\t\t\t.routes()\n\t\t\t.filter((r) => r.module && r.module !== adminModule && isLegacyAdminPath(r.method, r.path))\n\t\t\t.map((r) => r.module as string),\n\t);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tadmin: { ok: !adminProblems.some((p) => p.severity === 'error'), problems: adminProblems },\n\t\tlegacy: report.modules\n\t\t\t.map(({ name }) => ({ module: name, set: legacyOwners.has(name) }))\n\t\t\t.filter((e) => e.set),\n\t};\n}\n\nfunction isLegacyAdminPath(method: string, path: string): boolean {\n\tif (/\\/admin\\/(config|secrets|templates)(\\/|$)/.test(path)) return true;\n\tif (/\\/plans(\\/|$)/.test(path) && method !== 'GET') return true;\n\treturn /\\/billing\\/wallet\\/grant$/.test(path);\n}\n"],"mappings":"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","names":["module","module","module"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
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;
|
|
10
|
+
env?: string[];
|
|
8
11
|
}
|
|
9
12
|
interface IAdminModuleEntry {
|
|
10
13
|
name: string;
|
|
@@ -17,6 +20,7 @@ interface IAdminManifest {
|
|
|
17
20
|
env: string;
|
|
18
21
|
admin: {
|
|
19
22
|
version: string;
|
|
23
|
+
log: boolean;
|
|
20
24
|
};
|
|
21
25
|
modules: IAdminModuleEntry[];
|
|
22
26
|
readiness: IReadinessReport;
|
|
@@ -42,6 +46,56 @@ interface IAdminAttention {
|
|
|
42
46
|
ok: boolean;
|
|
43
47
|
items: IAdminAttentionItem[];
|
|
44
48
|
}
|
|
49
|
+
interface IAdminLogEntry {
|
|
50
|
+
id: string;
|
|
51
|
+
at: string;
|
|
52
|
+
actor: string;
|
|
53
|
+
method: string;
|
|
54
|
+
path: string;
|
|
55
|
+
route: string;
|
|
56
|
+
module: string;
|
|
57
|
+
status: number;
|
|
58
|
+
durationMs: number;
|
|
59
|
+
requestId: string | null;
|
|
60
|
+
clientIp: string | null;
|
|
61
|
+
}
|
|
62
|
+
interface IAdminLogPage {
|
|
63
|
+
entries: IAdminLogEntry[];
|
|
64
|
+
next: string | null;
|
|
65
|
+
}
|
|
66
|
+
interface IAdminEnvEntry {
|
|
67
|
+
name: string;
|
|
68
|
+
set: boolean;
|
|
69
|
+
}
|
|
70
|
+
interface IAdminConfigReport {
|
|
71
|
+
generatedAt: string;
|
|
72
|
+
readiness: IReadinessReport;
|
|
73
|
+
modules: Array<{
|
|
74
|
+
name: string;
|
|
75
|
+
problems: IReadinessReport['problems'];
|
|
76
|
+
}>;
|
|
77
|
+
env: IAdminEnvEntry[];
|
|
78
|
+
}
|
|
79
|
+
type AdminRouteGuard = 'admin' | 'probe' | 'app';
|
|
80
|
+
interface IAdminRouteEntry extends IRouteEntry {
|
|
81
|
+
guard: AdminRouteGuard;
|
|
82
|
+
}
|
|
83
|
+
interface IAdminRoutesReport {
|
|
84
|
+
generatedAt: string;
|
|
85
|
+
routes: IAdminRouteEntry[];
|
|
86
|
+
}
|
|
87
|
+
interface IAdminTokenEntry {
|
|
88
|
+
module: string;
|
|
89
|
+
set: boolean;
|
|
90
|
+
}
|
|
91
|
+
interface IAdminTokensReport {
|
|
92
|
+
generatedAt: string;
|
|
93
|
+
admin: {
|
|
94
|
+
ok: boolean;
|
|
95
|
+
problems: IReadinessReport['problems'];
|
|
96
|
+
};
|
|
97
|
+
legacy: IAdminTokenEntry[];
|
|
98
|
+
}
|
|
45
99
|
|
|
46
100
|
declare const DEFAULT_ADMIN_PATH = "/_admin";
|
|
47
101
|
declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
|
|
@@ -58,6 +112,7 @@ declare class AdminModule implements IFonderieModule {
|
|
|
58
112
|
|
|
59
113
|
declare function buildManifest(app: IFonderieApp, admin: {
|
|
60
114
|
version: string;
|
|
115
|
+
log: boolean;
|
|
61
116
|
}): IAdminManifest;
|
|
62
117
|
|
|
63
118
|
interface INamedCheck extends IAdminCheck {
|
|
@@ -67,4 +122,16 @@ declare function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedChe
|
|
|
67
122
|
declare function runDoctor(checks: INamedCheck[], timeoutMs: number): Promise<IAdminDoctorReport>;
|
|
68
123
|
declare function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention;
|
|
69
124
|
|
|
70
|
-
|
|
125
|
+
declare const DEFAULT_ACTOR = "admin-token";
|
|
126
|
+
declare function adminLog(store: IStoreAdapter, route: string, module: string): Middleware;
|
|
127
|
+
declare const MAX_PAGE = 200;
|
|
128
|
+
declare function readAdminLog(store: IStoreAdapter, opts?: {
|
|
129
|
+
limit?: number;
|
|
130
|
+
before?: string;
|
|
131
|
+
}): Promise<IAdminLogPage>;
|
|
132
|
+
|
|
133
|
+
declare function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport;
|
|
134
|
+
declare function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport;
|
|
135
|
+
declare function tokensReport(app: IFonderieApp, adminModule: string): IAdminTokensReport;
|
|
136
|
+
|
|
137
|
+
export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokensReport, MAX_PAGE, adminLog, attention, buildManifest, collectChecks, configReport, readAdminLog, routesReport, runDoctor, tokensReport };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
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;
|
|
10
|
+
env?: string[];
|
|
8
11
|
}
|
|
9
12
|
interface IAdminModuleEntry {
|
|
10
13
|
name: string;
|
|
@@ -17,6 +20,7 @@ interface IAdminManifest {
|
|
|
17
20
|
env: string;
|
|
18
21
|
admin: {
|
|
19
22
|
version: string;
|
|
23
|
+
log: boolean;
|
|
20
24
|
};
|
|
21
25
|
modules: IAdminModuleEntry[];
|
|
22
26
|
readiness: IReadinessReport;
|
|
@@ -42,6 +46,56 @@ interface IAdminAttention {
|
|
|
42
46
|
ok: boolean;
|
|
43
47
|
items: IAdminAttentionItem[];
|
|
44
48
|
}
|
|
49
|
+
interface IAdminLogEntry {
|
|
50
|
+
id: string;
|
|
51
|
+
at: string;
|
|
52
|
+
actor: string;
|
|
53
|
+
method: string;
|
|
54
|
+
path: string;
|
|
55
|
+
route: string;
|
|
56
|
+
module: string;
|
|
57
|
+
status: number;
|
|
58
|
+
durationMs: number;
|
|
59
|
+
requestId: string | null;
|
|
60
|
+
clientIp: string | null;
|
|
61
|
+
}
|
|
62
|
+
interface IAdminLogPage {
|
|
63
|
+
entries: IAdminLogEntry[];
|
|
64
|
+
next: string | null;
|
|
65
|
+
}
|
|
66
|
+
interface IAdminEnvEntry {
|
|
67
|
+
name: string;
|
|
68
|
+
set: boolean;
|
|
69
|
+
}
|
|
70
|
+
interface IAdminConfigReport {
|
|
71
|
+
generatedAt: string;
|
|
72
|
+
readiness: IReadinessReport;
|
|
73
|
+
modules: Array<{
|
|
74
|
+
name: string;
|
|
75
|
+
problems: IReadinessReport['problems'];
|
|
76
|
+
}>;
|
|
77
|
+
env: IAdminEnvEntry[];
|
|
78
|
+
}
|
|
79
|
+
type AdminRouteGuard = 'admin' | 'probe' | 'app';
|
|
80
|
+
interface IAdminRouteEntry extends IRouteEntry {
|
|
81
|
+
guard: AdminRouteGuard;
|
|
82
|
+
}
|
|
83
|
+
interface IAdminRoutesReport {
|
|
84
|
+
generatedAt: string;
|
|
85
|
+
routes: IAdminRouteEntry[];
|
|
86
|
+
}
|
|
87
|
+
interface IAdminTokenEntry {
|
|
88
|
+
module: string;
|
|
89
|
+
set: boolean;
|
|
90
|
+
}
|
|
91
|
+
interface IAdminTokensReport {
|
|
92
|
+
generatedAt: string;
|
|
93
|
+
admin: {
|
|
94
|
+
ok: boolean;
|
|
95
|
+
problems: IReadinessReport['problems'];
|
|
96
|
+
};
|
|
97
|
+
legacy: IAdminTokenEntry[];
|
|
98
|
+
}
|
|
45
99
|
|
|
46
100
|
declare const DEFAULT_ADMIN_PATH = "/_admin";
|
|
47
101
|
declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
|
|
@@ -58,6 +112,7 @@ declare class AdminModule implements IFonderieModule {
|
|
|
58
112
|
|
|
59
113
|
declare function buildManifest(app: IFonderieApp, admin: {
|
|
60
114
|
version: string;
|
|
115
|
+
log: boolean;
|
|
61
116
|
}): IAdminManifest;
|
|
62
117
|
|
|
63
118
|
interface INamedCheck extends IAdminCheck {
|
|
@@ -67,4 +122,16 @@ declare function collectChecks(app: IFonderieApp, own: IAdminCheck[]): INamedChe
|
|
|
67
122
|
declare function runDoctor(checks: INamedCheck[], timeoutMs: number): Promise<IAdminDoctorReport>;
|
|
68
123
|
declare function attention(app: IFonderieApp, doctor: IAdminDoctorReport): IAdminAttention;
|
|
69
124
|
|
|
70
|
-
|
|
125
|
+
declare const DEFAULT_ACTOR = "admin-token";
|
|
126
|
+
declare function adminLog(store: IStoreAdapter, route: string, module: string): Middleware;
|
|
127
|
+
declare const MAX_PAGE = 200;
|
|
128
|
+
declare function readAdminLog(store: IStoreAdapter, opts?: {
|
|
129
|
+
limit?: number;
|
|
130
|
+
before?: string;
|
|
131
|
+
}): Promise<IAdminLogPage>;
|
|
132
|
+
|
|
133
|
+
declare function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport;
|
|
134
|
+
declare function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport;
|
|
135
|
+
declare function tokensReport(app: IFonderieApp, adminModule: string): IAdminTokensReport;
|
|
136
|
+
|
|
137
|
+
export { ADMIN_VERSION, AdminModule, type AdminRouteGuard, DEFAULT_ACTOR, DEFAULT_ADMIN_PATH, DEFAULT_CHECK_TIMEOUT_MS, type IAdminAttention, type IAdminAttentionItem, type IAdminCheckResult, type IAdminConfigReport, type IAdminDoctorReport, type IAdminEnvEntry, type IAdminLogEntry, type IAdminLogPage, type IAdminManifest, type IAdminModuleEntry, type IAdminOptions, type IAdminRouteEntry, type IAdminRoutesReport, type IAdminTokenEntry, type IAdminTokensReport, MAX_PAGE, adminLog, attention, buildManifest, collectChecks, configReport, readAdminLog, routesReport, runDoctor, tokensReport };
|
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();
|
|
@@ -95,10 +166,54 @@ function buildManifest(app, admin) {
|
|
|
95
166
|
};
|
|
96
167
|
}
|
|
97
168
|
|
|
169
|
+
// src/pages.ts
|
|
170
|
+
function configReport(app, env) {
|
|
171
|
+
const report = app.securityReport();
|
|
172
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
173
|
+
for (const p of report.readiness.problems)
|
|
174
|
+
byModule.set(p.module, [...byModule.get(p.module) ?? [], p]);
|
|
175
|
+
return {
|
|
176
|
+
generatedAt: report.generatedAt,
|
|
177
|
+
readiness: report.readiness,
|
|
178
|
+
modules: report.modules.map(({ name }) => ({ name, problems: byModule.get(name) ?? [] })),
|
|
179
|
+
env: env.map((name) => ({
|
|
180
|
+
name,
|
|
181
|
+
set: process.env[name] !== void 0 && process.env[name] !== ""
|
|
182
|
+
}))
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
var PROBES = /* @__PURE__ */ new Set(["/healthz", "/readyz", "/metrics"]);
|
|
186
|
+
function routesReport(app, adminModule) {
|
|
187
|
+
return {
|
|
188
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
189
|
+
routes: app.routes().map((r) => ({
|
|
190
|
+
...r,
|
|
191
|
+
guard: r.module === adminModule ? "admin" : PROBES.has(r.path) && r.module === "@fonderie/core" ? "probe" : "app"
|
|
192
|
+
}))
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function tokensReport(app, adminModule) {
|
|
196
|
+
const report = app.securityReport();
|
|
197
|
+
const adminProblems = report.readiness.problems.filter((p) => p.module === adminModule);
|
|
198
|
+
const legacyOwners = new Set(
|
|
199
|
+
app.routes().filter((r) => r.module && r.module !== adminModule && isLegacyAdminPath(r.method, r.path)).map((r) => r.module)
|
|
200
|
+
);
|
|
201
|
+
return {
|
|
202
|
+
generatedAt: report.generatedAt,
|
|
203
|
+
admin: { ok: !adminProblems.some((p) => p.severity === "error"), problems: adminProblems },
|
|
204
|
+
legacy: report.modules.map(({ name }) => ({ module: name, set: legacyOwners.has(name) })).filter((e) => e.set)
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function isLegacyAdminPath(method, path) {
|
|
208
|
+
if (/\/admin\/(config|secrets|templates)(\/|$)/.test(path)) return true;
|
|
209
|
+
if (/\/plans(\/|$)/.test(path) && method !== "GET") return true;
|
|
210
|
+
return /\/billing\/wallet\/grant$/.test(path);
|
|
211
|
+
}
|
|
212
|
+
|
|
98
213
|
// src/module.ts
|
|
99
214
|
var DEFAULT_ADMIN_PATH = "/_admin";
|
|
100
215
|
var DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
101
|
-
var ADMIN_VERSION = "0.
|
|
216
|
+
var ADMIN_VERSION = "0.5.0";
|
|
102
217
|
var AdminModule = class {
|
|
103
218
|
constructor(options = {}) {
|
|
104
219
|
this.options = options;
|
|
@@ -114,6 +229,7 @@ var AdminModule = class {
|
|
|
114
229
|
app.reserve(this.path);
|
|
115
230
|
const guard = requireAdminToken(token);
|
|
116
231
|
const at = (p) => this.path + p.slice(DEFAULT_ADMIN_PATH.length);
|
|
232
|
+
const store = this.options.store;
|
|
117
233
|
const checks = collectChecks(app, this.options.checks ?? []);
|
|
118
234
|
const timeoutMs = this.options.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
119
235
|
const doctor = () => runDoctor(checks, timeoutMs);
|
|
@@ -135,15 +251,51 @@ var AdminModule = class {
|
|
|
135
251
|
HTTP.OK,
|
|
136
252
|
"ADMIN_MANIFEST",
|
|
137
253
|
"Deployment manifest",
|
|
138
|
-
buildManifest(app, { version: this.version })
|
|
254
|
+
buildManifest(app, { version: this.version, log: Boolean(store) })
|
|
139
255
|
)
|
|
140
256
|
],
|
|
141
257
|
[
|
|
142
258
|
"GET",
|
|
143
259
|
"/_admin/doctor",
|
|
144
260
|
async () => setApiResponse(HTTP.OK, "ADMIN_DOCTOR", "Reconciliation checks", await doctor())
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
"GET",
|
|
264
|
+
"/_admin/config",
|
|
265
|
+
async () => setApiResponse(
|
|
266
|
+
HTTP.OK,
|
|
267
|
+
"ADMIN_CONFIG",
|
|
268
|
+
"Declared vs held",
|
|
269
|
+
configReport(app, this.options.env ?? [])
|
|
270
|
+
)
|
|
271
|
+
],
|
|
272
|
+
[
|
|
273
|
+
"GET",
|
|
274
|
+
"/_admin/routes",
|
|
275
|
+
async () => setApiResponse(HTTP.OK, "ADMIN_ROUTES", "Exposed routes", routesReport(app, this.name))
|
|
276
|
+
],
|
|
277
|
+
[
|
|
278
|
+
"GET",
|
|
279
|
+
"/_admin/access/tokens",
|
|
280
|
+
async () => setApiResponse(HTTP.OK, "ADMIN_TOKENS", "Admin tokens", tokensReport(app, this.name))
|
|
145
281
|
]
|
|
146
282
|
];
|
|
283
|
+
if (store) {
|
|
284
|
+
own.push([
|
|
285
|
+
"GET",
|
|
286
|
+
"/_admin/activity/admin-log",
|
|
287
|
+
async (ctx) => {
|
|
288
|
+
const q = new URL(ctx.request.url).searchParams;
|
|
289
|
+
const limit = Number(q.get("limit")) || void 0;
|
|
290
|
+
const before = q.get("before") ?? void 0;
|
|
291
|
+
const page = await readAdminLog(store, {
|
|
292
|
+
...limit ? { limit } : {},
|
|
293
|
+
...before ? { before } : {}
|
|
294
|
+
});
|
|
295
|
+
return setApiResponse(HTTP.OK, "ADMIN_LOG", "Admin activity", page);
|
|
296
|
+
}
|
|
297
|
+
]);
|
|
298
|
+
}
|
|
147
299
|
const mounted = /* @__PURE__ */ new Map();
|
|
148
300
|
const mount = (module, method, path, handlers) => {
|
|
149
301
|
const key = `${method.toUpperCase()} ${path}`;
|
|
@@ -154,7 +306,8 @@ var AdminModule = class {
|
|
|
154
306
|
);
|
|
155
307
|
}
|
|
156
308
|
mounted.set(key, module);
|
|
157
|
-
|
|
309
|
+
const chain = store ? [adminLog(store, path, module), guard, ...handlers] : [guard, ...handlers];
|
|
310
|
+
app.addRoute(method, path, ...chain);
|
|
158
311
|
};
|
|
159
312
|
for (const [method, path, handler] of own) mount(this.name, method, at(path), [handler]);
|
|
160
313
|
for (const { module, description } of app.adminDescriptions()) {
|
|
@@ -169,11 +322,18 @@ var AdminModule = class {
|
|
|
169
322
|
export {
|
|
170
323
|
ADMIN_VERSION,
|
|
171
324
|
AdminModule,
|
|
325
|
+
DEFAULT_ACTOR,
|
|
172
326
|
DEFAULT_ADMIN_PATH,
|
|
173
327
|
DEFAULT_CHECK_TIMEOUT_MS,
|
|
328
|
+
MAX_PAGE,
|
|
329
|
+
adminLog,
|
|
174
330
|
attention,
|
|
175
331
|
buildManifest,
|
|
176
332
|
collectChecks,
|
|
177
|
-
|
|
333
|
+
configReport,
|
|
334
|
+
readAdminLog,
|
|
335
|
+
routesReport,
|
|
336
|
+
runDoctor,
|
|
337
|
+
tokensReport
|
|
178
338
|
};
|
|
179
339
|
//# 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","../src/pages.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 { configReport, routesReport, tokensReport } from './pages';\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\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/config',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'ADMIN_CONFIG',\n\t\t\t\t\t\t'Declared vs held',\n\t\t\t\t\t\tconfigReport(app, this.options.env ?? []),\n\t\t\t\t\t),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/routes',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_ROUTES', 'Exposed routes', routesReport(app, this.name)),\n\t\t\t],\n\t\t\t[\n\t\t\t\t'GET',\n\t\t\t\t'/_admin/access/tokens',\n\t\t\t\tasync () =>\n\t\t\t\t\tsetApiResponse(HTTP.OK, 'ADMIN_TOKENS', 'Admin tokens', tokensReport(app, this.name)),\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","import type { IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport type { IAdminConfigReport, IAdminRoutesReport, IAdminTokensReport } from './types';\n\nexport function configReport(app: IFonderieApp, env: string[]): IAdminConfigReport {\n\tconst report = app.securityReport();\n\tconst byModule = new Map<string, IReadinessProblem[]>();\n\tfor (const p of report.readiness.problems)\n\t\tbyModule.set(p.module, [...(byModule.get(p.module) ?? []), p]);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\treadiness: report.readiness,\n\t\tmodules: report.modules.map(({ name }) => ({ name, problems: byModule.get(name) ?? [] })),\n\t\tenv: env.map((name) => ({\n\t\t\tname,\n\t\t\tset: process.env[name] !== undefined && process.env[name] !== '',\n\t\t})),\n\t};\n}\n\nconst PROBES = new Set(['/healthz', '/readyz', '/metrics']);\n\nexport function routesReport(app: IFonderieApp, adminModule: string): IAdminRoutesReport {\n\treturn {\n\t\tgeneratedAt: new Date().toISOString(),\n\t\troutes: app.routes().map((r) => ({\n\t\t\t...r,\n\t\t\tguard:\n\t\t\t\tr.module === adminModule\n\t\t\t\t\t? 'admin'\n\t\t\t\t\t: PROBES.has(r.path) && r.module === '@fonderie/core'\n\t\t\t\t\t\t? 'probe'\n\t\t\t\t\t\t: 'app',\n\t\t})),\n\t};\n}\n\n// A brick's own adminToken shows as a readiness problem only when weak, so\n// \"set at all\" has to be asked; a described module that also registers a\n// legacy route under /admin, /plans or /billing/wallet/grant has one.\nexport function tokensReport(app: IFonderieApp, adminModule: string): IAdminTokensReport {\n\tconst report = app.securityReport();\n\tconst adminProblems = report.readiness.problems.filter((p) => p.module === adminModule);\n\tconst legacyOwners = new Set(\n\t\tapp\n\t\t\t.routes()\n\t\t\t.filter((r) => r.module && r.module !== adminModule && isLegacyAdminPath(r.method, r.path))\n\t\t\t.map((r) => r.module as string),\n\t);\n\treturn {\n\t\tgeneratedAt: report.generatedAt,\n\t\tadmin: { ok: !adminProblems.some((p) => p.severity === 'error'), problems: adminProblems },\n\t\tlegacy: report.modules\n\t\t\t.map(({ name }) => ({ module: name, set: legacyOwners.has(name) }))\n\t\t\t.filter((e) => e.set),\n\t};\n}\n\nfunction isLegacyAdminPath(method: string, path: string): boolean {\n\tif (/\\/admin\\/(config|secrets|templates)(\\/|$)/.test(path)) return true;\n\tif (/\\/plans(\\/|$)/.test(path) && method !== 'GET') return true;\n\treturn /\\/billing\\/wallet\\/grant$/.test(path);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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
|
+
"version": "0.5.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",
|