@fonderie/core 0.5.0 → 0.6.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 +7 -7
- package/brain/signatures.md +20 -0
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +4 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js.map +1 -1
- package/dist/index.cjs +137 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +134 -2
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +82 -2
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +14 -2
- package/dist/middlewares/index.d.ts +14 -2
- package/dist/middlewares/index.js +78 -1
- package/dist/middlewares/index.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +8 -1
- package/dist/types.d.ts +8 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -130,6 +130,39 @@ var withBody = async (ctx, next) => {
|
|
|
130
130
|
return next();
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
+
// src/middlewares/security-headers.ts
|
|
134
|
+
function withSecurityHeaders(options = {}) {
|
|
135
|
+
const {
|
|
136
|
+
hstsMaxAge = 60 * 60 * 24 * 180,
|
|
137
|
+
hstsIncludeSubDomains = false,
|
|
138
|
+
hstsPreload = false
|
|
139
|
+
} = options;
|
|
140
|
+
let hsts = "";
|
|
141
|
+
if (hstsMaxAge > 0) {
|
|
142
|
+
hsts = `max-age=${hstsMaxAge}`;
|
|
143
|
+
if (hstsIncludeSubDomains || hstsPreload) hsts += "; includeSubDomains";
|
|
144
|
+
if (hstsPreload) hsts += "; preload";
|
|
145
|
+
}
|
|
146
|
+
return async (ctx, next) => {
|
|
147
|
+
const response = await next();
|
|
148
|
+
const patched = new Headers(response.headers);
|
|
149
|
+
patched.set("X-Content-Type-Options", "nosniff");
|
|
150
|
+
if (hsts && isHttps(ctx.request)) {
|
|
151
|
+
patched.set("Strict-Transport-Security", hsts);
|
|
152
|
+
}
|
|
153
|
+
return new Response(response.body, {
|
|
154
|
+
headers: patched,
|
|
155
|
+
status: response.status,
|
|
156
|
+
statusText: response.statusText
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function isHttps(request) {
|
|
161
|
+
if (request.url.startsWith("https:")) return true;
|
|
162
|
+
const proto = request.headers.get("x-forwarded-proto");
|
|
163
|
+
return proto?.split(",")[0]?.trim() === "https";
|
|
164
|
+
}
|
|
165
|
+
|
|
133
166
|
// src/middlewares/error-handler.ts
|
|
134
167
|
function defaultErrorHandler(err) {
|
|
135
168
|
const dev = process.env["NODE_ENV"] !== "production";
|
|
@@ -145,6 +178,36 @@ function defaultErrorHandler(err) {
|
|
|
145
178
|
return setApiResponse(HTTP.SERVER_ERROR, "SERVER_ERROR", "Internal server error");
|
|
146
179
|
}
|
|
147
180
|
|
|
181
|
+
// src/middlewares/require-admin-token.ts
|
|
182
|
+
import { timingSafeEqual } from "crypto";
|
|
183
|
+
|
|
184
|
+
// src/metrics.ts
|
|
185
|
+
var MetricsRegistry = class {
|
|
186
|
+
counters = /* @__PURE__ */ new Map();
|
|
187
|
+
inc(name, labels = {}, by = 1) {
|
|
188
|
+
const key = seriesKey(name, labels);
|
|
189
|
+
this.counters.set(key, (this.counters.get(key) ?? 0) + by);
|
|
190
|
+
}
|
|
191
|
+
// Prometheus text exposition format.
|
|
192
|
+
render() {
|
|
193
|
+
const lines = [];
|
|
194
|
+
for (const [key, value] of this.counters) lines.push(`${key} ${value}`);
|
|
195
|
+
return lines.join("\n") + (lines.length ? "\n" : "");
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
function seriesKey(name, labels) {
|
|
199
|
+
const parts = Object.entries(labels).map(([k, v]) => `${k}="${String(v).replace(/"/g, "")}"`);
|
|
200
|
+
return parts.length ? `${name}{${parts.join(",")}}` : name;
|
|
201
|
+
}
|
|
202
|
+
function withMetrics(registry) {
|
|
203
|
+
return async (ctx, next) => {
|
|
204
|
+
const response = await next();
|
|
205
|
+
const cls = `${Math.floor(response.status / 100)}xx`;
|
|
206
|
+
registry.inc("http_requests_total", { status_class: cls });
|
|
207
|
+
return response;
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
148
211
|
// src/app.ts
|
|
149
212
|
var FonderieApp = class {
|
|
150
213
|
config;
|
|
@@ -152,10 +215,12 @@ var FonderieApp = class {
|
|
|
152
215
|
router = new Router();
|
|
153
216
|
middlewares = [];
|
|
154
217
|
modules = /* @__PURE__ */ new Map();
|
|
218
|
+
metrics = new MetricsRegistry();
|
|
155
219
|
constructor(config) {
|
|
156
220
|
this.config = config;
|
|
157
221
|
this.prefix = (config.basePath ?? "").replace(/\/$/, "");
|
|
158
|
-
this.middlewares = [withBody];
|
|
222
|
+
this.middlewares = [withBody, withSecurityHeaders()];
|
|
223
|
+
if (config.metrics) this.middlewares.push(withMetrics(this.metrics));
|
|
159
224
|
}
|
|
160
225
|
listen(port, options = {}) {
|
|
161
226
|
const {
|
|
@@ -230,12 +295,77 @@ var FonderieApp = class {
|
|
|
230
295
|
}
|
|
231
296
|
return { ok: !problems.some((p) => p.severity === "error"), problems };
|
|
232
297
|
}
|
|
298
|
+
// A point-in-time control-posture snapshot for SOC 2 evidence: which modules
|
|
299
|
+
// are registered and the current readiness report. Serialise to a file/log
|
|
300
|
+
// (e.g. on a schedule) as an audit artifact.
|
|
301
|
+
securityReport() {
|
|
302
|
+
return {
|
|
303
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
304
|
+
env: process.env["NODE_ENV"] ?? "development",
|
|
305
|
+
registeredModules: [...this.modules.keys()].sort(),
|
|
306
|
+
readiness: this.checkProductionReadiness()
|
|
307
|
+
};
|
|
308
|
+
}
|
|
233
309
|
async boot() {
|
|
310
|
+
this.enforceProductionReadiness();
|
|
234
311
|
for (const module of topoSort([...this.modules.values()])) {
|
|
235
312
|
await module.install(this);
|
|
236
313
|
}
|
|
314
|
+
this.registerHealthRoutes();
|
|
237
315
|
return this;
|
|
238
316
|
}
|
|
317
|
+
// Liveness (/healthz) and readiness (/readyz) probes. Registered unprefixed so
|
|
318
|
+
// they sit at a stable path regardless of basePath. Enabled unless disabled.
|
|
319
|
+
registerHealthRoutes() {
|
|
320
|
+
if (this.config.healthChecks === false) return;
|
|
321
|
+
this.router.add("GET", "/healthz", compose([async () => Response.json({ status: "ok" })]));
|
|
322
|
+
if (this.config.metrics) {
|
|
323
|
+
this.router.add(
|
|
324
|
+
"GET",
|
|
325
|
+
"/metrics",
|
|
326
|
+
compose([
|
|
327
|
+
async () => new Response(this.metrics.render(), {
|
|
328
|
+
status: 200,
|
|
329
|
+
headers: { "content-type": "text/plain; version=0.0.4" }
|
|
330
|
+
})
|
|
331
|
+
])
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
this.router.add(
|
|
335
|
+
"GET",
|
|
336
|
+
"/readyz",
|
|
337
|
+
compose([
|
|
338
|
+
async () => {
|
|
339
|
+
const report = this.checkProductionReadiness();
|
|
340
|
+
let dependencies = true;
|
|
341
|
+
if (this.config.readyProbe) {
|
|
342
|
+
try {
|
|
343
|
+
dependencies = Boolean(await this.config.readyProbe());
|
|
344
|
+
} catch {
|
|
345
|
+
dependencies = false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const ready = report.ok && dependencies;
|
|
349
|
+
return Response.json(
|
|
350
|
+
{ status: ready ? "ready" : "not_ready", dependencies, problems: report.problems },
|
|
351
|
+
{ status: ready ? 200 : 503 }
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
])
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
// Throws in production when `checkProductionReadiness()` reports any
|
|
358
|
+
// error-severity problem, unless explicitly overridden. No-op otherwise.
|
|
359
|
+
enforceProductionReadiness() {
|
|
360
|
+
if (process.env["NODE_ENV"] !== "production") return;
|
|
361
|
+
if (this.config.skipProductionReadinessGate) return;
|
|
362
|
+
const { ok, problems } = this.checkProductionReadiness();
|
|
363
|
+
if (ok) return;
|
|
364
|
+
const errors = problems.filter((p) => p.severity === "error");
|
|
365
|
+
throw new Error(
|
|
366
|
+
`[fonderie] refusing to boot in production \u2014 ${errors.length} readiness error(s): ${errors.map((e) => `${e.module}: ${e.message}`).join("; ")}. Fix them, or set skipProductionReadinessGate: true to override (not recommended).`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
239
369
|
// Runs global middleware only (no routing, no 404).
|
|
240
370
|
// Adapter packages call this to populate user/workspace/meta into their
|
|
241
371
|
// native context before handing off to user-defined route handlers.
|
|
@@ -371,6 +501,7 @@ function dateOrEmpty(value) {
|
|
|
371
501
|
export {
|
|
372
502
|
FonderieApp,
|
|
373
503
|
HTTP,
|
|
504
|
+
MetricsRegistry,
|
|
374
505
|
OPERATIONS,
|
|
375
506
|
arrayOrEmpty,
|
|
376
507
|
booleanOrFalse,
|
|
@@ -379,6 +510,7 @@ export {
|
|
|
379
510
|
defineConfig,
|
|
380
511
|
numberOrZero,
|
|
381
512
|
setApiResponse,
|
|
382
|
-
stringOrEmpty
|
|
513
|
+
stringOrEmpty,
|
|
514
|
+
withMetrics
|
|
383
515
|
};
|
|
384
516
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/constants.ts","../src/app.ts","../src/router.ts","../src/compose.ts","../src/response.ts","../src/middlewares/not-found.ts","../src/middlewares/body-parser.ts","../src/middlewares/error-handler.ts","../src/config.ts","../src/parser.ts"],"sourcesContent":["import type { Operation } from './types';\n\n// CRUD operation names shared across modules. Canonical home is core so the\n// adapters (which peer only on core) can re-export OPERATIONS without loading\n// @fonderie/permissions; permissions re-exports it for backward compatibility.\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n","import { networkInterfaces } from 'node:os';\nimport { createServer, type Server } from 'node:http';\n\nimport type {\n\tMiddleware,\n\tIFonderieApp,\n\tIFonderieContext,\n\tIFonderieModule,\n\tIReadinessProblem,\n\tIReadinessReport,\n} from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { withBody } from './middlewares/body-parser';\n\nexport class FonderieApp implements IFonderieApp {\n\tprivate config: FonderieConfig;\n\tprivate prefix: string;\n\tprivate router: Router = new Router();\n\tprivate middlewares: Middleware[] = [];\n\tprivate modules: Map<string, IFonderieModule> = new Map();\n\n\tconstructor(config: FonderieConfig) {\n\t\tthis.config = config;\n\t\tthis.prefix = (config.basePath ?? '').replace(/\\/$/, '');\n\t\tthis.middlewares = [withBody];\n\t}\n\n\tlisten(\n\t\tport: number,\n\t\toptions: {\n\t\t\tname?: string;\n\t\t\tversion?: string;\n\t\t\tenv?: string;\n\t\t\tquiet?: boolean; // suppress the startup banner (tests, quiet deploys)\n\t\t} = {},\n\t): Server {\n\t\tconst {\n\t\t\tname = 'Fonderie',\n\t\t\tversion = '0.0.1',\n\t\t\tenv = process.env['NODE_ENV'] ?? 'development',\n\t\t\tquiet = false,\n\t\t} = options;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\tconst host = req.headers.host ?? 'localhost';\n\t\t\tconst url = `http://${host}${req.url ?? '/'}`;\n\t\t\tconst headers = new Headers();\n\n\t\t\tfor (const [key, value] of Object.entries(req.headers)) {\n\t\t\t\tif (!value) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\tfor (const v of value) headers.append(key, v);\n\t\t\t\t} else {\n\t\t\t\t\theaders.set(key, value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Read the body stream — this was missing\n\t\t\tconst body = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\treq.on('data', (chunk: Buffer) => chunks.push(chunk));\n\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\treq.on('error', reject);\n\t\t\t});\n\n\t\t\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\tconst request = new Request(url, {\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: hasBody && body.length > 0 ? new Uint8Array(body) : null,\n\t\t\t});\n\n\t\t\tconst response = await this.handle(request);\n\n\t\t\tres.statusCode = response.status;\n\t\t\t// Set-Cookie must be forwarded as a LIST — forEach + setHeader would\n\t\t\t// overwrite all but the last cookie. getSetCookie() returns each intact.\n\t\t\tconst setCookies = response.headers.getSetCookie?.() ?? [];\n\t\t\tif (setCookies.length) res.setHeader('Set-Cookie', setCookies);\n\t\t\tresponse.headers.forEach((v, k) => {\n\t\t\t\tif (k.toLowerCase() !== 'set-cookie') res.setHeader(k, v);\n\t\t\t});\n\t\t\tres.end(Buffer.from(await response.arrayBuffer()));\n\t\t}).listen(port, () => {\n\t\t\tif (quiet) return;\n\t\t\tconst ip = getLocalIPv4();\n\t\t\tconst mode = env.includes('dev') ? 'development' : 'production';\n\n\t\t\tconsole.log(\n\t\t\t\t`\\n ƒ ${name} v${version} ${mode}\\n` +\n\t\t\t\t\t`\\n Local http://localhost:${port}` +\n\t\t\t\t\t`\\n Network http://${ip}:${port}\\n`,\n\t\t\t);\n\t\t});\n\t\treturn server;\n\t}\n\n\t// ─── Module registration ───────────────────────────────\n\n\tregister(module: IFonderieModule): this {\n\t\tthis.modules.set(module.name, module);\n\t\treturn this;\n\t}\n\n\t// Aggregate every registered module's self-reported readiness problems into\n\t// one report. Call it before boot to gate a deploy, or from a readiness\n\t// endpoint. `ok` is false when any module reports an `error`-severity problem\n\t// (e.g. a weak jwtSecret). Modules opt in via `checkReadiness`.\n\tcheckProductionReadiness(): IReadinessReport {\n\t\tconst problems: IReadinessProblem[] = [];\n\t\tfor (const module of this.modules.values()) {\n\t\t\tif (module.checkReadiness) problems.push(...module.checkReadiness());\n\t\t}\n\t\treturn { ok: !problems.some((p) => p.severity === 'error'), problems };\n\t}\n\n\tasync boot(): Promise<this> {\n\t\tfor (const module of topoSort([...this.modules.values()])) {\n\t\t\tawait module.install(this);\n\t\t}\n\t\treturn this;\n\t}\n\n\t// Runs global middleware only (no routing, no 404).\n\t// Adapter packages call this to populate user/workspace/meta into their\n\t// native context before handing off to user-defined route handlers.\n\tasync buildContext(request: Request): Promise<IFonderieContext> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: { _buildContext: true },\n\t\t};\n\t\tawait compose(this.middlewares)(ctx, async () => new Response());\n\t\tdelete ctx.meta['_buildContext'];\n\t\treturn ctx;\n\t}\n\n\t// ─── Middleware ────────────────────────────────────────\n\n\tuse(middleware: Middleware): this {\n\t\tthis.middlewares.push(middleware);\n\t\treturn this;\n\t}\n\n\t// Modules call this to register their routes\n\taddRoute(method: string, path: string, ...handlers: Middleware[]): void {\n\t\tthis.router.add(method, this.prefix + path, compose(handlers));\n\t}\n\n\t// ─── The core handler ──────────────────────────────────\n\t// This is the ONE thing every adapter calls.\n\t// Takes a Web Standard Request, returns a Web Standard Response.\n\n\tasync handle(request: Request): Promise<Response> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: {},\n\t\t};\n\n\t\t// Build the pipeline: global middleware → router → 404\n\t\tconst pipeline = compose([\n\t\t\t...this.middlewares,\n\t\t\trouterMiddleware(this.router),\n\t\t\tnotFoundMiddleware(),\n\t\t]);\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\tresponse = this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t\treturn this.config.onResponse ? this.transformResponse(response, request) : response;\n\t}\n\n\t// Apply config.onResponse to a JSON response body, preserving status, headers,\n\t// and cookies. Non-JSON responses and hooks that return `undefined` pass through.\n\tprivate async transformResponse(response: Response, request: Request): Promise<Response> {\n\t\tconst contentType = response.headers.get('content-type') ?? '';\n\t\tif (!contentType.includes('application/json')) return response;\n\t\tlet body: unknown;\n\t\ttry {\n\t\t\tbody = await response.clone().json();\n\t\t} catch {\n\t\t\treturn response; // not valid JSON after all — leave untouched\n\t\t}\n\t\tconst transformed = this.config.onResponse!(body, { status: response.status, request });\n\t\tif (transformed === undefined) return response;\n\t\t// Preserve headers/cookies; drop content-length (the new body sets its own).\n\t\tconst headers = new Headers(response.headers);\n\t\theaders.delete('content-length');\n\t\theaders.delete('content-type');\n\t\treturn Response.json(transformed, { status: response.status, headers });\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n","export function stringOrEmpty(value: unknown): string {\n\treturn typeof value === 'string' ? value : '';\n}\n\nexport function booleanOrFalse(value: unknown): boolean {\n\tif (typeof value === 'boolean') return value;\n\tif (value === 'true' || value === '1') return true;\n\treturn false;\n}\n\nexport function arrayOrEmpty<T>(value: unknown): T[] {\n\treturn Array.isArray(value) ? (value as T[]) : [];\n}\n\nexport function numberOrZero(value: unknown): number {\n\tconst n = Number(value);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\nexport function dateOrEmpty(value: unknown): string {\n\tif (typeof value === 'string') return value;\n\tif (value instanceof Date) return value.toISOString();\n\treturn '';\n}\n"],"mappings":";AAKO,IAAM,aAAa;AAAA,EACzB,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AACT;;;ACVA,SAAS,yBAAyB;AAClC,SAAS,oBAAiC;;;ACCnC,IAAM,SAAN,MAAgC;AAAA,EAC9B,SAAuE,CAAC;AAAA,EAEhF,IAAI,QAAgB,MAAc,SAA2B;AAC5D,SAAK,OAAO,KAAK,EAAE,QAAQ,OAAO,YAAY,GAAG,MAAM,QAAQ,CAAC;AAAA,EACjE;AAAA,EAEA,MAAM,QAAgB,MAAkC;AACvD,eAAW,SAAS,KAAK,QAAQ;AAChC,UAAI,MAAM,WAAW,OAAO,YAAY,GAAG;AAC1C;AAAA,MACD;AACA,YAAM,SAAS,UAAU,MAAM,MAAM,IAAI;AACzC,UAAI,WAAW,MAAM;AACpB,eAAO,EAAE,SAAS,MAAM,SAAS,OAAO;AAAA,MACzC;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;AAIA,SAAS,UAAU,SAAiB,MAA6C;AAChF,QAAM,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAAM,QAAQ,OAAO,EAAE,KAAK;AACjE,QAAM,KAAK,QAAQ,MAAM,GAAG;AAC5B,QAAM,KAAK,MAAM,MAAM,GAAG;AAE1B,MAAI,GAAG,WAAW,GAAG,QAAQ;AAC5B,WAAO;AAAA,EACR;AAEA,QAAM,SAAiC,CAAC;AAExC,WAAS,IAAI,GAAG,IAAI,GAAG,QAAQ,KAAK;AACnC,UAAM,KAAK,GAAG,CAAC,KAAK;AACpB,UAAM,KAAK,GAAG,CAAC,KAAK;AACpB,QAAI,GAAG,WAAW,GAAG,GAAG;AACvB,aAAO,GAAG,MAAM,CAAC,CAAC,IAAI,mBAAmB,EAAE;AAAA,IAC5C,WAAW,OAAO,IAAI;AACrB,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SAAO;AACR;AAGO,SAAS,iBAAiB,QAA4B;AAC5D,SAAO,OAAO,KAAuB,SAAS;AAC7C,UAAM,MAAM,IAAI,IAAI,IAAI,QAAQ,GAAG;AACnC,UAAM,QAAQ,OAAO,MAAM,IAAI,QAAQ,QAAQ,IAAI,QAAQ;AAE3D,QAAI,CAAC,OAAO;AACX,aAAO,KAAK;AAAA,IACb;AAGA,QAAI,KAAK,SAAS,MAAM;AACxB,WAAO,MAAM,QAAQ,KAAK,IAAI;AAAA,EAC/B;AACD;;;AC5DO,SAAS,QAAQ,aAA2B;AAClD,SAAO,SAAU,KAAuB,UAAsD;AAC7F,QAAI,QAAQ;AAEZ,aAAS,SAAS,GAA8B;AAC/C,UAAI,KAAK,OAAO;AACf,cAAM,IAAI,MAAM,8BAA8B;AAAA,MAC/C;AACA,cAAQ;AACR,YAAM,KAAK,YAAY,CAAC,KAAK;AAC7B,aAAO,GAAG,KAAK,MAAM,SAAS,IAAI,CAAC,CAAC;AAAA,IACrC;AAEA,WAAO,SAAS,CAAC;AAAA,EAClB;AACD;;;AClBO,IAAM,OAAO;AAAA,EACnB,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,MAAM;AAAA,EACN,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,qBAAqB;AACtB;AAgBO,SAAS,eACf,QACA,QACA,aACA,SACW;AACX,QAAM,OAAgC,EAAE,QAAQ,YAAY;AAC5D,MAAI,YAAY,QAAW;AAC1B,SAAK,SAAS,MAAM,WAAW,SAAS,IAAI;AAAA,EAC7C;AACA,SAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;AACtC;;;AC1CO,SAAS,qBAAiC;AAChD,SAAO,OAAO,MAAM,UAAU,eAAe,KAAK,WAAW,aAAa,WAAW;AACtF;;;ACFO,IAAM,WAAuB,OAAO,KAAK,SAAS;AACxD,QAAM,SAAS,IAAI,QAAQ,OAAO,YAAY;AAE9C,MAAI,WAAW,SAAS,WAAW,QAAQ;AAC1C,WAAO,KAAK;AAAA,EACb;AAEA,QAAM,KAAK,IAAI,QAAQ,QAAQ,IAAI,cAAc,KAAK;AAEtD,MAAI;AACH,QAAI,GAAG,SAAS,kBAAkB,GAAG;AACpC,YAAM,QAAQ,MAAM,IAAI,QAAQ,MAAM,EAAE,KAAK,GAAG,KAAK;AACrD,UAAI,KAAK,OAAO,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;AAAA,IAC5C,WAAW,GAAG,SAAS,mCAAmC,GAAG;AAC5D,YAAM,OAAO,MAAM,IAAI,QAAQ,MAAM,EAAE,KAAK;AAC5C,UAAI,KAAK,OAAO,OAAO,YAAY,IAAI,gBAAgB,IAAI,CAAC;AAAA,IAC7D;AAAA,EAED,QAAQ;AACP,WAAO,eAAe,KAAK,aAAa,mBAAmB,sBAAsB;AAAA,EAClF;AAEA,SAAO,KAAK;AACb;;;ACxBO,SAAS,oBAAoB,KAAwB;AAC3D,QAAM,MAAM,QAAQ,IAAI,UAAU,MAAM;AAExC,MAAI,eAAe,OAAO;AACzB,YAAQ,MAAM,cAAc,IAAI,SAAS,IAAI,KAAK;AAClD,WAAO;AAAA,MACN,KAAK;AAAA,MACL;AAAA,MACA,MAAM,IAAI,UAAU;AAAA,IACrB;AAAA,EACD;AAEA,UAAQ,MAAM,4BAA4B,GAAG;AAC7C,SAAO,eAAe,KAAK,cAAc,gBAAgB,uBAAuB;AACjF;;;ANCO,IAAM,cAAN,MAA0C;AAAA,EACxC;AAAA,EACA;AAAA,EACA,SAAiB,IAAI,OAAO;AAAA,EAC5B,cAA4B,CAAC;AAAA,EAC7B,UAAwC,oBAAI,IAAI;AAAA,EAExD,YAAY,QAAwB;AACnC,SAAK,SAAS;AACd,SAAK,UAAU,OAAO,YAAY,IAAI,QAAQ,OAAO,EAAE;AACvD,SAAK,cAAc,CAAC,QAAQ;AAAA,EAC7B;AAAA,EAEA,OACC,MACA,UAKI,CAAC,GACI;AACT,UAAM;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,MACV,MAAM,QAAQ,IAAI,UAAU,KAAK;AAAA,MACjC,QAAQ;AAAA,IACT,IAAI;AAEJ,UAAM,SAAS,aAAa,OAAO,KAAK,QAAQ;AAC/C,YAAM,OAAO,IAAI,QAAQ,QAAQ;AACjC,YAAM,MAAM,UAAU,IAAI,GAAG,IAAI,OAAO,GAAG;AAC3C,YAAM,UAAU,IAAI,QAAQ;AAE5B,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,OAAO,GAAG;AACvD,YAAI,CAAC,OAAO;AACX;AAAA,QACD;AAEA,YAAI,MAAM,QAAQ,KAAK,GAAG;AACzB,qBAAW,KAAK,MAAO,SAAQ,OAAO,KAAK,CAAC;AAAA,QAC7C,OAAO;AACN,kBAAQ,IAAI,KAAK,KAAK;AAAA,QACvB;AAAA,MACD;AAGA,YAAM,OAAO,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC3D,cAAM,SAAmB,CAAC;AAC1B,YAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,YAAI,GAAG,OAAO,MAAM,QAAQ,OAAO,OAAO,MAAM,CAAC,CAAC;AAClD,YAAI,GAAG,SAAS,MAAM;AAAA,MACvB,CAAC;AAED,YAAM,SAAS,IAAI,UAAU;AAC7B,YAAM,UAAU,CAAC,CAAC,OAAO,MAAM,EAAE,SAAS,OAAO,YAAY,CAAC;AAE9D,YAAM,UAAU,IAAI,QAAQ,KAAK;AAAA,QAChC;AAAA,QACA;AAAA,QACA,MAAM,WAAW,KAAK,SAAS,IAAI,IAAI,WAAW,IAAI,IAAI;AAAA,MAC3D,CAAC;AAED,YAAM,WAAW,MAAM,KAAK,OAAO,OAAO;AAE1C,UAAI,aAAa,SAAS;AAG1B,YAAM,aAAa,SAAS,QAAQ,eAAe,KAAK,CAAC;AACzD,UAAI,WAAW,OAAQ,KAAI,UAAU,cAAc,UAAU;AAC7D,eAAS,QAAQ,QAAQ,CAAC,GAAG,MAAM;AAClC,YAAI,EAAE,YAAY,MAAM,aAAc,KAAI,UAAU,GAAG,CAAC;AAAA,MACzD,CAAC;AACD,UAAI,IAAI,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC;AAAA,IAClD,CAAC,EAAE,OAAO,MAAM,MAAM;AACrB,UAAI,MAAO;AACX,YAAM,KAAK,aAAa;AACxB,YAAM,OAAO,IAAI,SAAS,KAAK,IAAI,gBAAgB;AAEnD,cAAQ;AAAA,QACP;AAAA,WAAS,IAAI,KAAK,OAAO,KAAK,IAAI;AAAA;AAAA,8BACA,IAAI;AAAA,oBACd,EAAE,IAAI,IAAI;AAAA;AAAA,MACnC;AAAA,IACD,CAAC;AACD,WAAO;AAAA,EACR;AAAA;AAAA,EAIA,SAAS,QAA+B;AACvC,SAAK,QAAQ,IAAI,OAAO,MAAM,MAAM;AACpC,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,2BAA6C;AAC5C,UAAM,WAAgC,CAAC;AACvC,eAAW,UAAU,KAAK,QAAQ,OAAO,GAAG;AAC3C,UAAI,OAAO,eAAgB,UAAS,KAAK,GAAG,OAAO,eAAe,CAAC;AAAA,IACpE;AACA,WAAO,EAAE,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,SAAS;AAAA,EACtE;AAAA,EAEA,MAAM,OAAsB;AAC3B,eAAW,UAAU,SAAS,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,GAAG;AAC1D,YAAM,OAAO,QAAQ,IAAI;AAAA,IAC1B;AACA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,SAA6C;AAC/D,UAAM,MAAwB;AAAA,MAC7B;AAAA,MACA,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,WAAW;AAAA,MACX,MAAM,EAAE,eAAe,KAAK;AAAA,IAC7B;AACA,UAAM,QAAQ,KAAK,WAAW,EAAE,KAAK,YAAY,IAAI,SAAS,CAAC;AAC/D,WAAO,IAAI,KAAK,eAAe;AAC/B,WAAO;AAAA,EACR;AAAA;AAAA,EAIA,IAAI,YAA8B;AACjC,SAAK,YAAY,KAAK,UAAU;AAChC,WAAO;AAAA,EACR;AAAA;AAAA,EAGA,SAAS,QAAgB,SAAiB,UAA8B;AACvE,SAAK,OAAO,IAAI,QAAQ,KAAK,SAAS,MAAM,QAAQ,QAAQ,CAAC;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,SAAqC;AACjD,UAAM,MAAwB;AAAA,MAC7B;AAAA,MACA,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,WAAW;AAAA,MACX,MAAM,CAAC;AAAA,IACR;AAGA,UAAM,WAAW,QAAQ;AAAA,MACxB,GAAG,KAAK;AAAA,MACR,iBAAiB,KAAK,MAAM;AAAA,MAC5B,mBAAmB;AAAA,IACpB,CAAC;AAED,QAAI;AACJ,QAAI;AACH,iBAAW,MAAM,SAAS,KAAK,YAAY,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,IACtF,SAAS,KAAK;AACb,iBAAW,KAAK,OAAO,UAAU,GAAG,KAAK,oBAAoB,GAAG;AAAA,IACjE;AACA,WAAO,KAAK,OAAO,aAAa,KAAK,kBAAkB,UAAU,OAAO,IAAI;AAAA,EAC7E;AAAA;AAAA;AAAA,EAIA,MAAc,kBAAkB,UAAoB,SAAqC;AACxF,UAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAC5D,QAAI,CAAC,YAAY,SAAS,kBAAkB,EAAG,QAAO;AACtD,QAAI;AACJ,QAAI;AACH,aAAO,MAAM,SAAS,MAAM,EAAE,KAAK;AAAA,IACpC,QAAQ;AACP,aAAO;AAAA,IACR;AACA,UAAM,cAAc,KAAK,OAAO,WAAY,MAAM,EAAE,QAAQ,SAAS,QAAQ,QAAQ,CAAC;AACtF,QAAI,gBAAgB,OAAW,QAAO;AAEtC,UAAM,UAAU,IAAI,QAAQ,SAAS,OAAO;AAC5C,YAAQ,OAAO,gBAAgB;AAC/B,YAAQ,OAAO,cAAc;AAC7B,WAAO,SAAS,KAAK,aAAa,EAAE,QAAQ,SAAS,QAAQ,QAAQ,CAAC;AAAA,EACvE;AACD;AAMA,SAAS,SAAS,SAA+C;AAChE,QAAM,SAAS,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACtD,QAAM,SAA4B,CAAC;AACnC,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,WAAW,oBAAI,IAAY;AAEjC,WAAS,MAAM,GAAoB,MAAsB;AACxD,QAAI,QAAQ,IAAI,EAAE,IAAI,EAAG;AACzB,QAAI,SAAS,IAAI,EAAE,IAAI,GAAG;AACzB,YAAM,IAAI,MAAM,mCAAmC,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,KAAK,UAAK,CAAC,EAAE;AAAA,IACnF;AACA,aAAS,IAAI,EAAE,IAAI;AACnB,eAAW,OAAO,EAAE,QAAQ,CAAC,GAAG;AAC/B,YAAM,QAAQ,OAAO,IAAI,GAAG;AAC5B,UAAI,CAAC;AACJ,cAAM,IAAI,MAAM,eAAe,EAAE,IAAI,eAAe,GAAG,4BAA4B;AACpF,YAAM,OAAO,CAAC,GAAG,MAAM,EAAE,IAAI,CAAC;AAAA,IAC/B;AACA,aAAS,OAAO,EAAE,IAAI;AACtB,YAAQ,IAAI,EAAE,IAAI;AAClB,WAAO,KAAK,CAAC;AAAA,EACd;AAEA,aAAW,KAAK,QAAS,OAAM,GAAG,CAAC,CAAC;AACpC,SAAO;AACR;AAEA,SAAS,eAAuB;AAC/B,QAAM,OAAO,kBAAkB;AAE/B,aAAW,cAAc,OAAO,OAAO,IAAI,GAAG;AAC7C,QAAI,CAAC,YAAY;AAChB;AAAA,IACD;AAEA,eAAW,SAAS,YAAY;AAC/B,UAAI,MAAM,WAAW,UAAU,CAAC,MAAM,UAAU;AAC/C,eAAO,MAAM;AAAA,MACd;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AACR;;;AO5MO,SAAS,aAAa,QAAwC;AACpE,SAAO;AACR;;;ACtDO,SAAS,cAAc,OAAwB;AACrD,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC5C;AAEO,SAAS,eAAe,OAAyB;AACvD,MAAI,OAAO,UAAU,UAAW,QAAO;AACvC,MAAI,UAAU,UAAU,UAAU,IAAK,QAAO;AAC9C,SAAO;AACR;AAEO,SAAS,aAAgB,OAAqB;AACpD,SAAO,MAAM,QAAQ,KAAK,IAAK,QAAgB,CAAC;AACjD;AAEO,SAAS,aAAa,OAAwB;AACpD,QAAM,IAAI,OAAO,KAAK;AACtB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AACjC;AAEO,SAAS,YAAY,OAAwB;AACnD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,SAAO;AACR;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/app.ts","../src/router.ts","../src/compose.ts","../src/response.ts","../src/middlewares/not-found.ts","../src/middlewares/body-parser.ts","../src/middlewares/security-headers.ts","../src/middlewares/error-handler.ts","../src/middlewares/require-admin-token.ts","../src/metrics.ts","../src/config.ts","../src/parser.ts"],"sourcesContent":["import type { Operation } from './types';\n\n// CRUD operation names shared across modules. Canonical home is core so the\n// adapters (which peer only on core) can re-export OPERATIONS without loading\n// @fonderie/permissions; permissions re-exports it for backward compatibility.\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n","import { networkInterfaces } from 'node:os';\nimport { createServer, type Server } from 'node:http';\n\nimport type {\n\tMiddleware,\n\tIFonderieApp,\n\tIFonderieContext,\n\tIFonderieModule,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\n} from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { withBody } from './middlewares/body-parser';\nimport { withSecurityHeaders } from './middlewares/security-headers';\nimport { MetricsRegistry, withMetrics } from './metrics';\n\nexport class FonderieApp implements IFonderieApp {\n\tprivate config: FonderieConfig;\n\tprivate prefix: string;\n\tprivate router: Router = new Router();\n\tprivate middlewares: Middleware[] = [];\n\tprivate modules: Map<string, IFonderieModule> = new Map();\n\treadonly metrics = new MetricsRegistry();\n\n\tconstructor(config: FonderieConfig) {\n\t\tthis.config = config;\n\t\tthis.prefix = (config.basePath ?? '').replace(/\\/$/, '');\n\t\t// Body parsing first, then baseline security headers (nosniff always; HSTS\n\t\t// over HTTPS). Apps can layer more via `.use()`.\n\t\tthis.middlewares = [withBody, withSecurityHeaders()];\n\t\tif (config.metrics) this.middlewares.push(withMetrics(this.metrics));\n\t}\n\n\tlisten(\n\t\tport: number,\n\t\toptions: {\n\t\t\tname?: string;\n\t\t\tversion?: string;\n\t\t\tenv?: string;\n\t\t\tquiet?: boolean; // suppress the startup banner (tests, quiet deploys)\n\t\t} = {},\n\t): Server {\n\t\tconst {\n\t\t\tname = 'Fonderie',\n\t\t\tversion = '0.0.1',\n\t\t\tenv = process.env['NODE_ENV'] ?? 'development',\n\t\t\tquiet = false,\n\t\t} = options;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\tconst host = req.headers.host ?? 'localhost';\n\t\t\tconst url = `http://${host}${req.url ?? '/'}`;\n\t\t\tconst headers = new Headers();\n\n\t\t\tfor (const [key, value] of Object.entries(req.headers)) {\n\t\t\t\tif (!value) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\tfor (const v of value) headers.append(key, v);\n\t\t\t\t} else {\n\t\t\t\t\theaders.set(key, value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Read the body stream — this was missing\n\t\t\tconst body = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\treq.on('data', (chunk: Buffer) => chunks.push(chunk));\n\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\treq.on('error', reject);\n\t\t\t});\n\n\t\t\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\tconst request = new Request(url, {\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: hasBody && body.length > 0 ? new Uint8Array(body) : null,\n\t\t\t});\n\n\t\t\tconst response = await this.handle(request);\n\n\t\t\tres.statusCode = response.status;\n\t\t\t// Set-Cookie must be forwarded as a LIST — forEach + setHeader would\n\t\t\t// overwrite all but the last cookie. getSetCookie() returns each intact.\n\t\t\tconst setCookies = response.headers.getSetCookie?.() ?? [];\n\t\t\tif (setCookies.length) res.setHeader('Set-Cookie', setCookies);\n\t\t\tresponse.headers.forEach((v, k) => {\n\t\t\t\tif (k.toLowerCase() !== 'set-cookie') res.setHeader(k, v);\n\t\t\t});\n\t\t\tres.end(Buffer.from(await response.arrayBuffer()));\n\t\t}).listen(port, () => {\n\t\t\tif (quiet) return;\n\t\t\tconst ip = getLocalIPv4();\n\t\t\tconst mode = env.includes('dev') ? 'development' : 'production';\n\n\t\t\tconsole.log(\n\t\t\t\t`\\n ƒ ${name} v${version} ${mode}\\n` +\n\t\t\t\t\t`\\n Local http://localhost:${port}` +\n\t\t\t\t\t`\\n Network http://${ip}:${port}\\n`,\n\t\t\t);\n\t\t});\n\t\treturn server;\n\t}\n\n\t// ─── Module registration ───────────────────────────────\n\n\tregister(module: IFonderieModule): this {\n\t\tthis.modules.set(module.name, module);\n\t\treturn this;\n\t}\n\n\t// Aggregate every registered module's self-reported readiness problems into\n\t// one report. Call it before boot to gate a deploy, or from a readiness\n\t// endpoint. `ok` is false when any module reports an `error`-severity problem\n\t// (e.g. a weak jwtSecret). Modules opt in via `checkReadiness`.\n\tcheckProductionReadiness(): IReadinessReport {\n\t\tconst problems: IReadinessProblem[] = [];\n\t\tfor (const module of this.modules.values()) {\n\t\t\tif (module.checkReadiness) problems.push(...module.checkReadiness());\n\t\t}\n\t\treturn { ok: !problems.some((p) => p.severity === 'error'), problems };\n\t}\n\n\t// A point-in-time control-posture snapshot for SOC 2 evidence: which modules\n\t// are registered and the current readiness report. Serialise to a file/log\n\t// (e.g. on a schedule) as an audit artifact.\n\tsecurityReport(): ISecurityReport {\n\t\treturn {\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t\tenv: process.env['NODE_ENV'] ?? 'development',\n\t\t\tregisteredModules: [...this.modules.keys()].sort(),\n\t\t\treadiness: this.checkProductionReadiness(),\n\t\t};\n\t}\n\n\tasync boot(): Promise<this> {\n\t\t// Fail closed before any side effects (transports, listeners): a\n\t\t// production deploy with an error-severity readiness problem must not boot.\n\t\tthis.enforceProductionReadiness();\n\t\tfor (const module of topoSort([...this.modules.values()])) {\n\t\t\tawait module.install(this);\n\t\t}\n\t\tthis.registerHealthRoutes();\n\t\treturn this;\n\t}\n\n\t// Liveness (/healthz) and readiness (/readyz) probes. Registered unprefixed so\n\t// they sit at a stable path regardless of basePath. Enabled unless disabled.\n\tprivate registerHealthRoutes(): void {\n\t\tif (this.config.healthChecks === false) return;\n\n\t\tthis.router.add('GET', '/healthz', compose([async () => Response.json({ status: 'ok' })]));\n\n\t\tif (this.config.metrics) {\n\t\t\tthis.router.add(\n\t\t\t\t'GET',\n\t\t\t\t'/metrics',\n\t\t\t\tcompose([\n\t\t\t\t\tasync () =>\n\t\t\t\t\t\tnew Response(this.metrics.render(), {\n\t\t\t\t\t\t\tstatus: 200,\n\t\t\t\t\t\t\theaders: { 'content-type': 'text/plain; version=0.0.4' },\n\t\t\t\t\t\t}),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tthis.router.add(\n\t\t\t'GET',\n\t\t\t'/readyz',\n\t\t\tcompose([\n\t\t\t\tasync () => {\n\t\t\t\t\tconst report = this.checkProductionReadiness();\n\t\t\t\t\tlet dependencies = true;\n\t\t\t\t\tif (this.config.readyProbe) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdependencies = Boolean(await this.config.readyProbe());\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tdependencies = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst ready = report.ok && dependencies;\n\t\t\t\t\treturn Response.json(\n\t\t\t\t\t\t{ status: ready ? 'ready' : 'not_ready', dependencies, problems: report.problems },\n\t\t\t\t\t\t{ status: ready ? 200 : 503 },\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t]),\n\t\t);\n\t}\n\n\t// Throws in production when `checkProductionReadiness()` reports any\n\t// error-severity problem, unless explicitly overridden. No-op otherwise.\n\tprivate enforceProductionReadiness(): void {\n\t\tif (process.env['NODE_ENV'] !== 'production') return;\n\t\tif (this.config.skipProductionReadinessGate) return;\n\t\tconst { ok, problems } = this.checkProductionReadiness();\n\t\tif (ok) return;\n\t\tconst errors = problems.filter((p) => p.severity === 'error');\n\t\tthrow new Error(\n\t\t\t`[fonderie] refusing to boot in production — ${errors.length} readiness ` +\n\t\t\t\t`error(s): ${errors.map((e) => `${e.module}: ${e.message}`).join('; ')}. ` +\n\t\t\t\t'Fix them, or set skipProductionReadinessGate: true to override (not recommended).',\n\t\t);\n\t}\n\n\t// Runs global middleware only (no routing, no 404).\n\t// Adapter packages call this to populate user/workspace/meta into their\n\t// native context before handing off to user-defined route handlers.\n\tasync buildContext(request: Request): Promise<IFonderieContext> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: { _buildContext: true },\n\t\t};\n\t\tawait compose(this.middlewares)(ctx, async () => new Response());\n\t\tdelete ctx.meta['_buildContext'];\n\t\treturn ctx;\n\t}\n\n\t// ─── Middleware ────────────────────────────────────────\n\n\tuse(middleware: Middleware): this {\n\t\tthis.middlewares.push(middleware);\n\t\treturn this;\n\t}\n\n\t// Modules call this to register their routes\n\taddRoute(method: string, path: string, ...handlers: Middleware[]): void {\n\t\tthis.router.add(method, this.prefix + path, compose(handlers));\n\t}\n\n\t// ─── The core handler ──────────────────────────────────\n\t// This is the ONE thing every adapter calls.\n\t// Takes a Web Standard Request, returns a Web Standard Response.\n\n\tasync handle(request: Request): Promise<Response> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: {},\n\t\t};\n\n\t\t// Build the pipeline: global middleware → router → 404\n\t\tconst pipeline = compose([\n\t\t\t...this.middlewares,\n\t\t\trouterMiddleware(this.router),\n\t\t\tnotFoundMiddleware(),\n\t\t]);\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\tresponse = this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t\treturn this.config.onResponse ? this.transformResponse(response, request) : response;\n\t}\n\n\t// Apply config.onResponse to a JSON response body, preserving status, headers,\n\t// and cookies. Non-JSON responses and hooks that return `undefined` pass through.\n\tprivate async transformResponse(response: Response, request: Request): Promise<Response> {\n\t\tconst contentType = response.headers.get('content-type') ?? '';\n\t\tif (!contentType.includes('application/json')) return response;\n\t\tlet body: unknown;\n\t\ttry {\n\t\t\tbody = await response.clone().json();\n\t\t} catch {\n\t\t\treturn response; // not valid JSON after all — leave untouched\n\t\t}\n\t\tconst transformed = this.config.onResponse!(body, { status: response.status, request });\n\t\tif (transformed === undefined) return response;\n\t\t// Preserve headers/cookies; drop content-length (the new body sets its own).\n\t\tconst headers = new Headers(response.headers);\n\t\theaders.delete('content-length');\n\t\theaders.delete('content-type');\n\t\treturn Response.json(transformed, { status: response.status, headers });\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import type { Middleware } from '../types';\n\nexport interface SecurityHeadersOptions {\n\t// HSTS max-age in seconds. Default 180 days. Set 0 to omit the header.\n\thstsMaxAge?: number;\n\t// Add `includeSubDomains` to the HSTS header. Off by default — only enable\n\t// once every subdomain is known to serve HTTPS.\n\thstsIncludeSubDomains?: boolean;\n\t// Add `preload` to the HSTS header (implies includeSubDomains). Off by default.\n\thstsPreload?: boolean;\n}\n\n// Baseline response hardening. `X-Content-Type-Options: nosniff` is always safe.\n// HSTS is only meaningful — and only emitted — over HTTPS: browsers ignore it on\n// plain HTTP, so gating on the effective scheme keeps local http/dev untouched\n// while enforcing TLS in production (behind a TLS-terminating proxy, detected via\n// X-Forwarded-Proto). Wired into the default pipeline by FonderieApp.\nexport function withSecurityHeaders(options: SecurityHeadersOptions = {}): Middleware {\n\tconst {\n\t\thstsMaxAge = 60 * 60 * 24 * 180,\n\t\thstsIncludeSubDomains = false,\n\t\thstsPreload = false,\n\t} = options;\n\n\tlet hsts = '';\n\tif (hstsMaxAge > 0) {\n\t\thsts = `max-age=${hstsMaxAge}`;\n\t\tif (hstsIncludeSubDomains || hstsPreload) hsts += '; includeSubDomains';\n\t\tif (hstsPreload) hsts += '; preload';\n\t}\n\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst patched = new Headers(response.headers);\n\n\t\tpatched.set('X-Content-Type-Options', 'nosniff');\n\n\t\tif (hsts && isHttps(ctx.request)) {\n\t\t\tpatched.set('Strict-Transport-Security', hsts);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n\n// HTTPS if the request URL is https, or a TLS-terminating proxy says so.\nfunction isHttps(request: Request): boolean {\n\tif (request.url.startsWith('https:')) return true;\n\tconst proto = request.headers.get('x-forwarded-proto');\n\treturn proto?.split(',')[0]?.trim() === 'https';\n}\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { timingSafeEqual } from 'node:crypto';\n\nimport { setApiResponse, HTTP } from '../response';\nimport type { IReadinessProblem, Middleware } from '../types';\n\n// The one admin-route guard for every Fonderie module with an ops surface\n// (billing plan-writes/wallet-grant, config/secrets admin, courier template\n// admin). A bootstrap Bearer token compared in constant time. Modules MUST\n// register their admin routes only when a token is configured (unset ⇒ 404),\n// so this guard only ever runs against a real token. See docs/ADMIN-AUTH-SPEC.md.\n\n// Constant-time comparison so a wrong token can't be recovered byte-by-byte from\n// response timing. Length-guard first: timingSafeEqual throws on unequal lengths,\n// and that early return is acceptable — the token's length is not the secret.\nfunction safeTokenEqual(a: string, b: string): boolean {\n\tconst bufA = Buffer.from(a);\n\tconst bufB = Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n\nexport function requireAdminToken(adminToken: string): Middleware {\n\treturn (ctx, next) => {\n\t\tconst header = ctx.request.headers.get('authorization') ?? '';\n\t\tconst token = header.startsWith('Bearer ') ? header.slice(7) : '';\n\t\t// Same 401 for a missing and a wrong token — no oracle distinguishing them.\n\t\tif (!token || !safeTokenEqual(token, adminToken)) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token'),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n\n// A bootstrap admin token must be strong: an admin surface guarded by a short or\n// placeholder token is barely guarded at all. Modules call this from\n// checkReadiness() so the rule is enforced identically everywhere (previously\n// only @fonderie/config validated it). Returns a problem for a weak/placeholder\n// token; nothing when unset (that surface is simply not exposed).\nconst MIN_ADMIN_TOKEN_LENGTH = 32;\nconst PLACEHOLDER_TOKEN =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\nexport function validateAdminToken(\n\ttoken: string | undefined,\n\topts: { module: string },\n): IReadinessProblem[] {\n\tif (!token) return [];\n\tif (token.length < MIN_ADMIN_TOKEN_LENGTH) {\n\t\treturn [\n\t\t\t{\n\t\t\t\tmodule: opts.module,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: `adminToken must be at least ${MIN_ADMIN_TOKEN_LENGTH} characters (got ${token.length})`,\n\t\t\t},\n\t\t];\n\t}\n\tif (PLACEHOLDER_TOKEN.test(token)) {\n\t\treturn [\n\t\t\t{ module: opts.module, severity: 'error', message: 'adminToken looks like a placeholder or dev-default value' },\n\t\t];\n\t}\n\treturn [];\n}\n","import type { Middleware } from './types';\n\n// Minimal, dependency-free metrics (SOC 2 CC7.2). Counts HTTP requests by\n// status class and exposes them in Prometheus text format at /metrics (opt-in\n// via config.metrics). Apps can also record custom counters. Not a full metrics\n// system — enough to alert on error rate and traffic without pulling a client.\n\nexport class MetricsRegistry {\n\tprivate counters = new Map<string, number>();\n\n\tinc(name: string, labels: Record<string, string> = {}, by = 1): void {\n\t\tconst key = seriesKey(name, labels);\n\t\tthis.counters.set(key, (this.counters.get(key) ?? 0) + by);\n\t}\n\n\t// Prometheus text exposition format.\n\trender(): string {\n\t\tconst lines: string[] = [];\n\t\tfor (const [key, value] of this.counters) lines.push(`${key} ${value}`);\n\t\treturn lines.join('\\n') + (lines.length ? '\\n' : '');\n\t}\n}\n\nfunction seriesKey(name: string, labels: Record<string, string>): string {\n\tconst parts = Object.entries(labels).map(([k, v]) => `${k}=\"${String(v).replace(/\"/g, '')}\"`);\n\treturn parts.length ? `${name}{${parts.join(',')}}` : name;\n}\n\n// Middleware that records one `http_requests_total{status_class}` per response.\nexport function withMetrics(registry: MetricsRegistry): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst cls = `${Math.floor(response.status / 100)}xx`;\n\t\tregistry.inc('http_requests_total', { status_class: cls });\n\t\treturn response;\n\t};\n}\n","export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\t// Fail-closed production boot gate. In production, `boot()` refuses to start\n\t// when any registered module reports an `error`-severity readiness problem\n\t// (e.g. a weak signing secret, a missing at-rest encryption key). Set this to\n\t// true only to deliberately override the gate — not recommended. Outside\n\t// production the gate never runs.\n\tskipProductionReadinessGate?: boolean;\n\n\t// Built-in health endpoints, registered on boot (unprefixed by basePath):\n\t// GET /healthz — liveness, always 200 while the process is up\n\t// GET /readyz — readiness, 200 when checkProductionReadiness() is ok AND\n\t// readyProbe() (if provided) resolves truthy, else 503\n\t// Set false to disable. Point your platform's probes at these.\n\thealthChecks?: boolean;\n\t// Optional dependency probe for /readyz — e.g. `() => store.testConnection()`.\n\t// Throwing or returning false makes /readyz report 503.\n\treadyProbe?: () => boolean | Promise<boolean>;\n\n\t// Enable Prometheus-format metrics: counts requests by status class and\n\t// serves them at GET /metrics (unprefixed). Off by default. Access to\n\t// /metrics should be restricted at your ingress/scraper.\n\tmetrics?: boolean;\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n","export function stringOrEmpty(value: unknown): string {\n\treturn typeof value === 'string' ? value : '';\n}\n\nexport function booleanOrFalse(value: unknown): boolean {\n\tif (typeof value === 'boolean') return value;\n\tif (value === 'true' || value === '1') return true;\n\treturn false;\n}\n\nexport function arrayOrEmpty<T>(value: unknown): T[] {\n\treturn Array.isArray(value) ? (value as T[]) : [];\n}\n\nexport function numberOrZero(value: unknown): number {\n\tconst n = Number(value);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\nexport function dateOrEmpty(value: unknown): string {\n\tif (typeof value === 'string') return value;\n\tif (value instanceof Date) return value.toISOString();\n\treturn '';\n}\n"],"mappings":"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","names":[]}
|
|
@@ -23,14 +23,17 @@ __export(middlewares_exports, {
|
|
|
23
23
|
checkProxyConfig: () => checkProxyConfig,
|
|
24
24
|
defaultErrorHandler: () => defaultErrorHandler,
|
|
25
25
|
notFoundMiddleware: () => notFoundMiddleware,
|
|
26
|
+
requireAdminToken: () => requireAdminToken,
|
|
26
27
|
requireAnyAuth: () => requireAnyAuth,
|
|
27
28
|
requireAuth: () => requireAuth,
|
|
28
29
|
requireVerified: () => requireVerified,
|
|
29
30
|
resolveClientIp: () => resolveClientIp,
|
|
30
31
|
validate: () => validate,
|
|
32
|
+
validateAdminToken: () => validateAdminToken,
|
|
31
33
|
withBody: () => withBody,
|
|
32
34
|
withCors: () => withCors,
|
|
33
|
-
withLogger: () => withLogger
|
|
35
|
+
withLogger: () => withLogger,
|
|
36
|
+
withSecurityHeaders: () => withSecurityHeaders
|
|
34
37
|
});
|
|
35
38
|
module.exports = __toCommonJS(middlewares_exports);
|
|
36
39
|
|
|
@@ -138,6 +141,39 @@ var withBody = async (ctx, next) => {
|
|
|
138
141
|
return next();
|
|
139
142
|
};
|
|
140
143
|
|
|
144
|
+
// src/middlewares/security-headers.ts
|
|
145
|
+
function withSecurityHeaders(options = {}) {
|
|
146
|
+
const {
|
|
147
|
+
hstsMaxAge = 60 * 60 * 24 * 180,
|
|
148
|
+
hstsIncludeSubDomains = false,
|
|
149
|
+
hstsPreload = false
|
|
150
|
+
} = options;
|
|
151
|
+
let hsts = "";
|
|
152
|
+
if (hstsMaxAge > 0) {
|
|
153
|
+
hsts = `max-age=${hstsMaxAge}`;
|
|
154
|
+
if (hstsIncludeSubDomains || hstsPreload) hsts += "; includeSubDomains";
|
|
155
|
+
if (hstsPreload) hsts += "; preload";
|
|
156
|
+
}
|
|
157
|
+
return async (ctx, next) => {
|
|
158
|
+
const response = await next();
|
|
159
|
+
const patched = new Headers(response.headers);
|
|
160
|
+
patched.set("X-Content-Type-Options", "nosniff");
|
|
161
|
+
if (hsts && isHttps(ctx.request)) {
|
|
162
|
+
patched.set("Strict-Transport-Security", hsts);
|
|
163
|
+
}
|
|
164
|
+
return new Response(response.body, {
|
|
165
|
+
headers: patched,
|
|
166
|
+
status: response.status,
|
|
167
|
+
statusText: response.statusText
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function isHttps(request) {
|
|
172
|
+
if (request.url.startsWith("https:")) return true;
|
|
173
|
+
const proto = request.headers.get("x-forwarded-proto");
|
|
174
|
+
return proto?.split(",")[0]?.trim() === "https";
|
|
175
|
+
}
|
|
176
|
+
|
|
141
177
|
// src/middlewares/error-handler.ts
|
|
142
178
|
function defaultErrorHandler(err) {
|
|
143
179
|
const dev = process.env["NODE_ENV"] !== "production";
|
|
@@ -170,6 +206,47 @@ var requireAnyAuth = async (ctx, next) => {
|
|
|
170
206
|
return next();
|
|
171
207
|
};
|
|
172
208
|
|
|
209
|
+
// src/middlewares/require-admin-token.ts
|
|
210
|
+
var import_node_crypto = require("crypto");
|
|
211
|
+
function safeTokenEqual(a, b) {
|
|
212
|
+
const bufA = Buffer.from(a);
|
|
213
|
+
const bufB = Buffer.from(b);
|
|
214
|
+
if (bufA.length !== bufB.length) return false;
|
|
215
|
+
return (0, import_node_crypto.timingSafeEqual)(bufA, bufB);
|
|
216
|
+
}
|
|
217
|
+
function requireAdminToken(adminToken) {
|
|
218
|
+
return (ctx, next) => {
|
|
219
|
+
const header = ctx.request.headers.get("authorization") ?? "";
|
|
220
|
+
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
221
|
+
if (!token || !safeTokenEqual(token, adminToken)) {
|
|
222
|
+
return Promise.resolve(
|
|
223
|
+
setApiResponse(HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Missing or invalid admin token")
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return next();
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
var MIN_ADMIN_TOKEN_LENGTH = 32;
|
|
230
|
+
var PLACEHOLDER_TOKEN = /dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;
|
|
231
|
+
function validateAdminToken(token, opts) {
|
|
232
|
+
if (!token) return [];
|
|
233
|
+
if (token.length < MIN_ADMIN_TOKEN_LENGTH) {
|
|
234
|
+
return [
|
|
235
|
+
{
|
|
236
|
+
module: opts.module,
|
|
237
|
+
severity: "error",
|
|
238
|
+
message: `adminToken must be at least ${MIN_ADMIN_TOKEN_LENGTH} characters (got ${token.length})`
|
|
239
|
+
}
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
if (PLACEHOLDER_TOKEN.test(token)) {
|
|
243
|
+
return [
|
|
244
|
+
{ module: opts.module, severity: "error", message: "adminToken looks like a placeholder or dev-default value" }
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
|
|
173
250
|
// src/middlewares/require-verified.ts
|
|
174
251
|
var requireVerified = async (ctx, next) => {
|
|
175
252
|
if (!ctx.user) {
|
|
@@ -265,13 +342,16 @@ function checkProxyConfig(socketAddress, headers, trustProxy) {
|
|
|
265
342
|
checkProxyConfig,
|
|
266
343
|
defaultErrorHandler,
|
|
267
344
|
notFoundMiddleware,
|
|
345
|
+
requireAdminToken,
|
|
268
346
|
requireAnyAuth,
|
|
269
347
|
requireAuth,
|
|
270
348
|
requireVerified,
|
|
271
349
|
resolveClientIp,
|
|
272
350
|
validate,
|
|
351
|
+
validateAdminToken,
|
|
273
352
|
withBody,
|
|
274
353
|
withCors,
|
|
275
|
-
withLogger
|
|
354
|
+
withLogger,
|
|
355
|
+
withSecurityHeaders
|
|
276
356
|
});
|
|
277
357
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/cors.ts","../../src/middlewares/logger.ts","../../src/response.ts","../../src/middlewares/not-found.ts","../../src/middlewares/body-parser.ts","../../src/middlewares/error-handler.ts","../../src/middlewares/require-auth.ts","../../src/middlewares/require-verified.ts","../../src/middlewares/validate.ts","../../src/middlewares/client-ip.ts"],"sourcesContent":["export type { CorsOptions } from './cors';\nexport { withCors } from './cors';\nexport { withLogger } from './logger';\nexport { notFoundMiddleware } from './not-found';\nexport { withBody } from './body-parser';\nexport { defaultErrorHandler } from './error-handler';\nexport { requireAuth, requireAnyAuth } from './require-auth';\nexport { requireVerified } from './require-verified';\nexport { validate } from './validate';\nexport type { IRequestSchema } from './validate';\nexport { resolveClientIp, checkProxyConfig } from './client-ip';\n","import type { Middleware } from '../types';\n\nexport interface CorsOptions {\n\tmethods?: string[];\n\theaders?: string[];\n\torigin?: string | ((requestOrigin: string) => boolean);\n}\n\nexport function withCors(options: CorsOptions = {}): Middleware {\n\tconst {\n\t\torigin = '*',\n\t\theaders = ['Content-Type', 'Authorization'],\n\t\tmethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n\t} = options;\n\n\treturn async (ctx, next) => {\n\t\tconst requestOrigin = ctx.request.headers.get('origin') ?? '';\n\n\t\tconst allowOrigin =\n\t\t\ttypeof origin === 'function' ? (origin(requestOrigin) ? requestOrigin : '') : origin;\n\n\t\tconst corsHeaders: Record<string, string> = {\n\t\t\t'Access-Control-Max-Age': '86400',\n\t\t\t'Access-Control-Allow-Origin': allowOrigin,\n\t\t\t'Access-Control-Allow-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\n\t\t// Preflight — respond immediately, skip the pipeline\n\t\tif (ctx.request.method === 'OPTIONS') {\n\t\t\treturn new Response(null, { status: 204, headers: corsHeaders });\n\t\t}\n\n\t\tconst response = await next();\n\n\t\tconst patched = new Headers(response.headers);\n\n\t\tfor (const [k, v] of Object.entries(corsHeaders)) {\n\t\t\tpatched.set(k, v);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n","import type { Middleware } from '../types';\n\nexport const withLogger: Middleware = async (ctx, next) => {\n\tconst start = Date.now();\n\n\tconst { method, url } = ctx.request;\n\tconst { pathname } = new URL(url);\n\n\tconst response = await next();\n\n\tconsole.log(\n\t\tJSON.stringify({\n\t\t\tts: new Date().toISOString(),\n\t\t\tmethod,\n\t\t\tpath: pathname,\n\t\t\tstatus: response.status,\n\t\t\tms: Date.now() - start,\n\t\t}),\n\t);\n\n\treturn response;\n};\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Requires a fully-authenticated user. Rejects mfaPending tokens — those are\n// short-lived pre-auth tokens issued mid-MFA-login and must not grant access\n// to any route other than /auth/mfa/verify.\nexport const requireAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\tif (ctx.user.mfaPending) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t}\n\treturn next();\n};\n\n// Accepts both fully-authenticated and mfaPending tokens. Only for routes that\n// need to serve both contexts on the same path (e.g. POST /auth/mfa/verify\n// handles setup confirmation with a full token and TOTP login with mfaPending).\nexport const requireAnyAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport const requireVerified: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\n\tif (ctx.user.loginMethod === 'phone') {\n\t\tif (!ctx.user.phoneVerified) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'PHONE_NOT_VERIFIED',\n\t\t\t\t'Please verify your phone number',\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t}\n\n\tif (!ctx.user.emailVerifiedAt) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'EMAIL_NOT_VERIFIED', 'Please verify your email address');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Route-boundary request validation — the one validation middleware every\n// package wires in front of its body-taking routes, so error shape and parse\n// semantics are identical across the whole surface.\n//\n// core stays dependency-free: this accepts anything implementing zod's\n// safeParse contract structurally (zod v3/v4 both match), without importing\n// zod. Feature packages own their schemas; see @fonderie/auth's schemas.ts\n// for the reference pattern.\n\nexport interface IRequestSchema {\n\tsafeParse(input: unknown):\n\t\t| { success: true; data: unknown }\n\t\t| { success: false; error: { issues: Array<{ path: PropertyKey[]; message: string }> } };\n}\n\nexport function validate(schema: IRequestSchema): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst result = schema.safeParse(ctx.meta['body'] ?? {});\n\t\tif (!result.success) {\n\t\t\tconst first = result.error.issues[0];\n\t\t\tconst path = first?.path.length ? `${first.path.map(String).join('.')}: ` : '';\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t`${path}${first?.message ?? 'Invalid request body'}`,\n\t\t\t);\n\t\t}\n\t\t// Parsed output replaces the raw body: trimmed, coerced, unknown keys\n\t\t// stripped — controllers read clean input.\n\t\tctx.meta['body'] = result.data;\n\t\treturn next();\n\t};\n}\n","// Client-IP resolution shared by the adapters. The web-standard Request the\n// pipeline runs on carries no socket address, so each adapter passes the\n// socket's remote address here together with the headers; this resolves the\n// effective client IP with explicit proxy trust.\n//\n// trustProxy semantics (deliberately explicit — a permissive default lets\n// any client spoof X-Forwarded-For and dodge per-IP rate limits):\n// 0 / undefined → ignore forwarding headers; the socket address is the client\n// N > 0 → the client is the Nth-from-the-right entry in\n// X-Forwarded-For (N = number of trusted proxy hops)\n//\n// ⚠️ THE PROXY FOOTGUN. With trustProxy=0 (the spoof-safe default) deployed\n// behind nginx, a Kubernetes ingress, or any L7 proxy, the socket address is\n// the PROXY's IP for every request — so every client collapses onto one\n// per-IP bucket and the limit becomes global (one attacker locks everyone\n// out). You cannot have a default that is both spoof-safe AND correct behind\n// a proxy; they contradict. So we ship spoof-safe and DETECT the mismatch:\n// checkProxyConfig() below warns once, loudly, when the deployment looks\n// proxied but trustProxy is unset. Set TRUST_PROXY=<hops> in that case.\n\nexport function resolveClientIp(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number = trustProxyFromEnv(),\n): string | undefined {\n\tif (trustProxy > 0) {\n\t\tconst xff = headers.get('x-forwarded-for');\n\t\tif (xff) {\n\t\t\tconst hops = xff\n\t\t\t\t.split(',')\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter(Boolean);\n\t\t\tconst candidate = hops[Math.max(0, hops.length - trustProxy)];\n\t\t\tif (candidate) return normalizeIp(candidate);\n\t\t}\n\t}\n\tcheckProxyConfig(socketAddress, headers, trustProxy);\n\treturn socketAddress ? normalizeIp(socketAddress) : undefined;\n}\n\nfunction trustProxyFromEnv(): number {\n\tconst raw = Number(process.env['TRUST_PROXY']);\n\treturn Number.isFinite(raw) && raw > 0 ? raw : 0;\n}\n\nfunction normalizeIp(ip: string): string {\n\t// ::ffff:203.0.113.7 → 203.0.113.7 ; strip port if a proxy appended one\n\tconst noV6Prefix = ip.startsWith('::ffff:') ? ip.slice(7) : ip;\n\tconst m = noV6Prefix.match(/^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/);\n\treturn m ? m[1]! : noV6Prefix;\n}\n\n// Address ranges that indicate a local proxy sits in front of us (so a\n// forwarding header without TRUST_PROXY is a misconfiguration, not spoofing).\nconst LOOPBACK_IPS = new Set(['127.0.0.1', '::1']);\nconst PRIVATE_IP_PREFIXES = [\n\t'10.', // RFC1918\n\t'192.168.', // RFC1918\n\t'169.254.', // link-local\n\t'fc', // IPv6 unique local (fc00::/7)\n\t'fd', // IPv6 unique local\n] as const;\nconst CGNAT_OR_RFC1918_172 = /^172\\.(1[6-9]|2\\d|3[01])\\./; // 172.16.0.0/12\n\nfunction isPrivateOrLoopback(ip: string): boolean {\n\tconst a = normalizeIp(ip);\n\treturn (\n\t\tLOOPBACK_IPS.has(a) ||\n\t\tCGNAT_OR_RFC1918_172.test(a) ||\n\t\tPRIVATE_IP_PREFIXES.some((prefix) => a.startsWith(prefix))\n\t);\n}\n\nlet warned = false;\n\n// Warn ONCE when the deployment looks proxied (forwarding header present, and\n// the socket is a private/loopback address — i.e. a local proxy) but\n// trustProxy is unset. That configuration silently rate-limits every client\n// as one IP. Emitting on the request path (not at boot) is deliberate: the\n// signal we need — an actual X-Forwarded-For header — only exists once real\n// traffic arrives.\nexport function checkProxyConfig(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number,\n): void {\n\tif (warned || trustProxy > 0) return;\n\tconst forwarded =\n\t\theaders.get('x-forwarded-for') ??\n\t\theaders.get('cf-connecting-ip') ??\n\t\theaders.get('x-real-ip');\n\tif (forwarded && socketAddress && isPrivateOrLoopback(socketAddress)) {\n\t\twarned = true;\n\t\tconsole.warn(\n\t\t\t'[fonderie] Requests carry a forwarding header (X-Forwarded-For) and ' +\n\t\t\t\t'arrive from a private/loopback socket, but TRUST_PROXY is unset. ' +\n\t\t\t\t'Every client is being rate-limited as a single IP, which will cause ' +\n\t\t\t\t'global lockout behind nginx / a Kubernetes ingress / any L7 proxy. ' +\n\t\t\t\t'Set TRUST_PROXY=<number of trusted proxy hops>. ' +\n\t\t\t\t'See @fonderie/rate-limit README § Deploying behind a proxy.',\n\t\t);\n\t}\n}\n\n// Test seam — reset the once-only warning latch.\nexport function _resetProxyWarning(): void {\n\twarned = false;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/cors.ts","../../src/middlewares/logger.ts","../../src/response.ts","../../src/middlewares/not-found.ts","../../src/middlewares/body-parser.ts","../../src/middlewares/security-headers.ts","../../src/middlewares/error-handler.ts","../../src/middlewares/require-auth.ts","../../src/middlewares/require-admin-token.ts","../../src/middlewares/require-verified.ts","../../src/middlewares/validate.ts","../../src/middlewares/client-ip.ts"],"sourcesContent":["export type { CorsOptions } from './cors';\nexport { withCors } from './cors';\nexport { withLogger } from './logger';\nexport { notFoundMiddleware } from './not-found';\nexport { withBody } from './body-parser';\nexport { withSecurityHeaders } from './security-headers';\nexport type { SecurityHeadersOptions } from './security-headers';\nexport { defaultErrorHandler } from './error-handler';\nexport { requireAuth, requireAnyAuth } from './require-auth';\nexport { requireAdminToken, validateAdminToken } from './require-admin-token';\nexport { requireVerified } from './require-verified';\nexport { validate } from './validate';\nexport type { IRequestSchema } from './validate';\nexport { resolveClientIp, checkProxyConfig } from './client-ip';\n","import type { Middleware } from '../types';\n\nexport interface CorsOptions {\n\tmethods?: string[];\n\theaders?: string[];\n\torigin?: string | ((requestOrigin: string) => boolean);\n}\n\nexport function withCors(options: CorsOptions = {}): Middleware {\n\tconst {\n\t\torigin = '*',\n\t\theaders = ['Content-Type', 'Authorization'],\n\t\tmethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n\t} = options;\n\n\treturn async (ctx, next) => {\n\t\tconst requestOrigin = ctx.request.headers.get('origin') ?? '';\n\n\t\tconst allowOrigin =\n\t\t\ttypeof origin === 'function' ? (origin(requestOrigin) ? requestOrigin : '') : origin;\n\n\t\tconst corsHeaders: Record<string, string> = {\n\t\t\t'Access-Control-Max-Age': '86400',\n\t\t\t'Access-Control-Allow-Origin': allowOrigin,\n\t\t\t'Access-Control-Allow-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\n\t\t// Preflight — respond immediately, skip the pipeline\n\t\tif (ctx.request.method === 'OPTIONS') {\n\t\t\treturn new Response(null, { status: 204, headers: corsHeaders });\n\t\t}\n\n\t\tconst response = await next();\n\n\t\tconst patched = new Headers(response.headers);\n\n\t\tfor (const [k, v] of Object.entries(corsHeaders)) {\n\t\t\tpatched.set(k, v);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n","import type { Middleware } from '../types';\n\nexport const withLogger: Middleware = async (ctx, next) => {\n\tconst start = Date.now();\n\n\tconst { method, url } = ctx.request;\n\tconst { pathname } = new URL(url);\n\n\tconst response = await next();\n\n\tconsole.log(\n\t\tJSON.stringify({\n\t\t\tts: new Date().toISOString(),\n\t\t\tmethod,\n\t\t\tpath: pathname,\n\t\t\tstatus: response.status,\n\t\t\tms: Date.now() - start,\n\t\t}),\n\t);\n\n\treturn response;\n};\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import type { Middleware } from '../types';\n\nexport interface SecurityHeadersOptions {\n\t// HSTS max-age in seconds. Default 180 days. Set 0 to omit the header.\n\thstsMaxAge?: number;\n\t// Add `includeSubDomains` to the HSTS header. Off by default — only enable\n\t// once every subdomain is known to serve HTTPS.\n\thstsIncludeSubDomains?: boolean;\n\t// Add `preload` to the HSTS header (implies includeSubDomains). Off by default.\n\thstsPreload?: boolean;\n}\n\n// Baseline response hardening. `X-Content-Type-Options: nosniff` is always safe.\n// HSTS is only meaningful — and only emitted — over HTTPS: browsers ignore it on\n// plain HTTP, so gating on the effective scheme keeps local http/dev untouched\n// while enforcing TLS in production (behind a TLS-terminating proxy, detected via\n// X-Forwarded-Proto). Wired into the default pipeline by FonderieApp.\nexport function withSecurityHeaders(options: SecurityHeadersOptions = {}): Middleware {\n\tconst {\n\t\thstsMaxAge = 60 * 60 * 24 * 180,\n\t\thstsIncludeSubDomains = false,\n\t\thstsPreload = false,\n\t} = options;\n\n\tlet hsts = '';\n\tif (hstsMaxAge > 0) {\n\t\thsts = `max-age=${hstsMaxAge}`;\n\t\tif (hstsIncludeSubDomains || hstsPreload) hsts += '; includeSubDomains';\n\t\tif (hstsPreload) hsts += '; preload';\n\t}\n\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst patched = new Headers(response.headers);\n\n\t\tpatched.set('X-Content-Type-Options', 'nosniff');\n\n\t\tif (hsts && isHttps(ctx.request)) {\n\t\t\tpatched.set('Strict-Transport-Security', hsts);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n\n// HTTPS if the request URL is https, or a TLS-terminating proxy says so.\nfunction isHttps(request: Request): boolean {\n\tif (request.url.startsWith('https:')) return true;\n\tconst proto = request.headers.get('x-forwarded-proto');\n\treturn proto?.split(',')[0]?.trim() === 'https';\n}\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Requires a fully-authenticated user. Rejects mfaPending tokens — those are\n// short-lived pre-auth tokens issued mid-MFA-login and must not grant access\n// to any route other than /auth/mfa/verify.\nexport const requireAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\tif (ctx.user.mfaPending) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t}\n\treturn next();\n};\n\n// Accepts both fully-authenticated and mfaPending tokens. Only for routes that\n// need to serve both contexts on the same path (e.g. POST /auth/mfa/verify\n// handles setup confirmation with a full token and TOTP login with mfaPending).\nexport const requireAnyAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\treturn next();\n};\n","import { timingSafeEqual } from 'node:crypto';\n\nimport { setApiResponse, HTTP } from '../response';\nimport type { IReadinessProblem, Middleware } from '../types';\n\n// The one admin-route guard for every Fonderie module with an ops surface\n// (billing plan-writes/wallet-grant, config/secrets admin, courier template\n// admin). A bootstrap Bearer token compared in constant time. Modules MUST\n// register their admin routes only when a token is configured (unset ⇒ 404),\n// so this guard only ever runs against a real token. See docs/ADMIN-AUTH-SPEC.md.\n\n// Constant-time comparison so a wrong token can't be recovered byte-by-byte from\n// response timing. Length-guard first: timingSafeEqual throws on unequal lengths,\n// and that early return is acceptable — the token's length is not the secret.\nfunction safeTokenEqual(a: string, b: string): boolean {\n\tconst bufA = Buffer.from(a);\n\tconst bufB = Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n\nexport function requireAdminToken(adminToken: string): Middleware {\n\treturn (ctx, next) => {\n\t\tconst header = ctx.request.headers.get('authorization') ?? '';\n\t\tconst token = header.startsWith('Bearer ') ? header.slice(7) : '';\n\t\t// Same 401 for a missing and a wrong token — no oracle distinguishing them.\n\t\tif (!token || !safeTokenEqual(token, adminToken)) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token'),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n\n// A bootstrap admin token must be strong: an admin surface guarded by a short or\n// placeholder token is barely guarded at all. Modules call this from\n// checkReadiness() so the rule is enforced identically everywhere (previously\n// only @fonderie/config validated it). Returns a problem for a weak/placeholder\n// token; nothing when unset (that surface is simply not exposed).\nconst MIN_ADMIN_TOKEN_LENGTH = 32;\nconst PLACEHOLDER_TOKEN =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\nexport function validateAdminToken(\n\ttoken: string | undefined,\n\topts: { module: string },\n): IReadinessProblem[] {\n\tif (!token) return [];\n\tif (token.length < MIN_ADMIN_TOKEN_LENGTH) {\n\t\treturn [\n\t\t\t{\n\t\t\t\tmodule: opts.module,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: `adminToken must be at least ${MIN_ADMIN_TOKEN_LENGTH} characters (got ${token.length})`,\n\t\t\t},\n\t\t];\n\t}\n\tif (PLACEHOLDER_TOKEN.test(token)) {\n\t\treturn [\n\t\t\t{ module: opts.module, severity: 'error', message: 'adminToken looks like a placeholder or dev-default value' },\n\t\t];\n\t}\n\treturn [];\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport const requireVerified: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\n\tif (ctx.user.loginMethod === 'phone') {\n\t\tif (!ctx.user.phoneVerified) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'PHONE_NOT_VERIFIED',\n\t\t\t\t'Please verify your phone number',\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t}\n\n\tif (!ctx.user.emailVerifiedAt) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'EMAIL_NOT_VERIFIED', 'Please verify your email address');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Route-boundary request validation — the one validation middleware every\n// package wires in front of its body-taking routes, so error shape and parse\n// semantics are identical across the whole surface.\n//\n// core stays dependency-free: this accepts anything implementing zod's\n// safeParse contract structurally (zod v3/v4 both match), without importing\n// zod. Feature packages own their schemas; see @fonderie/auth's schemas.ts\n// for the reference pattern.\n\nexport interface IRequestSchema {\n\tsafeParse(input: unknown):\n\t\t| { success: true; data: unknown }\n\t\t| { success: false; error: { issues: Array<{ path: PropertyKey[]; message: string }> } };\n}\n\nexport function validate(schema: IRequestSchema): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst result = schema.safeParse(ctx.meta['body'] ?? {});\n\t\tif (!result.success) {\n\t\t\tconst first = result.error.issues[0];\n\t\t\tconst path = first?.path.length ? `${first.path.map(String).join('.')}: ` : '';\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t`${path}${first?.message ?? 'Invalid request body'}`,\n\t\t\t);\n\t\t}\n\t\t// Parsed output replaces the raw body: trimmed, coerced, unknown keys\n\t\t// stripped — controllers read clean input.\n\t\tctx.meta['body'] = result.data;\n\t\treturn next();\n\t};\n}\n","// Client-IP resolution shared by the adapters. The web-standard Request the\n// pipeline runs on carries no socket address, so each adapter passes the\n// socket's remote address here together with the headers; this resolves the\n// effective client IP with explicit proxy trust.\n//\n// trustProxy semantics (deliberately explicit — a permissive default lets\n// any client spoof X-Forwarded-For and dodge per-IP rate limits):\n// 0 / undefined → ignore forwarding headers; the socket address is the client\n// N > 0 → the client is the Nth-from-the-right entry in\n// X-Forwarded-For (N = number of trusted proxy hops)\n//\n// ⚠️ THE PROXY FOOTGUN. With trustProxy=0 (the spoof-safe default) deployed\n// behind nginx, a Kubernetes ingress, or any L7 proxy, the socket address is\n// the PROXY's IP for every request — so every client collapses onto one\n// per-IP bucket and the limit becomes global (one attacker locks everyone\n// out). You cannot have a default that is both spoof-safe AND correct behind\n// a proxy; they contradict. So we ship spoof-safe and DETECT the mismatch:\n// checkProxyConfig() below warns once, loudly, when the deployment looks\n// proxied but trustProxy is unset. Set TRUST_PROXY=<hops> in that case.\n\nexport function resolveClientIp(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number = trustProxyFromEnv(),\n): string | undefined {\n\tif (trustProxy > 0) {\n\t\tconst xff = headers.get('x-forwarded-for');\n\t\tif (xff) {\n\t\t\tconst hops = xff\n\t\t\t\t.split(',')\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter(Boolean);\n\t\t\tconst candidate = hops[Math.max(0, hops.length - trustProxy)];\n\t\t\tif (candidate) return normalizeIp(candidate);\n\t\t}\n\t}\n\tcheckProxyConfig(socketAddress, headers, trustProxy);\n\treturn socketAddress ? normalizeIp(socketAddress) : undefined;\n}\n\nfunction trustProxyFromEnv(): number {\n\tconst raw = Number(process.env['TRUST_PROXY']);\n\treturn Number.isFinite(raw) && raw > 0 ? raw : 0;\n}\n\nfunction normalizeIp(ip: string): string {\n\t// ::ffff:203.0.113.7 → 203.0.113.7 ; strip port if a proxy appended one\n\tconst noV6Prefix = ip.startsWith('::ffff:') ? ip.slice(7) : ip;\n\tconst m = noV6Prefix.match(/^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/);\n\treturn m ? m[1]! : noV6Prefix;\n}\n\n// Address ranges that indicate a local proxy sits in front of us (so a\n// forwarding header without TRUST_PROXY is a misconfiguration, not spoofing).\nconst LOOPBACK_IPS = new Set(['127.0.0.1', '::1']);\nconst PRIVATE_IP_PREFIXES = [\n\t'10.', // RFC1918\n\t'192.168.', // RFC1918\n\t'169.254.', // link-local\n\t'fc', // IPv6 unique local (fc00::/7)\n\t'fd', // IPv6 unique local\n] as const;\nconst CGNAT_OR_RFC1918_172 = /^172\\.(1[6-9]|2\\d|3[01])\\./; // 172.16.0.0/12\n\nfunction isPrivateOrLoopback(ip: string): boolean {\n\tconst a = normalizeIp(ip);\n\treturn (\n\t\tLOOPBACK_IPS.has(a) ||\n\t\tCGNAT_OR_RFC1918_172.test(a) ||\n\t\tPRIVATE_IP_PREFIXES.some((prefix) => a.startsWith(prefix))\n\t);\n}\n\nlet warned = false;\n\n// Warn ONCE when the deployment looks proxied (forwarding header present, and\n// the socket is a private/loopback address — i.e. a local proxy) but\n// trustProxy is unset. That configuration silently rate-limits every client\n// as one IP. Emitting on the request path (not at boot) is deliberate: the\n// signal we need — an actual X-Forwarded-For header — only exists once real\n// traffic arrives.\nexport function checkProxyConfig(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number,\n): void {\n\tif (warned || trustProxy > 0) return;\n\tconst forwarded =\n\t\theaders.get('x-forwarded-for') ??\n\t\theaders.get('cf-connecting-ip') ??\n\t\theaders.get('x-real-ip');\n\tif (forwarded && socketAddress && isPrivateOrLoopback(socketAddress)) {\n\t\twarned = true;\n\t\tconsole.warn(\n\t\t\t'[fonderie] Requests carry a forwarding header (X-Forwarded-For) and ' +\n\t\t\t\t'arrive from a private/loopback socket, but TRUST_PROXY is unset. ' +\n\t\t\t\t'Every client is being rate-limited as a single IP, which will cause ' +\n\t\t\t\t'global lockout behind nginx / a Kubernetes ingress / any L7 proxy. ' +\n\t\t\t\t'Set TRUST_PROXY=<number of trusted proxy hops>. ' +\n\t\t\t\t'See @fonderie/rate-limit README § Deploying behind a proxy.',\n\t\t);\n\t}\n}\n\n// Test seam — reset the once-only warning latch.\nexport function _resetProxyWarning(): void {\n\twarned = false;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Middleware } from '../types.cjs';
|
|
1
|
+
import { Middleware, IReadinessProblem } from '../types.cjs';
|
|
2
2
|
|
|
3
3
|
interface CorsOptions {
|
|
4
4
|
methods?: string[];
|
|
@@ -13,11 +13,23 @@ declare function notFoundMiddleware(): Middleware;
|
|
|
13
13
|
|
|
14
14
|
declare const withBody: Middleware;
|
|
15
15
|
|
|
16
|
+
interface SecurityHeadersOptions {
|
|
17
|
+
hstsMaxAge?: number;
|
|
18
|
+
hstsIncludeSubDomains?: boolean;
|
|
19
|
+
hstsPreload?: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare function withSecurityHeaders(options?: SecurityHeadersOptions): Middleware;
|
|
22
|
+
|
|
16
23
|
declare function defaultErrorHandler(err: unknown): Response;
|
|
17
24
|
|
|
18
25
|
declare const requireAuth: Middleware;
|
|
19
26
|
declare const requireAnyAuth: Middleware;
|
|
20
27
|
|
|
28
|
+
declare function requireAdminToken(adminToken: string): Middleware;
|
|
29
|
+
declare function validateAdminToken(token: string | undefined, opts: {
|
|
30
|
+
module: string;
|
|
31
|
+
}): IReadinessProblem[];
|
|
32
|
+
|
|
21
33
|
declare const requireVerified: Middleware;
|
|
22
34
|
|
|
23
35
|
interface IRequestSchema {
|
|
@@ -39,4 +51,4 @@ declare function validate(schema: IRequestSchema): Middleware;
|
|
|
39
51
|
declare function resolveClientIp(socketAddress: string | undefined, headers: Headers, trustProxy?: number): string | undefined;
|
|
40
52
|
declare function checkProxyConfig(socketAddress: string | undefined, headers: Headers, trustProxy: number): void;
|
|
41
53
|
|
|
42
|
-
export { type CorsOptions, type IRequestSchema, checkProxyConfig, defaultErrorHandler, notFoundMiddleware, requireAnyAuth, requireAuth, requireVerified, resolveClientIp, validate, withBody, withCors, withLogger };
|
|
54
|
+
export { type CorsOptions, type IRequestSchema, type SecurityHeadersOptions, checkProxyConfig, defaultErrorHandler, notFoundMiddleware, requireAdminToken, requireAnyAuth, requireAuth, requireVerified, resolveClientIp, validate, validateAdminToken, withBody, withCors, withLogger, withSecurityHeaders };
|