@fonderie/core 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brain/signatures.md +10 -0
- package/dist/index.cjs +56 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +31 -4
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +25 -2
- package/dist/middlewares/index.d.ts +25 -2
- package/dist/middlewares/index.js +30 -4
- package/dist/middlewares/index.js.map +1 -1
- package/package.json +1 -1
package/brain/signatures.md
CHANGED
|
@@ -128,6 +128,16 @@ interface ISecurityReport {
|
|
|
128
128
|
|
|
129
129
|
const OPERATIONS: { readonly CREATE: "create"; readonly READ: "read"; readonly UPDATE: "update"; readonly DELETE: "delete"; }
|
|
130
130
|
|
|
131
|
+
function background(work: Promise<unknown> | undefined): Promise<void>
|
|
132
|
+
|
|
133
|
+
function setBackgroundRunner(fn: ((work: Promise<unknown>) => void) | null): void
|
|
134
|
+
|
|
135
|
+
function isServerlessRuntime(env?: ProcessEnv): boolean
|
|
136
|
+
|
|
137
|
+
function resolveBackgroundMode(env?: ProcessEnv): "await" | "detach"
|
|
138
|
+
|
|
139
|
+
type BackgroundMode = 'auto' | 'await' | 'detach';
|
|
140
|
+
|
|
131
141
|
new FonderieApp(config: FonderieConfig): FonderieApp
|
|
132
142
|
.metrics: MetricsRegistry
|
|
133
143
|
.listen(port: number, options?: { name?: string; version?: string; env?: string; quiet?: boolean; }): Server<typeof IncomingMessage, typeof ServerResponse>
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
OPERATIONS: () => OPERATIONS,
|
|
29
29
|
PLACEHOLDER_SECRET: () => PLACEHOLDER_SECRET,
|
|
30
30
|
arrayOrEmpty: () => arrayOrEmpty,
|
|
31
|
+
background: () => background,
|
|
31
32
|
booleanOrFalse: () => booleanOrFalse,
|
|
32
33
|
compose: () => compose,
|
|
33
34
|
constantTimeEqual: () => constantTimeEqual,
|
|
@@ -35,9 +36,12 @@ __export(index_exports, {
|
|
|
35
36
|
decodeKeysetCursor: () => decodeKeysetCursor,
|
|
36
37
|
defineConfig: () => defineConfig,
|
|
37
38
|
encodeKeysetCursor: () => encodeKeysetCursor,
|
|
39
|
+
isServerlessRuntime: () => isServerlessRuntime,
|
|
38
40
|
numberOrZero: () => numberOrZero,
|
|
41
|
+
resolveBackgroundMode: () => resolveBackgroundMode,
|
|
39
42
|
secretStrengthProblem: () => secretStrengthProblem,
|
|
40
43
|
setApiResponse: () => setApiResponse,
|
|
44
|
+
setBackgroundRunner: () => setBackgroundRunner,
|
|
41
45
|
stringOrEmpty: () => stringOrEmpty,
|
|
42
46
|
withMetrics: () => withMetrics
|
|
43
47
|
});
|
|
@@ -51,6 +55,54 @@ var OPERATIONS = {
|
|
|
51
55
|
DELETE: "delete"
|
|
52
56
|
};
|
|
53
57
|
|
|
58
|
+
// src/background.ts
|
|
59
|
+
var SERVERLESS_MARKERS = [
|
|
60
|
+
"VERCEL",
|
|
61
|
+
// Vercel
|
|
62
|
+
"AWS_LAMBDA_FUNCTION_NAME",
|
|
63
|
+
// AWS Lambda, Netlify Functions
|
|
64
|
+
"FUNCTION_TARGET",
|
|
65
|
+
// Google Cloud Functions
|
|
66
|
+
"K_SERVICE",
|
|
67
|
+
// Google Cloud Run (CPU is throttled outside a request)
|
|
68
|
+
"FUNCTIONS_WORKER_RUNTIME"
|
|
69
|
+
// Azure Functions
|
|
70
|
+
];
|
|
71
|
+
function isServerlessRuntime(env = process.env) {
|
|
72
|
+
return SERVERLESS_MARKERS.some((key) => !!env[key]);
|
|
73
|
+
}
|
|
74
|
+
function resolveBackgroundMode(env = process.env) {
|
|
75
|
+
const configured = (env["FONDERIE_BACKGROUND_TASKS"] ?? "auto").trim().toLowerCase();
|
|
76
|
+
if (configured === "await" || configured === "detach") return configured;
|
|
77
|
+
return isServerlessRuntime(env) ? "await" : "detach";
|
|
78
|
+
}
|
|
79
|
+
function resolveTimeoutMs(env = process.env) {
|
|
80
|
+
const raw = Number(env["FONDERIE_BACKGROUND_TIMEOUT_MS"]);
|
|
81
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 5e3;
|
|
82
|
+
}
|
|
83
|
+
var runner = null;
|
|
84
|
+
function setBackgroundRunner(fn) {
|
|
85
|
+
runner = fn;
|
|
86
|
+
}
|
|
87
|
+
async function background(work) {
|
|
88
|
+
if (!work) return;
|
|
89
|
+
const settled = Promise.resolve(work).catch(() => void 0);
|
|
90
|
+
if (runner) {
|
|
91
|
+
runner(settled);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (resolveBackgroundMode() === "detach") return;
|
|
95
|
+
let timer;
|
|
96
|
+
await Promise.race([
|
|
97
|
+
settled,
|
|
98
|
+
new Promise((resolve) => {
|
|
99
|
+
timer = setTimeout(resolve, resolveTimeoutMs());
|
|
100
|
+
timer.unref?.();
|
|
101
|
+
})
|
|
102
|
+
]);
|
|
103
|
+
if (timer) clearTimeout(timer);
|
|
104
|
+
}
|
|
105
|
+
|
|
54
106
|
// src/app.ts
|
|
55
107
|
var import_node_os = require("os");
|
|
56
108
|
var import_node_http = require("http");
|
|
@@ -733,6 +785,7 @@ function decodeKeysetCursor(cursor) {
|
|
|
733
785
|
OPERATIONS,
|
|
734
786
|
PLACEHOLDER_SECRET,
|
|
735
787
|
arrayOrEmpty,
|
|
788
|
+
background,
|
|
736
789
|
booleanOrFalse,
|
|
737
790
|
compose,
|
|
738
791
|
constantTimeEqual,
|
|
@@ -740,9 +793,12 @@ function decodeKeysetCursor(cursor) {
|
|
|
740
793
|
decodeKeysetCursor,
|
|
741
794
|
defineConfig,
|
|
742
795
|
encodeKeysetCursor,
|
|
796
|
+
isServerlessRuntime,
|
|
743
797
|
numberOrZero,
|
|
798
|
+
resolveBackgroundMode,
|
|
744
799
|
secretStrengthProblem,
|
|
745
800
|
setApiResponse,
|
|
801
|
+
setBackgroundRunner,
|
|
746
802
|
stringOrEmpty,
|
|
747
803
|
withMetrics
|
|
748
804
|
});
|
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/middlewares/cors.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\tIHandleInit,\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\tIHandleInit,\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// `init.meta` is how an adapter hands over what only IT can observe (the\n\t// socket's client IP). Without it those facts die with the adapter's own\n\t// context, because handle() builds a fresh one.\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 is a deliberate follow-up.\n\n\tasync handle(request: Request, init?: IHandleInit): 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\t// Facts the ADAPTER knows and the pipeline cannot rediscover — the\n\t\t\t// client IP above all: a Web Standard Request carries no socket\n\t\t\t// address, so if the adapter's value is not seeded here it is simply\n\t\t\t// lost, and every fonderie-owned route sees `undefined`. Copied, not\n\t\t\t// aliased, so a request never mutates the adapter's own context.\n\t\t\tmeta: { ...init?.meta },\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","import type { Middleware } from '../types';\n\n// Request headers @fonderie/client sends on browser calls. A preflight\n// rejects the WHOLE request when any of them is missing from the allow-list,\n// so they ship as defaults here, in lockstep with the client:\n// X-Request-ID — request correlation (client >= 0.19)\n// traceparent — W3C trace context (client >= 0.20)\n// X-Workspace-ID — workspace scoping (setWorkspaceId)\nexport const FONDERIE_CLIENT_HEADERS = ['X-Request-ID', 'traceparent', 'X-Workspace-ID'];\n\nexport const DEFAULT_CORS_HEADERS = ['Content-Type', 'Authorization', ...FONDERIE_CLIENT_HEADERS];\n\n// Response headers browser JS is allowed to read. Without X-Request-ID here\n// the client cannot see the echoed correlation id — FonderieApiError.requestId\n// would silently stay at the client-minted value instead of the server echo.\nexport const DEFAULT_CORS_EXPOSE_HEADERS = ['X-Request-ID'];\n\nexport interface CorsOptions {\n\tmethods?: string[];\n\theaders?: string[];\n\t/** Response headers exposed to browser JS (Access-Control-Expose-Headers). */\n\texposeHeaders?: string[];\n\torigin?: string | ((requestOrigin: string) => boolean);\n\t/**\n\t * Allow credentialed requests. @fonderie/client always fetches with\n\t * credentials:'include', so a browser frontend on another origin needs\n\t * this on. Requires an explicit `origin` — browsers reject '*' on\n\t * credentialed responses.\n\t */\n\tcredentials?: boolean;\n}\n\nexport type ResolvedCorsOptions = Required<CorsOptions>;\n\n// Applies the defaults and rejects impossible combinations at boot. The\n// framework adapters' native cors() middlewares resolve through here too, so\n// every mounting style shares one contract and one failure mode.\nexport function resolveCorsOptions(options: CorsOptions = {}): ResolvedCorsOptions {\n\tconst resolved: ResolvedCorsOptions = {\n\t\torigin: options.origin ?? '*',\n\t\theaders: options.headers ?? DEFAULT_CORS_HEADERS,\n\t\texposeHeaders: options.exposeHeaders ?? DEFAULT_CORS_EXPOSE_HEADERS,\n\t\tmethods: options.methods ?? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n\t\tcredentials: options.credentials ?? false,\n\t};\n\t// Browsers reject `Access-Control-Allow-Origin: *` on credentialed\n\t// requests — cookies demand a deliberate origin choice. Fail at boot with\n\t// a clear message, not per-request as an opaque browser error. Reflecting\n\t// every origin stays possible, but only as an explicit opt-in.\n\tif (resolved.credentials && resolved.origin === '*') {\n\t\tthrow new Error(\n\t\t\t\"withCors: credentials:true cannot be combined with origin:'*' (browsers reject it). \" +\n\t\t\t\t'Pass the frontend origin, or a predicate — `origin: () => true` deliberately reflects any origin.',\n\t\t);\n\t}\n\treturn resolved;\n}\n\n// The response headers for one request. Pure — withCors and the adapters'\n// native middlewares all emit exactly this, so the header contract cannot\n// fork per framework.\nexport function corsHeadersFor(\n\tresolved: ResolvedCorsOptions,\n\trequestOrigin: string,\n): Record<string, string> {\n\tconst { origin, headers, exposeHeaders, methods, credentials } = resolved;\n\n\tconst allowOrigin =\n\t\ttypeof origin === 'function' ? (origin(requestOrigin) ? requestOrigin : '') : origin;\n\n\tconst corsHeaders: Record<string, string> = {\n\t\t'Access-Control-Max-Age': '86400',\n\t\t'Access-Control-Allow-Methods': methods.join(', '),\n\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t};\n\tif (exposeHeaders.length > 0) {\n\t\tcorsHeaders['Access-Control-Expose-Headers'] = exposeHeaders.join(', ');\n\t}\n\tif (credentials) corsHeaders['Access-Control-Allow-Credentials'] = 'true';\n\t// Omit the header entirely for a denied origin (an empty ACAO value is\n\t// invalid); when the value varies by request origin, say so — otherwise a\n\t// shared cache can serve one origin's ACAO to another.\n\tif (allowOrigin) corsHeaders['Access-Control-Allow-Origin'] = allowOrigin;\n\tif (typeof origin === 'function') corsHeaders['Vary'] = 'Origin';\n\n\treturn corsHeaders;\n}\n\nexport function withCors(options: CorsOptions = {}): Middleware {\n\tconst resolved = resolveCorsOptions(options);\n\n\treturn async (ctx, next) => {\n\t\tconst corsHeaders = corsHeadersFor(resolved, ctx.request.headers.get('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","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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/background.ts","../src/app.ts","../src/router.ts","../src/compose.ts","../src/middlewares/cors.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\tIHandleInit,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\n} from './types';\n\nexport { OPERATIONS } from './constants';\nexport {\n\tbackground,\n\tsetBackgroundRunner,\n\tisServerlessRuntime,\n\tresolveBackgroundMode,\n\ttype BackgroundMode,\n} from './background';\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","// Work dispatched off the request path — notification emails, webhook\n// deliveries, domain events. On a long-running host a detached promise simply\n// finishes in the background. On serverless it does NOT: the instance is\n// frozen the moment the response is written, so the work is abandoned\n// mid-flight. The user is told \"check your email\" and nothing is ever sent,\n// with no error anywhere, because the code that would have logged it never ran.\n\nexport type BackgroundMode = 'auto' | 'await' | 'detach';\n\n/**\n * Environment markers that mean \"this process is frozen between requests\".\n *\n * Deliberately a positive list of SERVERLESS platforms rather than an attempt\n * to detect long-running ones: there is no reliable signal for \"this process\n * outlives the response\", so EC2 / Docker / bare metal are the FALLBACK and\n * can never be misdetected. An unknown serverless platform behaves exactly as\n * it does today until it is added here — or until the deployment sets\n * FONDERIE_BACKGROUND_TASKS=await.\n */\nconst SERVERLESS_MARKERS = [\n\t'VERCEL', // Vercel\n\t'AWS_LAMBDA_FUNCTION_NAME', // AWS Lambda, Netlify Functions\n\t'FUNCTION_TARGET', // Google Cloud Functions\n\t'K_SERVICE', // Google Cloud Run (CPU is throttled outside a request)\n\t'FUNCTIONS_WORKER_RUNTIME', // Azure Functions\n];\n\nexport function isServerlessRuntime(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn SERVERLESS_MARKERS.some((key) => !!env[key]);\n}\n\nexport function resolveBackgroundMode(env: NodeJS.ProcessEnv = process.env): 'await' | 'detach' {\n\tconst configured = (env['FONDERIE_BACKGROUND_TASKS'] ?? 'auto').trim().toLowerCase();\n\tif (configured === 'await' || configured === 'detach') return configured;\n\treturn isServerlessRuntime(env) ? 'await' : 'detach';\n}\n\nfunction resolveTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {\n\tconst raw = Number(env['FONDERIE_BACKGROUND_TIMEOUT_MS']);\n\treturn Number.isFinite(raw) && raw > 0 ? raw : 5000;\n}\n\n// A platform-provided \"keep the instance alive until this settles\" — Vercel's\n// waitUntil and its equivalents. Strictly better than awaiting, because the\n// work completes WITHOUT delaying the response. An adapter or app wires it\n// once at boot; when present it wins over both modes.\nlet runner: ((work: Promise<unknown>) => void) | null = null;\n\nexport function setBackgroundRunner(fn: ((work: Promise<unknown>) => void) | null): void {\n\trunner = fn;\n}\n\n/**\n * Hand off work that must not block the response but must still complete.\n *\n * Callers `await` this. In detach mode that resolves immediately (today's\n * behaviour, no added latency); in await mode it waits for the work, bounded\n * by a timeout so a hung provider degrades to \"the email was lost\" rather than\n * \"signup hangs\". Rejections are swallowed either way — background work must\n * never fail the request that triggered it.\n */\nexport async function background(work: Promise<unknown> | undefined): Promise<void> {\n\tif (!work) return;\n\tconst settled = Promise.resolve(work).catch(() => undefined);\n\n\tif (runner) {\n\t\trunner(settled);\n\t\treturn;\n\t}\n\tif (resolveBackgroundMode() === 'detach') return;\n\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tawait Promise.race([\n\t\tsettled,\n\t\tnew Promise<void>((resolve) => {\n\t\t\ttimer = setTimeout(resolve, resolveTimeoutMs());\n\t\t\t// Don't hold the event loop open on a long-running host.\n\t\t\ttimer.unref?.();\n\t\t}),\n\t]);\n\tif (timer) clearTimeout(timer);\n}\n","import { networkInterfaces } from 'node:os';\nimport { createServer, type Server } from 'node:http';\n\nimport type {\n\tMiddleware,\n\tIFonderieApp,\n\tIFonderieContext,\n\tIHandleInit,\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// `init.meta` is how an adapter hands over what only IT can observe (the\n\t// socket's client IP). Without it those facts die with the adapter's own\n\t// context, because handle() builds a fresh one.\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 is a deliberate follow-up.\n\n\tasync handle(request: Request, init?: IHandleInit): 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\t// Facts the ADAPTER knows and the pipeline cannot rediscover — the\n\t\t\t// client IP above all: a Web Standard Request carries no socket\n\t\t\t// address, so if the adapter's value is not seeded here it is simply\n\t\t\t// lost, and every fonderie-owned route sees `undefined`. Copied, not\n\t\t\t// aliased, so a request never mutates the adapter's own context.\n\t\t\tmeta: { ...init?.meta },\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","import type { Middleware } from '../types';\n\n// Request headers @fonderie/client sends on browser calls. A preflight\n// rejects the WHOLE request when any of them is missing from the allow-list,\n// so they ship as defaults here, in lockstep with the client:\n// X-Request-ID — request correlation (client >= 0.19)\n// traceparent — W3C trace context (client >= 0.20)\n// X-Workspace-ID — workspace scoping (setWorkspaceId)\nexport const FONDERIE_CLIENT_HEADERS = ['X-Request-ID', 'traceparent', 'X-Workspace-ID'];\n\nexport const DEFAULT_CORS_HEADERS = ['Content-Type', 'Authorization', ...FONDERIE_CLIENT_HEADERS];\n\n// Response headers browser JS is allowed to read. Without X-Request-ID here\n// the client cannot see the echoed correlation id — FonderieApiError.requestId\n// would silently stay at the client-minted value instead of the server echo.\nexport const DEFAULT_CORS_EXPOSE_HEADERS = ['X-Request-ID'];\n\nexport interface CorsOptions {\n\tmethods?: string[];\n\theaders?: string[];\n\t/** Response headers exposed to browser JS (Access-Control-Expose-Headers). */\n\texposeHeaders?: string[];\n\t/**\n\t * Who may call this API from a browser. A single origin, a list (apex and\n\t * www are two different origins), or a predicate for patterns such as\n\t * preview deployments.\n\t *\n\t * String forms are NORMALIZED — see normalizeOrigin. A predicate receives\n\t * the raw `Origin` header and owns its own matching.\n\t */\n\torigin?: string | string[] | ((requestOrigin: string) => boolean);\n\t/**\n\t * Allow credentialed requests. @fonderie/client always fetches with\n\t * credentials:'include', so a browser frontend on another origin needs\n\t * this on. Requires an explicit `origin` — browsers reject '*' on\n\t * credentialed responses.\n\t */\n\tcredentials?: boolean;\n}\n\nexport type ResolvedCorsOptions = Required<CorsOptions>;\n\n/**\n * An `Origin` header is `scheme://host[:port]` and, per RFC 6454, never carries\n * a path or a trailing slash — so a configured origin with one can never match\n * anything. That makes it unambiguously a typo rather than intent, and the\n * usual one: every address bar and dashboard \"copy URL\" hands you the slash.\n *\n * The failure it caused was a total outage with a misleading message — the\n * browser blocks every request and the app reports \"can't reach the server\" —\n * so normalizing beats honouring a value that cannot work. Scheme and host are\n * case-insensitive and browsers send them lowercased, so casing is folded too.\n *\n * A path (`https://x.com/app`) is a DIFFERENT mistake that normalizing cannot\n * silently repair, so it warns instead.\n */\nexport function normalizeOrigin(origin: string): string {\n\tconst trimmed = origin.trim();\n\tif (trimmed === '*') return trimmed;\n\n\tconst withoutTrailingSlashes = trimmed.replace(/\\/+$/, '');\n\n\t// Lowercase only scheme://host[:port]; anything after would be a path,\n\t// which is reported below rather than quietly reshaped.\n\tconst match = /^([a-zA-Z][a-zA-Z0-9+.-]*:\\/\\/[^/]+)(\\/.*)?$/.exec(withoutTrailingSlashes);\n\tif (!match) return withoutTrailingSlashes;\n\n\tconst [, schemeAndHost, path] = match;\n\tif (path) {\n\t\tconsole.warn(\n\t\t\t`[fonderie] CORS origin \"${origin}\" contains a path. An Origin header is ` +\n\t\t\t\t'scheme://host[:port] only, so this can never match a real request — ' +\n\t\t\t\t`use \"${schemeAndHost!.toLowerCase()}\".`,\n\t\t);\n\t}\n\treturn schemeAndHost!.toLowerCase();\n}\n\n// Applies the defaults and rejects impossible combinations at boot. The\n// framework adapters' native cors() middlewares resolve through here too, so\n// every mounting style shares one contract and one failure mode.\nexport function resolveCorsOptions(options: CorsOptions = {}): ResolvedCorsOptions {\n\tconst rawOrigin = options.origin ?? '*';\n\tconst resolved: ResolvedCorsOptions = {\n\t\torigin:\n\t\t\ttypeof rawOrigin === 'string'\n\t\t\t\t? normalizeOrigin(rawOrigin)\n\t\t\t\t: Array.isArray(rawOrigin)\n\t\t\t\t\t? rawOrigin.map(normalizeOrigin)\n\t\t\t\t\t: rawOrigin,\n\t\theaders: options.headers ?? DEFAULT_CORS_HEADERS,\n\t\texposeHeaders: options.exposeHeaders ?? DEFAULT_CORS_EXPOSE_HEADERS,\n\t\tmethods: options.methods ?? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n\t\tcredentials: options.credentials ?? false,\n\t};\n\t// Browsers reject `Access-Control-Allow-Origin: *` on credentialed\n\t// requests — cookies demand a deliberate origin choice. Fail at boot with\n\t// a clear message, not per-request as an opaque browser error. Reflecting\n\t// every origin stays possible, but only as an explicit opt-in.\n\tconst allowsWildcard =\n\t\tresolved.origin === '*' ||\n\t\t(Array.isArray(resolved.origin) && resolved.origin.includes('*'));\n\tif (resolved.credentials && allowsWildcard) {\n\t\tthrow new Error(\n\t\t\t\"withCors: credentials:true cannot be combined with origin:'*' (browsers reject it). \" +\n\t\t\t\t'Pass the frontend origin, or a predicate — `origin: () => true` deliberately reflects any origin.',\n\t\t);\n\t}\n\treturn resolved;\n}\n\n// The response headers for one request. Pure — withCors and the adapters'\n// native middlewares all emit exactly this, so the header contract cannot\n// fork per framework.\nexport function corsHeadersFor(\n\tresolved: ResolvedCorsOptions,\n\trequestOrigin: string,\n): Record<string, string> {\n\tconst { origin, headers, exposeHeaders, methods, credentials } = resolved;\n\n\t// Echo the REQUEST's origin on a match, never the configured spelling: the\n\t// browser compares byte-for-byte against what it sent, so reflecting a\n\t// normalized-but-different string would fail the very check normalizing is\n\t// meant to survive.\n\tlet allowOrigin: string;\n\tif (typeof origin === 'function') {\n\t\tallowOrigin = origin(requestOrigin) ? requestOrigin : '';\n\t} else if (Array.isArray(origin)) {\n\t\tallowOrigin = origin.includes(normalizeOrigin(requestOrigin)) ? requestOrigin : '';\n\t} else if (origin === '*') {\n\t\tallowOrigin = '*';\n\t} else {\n\t\tallowOrigin = normalizeOrigin(requestOrigin) === origin ? requestOrigin : '';\n\t}\n\n\tconst corsHeaders: Record<string, string> = {\n\t\t'Access-Control-Max-Age': '86400',\n\t\t'Access-Control-Allow-Methods': methods.join(', '),\n\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t};\n\tif (exposeHeaders.length > 0) {\n\t\tcorsHeaders['Access-Control-Expose-Headers'] = exposeHeaders.join(', ');\n\t}\n\tif (credentials) corsHeaders['Access-Control-Allow-Credentials'] = 'true';\n\t// Omit the header entirely for a denied origin (an empty ACAO value is\n\t// invalid); when the value varies by request origin, say so — otherwise a\n\t// shared cache can serve one origin's ACAO to another.\n\tif (allowOrigin) corsHeaders['Access-Control-Allow-Origin'] = allowOrigin;\n\t// Vary whenever the emitted value depends on the request — a shared cache\n\t// must not serve one origin's ACAO to another. Only the literal '*' is\n\t// request-independent.\n\tif (origin !== '*') corsHeaders['Vary'] = 'Origin';\n\n\treturn corsHeaders;\n}\n\nexport function withCors(options: CorsOptions = {}): Middleware {\n\tconst resolved = resolveCorsOptions(options);\n\n\treturn async (ctx, next) => {\n\t\tconst corsHeaders = corsHeadersFor(resolved, ctx.request.headers.get('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","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.d.cts
CHANGED
|
@@ -14,6 +14,21 @@ declare const OPERATIONS: {
|
|
|
14
14
|
readonly DELETE: "delete";
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
type BackgroundMode = 'auto' | 'await' | 'detach';
|
|
18
|
+
declare function isServerlessRuntime(env?: NodeJS.ProcessEnv): boolean;
|
|
19
|
+
declare function resolveBackgroundMode(env?: NodeJS.ProcessEnv): 'await' | 'detach';
|
|
20
|
+
declare function setBackgroundRunner(fn: ((work: Promise<unknown>) => void) | null): void;
|
|
21
|
+
/**
|
|
22
|
+
* Hand off work that must not block the response but must still complete.
|
|
23
|
+
*
|
|
24
|
+
* Callers `await` this. In detach mode that resolves immediately (today's
|
|
25
|
+
* behaviour, no added latency); in await mode it waits for the work, bounded
|
|
26
|
+
* by a timeout so a hung provider degrades to "the email was lost" rather than
|
|
27
|
+
* "signup hangs". Rejections are swallowed either way — background work must
|
|
28
|
+
* never fail the request that triggered it.
|
|
29
|
+
*/
|
|
30
|
+
declare function background(work: Promise<unknown> | undefined): Promise<void>;
|
|
31
|
+
|
|
17
32
|
declare class MetricsRegistry {
|
|
18
33
|
private counters;
|
|
19
34
|
inc(name: string, labels?: Record<string, string>, by?: number): void;
|
|
@@ -62,4 +77,4 @@ declare function decodeKeysetCursor(cursor: string): {
|
|
|
62
77
|
id: string;
|
|
63
78
|
} | null;
|
|
64
79
|
|
|
65
|
-
export { FonderieApp, FonderieConfig, IFonderieApp, IFonderieContext, IFonderieModule, IHandleInit, IReadinessReport, ISecurityReport, MIN_SECRET_LENGTH, MetricsRegistry, Middleware, OPERATIONS, PLACEHOLDER_SECRET, compose, constantTimeEqual, decodeKeysetCursor, encodeKeysetCursor, secretStrengthProblem, withMetrics };
|
|
80
|
+
export { type BackgroundMode, FonderieApp, FonderieConfig, IFonderieApp, IFonderieContext, IFonderieModule, IHandleInit, IReadinessReport, ISecurityReport, MIN_SECRET_LENGTH, MetricsRegistry, Middleware, OPERATIONS, PLACEHOLDER_SECRET, background, compose, constantTimeEqual, decodeKeysetCursor, encodeKeysetCursor, isServerlessRuntime, resolveBackgroundMode, secretStrengthProblem, setBackgroundRunner, withMetrics };
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,21 @@ declare const OPERATIONS: {
|
|
|
14
14
|
readonly DELETE: "delete";
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
type BackgroundMode = 'auto' | 'await' | 'detach';
|
|
18
|
+
declare function isServerlessRuntime(env?: NodeJS.ProcessEnv): boolean;
|
|
19
|
+
declare function resolveBackgroundMode(env?: NodeJS.ProcessEnv): 'await' | 'detach';
|
|
20
|
+
declare function setBackgroundRunner(fn: ((work: Promise<unknown>) => void) | null): void;
|
|
21
|
+
/**
|
|
22
|
+
* Hand off work that must not block the response but must still complete.
|
|
23
|
+
*
|
|
24
|
+
* Callers `await` this. In detach mode that resolves immediately (today's
|
|
25
|
+
* behaviour, no added latency); in await mode it waits for the work, bounded
|
|
26
|
+
* by a timeout so a hung provider degrades to "the email was lost" rather than
|
|
27
|
+
* "signup hangs". Rejections are swallowed either way — background work must
|
|
28
|
+
* never fail the request that triggered it.
|
|
29
|
+
*/
|
|
30
|
+
declare function background(work: Promise<unknown> | undefined): Promise<void>;
|
|
31
|
+
|
|
17
32
|
declare class MetricsRegistry {
|
|
18
33
|
private counters;
|
|
19
34
|
inc(name: string, labels?: Record<string, string>, by?: number): void;
|
|
@@ -62,4 +77,4 @@ declare function decodeKeysetCursor(cursor: string): {
|
|
|
62
77
|
id: string;
|
|
63
78
|
} | null;
|
|
64
79
|
|
|
65
|
-
export { FonderieApp, FonderieConfig, IFonderieApp, IFonderieContext, IFonderieModule, IHandleInit, IReadinessReport, ISecurityReport, MIN_SECRET_LENGTH, MetricsRegistry, Middleware, OPERATIONS, PLACEHOLDER_SECRET, compose, constantTimeEqual, decodeKeysetCursor, encodeKeysetCursor, secretStrengthProblem, withMetrics };
|
|
80
|
+
export { type BackgroundMode, FonderieApp, FonderieConfig, IFonderieApp, IFonderieContext, IFonderieModule, IHandleInit, IReadinessReport, ISecurityReport, MIN_SECRET_LENGTH, MetricsRegistry, Middleware, OPERATIONS, PLACEHOLDER_SECRET, background, compose, constantTimeEqual, decodeKeysetCursor, encodeKeysetCursor, isServerlessRuntime, resolveBackgroundMode, secretStrengthProblem, setBackgroundRunner, withMetrics };
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,54 @@ var OPERATIONS = {
|
|
|
6
6
|
DELETE: "delete"
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
// src/background.ts
|
|
10
|
+
var SERVERLESS_MARKERS = [
|
|
11
|
+
"VERCEL",
|
|
12
|
+
// Vercel
|
|
13
|
+
"AWS_LAMBDA_FUNCTION_NAME",
|
|
14
|
+
// AWS Lambda, Netlify Functions
|
|
15
|
+
"FUNCTION_TARGET",
|
|
16
|
+
// Google Cloud Functions
|
|
17
|
+
"K_SERVICE",
|
|
18
|
+
// Google Cloud Run (CPU is throttled outside a request)
|
|
19
|
+
"FUNCTIONS_WORKER_RUNTIME"
|
|
20
|
+
// Azure Functions
|
|
21
|
+
];
|
|
22
|
+
function isServerlessRuntime(env = process.env) {
|
|
23
|
+
return SERVERLESS_MARKERS.some((key) => !!env[key]);
|
|
24
|
+
}
|
|
25
|
+
function resolveBackgroundMode(env = process.env) {
|
|
26
|
+
const configured = (env["FONDERIE_BACKGROUND_TASKS"] ?? "auto").trim().toLowerCase();
|
|
27
|
+
if (configured === "await" || configured === "detach") return configured;
|
|
28
|
+
return isServerlessRuntime(env) ? "await" : "detach";
|
|
29
|
+
}
|
|
30
|
+
function resolveTimeoutMs(env = process.env) {
|
|
31
|
+
const raw = Number(env["FONDERIE_BACKGROUND_TIMEOUT_MS"]);
|
|
32
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 5e3;
|
|
33
|
+
}
|
|
34
|
+
var runner = null;
|
|
35
|
+
function setBackgroundRunner(fn) {
|
|
36
|
+
runner = fn;
|
|
37
|
+
}
|
|
38
|
+
async function background(work) {
|
|
39
|
+
if (!work) return;
|
|
40
|
+
const settled = Promise.resolve(work).catch(() => void 0);
|
|
41
|
+
if (runner) {
|
|
42
|
+
runner(settled);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (resolveBackgroundMode() === "detach") return;
|
|
46
|
+
let timer;
|
|
47
|
+
await Promise.race([
|
|
48
|
+
settled,
|
|
49
|
+
new Promise((resolve) => {
|
|
50
|
+
timer = setTimeout(resolve, resolveTimeoutMs());
|
|
51
|
+
timer.unref?.();
|
|
52
|
+
})
|
|
53
|
+
]);
|
|
54
|
+
if (timer) clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
|
|
9
57
|
// src/app.ts
|
|
10
58
|
import { networkInterfaces } from "os";
|
|
11
59
|
import { createServer } from "http";
|
|
@@ -687,6 +735,7 @@ export {
|
|
|
687
735
|
OPERATIONS,
|
|
688
736
|
PLACEHOLDER_SECRET,
|
|
689
737
|
arrayOrEmpty,
|
|
738
|
+
background,
|
|
690
739
|
booleanOrFalse,
|
|
691
740
|
compose,
|
|
692
741
|
constantTimeEqual,
|
|
@@ -694,9 +743,12 @@ export {
|
|
|
694
743
|
decodeKeysetCursor,
|
|
695
744
|
defineConfig,
|
|
696
745
|
encodeKeysetCursor,
|
|
746
|
+
isServerlessRuntime,
|
|
697
747
|
numberOrZero,
|
|
748
|
+
resolveBackgroundMode,
|
|
698
749
|
secretStrengthProblem,
|
|
699
750
|
setApiResponse,
|
|
751
|
+
setBackgroundRunner,
|
|
700
752
|
stringOrEmpty,
|
|
701
753
|
withMetrics
|
|
702
754
|
};
|