@fonderie/core 0.10.1 → 0.10.4
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/dist/index.cjs +34 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +34 -10
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +17 -9
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.js +17 -9
- package/dist/middlewares/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -86,7 +86,14 @@ function matchPath(pattern, path) {
|
|
|
86
86
|
const ps = pp[i] ?? "";
|
|
87
87
|
const vs = vp[i] ?? "";
|
|
88
88
|
if (ps.startsWith(":")) {
|
|
89
|
-
|
|
89
|
+
let decoded;
|
|
90
|
+
try {
|
|
91
|
+
decoded = decodeURIComponent(vs);
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (decoded.includes("\0")) return null;
|
|
96
|
+
params[ps.slice(1)] = decoded;
|
|
90
97
|
} else if (ps !== vs) {
|
|
91
98
|
return null;
|
|
92
99
|
}
|
|
@@ -160,7 +167,7 @@ var DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
|
160
167
|
var PayloadTooLargeError = class extends Error {
|
|
161
168
|
fonderiePayloadTooLarge = true;
|
|
162
169
|
};
|
|
163
|
-
async function
|
|
170
|
+
async function readBytesCapped(req, maxBytes) {
|
|
164
171
|
const declared = Number(req.headers.get("content-length"));
|
|
165
172
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
166
173
|
throw new PayloadTooLargeError();
|
|
@@ -185,7 +192,7 @@ async function readTextCapped(req, maxBytes) {
|
|
|
185
192
|
merged.set(c, offset);
|
|
186
193
|
offset += c.byteLength;
|
|
187
194
|
}
|
|
188
|
-
return
|
|
195
|
+
return merged;
|
|
189
196
|
}
|
|
190
197
|
function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
191
198
|
return async (ctx, next) => {
|
|
@@ -193,22 +200,29 @@ function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
|
193
200
|
if (method === "GET" || method === "HEAD") {
|
|
194
201
|
return next();
|
|
195
202
|
}
|
|
203
|
+
const declared = Number(ctx.request.headers.get("content-length"));
|
|
204
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
205
|
+
return setApiResponse(HTTP.PAYLOAD_TOO_LARGE, "PAYLOAD_TOO_LARGE", "Request body too large");
|
|
206
|
+
}
|
|
196
207
|
const ct = ctx.request.headers.get("content-type") ?? "";
|
|
197
208
|
try {
|
|
198
209
|
if (ct.includes("application/json") || ct.includes("application/x-www-form-urlencoded")) {
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
210
|
+
const bytes = await readBytesCapped(ctx.request, maxBytes);
|
|
211
|
+
if (bytes !== null) {
|
|
201
212
|
ctx.request = new Request(ctx.request.url, {
|
|
202
213
|
method: ctx.request.method,
|
|
203
214
|
headers: ctx.request.headers,
|
|
204
|
-
|
|
215
|
+
// Cast: a Uint8Array is a valid BodyInit at runtime; the lib's
|
|
216
|
+
// BodyInit union is narrower than Uint8Array<ArrayBufferLike>.
|
|
217
|
+
body: bytes.length > 0 ? bytes : null
|
|
205
218
|
});
|
|
206
219
|
}
|
|
220
|
+
const text = bytes ? new TextDecoder().decode(bytes) : "";
|
|
207
221
|
if (ct.includes("application/json")) {
|
|
208
|
-
const
|
|
209
|
-
ctx.meta.body =
|
|
222
|
+
const trimmed = text.trim();
|
|
223
|
+
ctx.meta.body = trimmed ? JSON.parse(trimmed) : {};
|
|
210
224
|
} else {
|
|
211
|
-
ctx.meta.body = Object.fromEntries(new URLSearchParams(
|
|
225
|
+
ctx.meta.body = Object.fromEntries(new URLSearchParams(text));
|
|
212
226
|
}
|
|
213
227
|
}
|
|
214
228
|
} catch (err) {
|
|
@@ -261,7 +275,8 @@ function isHttps(request) {
|
|
|
261
275
|
|
|
262
276
|
// src/middlewares/error-handler.ts
|
|
263
277
|
function defaultErrorHandler(err) {
|
|
264
|
-
const
|
|
278
|
+
const env = process.env["NODE_ENV"];
|
|
279
|
+
const dev = env === "development" || env === "test";
|
|
265
280
|
if (err instanceof Error) {
|
|
266
281
|
console.error("[fonderie]", err.message, err.stack);
|
|
267
282
|
return setApiResponse(
|
|
@@ -562,6 +577,15 @@ var FonderieApp = class {
|
|
|
562
577
|
// ─── The core handler ──────────────────────────────────
|
|
563
578
|
// This is the ONE thing every adapter calls.
|
|
564
579
|
// Takes a Web Standard Request, returns a Web Standard Response.
|
|
580
|
+
//
|
|
581
|
+
// NOTE (known limitation): adapters call buildContext() to populate their
|
|
582
|
+
// native context (running the global middleware stack) AND then call
|
|
583
|
+
// handle() for requests that fall through to fonderie's own routes — so for
|
|
584
|
+
// those fonderie-routed requests the global stack runs TWICE. bodyParser /
|
|
585
|
+
// security-headers are idempotent, but `withMetrics` double-counts and a
|
|
586
|
+
// user-added `.use()` rate-limiter consumes two tokens per request (stricter,
|
|
587
|
+
// never a bypass). Deduplicating this without changing the handle(Request)
|
|
588
|
+
// contract is a deliberate follow-up.
|
|
565
589
|
async handle(request) {
|
|
566
590
|
const ctx = {
|
|
567
591
|
request,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../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/crypto.ts","../src/secret-strength.ts","../src/metrics.ts","../src/config.ts","../src/parser.ts","../src/keyset-cursor.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport type {\n\tITenant,\n\tIRouter,\n\tOperation,\n\tIAuthUser,\n\tMiddleware,\n\tIWorkspace,\n\tIRouteMatch,\n\tIFonderieApp,\n\tIFonderieModule,\n\tIFonderieContext,\n\tICourierMessage,\n\tIDefaultTemplate,\n\tIFonderieContextMeta,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\n} from './types';\n\nexport { OPERATIONS } from './constants';\n\nexport { FonderieApp, DEFAULT_MAX_BODY_BYTES } from './app';\nexport { defineConfig } from './config';\nexport { compose } from './compose';\nexport type { FonderieConfig } from './config';\n\n// Built-in middleware — import from '@fonderie/core/middlewares', not the root barrel\n\n// Parser utilities\nexport { stringOrEmpty, booleanOrFalse, arrayOrEmpty, numberOrZero, dateOrEmpty } from './parser';\nexport { constantTimeEqual } from './crypto';\nexport { MIN_SECRET_LENGTH, PLACEHOLDER_SECRET, secretStrengthProblem } from './secret-strength';\nexport { encodeKeysetCursor, decodeKeysetCursor } from './keyset-cursor';\n\n// Response helpers\nexport type { IApiError, HttpStatus } from './response';\nexport { HTTP, setApiResponse } from './response';\n\n// NOT exported: adapters/, router internals, error-handler, not-found\n// Those are consumed by FonderieApp, never by users directly\n\nexport { MetricsRegistry, withMetrics } from './metrics';\n","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 { bodyParser, DEFAULT_MAX_BODY_BYTES } from './middlewares/body-parser';\nimport { withSecurityHeaders } from './middlewares/security-headers';\nimport { MetricsRegistry, withMetrics } from './metrics';\n\n// Re-exported from the body parser, which is where the cap is actually\n// enforced (so every adapter's buildContext()/handle() inherits it — not\n// just listen()'s transport). Kept here for import-path compatibility.\nexport { DEFAULT_MAX_BODY_BYTES };\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\nfunction payloadTooLarge(res: { statusCode: number; setHeader(k: string, v: string): void; end(body?: string): void }): void {\n\tres.statusCode = 413;\n\tres.setHeader('content-type', 'application/json');\n\tres.end(JSON.stringify({ reason: 'PAYLOAD_TOO_LARGE', explanation: 'Request body too large' }));\n}\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 (capped at config.maxBodyBytes — the cap lives in\n\t\t// the parser so every adapter inherits it), then baseline security\n\t\t// headers (nosniff always; HSTS over HTTPS). Apps can layer more via `.use()`.\n\t\tthis.middlewares = [bodyParser(config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES), 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 maxBodyBytes = this.config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\t// The whole callback is guarded below (see the catch at the bottom): an\n\t\t\t// exception ANYWHERE here — e.g. `new Request()` throwing on a\n\t\t\t// fetch-spec-forbidden method like TRACE, or an absolute-form\n\t\t\t// request-target producing an invalid URL — would otherwise be an\n\t\t\t// unhandled rejection and crash the PROCESS on a single request.\n\t\t\ttry {\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\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\t// Body cap — without it a single unauthenticated request could stream\n\t\t\t// an arbitrarily large body fully into memory before any handler runs.\n\t\t\t// Fast path: reject a declared-oversize body before reading a byte.\n\t\t\tconst declared = Number(req.headers['content-length']);\n\t\t\tif (hasBody && Number.isFinite(declared) && declared > maxBodyBytes) {\n\t\t\t\t// Answer FIRST, then drop the connection — destroying before the\n\t\t\t\t// write means the client sees a reset instead of the 413.\n\t\t\t\tpayloadTooLarge(res);\n\t\t\t\tres.once('close', () => req.destroy());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Read the body stream, capped as a backstop for chunked / missing /\n\t\t\t// lying Content-Length: stop buffering the moment the cap is crossed.\n\t\t\tlet body: Buffer;\n\t\t\ttry {\n\t\t\t\tbody = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\t\tlet total = 0;\n\t\t\t\t\treq.on('data', (chunk: Buffer) => {\n\t\t\t\t\t\ttotal += chunk.length;\n\t\t\t\t\t\tif (total > maxBodyBytes) {\n\t\t\t\t\t\t\treject(new PayloadTooLargeError());\n\t\t\t\t\t\t\treq.destroy();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchunks.push(chunk);\n\t\t\t\t\t});\n\t\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\t\treq.on('error', reject);\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof PayloadTooLargeError) {\n\t\t\t\t\tpayloadTooLarge(res);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tres.statusCode = 400;\n\t\t\t\tres.end();\n\t\t\t\treturn;\n\t\t\t}\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\t} catch (err) {\n\t\t\t\t// Malformed/hostile request (TRACE, absolute-form target, bad\n\t\t\t\t// headers): answer 400 and keep the process alive.\n\t\t\t\tconsole.error('[fonderie] request handling failed:', (err as Error)?.message);\n\t\t\t\ttry {\n\t\t\t\t\tif (!res.headersSent) {\n\t\t\t\t\t\tres.statusCode = 400;\n\t\t\t\t\t\tres.setHeader('content-type', 'application/json');\n\t\t\t\t\t}\n\t\t\t\t\tres.end(JSON.stringify({ reason: 'BAD_REQUEST', explanation: 'Malformed request' }));\n\t\t\t\t} catch {\n\t\t\t\t\treq.destroy();\n\t\t\t\t}\n\t\t\t}\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\t// The problems list names weak secrets, placeholder tokens, and\n\t\t\t\t\t// dependency state — a security-posture map. It is only exposed\n\t\t\t\t\t// outside production (or with an explicit opt-in); the probe\n\t\t\t\t\t// consumer (k8s, LB) needs nothing beyond the status code.\n\t\t\t\t\tconst exposeDetails =\n\t\t\t\t\t\tprocess.env['NODE_ENV'] !== 'production' || this.config.exposeReadyzDetails === true;\n\t\t\t\t\treturn Response.json(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstatus: ready ? 'ready' : 'not_ready',\n\t\t\t\t\t\t\tdependencies,\n\t\t\t\t\t\t\t...(exposeDetails ? { problems: report.problems } : {}),\n\t\t\t\t\t\t},\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\tlet completed = false;\n\t\tconst out = await compose(this.middlewares)(ctx, async () => {\n\t\t\tcompleted = true;\n\t\t\treturn new Response();\n\t\t});\n\t\t// A global middleware that answered WITHOUT calling next() (e.g. the\n\t\t// body parser's 413) produced a real response the adapter must send —\n\t\t// context-building normally discards middleware output, so surface the\n\t\t// short-circuit explicitly for bridges to check.\n\t\tif (!completed) ctx.meta['pipelineResponse'] = out;\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\tPAYLOAD_TOO_LARGE: 413,\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\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readTextCapped(req: Request, maxBytes: number): Promise<string | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn new TextDecoder().decode(merged);\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst raw = await readTextCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// so handlers that need the RAW body (webhook signature checks)\n\t\t\t\t// can still read it.\n\t\t\t\tif (raw !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\tbody: raw.length > 0 ? raw : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst text = raw?.trim() ?? '';\n\t\t\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(raw ?? ''));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\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\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\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\t// /readyz's `problems` list names weak secrets and placeholder tokens — a\n\t// security-posture map — so in production it is omitted unless this is\n\t// explicitly true. Probes only need the status code. Non-production always\n\t// includes details.\n\texposeReadyzDetails?: 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\t// Cap on the request body the built-in `listen()` server will buffer, in\n\t// bytes. Defaults to 5 MiB (matching the adapters). Without a cap, an\n\t// unauthenticated request could stream an arbitrarily large body fully into\n\t// memory before any handler runs — a memory-exhaustion DoS. Oversize\n\t// requests get 413. Raise for large uploads (e.g. @fonderie/media images);\n\t// adapter deployments configure this on the adapter instead.\n\tmaxBodyBytes?: number;\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","// One opaque keyset-pagination cursor over (created_at, id) — used by billing's\n// wallet ledger and audit's event log (previously two copies that had drifted:\n// audit's decode skipped field-range checks, so a crafted cursor reached the\n// ::timestamptz cast as a 500 instead of decoding to null → 422). Pure: no DB.\nexport function encodeKeysetCursor(createdAt: string, id: string): string {\n\treturn Buffer.from(JSON.stringify([createdAt, id])).toString('base64url');\n}\n\n// Range-checked (month 01-12, day 01-31, hour 00-23, min/sec 00-59) so a crafted\n// in-shape-but-out-of-range timestamp yields null (→ the caller's 422), never a\n// Postgres cast error (500). Accepts Postgres' own text form\n// ('2026-09-04 18:50:50.888123+00') so cursors survive round-trips without JS\n// millisecond truncation.\nconst CURSOR_TS_RE =\n\t/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[T ]([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function decodeKeysetCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));\n\t\tif (!Array.isArray(parsed) || typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string') {\n\t\t\treturn null;\n\t\t}\n\t\tif (!CURSOR_TS_RE.test(parsed[0]) || !CURSOR_ID_RE.test(parsed[1])) return null;\n\t\treturn { createdAt: parsed[0], id: parsed[1] };\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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","names":["PayloadTooLargeError","module"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../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/crypto.ts","../src/secret-strength.ts","../src/metrics.ts","../src/config.ts","../src/parser.ts","../src/keyset-cursor.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport type {\n\tITenant,\n\tIRouter,\n\tOperation,\n\tIAuthUser,\n\tMiddleware,\n\tIWorkspace,\n\tIRouteMatch,\n\tIFonderieApp,\n\tIFonderieModule,\n\tIFonderieContext,\n\tICourierMessage,\n\tIDefaultTemplate,\n\tIFonderieContextMeta,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\n} from './types';\n\nexport { OPERATIONS } from './constants';\n\nexport { FonderieApp, DEFAULT_MAX_BODY_BYTES } from './app';\nexport { defineConfig } from './config';\nexport { compose } from './compose';\nexport type { FonderieConfig } from './config';\n\n// Built-in middleware — import from '@fonderie/core/middlewares', not the root barrel\n\n// Parser utilities\nexport { stringOrEmpty, booleanOrFalse, arrayOrEmpty, numberOrZero, dateOrEmpty } from './parser';\nexport { constantTimeEqual } from './crypto';\nexport { MIN_SECRET_LENGTH, PLACEHOLDER_SECRET, secretStrengthProblem } from './secret-strength';\nexport { encodeKeysetCursor, decodeKeysetCursor } from './keyset-cursor';\n\n// Response helpers\nexport type { IApiError, HttpStatus } from './response';\nexport { HTTP, setApiResponse } from './response';\n\n// NOT exported: adapters/, router internals, error-handler, not-found\n// Those are consumed by FonderieApp, never by users directly\n\nexport { MetricsRegistry, withMetrics } from './metrics';\n","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 { bodyParser, DEFAULT_MAX_BODY_BYTES } from './middlewares/body-parser';\nimport { withSecurityHeaders } from './middlewares/security-headers';\nimport { MetricsRegistry, withMetrics } from './metrics';\n\n// Re-exported from the body parser, which is where the cap is actually\n// enforced (so every adapter's buildContext()/handle() inherits it — not\n// just listen()'s transport). Kept here for import-path compatibility.\nexport { DEFAULT_MAX_BODY_BYTES };\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\nfunction payloadTooLarge(res: { statusCode: number; setHeader(k: string, v: string): void; end(body?: string): void }): void {\n\tres.statusCode = 413;\n\tres.setHeader('content-type', 'application/json');\n\tres.end(JSON.stringify({ reason: 'PAYLOAD_TOO_LARGE', explanation: 'Request body too large' }));\n}\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 (capped at config.maxBodyBytes — the cap lives in\n\t\t// the parser so every adapter inherits it), then baseline security\n\t\t// headers (nosniff always; HSTS over HTTPS). Apps can layer more via `.use()`.\n\t\tthis.middlewares = [bodyParser(config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES), 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 maxBodyBytes = this.config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\t// The whole callback is guarded below (see the catch at the bottom): an\n\t\t\t// exception ANYWHERE here — e.g. `new Request()` throwing on a\n\t\t\t// fetch-spec-forbidden method like TRACE, or an absolute-form\n\t\t\t// request-target producing an invalid URL — would otherwise be an\n\t\t\t// unhandled rejection and crash the PROCESS on a single request.\n\t\t\ttry {\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\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\t// Body cap — without it a single unauthenticated request could stream\n\t\t\t// an arbitrarily large body fully into memory before any handler runs.\n\t\t\t// Fast path: reject a declared-oversize body before reading a byte.\n\t\t\tconst declared = Number(req.headers['content-length']);\n\t\t\tif (hasBody && Number.isFinite(declared) && declared > maxBodyBytes) {\n\t\t\t\t// Answer FIRST, then drop the connection — destroying before the\n\t\t\t\t// write means the client sees a reset instead of the 413.\n\t\t\t\tpayloadTooLarge(res);\n\t\t\t\tres.once('close', () => req.destroy());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Read the body stream, capped as a backstop for chunked / missing /\n\t\t\t// lying Content-Length: stop buffering the moment the cap is crossed.\n\t\t\tlet body: Buffer;\n\t\t\ttry {\n\t\t\t\tbody = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\t\tlet total = 0;\n\t\t\t\t\treq.on('data', (chunk: Buffer) => {\n\t\t\t\t\t\ttotal += chunk.length;\n\t\t\t\t\t\tif (total > maxBodyBytes) {\n\t\t\t\t\t\t\treject(new PayloadTooLargeError());\n\t\t\t\t\t\t\treq.destroy();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchunks.push(chunk);\n\t\t\t\t\t});\n\t\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\t\treq.on('error', reject);\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof PayloadTooLargeError) {\n\t\t\t\t\tpayloadTooLarge(res);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tres.statusCode = 400;\n\t\t\t\tres.end();\n\t\t\t\treturn;\n\t\t\t}\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\t} catch (err) {\n\t\t\t\t// Malformed/hostile request (TRACE, absolute-form target, bad\n\t\t\t\t// headers): answer 400 and keep the process alive.\n\t\t\t\tconsole.error('[fonderie] request handling failed:', (err as Error)?.message);\n\t\t\t\ttry {\n\t\t\t\t\tif (!res.headersSent) {\n\t\t\t\t\t\tres.statusCode = 400;\n\t\t\t\t\t\tres.setHeader('content-type', 'application/json');\n\t\t\t\t\t}\n\t\t\t\t\tres.end(JSON.stringify({ reason: 'BAD_REQUEST', explanation: 'Malformed request' }));\n\t\t\t\t} catch {\n\t\t\t\t\treq.destroy();\n\t\t\t\t}\n\t\t\t}\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\t// The problems list names weak secrets, placeholder tokens, and\n\t\t\t\t\t// dependency state — a security-posture map. It is only exposed\n\t\t\t\t\t// outside production (or with an explicit opt-in); the probe\n\t\t\t\t\t// consumer (k8s, LB) needs nothing beyond the status code.\n\t\t\t\t\tconst exposeDetails =\n\t\t\t\t\t\tprocess.env['NODE_ENV'] !== 'production' || this.config.exposeReadyzDetails === true;\n\t\t\t\t\treturn Response.json(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstatus: ready ? 'ready' : 'not_ready',\n\t\t\t\t\t\t\tdependencies,\n\t\t\t\t\t\t\t...(exposeDetails ? { problems: report.problems } : {}),\n\t\t\t\t\t\t},\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\tlet completed = false;\n\t\tconst out = await compose(this.middlewares)(ctx, async () => {\n\t\t\tcompleted = true;\n\t\t\treturn new Response();\n\t\t});\n\t\t// A global middleware that answered WITHOUT calling next() (e.g. the\n\t\t// body parser's 413) produced a real response the adapter must send —\n\t\t// context-building normally discards middleware output, so surface the\n\t\t// short-circuit explicitly for bridges to check.\n\t\tif (!completed) ctx.meta['pipelineResponse'] = out;\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\t//\n\t// NOTE (known limitation): adapters call buildContext() to populate their\n\t// native context (running the global middleware stack) AND then call\n\t// handle() for requests that fall through to fonderie's own routes — so for\n\t// those fonderie-routed requests the global stack runs TWICE. bodyParser /\n\t// security-headers are idempotent, but `withMetrics` double-counts and a\n\t// user-added `.use()` rate-limiter consumes two tokens per request (stricter,\n\t// never a bypass). Deduplicating this without changing the handle(Request)\n\t// contract is a deliberate follow-up.\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\t// A malformed percent-encoding (e.g. a lone '%') makes\n\t\t\t// decodeURIComponent throw — treat it as no-match (404) rather than\n\t\t\t// letting it bubble to a 500. A decoded NUL byte is rejected too:\n\t\t\t// it has no legitimate place in a path param and is a classic\n\t\t\t// truncation/injection primitive for downstream consumers.\n\t\t\tlet decoded: string;\n\t\t\ttry {\n\t\t\t\tdecoded = decodeURIComponent(vs);\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (decoded.includes('\\0')) return null;\n\t\t\tparams[ps.slice(1)] = decoded;\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\tPAYLOAD_TOO_LARGE: 413,\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\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readBytesCapped(req: Request, maxBytes: number): Promise<Uint8Array | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn merged;\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Universal Content-Length cap — regardless of content-type. The parser\n\t\t// only READS json/form bodies (so only those get the streamed cap), but\n\t\t// a declared-oversize body of ANY type (notably multipart, which the\n\t\t// parser hands to the route) must be refused here — otherwise, on an\n\t\t// adapter with no transport-level cap (adapter-hono on node-server), a\n\t\t// large multipart upload buffered by the route is an unbounded-memory\n\t\t// DoS. The route still owns the chunked/no-Content-Length streaming case.\n\t\tconst declared = Number(ctx.request.headers.get('content-length'));\n\t\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\t\treturn setApiResponse(HTTP.PAYLOAD_TOO_LARGE, 'PAYLOAD_TOO_LARGE', 'Request body too large');\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst bytes = await readBytesCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// with the ORIGINAL BYTES (not a re-encoded string) so a handler\n\t\t\t\t// that reads the raw body for signature verification (Stripe /\n\t\t\t\t// SendGrid webhooks) gets byte-identical input, even for payloads\n\t\t\t\t// with a BOM or non-UTF-8 bytes.\n\t\t\t\tif (bytes !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\t// Cast: a Uint8Array is a valid BodyInit at runtime; the lib's\n\t\t\t\t\t\t// BodyInit union is narrower than Uint8Array<ArrayBufferLike>.\n\t\t\t\t\t\tbody: bytes.length > 0 ? (bytes as unknown as BodyInit) : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst text = bytes ? new TextDecoder().decode(bytes) : '';\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst trimmed = text.trim();\n\t\t\t\t\tctx.meta.body = trimmed ? JSON.parse(trimmed) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\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\t// Only leak the raw error message in EXPLICITLY non-production-like envs.\n\t// `NODE_ENV !== 'production'` also covered 'staging' and any custom value,\n\t// where an error message can carry connection strings / PII. Unknown or\n\t// unset NODE_ENV is treated as production-safe (no leak).\n\tconst env = process.env['NODE_ENV'];\n\tconst dev = env === 'development' || env === 'test';\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\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\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\t// /readyz's `problems` list names weak secrets and placeholder tokens — a\n\t// security-posture map — so in production it is omitted unless this is\n\t// explicitly true. Probes only need the status code. Non-production always\n\t// includes details.\n\texposeReadyzDetails?: 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\t// Cap on the request body the built-in `listen()` server will buffer, in\n\t// bytes. Defaults to 5 MiB (matching the adapters). Without a cap, an\n\t// unauthenticated request could stream an arbitrarily large body fully into\n\t// memory before any handler runs — a memory-exhaustion DoS. Oversize\n\t// requests get 413. Raise for large uploads (e.g. @fonderie/media images);\n\t// adapter deployments configure this on the adapter instead.\n\tmaxBodyBytes?: number;\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","// One opaque keyset-pagination cursor over (created_at, id) — used by billing's\n// wallet ledger and audit's event log (previously two copies that had drifted:\n// audit's decode skipped field-range checks, so a crafted cursor reached the\n// ::timestamptz cast as a 500 instead of decoding to null → 422). Pure: no DB.\nexport function encodeKeysetCursor(createdAt: string, id: string): string {\n\treturn Buffer.from(JSON.stringify([createdAt, id])).toString('base64url');\n}\n\n// Range-checked (month 01-12, day 01-31, hour 00-23, min/sec 00-59) so a crafted\n// in-shape-but-out-of-range timestamp yields null (→ the caller's 422), never a\n// Postgres cast error (500). Accepts Postgres' own text form\n// ('2026-09-04 18:50:50.888123+00') so cursors survive round-trips without JS\n// millisecond truncation.\nconst CURSOR_TS_RE =\n\t/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[T ]([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function decodeKeysetCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));\n\t\tif (!Array.isArray(parsed) || typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string') {\n\t\t\treturn null;\n\t\t}\n\t\tif (!CURSOR_TS_RE.test(parsed[0]) || !CURSOR_ID_RE.test(parsed[1])) return null;\n\t\treturn { createdAt: parsed[0], id: parsed[1] };\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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","names":["PayloadTooLargeError","module"]}
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,14 @@ function matchPath(pattern, path) {
|
|
|
41
41
|
const ps = pp[i] ?? "";
|
|
42
42
|
const vs = vp[i] ?? "";
|
|
43
43
|
if (ps.startsWith(":")) {
|
|
44
|
-
|
|
44
|
+
let decoded;
|
|
45
|
+
try {
|
|
46
|
+
decoded = decodeURIComponent(vs);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (decoded.includes("\0")) return null;
|
|
51
|
+
params[ps.slice(1)] = decoded;
|
|
45
52
|
} else if (ps !== vs) {
|
|
46
53
|
return null;
|
|
47
54
|
}
|
|
@@ -115,7 +122,7 @@ var DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
|
115
122
|
var PayloadTooLargeError = class extends Error {
|
|
116
123
|
fonderiePayloadTooLarge = true;
|
|
117
124
|
};
|
|
118
|
-
async function
|
|
125
|
+
async function readBytesCapped(req, maxBytes) {
|
|
119
126
|
const declared = Number(req.headers.get("content-length"));
|
|
120
127
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
121
128
|
throw new PayloadTooLargeError();
|
|
@@ -140,7 +147,7 @@ async function readTextCapped(req, maxBytes) {
|
|
|
140
147
|
merged.set(c, offset);
|
|
141
148
|
offset += c.byteLength;
|
|
142
149
|
}
|
|
143
|
-
return
|
|
150
|
+
return merged;
|
|
144
151
|
}
|
|
145
152
|
function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
146
153
|
return async (ctx, next) => {
|
|
@@ -148,22 +155,29 @@ function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
|
148
155
|
if (method === "GET" || method === "HEAD") {
|
|
149
156
|
return next();
|
|
150
157
|
}
|
|
158
|
+
const declared = Number(ctx.request.headers.get("content-length"));
|
|
159
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
160
|
+
return setApiResponse(HTTP.PAYLOAD_TOO_LARGE, "PAYLOAD_TOO_LARGE", "Request body too large");
|
|
161
|
+
}
|
|
151
162
|
const ct = ctx.request.headers.get("content-type") ?? "";
|
|
152
163
|
try {
|
|
153
164
|
if (ct.includes("application/json") || ct.includes("application/x-www-form-urlencoded")) {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
165
|
+
const bytes = await readBytesCapped(ctx.request, maxBytes);
|
|
166
|
+
if (bytes !== null) {
|
|
156
167
|
ctx.request = new Request(ctx.request.url, {
|
|
157
168
|
method: ctx.request.method,
|
|
158
169
|
headers: ctx.request.headers,
|
|
159
|
-
|
|
170
|
+
// Cast: a Uint8Array is a valid BodyInit at runtime; the lib's
|
|
171
|
+
// BodyInit union is narrower than Uint8Array<ArrayBufferLike>.
|
|
172
|
+
body: bytes.length > 0 ? bytes : null
|
|
160
173
|
});
|
|
161
174
|
}
|
|
175
|
+
const text = bytes ? new TextDecoder().decode(bytes) : "";
|
|
162
176
|
if (ct.includes("application/json")) {
|
|
163
|
-
const
|
|
164
|
-
ctx.meta.body =
|
|
177
|
+
const trimmed = text.trim();
|
|
178
|
+
ctx.meta.body = trimmed ? JSON.parse(trimmed) : {};
|
|
165
179
|
} else {
|
|
166
|
-
ctx.meta.body = Object.fromEntries(new URLSearchParams(
|
|
180
|
+
ctx.meta.body = Object.fromEntries(new URLSearchParams(text));
|
|
167
181
|
}
|
|
168
182
|
}
|
|
169
183
|
} catch (err) {
|
|
@@ -216,7 +230,8 @@ function isHttps(request) {
|
|
|
216
230
|
|
|
217
231
|
// src/middlewares/error-handler.ts
|
|
218
232
|
function defaultErrorHandler(err) {
|
|
219
|
-
const
|
|
233
|
+
const env = process.env["NODE_ENV"];
|
|
234
|
+
const dev = env === "development" || env === "test";
|
|
220
235
|
if (err instanceof Error) {
|
|
221
236
|
console.error("[fonderie]", err.message, err.stack);
|
|
222
237
|
return setApiResponse(
|
|
@@ -517,6 +532,15 @@ var FonderieApp = class {
|
|
|
517
532
|
// ─── The core handler ──────────────────────────────────
|
|
518
533
|
// This is the ONE thing every adapter calls.
|
|
519
534
|
// Takes a Web Standard Request, returns a Web Standard Response.
|
|
535
|
+
//
|
|
536
|
+
// NOTE (known limitation): adapters call buildContext() to populate their
|
|
537
|
+
// native context (running the global middleware stack) AND then call
|
|
538
|
+
// handle() for requests that fall through to fonderie's own routes — so for
|
|
539
|
+
// those fonderie-routed requests the global stack runs TWICE. bodyParser /
|
|
540
|
+
// security-headers are idempotent, but `withMetrics` double-counts and a
|
|
541
|
+
// user-added `.use()` rate-limiter consumes two tokens per request (stricter,
|
|
542
|
+
// never a bypass). Deduplicating this without changing the handle(Request)
|
|
543
|
+
// contract is a deliberate follow-up.
|
|
520
544
|
async handle(request) {
|
|
521
545
|
const ctx = {
|
|
522
546
|
request,
|
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/security-headers.ts","../src/middlewares/error-handler.ts","../src/crypto.ts","../src/secret-strength.ts","../src/metrics.ts","../src/config.ts","../src/parser.ts","../src/keyset-cursor.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 { bodyParser, DEFAULT_MAX_BODY_BYTES } from './middlewares/body-parser';\nimport { withSecurityHeaders } from './middlewares/security-headers';\nimport { MetricsRegistry, withMetrics } from './metrics';\n\n// Re-exported from the body parser, which is where the cap is actually\n// enforced (so every adapter's buildContext()/handle() inherits it — not\n// just listen()'s transport). Kept here for import-path compatibility.\nexport { DEFAULT_MAX_BODY_BYTES };\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\nfunction payloadTooLarge(res: { statusCode: number; setHeader(k: string, v: string): void; end(body?: string): void }): void {\n\tres.statusCode = 413;\n\tres.setHeader('content-type', 'application/json');\n\tres.end(JSON.stringify({ reason: 'PAYLOAD_TOO_LARGE', explanation: 'Request body too large' }));\n}\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 (capped at config.maxBodyBytes — the cap lives in\n\t\t// the parser so every adapter inherits it), then baseline security\n\t\t// headers (nosniff always; HSTS over HTTPS). Apps can layer more via `.use()`.\n\t\tthis.middlewares = [bodyParser(config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES), 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 maxBodyBytes = this.config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\t// The whole callback is guarded below (see the catch at the bottom): an\n\t\t\t// exception ANYWHERE here — e.g. `new Request()` throwing on a\n\t\t\t// fetch-spec-forbidden method like TRACE, or an absolute-form\n\t\t\t// request-target producing an invalid URL — would otherwise be an\n\t\t\t// unhandled rejection and crash the PROCESS on a single request.\n\t\t\ttry {\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\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\t// Body cap — without it a single unauthenticated request could stream\n\t\t\t// an arbitrarily large body fully into memory before any handler runs.\n\t\t\t// Fast path: reject a declared-oversize body before reading a byte.\n\t\t\tconst declared = Number(req.headers['content-length']);\n\t\t\tif (hasBody && Number.isFinite(declared) && declared > maxBodyBytes) {\n\t\t\t\t// Answer FIRST, then drop the connection — destroying before the\n\t\t\t\t// write means the client sees a reset instead of the 413.\n\t\t\t\tpayloadTooLarge(res);\n\t\t\t\tres.once('close', () => req.destroy());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Read the body stream, capped as a backstop for chunked / missing /\n\t\t\t// lying Content-Length: stop buffering the moment the cap is crossed.\n\t\t\tlet body: Buffer;\n\t\t\ttry {\n\t\t\t\tbody = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\t\tlet total = 0;\n\t\t\t\t\treq.on('data', (chunk: Buffer) => {\n\t\t\t\t\t\ttotal += chunk.length;\n\t\t\t\t\t\tif (total > maxBodyBytes) {\n\t\t\t\t\t\t\treject(new PayloadTooLargeError());\n\t\t\t\t\t\t\treq.destroy();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchunks.push(chunk);\n\t\t\t\t\t});\n\t\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\t\treq.on('error', reject);\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof PayloadTooLargeError) {\n\t\t\t\t\tpayloadTooLarge(res);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tres.statusCode = 400;\n\t\t\t\tres.end();\n\t\t\t\treturn;\n\t\t\t}\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\t} catch (err) {\n\t\t\t\t// Malformed/hostile request (TRACE, absolute-form target, bad\n\t\t\t\t// headers): answer 400 and keep the process alive.\n\t\t\t\tconsole.error('[fonderie] request handling failed:', (err as Error)?.message);\n\t\t\t\ttry {\n\t\t\t\t\tif (!res.headersSent) {\n\t\t\t\t\t\tres.statusCode = 400;\n\t\t\t\t\t\tres.setHeader('content-type', 'application/json');\n\t\t\t\t\t}\n\t\t\t\t\tres.end(JSON.stringify({ reason: 'BAD_REQUEST', explanation: 'Malformed request' }));\n\t\t\t\t} catch {\n\t\t\t\t\treq.destroy();\n\t\t\t\t}\n\t\t\t}\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\t// The problems list names weak secrets, placeholder tokens, and\n\t\t\t\t\t// dependency state — a security-posture map. It is only exposed\n\t\t\t\t\t// outside production (or with an explicit opt-in); the probe\n\t\t\t\t\t// consumer (k8s, LB) needs nothing beyond the status code.\n\t\t\t\t\tconst exposeDetails =\n\t\t\t\t\t\tprocess.env['NODE_ENV'] !== 'production' || this.config.exposeReadyzDetails === true;\n\t\t\t\t\treturn Response.json(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstatus: ready ? 'ready' : 'not_ready',\n\t\t\t\t\t\t\tdependencies,\n\t\t\t\t\t\t\t...(exposeDetails ? { problems: report.problems } : {}),\n\t\t\t\t\t\t},\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\tlet completed = false;\n\t\tconst out = await compose(this.middlewares)(ctx, async () => {\n\t\t\tcompleted = true;\n\t\t\treturn new Response();\n\t\t});\n\t\t// A global middleware that answered WITHOUT calling next() (e.g. the\n\t\t// body parser's 413) produced a real response the adapter must send —\n\t\t// context-building normally discards middleware output, so surface the\n\t\t// short-circuit explicitly for bridges to check.\n\t\tif (!completed) ctx.meta['pipelineResponse'] = out;\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\tPAYLOAD_TOO_LARGE: 413,\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\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readTextCapped(req: Request, maxBytes: number): Promise<string | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn new TextDecoder().decode(merged);\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst raw = await readTextCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// so handlers that need the RAW body (webhook signature checks)\n\t\t\t\t// can still read it.\n\t\t\t\tif (raw !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\tbody: raw.length > 0 ? raw : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst text = raw?.trim() ?? '';\n\t\t\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(raw ?? ''));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\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\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\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\t// /readyz's `problems` list names weak secrets and placeholder tokens — a\n\t// security-posture map — so in production it is omitted unless this is\n\t// explicitly true. Probes only need the status code. Non-production always\n\t// includes details.\n\texposeReadyzDetails?: 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\t// Cap on the request body the built-in `listen()` server will buffer, in\n\t// bytes. Defaults to 5 MiB (matching the adapters). Without a cap, an\n\t// unauthenticated request could stream an arbitrarily large body fully into\n\t// memory before any handler runs — a memory-exhaustion DoS. Oversize\n\t// requests get 413. Raise for large uploads (e.g. @fonderie/media images);\n\t// adapter deployments configure this on the adapter instead.\n\tmaxBodyBytes?: number;\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","// One opaque keyset-pagination cursor over (created_at, id) — used by billing's\n// wallet ledger and audit's event log (previously two copies that had drifted:\n// audit's decode skipped field-range checks, so a crafted cursor reached the\n// ::timestamptz cast as a 500 instead of decoding to null → 422). Pure: no DB.\nexport function encodeKeysetCursor(createdAt: string, id: string): string {\n\treturn Buffer.from(JSON.stringify([createdAt, id])).toString('base64url');\n}\n\n// Range-checked (month 01-12, day 01-31, hour 00-23, min/sec 00-59) so a crafted\n// in-shape-but-out-of-range timestamp yields null (→ the caller's 422), never a\n// Postgres cast error (500). Accepts Postgres' own text form\n// ('2026-09-04 18:50:50.888123+00') so cursors survive round-trips without JS\n// millisecond truncation.\nconst CURSOR_TS_RE =\n\t/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[T ]([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function decodeKeysetCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));\n\t\tif (!Array.isArray(parsed) || typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string') {\n\t\t\treturn null;\n\t\t}\n\t\tif (!CURSOR_TS_RE.test(parsed[0]) || !CURSOR_ID_RE.test(parsed[1])) return null;\n\t\treturn { createdAt: parsed[0], id: parsed[1] };\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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","names":["PayloadTooLargeError"]}
|
|
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/crypto.ts","../src/secret-strength.ts","../src/metrics.ts","../src/config.ts","../src/parser.ts","../src/keyset-cursor.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 { bodyParser, DEFAULT_MAX_BODY_BYTES } from './middlewares/body-parser';\nimport { withSecurityHeaders } from './middlewares/security-headers';\nimport { MetricsRegistry, withMetrics } from './metrics';\n\n// Re-exported from the body parser, which is where the cap is actually\n// enforced (so every adapter's buildContext()/handle() inherits it — not\n// just listen()'s transport). Kept here for import-path compatibility.\nexport { DEFAULT_MAX_BODY_BYTES };\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\nfunction payloadTooLarge(res: { statusCode: number; setHeader(k: string, v: string): void; end(body?: string): void }): void {\n\tres.statusCode = 413;\n\tres.setHeader('content-type', 'application/json');\n\tres.end(JSON.stringify({ reason: 'PAYLOAD_TOO_LARGE', explanation: 'Request body too large' }));\n}\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 (capped at config.maxBodyBytes — the cap lives in\n\t\t// the parser so every adapter inherits it), then baseline security\n\t\t// headers (nosniff always; HSTS over HTTPS). Apps can layer more via `.use()`.\n\t\tthis.middlewares = [bodyParser(config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES), 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 maxBodyBytes = this.config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\t// The whole callback is guarded below (see the catch at the bottom): an\n\t\t\t// exception ANYWHERE here — e.g. `new Request()` throwing on a\n\t\t\t// fetch-spec-forbidden method like TRACE, or an absolute-form\n\t\t\t// request-target producing an invalid URL — would otherwise be an\n\t\t\t// unhandled rejection and crash the PROCESS on a single request.\n\t\t\ttry {\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\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\t// Body cap — without it a single unauthenticated request could stream\n\t\t\t// an arbitrarily large body fully into memory before any handler runs.\n\t\t\t// Fast path: reject a declared-oversize body before reading a byte.\n\t\t\tconst declared = Number(req.headers['content-length']);\n\t\t\tif (hasBody && Number.isFinite(declared) && declared > maxBodyBytes) {\n\t\t\t\t// Answer FIRST, then drop the connection — destroying before the\n\t\t\t\t// write means the client sees a reset instead of the 413.\n\t\t\t\tpayloadTooLarge(res);\n\t\t\t\tres.once('close', () => req.destroy());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Read the body stream, capped as a backstop for chunked / missing /\n\t\t\t// lying Content-Length: stop buffering the moment the cap is crossed.\n\t\t\tlet body: Buffer;\n\t\t\ttry {\n\t\t\t\tbody = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\t\tlet total = 0;\n\t\t\t\t\treq.on('data', (chunk: Buffer) => {\n\t\t\t\t\t\ttotal += chunk.length;\n\t\t\t\t\t\tif (total > maxBodyBytes) {\n\t\t\t\t\t\t\treject(new PayloadTooLargeError());\n\t\t\t\t\t\t\treq.destroy();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchunks.push(chunk);\n\t\t\t\t\t});\n\t\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\t\treq.on('error', reject);\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof PayloadTooLargeError) {\n\t\t\t\t\tpayloadTooLarge(res);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tres.statusCode = 400;\n\t\t\t\tres.end();\n\t\t\t\treturn;\n\t\t\t}\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\t} catch (err) {\n\t\t\t\t// Malformed/hostile request (TRACE, absolute-form target, bad\n\t\t\t\t// headers): answer 400 and keep the process alive.\n\t\t\t\tconsole.error('[fonderie] request handling failed:', (err as Error)?.message);\n\t\t\t\ttry {\n\t\t\t\t\tif (!res.headersSent) {\n\t\t\t\t\t\tres.statusCode = 400;\n\t\t\t\t\t\tres.setHeader('content-type', 'application/json');\n\t\t\t\t\t}\n\t\t\t\t\tres.end(JSON.stringify({ reason: 'BAD_REQUEST', explanation: 'Malformed request' }));\n\t\t\t\t} catch {\n\t\t\t\t\treq.destroy();\n\t\t\t\t}\n\t\t\t}\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\t// The problems list names weak secrets, placeholder tokens, and\n\t\t\t\t\t// dependency state — a security-posture map. It is only exposed\n\t\t\t\t\t// outside production (or with an explicit opt-in); the probe\n\t\t\t\t\t// consumer (k8s, LB) needs nothing beyond the status code.\n\t\t\t\t\tconst exposeDetails =\n\t\t\t\t\t\tprocess.env['NODE_ENV'] !== 'production' || this.config.exposeReadyzDetails === true;\n\t\t\t\t\treturn Response.json(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstatus: ready ? 'ready' : 'not_ready',\n\t\t\t\t\t\t\tdependencies,\n\t\t\t\t\t\t\t...(exposeDetails ? { problems: report.problems } : {}),\n\t\t\t\t\t\t},\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\tlet completed = false;\n\t\tconst out = await compose(this.middlewares)(ctx, async () => {\n\t\t\tcompleted = true;\n\t\t\treturn new Response();\n\t\t});\n\t\t// A global middleware that answered WITHOUT calling next() (e.g. the\n\t\t// body parser's 413) produced a real response the adapter must send —\n\t\t// context-building normally discards middleware output, so surface the\n\t\t// short-circuit explicitly for bridges to check.\n\t\tif (!completed) ctx.meta['pipelineResponse'] = out;\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\t//\n\t// NOTE (known limitation): adapters call buildContext() to populate their\n\t// native context (running the global middleware stack) AND then call\n\t// handle() for requests that fall through to fonderie's own routes — so for\n\t// those fonderie-routed requests the global stack runs TWICE. bodyParser /\n\t// security-headers are idempotent, but `withMetrics` double-counts and a\n\t// user-added `.use()` rate-limiter consumes two tokens per request (stricter,\n\t// never a bypass). Deduplicating this without changing the handle(Request)\n\t// contract is a deliberate follow-up.\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\t// A malformed percent-encoding (e.g. a lone '%') makes\n\t\t\t// decodeURIComponent throw — treat it as no-match (404) rather than\n\t\t\t// letting it bubble to a 500. A decoded NUL byte is rejected too:\n\t\t\t// it has no legitimate place in a path param and is a classic\n\t\t\t// truncation/injection primitive for downstream consumers.\n\t\t\tlet decoded: string;\n\t\t\ttry {\n\t\t\t\tdecoded = decodeURIComponent(vs);\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (decoded.includes('\\0')) return null;\n\t\t\tparams[ps.slice(1)] = decoded;\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\tPAYLOAD_TOO_LARGE: 413,\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\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readBytesCapped(req: Request, maxBytes: number): Promise<Uint8Array | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn merged;\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Universal Content-Length cap — regardless of content-type. The parser\n\t\t// only READS json/form bodies (so only those get the streamed cap), but\n\t\t// a declared-oversize body of ANY type (notably multipart, which the\n\t\t// parser hands to the route) must be refused here — otherwise, on an\n\t\t// adapter with no transport-level cap (adapter-hono on node-server), a\n\t\t// large multipart upload buffered by the route is an unbounded-memory\n\t\t// DoS. The route still owns the chunked/no-Content-Length streaming case.\n\t\tconst declared = Number(ctx.request.headers.get('content-length'));\n\t\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\t\treturn setApiResponse(HTTP.PAYLOAD_TOO_LARGE, 'PAYLOAD_TOO_LARGE', 'Request body too large');\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst bytes = await readBytesCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// with the ORIGINAL BYTES (not a re-encoded string) so a handler\n\t\t\t\t// that reads the raw body for signature verification (Stripe /\n\t\t\t\t// SendGrid webhooks) gets byte-identical input, even for payloads\n\t\t\t\t// with a BOM or non-UTF-8 bytes.\n\t\t\t\tif (bytes !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\t// Cast: a Uint8Array is a valid BodyInit at runtime; the lib's\n\t\t\t\t\t\t// BodyInit union is narrower than Uint8Array<ArrayBufferLike>.\n\t\t\t\t\t\tbody: bytes.length > 0 ? (bytes as unknown as BodyInit) : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst text = bytes ? new TextDecoder().decode(bytes) : '';\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst trimmed = text.trim();\n\t\t\t\t\tctx.meta.body = trimmed ? JSON.parse(trimmed) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\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\t// Only leak the raw error message in EXPLICITLY non-production-like envs.\n\t// `NODE_ENV !== 'production'` also covered 'staging' and any custom value,\n\t// where an error message can carry connection strings / PII. Unknown or\n\t// unset NODE_ENV is treated as production-safe (no leak).\n\tconst env = process.env['NODE_ENV'];\n\tconst dev = env === 'development' || env === 'test';\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\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\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\t// /readyz's `problems` list names weak secrets and placeholder tokens — a\n\t// security-posture map — so in production it is omitted unless this is\n\t// explicitly true. Probes only need the status code. Non-production always\n\t// includes details.\n\texposeReadyzDetails?: 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\t// Cap on the request body the built-in `listen()` server will buffer, in\n\t// bytes. Defaults to 5 MiB (matching the adapters). Without a cap, an\n\t// unauthenticated request could stream an arbitrarily large body fully into\n\t// memory before any handler runs — a memory-exhaustion DoS. Oversize\n\t// requests get 413. Raise for large uploads (e.g. @fonderie/media images);\n\t// adapter deployments configure this on the adapter instead.\n\tmaxBodyBytes?: number;\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","// One opaque keyset-pagination cursor over (created_at, id) — used by billing's\n// wallet ledger and audit's event log (previously two copies that had drifted:\n// audit's decode skipped field-range checks, so a crafted cursor reached the\n// ::timestamptz cast as a 500 instead of decoding to null → 422). Pure: no DB.\nexport function encodeKeysetCursor(createdAt: string, id: string): string {\n\treturn Buffer.from(JSON.stringify([createdAt, id])).toString('base64url');\n}\n\n// Range-checked (month 01-12, day 01-31, hour 00-23, min/sec 00-59) so a crafted\n// in-shape-but-out-of-range timestamp yields null (→ the caller's 422), never a\n// Postgres cast error (500). Accepts Postgres' own text form\n// ('2026-09-04 18:50:50.888123+00') so cursors survive round-trips without JS\n// millisecond truncation.\nconst CURSOR_TS_RE =\n\t/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[T ]([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function decodeKeysetCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));\n\t\tif (!Array.isArray(parsed) || typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string') {\n\t\t\treturn null;\n\t\t}\n\t\tif (!CURSOR_TS_RE.test(parsed[0]) || !CURSOR_ID_RE.test(parsed[1])) return null;\n\t\treturn { createdAt: parsed[0], id: parsed[1] };\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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","names":["PayloadTooLargeError"]}
|
|
@@ -129,7 +129,7 @@ var DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
|
129
129
|
var PayloadTooLargeError = class extends Error {
|
|
130
130
|
fonderiePayloadTooLarge = true;
|
|
131
131
|
};
|
|
132
|
-
async function
|
|
132
|
+
async function readBytesCapped(req, maxBytes) {
|
|
133
133
|
const declared = Number(req.headers.get("content-length"));
|
|
134
134
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
135
135
|
throw new PayloadTooLargeError();
|
|
@@ -154,7 +154,7 @@ async function readTextCapped(req, maxBytes) {
|
|
|
154
154
|
merged.set(c, offset);
|
|
155
155
|
offset += c.byteLength;
|
|
156
156
|
}
|
|
157
|
-
return
|
|
157
|
+
return merged;
|
|
158
158
|
}
|
|
159
159
|
function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
160
160
|
return async (ctx, next) => {
|
|
@@ -162,22 +162,29 @@ function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
|
162
162
|
if (method === "GET" || method === "HEAD") {
|
|
163
163
|
return next();
|
|
164
164
|
}
|
|
165
|
+
const declared = Number(ctx.request.headers.get("content-length"));
|
|
166
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
167
|
+
return setApiResponse(HTTP.PAYLOAD_TOO_LARGE, "PAYLOAD_TOO_LARGE", "Request body too large");
|
|
168
|
+
}
|
|
165
169
|
const ct = ctx.request.headers.get("content-type") ?? "";
|
|
166
170
|
try {
|
|
167
171
|
if (ct.includes("application/json") || ct.includes("application/x-www-form-urlencoded")) {
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
172
|
+
const bytes = await readBytesCapped(ctx.request, maxBytes);
|
|
173
|
+
if (bytes !== null) {
|
|
170
174
|
ctx.request = new Request(ctx.request.url, {
|
|
171
175
|
method: ctx.request.method,
|
|
172
176
|
headers: ctx.request.headers,
|
|
173
|
-
|
|
177
|
+
// Cast: a Uint8Array is a valid BodyInit at runtime; the lib's
|
|
178
|
+
// BodyInit union is narrower than Uint8Array<ArrayBufferLike>.
|
|
179
|
+
body: bytes.length > 0 ? bytes : null
|
|
174
180
|
});
|
|
175
181
|
}
|
|
182
|
+
const text = bytes ? new TextDecoder().decode(bytes) : "";
|
|
176
183
|
if (ct.includes("application/json")) {
|
|
177
|
-
const
|
|
178
|
-
ctx.meta.body =
|
|
184
|
+
const trimmed = text.trim();
|
|
185
|
+
ctx.meta.body = trimmed ? JSON.parse(trimmed) : {};
|
|
179
186
|
} else {
|
|
180
|
-
ctx.meta.body = Object.fromEntries(new URLSearchParams(
|
|
187
|
+
ctx.meta.body = Object.fromEntries(new URLSearchParams(text));
|
|
181
188
|
}
|
|
182
189
|
}
|
|
183
190
|
} catch (err) {
|
|
@@ -230,7 +237,8 @@ function isHttps(request) {
|
|
|
230
237
|
|
|
231
238
|
// src/middlewares/error-handler.ts
|
|
232
239
|
function defaultErrorHandler(err) {
|
|
233
|
-
const
|
|
240
|
+
const env = process.env["NODE_ENV"];
|
|
241
|
+
const dev = env === "development" || env === "test";
|
|
234
242
|
if (err instanceof Error) {
|
|
235
243
|
console.error("[fonderie]", err.message, err.stack);
|
|
236
244
|
return setApiResponse(
|
|
@@ -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/security-headers.ts","../../src/middlewares/error-handler.ts","../../src/middlewares/require-auth.ts","../../src/crypto.ts","../../src/secret-strength.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, bodyParser, DEFAULT_MAX_BODY_BYTES } 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-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\t\t// Omit the header entirely for a denied origin (an empty ACAO value is\n\t\t// invalid); when the value varies by request origin, say so — otherwise a\n\t\t// shared cache can serve one origin's ACAO to another.\n\t\tif (allowOrigin) corsHeaders['Access-Control-Allow-Origin'] = allowOrigin;\n\t\tif (typeof origin === 'function') corsHeaders['Vary'] = 'Origin';\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\tPAYLOAD_TOO_LARGE: 413,\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\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readTextCapped(req: Request, maxBytes: number): Promise<string | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn new TextDecoder().decode(merged);\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst raw = await readTextCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// so handlers that need the RAW body (webhook signature checks)\n\t\t\t\t// can still read it.\n\t\t\t\tif (raw !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\tbody: raw.length > 0 ? raw : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst text = raw?.trim() ?? '';\n\t\t\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(raw ?? ''));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\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\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\n}\n","import { constantTimeEqual } from '../crypto';\nimport { setApiResponse, HTTP } from '../response';\nimport { MIN_SECRET_LENGTH, secretStrengthProblem } from '../secret-strength';\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.\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 || !constantTimeEqual(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 (shared secret-strength denylist) is enforced\n// identically everywhere. Returns a problem for a weak/placeholder token;\n// nothing when unset (that surface is simply not exposed).\nexport function validateAdminToken(\n\ttoken: string | undefined,\n\topts: { module: string },\n): IReadinessProblem[] {\n\tif (!token) return [];\n\tconst problem = secretStrengthProblem(token);\n\tif (problem === 'too-short') {\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_SECRET_LENGTH} characters (got ${token.length})`,\n\t\t\t},\n\t\t];\n\t}\n\tif (problem === 'placeholder') {\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
|
+
{"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/crypto.ts","../../src/secret-strength.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, bodyParser, DEFAULT_MAX_BODY_BYTES } 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-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\t\t// Omit the header entirely for a denied origin (an empty ACAO value is\n\t\t// invalid); when the value varies by request origin, say so — otherwise a\n\t\t// shared cache can serve one origin's ACAO to another.\n\t\tif (allowOrigin) corsHeaders['Access-Control-Allow-Origin'] = allowOrigin;\n\t\tif (typeof origin === 'function') corsHeaders['Vary'] = 'Origin';\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\tPAYLOAD_TOO_LARGE: 413,\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\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readBytesCapped(req: Request, maxBytes: number): Promise<Uint8Array | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn merged;\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Universal Content-Length cap — regardless of content-type. The parser\n\t\t// only READS json/form bodies (so only those get the streamed cap), but\n\t\t// a declared-oversize body of ANY type (notably multipart, which the\n\t\t// parser hands to the route) must be refused here — otherwise, on an\n\t\t// adapter with no transport-level cap (adapter-hono on node-server), a\n\t\t// large multipart upload buffered by the route is an unbounded-memory\n\t\t// DoS. The route still owns the chunked/no-Content-Length streaming case.\n\t\tconst declared = Number(ctx.request.headers.get('content-length'));\n\t\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\t\treturn setApiResponse(HTTP.PAYLOAD_TOO_LARGE, 'PAYLOAD_TOO_LARGE', 'Request body too large');\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst bytes = await readBytesCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// with the ORIGINAL BYTES (not a re-encoded string) so a handler\n\t\t\t\t// that reads the raw body for signature verification (Stripe /\n\t\t\t\t// SendGrid webhooks) gets byte-identical input, even for payloads\n\t\t\t\t// with a BOM or non-UTF-8 bytes.\n\t\t\t\tif (bytes !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\t// Cast: a Uint8Array is a valid BodyInit at runtime; the lib's\n\t\t\t\t\t\t// BodyInit union is narrower than Uint8Array<ArrayBufferLike>.\n\t\t\t\t\t\tbody: bytes.length > 0 ? (bytes as unknown as BodyInit) : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst text = bytes ? new TextDecoder().decode(bytes) : '';\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst trimmed = text.trim();\n\t\t\t\t\tctx.meta.body = trimmed ? JSON.parse(trimmed) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\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\t// Only leak the raw error message in EXPLICITLY non-production-like envs.\n\t// `NODE_ENV !== 'production'` also covered 'staging' and any custom value,\n\t// where an error message can carry connection strings / PII. Unknown or\n\t// unset NODE_ENV is treated as production-safe (no leak).\n\tconst env = process.env['NODE_ENV'];\n\tconst dev = env === 'development' || env === 'test';\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\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\n}\n","import { constantTimeEqual } from '../crypto';\nimport { setApiResponse, HTTP } from '../response';\nimport { MIN_SECRET_LENGTH, secretStrengthProblem } from '../secret-strength';\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.\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 || !constantTimeEqual(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 (shared secret-strength denylist) is enforced\n// identically everywhere. Returns a problem for a weak/placeholder token;\n// nothing when unset (that surface is simply not exposed).\nexport function validateAdminToken(\n\ttoken: string | undefined,\n\topts: { module: string },\n): IReadinessProblem[] {\n\tif (!token) return [];\n\tconst problem = secretStrengthProblem(token);\n\tif (problem === 'too-short') {\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_SECRET_LENGTH} characters (got ${token.length})`,\n\t\t\t},\n\t\t];\n\t}\n\tif (problem === 'placeholder') {\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":[]}
|
|
@@ -88,7 +88,7 @@ var DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
|
88
88
|
var PayloadTooLargeError = class extends Error {
|
|
89
89
|
fonderiePayloadTooLarge = true;
|
|
90
90
|
};
|
|
91
|
-
async function
|
|
91
|
+
async function readBytesCapped(req, maxBytes) {
|
|
92
92
|
const declared = Number(req.headers.get("content-length"));
|
|
93
93
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
94
94
|
throw new PayloadTooLargeError();
|
|
@@ -113,7 +113,7 @@ async function readTextCapped(req, maxBytes) {
|
|
|
113
113
|
merged.set(c, offset);
|
|
114
114
|
offset += c.byteLength;
|
|
115
115
|
}
|
|
116
|
-
return
|
|
116
|
+
return merged;
|
|
117
117
|
}
|
|
118
118
|
function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
119
119
|
return async (ctx, next) => {
|
|
@@ -121,22 +121,29 @@ function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
|
121
121
|
if (method === "GET" || method === "HEAD") {
|
|
122
122
|
return next();
|
|
123
123
|
}
|
|
124
|
+
const declared = Number(ctx.request.headers.get("content-length"));
|
|
125
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
126
|
+
return setApiResponse(HTTP.PAYLOAD_TOO_LARGE, "PAYLOAD_TOO_LARGE", "Request body too large");
|
|
127
|
+
}
|
|
124
128
|
const ct = ctx.request.headers.get("content-type") ?? "";
|
|
125
129
|
try {
|
|
126
130
|
if (ct.includes("application/json") || ct.includes("application/x-www-form-urlencoded")) {
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
131
|
+
const bytes = await readBytesCapped(ctx.request, maxBytes);
|
|
132
|
+
if (bytes !== null) {
|
|
129
133
|
ctx.request = new Request(ctx.request.url, {
|
|
130
134
|
method: ctx.request.method,
|
|
131
135
|
headers: ctx.request.headers,
|
|
132
|
-
|
|
136
|
+
// Cast: a Uint8Array is a valid BodyInit at runtime; the lib's
|
|
137
|
+
// BodyInit union is narrower than Uint8Array<ArrayBufferLike>.
|
|
138
|
+
body: bytes.length > 0 ? bytes : null
|
|
133
139
|
});
|
|
134
140
|
}
|
|
141
|
+
const text = bytes ? new TextDecoder().decode(bytes) : "";
|
|
135
142
|
if (ct.includes("application/json")) {
|
|
136
|
-
const
|
|
137
|
-
ctx.meta.body =
|
|
143
|
+
const trimmed = text.trim();
|
|
144
|
+
ctx.meta.body = trimmed ? JSON.parse(trimmed) : {};
|
|
138
145
|
} else {
|
|
139
|
-
ctx.meta.body = Object.fromEntries(new URLSearchParams(
|
|
146
|
+
ctx.meta.body = Object.fromEntries(new URLSearchParams(text));
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
149
|
} catch (err) {
|
|
@@ -189,7 +196,8 @@ function isHttps(request) {
|
|
|
189
196
|
|
|
190
197
|
// src/middlewares/error-handler.ts
|
|
191
198
|
function defaultErrorHandler(err) {
|
|
192
|
-
const
|
|
199
|
+
const env = process.env["NODE_ENV"];
|
|
200
|
+
const dev = env === "development" || env === "test";
|
|
193
201
|
if (err instanceof Error) {
|
|
194
202
|
console.error("[fonderie]", err.message, err.stack);
|
|
195
203
|
return setApiResponse(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../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/crypto.ts","../../src/secret-strength.ts","../../src/middlewares/require-admin-token.ts","../../src/middlewares/require-verified.ts","../../src/middlewares/validate.ts","../../src/middlewares/client-ip.ts"],"sourcesContent":["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-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\t\t// Omit the header entirely for a denied origin (an empty ACAO value is\n\t\t// invalid); when the value varies by request origin, say so — otherwise a\n\t\t// shared cache can serve one origin's ACAO to another.\n\t\tif (allowOrigin) corsHeaders['Access-Control-Allow-Origin'] = allowOrigin;\n\t\tif (typeof origin === 'function') corsHeaders['Vary'] = 'Origin';\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\tPAYLOAD_TOO_LARGE: 413,\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\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readTextCapped(req: Request, maxBytes: number): Promise<string | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn new TextDecoder().decode(merged);\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst raw = await readTextCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// so handlers that need the RAW body (webhook signature checks)\n\t\t\t\t// can still read it.\n\t\t\t\tif (raw !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\tbody: raw.length > 0 ? raw : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst text = raw?.trim() ?? '';\n\t\t\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(raw ?? ''));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\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\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\n}\n","import { constantTimeEqual } from '../crypto';\nimport { setApiResponse, HTTP } from '../response';\nimport { MIN_SECRET_LENGTH, secretStrengthProblem } from '../secret-strength';\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.\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 || !constantTimeEqual(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 (shared secret-strength denylist) is enforced\n// identically everywhere. Returns a problem for a weak/placeholder token;\n// nothing when unset (that surface is simply not exposed).\nexport function validateAdminToken(\n\ttoken: string | undefined,\n\topts: { module: string },\n): IReadinessProblem[] {\n\tif (!token) return [];\n\tconst problem = secretStrengthProblem(token);\n\tif (problem === 'too-short') {\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_SECRET_LENGTH} characters (got ${token.length})`,\n\t\t\t},\n\t\t];\n\t}\n\tif (problem === 'placeholder') {\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
|
+
{"version":3,"sources":["../../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/crypto.ts","../../src/secret-strength.ts","../../src/middlewares/require-admin-token.ts","../../src/middlewares/require-verified.ts","../../src/middlewares/validate.ts","../../src/middlewares/client-ip.ts"],"sourcesContent":["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-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\t\t// Omit the header entirely for a denied origin (an empty ACAO value is\n\t\t// invalid); when the value varies by request origin, say so — otherwise a\n\t\t// shared cache can serve one origin's ACAO to another.\n\t\tif (allowOrigin) corsHeaders['Access-Control-Allow-Origin'] = allowOrigin;\n\t\tif (typeof origin === 'function') corsHeaders['Vary'] = 'Origin';\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\tPAYLOAD_TOO_LARGE: 413,\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\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readBytesCapped(req: Request, maxBytes: number): Promise<Uint8Array | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn merged;\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Universal Content-Length cap — regardless of content-type. The parser\n\t\t// only READS json/form bodies (so only those get the streamed cap), but\n\t\t// a declared-oversize body of ANY type (notably multipart, which the\n\t\t// parser hands to the route) must be refused here — otherwise, on an\n\t\t// adapter with no transport-level cap (adapter-hono on node-server), a\n\t\t// large multipart upload buffered by the route is an unbounded-memory\n\t\t// DoS. The route still owns the chunked/no-Content-Length streaming case.\n\t\tconst declared = Number(ctx.request.headers.get('content-length'));\n\t\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\t\treturn setApiResponse(HTTP.PAYLOAD_TOO_LARGE, 'PAYLOAD_TOO_LARGE', 'Request body too large');\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst bytes = await readBytesCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// with the ORIGINAL BYTES (not a re-encoded string) so a handler\n\t\t\t\t// that reads the raw body for signature verification (Stripe /\n\t\t\t\t// SendGrid webhooks) gets byte-identical input, even for payloads\n\t\t\t\t// with a BOM or non-UTF-8 bytes.\n\t\t\t\tif (bytes !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\t// Cast: a Uint8Array is a valid BodyInit at runtime; the lib's\n\t\t\t\t\t\t// BodyInit union is narrower than Uint8Array<ArrayBufferLike>.\n\t\t\t\t\t\tbody: bytes.length > 0 ? (bytes as unknown as BodyInit) : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst text = bytes ? new TextDecoder().decode(bytes) : '';\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst trimmed = text.trim();\n\t\t\t\t\tctx.meta.body = trimmed ? JSON.parse(trimmed) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\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\t// Only leak the raw error message in EXPLICITLY non-production-like envs.\n\t// `NODE_ENV !== 'production'` also covered 'staging' and any custom value,\n\t// where an error message can carry connection strings / PII. Unknown or\n\t// unset NODE_ENV is treated as production-safe (no leak).\n\tconst env = process.env['NODE_ENV'];\n\tconst dev = env === 'development' || env === 'test';\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\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\n}\n","import { constantTimeEqual } from '../crypto';\nimport { setApiResponse, HTTP } from '../response';\nimport { MIN_SECRET_LENGTH, secretStrengthProblem } from '../secret-strength';\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.\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 || !constantTimeEqual(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 (shared secret-strength denylist) is enforced\n// identically everywhere. Returns a problem for a weak/placeholder token;\n// nothing when unset (that surface is simply not exposed).\nexport function validateAdminToken(\n\ttoken: string | undefined,\n\topts: { module: string },\n): IReadinessProblem[] {\n\tif (!token) return [];\n\tconst problem = secretStrengthProblem(token);\n\tif (problem === 'too-short') {\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_SECRET_LENGTH} characters (got ${token.length})`,\n\t\t\t},\n\t\t];\n\t}\n\tif (problem === 'placeholder') {\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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/core",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "Framework core — request router, middleware pipeline, module system, and shared context types. Every other @fonderiejs package depends on this.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fonderiejs",
|