@fonderie/core 0.9.0 → 0.10.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/app.ts","../src/router.ts","../src/compose.ts","../src/response.ts","../src/middlewares/not-found.ts","../src/middlewares/body-parser.ts","../src/middlewares/security-headers.ts","../src/middlewares/error-handler.ts","../src/crypto.ts","../src/secret-strength.ts","../src/metrics.ts","../src/config.ts","../src/parser.ts","../src/keyset-cursor.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport type {\n\tITenant,\n\tIRouter,\n\tOperation,\n\tIAuthUser,\n\tMiddleware,\n\tIWorkspace,\n\tIRouteMatch,\n\tIFonderieApp,\n\tIFonderieModule,\n\tIFonderieContext,\n\tICourierMessage,\n\tIDefaultTemplate,\n\tIFonderieContextMeta,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\n} from './types';\n\nexport { OPERATIONS } from './constants';\n\nexport { FonderieApp, DEFAULT_MAX_BODY_BYTES } from './app';\nexport { defineConfig } from './config';\nexport { compose } from './compose';\nexport type { FonderieConfig } from './config';\n\n// Built-in middleware — import from '@fonderie/core/middlewares', not the root barrel\n\n// Parser utilities\nexport { stringOrEmpty, booleanOrFalse, arrayOrEmpty, numberOrZero, dateOrEmpty } from './parser';\nexport { constantTimeEqual } from './crypto';\nexport { MIN_SECRET_LENGTH, PLACEHOLDER_SECRET, secretStrengthProblem } from './secret-strength';\nexport { encodeKeysetCursor, decodeKeysetCursor } from './keyset-cursor';\n\n// Response helpers\nexport type { IApiError, HttpStatus } from './response';\nexport { HTTP, setApiResponse } from './response';\n\n// NOT exported: adapters/, router internals, error-handler, not-found\n// Those are consumed by FonderieApp, never by users directly\n\nexport { MetricsRegistry, withMetrics } from './metrics';\n","import type { Operation } from './types';\n\n// CRUD operation names shared across modules. Canonical home is core so the\n// adapters (which peer only on core) can re-export OPERATIONS without loading\n// @fonderie/permissions; permissions re-exports it for backward compatibility.\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n","import { networkInterfaces } from 'node:os';\nimport { createServer, type Server } from 'node:http';\n\nimport type {\n\tMiddleware,\n\tIFonderieApp,\n\tIFonderieContext,\n\tIFonderieModule,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\n} from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { withBody } from './middlewares/body-parser';\nimport { withSecurityHeaders } from './middlewares/security-headers';\nimport { MetricsRegistry, withMetrics } from './metrics';\n\n/**\n * Default cap on the request body the built-in `listen()` server will buffer,\n * in bytes (5 MiB) — same default as the adapter packages. Override per app\n * via `config.maxBodyBytes`.\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\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, then baseline security headers (nosniff always; HSTS\n\t\t// over HTTPS). Apps can layer more via `.use()`.\n\t\tthis.middlewares = [withBody, withSecurityHeaders()];\n\t\tif (config.metrics) this.middlewares.push(withMetrics(this.metrics));\n\t}\n\n\tlisten(\n\t\tport: number,\n\t\toptions: {\n\t\t\tname?: string;\n\t\t\tversion?: string;\n\t\t\tenv?: string;\n\t\t\tquiet?: boolean; // suppress the startup banner (tests, quiet deploys)\n\t\t} = {},\n\t): Server {\n\t\tconst {\n\t\t\tname = 'Fonderie',\n\t\t\tversion = '0.0.1',\n\t\t\tenv = process.env['NODE_ENV'] ?? 'development',\n\t\t\tquiet = false,\n\t\t} = options;\n\n\t\tconst maxBodyBytes = this.config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\tconst host = req.headers.host ?? 'localhost';\n\t\t\tconst url = `http://${host}${req.url ?? '/'}`;\n\t\t\tconst headers = new Headers();\n\n\t\t\tfor (const [key, value] of Object.entries(req.headers)) {\n\t\t\t\tif (!value) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\tfor (const v of value) headers.append(key, v);\n\t\t\t\t} else {\n\t\t\t\t\theaders.set(key, value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\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\tpayloadTooLarge(res);\n\t\t\t\treq.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}).listen(port, () => {\n\t\t\tif (quiet) return;\n\t\t\tconst ip = getLocalIPv4();\n\t\t\tconst mode = env.includes('dev') ? 'development' : 'production';\n\n\t\t\tconsole.log(\n\t\t\t\t`\\n ƒ ${name} v${version} ${mode}\\n` +\n\t\t\t\t\t`\\n Local http://localhost:${port}` +\n\t\t\t\t\t`\\n Network http://${ip}:${port}\\n`,\n\t\t\t);\n\t\t});\n\t\treturn server;\n\t}\n\n\t// ─── Module registration ───────────────────────────────\n\n\tregister(module: IFonderieModule): this {\n\t\tthis.modules.set(module.name, module);\n\t\treturn this;\n\t}\n\n\t// Aggregate every registered module's self-reported readiness problems into\n\t// one report. Call it before boot to gate a deploy, or from a readiness\n\t// endpoint. `ok` is false when any module reports an `error`-severity problem\n\t// (e.g. a weak jwtSecret). Modules opt in via `checkReadiness`.\n\tcheckProductionReadiness(): IReadinessReport {\n\t\tconst problems: IReadinessProblem[] = [];\n\t\tfor (const module of this.modules.values()) {\n\t\t\tif (module.checkReadiness) problems.push(...module.checkReadiness());\n\t\t}\n\t\treturn { ok: !problems.some((p) => p.severity === 'error'), problems };\n\t}\n\n\t// A point-in-time control-posture snapshot for SOC 2 evidence: which modules\n\t// are registered and the current readiness report. Serialise to a file/log\n\t// (e.g. on a schedule) as an audit artifact.\n\tsecurityReport(): ISecurityReport {\n\t\treturn {\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t\tenv: process.env['NODE_ENV'] ?? 'development',\n\t\t\tregisteredModules: [...this.modules.keys()].sort(),\n\t\t\treadiness: this.checkProductionReadiness(),\n\t\t};\n\t}\n\n\tasync boot(): Promise<this> {\n\t\t// Fail closed before any side effects (transports, listeners): a\n\t\t// production deploy with an error-severity readiness problem must not boot.\n\t\tthis.enforceProductionReadiness();\n\t\tfor (const module of topoSort([...this.modules.values()])) {\n\t\t\tawait module.install(this);\n\t\t}\n\t\tthis.registerHealthRoutes();\n\t\treturn this;\n\t}\n\n\t// Liveness (/healthz) and readiness (/readyz) probes. Registered unprefixed so\n\t// they sit at a stable path regardless of basePath. Enabled unless disabled.\n\tprivate registerHealthRoutes(): void {\n\t\tif (this.config.healthChecks === false) return;\n\n\t\tthis.router.add('GET', '/healthz', compose([async () => Response.json({ status: 'ok' })]));\n\n\t\tif (this.config.metrics) {\n\t\t\tthis.router.add(\n\t\t\t\t'GET',\n\t\t\t\t'/metrics',\n\t\t\t\tcompose([\n\t\t\t\t\tasync () =>\n\t\t\t\t\t\tnew Response(this.metrics.render(), {\n\t\t\t\t\t\t\tstatus: 200,\n\t\t\t\t\t\t\theaders: { 'content-type': 'text/plain; version=0.0.4' },\n\t\t\t\t\t\t}),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tthis.router.add(\n\t\t\t'GET',\n\t\t\t'/readyz',\n\t\t\tcompose([\n\t\t\t\tasync () => {\n\t\t\t\t\tconst report = this.checkProductionReadiness();\n\t\t\t\t\tlet dependencies = true;\n\t\t\t\t\tif (this.config.readyProbe) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdependencies = Boolean(await this.config.readyProbe());\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tdependencies = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst ready = report.ok && dependencies;\n\t\t\t\t\treturn Response.json(\n\t\t\t\t\t\t{ status: ready ? 'ready' : 'not_ready', dependencies, problems: report.problems },\n\t\t\t\t\t\t{ status: ready ? 200 : 503 },\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t]),\n\t\t);\n\t}\n\n\t// Throws in production when `checkProductionReadiness()` reports any\n\t// error-severity problem, unless explicitly overridden. No-op otherwise.\n\tprivate enforceProductionReadiness(): void {\n\t\tif (process.env['NODE_ENV'] !== 'production') return;\n\t\tif (this.config.skipProductionReadinessGate) return;\n\t\tconst { ok, problems } = this.checkProductionReadiness();\n\t\tif (ok) return;\n\t\tconst errors = problems.filter((p) => p.severity === 'error');\n\t\tthrow new Error(\n\t\t\t`[fonderie] refusing to boot in production — ${errors.length} readiness ` +\n\t\t\t\t`error(s): ${errors.map((e) => `${e.module}: ${e.message}`).join('; ')}. ` +\n\t\t\t\t'Fix them, or set skipProductionReadinessGate: true to override (not recommended).',\n\t\t);\n\t}\n\n\t// Runs global middleware only (no routing, no 404).\n\t// Adapter packages call this to populate user/workspace/meta into their\n\t// native context before handing off to user-defined route handlers.\n\tasync buildContext(request: Request): Promise<IFonderieContext> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: { _buildContext: true },\n\t\t};\n\t\tawait compose(this.middlewares)(ctx, async () => new Response());\n\t\tdelete ctx.meta['_buildContext'];\n\t\treturn ctx;\n\t}\n\n\t// ─── Middleware ────────────────────────────────────────\n\n\tuse(middleware: Middleware): this {\n\t\tthis.middlewares.push(middleware);\n\t\treturn this;\n\t}\n\n\t// Modules call this to register their routes\n\taddRoute(method: string, path: string, ...handlers: Middleware[]): void {\n\t\tthis.router.add(method, this.prefix + path, compose(handlers));\n\t}\n\n\t// ─── The core handler ──────────────────────────────────\n\t// This is the ONE thing every adapter calls.\n\t// Takes a Web Standard Request, returns a Web Standard Response.\n\n\tasync handle(request: Request): Promise<Response> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: {},\n\t\t};\n\n\t\t// Build the pipeline: global middleware → router → 404\n\t\tconst pipeline = compose([\n\t\t\t...this.middlewares,\n\t\t\trouterMiddleware(this.router),\n\t\t\tnotFoundMiddleware(),\n\t\t]);\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\tresponse = this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t\treturn this.config.onResponse ? this.transformResponse(response, request) : response;\n\t}\n\n\t// Apply config.onResponse to a JSON response body, preserving status, headers,\n\t// and cookies. Non-JSON responses and hooks that return `undefined` pass through.\n\tprivate async transformResponse(response: Response, request: Request): Promise<Response> {\n\t\tconst contentType = response.headers.get('content-type') ?? '';\n\t\tif (!contentType.includes('application/json')) return response;\n\t\tlet body: unknown;\n\t\ttry {\n\t\t\tbody = await response.clone().json();\n\t\t} catch {\n\t\t\treturn response; // not valid JSON after all — leave untouched\n\t\t}\n\t\tconst transformed = this.config.onResponse!(body, { status: response.status, request });\n\t\tif (transformed === undefined) return response;\n\t\t// Preserve headers/cookies; drop content-length (the new body sets its own).\n\t\tconst headers = new Headers(response.headers);\n\t\theaders.delete('content-length');\n\t\theaders.delete('content-type');\n\t\treturn Response.json(transformed, { status: response.status, headers });\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import type { Middleware } from '../types';\n\nexport interface SecurityHeadersOptions {\n\t// HSTS max-age in seconds. Default 180 days. Set 0 to omit the header.\n\thstsMaxAge?: number;\n\t// Add `includeSubDomains` to the HSTS header. Off by default — only enable\n\t// once every subdomain is known to serve HTTPS.\n\thstsIncludeSubDomains?: boolean;\n\t// Add `preload` to the HSTS header (implies includeSubDomains). Off by default.\n\thstsPreload?: boolean;\n}\n\n// Baseline response hardening. `X-Content-Type-Options: nosniff` is always safe.\n// HSTS is only meaningful — and only emitted — over HTTPS: browsers ignore it on\n// plain HTTP, so gating on the effective scheme keeps local http/dev untouched\n// while enforcing TLS in production (behind a TLS-terminating proxy, detected via\n// X-Forwarded-Proto). Wired into the default pipeline by FonderieApp.\nexport function withSecurityHeaders(options: SecurityHeadersOptions = {}): Middleware {\n\tconst {\n\t\thstsMaxAge = 60 * 60 * 24 * 180,\n\t\thstsIncludeSubDomains = false,\n\t\thstsPreload = false,\n\t} = options;\n\n\tlet hsts = '';\n\tif (hstsMaxAge > 0) {\n\t\thsts = `max-age=${hstsMaxAge}`;\n\t\tif (hstsIncludeSubDomains || hstsPreload) hsts += '; includeSubDomains';\n\t\tif (hstsPreload) hsts += '; preload';\n\t}\n\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst patched = new Headers(response.headers);\n\n\t\tpatched.set('X-Content-Type-Options', 'nosniff');\n\n\t\tif (hsts && isHttps(ctx.request)) {\n\t\t\tpatched.set('Strict-Transport-Security', hsts);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n\n// HTTPS if the request URL is https, or a TLS-terminating proxy says so.\nfunction isHttps(request: Request): boolean {\n\tif (request.url.startsWith('https:')) return true;\n\tconst proto = request.headers.get('x-forwarded-proto');\n\treturn proto?.split(',')[0]?.trim() === 'https';\n}\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { timingSafeEqual } from 'node:crypto';\n\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\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,aAAa;AAAA,EACzB,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AACT;;;ACVA,qBAAkC;AAClC,uBAA0C;;;ACCnC,IAAM,SAAN,MAAgC;AAAA,EAC9B,SAAuE,CAAC;AAAA,EAEhF,IAAI,QAAgB,MAAc,SAA2B;AAC5D,SAAK,OAAO,KAAK,EAAE,QAAQ,OAAO,YAAY,GAAG,MAAM,QAAQ,CAAC;AAAA,EACjE;AAAA,EAEA,MAAM,QAAgB,MAAkC;AACvD,eAAW,SAAS,KAAK,QAAQ;AAChC,UAAI,MAAM,WAAW,OAAO,YAAY,GAAG;AAC1C;AAAA,MACD;AACA,YAAM,SAAS,UAAU,MAAM,MAAM,IAAI;AACzC,UAAI,WAAW,MAAM;AACpB,eAAO,EAAE,SAAS,MAAM,SAAS,OAAO;AAAA,MACzC;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;AAIA,SAAS,UAAU,SAAiB,MAA6C;AAChF,QAAM,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAAM,QAAQ,OAAO,EAAE,KAAK;AACjE,QAAM,KAAK,QAAQ,MAAM,GAAG;AAC5B,QAAM,KAAK,MAAM,MAAM,GAAG;AAE1B,MAAI,GAAG,WAAW,GAAG,QAAQ;AAC5B,WAAO;AAAA,EACR;AAEA,QAAM,SAAiC,CAAC;AAExC,WAAS,IAAI,GAAG,IAAI,GAAG,QAAQ,KAAK;AACnC,UAAM,KAAK,GAAG,CAAC,KAAK;AACpB,UAAM,KAAK,GAAG,CAAC,KAAK;AACpB,QAAI,GAAG,WAAW,GAAG,GAAG;AACvB,aAAO,GAAG,MAAM,CAAC,CAAC,IAAI,mBAAmB,EAAE;AAAA,IAC5C,WAAW,OAAO,IAAI;AACrB,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SAAO;AACR;AAGO,SAAS,iBAAiB,QAA4B;AAC5D,SAAO,OAAO,KAAuB,SAAS;AAC7C,UAAM,MAAM,IAAI,IAAI,IAAI,QAAQ,GAAG;AACnC,UAAM,QAAQ,OAAO,MAAM,IAAI,QAAQ,QAAQ,IAAI,QAAQ;AAE3D,QAAI,CAAC,OAAO;AACX,aAAO,KAAK;AAAA,IACb;AAGA,QAAI,KAAK,SAAS,MAAM;AACxB,WAAO,MAAM,QAAQ,KAAK,IAAI;AAAA,EAC/B;AACD;;;AC5DO,SAAS,QAAQ,aAA2B;AAClD,SAAO,SAAU,KAAuB,UAAsD;AAC7F,QAAI,QAAQ;AAEZ,aAAS,SAAS,GAA8B;AAC/C,UAAI,KAAK,OAAO;AACf,cAAM,IAAI,MAAM,8BAA8B;AAAA,MAC/C;AACA,cAAQ;AACR,YAAM,KAAK,YAAY,CAAC,KAAK;AAC7B,aAAO,GAAG,KAAK,MAAM,SAAS,IAAI,CAAC,CAAC;AAAA,IACrC;AAEA,WAAO,SAAS,CAAC;AAAA,EAClB;AACD;;;AClBO,IAAM,OAAO;AAAA,EACnB,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,MAAM;AAAA,EACN,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,qBAAqB;AACtB;AAgBO,SAAS,eACf,QACA,QACA,aACA,SACW;AACX,QAAM,OAAgC,EAAE,QAAQ,YAAY;AAC5D,MAAI,YAAY,QAAW;AAC1B,SAAK,SAAS,MAAM,WAAW,SAAS,IAAI;AAAA,EAC7C;AACA,SAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;AACtC;;;AC1CO,SAAS,qBAAiC;AAChD,SAAO,OAAO,MAAM,UAAU,eAAe,KAAK,WAAW,aAAa,WAAW;AACtF;;;ACFO,IAAM,WAAuB,OAAO,KAAK,SAAS;AACxD,QAAM,SAAS,IAAI,QAAQ,OAAO,YAAY;AAE9C,MAAI,WAAW,SAAS,WAAW,QAAQ;AAC1C,WAAO,KAAK;AAAA,EACb;AAEA,QAAM,KAAK,IAAI,QAAQ,QAAQ,IAAI,cAAc,KAAK;AAEtD,MAAI;AACH,QAAI,GAAG,SAAS,kBAAkB,GAAG;AACpC,YAAM,QAAQ,MAAM,IAAI,QAAQ,MAAM,EAAE,KAAK,GAAG,KAAK;AACrD,UAAI,KAAK,OAAO,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;AAAA,IAC5C,WAAW,GAAG,SAAS,mCAAmC,GAAG;AAC5D,YAAM,OAAO,MAAM,IAAI,QAAQ,MAAM,EAAE,KAAK;AAC5C,UAAI,KAAK,OAAO,OAAO,YAAY,IAAI,gBAAgB,IAAI,CAAC;AAAA,IAC7D;AAAA,EAED,QAAQ;AACP,WAAO,eAAe,KAAK,aAAa,mBAAmB,sBAAsB;AAAA,EAClF;AAEA,SAAO,KAAK;AACb;;;ACTO,SAAS,oBAAoB,UAAkC,CAAC,GAAe;AACrF,QAAM;AAAA,IACL,aAAa,KAAK,KAAK,KAAK;AAAA,IAC5B,wBAAwB;AAAA,IACxB,cAAc;AAAA,EACf,IAAI;AAEJ,MAAI,OAAO;AACX,MAAI,aAAa,GAAG;AACnB,WAAO,WAAW,UAAU;AAC5B,QAAI,yBAAyB,YAAa,SAAQ;AAClD,QAAI,YAAa,SAAQ;AAAA,EAC1B;AAEA,SAAO,OAAO,KAAK,SAAS;AAC3B,UAAM,WAAW,MAAM,KAAK;AAC5B,UAAM,UAAU,IAAI,QAAQ,SAAS,OAAO;AAE5C,YAAQ,IAAI,0BAA0B,SAAS;AAE/C,QAAI,QAAQ,QAAQ,IAAI,OAAO,GAAG;AACjC,cAAQ,IAAI,6BAA6B,IAAI;AAAA,IAC9C;AAEA,WAAO,IAAI,SAAS,SAAS,MAAM;AAAA,MAClC,SAAS;AAAA,MACT,QAAQ,SAAS;AAAA,MACjB,YAAY,SAAS;AAAA,IACtB,CAAC;AAAA,EACF;AACD;AAGA,SAAS,QAAQ,SAA2B;AAC3C,MAAI,QAAQ,IAAI,WAAW,QAAQ,EAAG,QAAO;AAC7C,QAAM,QAAQ,QAAQ,QAAQ,IAAI,mBAAmB;AACrD,SAAO,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,MAAM;AACzC;;;ACpDO,SAAS,oBAAoB,KAAwB;AAC3D,QAAM,MAAM,QAAQ,IAAI,UAAU,MAAM;AAExC,MAAI,eAAe,OAAO;AACzB,YAAQ,MAAM,cAAc,IAAI,SAAS,IAAI,KAAK;AAClD,WAAO;AAAA,MACN,KAAK;AAAA,MACL;AAAA,MACA,MAAM,IAAI,UAAU;AAAA,IACrB;AAAA,EACD;AAEA,UAAQ,MAAM,4BAA4B,GAAG;AAC7C,SAAO,eAAe,KAAK,cAAc,gBAAgB,uBAAuB;AACjF;;;AChBA,yBAAgC;AAOzB,SAAS,kBAAkB,GAAoB,GAA6B;AAClF,QAAM,OAAO,OAAO,SAAS,CAAC,IAAI,IAAI,OAAO,KAAK,CAAC;AACnD,QAAM,OAAO,OAAO,SAAS,CAAC,IAAI,IAAI,OAAO,KAAK,CAAC;AACnD,MAAI,KAAK,WAAW,KAAK,OAAQ,QAAO;AACxC,aAAO,oCAAgB,MAAM,IAAI;AAClC;;;ACRO,IAAM,oBAAoB;AAC1B,IAAM,qBACZ;AAIM,SAAS,sBAAsB,QAAoD;AACzF,MAAI,OAAO,SAAS,kBAAmB,QAAO;AAC9C,MAAI,mBAAmB,KAAK,MAAM,EAAG,QAAO;AAC5C,SAAO;AACR;;;ACPO,IAAM,kBAAN,MAAsB;AAAA,EACpB,WAAW,oBAAI,IAAoB;AAAA,EAE3C,IAAI,MAAc,SAAiC,CAAC,GAAG,KAAK,GAAS;AACpE,UAAM,MAAM,UAAU,MAAM,MAAM;AAClC,SAAK,SAAS,IAAI,MAAM,KAAK,SAAS,IAAI,GAAG,KAAK,KAAK,EAAE;AAAA,EAC1D;AAAA;AAAA,EAGA,SAAiB;AAChB,UAAM,QAAkB,CAAC;AACzB,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,SAAU,OAAM,KAAK,GAAG,GAAG,IAAI,KAAK,EAAE;AACtE,WAAO,MAAM,KAAK,IAAI,KAAK,MAAM,SAAS,OAAO;AAAA,EAClD;AACD;AAEA,SAAS,UAAU,MAAc,QAAwC;AACxE,QAAM,QAAQ,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,OAAO,CAAC,EAAE,QAAQ,MAAM,EAAE,CAAC,GAAG;AAC5F,SAAO,MAAM,SAAS,GAAG,IAAI,IAAI,MAAM,KAAK,GAAG,CAAC,MAAM;AACvD;AAGO,SAAS,YAAY,UAAuC;AAClE,SAAO,OAAO,KAAK,SAAS;AAC3B,UAAM,WAAW,MAAM,KAAK;AAC5B,UAAM,MAAM,GAAG,KAAK,MAAM,SAAS,SAAS,GAAG,CAAC;AAChD,aAAS,IAAI,uBAAuB,EAAE,cAAc,IAAI,CAAC;AACzD,WAAO;AAAA,EACR;AACD;;;AVXO,IAAM,yBAAyB,IAAI,OAAO;AAEjD,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC/B,0BAA0B;AACpC;AAEA,SAAS,gBAAgB,KAAoG;AAC5H,MAAI,aAAa;AACjB,MAAI,UAAU,gBAAgB,kBAAkB;AAChD,MAAI,IAAI,KAAK,UAAU,EAAE,QAAQ,qBAAqB,aAAa,yBAAyB,CAAC,CAAC;AAC/F;AAEO,IAAM,cAAN,MAA0C;AAAA,EACxC;AAAA,EACA;AAAA,EACA,SAAiB,IAAI,OAAO;AAAA,EAC5B,cAA4B,CAAC;AAAA,EAC7B,UAAwC,oBAAI,IAAI;AAAA,EAC/C,UAAU,IAAI,gBAAgB;AAAA,EAEvC,YAAY,QAAwB;AACnC,SAAK,SAAS;AACd,SAAK,UAAU,OAAO,YAAY,IAAI,QAAQ,OAAO,EAAE;AAGvD,SAAK,cAAc,CAAC,UAAU,oBAAoB,CAAC;AACnD,QAAI,OAAO,QAAS,MAAK,YAAY,KAAK,YAAY,KAAK,OAAO,CAAC;AAAA,EACpE;AAAA,EAEA,OACC,MACA,UAKI,CAAC,GACI;AACT,UAAM;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,MACV,MAAM,QAAQ,IAAI,UAAU,KAAK;AAAA,MACjC,QAAQ;AAAA,IACT,IAAI;AAEJ,UAAM,eAAe,KAAK,OAAO,gBAAgB;AAEjD,UAAM,aAAS,+BAAa,OAAO,KAAK,QAAQ;AAC/C,YAAM,OAAO,IAAI,QAAQ,QAAQ;AACjC,YAAM,MAAM,UAAU,IAAI,GAAG,IAAI,OAAO,GAAG;AAC3C,YAAM,UAAU,IAAI,QAAQ;AAE5B,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,OAAO,GAAG;AACvD,YAAI,CAAC,OAAO;AACX;AAAA,QACD;AAEA,YAAI,MAAM,QAAQ,KAAK,GAAG;AACzB,qBAAW,KAAK,MAAO,SAAQ,OAAO,KAAK,CAAC;AAAA,QAC7C,OAAO;AACN,kBAAQ,IAAI,KAAK,KAAK;AAAA,QACvB;AAAA,MACD;AAEA,YAAM,SAAS,IAAI,UAAU;AAC7B,YAAM,UAAU,CAAC,CAAC,OAAO,MAAM,EAAE,SAAS,OAAO,YAAY,CAAC;AAK9D,YAAM,WAAW,OAAO,IAAI,QAAQ,gBAAgB,CAAC;AACrD,UAAI,WAAW,OAAO,SAAS,QAAQ,KAAK,WAAW,cAAc;AACpE,wBAAgB,GAAG;AACnB,YAAI,QAAQ;AACZ;AAAA,MACD;AAIA,UAAI;AACJ,UAAI;AACH,eAAO,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AACrD,gBAAM,SAAmB,CAAC;AAC1B,cAAI,QAAQ;AACZ,cAAI,GAAG,QAAQ,CAAC,UAAkB;AACjC,qBAAS,MAAM;AACf,gBAAI,QAAQ,cAAc;AACzB,qBAAO,IAAI,qBAAqB,CAAC;AACjC,kBAAI,QAAQ;AACZ;AAAA,YACD;AACA,mBAAO,KAAK,KAAK;AAAA,UAClB,CAAC;AACD,cAAI,GAAG,OAAO,MAAM,QAAQ,OAAO,OAAO,MAAM,CAAC,CAAC;AAClD,cAAI,GAAG,SAAS,MAAM;AAAA,QACvB,CAAC;AAAA,MACF,SAAS,KAAK;AACb,YAAI,eAAe,sBAAsB;AACxC,0BAAgB,GAAG;AACnB;AAAA,QACD;AACA,YAAI,aAAa;AACjB,YAAI,IAAI;AACR;AAAA,MACD;AAEA,YAAM,UAAU,IAAI,QAAQ,KAAK;AAAA,QAChC;AAAA,QACA;AAAA,QACA,MAAM,WAAW,KAAK,SAAS,IAAI,IAAI,WAAW,IAAI,IAAI;AAAA,MAC3D,CAAC;AAED,YAAM,WAAW,MAAM,KAAK,OAAO,OAAO;AAE1C,UAAI,aAAa,SAAS;AAG1B,YAAM,aAAa,SAAS,QAAQ,eAAe,KAAK,CAAC;AACzD,UAAI,WAAW,OAAQ,KAAI,UAAU,cAAc,UAAU;AAC7D,eAAS,QAAQ,QAAQ,CAAC,GAAG,MAAM;AAClC,YAAI,EAAE,YAAY,MAAM,aAAc,KAAI,UAAU,GAAG,CAAC;AAAA,MACzD,CAAC;AACD,UAAI,IAAI,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC;AAAA,IAClD,CAAC,EAAE,OAAO,MAAM,MAAM;AACrB,UAAI,MAAO;AACX,YAAM,KAAK,aAAa;AACxB,YAAM,OAAO,IAAI,SAAS,KAAK,IAAI,gBAAgB;AAEnD,cAAQ;AAAA,QACP;AAAA,WAAS,IAAI,KAAK,OAAO,KAAK,IAAI;AAAA;AAAA,8BACA,IAAI;AAAA,oBACd,EAAE,IAAI,IAAI;AAAA;AAAA,MACnC;AAAA,IACD,CAAC;AACD,WAAO;AAAA,EACR;AAAA;AAAA,EAIA,SAASA,SAA+B;AACvC,SAAK,QAAQ,IAAIA,QAAO,MAAMA,OAAM;AACpC,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,2BAA6C;AAC5C,UAAM,WAAgC,CAAC;AACvC,eAAWA,WAAU,KAAK,QAAQ,OAAO,GAAG;AAC3C,UAAIA,QAAO,eAAgB,UAAS,KAAK,GAAGA,QAAO,eAAe,CAAC;AAAA,IACpE;AACA,WAAO,EAAE,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO,GAAG,SAAS;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAkC;AACjC,WAAO;AAAA,MACN,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC,KAAK,QAAQ,IAAI,UAAU,KAAK;AAAA,MAChC,mBAAmB,CAAC,GAAG,KAAK,QAAQ,KAAK,CAAC,EAAE,KAAK;AAAA,MACjD,WAAW,KAAK,yBAAyB;AAAA,IAC1C;AAAA,EACD;AAAA,EAEA,MAAM,OAAsB;AAG3B,SAAK,2BAA2B;AAChC,eAAWA,WAAU,SAAS,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,GAAG;AAC1D,YAAMA,QAAO,QAAQ,IAAI;AAAA,IAC1B;AACA,SAAK,qBAAqB;AAC1B,WAAO;AAAA,EACR;AAAA;AAAA;AAAA,EAIQ,uBAA6B;AACpC,QAAI,KAAK,OAAO,iBAAiB,MAAO;AAExC,SAAK,OAAO,IAAI,OAAO,YAAY,QAAQ,CAAC,YAAY,SAAS,KAAK,EAAE,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC;AAEzF,QAAI,KAAK,OAAO,SAAS;AACxB,WAAK,OAAO;AAAA,QACX;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,UACP,YACC,IAAI,SAAS,KAAK,QAAQ,OAAO,GAAG;AAAA,YACnC,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,4BAA4B;AAAA,UACxD,CAAC;AAAA,QACH,CAAC;AAAA,MACF;AAAA,IACD;AAEA,SAAK,OAAO;AAAA,MACX;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,QACP,YAAY;AACX,gBAAM,SAAS,KAAK,yBAAyB;AAC7C,cAAI,eAAe;AACnB,cAAI,KAAK,OAAO,YAAY;AAC3B,gBAAI;AACH,6BAAe,QAAQ,MAAM,KAAK,OAAO,WAAW,CAAC;AAAA,YACtD,QAAQ;AACP,6BAAe;AAAA,YAChB;AAAA,UACD;AACA,gBAAM,QAAQ,OAAO,MAAM;AAC3B,iBAAO,SAAS;AAAA,YACf,EAAE,QAAQ,QAAQ,UAAU,aAAa,cAAc,UAAU,OAAO,SAAS;AAAA,YACjF,EAAE,QAAQ,QAAQ,MAAM,IAAI;AAAA,UAC7B;AAAA,QACD;AAAA,MACD,CAAC;AAAA,IACF;AAAA,EACD;AAAA;AAAA;AAAA,EAIQ,6BAAmC;AAC1C,QAAI,QAAQ,IAAI,UAAU,MAAM,aAAc;AAC9C,QAAI,KAAK,OAAO,4BAA6B;AAC7C,UAAM,EAAE,IAAI,SAAS,IAAI,KAAK,yBAAyB;AACvD,QAAI,GAAI;AACR,UAAM,SAAS,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO;AAC5D,UAAM,IAAI;AAAA,MACT,oDAA+C,OAAO,MAAM,wBAC9C,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,IAExE;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,SAA6C;AAC/D,UAAM,MAAwB;AAAA,MAC7B;AAAA,MACA,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,WAAW;AAAA,MACX,MAAM,EAAE,eAAe,KAAK;AAAA,IAC7B;AACA,UAAM,QAAQ,KAAK,WAAW,EAAE,KAAK,YAAY,IAAI,SAAS,CAAC;AAC/D,WAAO,IAAI,KAAK,eAAe;AAC/B,WAAO;AAAA,EACR;AAAA;AAAA,EAIA,IAAI,YAA8B;AACjC,SAAK,YAAY,KAAK,UAAU;AAChC,WAAO;AAAA,EACR;AAAA;AAAA,EAGA,SAAS,QAAgB,SAAiB,UAA8B;AACvE,SAAK,OAAO,IAAI,QAAQ,KAAK,SAAS,MAAM,QAAQ,QAAQ,CAAC;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,SAAqC;AACjD,UAAM,MAAwB;AAAA,MAC7B;AAAA,MACA,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,WAAW;AAAA,MACX,MAAM,CAAC;AAAA,IACR;AAGA,UAAM,WAAW,QAAQ;AAAA,MACxB,GAAG,KAAK;AAAA,MACR,iBAAiB,KAAK,MAAM;AAAA,MAC5B,mBAAmB;AAAA,IACpB,CAAC;AAED,QAAI;AACJ,QAAI;AACH,iBAAW,MAAM,SAAS,KAAK,YAAY,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,IACtF,SAAS,KAAK;AACb,iBAAW,KAAK,OAAO,UAAU,GAAG,KAAK,oBAAoB,GAAG;AAAA,IACjE;AACA,WAAO,KAAK,OAAO,aAAa,KAAK,kBAAkB,UAAU,OAAO,IAAI;AAAA,EAC7E;AAAA;AAAA;AAAA,EAIA,MAAc,kBAAkB,UAAoB,SAAqC;AACxF,UAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAC5D,QAAI,CAAC,YAAY,SAAS,kBAAkB,EAAG,QAAO;AACtD,QAAI;AACJ,QAAI;AACH,aAAO,MAAM,SAAS,MAAM,EAAE,KAAK;AAAA,IACpC,QAAQ;AACP,aAAO;AAAA,IACR;AACA,UAAM,cAAc,KAAK,OAAO,WAAY,MAAM,EAAE,QAAQ,SAAS,QAAQ,QAAQ,CAAC;AACtF,QAAI,gBAAgB,OAAW,QAAO;AAEtC,UAAM,UAAU,IAAI,QAAQ,SAAS,OAAO;AAC5C,YAAQ,OAAO,gBAAgB;AAC/B,YAAQ,OAAO,cAAc;AAC7B,WAAO,SAAS,KAAK,aAAa,EAAE,QAAQ,SAAS,QAAQ,QAAQ,CAAC;AAAA,EACvE;AACD;AAMA,SAAS,SAAS,SAA+C;AAChE,QAAM,SAAS,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACtD,QAAM,SAA4B,CAAC;AACnC,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,WAAW,oBAAI,IAAY;AAEjC,WAAS,MAAM,GAAoB,MAAsB;AACxD,QAAI,QAAQ,IAAI,EAAE,IAAI,EAAG;AACzB,QAAI,SAAS,IAAI,EAAE,IAAI,GAAG;AACzB,YAAM,IAAI,MAAM,mCAAmC,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,KAAK,UAAK,CAAC,EAAE;AAAA,IACnF;AACA,aAAS,IAAI,EAAE,IAAI;AACnB,eAAW,OAAO,EAAE,QAAQ,CAAC,GAAG;AAC/B,YAAM,QAAQ,OAAO,IAAI,GAAG;AAC5B,UAAI,CAAC;AACJ,cAAM,IAAI,MAAM,eAAe,EAAE,IAAI,eAAe,GAAG,4BAA4B;AACpF,YAAM,OAAO,CAAC,GAAG,MAAM,EAAE,IAAI,CAAC;AAAA,IAC/B;AACA,aAAS,OAAO,EAAE,IAAI;AACtB,YAAQ,IAAI,EAAE,IAAI;AAClB,WAAO,KAAK,CAAC;AAAA,EACd;AAEA,aAAW,KAAK,QAAS,OAAM,GAAG,CAAC,CAAC;AACpC,SAAO;AACR;AAEA,SAAS,eAAuB;AAC/B,QAAM,WAAO,kCAAkB;AAE/B,aAAW,cAAc,OAAO,OAAO,IAAI,GAAG;AAC7C,QAAI,CAAC,YAAY;AAChB;AAAA,IACD;AAEA,eAAW,SAAS,YAAY;AAC/B,UAAI,MAAM,WAAW,UAAU,CAAC,MAAM,UAAU;AAC/C,eAAO,MAAM;AAAA,MACd;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AACR;;;AWnTO,SAAS,aAAa,QAAwC;AACpE,SAAO;AACR;;;ACpFO,SAAS,cAAc,OAAwB;AACrD,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC5C;AAEO,SAAS,eAAe,OAAyB;AACvD,MAAI,OAAO,UAAU,UAAW,QAAO;AACvC,MAAI,UAAU,UAAU,UAAU,IAAK,QAAO;AAC9C,SAAO;AACR;AAEO,SAAS,aAAgB,OAAqB;AACpD,SAAO,MAAM,QAAQ,KAAK,IAAK,QAAgB,CAAC;AACjD;AAEO,SAAS,aAAa,OAAwB;AACpD,QAAM,IAAI,OAAO,KAAK;AACtB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AACjC;AAEO,SAAS,YAAY,OAAwB;AACnD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,SAAO;AACR;;;ACnBO,SAAS,mBAAmB,WAAmB,IAAoB;AACzE,SAAO,OAAO,KAAK,KAAK,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,SAAS,WAAW;AACzE;AAOA,IAAM,eACL;AACD,IAAM,eAAe;AAEd,SAAS,mBAAmB,QAA0D;AAC5F,MAAI,OAAO,SAAS,IAAK,QAAO;AAChC,MAAI;AACH,UAAM,SAAS,KAAK,MAAM,OAAO,KAAK,QAAQ,WAAW,EAAE,SAAS,MAAM,CAAC;AAC3E,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,OAAO,CAAC,MAAM,YAAY,OAAO,OAAO,CAAC,MAAM,UAAU;AAC7F,aAAO;AAAA,IACR;AACA,QAAI,CAAC,aAAa,KAAK,OAAO,CAAC,CAAC,KAAK,CAAC,aAAa,KAAK,OAAO,CAAC,CAAC,EAAG,QAAO;AAC3E,WAAO,EAAE,WAAW,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE;AAAA,EAC9C,QAAQ;AACP,WAAO;AAAA,EACR;AACD;","names":["module"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/app.ts","../src/router.ts","../src/compose.ts","../src/response.ts","../src/middlewares/not-found.ts","../src/middlewares/body-parser.ts","../src/middlewares/security-headers.ts","../src/middlewares/error-handler.ts","../src/crypto.ts","../src/secret-strength.ts","../src/metrics.ts","../src/config.ts","../src/parser.ts","../src/keyset-cursor.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport type {\n\tITenant,\n\tIRouter,\n\tOperation,\n\tIAuthUser,\n\tMiddleware,\n\tIWorkspace,\n\tIRouteMatch,\n\tIFonderieApp,\n\tIFonderieModule,\n\tIFonderieContext,\n\tICourierMessage,\n\tIDefaultTemplate,\n\tIFonderieContextMeta,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\n} from './types';\n\nexport { OPERATIONS } from './constants';\n\nexport { FonderieApp, DEFAULT_MAX_BODY_BYTES } from './app';\nexport { defineConfig } from './config';\nexport { compose } from './compose';\nexport type { FonderieConfig } from './config';\n\n// Built-in middleware — import from '@fonderie/core/middlewares', not the root barrel\n\n// Parser utilities\nexport { stringOrEmpty, booleanOrFalse, arrayOrEmpty, numberOrZero, dateOrEmpty } from './parser';\nexport { constantTimeEqual } from './crypto';\nexport { MIN_SECRET_LENGTH, PLACEHOLDER_SECRET, secretStrengthProblem } from './secret-strength';\nexport { encodeKeysetCursor, decodeKeysetCursor } from './keyset-cursor';\n\n// Response helpers\nexport type { IApiError, HttpStatus } from './response';\nexport { HTTP, setApiResponse } from './response';\n\n// NOT exported: adapters/, router internals, error-handler, not-found\n// Those are consumed by FonderieApp, never by users directly\n\nexport { MetricsRegistry, withMetrics } from './metrics';\n","import type { Operation } from './types';\n\n// CRUD operation names shared across modules. Canonical home is core so the\n// adapters (which peer only on core) can re-export OPERATIONS without loading\n// @fonderie/permissions; permissions re-exports it for backward compatibility.\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n","import { networkInterfaces } from 'node:os';\nimport { createServer, type Server } from 'node:http';\n\nimport type {\n\tMiddleware,\n\tIFonderieApp,\n\tIFonderieContext,\n\tIFonderieModule,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\n} from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { bodyParser, DEFAULT_MAX_BODY_BYTES } from './middlewares/body-parser';\nimport { withSecurityHeaders } from './middlewares/security-headers';\nimport { MetricsRegistry, withMetrics } from './metrics';\n\n// Re-exported from the body parser, which is where the cap is actually\n// enforced (so every adapter's buildContext()/handle() inherits it — not\n// just listen()'s transport). Kept here for import-path compatibility.\nexport { DEFAULT_MAX_BODY_BYTES };\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\nfunction payloadTooLarge(res: { statusCode: number; setHeader(k: string, v: string): void; end(body?: string): void }): void {\n\tres.statusCode = 413;\n\tres.setHeader('content-type', 'application/json');\n\tres.end(JSON.stringify({ reason: 'PAYLOAD_TOO_LARGE', explanation: 'Request body too large' }));\n}\n\nexport class FonderieApp implements IFonderieApp {\n\tprivate config: FonderieConfig;\n\tprivate prefix: string;\n\tprivate router: Router = new Router();\n\tprivate middlewares: Middleware[] = [];\n\tprivate modules: Map<string, IFonderieModule> = new Map();\n\treadonly metrics = new MetricsRegistry();\n\n\tconstructor(config: FonderieConfig) {\n\t\tthis.config = config;\n\t\tthis.prefix = (config.basePath ?? '').replace(/\\/$/, '');\n\t\t// Body parsing first (capped at config.maxBodyBytes — the cap lives in\n\t\t// the parser so every adapter inherits it), then baseline security\n\t\t// headers (nosniff always; HSTS over HTTPS). Apps can layer more via `.use()`.\n\t\tthis.middlewares = [bodyParser(config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES), withSecurityHeaders()];\n\t\tif (config.metrics) this.middlewares.push(withMetrics(this.metrics));\n\t}\n\n\tlisten(\n\t\tport: number,\n\t\toptions: {\n\t\t\tname?: string;\n\t\t\tversion?: string;\n\t\t\tenv?: string;\n\t\t\tquiet?: boolean; // suppress the startup banner (tests, quiet deploys)\n\t\t} = {},\n\t): Server {\n\t\tconst {\n\t\t\tname = 'Fonderie',\n\t\t\tversion = '0.0.1',\n\t\t\tenv = process.env['NODE_ENV'] ?? 'development',\n\t\t\tquiet = false,\n\t\t} = options;\n\n\t\tconst maxBodyBytes = this.config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\t// The whole callback is guarded below (see the catch at the bottom): an\n\t\t\t// exception ANYWHERE here — e.g. `new Request()` throwing on a\n\t\t\t// fetch-spec-forbidden method like TRACE, or an absolute-form\n\t\t\t// request-target producing an invalid URL — would otherwise be an\n\t\t\t// unhandled rejection and crash the PROCESS on a single request.\n\t\t\ttry {\n\t\t\tconst host = req.headers.host ?? 'localhost';\n\t\t\tconst url = `http://${host}${req.url ?? '/'}`;\n\t\t\tconst headers = new Headers();\n\n\t\t\tfor (const [key, value] of Object.entries(req.headers)) {\n\t\t\t\tif (!value) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\tfor (const v of value) headers.append(key, v);\n\t\t\t\t} else {\n\t\t\t\t\theaders.set(key, value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\t// Body cap — without it a single unauthenticated request could stream\n\t\t\t// an arbitrarily large body fully into memory before any handler runs.\n\t\t\t// Fast path: reject a declared-oversize body before reading a byte.\n\t\t\tconst declared = Number(req.headers['content-length']);\n\t\t\tif (hasBody && Number.isFinite(declared) && declared > maxBodyBytes) {\n\t\t\t\t// Answer FIRST, then drop the connection — destroying before the\n\t\t\t\t// write means the client sees a reset instead of the 413.\n\t\t\t\tpayloadTooLarge(res);\n\t\t\t\tres.once('close', () => req.destroy());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Read the body stream, capped as a backstop for chunked / missing /\n\t\t\t// lying Content-Length: stop buffering the moment the cap is crossed.\n\t\t\tlet body: Buffer;\n\t\t\ttry {\n\t\t\t\tbody = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\t\tlet total = 0;\n\t\t\t\t\treq.on('data', (chunk: Buffer) => {\n\t\t\t\t\t\ttotal += chunk.length;\n\t\t\t\t\t\tif (total > maxBodyBytes) {\n\t\t\t\t\t\t\treject(new PayloadTooLargeError());\n\t\t\t\t\t\t\treq.destroy();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchunks.push(chunk);\n\t\t\t\t\t});\n\t\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\t\treq.on('error', reject);\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof PayloadTooLargeError) {\n\t\t\t\t\tpayloadTooLarge(res);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tres.statusCode = 400;\n\t\t\t\tres.end();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst request = new Request(url, {\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: hasBody && body.length > 0 ? new Uint8Array(body) : null,\n\t\t\t});\n\n\t\t\tconst response = await this.handle(request);\n\n\t\t\tres.statusCode = response.status;\n\t\t\t// Set-Cookie must be forwarded as a LIST — forEach + setHeader would\n\t\t\t// overwrite all but the last cookie. getSetCookie() returns each intact.\n\t\t\tconst setCookies = response.headers.getSetCookie?.() ?? [];\n\t\t\tif (setCookies.length) res.setHeader('Set-Cookie', setCookies);\n\t\t\tresponse.headers.forEach((v, k) => {\n\t\t\t\tif (k.toLowerCase() !== 'set-cookie') res.setHeader(k, v);\n\t\t\t});\n\t\t\tres.end(Buffer.from(await response.arrayBuffer()));\n\t\t\t} catch (err) {\n\t\t\t\t// Malformed/hostile request (TRACE, absolute-form target, bad\n\t\t\t\t// headers): answer 400 and keep the process alive.\n\t\t\t\tconsole.error('[fonderie] request handling failed:', (err as Error)?.message);\n\t\t\t\ttry {\n\t\t\t\t\tif (!res.headersSent) {\n\t\t\t\t\t\tres.statusCode = 400;\n\t\t\t\t\t\tres.setHeader('content-type', 'application/json');\n\t\t\t\t\t}\n\t\t\t\t\tres.end(JSON.stringify({ reason: 'BAD_REQUEST', explanation: 'Malformed request' }));\n\t\t\t\t} catch {\n\t\t\t\t\treq.destroy();\n\t\t\t\t}\n\t\t\t}\n\t\t}).listen(port, () => {\n\t\t\tif (quiet) return;\n\t\t\tconst ip = getLocalIPv4();\n\t\t\tconst mode = env.includes('dev') ? 'development' : 'production';\n\n\t\t\tconsole.log(\n\t\t\t\t`\\n ƒ ${name} v${version} ${mode}\\n` +\n\t\t\t\t\t`\\n Local http://localhost:${port}` +\n\t\t\t\t\t`\\n Network http://${ip}:${port}\\n`,\n\t\t\t);\n\t\t});\n\t\treturn server;\n\t}\n\n\t// ─── Module registration ───────────────────────────────\n\n\tregister(module: IFonderieModule): this {\n\t\tthis.modules.set(module.name, module);\n\t\treturn this;\n\t}\n\n\t// Aggregate every registered module's self-reported readiness problems into\n\t// one report. Call it before boot to gate a deploy, or from a readiness\n\t// endpoint. `ok` is false when any module reports an `error`-severity problem\n\t// (e.g. a weak jwtSecret). Modules opt in via `checkReadiness`.\n\tcheckProductionReadiness(): IReadinessReport {\n\t\tconst problems: IReadinessProblem[] = [];\n\t\tfor (const module of this.modules.values()) {\n\t\t\tif (module.checkReadiness) problems.push(...module.checkReadiness());\n\t\t}\n\t\treturn { ok: !problems.some((p) => p.severity === 'error'), problems };\n\t}\n\n\t// A point-in-time control-posture snapshot for SOC 2 evidence: which modules\n\t// are registered and the current readiness report. Serialise to a file/log\n\t// (e.g. on a schedule) as an audit artifact.\n\tsecurityReport(): ISecurityReport {\n\t\treturn {\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t\tenv: process.env['NODE_ENV'] ?? 'development',\n\t\t\tregisteredModules: [...this.modules.keys()].sort(),\n\t\t\treadiness: this.checkProductionReadiness(),\n\t\t};\n\t}\n\n\tasync boot(): Promise<this> {\n\t\t// Fail closed before any side effects (transports, listeners): a\n\t\t// production deploy with an error-severity readiness problem must not boot.\n\t\tthis.enforceProductionReadiness();\n\t\tfor (const module of topoSort([...this.modules.values()])) {\n\t\t\tawait module.install(this);\n\t\t}\n\t\tthis.registerHealthRoutes();\n\t\treturn this;\n\t}\n\n\t// Liveness (/healthz) and readiness (/readyz) probes. Registered unprefixed so\n\t// they sit at a stable path regardless of basePath. Enabled unless disabled.\n\tprivate registerHealthRoutes(): void {\n\t\tif (this.config.healthChecks === false) return;\n\n\t\tthis.router.add('GET', '/healthz', compose([async () => Response.json({ status: 'ok' })]));\n\n\t\tif (this.config.metrics) {\n\t\t\tthis.router.add(\n\t\t\t\t'GET',\n\t\t\t\t'/metrics',\n\t\t\t\tcompose([\n\t\t\t\t\tasync () =>\n\t\t\t\t\t\tnew Response(this.metrics.render(), {\n\t\t\t\t\t\t\tstatus: 200,\n\t\t\t\t\t\t\theaders: { 'content-type': 'text/plain; version=0.0.4' },\n\t\t\t\t\t\t}),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tthis.router.add(\n\t\t\t'GET',\n\t\t\t'/readyz',\n\t\t\tcompose([\n\t\t\t\tasync () => {\n\t\t\t\t\tconst report = this.checkProductionReadiness();\n\t\t\t\t\tlet dependencies = true;\n\t\t\t\t\tif (this.config.readyProbe) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdependencies = Boolean(await this.config.readyProbe());\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tdependencies = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst ready = report.ok && dependencies;\n\t\t\t\t\t// The problems list names weak secrets, placeholder tokens, and\n\t\t\t\t\t// dependency state — a security-posture map. It is only exposed\n\t\t\t\t\t// outside production (or with an explicit opt-in); the probe\n\t\t\t\t\t// consumer (k8s, LB) needs nothing beyond the status code.\n\t\t\t\t\tconst exposeDetails =\n\t\t\t\t\t\tprocess.env['NODE_ENV'] !== 'production' || this.config.exposeReadyzDetails === true;\n\t\t\t\t\treturn Response.json(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstatus: ready ? 'ready' : 'not_ready',\n\t\t\t\t\t\t\tdependencies,\n\t\t\t\t\t\t\t...(exposeDetails ? { problems: report.problems } : {}),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ status: ready ? 200 : 503 },\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t]),\n\t\t);\n\t}\n\n\t// Throws in production when `checkProductionReadiness()` reports any\n\t// error-severity problem, unless explicitly overridden. No-op otherwise.\n\tprivate enforceProductionReadiness(): void {\n\t\tif (process.env['NODE_ENV'] !== 'production') return;\n\t\tif (this.config.skipProductionReadinessGate) return;\n\t\tconst { ok, problems } = this.checkProductionReadiness();\n\t\tif (ok) return;\n\t\tconst errors = problems.filter((p) => p.severity === 'error');\n\t\tthrow new Error(\n\t\t\t`[fonderie] refusing to boot in production — ${errors.length} readiness ` +\n\t\t\t\t`error(s): ${errors.map((e) => `${e.module}: ${e.message}`).join('; ')}. ` +\n\t\t\t\t'Fix them, or set skipProductionReadinessGate: true to override (not recommended).',\n\t\t);\n\t}\n\n\t// Runs global middleware only (no routing, no 404).\n\t// Adapter packages call this to populate user/workspace/meta into their\n\t// native context before handing off to user-defined route handlers.\n\tasync buildContext(request: Request): Promise<IFonderieContext> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: { _buildContext: true },\n\t\t};\n\t\tawait compose(this.middlewares)(ctx, async () => new Response());\n\t\tdelete ctx.meta['_buildContext'];\n\t\treturn ctx;\n\t}\n\n\t// ─── Middleware ────────────────────────────────────────\n\n\tuse(middleware: Middleware): this {\n\t\tthis.middlewares.push(middleware);\n\t\treturn this;\n\t}\n\n\t// Modules call this to register their routes\n\taddRoute(method: string, path: string, ...handlers: Middleware[]): void {\n\t\tthis.router.add(method, this.prefix + path, compose(handlers));\n\t}\n\n\t// ─── The core handler ──────────────────────────────────\n\t// This is the ONE thing every adapter calls.\n\t// Takes a Web Standard Request, returns a Web Standard Response.\n\n\tasync handle(request: Request): Promise<Response> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: {},\n\t\t};\n\n\t\t// Build the pipeline: global middleware → router → 404\n\t\tconst pipeline = compose([\n\t\t\t...this.middlewares,\n\t\t\trouterMiddleware(this.router),\n\t\t\tnotFoundMiddleware(),\n\t\t]);\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\tresponse = this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t\treturn this.config.onResponse ? this.transformResponse(response, request) : response;\n\t}\n\n\t// Apply config.onResponse to a JSON response body, preserving status, headers,\n\t// and cookies. Non-JSON responses and hooks that return `undefined` pass through.\n\tprivate async transformResponse(response: Response, request: Request): Promise<Response> {\n\t\tconst contentType = response.headers.get('content-type') ?? '';\n\t\tif (!contentType.includes('application/json')) return response;\n\t\tlet body: unknown;\n\t\ttry {\n\t\t\tbody = await response.clone().json();\n\t\t} catch {\n\t\t\treturn response; // not valid JSON after all — leave untouched\n\t\t}\n\t\tconst transformed = this.config.onResponse!(body, { status: response.status, request });\n\t\tif (transformed === undefined) return response;\n\t\t// Preserve headers/cookies; drop content-length (the new body sets its own).\n\t\tconst headers = new Headers(response.headers);\n\t\theaders.delete('content-length');\n\t\theaders.delete('content-type');\n\t\treturn Response.json(transformed, { status: response.status, headers });\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tPAYLOAD_TOO_LARGE: 413,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readTextCapped(req: Request, maxBytes: number): Promise<string | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn new TextDecoder().decode(merged);\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst raw = await readTextCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// so handlers that need the RAW body (webhook signature checks)\n\t\t\t\t// can still read it.\n\t\t\t\tif (raw !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\tbody: raw.length > 0 ? raw : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst text = raw?.trim() ?? '';\n\t\t\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(raw ?? ''));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\n","import type { Middleware } from '../types';\n\nexport interface SecurityHeadersOptions {\n\t// HSTS max-age in seconds. Default 180 days. Set 0 to omit the header.\n\thstsMaxAge?: number;\n\t// Add `includeSubDomains` to the HSTS header. Off by default — only enable\n\t// once every subdomain is known to serve HTTPS.\n\thstsIncludeSubDomains?: boolean;\n\t// Add `preload` to the HSTS header (implies includeSubDomains). Off by default.\n\thstsPreload?: boolean;\n}\n\n// Baseline response hardening. `X-Content-Type-Options: nosniff` is always safe.\n// HSTS is only meaningful — and only emitted — over HTTPS: browsers ignore it on\n// plain HTTP, so gating on the effective scheme keeps local http/dev untouched\n// while enforcing TLS in production (behind a TLS-terminating proxy, detected via\n// X-Forwarded-Proto). Wired into the default pipeline by FonderieApp.\nexport function withSecurityHeaders(options: SecurityHeadersOptions = {}): Middleware {\n\tconst {\n\t\thstsMaxAge = 60 * 60 * 24 * 180,\n\t\thstsIncludeSubDomains = false,\n\t\thstsPreload = false,\n\t} = options;\n\n\tlet hsts = '';\n\tif (hstsMaxAge > 0) {\n\t\thsts = `max-age=${hstsMaxAge}`;\n\t\tif (hstsIncludeSubDomains || hstsPreload) hsts += '; includeSubDomains';\n\t\tif (hstsPreload) hsts += '; preload';\n\t}\n\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst patched = new Headers(response.headers);\n\n\t\tpatched.set('X-Content-Type-Options', 'nosniff');\n\n\t\tif (hsts && isHttps(ctx.request)) {\n\t\t\tpatched.set('Strict-Transport-Security', hsts);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n\n// HTTPS if the request URL is https, or a TLS-terminating proxy says so.\nfunction isHttps(request: Request): boolean {\n\tif (request.url.startsWith('https:')) return true;\n\tconst proto = request.headers.get('x-forwarded-proto');\n\treturn proto?.split(',')[0]?.trim() === 'https';\n}\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { timingSafeEqual } from 'node:crypto';\n\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\n}\n","import type { Middleware } from './types';\n\n// Minimal, dependency-free metrics (SOC 2 CC7.2). Counts HTTP requests by\n// status class and exposes them in Prometheus text format at /metrics (opt-in\n// via config.metrics). Apps can also record custom counters. Not a full metrics\n// system — enough to alert on error rate and traffic without pulling a client.\n\nexport class MetricsRegistry {\n\tprivate counters = new Map<string, number>();\n\n\tinc(name: string, labels: Record<string, string> = {}, by = 1): void {\n\t\tconst key = seriesKey(name, labels);\n\t\tthis.counters.set(key, (this.counters.get(key) ?? 0) + by);\n\t}\n\n\t// Prometheus text exposition format.\n\trender(): string {\n\t\tconst lines: string[] = [];\n\t\tfor (const [key, value] of this.counters) lines.push(`${key} ${value}`);\n\t\treturn lines.join('\\n') + (lines.length ? '\\n' : '');\n\t}\n}\n\nfunction seriesKey(name: string, labels: Record<string, string>): string {\n\tconst parts = Object.entries(labels).map(([k, v]) => `${k}=\"${String(v).replace(/\"/g, '')}\"`);\n\treturn parts.length ? `${name}{${parts.join(',')}}` : name;\n}\n\n// Middleware that records one `http_requests_total{status_class}` per response.\nexport function withMetrics(registry: MetricsRegistry): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst cls = `${Math.floor(response.status / 100)}xx`;\n\t\tregistry.inc('http_requests_total', { status_class: cls });\n\t\treturn response;\n\t};\n}\n","export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\t// Fail-closed production boot gate. In production, `boot()` refuses to start\n\t// when any registered module reports an `error`-severity readiness problem\n\t// (e.g. a weak signing secret, a missing at-rest encryption key). Set this to\n\t// true only to deliberately override the gate — not recommended. Outside\n\t// production the gate never runs.\n\tskipProductionReadinessGate?: boolean;\n\n\t// Built-in health endpoints, registered on boot (unprefixed by basePath):\n\t// GET /healthz — liveness, always 200 while the process is up\n\t// GET /readyz — readiness, 200 when checkProductionReadiness() is ok AND\n\t// readyProbe() (if provided) resolves truthy, else 503\n\t// Set false to disable. Point your platform's probes at these.\n\thealthChecks?: boolean;\n\t// Optional dependency probe for /readyz — e.g. `() => store.testConnection()`.\n\t// Throwing or returning false makes /readyz report 503.\n\treadyProbe?: () => boolean | Promise<boolean>;\n\t// /readyz's `problems` list names weak secrets and placeholder tokens — a\n\t// security-posture map — so in production it is omitted unless this is\n\t// explicitly true. Probes only need the status code. Non-production always\n\t// includes details.\n\texposeReadyzDetails?: boolean;\n\n\t// Enable Prometheus-format metrics: counts requests by status class and\n\t// serves them at GET /metrics (unprefixed). Off by default. Access to\n\t// /metrics should be restricted at your ingress/scraper.\n\tmetrics?: boolean;\n\n\t// Cap on the request body the built-in `listen()` server will buffer, in\n\t// bytes. Defaults to 5 MiB (matching the adapters). Without a cap, an\n\t// unauthenticated request could stream an arbitrarily large body fully into\n\t// memory before any handler runs — a memory-exhaustion DoS. Oversize\n\t// requests get 413. Raise for large uploads (e.g. @fonderie/media images);\n\t// adapter deployments configure this on the adapter instead.\n\tmaxBodyBytes?: number;\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n","export function stringOrEmpty(value: unknown): string {\n\treturn typeof value === 'string' ? value : '';\n}\n\nexport function booleanOrFalse(value: unknown): boolean {\n\tif (typeof value === 'boolean') return value;\n\tif (value === 'true' || value === '1') return true;\n\treturn false;\n}\n\nexport function arrayOrEmpty<T>(value: unknown): T[] {\n\treturn Array.isArray(value) ? (value as T[]) : [];\n}\n\nexport function numberOrZero(value: unknown): number {\n\tconst n = Number(value);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\nexport function dateOrEmpty(value: unknown): string {\n\tif (typeof value === 'string') return value;\n\tif (value instanceof Date) return value.toISOString();\n\treturn '';\n}\n","// One opaque keyset-pagination cursor over (created_at, id) — used by billing's\n// wallet ledger and audit's event log (previously two copies that had drifted:\n// audit's decode skipped field-range checks, so a crafted cursor reached the\n// ::timestamptz cast as a 500 instead of decoding to null → 422). Pure: no DB.\nexport function encodeKeysetCursor(createdAt: string, id: string): string {\n\treturn Buffer.from(JSON.stringify([createdAt, id])).toString('base64url');\n}\n\n// Range-checked (month 01-12, day 01-31, hour 00-23, min/sec 00-59) so a crafted\n// in-shape-but-out-of-range timestamp yields null (→ the caller's 422), never a\n// Postgres cast error (500). Accepts Postgres' own text form\n// ('2026-09-04 18:50:50.888123+00') so cursors survive round-trips without JS\n// millisecond truncation.\nconst CURSOR_TS_RE =\n\t/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[T ]([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function decodeKeysetCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));\n\t\tif (!Array.isArray(parsed) || typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string') {\n\t\t\treturn null;\n\t\t}\n\t\tif (!CURSOR_TS_RE.test(parsed[0]) || !CURSOR_ID_RE.test(parsed[1])) return null;\n\t\treturn { createdAt: parsed[0], id: parsed[1] };\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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","names":["PayloadTooLargeError","module"]}
package/dist/index.d.cts CHANGED
@@ -5,6 +5,7 @@ import { FonderieConfig } from './config.cjs';
5
5
  export { defineConfig } from './config.cjs';
