@fonderie/core 0.9.0 → 0.10.1

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.
@@ -10,10 +10,11 @@ function withCors(options = {}) {
10
10
  const allowOrigin = typeof origin === "function" ? origin(requestOrigin) ? requestOrigin : "" : origin;
11
11
  const corsHeaders = {
12
12
  "Access-Control-Max-Age": "86400",
13
- "Access-Control-Allow-Origin": allowOrigin,
14
13
  "Access-Control-Allow-Methods": methods.join(", "),
15
14
  "Access-Control-Allow-Headers": headers.join(", ")
16
15
  };
16
+ if (allowOrigin) corsHeaders["Access-Control-Allow-Origin"] = allowOrigin;
17
+ if (typeof origin === "function") corsHeaders["Vary"] = "Origin";
17
18
  if (ctx.request.method === "OPTIONS") {
18
19
  return new Response(null, { status: 204, headers: corsHeaders });
19
20
  }
@@ -61,6 +62,7 @@ var HTTP = {
61
62
  NOT_FOUND: 404,
62
63
  CONFLICT: 409,
63
64
  GONE: 410,
65
+ PAYLOAD_TOO_LARGE: 413,
64
66
  UNPROCESSABLE: 422,
65
67
  TOO_MANY_REQUESTS: 429,
66
68
  SERVER_ERROR: 500,
@@ -82,25 +84,75 @@ function notFoundMiddleware() {
82
84
  }
83
85
 
84
86
  // src/middlewares/body-parser.ts
85
- var withBody = async (ctx, next) => {
86
- const method = ctx.request.method.toUpperCase();
87
- if (method === "GET" || method === "HEAD") {
88
- return next();
87
+ var DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
88
+ var PayloadTooLargeError = class extends Error {
89
+ fonderiePayloadTooLarge = true;
90
+ };
91
+ async function readTextCapped(req, maxBytes) {
92
+ const declared = Number(req.headers.get("content-length"));
93
+ if (Number.isFinite(declared) && declared > maxBytes) {
94
+ throw new PayloadTooLargeError();
89
95
  }
90
- const ct = ctx.request.headers.get("content-type") ?? "";
91
- try {
92
- if (ct.includes("application/json")) {
93
- const text = (await ctx.request.clone().text()).trim();
94
- ctx.meta.body = text ? JSON.parse(text) : {};
95
- } else if (ct.includes("application/x-www-form-urlencoded")) {
96
- const text = await ctx.request.clone().text();
97
- ctx.meta.body = Object.fromEntries(new URLSearchParams(text));
96
+ if (!req.body) return null;
97
+ const reader = req.body.getReader();
98
+ const chunks = [];
99
+ let total = 0;
100
+ for (; ; ) {
101
+ const { done, value } = await reader.read();
102
+ if (done) break;
103
+ total += value.byteLength;
104
+ if (total > maxBytes) {
105
+ await reader.cancel().catch(() => void 0);
106
+ throw new PayloadTooLargeError();
98
107
  }
99
- } catch {
100
- return setApiResponse(HTTP.BAD_REQUEST, "INVALID_REQUEST", "Invalid request body");
108
+ chunks.push(value);
101
109
  }
102
- return next();
103
- };
110
+ const merged = new Uint8Array(total);
111
+ let offset = 0;
112
+ for (const c of chunks) {
113
+ merged.set(c, offset);
114
+ offset += c.byteLength;
115
+ }
116
+ return new TextDecoder().decode(merged);
117
+ }
118
+ function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
119
+ return async (ctx, next) => {
120
+ const method = ctx.request.method.toUpperCase();
121
+ if (method === "GET" || method === "HEAD") {
122
+ return next();
123
+ }
124
+ const ct = ctx.request.headers.get("content-type") ?? "";
125
+ try {
126
+ if (ct.includes("application/json") || ct.includes("application/x-www-form-urlencoded")) {
127
+ const raw = await readTextCapped(ctx.request, maxBytes);
128
+ if (raw !== null) {
129
+ ctx.request = new Request(ctx.request.url, {
130
+ method: ctx.request.method,
131
+ headers: ctx.request.headers,
132
+ body: raw.length > 0 ? raw : null
133
+ });
134
+ }
135
+ if (ct.includes("application/json")) {
136
+ const text = raw?.trim() ?? "";
137
+ ctx.meta.body = text ? JSON.parse(text) : {};
138
+ } else {
139
+ ctx.meta.body = Object.fromEntries(new URLSearchParams(raw ?? ""));
140
+ }
141
+ }
142
+ } catch (err) {
143
+ if (err?.fonderiePayloadTooLarge) {
144
+ return setApiResponse(
145
+ HTTP.PAYLOAD_TOO_LARGE,
146
+ "PAYLOAD_TOO_LARGE",
147
+ "Request body too large"
148
+ );
149
+ }
150
+ return setApiResponse(HTTP.BAD_REQUEST, "INVALID_REQUEST", "Invalid request body");
151
+ }
152
+ return next();
153
+ };
154
+ }
155
+ var withBody = bodyParser();
104
156
 
105
157
  // src/middlewares/security-headers.ts
106
158
  function withSecurityHeaders(options = {}) {
@@ -309,6 +361,8 @@ function checkProxyConfig(socketAddress, headers, trustProxy) {
309
361
  }
310
362
  }
311
363
  export {
364
+ DEFAULT_MAX_BODY_BYTES,
365
+ bodyParser,
312
366
  checkProxyConfig,
313
367
  defaultErrorHandler,
314
368
  notFoundMiddleware,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middlewares/cors.ts","../../src/middlewares/logger.ts","../../src/response.ts","../../src/middlewares/not-found.ts","../../src/middlewares/body-parser.ts","../../src/middlewares/security-headers.ts","../../src/middlewares/error-handler.ts","../../src/middlewares/require-auth.ts","../../src/crypto.ts","../../src/secret-strength.ts","../../src/middlewares/require-admin-token.ts","../../src/middlewares/require-verified.ts","../../src/middlewares/validate.ts","../../src/middlewares/client-ip.ts"],"sourcesContent":["import type { Middleware } from '../types';\n\nexport interface CorsOptions {\n\tmethods?: string[];\n\theaders?: string[];\n\torigin?: string | ((requestOrigin: string) => boolean);\n}\n\nexport function withCors(options: CorsOptions = {}): Middleware {\n\tconst {\n\t\torigin = '*',\n\t\theaders = ['Content-Type', 'Authorization'],\n\t\tmethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n\t} = options;\n\n\treturn async (ctx, next) => {\n\t\tconst requestOrigin = ctx.request.headers.get('origin') ?? '';\n\n\t\tconst allowOrigin =\n\t\t\ttypeof origin === 'function' ? (origin(requestOrigin) ? requestOrigin : '') : origin;\n\n\t\tconst corsHeaders: Record<string, string> = {\n\t\t\t'Access-Control-Max-Age': '86400',\n\t\t\t'Access-Control-Allow-Origin': allowOrigin,\n\t\t\t'Access-Control-Allow-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\n\t\t// Preflight — respond immediately, skip the pipeline\n\t\tif (ctx.request.method === 'OPTIONS') {\n\t\t\treturn new Response(null, { status: 204, headers: corsHeaders });\n\t\t}\n\n\t\tconst response = await next();\n\n\t\tconst patched = new Headers(response.headers);\n\n\t\tfor (const [k, v] of Object.entries(corsHeaders)) {\n\t\t\tpatched.set(k, v);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n","import type { Middleware } from '../types';\n\nexport const withLogger: Middleware = async (ctx, next) => {\n\tconst start = Date.now();\n\n\tconst { method, url } = ctx.request;\n\tconst { pathname } = new URL(url);\n\n\tconst response = await next();\n\n\tconsole.log(\n\t\tJSON.stringify({\n\t\t\tts: new Date().toISOString(),\n\t\t\tmethod,\n\t\t\tpath: pathname,\n\t\t\tstatus: response.status,\n\t\t\tms: Date.now() - start,\n\t\t}),\n\t);\n\n\treturn response;\n};\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import type { Middleware } from '../types';\n\nexport interface SecurityHeadersOptions {\n\t// HSTS max-age in seconds. Default 180 days. Set 0 to omit the header.\n\thstsMaxAge?: number;\n\t// Add `includeSubDomains` to the HSTS header. Off by default — only enable\n\t// once every subdomain is known to serve HTTPS.\n\thstsIncludeSubDomains?: boolean;\n\t// Add `preload` to the HSTS header (implies includeSubDomains). Off by default.\n\thstsPreload?: boolean;\n}\n\n// Baseline response hardening. `X-Content-Type-Options: nosniff` is always safe.\n// HSTS is only meaningful — and only emitted — over HTTPS: browsers ignore it on\n// plain HTTP, so gating on the effective scheme keeps local http/dev untouched\n// while enforcing TLS in production (behind a TLS-terminating proxy, detected via\n// X-Forwarded-Proto). Wired into the default pipeline by FonderieApp.\nexport function withSecurityHeaders(options: SecurityHeadersOptions = {}): Middleware {\n\tconst {\n\t\thstsMaxAge = 60 * 60 * 24 * 180,\n\t\thstsIncludeSubDomains = false,\n\t\thstsPreload = false,\n\t} = options;\n\n\tlet hsts = '';\n\tif (hstsMaxAge > 0) {\n\t\thsts = `max-age=${hstsMaxAge}`;\n\t\tif (hstsIncludeSubDomains || hstsPreload) hsts += '; includeSubDomains';\n\t\tif (hstsPreload) hsts += '; preload';\n\t}\n\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst patched = new Headers(response.headers);\n\n\t\tpatched.set('X-Content-Type-Options', 'nosniff');\n\n\t\tif (hsts && isHttps(ctx.request)) {\n\t\t\tpatched.set('Strict-Transport-Security', hsts);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n\n// HTTPS if the request URL is https, or a TLS-terminating proxy says so.\nfunction isHttps(request: Request): boolean {\n\tif (request.url.startsWith('https:')) return true;\n\tconst proto = request.headers.get('x-forwarded-proto');\n\treturn proto?.split(',')[0]?.trim() === 'https';\n}\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Requires a fully-authenticated user. Rejects mfaPending tokens — those are\n// short-lived pre-auth tokens issued mid-MFA-login and must not grant access\n// to any route other than /auth/mfa/verify.\nexport const requireAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\tif (ctx.user.mfaPending) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t}\n\treturn next();\n};\n\n// Accepts both fully-authenticated and mfaPending tokens. Only for routes that\n// need to serve both contexts on the same path (e.g. POST /auth/mfa/verify\n// handles setup confirmation with a full token and TOTP login with mfaPending).\nexport const requireAnyAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\treturn next();\n};\n","import { timingSafeEqual } from 'node:crypto';\n\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\n}\n","import { constantTimeEqual } from '../crypto';\nimport { setApiResponse, HTTP } from '../response';\nimport { MIN_SECRET_LENGTH, secretStrengthProblem } from '../secret-strength';\nimport type { IReadinessProblem, Middleware } from '../types';\n\n// The one admin-route guard for every Fonderie module with an ops surface\n// (billing plan-writes/wallet-grant, config/secrets admin, courier template\n// admin). A bootstrap Bearer token compared in constant time. Modules MUST\n// register their admin routes only when a token is configured (unset ⇒ 404),\n// so this guard only ever runs against a real token. See docs/ADMIN-AUTH-SPEC.md.\nexport function requireAdminToken(adminToken: string): Middleware {\n\treturn (ctx, next) => {\n\t\tconst header = ctx.request.headers.get('authorization') ?? '';\n\t\tconst token = header.startsWith('Bearer ') ? header.slice(7) : '';\n\t\t// Same 401 for a missing and a wrong token — no oracle distinguishing them.\n\t\tif (!token || !constantTimeEqual(token, adminToken)) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token'),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n\n// A bootstrap admin token must be strong: an admin surface guarded by a short or\n// placeholder token is barely guarded at all. Modules call this from\n// checkReadiness() so the rule (shared secret-strength denylist) is enforced\n// identically everywhere. Returns a problem for a weak/placeholder token;\n// nothing when unset (that surface is simply not exposed).\nexport function validateAdminToken(\n\ttoken: string | undefined,\n\topts: { module: string },\n): IReadinessProblem[] {\n\tif (!token) return [];\n\tconst problem = secretStrengthProblem(token);\n\tif (problem === 'too-short') {\n\t\treturn [\n\t\t\t{\n\t\t\t\tmodule: opts.module,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: `adminToken must be at least ${MIN_SECRET_LENGTH} characters (got ${token.length})`,\n\t\t\t},\n\t\t];\n\t}\n\tif (problem === 'placeholder') {\n\t\treturn [\n\t\t\t{ module: opts.module, severity: 'error', message: 'adminToken looks like a placeholder or dev-default value' },\n\t\t];\n\t}\n\treturn [];\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport const requireVerified: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\n\tif (ctx.user.loginMethod === 'phone') {\n\t\tif (!ctx.user.phoneVerified) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'PHONE_NOT_VERIFIED',\n\t\t\t\t'Please verify your phone number',\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t}\n\n\tif (!ctx.user.emailVerifiedAt) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'EMAIL_NOT_VERIFIED', 'Please verify your email address');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Route-boundary request validation — the one validation middleware every\n// package wires in front of its body-taking routes, so error shape and parse\n// semantics are identical across the whole surface.\n//\n// core stays dependency-free: this accepts anything implementing zod's\n// safeParse contract structurally (zod v3/v4 both match), without importing\n// zod. Feature packages own their schemas; see @fonderie/auth's schemas.ts\n// for the reference pattern.\n\nexport interface IRequestSchema {\n\tsafeParse(input: unknown):\n\t\t| { success: true; data: unknown }\n\t\t| { success: false; error: { issues: Array<{ path: PropertyKey[]; message: string }> } };\n}\n\nexport function validate(schema: IRequestSchema): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst result = schema.safeParse(ctx.meta['body'] ?? {});\n\t\tif (!result.success) {\n\t\t\tconst first = result.error.issues[0];\n\t\t\tconst path = first?.path.length ? `${first.path.map(String).join('.')}: ` : '';\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t`${path}${first?.message ?? 'Invalid request body'}`,\n\t\t\t);\n\t\t}\n\t\t// Parsed output replaces the raw body: trimmed, coerced, unknown keys\n\t\t// stripped — controllers read clean input.\n\t\tctx.meta['body'] = result.data;\n\t\treturn next();\n\t};\n}\n","// Client-IP resolution shared by the adapters. The web-standard Request the\n// pipeline runs on carries no socket address, so each adapter passes the\n// socket's remote address here together with the headers; this resolves the\n// effective client IP with explicit proxy trust.\n//\n// trustProxy semantics (deliberately explicit — a permissive default lets\n// any client spoof X-Forwarded-For and dodge per-IP rate limits):\n// 0 / undefined → ignore forwarding headers; the socket address is the client\n// N > 0 → the client is the Nth-from-the-right entry in\n// X-Forwarded-For (N = number of trusted proxy hops)\n//\n// ⚠️ THE PROXY FOOTGUN. With trustProxy=0 (the spoof-safe default) deployed\n// behind nginx, a Kubernetes ingress, or any L7 proxy, the socket address is\n// the PROXY's IP for every request — so every client collapses onto one\n// per-IP bucket and the limit becomes global (one attacker locks everyone\n// out). You cannot have a default that is both spoof-safe AND correct behind\n// a proxy; they contradict. So we ship spoof-safe and DETECT the mismatch:\n// checkProxyConfig() below warns once, loudly, when the deployment looks\n// proxied but trustProxy is unset. Set TRUST_PROXY=<hops> in that case.\n\nexport function resolveClientIp(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number = trustProxyFromEnv(),\n): string | undefined {\n\tif (trustProxy > 0) {\n\t\tconst xff = headers.get('x-forwarded-for');\n\t\tif (xff) {\n\t\t\tconst hops = xff\n\t\t\t\t.split(',')\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter(Boolean);\n\t\t\tconst candidate = hops[Math.max(0, hops.length - trustProxy)];\n\t\t\tif (candidate) return normalizeIp(candidate);\n\t\t}\n\t}\n\tcheckProxyConfig(socketAddress, headers, trustProxy);\n\treturn socketAddress ? normalizeIp(socketAddress) : undefined;\n}\n\nfunction trustProxyFromEnv(): number {\n\tconst raw = Number(process.env['TRUST_PROXY']);\n\treturn Number.isFinite(raw) && raw > 0 ? raw : 0;\n}\n\nfunction normalizeIp(ip: string): string {\n\t// ::ffff:203.0.113.7 → 203.0.113.7 ; strip port if a proxy appended one\n\tconst noV6Prefix = ip.startsWith('::ffff:') ? ip.slice(7) : ip;\n\tconst m = noV6Prefix.match(/^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/);\n\treturn m ? m[1]! : noV6Prefix;\n}\n\n// Address ranges that indicate a local proxy sits in front of us (so a\n// forwarding header without TRUST_PROXY is a misconfiguration, not spoofing).\nconst LOOPBACK_IPS = new Set(['127.0.0.1', '::1']);\nconst PRIVATE_IP_PREFIXES = [\n\t'10.', // RFC1918\n\t'192.168.', // RFC1918\n\t'169.254.', // link-local\n\t'fc', // IPv6 unique local (fc00::/7)\n\t'fd', // IPv6 unique local\n] as const;\nconst CGNAT_OR_RFC1918_172 = /^172\\.(1[6-9]|2\\d|3[01])\\./; // 172.16.0.0/12\n\nfunction isPrivateOrLoopback(ip: string): boolean {\n\tconst a = normalizeIp(ip);\n\treturn (\n\t\tLOOPBACK_IPS.has(a) ||\n\t\tCGNAT_OR_RFC1918_172.test(a) ||\n\t\tPRIVATE_IP_PREFIXES.some((prefix) => a.startsWith(prefix))\n\t);\n}\n\nlet warned = false;\n\n// Warn ONCE when the deployment looks proxied (forwarding header present, and\n// the socket is a private/loopback address — i.e. a local proxy) but\n// trustProxy is unset. That configuration silently rate-limits every client\n// as one IP. Emitting on the request path (not at boot) is deliberate: the\n// signal we need — an actual X-Forwarded-For header — only exists once real\n// traffic arrives.\nexport function checkProxyConfig(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number,\n): void {\n\tif (warned || trustProxy > 0) return;\n\tconst forwarded =\n\t\theaders.get('x-forwarded-for') ??\n\t\theaders.get('cf-connecting-ip') ??\n\t\theaders.get('x-real-ip');\n\tif (forwarded && socketAddress && isPrivateOrLoopback(socketAddress)) {\n\t\twarned = true;\n\t\tconsole.warn(\n\t\t\t'[fonderie] Requests carry a forwarding header (X-Forwarded-For) and ' +\n\t\t\t\t'arrive from a private/loopback socket, but TRUST_PROXY is unset. ' +\n\t\t\t\t'Every client is being rate-limited as a single IP, which will cause ' +\n\t\t\t\t'global lockout behind nginx / a Kubernetes ingress / any L7 proxy. ' +\n\t\t\t\t'Set TRUST_PROXY=<number of trusted proxy hops>. ' +\n\t\t\t\t'See @fonderie/rate-limit README § Deploying behind a proxy.',\n\t\t);\n\t}\n}\n\n// Test seam — reset the once-only warning latch.\nexport function _resetProxyWarning(): void {\n\twarned = false;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/middlewares/cors.ts","../../src/middlewares/logger.ts","../../src/response.ts","../../src/middlewares/not-found.ts","../../src/middlewares/body-parser.ts","../../src/middlewares/security-headers.ts","../../src/middlewares/error-handler.ts","../../src/middlewares/require-auth.ts","../../src/crypto.ts","../../src/secret-strength.ts","../../src/middlewares/require-admin-token.ts","../../src/middlewares/require-verified.ts","../../src/middlewares/validate.ts","../../src/middlewares/client-ip.ts"],"sourcesContent":["import type { Middleware } from '../types';\n\nexport interface CorsOptions {\n\tmethods?: string[];\n\theaders?: string[];\n\torigin?: string | ((requestOrigin: string) => boolean);\n}\n\nexport function withCors(options: CorsOptions = {}): Middleware {\n\tconst {\n\t\torigin = '*',\n\t\theaders = ['Content-Type', 'Authorization'],\n\t\tmethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n\t} = options;\n\n\treturn async (ctx, next) => {\n\t\tconst requestOrigin = ctx.request.headers.get('origin') ?? '';\n\n\t\tconst allowOrigin =\n\t\t\ttypeof origin === 'function' ? (origin(requestOrigin) ? requestOrigin : '') : origin;\n\n\t\tconst corsHeaders: Record<string, string> = {\n\t\t\t'Access-Control-Max-Age': '86400',\n\t\t\t'Access-Control-Allow-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\t\t// Omit the header entirely for a denied origin (an empty ACAO value is\n\t\t// invalid); when the value varies by request origin, say so — otherwise a\n\t\t// shared cache can serve one origin's ACAO to another.\n\t\tif (allowOrigin) corsHeaders['Access-Control-Allow-Origin'] = allowOrigin;\n\t\tif (typeof origin === 'function') corsHeaders['Vary'] = 'Origin';\n\n\t\t// Preflight — respond immediately, skip the pipeline\n\t\tif (ctx.request.method === 'OPTIONS') {\n\t\t\treturn new Response(null, { status: 204, headers: corsHeaders });\n\t\t}\n\n\t\tconst response = await next();\n\n\t\tconst patched = new Headers(response.headers);\n\n\t\tfor (const [k, v] of Object.entries(corsHeaders)) {\n\t\t\tpatched.set(k, v);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n","import type { Middleware } from '../types';\n\nexport const withLogger: Middleware = async (ctx, next) => {\n\tconst start = Date.now();\n\n\tconst { method, url } = ctx.request;\n\tconst { pathname } = new URL(url);\n\n\tconst response = await next();\n\n\tconsole.log(\n\t\tJSON.stringify({\n\t\t\tts: new Date().toISOString(),\n\t\t\tmethod,\n\t\t\tpath: pathname,\n\t\t\tstatus: response.status,\n\t\t\tms: Date.now() - start,\n\t\t}),\n\t);\n\n\treturn response;\n};\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tPAYLOAD_TOO_LARGE: 413,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\n/**\n * Default cap on the request body the pipeline will buffer, in bytes (5 MiB).\n * Enforced HERE, in the body parser, so EVERY entry point inherits it — the\n * built-in listen() server, and all adapters' buildContext()/handle() paths.\n * (An uncapped parser was a memory-exhaustion DoS on any adapter whose\n * transport didn't add its own cap, e.g. adapter-hono on node-server.)\n */\nexport const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;\n\nclass PayloadTooLargeError extends Error {\n\treadonly fonderiePayloadTooLarge = true as const;\n}\n\n// Read the request body as text, refusing to buffer past maxBytes: a\n// Content-Length fast path rejects declared-oversize bodies without reading a\n// byte, and the streamed read stops the moment a chunked/lying body crosses\n// the cap. Returns null when there is no body stream.\n//\n// Deliberately CONSUMES the original stream instead of clone()-ing it:\n// clone() tees the stream, and a tee applies backpressure from BOTH branches\n// — with the second branch never read, any body larger than the stream's\n// high-water mark stalls the read forever. The caller re-materializes\n// ctx.request from the buffered text so downstream raw-body readers (e.g.\n// webhook signature verification) keep working.\nasync function readTextCapped(req: Request, maxBytes: number): Promise<string | null> {\n\tconst declared = Number(req.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > maxBytes) {\n\t\tthrow new PayloadTooLargeError();\n\t}\n\n\tif (!req.body) return null;\n\n\tconst reader = req.body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel().catch(() => undefined);\n\t\t\tthrow new PayloadTooLargeError();\n\t\t}\n\t\tchunks.push(value);\n\t}\n\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const c of chunks) {\n\t\tmerged.set(c, offset);\n\t\toffset += c.byteLength;\n\t}\n\treturn new TextDecoder().decode(merged);\n}\n\n/**\n * Build the body-parsing middleware with an explicit byte cap. The core app\n * wires this with `config.maxBodyBytes`; the bare `withBody` export below\n * keeps the default cap for direct users.\n */\nexport function bodyParser(maxBytes: number = DEFAULT_MAX_BODY_BYTES): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst method = ctx.request.method.toUpperCase();\n\n\t\tif (method === 'GET' || method === 'HEAD') {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\t\ttry {\n\t\t\tif (ct.includes('application/json') || ct.includes('application/x-www-form-urlencoded')) {\n\t\t\t\tconst raw = await readTextCapped(ctx.request, maxBytes);\n\t\t\t\t// The read consumed the original stream — re-materialize the request\n\t\t\t\t// so handlers that need the RAW body (webhook signature checks)\n\t\t\t\t// can still read it.\n\t\t\t\tif (raw !== null) {\n\t\t\t\t\tctx.request = new Request(ctx.request.url, {\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders: ctx.request.headers,\n\t\t\t\t\t\tbody: raw.length > 0 ? raw : null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (ct.includes('application/json')) {\n\t\t\t\t\tconst text = raw?.trim() ?? '';\n\t\t\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t\t\t} else {\n\t\t\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(raw ?? ''));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t\t} catch (err) {\n\t\t\tif ((err as { fonderiePayloadTooLarge?: boolean } | null)?.fonderiePayloadTooLarge) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.PAYLOAD_TOO_LARGE,\n\t\t\t\t\t'PAYLOAD_TOO_LARGE',\n\t\t\t\t\t'Request body too large',\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// Backward-compatible bare middleware with the default cap.\nexport const withBody: Middleware = bodyParser();\n","import type { Middleware } from '../types';\n\nexport interface SecurityHeadersOptions {\n\t// HSTS max-age in seconds. Default 180 days. Set 0 to omit the header.\n\thstsMaxAge?: number;\n\t// Add `includeSubDomains` to the HSTS header. Off by default — only enable\n\t// once every subdomain is known to serve HTTPS.\n\thstsIncludeSubDomains?: boolean;\n\t// Add `preload` to the HSTS header (implies includeSubDomains). Off by default.\n\thstsPreload?: boolean;\n}\n\n// Baseline response hardening. `X-Content-Type-Options: nosniff` is always safe.\n// HSTS is only meaningful — and only emitted — over HTTPS: browsers ignore it on\n// plain HTTP, so gating on the effective scheme keeps local http/dev untouched\n// while enforcing TLS in production (behind a TLS-terminating proxy, detected via\n// X-Forwarded-Proto). Wired into the default pipeline by FonderieApp.\nexport function withSecurityHeaders(options: SecurityHeadersOptions = {}): Middleware {\n\tconst {\n\t\thstsMaxAge = 60 * 60 * 24 * 180,\n\t\thstsIncludeSubDomains = false,\n\t\thstsPreload = false,\n\t} = options;\n\n\tlet hsts = '';\n\tif (hstsMaxAge > 0) {\n\t\thsts = `max-age=${hstsMaxAge}`;\n\t\tif (hstsIncludeSubDomains || hstsPreload) hsts += '; includeSubDomains';\n\t\tif (hstsPreload) hsts += '; preload';\n\t}\n\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst patched = new Headers(response.headers);\n\n\t\tpatched.set('X-Content-Type-Options', 'nosniff');\n\n\t\tif (hsts && isHttps(ctx.request)) {\n\t\t\tpatched.set('Strict-Transport-Security', hsts);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n\n// HTTPS if the request URL is https, or a TLS-terminating proxy says so.\nfunction isHttps(request: Request): boolean {\n\tif (request.url.startsWith('https:')) return true;\n\tconst proto = request.headers.get('x-forwarded-proto');\n\treturn proto?.split(',')[0]?.trim() === 'https';\n}\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Requires a fully-authenticated user. Rejects mfaPending tokens — those are\n// short-lived pre-auth tokens issued mid-MFA-login and must not grant access\n// to any route other than /auth/mfa/verify.\nexport const requireAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\tif (ctx.user.mfaPending) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t}\n\treturn next();\n};\n\n// Accepts both fully-authenticated and mfaPending tokens. Only for routes that\n// need to serve both contexts on the same path (e.g. POST /auth/mfa/verify\n// handles setup confirmation with a full token and TOTP login with mfaPending).\nexport const requireAnyAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\treturn next();\n};\n","import { timingSafeEqual } from 'node:crypto';\n\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\n}\n","import { constantTimeEqual } from '../crypto';\nimport { setApiResponse, HTTP } from '../response';\nimport { MIN_SECRET_LENGTH, secretStrengthProblem } from '../secret-strength';\nimport type { IReadinessProblem, Middleware } from '../types';\n\n// The one admin-route guard for every Fonderie module with an ops surface\n// (billing plan-writes/wallet-grant, config/secrets admin, courier template\n// admin). A bootstrap Bearer token compared in constant time. Modules MUST\n// register their admin routes only when a token is configured (unset ⇒ 404),\n// so this guard only ever runs against a real token. See docs/ADMIN-AUTH-SPEC.md.\nexport function requireAdminToken(adminToken: string): Middleware {\n\treturn (ctx, next) => {\n\t\tconst header = ctx.request.headers.get('authorization') ?? '';\n\t\tconst token = header.startsWith('Bearer ') ? header.slice(7) : '';\n\t\t// Same 401 for a missing and a wrong token — no oracle distinguishing them.\n\t\tif (!token || !constantTimeEqual(token, adminToken)) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token'),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n\n// A bootstrap admin token must be strong: an admin surface guarded by a short or\n// placeholder token is barely guarded at all. Modules call this from\n// checkReadiness() so the rule (shared secret-strength denylist) is enforced\n// identically everywhere. Returns a problem for a weak/placeholder token;\n// nothing when unset (that surface is simply not exposed).\nexport function validateAdminToken(\n\ttoken: string | undefined,\n\topts: { module: string },\n): IReadinessProblem[] {\n\tif (!token) return [];\n\tconst problem = secretStrengthProblem(token);\n\tif (problem === 'too-short') {\n\t\treturn [\n\t\t\t{\n\t\t\t\tmodule: opts.module,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: `adminToken must be at least ${MIN_SECRET_LENGTH} characters (got ${token.length})`,\n\t\t\t},\n\t\t];\n\t}\n\tif (problem === 'placeholder') {\n\t\treturn [\n\t\t\t{ module: opts.module, severity: 'error', message: 'adminToken looks like a placeholder or dev-default value' },\n\t\t];\n\t}\n\treturn [];\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport const requireVerified: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\n\tif (ctx.user.loginMethod === 'phone') {\n\t\tif (!ctx.user.phoneVerified) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'PHONE_NOT_VERIFIED',\n\t\t\t\t'Please verify your phone number',\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t}\n\n\tif (!ctx.user.emailVerifiedAt) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'EMAIL_NOT_VERIFIED', 'Please verify your email address');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Route-boundary request validation — the one validation middleware every\n// package wires in front of its body-taking routes, so error shape and parse\n// semantics are identical across the whole surface.\n//\n// core stays dependency-free: this accepts anything implementing zod's\n// safeParse contract structurally (zod v3/v4 both match), without importing\n// zod. Feature packages own their schemas; see @fonderie/auth's schemas.ts\n// for the reference pattern.\n\nexport interface IRequestSchema {\n\tsafeParse(input: unknown):\n\t\t| { success: true; data: unknown }\n\t\t| { success: false; error: { issues: Array<{ path: PropertyKey[]; message: string }> } };\n}\n\nexport function validate(schema: IRequestSchema): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst result = schema.safeParse(ctx.meta['body'] ?? {});\n\t\tif (!result.success) {\n\t\t\tconst first = result.error.issues[0];\n\t\t\tconst path = first?.path.length ? `${first.path.map(String).join('.')}: ` : '';\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t`${path}${first?.message ?? 'Invalid request body'}`,\n\t\t\t);\n\t\t}\n\t\t// Parsed output replaces the raw body: trimmed, coerced, unknown keys\n\t\t// stripped — controllers read clean input.\n\t\tctx.meta['body'] = result.data;\n\t\treturn next();\n\t};\n}\n","// Client-IP resolution shared by the adapters. The web-standard Request the\n// pipeline runs on carries no socket address, so each adapter passes the\n// socket's remote address here together with the headers; this resolves the\n// effective client IP with explicit proxy trust.\n//\n// trustProxy semantics (deliberately explicit — a permissive default lets\n// any client spoof X-Forwarded-For and dodge per-IP rate limits):\n// 0 / undefined → ignore forwarding headers; the socket address is the client\n// N > 0 → the client is the Nth-from-the-right entry in\n// X-Forwarded-For (N = number of trusted proxy hops)\n//\n// ⚠️ THE PROXY FOOTGUN. With trustProxy=0 (the spoof-safe default) deployed\n// behind nginx, a Kubernetes ingress, or any L7 proxy, the socket address is\n// the PROXY's IP for every request — so every client collapses onto one\n// per-IP bucket and the limit becomes global (one attacker locks everyone\n// out). You cannot have a default that is both spoof-safe AND correct behind\n// a proxy; they contradict. So we ship spoof-safe and DETECT the mismatch:\n// checkProxyConfig() below warns once, loudly, when the deployment looks\n// proxied but trustProxy is unset. Set TRUST_PROXY=<hops> in that case.\n\nexport function resolveClientIp(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number = trustProxyFromEnv(),\n): string | undefined {\n\tif (trustProxy > 0) {\n\t\tconst xff = headers.get('x-forwarded-for');\n\t\tif (xff) {\n\t\t\tconst hops = xff\n\t\t\t\t.split(',')\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter(Boolean);\n\t\t\tconst candidate = hops[Math.max(0, hops.length - trustProxy)];\n\t\t\tif (candidate) return normalizeIp(candidate);\n\t\t}\n\t}\n\tcheckProxyConfig(socketAddress, headers, trustProxy);\n\treturn socketAddress ? normalizeIp(socketAddress) : undefined;\n}\n\nfunction trustProxyFromEnv(): number {\n\tconst raw = Number(process.env['TRUST_PROXY']);\n\treturn Number.isFinite(raw) && raw > 0 ? raw : 0;\n}\n\nfunction normalizeIp(ip: string): string {\n\t// ::ffff:203.0.113.7 → 203.0.113.7 ; strip port if a proxy appended one\n\tconst noV6Prefix = ip.startsWith('::ffff:') ? ip.slice(7) : ip;\n\tconst m = noV6Prefix.match(/^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/);\n\treturn m ? m[1]! : noV6Prefix;\n}\n\n// Address ranges that indicate a local proxy sits in front of us (so a\n// forwarding header without TRUST_PROXY is a misconfiguration, not spoofing).\nconst LOOPBACK_IPS = new Set(['127.0.0.1', '::1']);\nconst PRIVATE_IP_PREFIXES = [\n\t'10.', // RFC1918\n\t'192.168.', // RFC1918\n\t'169.254.', // link-local\n\t'fc', // IPv6 unique local (fc00::/7)\n\t'fd', // IPv6 unique local\n] as const;\nconst CGNAT_OR_RFC1918_172 = /^172\\.(1[6-9]|2\\d|3[01])\\./; // 172.16.0.0/12\n\nfunction isPrivateOrLoopback(ip: string): boolean {\n\tconst a = normalizeIp(ip);\n\treturn (\n\t\tLOOPBACK_IPS.has(a) ||\n\t\tCGNAT_OR_RFC1918_172.test(a) ||\n\t\tPRIVATE_IP_PREFIXES.some((prefix) => a.startsWith(prefix))\n\t);\n}\n\nlet warned = false;\n\n// Warn ONCE when the deployment looks proxied (forwarding header present, and\n// the socket is a private/loopback address — i.e. a local proxy) but\n// trustProxy is unset. That configuration silently rate-limits every client\n// as one IP. Emitting on the request path (not at boot) is deliberate: the\n// signal we need — an actual X-Forwarded-For header — only exists once real\n// traffic arrives.\nexport function checkProxyConfig(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number,\n): void {\n\tif (warned || trustProxy > 0) return;\n\tconst forwarded =\n\t\theaders.get('x-forwarded-for') ??\n\t\theaders.get('cf-connecting-ip') ??\n\t\theaders.get('x-real-ip');\n\tif (forwarded && socketAddress && isPrivateOrLoopback(socketAddress)) {\n\t\twarned = true;\n\t\tconsole.warn(\n\t\t\t'[fonderie] Requests carry a forwarding header (X-Forwarded-For) and ' +\n\t\t\t\t'arrive from a private/loopback socket, but TRUST_PROXY is unset. ' +\n\t\t\t\t'Every client is being rate-limited as a single IP, which will cause ' +\n\t\t\t\t'global lockout behind nginx / a Kubernetes ingress / any L7 proxy. ' +\n\t\t\t\t'Set TRUST_PROXY=<number of trusted proxy hops>. ' +\n\t\t\t\t'See @fonderie/rate-limit README § Deploying behind a proxy.',\n\t\t);\n\t}\n}\n\n// Test seam — reset the once-only warning latch.\nexport function _resetProxyWarning(): void {\n\twarned = false;\n}\n"],"mappings":"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","names":[]}
package/dist/response.cjs CHANGED
@@ -36,6 +36,7 @@ var HTTP = {
36
36
  NOT_FOUND: 404,
37
37
  CONFLICT: 409,
38
38
  GONE: 410,
39
+ PAYLOAD_TOO_LARGE: 413,
39
40
  UNPROCESSABLE: 422,
40
41
  TOO_MANY_REQUESTS: 429,
41
42
  SERVER_ERROR: 500,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/response.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,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;","names":[]}
1
+ {"version":3,"sources":["../src/response.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,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,mBAAmB;AAAA,EACnB,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;","names":[]}
@@ -10,6 +10,7 @@ declare const HTTP: {
10
10
  readonly NOT_FOUND: 404;
11
11
  readonly CONFLICT: 409;
12
12
  readonly GONE: 410;
13
+ readonly PAYLOAD_TOO_LARGE: 413;
13
14
  readonly UNPROCESSABLE: 422;
14
15
  readonly TOO_MANY_REQUESTS: 429;
15
16
  readonly SERVER_ERROR: 500;
@@ -10,6 +10,7 @@ declare const HTTP: {
10
10
  readonly NOT_FOUND: 404;
11
11
  readonly CONFLICT: 409;
12
12
  readonly GONE: 410;
13
+ readonly PAYLOAD_TOO_LARGE: 413;
13
14
  readonly UNPROCESSABLE: 422;
14
15
  readonly TOO_MANY_REQUESTS: 429;
15
16
  readonly SERVER_ERROR: 500;
package/dist/response.js CHANGED
@@ -11,6 +11,7 @@ var HTTP = {
11
11
  NOT_FOUND: 404,
12
12
  CONFLICT: 409,
13
13
  GONE: 410,
14
+ PAYLOAD_TOO_LARGE: 413,
14
15
  UNPROCESSABLE: 422,
15
16
  TOO_MANY_REQUESTS: 429,
16
17
  SERVER_ERROR: 500,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/response.ts"],"sourcesContent":["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"],"mappings":";AAAO,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;","names":[]}
1
+ {"version":3,"sources":["../src/response.ts"],"sourcesContent":["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"],"mappings":";AAAO,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,mBAAmB;AAAA,EACnB,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;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonderie/core",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Framework core — request router, middleware pipeline, module system, and shared context types. Every other @fonderiejs package depends on this.",
5
5
  "keywords": [
6
6
  "fonderiejs",