@fonderie/core 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  FonderieApp: () => FonderieApp,
24
24
  HTTP: () => HTTP,
25
+ OPERATIONS: () => OPERATIONS,
25
26
  arrayOrEmpty: () => arrayOrEmpty,
26
27
  booleanOrFalse: () => booleanOrFalse,
27
28
  compose: () => compose,
@@ -33,6 +34,14 @@ __export(index_exports, {
33
34
  });
34
35
  module.exports = __toCommonJS(index_exports);
35
36
 
37
+ // src/constants.ts
38
+ var OPERATIONS = {
39
+ CREATE: "create",
40
+ READ: "read",
41
+ UPDATE: "update",
42
+ DELETE: "delete"
43
+ };
44
+
36
45
  // src/app.ts
37
46
  var import_node_os = require("os");
38
47
  var import_node_http = require("http");
@@ -359,6 +368,7 @@ function dateOrEmpty(value) {
359
368
  0 && (module.exports = {
360
369
  FonderieApp,
361
370
  HTTP,
371
+ OPERATIONS,
362
372
  arrayOrEmpty,
363
373
  booleanOrFalse,
364
374
  compose,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/app.ts","../src/router.ts","../src/compose.ts","../src/response.ts","../src/middlewares/not-found.ts","../src/middlewares/body-parser.ts","../src/middlewares/error-handler.ts","../src/config.ts","../src/parser.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport type {\n\tITenant,\n\tIRouter,\n\tIAuthUser,\n\tMiddleware,\n\tIWorkspace,\n\tIRouteMatch,\n\tIFonderieApp,\n\tIFonderieModule,\n\tIFonderieContext,\n\tICourierMessage,\n\tIFonderieContextMeta,\n} from './types';\n\nexport { FonderieApp } 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';\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","import { networkInterfaces } from 'node:os';\nimport { createServer } from 'node:http';\n\nimport type { Middleware, IFonderieApp, IFonderieContext, IFonderieModule } from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { withBody } from './middlewares/body-parser';\n\nexport class FonderieApp {\n\tprivate config: FonderieConfig;\n\tprivate prefix: string;\n\tprivate router: Router = new Router();\n\tprivate middlewares: Middleware[] = [];\n\tprivate modules: Map<string, IFonderieModule> = new Map();\n\n\tconstructor(config: FonderieConfig) {\n\t\tthis.config = config;\n\t\tthis.prefix = (config.basePath ?? '').replace(/\\/$/, '');\n\t\tthis.middlewares = [withBody];\n\t}\n\n\tlisten(\n\t\tport: number,\n\t\toptions: {\n\t\t\tname?: string;\n\t\t\tversion?: string;\n\t\t\tenv?: string;\n\t\t} = {},\n\t): void {\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} = options;\n\n\t\tcreateServer(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\tArray.isArray(value)\n\t\t\t\t\t? value.forEach((v) => headers.append(key, v))\n\t\t\t\t\t: headers.set(key, value);\n\t\t\t}\n\n\t\t\t// Read the body stream — this was missing\n\t\t\tconst body = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\treq.on('data', (chunk: Buffer) => chunks.push(chunk));\n\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\treq.on('error', reject);\n\t\t\t});\n\n\t\t\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\tconst request = new Request(url, {\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: hasBody && body.length > 0 ? new Uint8Array(body) : null,\n\t\t\t});\n\n\t\t\tconst response = await this.handle(request);\n\n\t\t\tres.statusCode = response.status;\n\t\t\tresponse.headers.forEach((v, k) => res.setHeader(k, v));\n\t\t\tres.end(Buffer.from(await response.arrayBuffer()));\n\t\t}).listen(port, () => {\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}\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\tasync boot(): Promise<this> {\n\t\tfor (const module of topoSort([...this.modules.values()])) {\n\t\t\tawait module.install(this);\n\t\t}\n\t\treturn this;\n\t}\n\n\t// Runs global middleware only (no routing, no 404).\n\t// Adapter packages call this to populate user/workspace/meta into their\n\t// native context before handing off to user-defined route handlers.\n\tasync buildContext(request: Request): Promise<IFonderieContext> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: { _buildContext: true },\n\t\t\t_router: this.router,\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\t_router: this.router,\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\ttry {\n\t\t\treturn await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\treturn this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\tonError?: (err: unknown) => Response;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n","export function stringOrEmpty(value: unknown): string {\n\treturn typeof value === 'string' ? value : '';\n}\n\nexport function booleanOrFalse(value: unknown): boolean {\n\tif (typeof value === 'boolean') return value;\n\tif (value === 'true' || value === '1') return true;\n\treturn false;\n}\n\nexport function arrayOrEmpty<T>(value: unknown): T[] {\n\treturn Array.isArray(value) ? (value as T[]) : [];\n}\n\nexport function numberOrZero(value: unknown): number {\n\tconst n = Number(value);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\nexport function dateOrEmpty(value: unknown): string {\n\tif (typeof value === 'string') return value;\n\tif (value instanceof Date) return value.toISOString();\n\treturn '';\n}\n"],"mappings":"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","names":["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/error-handler.ts","../src/config.ts","../src/parser.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\tIFonderieContextMeta,\n} from './types';\n\nexport { OPERATIONS } from './constants';\n\nexport { FonderieApp } 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';\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","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 } from 'node:http';\n\nimport type { Middleware, IFonderieApp, IFonderieContext, IFonderieModule } from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { withBody } from './middlewares/body-parser';\n\nexport class FonderieApp {\n\tprivate config: FonderieConfig;\n\tprivate prefix: string;\n\tprivate router: Router = new Router();\n\tprivate middlewares: Middleware[] = [];\n\tprivate modules: Map<string, IFonderieModule> = new Map();\n\n\tconstructor(config: FonderieConfig) {\n\t\tthis.config = config;\n\t\tthis.prefix = (config.basePath ?? '').replace(/\\/$/, '');\n\t\tthis.middlewares = [withBody];\n\t}\n\n\tlisten(\n\t\tport: number,\n\t\toptions: {\n\t\t\tname?: string;\n\t\t\tversion?: string;\n\t\t\tenv?: string;\n\t\t} = {},\n\t): void {\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} = options;\n\n\t\tcreateServer(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\tArray.isArray(value)\n\t\t\t\t\t? value.forEach((v) => headers.append(key, v))\n\t\t\t\t\t: headers.set(key, value);\n\t\t\t}\n\n\t\t\t// Read the body stream — this was missing\n\t\t\tconst body = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\treq.on('data', (chunk: Buffer) => chunks.push(chunk));\n\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\treq.on('error', reject);\n\t\t\t});\n\n\t\t\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\tconst request = new Request(url, {\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: hasBody && body.length > 0 ? new Uint8Array(body) : null,\n\t\t\t});\n\n\t\t\tconst response = await this.handle(request);\n\n\t\t\tres.statusCode = response.status;\n\t\t\tresponse.headers.forEach((v, k) => res.setHeader(k, v));\n\t\t\tres.end(Buffer.from(await response.arrayBuffer()));\n\t\t}).listen(port, () => {\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}\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\tasync boot(): Promise<this> {\n\t\tfor (const module of topoSort([...this.modules.values()])) {\n\t\t\tawait module.install(this);\n\t\t}\n\t\treturn this;\n\t}\n\n\t// Runs global middleware only (no routing, no 404).\n\t// Adapter packages call this to populate user/workspace/meta into their\n\t// native context before handing off to user-defined route handlers.\n\tasync buildContext(request: Request): Promise<IFonderieContext> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: { _buildContext: true },\n\t\t\t_router: this.router,\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\t_router: this.router,\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\ttry {\n\t\t\treturn await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\treturn this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\tonError?: (err: unknown) => Response;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n","export function stringOrEmpty(value: unknown): string {\n\treturn typeof value === 'string' ? value : '';\n}\n\nexport function booleanOrFalse(value: unknown): boolean {\n\tif (typeof value === 'boolean') return value;\n\tif (value === 'true' || value === '1') return true;\n\treturn false;\n}\n\nexport function arrayOrEmpty<T>(value: unknown): T[] {\n\treturn Array.isArray(value) ? (value as T[]) : [];\n}\n\nexport function numberOrZero(value: unknown): number {\n\tconst n = Number(value);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\nexport function dateOrEmpty(value: unknown): string {\n\tif (typeof value === 'string') return value;\n\tif (value instanceof Date) return value.toISOString();\n\treturn '';\n}\n"],"mappings":"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","names":["module"]}
package/dist/index.d.cts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { IFonderieModule, IFonderieContext, Middleware } from './types.cjs';
2
- export { IAuthUser, ICourierMessage, IFonderieApp, IFonderieContextMeta, IRouteMatch, IRouter, ITenant, IWorkspace } from './types.cjs';
2
+ export { IAuthUser, ICourierMessage, IFonderieApp, IFonderieContextMeta, IRouteMatch, IRouter, ITenant, IWorkspace, Operation } from './types.cjs';
3
3
  import { FonderieConfig } from './config.cjs';
4
4
  export { defineConfig } from './config.cjs';
5
5
  export { arrayOrEmpty, booleanOrFalse, dateOrEmpty, numberOrZero, stringOrEmpty } from './parser.cjs';
6
6
  export { HTTP, HttpStatus, IApiError, setApiResponse } from './response.cjs';
7
7
 
8
+ declare const OPERATIONS: {
9
+ readonly CREATE: "create";
10
+ readonly READ: "read";
11
+ readonly UPDATE: "update";
12
+ readonly DELETE: "delete";
13
+ };
14
+
8
15
  declare class FonderieApp {
9
16
  private config;
10
17
  private prefix;
@@ -27,4 +34,4 @@ declare class FonderieApp {
27
34
 
28
35
  declare function compose(middlewares: Middleware[]): (ctx: IFonderieContext, fallback: () => Promise<Response>) => Promise<Response>;
29
36
 
30
- export { FonderieApp, FonderieConfig, IFonderieContext, IFonderieModule, Middleware, compose };
37
+ export { FonderieApp, FonderieConfig, IFonderieContext, IFonderieModule, Middleware, OPERATIONS, compose };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { IFonderieModule, IFonderieContext, Middleware } from './types.js';
2
- export { IAuthUser, ICourierMessage, IFonderieApp, IFonderieContextMeta, IRouteMatch, IRouter, ITenant, IWorkspace } from './types.js';
2
+ export { IAuthUser, ICourierMessage, IFonderieApp, IFonderieContextMeta, IRouteMatch, IRouter, ITenant, IWorkspace, Operation } from './types.js';
3
3
  import { FonderieConfig } from './config.js';
4
4
  export { defineConfig } from './config.js';
5
5
  export { arrayOrEmpty, booleanOrFalse, dateOrEmpty, numberOrZero, stringOrEmpty } from './parser.js';
6
6
  export { HTTP, HttpStatus, IApiError, setApiResponse } from './response.js';
7
7
 
8
+ declare const OPERATIONS: {
9
+ readonly CREATE: "create";
10
+ readonly READ: "read";
11
+ readonly UPDATE: "update";
12
+ readonly DELETE: "delete";
13
+ };
14
+
8
15
  declare class FonderieApp {
9
16
  private config;
10
17
  private prefix;
@@ -27,4 +34,4 @@ declare class FonderieApp {
27
34
 
28
35
  declare function compose(middlewares: Middleware[]): (ctx: IFonderieContext, fallback: () => Promise<Response>) => Promise<Response>;
29
36
 
30
- export { FonderieApp, FonderieConfig, IFonderieContext, IFonderieModule, Middleware, compose };
37
+ export { FonderieApp, FonderieConfig, IFonderieContext, IFonderieModule, Middleware, OPERATIONS, compose };
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ // src/constants.ts
2
+ var OPERATIONS = {
3
+ CREATE: "create",
4
+ READ: "read",
5
+ UPDATE: "update",
6
+ DELETE: "delete"
7
+ };
8
+
1
9
  // src/app.ts
2
10
  import { networkInterfaces } from "os";
3
11
  import { createServer } from "http";
@@ -323,6 +331,7 @@ function dateOrEmpty(value) {
323
331
  export {
324
332
  FonderieApp,
325
333
  HTTP,
334
+ OPERATIONS,
326
335
  arrayOrEmpty,
327
336
  booleanOrFalse,
328
337
  compose,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/app.ts","../src/router.ts","../src/compose.ts","../src/response.ts","../src/middlewares/not-found.ts","../src/middlewares/body-parser.ts","../src/middlewares/error-handler.ts","../src/config.ts","../src/parser.ts"],"sourcesContent":["import { networkInterfaces } from 'node:os';\nimport { createServer } from 'node:http';\n\nimport type { Middleware, IFonderieApp, IFonderieContext, IFonderieModule } from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { withBody } from './middlewares/body-parser';\n\nexport class FonderieApp {\n\tprivate config: FonderieConfig;\n\tprivate prefix: string;\n\tprivate router: Router = new Router();\n\tprivate middlewares: Middleware[] = [];\n\tprivate modules: Map<string, IFonderieModule> = new Map();\n\n\tconstructor(config: FonderieConfig) {\n\t\tthis.config = config;\n\t\tthis.prefix = (config.basePath ?? '').replace(/\\/$/, '');\n\t\tthis.middlewares = [withBody];\n\t}\n\n\tlisten(\n\t\tport: number,\n\t\toptions: {\n\t\t\tname?: string;\n\t\t\tversion?: string;\n\t\t\tenv?: string;\n\t\t} = {},\n\t): void {\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} = options;\n\n\t\tcreateServer(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\tArray.isArray(value)\n\t\t\t\t\t? value.forEach((v) => headers.append(key, v))\n\t\t\t\t\t: headers.set(key, value);\n\t\t\t}\n\n\t\t\t// Read the body stream — this was missing\n\t\t\tconst body = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\treq.on('data', (chunk: Buffer) => chunks.push(chunk));\n\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\treq.on('error', reject);\n\t\t\t});\n\n\t\t\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\tconst request = new Request(url, {\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: hasBody && body.length > 0 ? new Uint8Array(body) : null,\n\t\t\t});\n\n\t\t\tconst response = await this.handle(request);\n\n\t\t\tres.statusCode = response.status;\n\t\t\tresponse.headers.forEach((v, k) => res.setHeader(k, v));\n\t\t\tres.end(Buffer.from(await response.arrayBuffer()));\n\t\t}).listen(port, () => {\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}\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\tasync boot(): Promise<this> {\n\t\tfor (const module of topoSort([...this.modules.values()])) {\n\t\t\tawait module.install(this);\n\t\t}\n\t\treturn this;\n\t}\n\n\t// Runs global middleware only (no routing, no 404).\n\t// Adapter packages call this to populate user/workspace/meta into their\n\t// native context before handing off to user-defined route handlers.\n\tasync buildContext(request: Request): Promise<IFonderieContext> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: { _buildContext: true },\n\t\t\t_router: this.router,\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\t_router: this.router,\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\ttry {\n\t\t\treturn await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\treturn this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\tonError?: (err: unknown) => Response;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n","export function stringOrEmpty(value: unknown): string {\n\treturn typeof value === 'string' ? value : '';\n}\n\nexport function booleanOrFalse(value: unknown): boolean {\n\tif (typeof value === 'boolean') return value;\n\tif (value === 'true' || value === '1') return true;\n\treturn false;\n}\n\nexport function arrayOrEmpty<T>(value: unknown): T[] {\n\treturn Array.isArray(value) ? (value as T[]) : [];\n}\n\nexport function numberOrZero(value: unknown): number {\n\tconst n = Number(value);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\nexport function dateOrEmpty(value: unknown): string {\n\tif (typeof value === 'string') return value;\n\tif (value instanceof Date) return value.toISOString();\n\treturn '';\n}\n"],"mappings":";AAAA,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;;;ACCtB,IAAM,SAAN,MAAgC;AAAA,EAC9B,SAAuE,CAAC;AAAA,EAEhF,IAAI,QAAgB,MAAc,SAA2B;AAC5D,SAAK,OAAO,KAAK,EAAE,QAAQ,OAAO,YAAY,GAAG,MAAM,QAAQ,CAAC;AAAA,EACjE;AAAA,EAEA,MAAM,QAAgB,MAAkC;AACvD,eAAW,SAAS,KAAK,QAAQ;AAChC,UAAI,MAAM,WAAW,OAAO,YAAY,GAAG;AAC1C;AAAA,MACD;AACA,YAAM,SAAS,UAAU,MAAM,MAAM,IAAI;AACzC,UAAI,WAAW,MAAM;AACpB,eAAO,EAAE,SAAS,MAAM,SAAS,OAAO;AAAA,MACzC;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;AAIA,SAAS,UAAU,SAAiB,MAA6C;AAChF,QAAM,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAAM,QAAQ,OAAO,EAAE,KAAK;AACjE,QAAM,KAAK,QAAQ,MAAM,GAAG;AAC5B,QAAM,KAAK,MAAM,MAAM,GAAG;AAE1B,MAAI,GAAG,WAAW,GAAG,QAAQ;AAC5B,WAAO;AAAA,EACR;AAEA,QAAM,SAAiC,CAAC;AAExC,WAAS,IAAI,GAAG,IAAI,GAAG,QAAQ,KAAK;AACnC,UAAM,KAAK,GAAG,CAAC,KAAK;AACpB,UAAM,KAAK,GAAG,CAAC,KAAK;AACpB,QAAI,GAAG,WAAW,GAAG,GAAG;AACvB,aAAO,GAAG,MAAM,CAAC,CAAC,IAAI,mBAAmB,EAAE;AAAA,IAC5C,WAAW,OAAO,IAAI;AACrB,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SAAO;AACR;AAGO,SAAS,iBAAiB,QAA4B;AAC5D,SAAO,OAAO,KAAuB,SAAS;AAC7C,UAAM,MAAM,IAAI,IAAI,IAAI,QAAQ,GAAG;AACnC,UAAM,QAAQ,OAAO,MAAM,IAAI,QAAQ,QAAQ,IAAI,QAAQ;AAE3D,QAAI,CAAC,OAAO;AACX,aAAO,KAAK;AAAA,IACb;AAGA,QAAI,KAAK,SAAS,MAAM;AACxB,WAAO,MAAM,QAAQ,KAAK,IAAI;AAAA,EAC/B;AACD;;;AC5DO,SAAS,QAAQ,aAA2B;AAClD,SAAO,SAAU,KAAuB,UAAsD;AAC7F,QAAI,QAAQ;AAEZ,aAAS,SAAS,GAA8B;AAC/C,UAAI,KAAK,OAAO;AACf,cAAM,IAAI,MAAM,8BAA8B;AAAA,MAC/C;AACA,cAAQ;AACR,YAAM,KAAK,YAAY,CAAC,KAAK;AAC7B,aAAO,GAAG,KAAK,MAAM,SAAS,IAAI,CAAC,CAAC;AAAA,IACrC;AAEA,WAAO,SAAS,CAAC;AAAA,EAClB;AACD;;;AClBO,IAAM,OAAO;AAAA,EACnB,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,MAAM;AAAA,EACN,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,qBAAqB;AACtB;AAgBO,SAAS,eACf,QACA,QACA,aACA,SACW;AACX,QAAM,OAAgC,EAAE,QAAQ,YAAY;AAC5D,MAAI,YAAY,QAAW;AAC1B,SAAK,SAAS,MAAM,WAAW,SAAS,IAAI;AAAA,EAC7C;AACA,SAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;AACtC;;;AC1CO,SAAS,qBAAiC;AAChD,SAAO,OAAO,MAAM,UAAU,eAAe,KAAK,WAAW,aAAa,WAAW;AACtF;;;ACFO,IAAM,WAAuB,OAAO,KAAK,SAAS;AACxD,QAAM,SAAS,IAAI,QAAQ,OAAO,YAAY;AAE9C,MAAI,WAAW,SAAS,WAAW,QAAQ;AAC1C,WAAO,KAAK;AAAA,EACb;AAEA,QAAM,KAAK,IAAI,QAAQ,QAAQ,IAAI,cAAc,KAAK;AAEtD,MAAI;AACH,QAAI,GAAG,SAAS,kBAAkB,GAAG;AACpC,YAAM,QAAQ,MAAM,IAAI,QAAQ,MAAM,EAAE,KAAK,GAAG,KAAK;AACrD,UAAI,KAAK,OAAO,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;AAAA,IAC5C,WAAW,GAAG,SAAS,mCAAmC,GAAG;AAC5D,YAAM,OAAO,MAAM,IAAI,QAAQ,MAAM,EAAE,KAAK;AAC5C,UAAI,KAAK,OAAO,OAAO,YAAY,IAAI,gBAAgB,IAAI,CAAC;AAAA,IAC7D;AAAA,EAED,QAAQ;AACP,WAAO,eAAe,KAAK,aAAa,mBAAmB,sBAAsB;AAAA,EAClF;AAEA,SAAO,KAAK;AACb;;;ACxBO,SAAS,oBAAoB,KAAwB;AAC3D,QAAM,MAAM,QAAQ,IAAI,UAAU,MAAM;AAExC,MAAI,eAAe,OAAO;AACzB,YAAQ,MAAM,cAAc,IAAI,SAAS,IAAI,KAAK;AAClD,WAAO;AAAA,MACN,KAAK;AAAA,MACL;AAAA,MACA,MAAM,IAAI,UAAU;AAAA,IACrB;AAAA,EACD;AAEA,UAAQ,MAAM,4BAA4B,GAAG;AAC7C,SAAO,eAAe,KAAK,cAAc,gBAAgB,uBAAuB;AACjF;;;ANNO,IAAM,cAAN,MAAkB;AAAA,EAChB;AAAA,EACA;AAAA,EACA,SAAiB,IAAI,OAAO;AAAA,EAC5B,cAA4B,CAAC;AAAA,EAC7B,UAAwC,oBAAI,IAAI;AAAA,EAExD,YAAY,QAAwB;AACnC,SAAK,SAAS;AACd,SAAK,UAAU,OAAO,YAAY,IAAI,QAAQ,OAAO,EAAE;AACvD,SAAK,cAAc,CAAC,QAAQ;AAAA,EAC7B;AAAA,EAEA,OACC,MACA,UAII,CAAC,GACE;AACP,UAAM;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,MACV,MAAM,QAAQ,IAAI,UAAU,KAAK;AAAA,IAClC,IAAI;AAEJ,iBAAa,OAAO,KAAK,QAAQ;AAChC,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,cAAM,QAAQ,KAAK,IAChB,MAAM,QAAQ,CAAC,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,IAC3C,QAAQ,IAAI,KAAK,KAAK;AAAA,MAC1B;AAGA,YAAM,OAAO,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC3D,cAAM,SAAmB,CAAC;AAC1B,YAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,YAAI,GAAG,OAAO,MAAM,QAAQ,OAAO,OAAO,MAAM,CAAC,CAAC;AAClD,YAAI,GAAG,SAAS,MAAM;AAAA,MACvB,CAAC;AAED,YAAM,SAAS,IAAI,UAAU;AAC7B,YAAM,UAAU,CAAC,CAAC,OAAO,MAAM,EAAE,SAAS,OAAO,YAAY,CAAC;AAE9D,YAAM,UAAU,IAAI,QAAQ,KAAK;AAAA,QAChC;AAAA,QACA;AAAA,QACA,MAAM,WAAW,KAAK,SAAS,IAAI,IAAI,WAAW,IAAI,IAAI;AAAA,MAC3D,CAAC;AAED,YAAM,WAAW,MAAM,KAAK,OAAO,OAAO;AAE1C,UAAI,aAAa,SAAS;AAC1B,eAAS,QAAQ,QAAQ,CAAC,GAAG,MAAM,IAAI,UAAU,GAAG,CAAC,CAAC;AACtD,UAAI,IAAI,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC;AAAA,IAClD,CAAC,EAAE,OAAO,MAAM,MAAM;AACrB,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;AAAA,EACF;AAAA;AAAA,EAIA,SAAS,QAA+B;AACvC,SAAK,QAAQ,IAAI,OAAO,MAAM,MAAM;AACpC,WAAO;AAAA,EACR;AAAA,EAEA,MAAM,OAAsB;AAC3B,eAAW,UAAU,SAAS,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,GAAG;AAC1D,YAAM,OAAO,QAAQ,IAAI;AAAA,IAC1B;AACA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,SAA6C;AAC/D,UAAM,MAAwB;AAAA,MAC7B;AAAA,MACA,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,WAAW;AAAA,MACX,MAAM,EAAE,eAAe,KAAK;AAAA,MAC5B,SAAS,KAAK;AAAA,IACf;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,MACP,SAAS,KAAK;AAAA,IACf;AAGA,UAAM,WAAW,QAAQ;AAAA,MACxB,GAAG,KAAK;AAAA,MACR,iBAAiB,KAAK,MAAM;AAAA,MAC5B,mBAAmB;AAAA,IACpB,CAAC;AAED,QAAI;AACH,aAAO,MAAM,SAAS,KAAK,YAAY,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,IAClF,SAAS,KAAK;AACb,aAAO,KAAK,OAAO,UAAU,GAAG,KAAK,oBAAoB,GAAG;AAAA,IAC7D;AAAA,EACD;AACD;AAMA,SAAS,SAAS,SAA+C;AAChE,QAAM,SAAS,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACtD,QAAM,SAA4B,CAAC;AACnC,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,WAAW,oBAAI,IAAY;AAEjC,WAAS,MAAM,GAAoB,MAAsB;AACxD,QAAI,QAAQ,IAAI,EAAE,IAAI,EAAG;AACzB,QAAI,SAAS,IAAI,EAAE,IAAI,GAAG;AACzB,YAAM,IAAI,MAAM,mCAAmC,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,KAAK,UAAK,CAAC,EAAE;AAAA,IACnF;AACA,aAAS,IAAI,EAAE,IAAI;AACnB,eAAW,OAAO,EAAE,QAAQ,CAAC,GAAG;AAC/B,YAAM,QAAQ,OAAO,IAAI,GAAG;AAC5B,UAAI,CAAC;AACJ,cAAM,IAAI,MAAM,eAAe,EAAE,IAAI,eAAe,GAAG,4BAA4B;AACpF,YAAM,OAAO,CAAC,GAAG,MAAM,EAAE,IAAI,CAAC;AAAA,IAC/B;AACA,aAAS,OAAO,EAAE,IAAI;AACtB,YAAQ,IAAI,EAAE,IAAI;AAClB,WAAO,KAAK,CAAC;AAAA,EACd;AAEA,aAAW,KAAK,QAAS,OAAM,GAAG,CAAC,CAAC;AACpC,SAAO;AACR;AAEA,SAAS,eAAuB;AAC/B,QAAM,OAAO,kBAAkB;AAE/B,aAAW,cAAc,OAAO,OAAO,IAAI,GAAG;AAC7C,QAAI,CAAC,YAAY;AAChB;AAAA,IACD;AAEA,eAAW,SAAS,YAAY;AAC/B,UAAI,MAAM,WAAW,UAAU,CAAC,MAAM,UAAU;AAC/C,eAAO,MAAM;AAAA,MACd;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AACR;;;AOnKO,SAAS,aAAa,QAAwC;AACpE,SAAO;AACR;;;AC5CO,SAAS,cAAc,OAAwB;AACrD,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC5C;AAEO,SAAS,eAAe,OAAyB;AACvD,MAAI,OAAO,UAAU,UAAW,QAAO;AACvC,MAAI,UAAU,UAAU,UAAU,IAAK,QAAO;AAC9C,SAAO;AACR;AAEO,SAAS,aAAgB,OAAqB;AACpD,SAAO,MAAM,QAAQ,KAAK,IAAK,QAAgB,CAAC;AACjD;AAEO,SAAS,aAAa,OAAwB;AACpD,QAAM,IAAI,OAAO,KAAK;AACtB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AACjC;AAEO,SAAS,YAAY,OAAwB;AACnD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,SAAO;AACR;","names":[]}
1
+ {"version":3,"sources":["../src/constants.ts","../src/app.ts","../src/router.ts","../src/compose.ts","../src/response.ts","../src/middlewares/not-found.ts","../src/middlewares/body-parser.ts","../src/middlewares/error-handler.ts","../src/config.ts","../src/parser.ts"],"sourcesContent":["import type { Operation } from './types';\n\n// CRUD operation names shared across modules. Canonical home is core so the\n// adapters (which peer only on core) can re-export OPERATIONS without loading\n// @fonderie/permissions; permissions re-exports it for backward compatibility.\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n","import { networkInterfaces } from 'node:os';\nimport { createServer } from 'node:http';\n\nimport type { Middleware, IFonderieApp, IFonderieContext, IFonderieModule } from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { withBody } from './middlewares/body-parser';\n\nexport class FonderieApp {\n\tprivate config: FonderieConfig;\n\tprivate prefix: string;\n\tprivate router: Router = new Router();\n\tprivate middlewares: Middleware[] = [];\n\tprivate modules: Map<string, IFonderieModule> = new Map();\n\n\tconstructor(config: FonderieConfig) {\n\t\tthis.config = config;\n\t\tthis.prefix = (config.basePath ?? '').replace(/\\/$/, '');\n\t\tthis.middlewares = [withBody];\n\t}\n\n\tlisten(\n\t\tport: number,\n\t\toptions: {\n\t\t\tname?: string;\n\t\t\tversion?: string;\n\t\t\tenv?: string;\n\t\t} = {},\n\t): void {\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} = options;\n\n\t\tcreateServer(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\tArray.isArray(value)\n\t\t\t\t\t? value.forEach((v) => headers.append(key, v))\n\t\t\t\t\t: headers.set(key, value);\n\t\t\t}\n\n\t\t\t// Read the body stream — this was missing\n\t\t\tconst body = await new Promise<Buffer>((resolve, reject) => {\n\t\t\t\tconst chunks: Buffer[] = [];\n\t\t\t\treq.on('data', (chunk: Buffer) => chunks.push(chunk));\n\t\t\t\treq.on('end', () => resolve(Buffer.concat(chunks)));\n\t\t\t\treq.on('error', reject);\n\t\t\t});\n\n\t\t\tconst method = req.method ?? 'GET';\n\t\t\tconst hasBody = !['GET', 'HEAD'].includes(method.toUpperCase());\n\n\t\t\tconst request = new Request(url, {\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: hasBody && body.length > 0 ? new Uint8Array(body) : null,\n\t\t\t});\n\n\t\t\tconst response = await this.handle(request);\n\n\t\t\tres.statusCode = response.status;\n\t\t\tresponse.headers.forEach((v, k) => res.setHeader(k, v));\n\t\t\tres.end(Buffer.from(await response.arrayBuffer()));\n\t\t}).listen(port, () => {\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}\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\tasync boot(): Promise<this> {\n\t\tfor (const module of topoSort([...this.modules.values()])) {\n\t\t\tawait module.install(this);\n\t\t}\n\t\treturn this;\n\t}\n\n\t// Runs global middleware only (no routing, no 404).\n\t// Adapter packages call this to populate user/workspace/meta into their\n\t// native context before handing off to user-defined route handlers.\n\tasync buildContext(request: Request): Promise<IFonderieContext> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: { _buildContext: true },\n\t\t\t_router: this.router,\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\t_router: this.router,\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\ttry {\n\t\t\treturn await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\treturn this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\tonError?: (err: unknown) => Response;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n","export function stringOrEmpty(value: unknown): string {\n\treturn typeof value === 'string' ? value : '';\n}\n\nexport function booleanOrFalse(value: unknown): boolean {\n\tif (typeof value === 'boolean') return value;\n\tif (value === 'true' || value === '1') return true;\n\treturn false;\n}\n\nexport function arrayOrEmpty<T>(value: unknown): T[] {\n\treturn Array.isArray(value) ? (value as T[]) : [];\n}\n\nexport function numberOrZero(value: unknown): number {\n\tconst n = Number(value);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\nexport function dateOrEmpty(value: unknown): string {\n\tif (typeof value === 'string') return value;\n\tif (value instanceof Date) return value.toISOString();\n\treturn '';\n}\n"],"mappings":"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","names":[]}
@@ -20,11 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/middlewares/index.ts
21
21
  var middlewares_exports = {};
22
22
  __export(middlewares_exports, {
23
+ checkProxyConfig: () => checkProxyConfig,
23
24
  defaultErrorHandler: () => defaultErrorHandler,
24
25
  notFoundMiddleware: () => notFoundMiddleware,
25
26
  requireAnyAuth: () => requireAnyAuth,
26
27
  requireAuth: () => requireAuth,
27
28
  requireVerified: () => requireVerified,
29
+ resolveClientIp: () => resolveClientIp,
30
+ validate: () => validate,
28
31
  withBody: () => withBody,
29
32
  withCors: () => withCors,
30
33
  withLogger: () => withLogger
@@ -187,13 +190,74 @@ var requireVerified = async (ctx, next) => {
187
190
  }
188
191
  return next();
189
192
  };
193
+
194
+ // src/middlewares/validate.ts
195
+ function validate(schema) {
196
+ return async (ctx, next) => {
197
+ const result = schema.safeParse(ctx.meta["body"] ?? {});
198
+ if (!result.success) {
199
+ const first = result.error.issues[0];
200
+ const path = first?.path.length ? `${first.path.map(String).join(".")}: ` : "";
201
+ return setApiResponse(
202
+ HTTP.UNPROCESSABLE,
203
+ "INVALID_PARAMETER",
204
+ `${path}${first?.message ?? "Invalid request body"}`
205
+ );
206
+ }
207
+ ctx.meta["body"] = result.data;
208
+ return next();
209
+ };
210
+ }
211
+
212
+ // src/middlewares/client-ip.ts
213
+ function resolveClientIp(socketAddress, headers, trustProxy = trustProxyFromEnv()) {
214
+ if (trustProxy > 0) {
215
+ const xff = headers.get("x-forwarded-for");
216
+ if (xff) {
217
+ const hops = xff.split(",").map((s) => s.trim()).filter(Boolean);
218
+ const candidate = hops[Math.max(0, hops.length - trustProxy)];
219
+ if (candidate) return normalizeIp(candidate);
220
+ }
221
+ }
222
+ checkProxyConfig(socketAddress, headers, trustProxy);
223
+ return socketAddress ? normalizeIp(socketAddress) : void 0;
224
+ }
225
+ function trustProxyFromEnv() {
226
+ const raw = Number(process.env["TRUST_PROXY"]);
227
+ return Number.isFinite(raw) && raw > 0 ? raw : 0;
228
+ }
229
+ function normalizeIp(ip) {
230
+ const noV6Prefix = ip.startsWith("::ffff:") ? ip.slice(7) : ip;
231
+ const m = noV6Prefix.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/);
232
+ return m ? m[1] : noV6Prefix;
233
+ }
234
+ function isPrivateOrLoopback(ip) {
235
+ const a = normalizeIp(ip);
236
+ return a === "127.0.0.1" || a === "::1" || a.startsWith("10.") || a.startsWith("192.168.") || a.startsWith("169.254.") || // link-local
237
+ a.startsWith("fc") || a.startsWith("fd") || // IPv6 ULA
238
+ /^172\.(1[6-9]|2\d|3[01])\./.test(a);
239
+ }
240
+ var warned = false;
241
+ function checkProxyConfig(socketAddress, headers, trustProxy) {
242
+ if (warned || trustProxy > 0) return;
243
+ const forwarded = headers.get("x-forwarded-for") ?? headers.get("cf-connecting-ip") ?? headers.get("x-real-ip");
244
+ if (forwarded && socketAddress && isPrivateOrLoopback(socketAddress)) {
245
+ warned = true;
246
+ console.warn(
247
+ "[fonderie] Requests carry a forwarding header (X-Forwarded-For) and arrive from a private/loopback socket, but TRUST_PROXY is unset. Every client is being rate-limited as a single IP, which will cause global lockout behind nginx / a Kubernetes ingress / any L7 proxy. Set TRUST_PROXY=<number of trusted proxy hops>. See @fonderie/rate-limit README \xA7 Deploying behind a proxy."
248
+ );
249
+ }
250
+ }
190
251
  // Annotate the CommonJS export names for ESM import in node:
191
252
  0 && (module.exports = {
253
+ checkProxyConfig,
192
254
  defaultErrorHandler,
193
255
  notFoundMiddleware,
194
256
  requireAnyAuth,
195
257
  requireAuth,
196
258
  requireVerified,
259
+ resolveClientIp,
260
+ validate,
197
261
  withBody,
198
262
  withCors,
199
263
  withLogger
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/cors.ts","../../src/middlewares/logger.ts","../../src/response.ts","../../src/middlewares/not-found.ts","../../src/middlewares/body-parser.ts","../../src/middlewares/error-handler.ts","../../src/middlewares/require-auth.ts","../../src/middlewares/require-verified.ts"],"sourcesContent":["export type { CorsOptions } from './cors';\nexport { withCors } from './cors';\nexport { withLogger } from './logger';\nexport { notFoundMiddleware } from './not-found';\nexport { withBody } from './body-parser';\nexport { defaultErrorHandler } from './error-handler';\nexport { requireAuth, requireAnyAuth } from './require-auth';\nexport { requireVerified } from './require-verified';\n","import type { Middleware } from '../types';\n\nexport interface CorsOptions {\n\tmethods?: string[];\n\theaders?: string[];\n\torigin?: string | ((requestOrigin: string) => boolean);\n}\n\nexport function withCors(options: CorsOptions = {}): Middleware {\n\tconst {\n\t\torigin = '*',\n\t\theaders = ['Content-Type', 'Authorization'],\n\t\tmethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n\t} = options;\n\n\treturn async (ctx, next) => {\n\t\tconst requestOrigin = ctx.request.headers.get('origin') ?? '';\n\n\t\tconst allowOrigin =\n\t\t\ttypeof origin === 'function' ? (origin(requestOrigin) ? requestOrigin : '') : origin;\n\n\t\tconst corsHeaders: Record<string, string> = {\n\t\t\t'Access-Control-Max-Age': '86400',\n\t\t\t'Access-Control-Allow-Origin': allowOrigin,\n\t\t\t'Access-Control-Allow-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\n\t\t// Preflight — respond immediately, skip the pipeline\n\t\tif (ctx.request.method === 'OPTIONS') {\n\t\t\treturn new Response(null, { status: 204, headers: corsHeaders });\n\t\t}\n\n\t\tconst response = await next();\n\n\t\tconst patched = new Headers(response.headers);\n\n\t\tfor (const [k, v] of Object.entries(corsHeaders)) {\n\t\t\tpatched.set(k, v);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n","import type { Middleware } from '../types';\n\nexport const withLogger: Middleware = async (ctx, next) => {\n\tconst start = Date.now();\n\n\tconst { method, url } = ctx.request;\n\tconst { pathname } = new URL(url);\n\n\tconst response = await next();\n\n\tconsole.log(\n\t\tJSON.stringify({\n\t\t\tts: new Date().toISOString(),\n\t\t\tmethod,\n\t\t\tpath: pathname,\n\t\t\tstatus: response.status,\n\t\t\tms: Date.now() - start,\n\t\t}),\n\t);\n\n\treturn response;\n};\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Requires a fully-authenticated user. Rejects mfaPending tokens — those are\n// short-lived pre-auth tokens issued mid-MFA-login and must not grant access\n// to any route other than /auth/mfa/verify.\nexport const requireAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\tif (ctx.user.mfaPending) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t}\n\treturn next();\n};\n\n// Accepts both fully-authenticated and mfaPending tokens. Only for routes that\n// need to serve both contexts on the same path (e.g. POST /auth/mfa/verify\n// handles setup confirmation with a full token and TOTP login with mfaPending).\nexport const requireAnyAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport const requireVerified: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\n\tif (ctx.user.loginMethod === 'phone') {\n\t\tif (!ctx.user.phoneVerified) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'PHONE_NOT_VERIFIED',\n\t\t\t\t'Please verify your phone number',\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t}\n\n\tif (!ctx.user.emailVerifiedAt) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'EMAIL_NOT_VERIFIED', 'Please verify your email address');\n\t}\n\n\treturn next();\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/cors.ts","../../src/middlewares/logger.ts","../../src/response.ts","../../src/middlewares/not-found.ts","../../src/middlewares/body-parser.ts","../../src/middlewares/error-handler.ts","../../src/middlewares/require-auth.ts","../../src/middlewares/require-verified.ts","../../src/middlewares/validate.ts","../../src/middlewares/client-ip.ts"],"sourcesContent":["export type { CorsOptions } from './cors';\nexport { withCors } from './cors';\nexport { withLogger } from './logger';\nexport { notFoundMiddleware } from './not-found';\nexport { withBody } from './body-parser';\nexport { defaultErrorHandler } from './error-handler';\nexport { requireAuth, requireAnyAuth } from './require-auth';\nexport { requireVerified } from './require-verified';\nexport { validate } from './validate';\nexport type { IRequestSchema } from './validate';\nexport { resolveClientIp, checkProxyConfig } from './client-ip';\n","import type { Middleware } from '../types';\n\nexport interface CorsOptions {\n\tmethods?: string[];\n\theaders?: string[];\n\torigin?: string | ((requestOrigin: string) => boolean);\n}\n\nexport function withCors(options: CorsOptions = {}): Middleware {\n\tconst {\n\t\torigin = '*',\n\t\theaders = ['Content-Type', 'Authorization'],\n\t\tmethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n\t} = options;\n\n\treturn async (ctx, next) => {\n\t\tconst requestOrigin = ctx.request.headers.get('origin') ?? '';\n\n\t\tconst allowOrigin =\n\t\t\ttypeof origin === 'function' ? (origin(requestOrigin) ? requestOrigin : '') : origin;\n\n\t\tconst corsHeaders: Record<string, string> = {\n\t\t\t'Access-Control-Max-Age': '86400',\n\t\t\t'Access-Control-Allow-Origin': allowOrigin,\n\t\t\t'Access-Control-Allow-Methods': methods.join(', '),\n\t\t\t'Access-Control-Allow-Headers': headers.join(', '),\n\t\t};\n\n\t\t// Preflight — respond immediately, skip the pipeline\n\t\tif (ctx.request.method === 'OPTIONS') {\n\t\t\treturn new Response(null, { status: 204, headers: corsHeaders });\n\t\t}\n\n\t\tconst response = await next();\n\n\t\tconst patched = new Headers(response.headers);\n\n\t\tfor (const [k, v] of Object.entries(corsHeaders)) {\n\t\t\tpatched.set(k, v);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n","import type { Middleware } from '../types';\n\nexport const withLogger: Middleware = async (ctx, next) => {\n\tconst start = Date.now();\n\n\tconst { method, url } = ctx.request;\n\tconst { pathname } = new URL(url);\n\n\tconst response = await next();\n\n\tconsole.log(\n\t\tJSON.stringify({\n\t\t\tts: new Date().toISOString(),\n\t\t\tmethod,\n\t\t\tpath: pathname,\n\t\t\tstatus: response.status,\n\t\t\tms: Date.now() - start,\n\t\t}),\n\t);\n\n\treturn response;\n};\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Requires a fully-authenticated user. Rejects mfaPending tokens — those are\n// short-lived pre-auth tokens issued mid-MFA-login and must not grant access\n// to any route other than /auth/mfa/verify.\nexport const requireAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\tif (ctx.user.mfaPending) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t}\n\treturn next();\n};\n\n// Accepts both fully-authenticated and mfaPending tokens. Only for routes that\n// need to serve both contexts on the same path (e.g. POST /auth/mfa/verify\n// handles setup confirmation with a full token and TOTP login with mfaPending).\nexport const requireAnyAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport const requireVerified: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\n\tif (ctx.user.loginMethod === 'phone') {\n\t\tif (!ctx.user.phoneVerified) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'PHONE_NOT_VERIFIED',\n\t\t\t\t'Please verify your phone number',\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t}\n\n\tif (!ctx.user.emailVerifiedAt) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'EMAIL_NOT_VERIFIED', 'Please verify your email address');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Route-boundary request validation — the one validation middleware every\n// package wires in front of its body-taking routes, so error shape and parse\n// semantics are identical across the whole surface.\n//\n// core stays dependency-free: this accepts anything implementing zod's\n// safeParse contract structurally (zod v3/v4 both match), without importing\n// zod. Feature packages own their schemas; see @fonderie/auth's schemas.ts\n// for the reference pattern.\n\nexport interface IRequestSchema {\n\tsafeParse(input: unknown):\n\t\t| { success: true; data: unknown }\n\t\t| { success: false; error: { issues: Array<{ path: PropertyKey[]; message: string }> } };\n}\n\nexport function validate(schema: IRequestSchema): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst result = schema.safeParse(ctx.meta['body'] ?? {});\n\t\tif (!result.success) {\n\t\t\tconst first = result.error.issues[0];\n\t\t\tconst path = first?.path.length ? `${first.path.map(String).join('.')}: ` : '';\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t`${path}${first?.message ?? 'Invalid request body'}`,\n\t\t\t);\n\t\t}\n\t\t// Parsed output replaces the raw body: trimmed, coerced, unknown keys\n\t\t// stripped — controllers read clean input.\n\t\tctx.meta['body'] = result.data;\n\t\treturn next();\n\t};\n}\n","// Client-IP resolution shared by the adapters. The web-standard Request the\n// pipeline runs on carries no socket address, so each adapter passes the\n// socket's remote address here together with the headers; this resolves the\n// effective client IP with explicit proxy trust.\n//\n// trustProxy semantics (deliberately explicit — a permissive default lets\n// any client spoof X-Forwarded-For and dodge per-IP rate limits):\n// 0 / undefined → ignore forwarding headers; the socket address is the client\n// N > 0 → the client is the Nth-from-the-right entry in\n// X-Forwarded-For (N = number of trusted proxy hops)\n//\n// ⚠️ THE PROXY FOOTGUN. With trustProxy=0 (the spoof-safe default) deployed\n// behind nginx, a Kubernetes ingress, or any L7 proxy, the socket address is\n// the PROXY's IP for every request — so every client collapses onto one\n// per-IP bucket and the limit becomes global (one attacker locks everyone\n// out). You cannot have a default that is both spoof-safe AND correct behind\n// a proxy; they contradict. So we ship spoof-safe and DETECT the mismatch:\n// checkProxyConfig() below warns once, loudly, when the deployment looks\n// proxied but trustProxy is unset. Set TRUST_PROXY=<hops> in that case.\n\nexport function resolveClientIp(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number = trustProxyFromEnv(),\n): string | undefined {\n\tif (trustProxy > 0) {\n\t\tconst xff = headers.get('x-forwarded-for');\n\t\tif (xff) {\n\t\t\tconst hops = xff\n\t\t\t\t.split(',')\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter(Boolean);\n\t\t\tconst candidate = hops[Math.max(0, hops.length - trustProxy)];\n\t\t\tif (candidate) return normalizeIp(candidate);\n\t\t}\n\t}\n\tcheckProxyConfig(socketAddress, headers, trustProxy);\n\treturn socketAddress ? normalizeIp(socketAddress) : undefined;\n}\n\nfunction trustProxyFromEnv(): number {\n\tconst raw = Number(process.env['TRUST_PROXY']);\n\treturn Number.isFinite(raw) && raw > 0 ? raw : 0;\n}\n\nfunction normalizeIp(ip: string): string {\n\t// ::ffff:203.0.113.7 → 203.0.113.7 ; strip port if a proxy appended one\n\tconst noV6Prefix = ip.startsWith('::ffff:') ? ip.slice(7) : ip;\n\tconst m = noV6Prefix.match(/^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/);\n\treturn m ? m[1]! : noV6Prefix;\n}\n\nfunction isPrivateOrLoopback(ip: string): boolean {\n\tconst a = normalizeIp(ip);\n\treturn (\n\t\ta === '127.0.0.1' ||\n\t\ta === '::1' ||\n\t\ta.startsWith('10.') ||\n\t\ta.startsWith('192.168.') ||\n\t\ta.startsWith('169.254.') || // link-local\n\t\ta.startsWith('fc') ||\n\t\ta.startsWith('fd') || // IPv6 ULA\n\t\t/^172\\.(1[6-9]|2\\d|3[01])\\./.test(a) // 172.16.0.0/12\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":[]}
@@ -20,4 +20,23 @@ declare const requireAnyAuth: Middleware;
20
20
 
21
21
  declare const requireVerified: Middleware;
22
22
 
23
- export { type CorsOptions, defaultErrorHandler, notFoundMiddleware, requireAnyAuth, requireAuth, requireVerified, withBody, withCors, withLogger };
23
+ interface IRequestSchema {
24
+ safeParse(input: unknown): {
25
+ success: true;
26
+ data: unknown;
27
+ } | {
28
+ success: false;
29
+ error: {
30
+ issues: Array<{
31
+ path: PropertyKey[];
32
+ message: string;
33
+ }>;
34
+ };
35
+ };
36
+ }
37
+ declare function validate(schema: IRequestSchema): Middleware;
38
+
39
+ declare function resolveClientIp(socketAddress: string | undefined, headers: Headers, trustProxy?: number): string | undefined;
40
+ declare function checkProxyConfig(socketAddress: string | undefined, headers: Headers, trustProxy: number): void;
41
+
42
+ export { type CorsOptions, type IRequestSchema, checkProxyConfig, defaultErrorHandler, notFoundMiddleware, requireAnyAuth, requireAuth, requireVerified, resolveClientIp, validate, withBody, withCors, withLogger };
@@ -20,4 +20,23 @@ declare const requireAnyAuth: Middleware;
20
20
 
21
21
  declare const requireVerified: Middleware;
22
22
 
23
- export { type CorsOptions, defaultErrorHandler, notFoundMiddleware, requireAnyAuth, requireAuth, requireVerified, withBody, withCors, withLogger };
23
+ interface IRequestSchema {
24
+ safeParse(input: unknown): {
25
+ success: true;
26
+ data: unknown;
27
+ } | {
28
+ success: false;
29
+ error: {
30
+ issues: Array<{
31
+ path: PropertyKey[];
32
+ message: string;
33
+ }>;
34
+ };
35
+ };
36
+ }
37
+ declare function validate(schema: IRequestSchema): Middleware;
38
+
39
+ declare function resolveClientIp(socketAddress: string | undefined, headers: Headers, trustProxy?: number): string | undefined;
40
+ declare function checkProxyConfig(socketAddress: string | undefined, headers: Headers, trustProxy: number): void;
41
+
42
+ export { type CorsOptions, type IRequestSchema, checkProxyConfig, defaultErrorHandler, notFoundMiddleware, requireAnyAuth, requireAuth, requireVerified, resolveClientIp, validate, withBody, withCors, withLogger };
@@ -154,12 +154,73 @@ var requireVerified = async (ctx, next) => {
154
154
  }
155
155
  return next();
156
156
  };
157
+
158
+ // src/middlewares/validate.ts
159
+ function validate(schema) {
160
+ return async (ctx, next) => {
161
+ const result = schema.safeParse(ctx.meta["body"] ?? {});
162
+ if (!result.success) {
163
+ const first = result.error.issues[0];
164
+ const path = first?.path.length ? `${first.path.map(String).join(".")}: ` : "";
165
+ return setApiResponse(
166
+ HTTP.UNPROCESSABLE,
167
+ "INVALID_PARAMETER",
168
+ `${path}${first?.message ?? "Invalid request body"}`
169
+ );
170
+ }
171
+ ctx.meta["body"] = result.data;
172
+ return next();
173
+ };
174
+ }
175
+
176
+ // src/middlewares/client-ip.ts
177
+ function resolveClientIp(socketAddress, headers, trustProxy = trustProxyFromEnv()) {
178
+ if (trustProxy > 0) {
179
+ const xff = headers.get("x-forwarded-for");
180
+ if (xff) {
181
+ const hops = xff.split(",").map((s) => s.trim()).filter(Boolean);
182
+ const candidate = hops[Math.max(0, hops.length - trustProxy)];
183
+ if (candidate) return normalizeIp(candidate);
184
+ }
185
+ }
186
+ checkProxyConfig(socketAddress, headers, trustProxy);
187
+ return socketAddress ? normalizeIp(socketAddress) : void 0;
188
+ }
189
+ function trustProxyFromEnv() {
190
+ const raw = Number(process.env["TRUST_PROXY"]);
191
+ return Number.isFinite(raw) && raw > 0 ? raw : 0;
192
+ }
193
+ function normalizeIp(ip) {
194
+ const noV6Prefix = ip.startsWith("::ffff:") ? ip.slice(7) : ip;
195
+ const m = noV6Prefix.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/);
196
+ return m ? m[1] : noV6Prefix;
197
+ }
198
+ function isPrivateOrLoopback(ip) {
199
+ const a = normalizeIp(ip);
200
+ return a === "127.0.0.1" || a === "::1" || a.startsWith("10.") || a.startsWith("192.168.") || a.startsWith("169.254.") || // link-local
201
+ a.startsWith("fc") || a.startsWith("fd") || // IPv6 ULA
202
+ /^172\.(1[6-9]|2\d|3[01])\./.test(a);
203
+ }
204
+ var warned = false;
205
+ function checkProxyConfig(socketAddress, headers, trustProxy) {
206
+ if (warned || trustProxy > 0) return;
207
+ const forwarded = headers.get("x-forwarded-for") ?? headers.get("cf-connecting-ip") ?? headers.get("x-real-ip");
208
+ if (forwarded && socketAddress && isPrivateOrLoopback(socketAddress)) {
209
+ warned = true;
210
+ console.warn(
211
+ "[fonderie] Requests carry a forwarding header (X-Forwarded-For) and arrive from a private/loopback socket, but TRUST_PROXY is unset. Every client is being rate-limited as a single IP, which will cause global lockout behind nginx / a Kubernetes ingress / any L7 proxy. Set TRUST_PROXY=<number of trusted proxy hops>. See @fonderie/rate-limit README \xA7 Deploying behind a proxy."
212
+ );
213
+ }
214
+ }
157
215
  export {
216
+ checkProxyConfig,
158
217
  defaultErrorHandler,
159
218
  notFoundMiddleware,
160
219
  requireAnyAuth,
161
220
  requireAuth,
162
221
  requireVerified,
222
+ resolveClientIp,
223
+ validate,
163
224
  withBody,
164
225
  withCors,
165
226
  withLogger
@@ -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/error-handler.ts","../../src/middlewares/require-auth.ts","../../src/middlewares/require-verified.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 { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Requires a fully-authenticated user. Rejects mfaPending tokens — those are\n// short-lived pre-auth tokens issued mid-MFA-login and must not grant access\n// to any route other than /auth/mfa/verify.\nexport const requireAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\tif (ctx.user.mfaPending) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t}\n\treturn next();\n};\n\n// Accepts both fully-authenticated and mfaPending tokens. Only for routes that\n// need to serve both contexts on the same path (e.g. POST /auth/mfa/verify\n// handles setup confirmation with a full token and TOTP login with mfaPending).\nexport const requireAnyAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport const requireVerified: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\n\tif (ctx.user.loginMethod === 'phone') {\n\t\tif (!ctx.user.phoneVerified) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'PHONE_NOT_VERIFIED',\n\t\t\t\t'Please verify your phone number',\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t}\n\n\tif (!ctx.user.emailVerifiedAt) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'EMAIL_NOT_VERIFIED', 'Please verify your email address');\n\t}\n\n\treturn next();\n};\n"],"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/error-handler.ts","../../src/middlewares/require-auth.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 { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Requires a fully-authenticated user. Rejects mfaPending tokens — those are\n// short-lived pre-auth tokens issued mid-MFA-login and must not grant access\n// to any route other than /auth/mfa/verify.\nexport const requireAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\tif (ctx.user.mfaPending) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t}\n\treturn next();\n};\n\n// Accepts both fully-authenticated and mfaPending tokens. Only for routes that\n// need to serve both contexts on the same path (e.g. POST /auth/mfa/verify\n// handles setup confirmation with a full token and TOTP login with mfaPending).\nexport const requireAnyAuth: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport const requireVerified: Middleware = async (ctx, next) => {\n\tif (!ctx.user) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t}\n\n\tif (ctx.user.loginMethod === 'phone') {\n\t\tif (!ctx.user.phoneVerified) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'PHONE_NOT_VERIFIED',\n\t\t\t\t'Please verify your phone number',\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t}\n\n\tif (!ctx.user.emailVerifiedAt) {\n\t\treturn setApiResponse(HTTP.FORBIDDEN, 'EMAIL_NOT_VERIFIED', 'Please verify your email address');\n\t}\n\n\treturn next();\n};\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\n// Route-boundary request validation — the one validation middleware every\n// package wires in front of its body-taking routes, so error shape and parse\n// semantics are identical across the whole surface.\n//\n// core stays dependency-free: this accepts anything implementing zod's\n// safeParse contract structurally (zod v3/v4 both match), without importing\n// zod. Feature packages own their schemas; see @fonderie/auth's schemas.ts\n// for the reference pattern.\n\nexport interface IRequestSchema {\n\tsafeParse(input: unknown):\n\t\t| { success: true; data: unknown }\n\t\t| { success: false; error: { issues: Array<{ path: PropertyKey[]; message: string }> } };\n}\n\nexport function validate(schema: IRequestSchema): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst result = schema.safeParse(ctx.meta['body'] ?? {});\n\t\tif (!result.success) {\n\t\t\tconst first = result.error.issues[0];\n\t\t\tconst path = first?.path.length ? `${first.path.map(String).join('.')}: ` : '';\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t`${path}${first?.message ?? 'Invalid request body'}`,\n\t\t\t);\n\t\t}\n\t\t// Parsed output replaces the raw body: trimmed, coerced, unknown keys\n\t\t// stripped — controllers read clean input.\n\t\tctx.meta['body'] = result.data;\n\t\treturn next();\n\t};\n}\n","// Client-IP resolution shared by the adapters. The web-standard Request the\n// pipeline runs on carries no socket address, so each adapter passes the\n// socket's remote address here together with the headers; this resolves the\n// effective client IP with explicit proxy trust.\n//\n// trustProxy semantics (deliberately explicit — a permissive default lets\n// any client spoof X-Forwarded-For and dodge per-IP rate limits):\n// 0 / undefined → ignore forwarding headers; the socket address is the client\n// N > 0 → the client is the Nth-from-the-right entry in\n// X-Forwarded-For (N = number of trusted proxy hops)\n//\n// ⚠️ THE PROXY FOOTGUN. With trustProxy=0 (the spoof-safe default) deployed\n// behind nginx, a Kubernetes ingress, or any L7 proxy, the socket address is\n// the PROXY's IP for every request — so every client collapses onto one\n// per-IP bucket and the limit becomes global (one attacker locks everyone\n// out). You cannot have a default that is both spoof-safe AND correct behind\n// a proxy; they contradict. So we ship spoof-safe and DETECT the mismatch:\n// checkProxyConfig() below warns once, loudly, when the deployment looks\n// proxied but trustProxy is unset. Set TRUST_PROXY=<hops> in that case.\n\nexport function resolveClientIp(\n\tsocketAddress: string | undefined,\n\theaders: Headers,\n\ttrustProxy: number = trustProxyFromEnv(),\n): string | undefined {\n\tif (trustProxy > 0) {\n\t\tconst xff = headers.get('x-forwarded-for');\n\t\tif (xff) {\n\t\t\tconst hops = xff\n\t\t\t\t.split(',')\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter(Boolean);\n\t\t\tconst candidate = hops[Math.max(0, hops.length - trustProxy)];\n\t\t\tif (candidate) return normalizeIp(candidate);\n\t\t}\n\t}\n\tcheckProxyConfig(socketAddress, headers, trustProxy);\n\treturn socketAddress ? normalizeIp(socketAddress) : undefined;\n}\n\nfunction trustProxyFromEnv(): number {\n\tconst raw = Number(process.env['TRUST_PROXY']);\n\treturn Number.isFinite(raw) && raw > 0 ? raw : 0;\n}\n\nfunction normalizeIp(ip: string): string {\n\t// ::ffff:203.0.113.7 → 203.0.113.7 ; strip port if a proxy appended one\n\tconst noV6Prefix = ip.startsWith('::ffff:') ? ip.slice(7) : ip;\n\tconst m = noV6Prefix.match(/^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/);\n\treturn m ? m[1]! : noV6Prefix;\n}\n\nfunction isPrivateOrLoopback(ip: string): boolean {\n\tconst a = normalizeIp(ip);\n\treturn (\n\t\ta === '127.0.0.1' ||\n\t\ta === '::1' ||\n\t\ta.startsWith('10.') ||\n\t\ta.startsWith('192.168.') ||\n\t\ta.startsWith('169.254.') || // link-local\n\t\ta.startsWith('fc') ||\n\t\ta.startsWith('fd') || // IPv6 ULA\n\t\t/^172\\.(1[6-9]|2\\d|3[01])\\./.test(a) // 172.16.0.0/12\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 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["// ── Stubbed until @fonderie-labs/auth ships ──────────────────────\nexport interface ITenant {\n\tid: string;\n\tslug: string;\n\tplan: string;\n}\n\nexport interface IAuthUser {\n\tid: string;\n\temail: string | null;\n\tphone: string | null;\n\tsuspended: boolean;\n\tmfaEnabled: boolean;\n\tdeletedAt: Date | null;\n\temailVerifiedAt: Date | null;\n\tloginMethod: 'email' | 'phone' | 'google'; // sourced from JWT payload\n\tphoneVerified: boolean; // per-session, sourced from JWT payload\n\tmfaPending?: boolean; // true on the short-lived pre-auth token issued during MFA login\n}\n\nexport interface IWorkspace {\n\tid: string;\n\tname: string;\n\tisPersonal?: boolean;\n}\n\n// ── Courier contract — lives in core because auth + workspaces emit\n// messages without importing @fonderie/courier.\nexport interface ICourierMessage {\n\ttype: string;\n\tlocale?: string;\n\trecipient: {\n\t\temail: string | null;\n\t\tphone: string | null;\n\t\tdeviceToken: string | null;\n\t};\n\tdata: Record<string, unknown>;\n}\n\n// ── Router interface — avoids circular dep with router.ts ────────\nexport interface IRouteMatch {\n\thandler: Middleware;\n\tparams: Record<string, string>;\n}\n\nexport interface IRouter {\n\tmatch(method: string, path: string): IRouteMatch | null;\n\tadd(method: string, path: string, handler: Middleware): void;\n}\n\n// ── Typed well-known ctx.meta keys ───────────────────────────────\nexport interface IFonderieContextMeta {\n\tparams?: Record<string, string>;\n\tbody?: unknown;\n\tworkspaceId?: string;\n\tuserId?: string;\n\tuserWorkspaceRoles?: string[];\n\tmessage?: ICourierMessage;\n\t[key: string]: unknown;\n}\n\n// ── Core types ───────────────────────────────────────────────────\nexport interface IFonderieContext {\n\trequest: Request;\n\tmeta: IFonderieContextMeta;\n\treadonly tenant: ITenant | null;\n\treadonly user: IAuthUser | null;\n\treadonly workspace: IWorkspace | null;\n\t_router: IRouter;\n}\n\nexport type Middleware = (\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n) => Promise<Response>;\n\n// ── App + module contracts ────────────────────────────────────────\nexport interface IFonderieApp {\n\tuse(middleware: Middleware): IFonderieApp;\n\tregister(module: IFonderieModule): IFonderieApp;\n\taddRoute(method: string, path: string, ...handlers: Middleware[]): void;\n\tlisten(port: number, options?: { name?: string; version?: string; env?: string }): void;\n}\n\nexport interface IFonderieModule {\n\tname: string;\n\tdeps?: string[];\n\tinstall(app: IFonderieApp): void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["// ── Stubbed until @fonderie-labs/auth ships ──────────────────────\nexport interface ITenant {\n\tid: string;\n\tslug: string;\n\tplan: string;\n}\n\nexport interface IAuthUser {\n\tid: string;\n\temail: string | null;\n\tphone: string | null;\n\tsuspended: boolean;\n\tmfaEnabled: boolean;\n\tdeletedAt: Date | null;\n\temailVerifiedAt: Date | null;\n\tloginMethod: 'email' | 'phone' | 'google'; // sourced from JWT payload\n\tphoneVerified: boolean; // per-session, sourced from JWT payload\n\tmfaPending?: boolean; // true on the short-lived pre-auth token issued during MFA login\n}\n\nexport interface IWorkspace {\n\tid: string;\n\tname: string;\n\tisPersonal?: boolean;\n}\n\n// ── Courier contract — lives in core because auth + workspaces emit\n// messages without importing @fonderie/courier.\nexport interface ICourierMessage {\n\ttype: string;\n\tlocale?: string;\n\trecipient: {\n\t\temail: string | null;\n\t\tphone: string | null;\n\t\tdeviceToken: string | null;\n\t};\n\tdata: Record<string, unknown>;\n}\n\n// ── Router interface — avoids circular dep with router.ts ────────\nexport interface IRouteMatch {\n\thandler: Middleware;\n\tparams: Record<string, string>;\n}\n\nexport interface IRouter {\n\tmatch(method: string, path: string): IRouteMatch | null;\n\tadd(method: string, path: string, handler: Middleware): void;\n}\n\n// ── Typed well-known ctx.meta keys ───────────────────────────────\nexport interface IFonderieContextMeta {\n\tparams?: Record<string, string>;\n\tbody?: unknown;\n\t// Trust-proxy-resolved client IP, populated by the adapters (see\n\t// resolveClientIp in @fonderie/core/middlewares). Consumed by\n\t// @fonderie/rate-limit's byIp() keying.\n\tclientIp?: string;\n\tworkspaceId?: string;\n\tuserId?: string;\n\tuserWorkspaceRoles?: string[];\n\tmessage?: ICourierMessage;\n\t[key: string]: unknown;\n}\n\n// ── Core types ───────────────────────────────────────────────────\nexport interface IFonderieContext {\n\trequest: Request;\n\tmeta: IFonderieContextMeta;\n\treadonly tenant: ITenant | null;\n\treadonly user: IAuthUser | null;\n\treadonly workspace: IWorkspace | null;\n\t_router: IRouter;\n}\n\nexport type Middleware = (\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n) => Promise<Response>;\n\n// ── App + module contracts ────────────────────────────────────────\nexport interface IFonderieApp {\n\tuse(middleware: Middleware): IFonderieApp;\n\tregister(module: IFonderieModule): IFonderieApp;\n\taddRoute(method: string, path: string, ...handlers: Middleware[]): void;\n\tlisten(port: number, options?: { name?: string; version?: string; env?: string }): void;\n}\n\nexport interface IFonderieModule {\n\tname: string;\n\tdeps?: string[];\n\tinstall(app: IFonderieApp): void | Promise<void>;\n}\n\n// ── Cross-module vocabulary ───────────────────────────────────────\n// Lives in core (not permissions) so packages that only peer on core —\n// the adapters — can re-export it without loading optional peers.\nexport type Operation = 'create' | 'read' | 'update' | 'delete';\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
package/dist/types.d.cts CHANGED
@@ -41,6 +41,7 @@ interface IRouter {
41
41
  interface IFonderieContextMeta {
42
42
  params?: Record<string, string>;
43
43
  body?: unknown;
44
+ clientIp?: string;
44
45
  workspaceId?: string;
45
46
  userId?: string;
46
47
  userWorkspaceRoles?: string[];
@@ -71,5 +72,6 @@ interface IFonderieModule {
71
72
  deps?: string[];
72
73
  install(app: IFonderieApp): void | Promise<void>;
73
74
  }
75
+ type Operation = 'create' | 'read' | 'update' | 'delete';
74
76
 
75
- export type { IAuthUser, ICourierMessage, IFonderieApp, IFonderieContext, IFonderieContextMeta, IFonderieModule, IRouteMatch, IRouter, ITenant, IWorkspace, Middleware };
77
+ export type { IAuthUser, ICourierMessage, IFonderieApp, IFonderieContext, IFonderieContextMeta, IFonderieModule, IRouteMatch, IRouter, ITenant, IWorkspace, Middleware, Operation };
package/dist/types.d.ts CHANGED
@@ -41,6 +41,7 @@ interface IRouter {
41
41
  interface IFonderieContextMeta {
42
42
  params?: Record<string, string>;
43
43
  body?: unknown;
44
+ clientIp?: string;
44
45
  workspaceId?: string;
45
46
  userId?: string;
46
47
  userWorkspaceRoles?: string[];
@@ -71,5 +72,6 @@ interface IFonderieModule {
71
72
  deps?: string[];
72
73
  install(app: IFonderieApp): void | Promise<void>;
73
74
  }
75
+ type Operation = 'create' | 'read' | 'update' | 'delete';
74
76
 
75
- export type { IAuthUser, ICourierMessage, IFonderieApp, IFonderieContext, IFonderieContextMeta, IFonderieModule, IRouteMatch, IRouter, ITenant, IWorkspace, Middleware };
77
+ export type { IAuthUser, ICourierMessage, IFonderieApp, IFonderieContext, IFonderieContextMeta, IFonderieModule, IRouteMatch, IRouter, ITenant, IWorkspace, Middleware, Operation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonderie/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Framework core — request router, middleware pipeline, module system, and shared context types. Every other @fonderie-js package depends on this.",
5
5
  "keywords": [
6
6
  "fonderie-js",