6
6
  export { arrayOrEmpty, booleanOrFalse, dateOrEmpty, numberOrZero, stringOrEmpty } from './parser.cjs';
7
7
  export { HTTP, HttpStatus, IApiError, setApiResponse } from './response.cjs';
8
+ export { D as DEFAULT_MAX_BODY_BYTES } from './body-parser-hOh-q32S.cjs';
8
9
 
9
10
  declare const OPERATIONS: {
10
11
  readonly CREATE: "create";
@@ -20,12 +21,6 @@ declare class MetricsRegistry {
20
21
  }
21
22
  declare function withMetrics(registry: MetricsRegistry): Middleware;
22
23
 
23
- /**
24
- * Default cap on the request body the built-in `listen()` server will buffer,
25
- * in bytes (5 MiB) — same default as the adapter packages. Override per app
26
- * via `config.maxBodyBytes`.
27
- */
28
- declare const DEFAULT_MAX_BODY_BYTES: number;
29
24
  declare class FonderieApp implements IFonderieApp {
30
25
  private config;
31
26
  private prefix;
@@ -67,4 +62,4 @@ declare function decodeKeysetCursor(cursor: string): {
67
62
  id: string;
68
63
  } | null;
69
64
 
70
- export { DEFAULT_MAX_BODY_BYTES, FonderieApp, FonderieConfig, IFonderieApp, IFonderieContext, IFonderieModule, IReadinessReport, ISecurityReport, MIN_SECRET_LENGTH, MetricsRegistry, Middleware, OPERATIONS, PLACEHOLDER_SECRET, compose, constantTimeEqual, decodeKeysetCursor, encodeKeysetCursor, secretStrengthProblem, withMetrics };
65
+ export { FonderieApp, FonderieConfig, IFonderieApp, IFonderieContext, IFonderieModule, IReadinessReport, ISecurityReport, MIN_SECRET_LENGTH, MetricsRegistry, Middleware, OPERATIONS, PLACEHOLDER_SECRET, compose, constantTimeEqual, decodeKeysetCursor, encodeKeysetCursor, secretStrengthProblem, withMetrics };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import { FonderieConfig } from './config.js';
5
5
  export { defineConfig } from './config.js';
6
6
  export { arrayOrEmpty, booleanOrFalse, dateOrEmpty, numberOrZero, stringOrEmpty } from './parser.js';
7
7
  export { HTTP, HttpStatus, IApiError, setApiResponse } from './response.js';
8
+ export { D as DEFAULT_MAX_BODY_BYTES } from './body-parser-dk4YTT5g.js';
8
9
 
9
10
  declare const OPERATIONS: {
10
11
  readonly CREATE: "create";
@@ -20,12 +21,6 @@ declare class MetricsRegistry {
20
21
  }
21
22
  declare function withMetrics(registry: MetricsRegistry): Middleware;
22
23
 
23
- /**
24
- * Default cap on the request body the built-in `listen()` server will buffer,
25
- * in bytes (5 MiB) — same default as the adapter packages. Override per app
26
- * via `config.maxBodyBytes`.
27
- */
28
- declare const DEFAULT_MAX_BODY_BYTES: number;
29
24
  declare class FonderieApp implements IFonderieApp {
30
25
  private config;
31
26
  private prefix;
@@ -67,4 +62,4 @@ declare function decodeKeysetCursor(cursor: string): {
67
62
  id: string;
68
63
  } | null;
69
64
 
70
- export { DEFAULT_MAX_BODY_BYTES, FonderieApp, FonderieConfig, IFonderieApp, IFonderieContext, IFonderieModule, IReadinessReport, ISecurityReport, MIN_SECRET_LENGTH, MetricsRegistry, Middleware, OPERATIONS, PLACEHOLDER_SECRET, compose, constantTimeEqual, decodeKeysetCursor, encodeKeysetCursor, secretStrengthProblem, withMetrics };
65
+ export { FonderieApp, FonderieConfig, IFonderieApp, IFonderieContext, IFonderieModule, IReadinessReport, ISecurityReport, MIN_SECRET_LENGTH, MetricsRegistry, Middleware, OPERATIONS, PLACEHOLDER_SECRET, compose, constantTimeEqual, decodeKeysetCursor, encodeKeysetCursor, secretStrengthProblem, withMetrics };
package/dist/index.js CHANGED
@@ -89,6 +89,7 @@ var HTTP = {
89
89
  NOT_FOUND: 404,
90
90
  CONFLICT: 409,
91
91
  GONE: 410,
92
+ PAYLOAD_TOO_LARGE: 413,
92
93
  UNPROCESSABLE: 422,
93
94
  TOO_MANY_REQUESTS: 429,
94
95
  SERVER_ERROR: 500,
@@ -110,25 +111,75 @@ function notFoundMiddleware() {
110
111
  }
111
112
 
112
113
  // src/middlewares/body-parser.ts
113
- var withBody = async (ctx, next) => {
114
- const method = ctx.request.method.toUpperCase();
115
- if (method === "GET" || method === "HEAD") {
116
- return next();
114
+ var DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
115
+ var PayloadTooLargeError = class extends Error {
116
+ fonderiePayloadTooLarge = true;
117
+ };
118
+ async function readTextCapped(req, maxBytes) {
119
+ const declared = Number(req.headers.get("content-length"));
120
+ if (Number.isFinite(declared) && declared > maxBytes) {
121
+ throw new PayloadTooLargeError();
117
122
  }
118
- const ct = ctx.request.headers.get("content-type") ?? "";
119
- try {
120
- if (ct.includes("application/json")) {
121
- const text = (await ctx.request.clone().text()).trim();
122
- ctx.meta.body = text ? JSON.parse(text) : {};
123
- } else if (ct.includes("application/x-www-form-urlencoded")) {
124
- const text = await ctx.request.clone().text();
125
- ctx.meta.body = Object.fromEntries(new URLSearchParams(text));
123
+ if (!req.body) return null;
124
+ const reader = req.body.getReader();
125
+ const chunks = [];
126
+ let total = 0;
127
+ for (; ; ) {
128
+ const { done, value } = await reader.read();
129
+ if (done) break;
130
+ total += value.byteLength;
131
+ if (total > maxBytes) {
132
+ await reader.cancel().catch(() => void 0);
133
+ throw new PayloadTooLargeError();
126
134
  }
127
- } catch {
128
- return setApiResponse(HTTP.BAD_REQUEST, "INVALID_REQUEST", "Invalid request body");
135
+ chunks.push(value);
129
136
  }
130
- return next();
131
- };
137
+ const merged = new Uint8Array(total);
138
+ let offset = 0;
139
+ for (const c of chunks) {
140
+ merged.set(c, offset);
141
+ offset += c.byteLength;
142
+ }
143
+ return new TextDecoder().decode(merged);
144
+ }
145
+ function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
146
+ return async (ctx, next) => {
147
+ const method = ctx.request.method.toUpperCase();
148
+ if (method === "GET" || method === "HEAD") {
149
+ return next();
150
+ }
151
+ const ct = ctx.request.headers.get("content-type") ?? "";
152
+ try {
153
+ if (ct.includes("application/json") || ct.includes("application/x-www-form-urlencoded")) {
154
+ const raw = await readTextCapped(ctx.request, maxBytes);
155
+ if (raw !== null) {
156
+ ctx.request = new Request(ctx.request.url, {
157
+ method: ctx.request.method,
158
+ headers: ctx.request.headers,
159
+ body: raw.length > 0 ? raw : null
160
+ });
161
+ }
162
+ if (ct.includes("application/json")) {
163
+ const text = raw?.trim() ?? "";
164
+ ctx.meta.body = text ? JSON.parse(text) : {};
165
+ } else {
166
+ ctx.meta.body = Object.fromEntries(new URLSearchParams(raw ?? ""));
167
+ }
168
+ }
169
+ } catch (err) {
170
+ if (err?.fonderiePayloadTooLarge) {
171
+ return setApiResponse(
172
+ HTTP.PAYLOAD_TOO_LARGE,
173
+ "PAYLOAD_TOO_LARGE",
174
+ "Request body too large"
175
+ );
176
+ }
177
+ return setApiResponse(HTTP.BAD_REQUEST, "INVALID_REQUEST", "Invalid request body");
178
+ }
179
+ return next();
180
+ };
181
+ }
182
+ var withBody = bodyParser();
132
183
 
133
184
  // src/middlewares/security-headers.ts
134
185
  function withSecurityHeaders(options = {}) {
@@ -224,8 +275,7 @@ function withMetrics(registry) {
224
275
  }
225
276
 
226
277
  // src/app.ts
227
- var DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
228
- var PayloadTooLargeError = class extends Error {
278
+ var PayloadTooLargeError2 = class extends Error {
229
279
  fonderiePayloadTooLarge = true;
230
280
  };
231
281
  function payloadTooLarge(res) {
@@ -243,7 +293,7 @@ var FonderieApp = class {
243
293
  constructor(config) {
244
294
  this.config = config;
245
295
  this.prefix = (config.basePath ?? "").replace(/\/$/, "");
246
- this.middlewares = [withBody, withSecurityHeaders()];
296
+ this.middlewares = [bodyParser(config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES), withSecurityHeaders()];
247
297
  if (config.metrics) this.middlewares.push(withMetrics(this.metrics));
248
298
  }
249
299
  listen(port, options = {}) {
@@ -255,66 +305,79 @@ var FonderieApp = class {
255
305
  } = options;
256
306
  const maxBodyBytes = this.config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
257
307
  const server = createServer(async (req, res) => {
258
- const host = req.headers.host ?? "localhost";
259
- const url = `http://${host}${req.url ?? "/"}`;
260
- const headers = new Headers();
261
- for (const [key, value] of Object.entries(req.headers)) {
262
- if (!value) {
263
- continue;
308
+ try {
309
+ const host = req.headers.host ?? "localhost";
310
+ const url = `http://${host}${req.url ?? "/"}`;
311
+ const headers = new Headers();
312
+ for (const [key, value] of Object.entries(req.headers)) {
313
+ if (!value) {
314
+ continue;
315
+ }
316
+ if (Array.isArray(value)) {
317
+ for (const v of value) headers.append(key, v);
318
+ } else {
319
+ headers.set(key, value);
320
+ }
264
321
  }
265
- if (Array.isArray(value)) {
266
- for (const v of value) headers.append(key, v);
267
- } else {
268
- headers.set(key, value);
322
+ const method = req.method ?? "GET";
323
+ const hasBody = !["GET", "HEAD"].includes(method.toUpperCase());
324
+ const declared = Number(req.headers["content-length"]);
325
+ if (hasBody && Number.isFinite(declared) && declared > maxBodyBytes) {
326
+ payloadTooLarge(res);
327
+ res.once("close", () => req.destroy());
328
+ return;
269
329
  }
270
- }
271
- const method = req.method ?? "GET";
272
- const hasBody = !["GET", "HEAD"].includes(method.toUpperCase());
273
- const declared = Number(req.headers["content-length"]);
274
- if (hasBody && Number.isFinite(declared) && declared > maxBodyBytes) {
275
- payloadTooLarge(res);
276
- req.destroy();
277
- return;
278
- }
279
- let body;
280
- try {
281
- body = await new Promise((resolve, reject) => {
282
- const chunks = [];
283
- let total = 0;
284
- req.on("data", (chunk) => {
285
- total += chunk.length;
286
- if (total > maxBodyBytes) {
287
- reject(new PayloadTooLargeError());
288
- req.destroy();
289
- return;
290
- }
291
- chunks.push(chunk);
330
+ let body;
331
+ try {
332
+ body = await new Promise((resolve, reject) => {
333
+ const chunks = [];
334
+ let total = 0;
335
+ req.on("data", (chunk) => {
336
+ total += chunk.length;
337
+ if (total > maxBodyBytes) {
338
+ reject(new PayloadTooLargeError2());
339
+ req.destroy();
340
+ return;
341
+ }
342
+ chunks.push(chunk);
343
+ });
344
+ req.on("end", () => resolve(Buffer.concat(chunks)));
345
+ req.on("error", reject);
292
346
  });
293
- req.on("end", () => resolve(Buffer.concat(chunks)));
294
- req.on("error", reject);
347
+ } catch (err) {
348
+ if (err instanceof PayloadTooLargeError2) {
349
+ payloadTooLarge(res);
350
+ return;
351
+ }
352
+ res.statusCode = 400;
353
+ res.end();
354
+ return;
355
+ }
356
+ const request = new Request(url, {
357
+ method,
358
+ headers,
359
+ body: hasBody && body.length > 0 ? new Uint8Array(body) : null
360
+ });
361
+ const response = await this.handle(request);
362
+ res.statusCode = response.status;
363
+ const setCookies = response.headers.getSetCookie?.() ?? [];
364
+ if (setCookies.length) res.setHeader("Set-Cookie", setCookies);
365
+ response.headers.forEach((v, k) => {
366
+ if (k.toLowerCase() !== "set-cookie") res.setHeader(k, v);
295
367
  });
368
+ res.end(Buffer.from(await response.arrayBuffer()));
296
369
  } catch (err) {
297
- if (err instanceof PayloadTooLargeError) {
298
- payloadTooLarge(res);
299
- return;
370
+ console.error("[fonderie] request handling failed:", err?.message);
371
+ try {
372
+ if (!res.headersSent) {
373
+ res.statusCode = 400;
374
+ res.setHeader("content-type", "application/json");
375
+ }
376
+ res.end(JSON.stringify({ reason: "BAD_REQUEST", explanation: "Malformed request" }));
377
+ } catch {
378
+ req.destroy();
300
379
  }
301
- res.statusCode = 400;
302
- res.end();
303
- return;
304
380
  }
305
- const request = new Request(url, {
306
- method,
307
- headers,
308
- body: hasBody && body.length > 0 ? new Uint8Array(body) : null
309
- });
310
- const response = await this.handle(request);
311
- res.statusCode = response.status;
312
- const setCookies = response.headers.getSetCookie?.() ?? [];
313
- if (setCookies.length) res.setHeader("Set-Cookie", setCookies);
314
- response.headers.forEach((v, k) => {
315
- if (k.toLowerCase() !== "set-cookie") res.setHeader(k, v);
316
- });
317
- res.end(Buffer.from(await response.arrayBuffer()));
318
381
  }).listen(port, () => {
319
382
  if (quiet) return;
320
383
  const ip = getLocalIPv4();
@@ -397,8 +460,13 @@ var FonderieApp = class {
397
460
  }
398
461
  }
399
462
  const ready = report.ok && dependencies;
463
+ const exposeDetails = process.env["NODE_ENV"] !== "production" || this.config.exposeReadyzDetails === true;
400
464
  return Response.json(
401
- { status: ready ? "ready" : "not_ready", dependencies, problems: report.problems },
465
+ {
466
+ status: ready ? "ready" : "not_ready",
467
+ dependencies,
468
+ ...exposeDetails ? { problems: report.problems } : {}
469
+ },
402
470
  { status: ready ? 200 : 503 }
403
471
  );
404
472
  }