@fonderie/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @fonderie/core
2
2
 
3
- The framework core every other `@fonderie-js` package builds on: a web-standard
3
+ The framework core every other `@fonderiejs` package builds on: a web-standard
4
4
  request router, a composable middleware pipeline, a module system, and the
5
5
  shared `IFonderieContext` that flows through all of it.
6
6
 
@@ -35,10 +35,10 @@ for untrusted input.
35
35
 
36
36
  ## The module system
37
37
 
38
- Feature packages — [auth](https://github.com/fonderie-js/sdk/tree/main/packages/auth),
39
- [workspaces](https://github.com/fonderie-js/sdk/tree/main/packages/workspaces),
40
- [billing](https://github.com/fonderie-js/sdk/tree/main/packages/billing),
41
- [courier](https://github.com/fonderie-js/sdk/tree/main/packages/courier), and
38
+ Feature packages — [auth](https://github.com/fonderiejs/sdk/tree/main/packages/auth),
39
+ [workspaces](https://github.com/fonderiejs/sdk/tree/main/packages/workspaces),
40
+ [billing](https://github.com/fonderiejs/sdk/tree/main/packages/billing),
41
+ [courier](https://github.com/fonderiejs/sdk/tree/main/packages/courier), and
42
42
  friends — implement `IFonderieModule` and register their routes, migrations,
43
43
  and event handlers against this core. Pick the modules your product needs;
44
44
  skip the rest.
@@ -48,7 +48,7 @@ skip the rest.
48
48
  You've shipped this plumbing before — auth, teams, billing, messaging —
49
49
  and the next project will ask for it again. Fonderie packages it once:
50
50
  plain TypeScript modules for
51
- [`@fonderie/core`](https://github.com/fonderie-js/sdk/tree/main/packages/core),
51
+ [`@fonderie/core`](https://github.com/fonderiejs/sdk/tree/main/packages/core),
52
52
  PostgreSQL-backed, self-hosted, MIT. No external control plane, no
53
53
  per-seat anything. Register the modules you need; skip the ones you don't.
54
54
 
@@ -57,7 +57,7 @@ module lifecycle every other brick builds against. It depends on nothing;
57
57
  everything depends on it.
58
58
 
59
59
  Browse the whole set at
60
- [fonderie-js/sdk](https://github.com/fonderie-js/sdk) · follow
60
+ [fonderiejs/sdk](https://github.com/fonderiejs/sdk) · follow
61
61
  [@fonderiejs](https://x.com/fonderiejs)
62
62
 
63
63
  ## License
@@ -58,6 +58,7 @@ interface IFonderieApp {
58
58
  }): void;
59
59
  boot(): Promise<IFonderieApp>;
60
60
  checkProductionReadiness(): IReadinessReport;
61
+ securityReport(): ISecurityReport;
61
62
  }
62
63
 
63
64
  interface IFonderieModule {
@@ -108,12 +109,21 @@ interface IReadinessReport {
108
109
  problems: IReadinessProblem[];
109
110
  }
110
111
 
112
+ interface ISecurityReport {
113
+ generatedAt: string;
114
+ env: string;
115
+ registeredModules: string[];
116
+ readiness: IReadinessReport;
117
+ }
118
+
111
119
  const OPERATIONS: { readonly CREATE: "create"; readonly READ: "read"; readonly UPDATE: "update"; readonly DELETE: "delete"; }
112
120
 
113
121
  new FonderieApp(config: FonderieConfig): FonderieApp
122
+ .metrics: MetricsRegistry
114
123
  .listen(port: number, options?: { name?: string; version?: string; env?: string; quiet?: boolean; }): Server<typeof IncomingMessage, typeof ServerResponse>
115
124
  .register(module: IFonderieModule): FonderieApp
116
125
  .checkProductionReadiness(): IReadinessReport
126
+ .securityReport(): ISecurityReport
117
127
  .boot(): Promise<FonderieApp>
118
128
  .buildContext(request: Request): Promise<IFonderieContext>
119
129
  .use(middleware: Middleware): FonderieApp
@@ -140,6 +150,10 @@ interface FonderieConfig {
140
150
  smtp?: ISMTPConfig;
141
151
  provider: 'resend' | 'ses' | 'smtp';
142
152
  };
153
+ skipProductionReadinessGate?: boolean;
154
+ healthChecks?: boolean;
155
+ readyProbe?: () => boolean | Promise<boolean>;
156
+ metrics?: boolean;
143
157
  onError?: (err: unknown) => Response;
144
158
  onResponse?: (body: unknown, info: {
145
159
  status: number;
@@ -157,6 +171,18 @@ function numberOrZero(value: unknown): number
157
171
 
158
172
  function dateOrEmpty(value: unknown): string
159
173
 
174
+ function constantTimeEqual(a: string | Buffer<ArrayBufferLike>, b: string | Buffer<ArrayBufferLike>): boolean
175
+
176
+ const MIN_SECRET_LENGTH: 32
177
+
178
+ const PLACEHOLDER_SECRET: RegExp
179
+
180
+ function secretStrengthProblem(secret: string): "too-short" | "placeholder" | null
181
+
182
+ function encodeKeysetCursor(createdAt: string, id: string): string
183
+
184
+ function decodeKeysetCursor(cursor: string): { createdAt: string; id: string; } | null
185
+
160
186
  interface IApiError {
161
187
  reason: string;
162
188
  explanation: string;
@@ -168,4 +194,10 @@ type HttpStatus = (typeof HTTP)[keyof typeof HTTP];
168
194
  const HTTP: { readonly OK: 200; readonly CREATED: 201; readonly ACCEPTED: 202; readonly NO_CONTENT: 204; readonly BAD_REQUEST: 400; readonly UNAUTHORIZED: 401; readonly PAYMENT_REQUIRED: 402; readonly FORBIDDEN: 403; readonly NOT_FOUND: 404; readonly CONFLICT: 409; readonly GONE: 410; readonly UNPROCESSABLE: 422; readonly TOO_MANY_REQUESTS: 429; readonly SERVER_ERROR: 500; readonly NOT_IMPLEMENTED: 501; readonly BAD_GATEWAY: 502; readonly SERVICE_UNAVAILABLE: 503; }
169
195
 
170
196
  function setApiResponse<T>(status: number, reason: string, explanation: string, payload?: T | undefined): Response
197
+
198
+ new MetricsRegistry(): MetricsRegistry
199
+ .inc(name: string, labels?: Record<string, string>, by?: number): void
200
+ .render(): string
201
+
202
+ function withMetrics(registry: MetricsRegistry): Middleware
171
203
  ```
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts"],"sourcesContent":["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\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAoDO,SAAS,aAAa,QAAwC;AACpE,SAAO;AACR;","names":[]}
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\t// Fail-closed production boot gate. In production, `boot()` refuses to start\n\t// when any registered module reports an `error`-severity readiness problem\n\t// (e.g. a weak signing secret, a missing at-rest encryption key). Set this to\n\t// true only to deliberately override the gate — not recommended. Outside\n\t// production the gate never runs.\n\tskipProductionReadinessGate?: boolean;\n\n\t// Built-in health endpoints, registered on boot (unprefixed by basePath):\n\t// GET /healthz — liveness, always 200 while the process is up\n\t// GET /readyz — readiness, 200 when checkProductionReadiness() is ok AND\n\t// readyProbe() (if provided) resolves truthy, else 503\n\t// Set false to disable. Point your platform's probes at these.\n\thealthChecks?: boolean;\n\t// Optional dependency probe for /readyz — e.g. `() => store.testConnection()`.\n\t// Throwing or returning false makes /readyz report 503.\n\treadyProbe?: () => boolean | Promise<boolean>;\n\n\t// Enable Prometheus-format metrics: counts requests by status class and\n\t// serves them at GET /metrics (unprefixed). Off by default. Access to\n\t// /metrics should be restricted at your ingress/scraper.\n\tmetrics?: boolean;\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA0EO,SAAS,aAAa,QAAwC;AACpE,SAAO;AACR;","names":[]}
package/dist/config.d.cts CHANGED
@@ -28,6 +28,10 @@ interface FonderieConfig {
28
28
  smtp?: ISMTPConfig;
29
29
  provider: 'resend' | 'ses' | 'smtp';
30
30
  };
31
+ skipProductionReadinessGate?: boolean;
32
+ healthChecks?: boolean;
33
+ readyProbe?: () => boolean | Promise<boolean>;
34
+ metrics?: boolean;
31
35
  onError?: (err: unknown) => Response;
32
36
  onResponse?: (body: unknown, info: {
33
37
  status: number;
package/dist/config.d.ts CHANGED
@@ -28,6 +28,10 @@ interface FonderieConfig {
28
28
  smtp?: ISMTPConfig;
29
29
  provider: 'resend' | 'ses' | 'smtp';
30
30
  };
31
+ skipProductionReadinessGate?: boolean;
32
+ healthChecks?: boolean;
33
+ readyProbe?: () => boolean | Promise<boolean>;
34
+ metrics?: boolean;
31
35
  onError?: (err: unknown) => Response;
32
36
  onResponse?: (body: unknown, info: {
33
37
  status: number;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts"],"sourcesContent":["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\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n"],"mappings":";AAoDO,SAAS,aAAa,QAAwC;AACpE,SAAO;AACR;","names":[]}
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\t// Fail-closed production boot gate. In production, `boot()` refuses to start\n\t// when any registered module reports an `error`-severity readiness problem\n\t// (e.g. a weak signing secret, a missing at-rest encryption key). Set this to\n\t// true only to deliberately override the gate — not recommended. Outside\n\t// production the gate never runs.\n\tskipProductionReadinessGate?: boolean;\n\n\t// Built-in health endpoints, registered on boot (unprefixed by basePath):\n\t// GET /healthz — liveness, always 200 while the process is up\n\t// GET /readyz — readiness, 200 when checkProductionReadiness() is ok AND\n\t// readyProbe() (if provided) resolves truthy, else 503\n\t// Set false to disable. Point your platform's probes at these.\n\thealthChecks?: boolean;\n\t// Optional dependency probe for /readyz — e.g. `() => store.testConnection()`.\n\t// Throwing or returning false makes /readyz report 503.\n\treadyProbe?: () => boolean | Promise<boolean>;\n\n\t// Enable Prometheus-format metrics: counts requests by status class and\n\t// serves them at GET /metrics (unprefixed). Off by default. Access to\n\t// /metrics should be restricted at your ingress/scraper.\n\tmetrics?: boolean;\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n"],"mappings":";AA0EO,SAAS,aAAa,QAAwC;AACpE,SAAO;AACR;","names":[]}
package/dist/index.cjs CHANGED
@@ -22,15 +22,23 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  FonderieApp: () => FonderieApp,
24
24
  HTTP: () => HTTP,
25
+ MIN_SECRET_LENGTH: () => MIN_SECRET_LENGTH,
26
+ MetricsRegistry: () => MetricsRegistry,
25
27
  OPERATIONS: () => OPERATIONS,
28
+ PLACEHOLDER_SECRET: () => PLACEHOLDER_SECRET,
26
29
  arrayOrEmpty: () => arrayOrEmpty,
27
30
  booleanOrFalse: () => booleanOrFalse,
28
31
  compose: () => compose,
32
+ constantTimeEqual: () => constantTimeEqual,
29
33
  dateOrEmpty: () => dateOrEmpty,
34
+ decodeKeysetCursor: () => decodeKeysetCursor,
30
35
  defineConfig: () => defineConfig,
36
+ encodeKeysetCursor: () => encodeKeysetCursor,
31
37
  numberOrZero: () => numberOrZero,
38
+ secretStrengthProblem: () => secretStrengthProblem,
32
39
  setApiResponse: () => setApiResponse,
33
- stringOrEmpty: () => stringOrEmpty
40
+ stringOrEmpty: () => stringOrEmpty,
41
+ withMetrics: () => withMetrics
34
42
  });
35
43
  module.exports = __toCommonJS(index_exports);
36
44
 
@@ -166,6 +174,39 @@ var withBody = async (ctx, next) => {
166
174
  return next();
167
175
  };
168
176
 
177
+ // src/middlewares/security-headers.ts
178
+ function withSecurityHeaders(options = {}) {
179
+ const {
180
+ hstsMaxAge = 60 * 60 * 24 * 180,
181
+ hstsIncludeSubDomains = false,
182
+ hstsPreload = false
183
+ } = options;
184
+ let hsts = "";
185
+ if (hstsMaxAge > 0) {
186
+ hsts = `max-age=${hstsMaxAge}`;
187
+ if (hstsIncludeSubDomains || hstsPreload) hsts += "; includeSubDomains";
188
+ if (hstsPreload) hsts += "; preload";
189
+ }
190
+ return async (ctx, next) => {
191
+ const response = await next();
192
+ const patched = new Headers(response.headers);
193
+ patched.set("X-Content-Type-Options", "nosniff");
194
+ if (hsts && isHttps(ctx.request)) {
195
+ patched.set("Strict-Transport-Security", hsts);
196
+ }
197
+ return new Response(response.body, {
198
+ headers: patched,
199
+ status: response.status,
200
+ statusText: response.statusText
201
+ });
202
+ };
203
+ }
204
+ function isHttps(request) {
205
+ if (request.url.startsWith("https:")) return true;
206
+ const proto = request.headers.get("x-forwarded-proto");
207
+ return proto?.split(",")[0]?.trim() === "https";
208
+ }
209
+
169
210
  // src/middlewares/error-handler.ts
170
211
  function defaultErrorHandler(err) {
171
212
  const dev = process.env["NODE_ENV"] !== "production";
@@ -181,6 +222,51 @@ function defaultErrorHandler(err) {
181
222
  return setApiResponse(HTTP.SERVER_ERROR, "SERVER_ERROR", "Internal server error");
182
223
  }
183
224
 
225
+ // src/crypto.ts
226
+ var import_node_crypto = require("crypto");
227
+ function constantTimeEqual(a, b) {
228
+ const bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);
229
+ const bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);
230
+ if (bufA.length !== bufB.length) return false;
231
+ return (0, import_node_crypto.timingSafeEqual)(bufA, bufB);
232
+ }
233
+
234
+ // src/secret-strength.ts
235
+ var MIN_SECRET_LENGTH = 32;
236
+ var PLACEHOLDER_SECRET = /dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;
237
+ function secretStrengthProblem(secret) {
238
+ if (secret.length < MIN_SECRET_LENGTH) return "too-short";
239
+ if (PLACEHOLDER_SECRET.test(secret)) return "placeholder";
240
+ return null;
241
+ }
242
+
243
+ // src/metrics.ts
244
+ var MetricsRegistry = class {
245
+ counters = /* @__PURE__ */ new Map();
246
+ inc(name, labels = {}, by = 1) {
247
+ const key = seriesKey(name, labels);
248
+ this.counters.set(key, (this.counters.get(key) ?? 0) + by);
249
+ }
250
+ // Prometheus text exposition format.
251
+ render() {
252
+ const lines = [];
253
+ for (const [key, value] of this.counters) lines.push(`${key} ${value}`);
254
+ return lines.join("\n") + (lines.length ? "\n" : "");
255
+ }
256
+ };
257
+ function seriesKey(name, labels) {
258
+ const parts = Object.entries(labels).map(([k, v]) => `${k}="${String(v).replace(/"/g, "")}"`);
259
+ return parts.length ? `${name}{${parts.join(",")}}` : name;
260
+ }
261
+ function withMetrics(registry) {
262
+ return async (ctx, next) => {
263
+ const response = await next();
264
+ const cls = `${Math.floor(response.status / 100)}xx`;
265
+ registry.inc("http_requests_total", { status_class: cls });
266
+ return response;
267
+ };
268
+ }
269
+
184
270
  // src/app.ts
185
271
  var FonderieApp = class {
186
272
  config;
@@ -188,10 +274,12 @@ var FonderieApp = class {
188
274
  router = new Router();
189
275
  middlewares = [];
190
276
  modules = /* @__PURE__ */ new Map();
277
+ metrics = new MetricsRegistry();
191
278
  constructor(config) {
192
279
  this.config = config;
193
280
  this.prefix = (config.basePath ?? "").replace(/\/$/, "");
194
- this.middlewares = [withBody];
281
+ this.middlewares = [withBody, withSecurityHeaders()];
282
+ if (config.metrics) this.middlewares.push(withMetrics(this.metrics));
195
283
  }
196
284
  listen(port, options = {}) {
197
285
  const {
@@ -266,12 +354,77 @@ var FonderieApp = class {
266
354
  }
267
355
  return { ok: !problems.some((p) => p.severity === "error"), problems };
268
356
  }
357
+ // A point-in-time control-posture snapshot for SOC 2 evidence: which modules
358
+ // are registered and the current readiness report. Serialise to a file/log
359
+ // (e.g. on a schedule) as an audit artifact.
360
+ securityReport() {
361
+ return {
362
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
363
+ env: process.env["NODE_ENV"] ?? "development",
364
+ registeredModules: [...this.modules.keys()].sort(),
365
+ readiness: this.checkProductionReadiness()
366
+ };
367
+ }
269
368
  async boot() {
369
+ this.enforceProductionReadiness();
270
370
  for (const module2 of topoSort([...this.modules.values()])) {
271
371
  await module2.install(this);
272
372
  }
373
+ this.registerHealthRoutes();
273
374
  return this;
274
375
  }
376
+ // Liveness (/healthz) and readiness (/readyz) probes. Registered unprefixed so
377
+ // they sit at a stable path regardless of basePath. Enabled unless disabled.
378
+ registerHealthRoutes() {
379
+ if (this.config.healthChecks === false) return;
380
+ this.router.add("GET", "/healthz", compose([async () => Response.json({ status: "ok" })]));
381
+ if (this.config.metrics) {
382
+ this.router.add(
383
+ "GET",
384
+ "/metrics",
385
+ compose([
386
+ async () => new Response(this.metrics.render(), {
387
+ status: 200,
388
+ headers: { "content-type": "text/plain; version=0.0.4" }
389
+ })
390
+ ])
391
+ );
392
+ }
393
+ this.router.add(
394
+ "GET",
395
+ "/readyz",
396
+ compose([
397
+ async () => {
398
+ const report = this.checkProductionReadiness();
399
+ let dependencies = true;
400
+ if (this.config.readyProbe) {
401
+ try {
402
+ dependencies = Boolean(await this.config.readyProbe());
403
+ } catch {
404
+ dependencies = false;
405
+ }
406
+ }
407
+ const ready = report.ok && dependencies;
408
+ return Response.json(
409
+ { status: ready ? "ready" : "not_ready", dependencies, problems: report.problems },
410
+ { status: ready ? 200 : 503 }
411
+ );
412
+ }
413
+ ])
414
+ );
415
+ }
416
+ // Throws in production when `checkProductionReadiness()` reports any
417
+ // error-severity problem, unless explicitly overridden. No-op otherwise.
418
+ enforceProductionReadiness() {
419
+ if (process.env["NODE_ENV"] !== "production") return;
420
+ if (this.config.skipProductionReadinessGate) return;
421
+ const { ok, problems } = this.checkProductionReadiness();
422
+ if (ok) return;
423
+ const errors = problems.filter((p) => p.severity === "error");
424
+ throw new Error(
425
+ `[fonderie] refusing to boot in production \u2014 ${errors.length} readiness error(s): ${errors.map((e) => `${e.module}: ${e.message}`).join("; ")}. Fix them, or set skipProductionReadinessGate: true to override (not recommended).`
426
+ );
427
+ }
275
428
  // Runs global middleware only (no routing, no 404).
276
429
  // Adapter packages call this to populate user/workspace/meta into their
277
430
  // native context before handing off to user-defined route handlers.
@@ -404,18 +557,46 @@ function dateOrEmpty(value) {
404
557
  if (value instanceof Date) return value.toISOString();
405
558
  return "";
406
559
  }
560
+
561
+ // src/keyset-cursor.ts
562
+ function encodeKeysetCursor(createdAt, id) {
563
+ return Buffer.from(JSON.stringify([createdAt, id])).toString("base64url");
564
+ }
565
+ var CURSOR_TS_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[T ]([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d{1,6})?(Z|[+-]\d{2}(:?\d{2})?)?$/;
566
+ var CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
567
+ function decodeKeysetCursor(cursor) {
568
+ if (cursor.length > 256) return null;
569
+ try {
570
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
571
+ if (!Array.isArray(parsed) || typeof parsed[0] !== "string" || typeof parsed[1] !== "string") {
572
+ return null;
573
+ }
574
+ if (!CURSOR_TS_RE.test(parsed[0]) || !CURSOR_ID_RE.test(parsed[1])) return null;
575
+ return { createdAt: parsed[0], id: parsed[1] };
576
+ } catch {
577
+ return null;
578
+ }
579
+ }
407
580
  // Annotate the CommonJS export names for ESM import in node:
408
581
  0 && (module.exports = {
409
582
  FonderieApp,
410
583
  HTTP,
584
+ MIN_SECRET_LENGTH,
585
+ MetricsRegistry,
411
586
  OPERATIONS,
587
+ PLACEHOLDER_SECRET,
412
588
  arrayOrEmpty,
413
589
  booleanOrFalse,
414
590
  compose,
591
+ constantTimeEqual,
415
592
  dateOrEmpty,
593
+ decodeKeysetCursor,
416
594
  defineConfig,
595
+ encodeKeysetCursor,
417
596
  numberOrZero,
597
+ secretStrengthProblem,
418
598
  setApiResponse,
419
- stringOrEmpty
599
+ stringOrEmpty,
600
+ withMetrics
420
601
  });
421
602
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/app.ts","../src/router.ts","../src/compose.ts","../src/response.ts","../src/middlewares/not-found.ts","../src/middlewares/body-parser.ts","../src/middlewares/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\tIReadinessProblem,\n\tIReadinessReport,\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, type Server } from 'node:http';\n\nimport type {\n\tMiddleware,\n\tIFonderieApp,\n\tIFonderieContext,\n\tIFonderieModule,\n\tIReadinessProblem,\n\tIReadinessReport,\n} from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { withBody } from './middlewares/body-parser';\n\nexport class FonderieApp implements IFonderieApp {\n\tprivate config: FonderieConfig;\n\tprivate prefix: string;\n\tprivate router: Router = new Router();\n\tprivate middlewares: Middleware[] = [];\n\tprivate modules: Map<string, IFonderieModule> = new Map();\n\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\tquiet?: boolean; // suppress the startup banner (tests, quiet deploys)\n\t\t} = {},\n\t): Server {\n\t\tconst {\n\t\t\tname = 'Fonderie',\n\t\t\tversion = '0.0.1',\n\t\t\tenv = process.env['NODE_ENV'] ?? 'development',\n\t\t\tquiet = false,\n\t\t} = options;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\tconst host = req.headers.host ?? 'localhost';\n\t\t\tconst url = `http://${host}${req.url ?? '/'}`;\n\t\t\tconst headers = new Headers();\n\n\t\t\tfor (const [key, value] of Object.entries(req.headers)) {\n\t\t\t\tif (!value) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\tfor (const v of value) headers.append(key, v);\n\t\t\t\t} else {\n\t\t\t\t\theaders.set(key, value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\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\t// Set-Cookie must be forwarded as a LIST — forEach + setHeader would\n\t\t\t// overwrite all but the last cookie. getSetCookie() returns each intact.\n\t\t\tconst setCookies = response.headers.getSetCookie?.() ?? [];\n\t\t\tif (setCookies.length) res.setHeader('Set-Cookie', setCookies);\n\t\t\tresponse.headers.forEach((v, k) => {\n\t\t\t\tif (k.toLowerCase() !== 'set-cookie') res.setHeader(k, v);\n\t\t\t});\n\t\t\tres.end(Buffer.from(await response.arrayBuffer()));\n\t\t}).listen(port, () => {\n\t\t\tif (quiet) return;\n\t\t\tconst ip = getLocalIPv4();\n\t\t\tconst mode = env.includes('dev') ? 'development' : 'production';\n\n\t\t\tconsole.log(\n\t\t\t\t`\\n ƒ ${name} v${version} ${mode}\\n` +\n\t\t\t\t\t`\\n Local http://localhost:${port}` +\n\t\t\t\t\t`\\n Network http://${ip}:${port}\\n`,\n\t\t\t);\n\t\t});\n\t\treturn server;\n\t}\n\n\t// ─── Module registration ───────────────────────────────\n\n\tregister(module: IFonderieModule): this {\n\t\tthis.modules.set(module.name, module);\n\t\treturn this;\n\t}\n\n\t// Aggregate every registered module's self-reported readiness problems into\n\t// one report. Call it before boot to gate a deploy, or from a readiness\n\t// endpoint. `ok` is false when any module reports an `error`-severity problem\n\t// (e.g. a weak jwtSecret). Modules opt in via `checkReadiness`.\n\tcheckProductionReadiness(): IReadinessReport {\n\t\tconst problems: IReadinessProblem[] = [];\n\t\tfor (const module of this.modules.values()) {\n\t\t\tif (module.checkReadiness) problems.push(...module.checkReadiness());\n\t\t}\n\t\treturn { ok: !problems.some((p) => p.severity === 'error'), problems };\n\t}\n\n\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};\n\t\tawait compose(this.middlewares)(ctx, async () => new Response());\n\t\tdelete ctx.meta['_buildContext'];\n\t\treturn ctx;\n\t}\n\n\t// ─── Middleware ────────────────────────────────────────\n\n\tuse(middleware: Middleware): this {\n\t\tthis.middlewares.push(middleware);\n\t\treturn this;\n\t}\n\n\t// Modules call this to register their routes\n\taddRoute(method: string, path: string, ...handlers: Middleware[]): void {\n\t\tthis.router.add(method, this.prefix + path, compose(handlers));\n\t}\n\n\t// ─── The core handler ──────────────────────────────────\n\t// This is the ONE thing every adapter calls.\n\t// Takes a Web Standard Request, returns a Web Standard Response.\n\n\tasync handle(request: Request): Promise<Response> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: {},\n\t\t};\n\n\t\t// Build the pipeline: global middleware → router → 404\n\t\tconst pipeline = compose([\n\t\t\t...this.middlewares,\n\t\t\trouterMiddleware(this.router),\n\t\t\tnotFoundMiddleware(),\n\t\t]);\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\tresponse = this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t\treturn this.config.onResponse ? this.transformResponse(response, request) : response;\n\t}\n\n\t// Apply config.onResponse to a JSON response body, preserving status, headers,\n\t// and cookies. Non-JSON responses and hooks that return `undefined` pass through.\n\tprivate async transformResponse(response: Response, request: Request): Promise<Response> {\n\t\tconst contentType = response.headers.get('content-type') ?? '';\n\t\tif (!contentType.includes('application/json')) return response;\n\t\tlet body: unknown;\n\t\ttry {\n\t\t\tbody = await response.clone().json();\n\t\t} catch {\n\t\t\treturn response; // not valid JSON after all — leave untouched\n\t\t}\n\t\tconst transformed = this.config.onResponse!(body, { status: response.status, request });\n\t\tif (transformed === undefined) return response;\n\t\t// Preserve headers/cookies; drop content-length (the new body sets its own).\n\t\tconst headers = new Headers(response.headers);\n\t\theaders.delete('content-length');\n\t\theaders.delete('content-type');\n\t\treturn Response.json(transformed, { status: response.status, headers });\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import { 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\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n","export function stringOrEmpty(value: unknown): string {\n\treturn typeof value === 'string' ? value : '';\n}\n\nexport function booleanOrFalse(value: unknown): boolean {\n\tif (typeof value === 'boolean') return value;\n\tif (value === 'true' || value === '1') return true;\n\treturn false;\n}\n\nexport function arrayOrEmpty<T>(value: unknown): T[] {\n\treturn Array.isArray(value) ? (value as T[]) : [];\n}\n\nexport function numberOrZero(value: unknown): number {\n\tconst n = Number(value);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\nexport function dateOrEmpty(value: unknown): string {\n\tif (typeof value === 'string') return value;\n\tif (value instanceof Date) return value.toISOString();\n\treturn '';\n}\n"],"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/security-headers.ts","../src/middlewares/error-handler.ts","../src/crypto.ts","../src/secret-strength.ts","../src/metrics.ts","../src/config.ts","../src/parser.ts","../src/keyset-cursor.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport type {\n\tITenant,\n\tIRouter,\n\tOperation,\n\tIAuthUser,\n\tMiddleware,\n\tIWorkspace,\n\tIRouteMatch,\n\tIFonderieApp,\n\tIFonderieModule,\n\tIFonderieContext,\n\tICourierMessage,\n\tIFonderieContextMeta,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\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';\nexport { constantTimeEqual } from './crypto';\nexport { MIN_SECRET_LENGTH, PLACEHOLDER_SECRET, secretStrengthProblem } from './secret-strength';\nexport { encodeKeysetCursor, decodeKeysetCursor } from './keyset-cursor';\n\n// Response helpers\nexport type { IApiError, HttpStatus } from './response';\nexport { HTTP, setApiResponse } from './response';\n\n// NOT exported: adapters/, router internals, error-handler, not-found\n// Those are consumed by FonderieApp, never by users directly\n\nexport { MetricsRegistry, withMetrics } from './metrics';\n","import type { Operation } from './types';\n\n// CRUD operation names shared across modules. Canonical home is core so the\n// adapters (which peer only on core) can re-export OPERATIONS without loading\n// @fonderie/permissions; permissions re-exports it for backward compatibility.\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n","import { networkInterfaces } from 'node:os';\nimport { createServer, type Server } from 'node:http';\n\nimport type {\n\tMiddleware,\n\tIFonderieApp,\n\tIFonderieContext,\n\tIFonderieModule,\n\tIReadinessProblem,\n\tIReadinessReport,\n\tISecurityReport,\n} from './types';\nimport type { FonderieConfig } from './config';\nimport { Router, routerMiddleware } from './router';\nimport { compose } from './compose';\nimport { notFoundMiddleware, defaultErrorHandler } from './middlewares';\nimport { withBody } from './middlewares/body-parser';\nimport { withSecurityHeaders } from './middlewares/security-headers';\nimport { MetricsRegistry, withMetrics } from './metrics';\n\nexport class FonderieApp implements IFonderieApp {\n\tprivate config: FonderieConfig;\n\tprivate prefix: string;\n\tprivate router: Router = new Router();\n\tprivate middlewares: Middleware[] = [];\n\tprivate modules: Map<string, IFonderieModule> = new Map();\n\treadonly metrics = new MetricsRegistry();\n\n\tconstructor(config: FonderieConfig) {\n\t\tthis.config = config;\n\t\tthis.prefix = (config.basePath ?? '').replace(/\\/$/, '');\n\t\t// Body parsing first, then baseline security headers (nosniff always; HSTS\n\t\t// over HTTPS). Apps can layer more via `.use()`.\n\t\tthis.middlewares = [withBody, withSecurityHeaders()];\n\t\tif (config.metrics) this.middlewares.push(withMetrics(this.metrics));\n\t}\n\n\tlisten(\n\t\tport: number,\n\t\toptions: {\n\t\t\tname?: string;\n\t\t\tversion?: string;\n\t\t\tenv?: string;\n\t\t\tquiet?: boolean; // suppress the startup banner (tests, quiet deploys)\n\t\t} = {},\n\t): Server {\n\t\tconst {\n\t\t\tname = 'Fonderie',\n\t\t\tversion = '0.0.1',\n\t\t\tenv = process.env['NODE_ENV'] ?? 'development',\n\t\t\tquiet = false,\n\t\t} = options;\n\n\t\tconst server = createServer(async (req, res) => {\n\t\t\tconst host = req.headers.host ?? 'localhost';\n\t\t\tconst url = `http://${host}${req.url ?? '/'}`;\n\t\t\tconst headers = new Headers();\n\n\t\t\tfor (const [key, value] of Object.entries(req.headers)) {\n\t\t\t\tif (!value) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\tfor (const v of value) headers.append(key, v);\n\t\t\t\t} else {\n\t\t\t\t\theaders.set(key, value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\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\t// Set-Cookie must be forwarded as a LIST — forEach + setHeader would\n\t\t\t// overwrite all but the last cookie. getSetCookie() returns each intact.\n\t\t\tconst setCookies = response.headers.getSetCookie?.() ?? [];\n\t\t\tif (setCookies.length) res.setHeader('Set-Cookie', setCookies);\n\t\t\tresponse.headers.forEach((v, k) => {\n\t\t\t\tif (k.toLowerCase() !== 'set-cookie') res.setHeader(k, v);\n\t\t\t});\n\t\t\tres.end(Buffer.from(await response.arrayBuffer()));\n\t\t}).listen(port, () => {\n\t\t\tif (quiet) return;\n\t\t\tconst ip = getLocalIPv4();\n\t\t\tconst mode = env.includes('dev') ? 'development' : 'production';\n\n\t\t\tconsole.log(\n\t\t\t\t`\\n ƒ ${name} v${version} ${mode}\\n` +\n\t\t\t\t\t`\\n Local http://localhost:${port}` +\n\t\t\t\t\t`\\n Network http://${ip}:${port}\\n`,\n\t\t\t);\n\t\t});\n\t\treturn server;\n\t}\n\n\t// ─── Module registration ───────────────────────────────\n\n\tregister(module: IFonderieModule): this {\n\t\tthis.modules.set(module.name, module);\n\t\treturn this;\n\t}\n\n\t// Aggregate every registered module's self-reported readiness problems into\n\t// one report. Call it before boot to gate a deploy, or from a readiness\n\t// endpoint. `ok` is false when any module reports an `error`-severity problem\n\t// (e.g. a weak jwtSecret). Modules opt in via `checkReadiness`.\n\tcheckProductionReadiness(): IReadinessReport {\n\t\tconst problems: IReadinessProblem[] = [];\n\t\tfor (const module of this.modules.values()) {\n\t\t\tif (module.checkReadiness) problems.push(...module.checkReadiness());\n\t\t}\n\t\treturn { ok: !problems.some((p) => p.severity === 'error'), problems };\n\t}\n\n\t// A point-in-time control-posture snapshot for SOC 2 evidence: which modules\n\t// are registered and the current readiness report. Serialise to a file/log\n\t// (e.g. on a schedule) as an audit artifact.\n\tsecurityReport(): ISecurityReport {\n\t\treturn {\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t\tenv: process.env['NODE_ENV'] ?? 'development',\n\t\t\tregisteredModules: [...this.modules.keys()].sort(),\n\t\t\treadiness: this.checkProductionReadiness(),\n\t\t};\n\t}\n\n\tasync boot(): Promise<this> {\n\t\t// Fail closed before any side effects (transports, listeners): a\n\t\t// production deploy with an error-severity readiness problem must not boot.\n\t\tthis.enforceProductionReadiness();\n\t\tfor (const module of topoSort([...this.modules.values()])) {\n\t\t\tawait module.install(this);\n\t\t}\n\t\tthis.registerHealthRoutes();\n\t\treturn this;\n\t}\n\n\t// Liveness (/healthz) and readiness (/readyz) probes. Registered unprefixed so\n\t// they sit at a stable path regardless of basePath. Enabled unless disabled.\n\tprivate registerHealthRoutes(): void {\n\t\tif (this.config.healthChecks === false) return;\n\n\t\tthis.router.add('GET', '/healthz', compose([async () => Response.json({ status: 'ok' })]));\n\n\t\tif (this.config.metrics) {\n\t\t\tthis.router.add(\n\t\t\t\t'GET',\n\t\t\t\t'/metrics',\n\t\t\t\tcompose([\n\t\t\t\t\tasync () =>\n\t\t\t\t\t\tnew Response(this.metrics.render(), {\n\t\t\t\t\t\t\tstatus: 200,\n\t\t\t\t\t\t\theaders: { 'content-type': 'text/plain; version=0.0.4' },\n\t\t\t\t\t\t}),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tthis.router.add(\n\t\t\t'GET',\n\t\t\t'/readyz',\n\t\t\tcompose([\n\t\t\t\tasync () => {\n\t\t\t\t\tconst report = this.checkProductionReadiness();\n\t\t\t\t\tlet dependencies = true;\n\t\t\t\t\tif (this.config.readyProbe) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdependencies = Boolean(await this.config.readyProbe());\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tdependencies = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst ready = report.ok && dependencies;\n\t\t\t\t\treturn Response.json(\n\t\t\t\t\t\t{ status: ready ? 'ready' : 'not_ready', dependencies, problems: report.problems },\n\t\t\t\t\t\t{ status: ready ? 200 : 503 },\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t]),\n\t\t);\n\t}\n\n\t// Throws in production when `checkProductionReadiness()` reports any\n\t// error-severity problem, unless explicitly overridden. No-op otherwise.\n\tprivate enforceProductionReadiness(): void {\n\t\tif (process.env['NODE_ENV'] !== 'production') return;\n\t\tif (this.config.skipProductionReadinessGate) return;\n\t\tconst { ok, problems } = this.checkProductionReadiness();\n\t\tif (ok) return;\n\t\tconst errors = problems.filter((p) => p.severity === 'error');\n\t\tthrow new Error(\n\t\t\t`[fonderie] refusing to boot in production — ${errors.length} readiness ` +\n\t\t\t\t`error(s): ${errors.map((e) => `${e.module}: ${e.message}`).join('; ')}. ` +\n\t\t\t\t'Fix them, or set skipProductionReadinessGate: true to override (not recommended).',\n\t\t);\n\t}\n\n\t// Runs global middleware only (no routing, no 404).\n\t// Adapter packages call this to populate user/workspace/meta into their\n\t// native context before handing off to user-defined route handlers.\n\tasync buildContext(request: Request): Promise<IFonderieContext> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: { _buildContext: true },\n\t\t};\n\t\tawait compose(this.middlewares)(ctx, async () => new Response());\n\t\tdelete ctx.meta['_buildContext'];\n\t\treturn ctx;\n\t}\n\n\t// ─── Middleware ────────────────────────────────────────\n\n\tuse(middleware: Middleware): this {\n\t\tthis.middlewares.push(middleware);\n\t\treturn this;\n\t}\n\n\t// Modules call this to register their routes\n\taddRoute(method: string, path: string, ...handlers: Middleware[]): void {\n\t\tthis.router.add(method, this.prefix + path, compose(handlers));\n\t}\n\n\t// ─── The core handler ──────────────────────────────────\n\t// This is the ONE thing every adapter calls.\n\t// Takes a Web Standard Request, returns a Web Standard Response.\n\n\tasync handle(request: Request): Promise<Response> {\n\t\tconst ctx: IFonderieContext = {\n\t\t\trequest,\n\t\t\ttenant: null,\n\t\t\tuser: null,\n\t\t\tworkspace: null,\n\t\t\tmeta: {},\n\t\t};\n\n\t\t// Build the pipeline: global middleware → router → 404\n\t\tconst pipeline = compose([\n\t\t\t...this.middlewares,\n\t\t\trouterMiddleware(this.router),\n\t\t\tnotFoundMiddleware(),\n\t\t]);\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await pipeline(ctx, async () => new Response('Not Found', { status: 404 }));\n\t\t} catch (err) {\n\t\t\tresponse = this.config.onError?.(err) ?? defaultErrorHandler(err);\n\t\t}\n\t\treturn this.config.onResponse ? this.transformResponse(response, request) : response;\n\t}\n\n\t// Apply config.onResponse to a JSON response body, preserving status, headers,\n\t// and cookies. Non-JSON responses and hooks that return `undefined` pass through.\n\tprivate async transformResponse(response: Response, request: Request): Promise<Response> {\n\t\tconst contentType = response.headers.get('content-type') ?? '';\n\t\tif (!contentType.includes('application/json')) return response;\n\t\tlet body: unknown;\n\t\ttry {\n\t\t\tbody = await response.clone().json();\n\t\t} catch {\n\t\t\treturn response; // not valid JSON after all — leave untouched\n\t\t}\n\t\tconst transformed = this.config.onResponse!(body, { status: response.status, request });\n\t\tif (transformed === undefined) return response;\n\t\t// Preserve headers/cookies; drop content-length (the new body sets its own).\n\t\tconst headers = new Headers(response.headers);\n\t\theaders.delete('content-length');\n\t\theaders.delete('content-type');\n\t\treturn Response.json(transformed, { status: response.status, headers });\n\t}\n}\n// Framework adapters live in their own packages — no framework deps in core:\n// @fonderie/adapter-hono\n// @fonderie/adapter-express\n// @fonderie/adapter-koa\n\nfunction topoSort(modules: IFonderieModule[]): IFonderieModule[] {\n\tconst byName = new Map(modules.map((m) => [m.name, m]));\n\tconst result: IFonderieModule[] = [];\n\tconst visited = new Set<string>();\n\tconst visiting = new Set<string>();\n\n\tfunction visit(m: IFonderieModule, path: string[]): void {\n\t\tif (visited.has(m.name)) return;\n\t\tif (visiting.has(m.name)) {\n\t\t\tthrow new Error(`[fonderie] circular dependency: ${[...path, m.name].join(' → ')}`);\n\t\t}\n\t\tvisiting.add(m.name);\n\t\tfor (const dep of m.deps ?? []) {\n\t\t\tconst found = byName.get(dep);\n\t\t\tif (!found)\n\t\t\t\tthrow new Error(`[fonderie] \"${m.name}\" requires \"${dep}\" but it is not registered`);\n\t\t\tvisit(found, [...path, m.name]);\n\t\t}\n\t\tvisiting.delete(m.name);\n\t\tvisited.add(m.name);\n\t\tresult.push(m);\n\t}\n\n\tfor (const m of modules) visit(m, []);\n\treturn result;\n}\n\nfunction getLocalIPv4(): string {\n\tconst nets = networkInterfaces();\n\n\tfor (const interfaces of Object.values(nets)) {\n\t\tif (!interfaces) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const iface of interfaces) {\n\t\t\tif (iface.family === 'IPv4' && !iface.internal) {\n\t\t\t\treturn iface.address;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn '127.0.0.1'; // fallback if no external interface found\n}\n","import type { IRouter, Middleware, IRouteMatch, IFonderieContext } from './types';\n\nexport class Router implements IRouter {\n\tprivate routes: Array<{ method: string; path: string; handler: Middleware }> = [];\n\n\tadd(method: string, path: string, handler: Middleware): void {\n\t\tthis.routes.push({ method: method.toUpperCase(), path, handler });\n\t}\n\n\tmatch(method: string, path: string): IRouteMatch | null {\n\t\tfor (const route of this.routes) {\n\t\t\tif (route.method !== method.toUpperCase()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst params = matchPath(route.path, path);\n\t\t\tif (params !== null) {\n\t\t\t\treturn { handler: route.handler, params };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n\n// Segment-by-segment match with :param extraction\n// /users/:id matches /users/42 → { id: '42' }\nfunction matchPath(pattern: string, path: string): Record<string, string> | null {\n\tconst clean = (path.split('?')[0] ?? path).replace(/\\/$/, '') || '/'; // strip query string and trailing slash\n\tconst pp = pattern.split('/');\n\tconst vp = clean.split('/');\n\n\tif (pp.length !== vp.length) {\n\t\treturn null;\n\t}\n\n\tconst params: Record<string, string> = {};\n\n\tfor (let i = 0; i < pp.length; i++) {\n\t\tconst ps = pp[i] ?? '';\n\t\tconst vs = vp[i] ?? '';\n\t\tif (ps.startsWith(':')) {\n\t\t\tparams[ps.slice(1)] = decodeURIComponent(vs);\n\t\t} else if (ps !== vs) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Middleware that runs the router inside the pipeline\nexport function routerMiddleware(router: Router): Middleware {\n\treturn async (ctx: IFonderieContext, next) => {\n\t\tconst url = new URL(ctx.request.url);\n\t\tconst match = router.match(ctx.request.method, url.pathname);\n\n\t\tif (!match) {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Route params available to handlers via ctx.meta.params\n\t\tctx.meta.params = match.params;\n\t\treturn match.handler(ctx, next);\n\t};\n}\n","import type { IFonderieContext, Middleware } from './types';\n\n// Classic onion middleware compose — same pattern as Koa's\nexport function compose(middlewares: Middleware[]) {\n\treturn function (ctx: IFonderieContext, fallback: () => Promise<Response>): Promise<Response> {\n\t\tlet index = -1;\n\n\t\tfunction dispatch(i: number): Promise<Response> {\n\t\t\tif (i <= index) {\n\t\t\t\tthrow new Error('next() called multiple times');\n\t\t\t}\n\t\t\tindex = i;\n\t\t\tconst fn = middlewares[i] ?? fallback;\n\t\t\treturn fn(ctx, () => dispatch(i + 1));\n\t\t}\n\n\t\treturn dispatch(0);\n\t};\n}\n","export const HTTP = {\n\tOK: 200,\n\tCREATED: 201,\n\tACCEPTED: 202,\n\tNO_CONTENT: 204,\n\tBAD_REQUEST: 400,\n\tUNAUTHORIZED: 401,\n\tPAYMENT_REQUIRED: 402,\n\tFORBIDDEN: 403,\n\tNOT_FOUND: 404,\n\tCONFLICT: 409,\n\tGONE: 410,\n\tUNPROCESSABLE: 422,\n\tTOO_MANY_REQUESTS: 429,\n\tSERVER_ERROR: 500,\n\tNOT_IMPLEMENTED: 501,\n\tBAD_GATEWAY: 502,\n\tSERVICE_UNAVAILABLE: 503,\n} as const;\n\nexport type HttpStatus = (typeof HTTP)[keyof typeof HTTP];\n\nexport interface IApiEnvelope {\n\treason: string;\n\texplanation: string;\n\tresult?: unknown;\n}\n\nexport interface IApiError {\n\treason: string;\n\texplanation: string;\n\tdetails?: unknown;\n}\n\nexport function setApiResponse<T>(\n\tstatus: number,\n\treason: string,\n\texplanation: string,\n\tpayload?: T,\n): Response {\n\tconst body: Record<string, unknown> = { reason, explanation };\n\tif (payload !== undefined) {\n\t\tbody[status < 400 ? 'result' : 'details'] = payload;\n\t}\n\treturn Response.json(body, { status });\n}\n","import { setApiResponse, HTTP } from '../response';\nimport type { Middleware } from '../types';\n\nexport function notFoundMiddleware(): Middleware {\n\treturn async (_ctx, _next) => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Not found');\n}\n","import type { Middleware } from '../types';\nimport { setApiResponse, HTTP } from '../response';\n\nexport const withBody: Middleware = async (ctx, next) => {\n\tconst method = ctx.request.method.toUpperCase();\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn next();\n\t}\n\n\tconst ct = ctx.request.headers.get('content-type') ?? '';\n\n\ttry {\n\t\tif (ct.includes('application/json')) {\n\t\t\tconst text = (await ctx.request.clone().text()).trim();\n\t\t\tctx.meta.body = text ? JSON.parse(text) : {};\n\t\t} else if (ct.includes('application/x-www-form-urlencoded')) {\n\t\t\tconst text = await ctx.request.clone().text();\n\t\t\tctx.meta.body = Object.fromEntries(new URLSearchParams(text));\n\t\t}\n\t\t// multipart/form-data left to the handler — no dep-free way to parse it\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid request body');\n\t}\n\n\treturn next();\n};\n","import type { Middleware } from '../types';\n\nexport interface SecurityHeadersOptions {\n\t// HSTS max-age in seconds. Default 180 days. Set 0 to omit the header.\n\thstsMaxAge?: number;\n\t// Add `includeSubDomains` to the HSTS header. Off by default — only enable\n\t// once every subdomain is known to serve HTTPS.\n\thstsIncludeSubDomains?: boolean;\n\t// Add `preload` to the HSTS header (implies includeSubDomains). Off by default.\n\thstsPreload?: boolean;\n}\n\n// Baseline response hardening. `X-Content-Type-Options: nosniff` is always safe.\n// HSTS is only meaningful — and only emitted — over HTTPS: browsers ignore it on\n// plain HTTP, so gating on the effective scheme keeps local http/dev untouched\n// while enforcing TLS in production (behind a TLS-terminating proxy, detected via\n// X-Forwarded-Proto). Wired into the default pipeline by FonderieApp.\nexport function withSecurityHeaders(options: SecurityHeadersOptions = {}): Middleware {\n\tconst {\n\t\thstsMaxAge = 60 * 60 * 24 * 180,\n\t\thstsIncludeSubDomains = false,\n\t\thstsPreload = false,\n\t} = options;\n\n\tlet hsts = '';\n\tif (hstsMaxAge > 0) {\n\t\thsts = `max-age=${hstsMaxAge}`;\n\t\tif (hstsIncludeSubDomains || hstsPreload) hsts += '; includeSubDomains';\n\t\tif (hstsPreload) hsts += '; preload';\n\t}\n\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst patched = new Headers(response.headers);\n\n\t\tpatched.set('X-Content-Type-Options', 'nosniff');\n\n\t\tif (hsts && isHttps(ctx.request)) {\n\t\t\tpatched.set('Strict-Transport-Security', hsts);\n\t\t}\n\n\t\treturn new Response(response.body, {\n\t\t\theaders: patched,\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t});\n\t};\n}\n\n// HTTPS if the request URL is https, or a TLS-terminating proxy says so.\nfunction isHttps(request: Request): boolean {\n\tif (request.url.startsWith('https:')) return true;\n\tconst proto = request.headers.get('x-forwarded-proto');\n\treturn proto?.split(',')[0]?.trim() === 'https';\n}\n","import { setApiResponse, HTTP } from '../response';\n\nexport function defaultErrorHandler(err: unknown): Response {\n\tconst dev = process.env['NODE_ENV'] !== 'production';\n\n\tif (err instanceof Error) {\n\t\tconsole.error('[fonderie]', err.message, err.stack);\n\t\treturn setApiResponse(\n\t\t\tHTTP.SERVER_ERROR,\n\t\t\t'SERVER_ERROR',\n\t\t\tdev ? err.message : 'Internal server error',\n\t\t);\n\t}\n\n\tconsole.error('[fonderie] unknown error', err);\n\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Internal server error');\n}\n","import { timingSafeEqual } from 'node:crypto';\n\n// The one timing-attack-safe equality across Fonderie — admin tokens (billing/\n// config/courier), MFA/TOTP codes (auth), event-log + inbound webhook-signature\n// HMACs (events/courier). Length-guard first: timingSafeEqual throws on unequal\n// lengths, and the length is not the secret. Accepts strings or Buffers (callers\n// comparing decoded signature bytes pass Buffers).\nexport function constantTimeEqual(a: string | Buffer, b: string | Buffer): boolean {\n\tconst bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);\n\tconst bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n","// One weak/placeholder-secret denylist + minimum length, so every module that\n// guards a surface with a bootstrap secret (auth jwtSecret, module adminTokens)\n// enforces the SAME bar. Call sites keep their own POLICY (required vs optional,\n// which field name to name in the message) — only the rule is shared.\nexport const MIN_SECRET_LENGTH = 32;\nexport const PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\n\n// The single classification. null = strong enough. Each caller formats its own\n// IReadinessProblem (naming its field) from this verdict.\nexport function secretStrengthProblem(secret: string): 'too-short' | 'placeholder' | null {\n\tif (secret.length < MIN_SECRET_LENGTH) return 'too-short';\n\tif (PLACEHOLDER_SECRET.test(secret)) return 'placeholder';\n\treturn null;\n}\n","import type { Middleware } from './types';\n\n// Minimal, dependency-free metrics (SOC 2 CC7.2). Counts HTTP requests by\n// status class and exposes them in Prometheus text format at /metrics (opt-in\n// via config.metrics). Apps can also record custom counters. Not a full metrics\n// system — enough to alert on error rate and traffic without pulling a client.\n\nexport class MetricsRegistry {\n\tprivate counters = new Map<string, number>();\n\n\tinc(name: string, labels: Record<string, string> = {}, by = 1): void {\n\t\tconst key = seriesKey(name, labels);\n\t\tthis.counters.set(key, (this.counters.get(key) ?? 0) + by);\n\t}\n\n\t// Prometheus text exposition format.\n\trender(): string {\n\t\tconst lines: string[] = [];\n\t\tfor (const [key, value] of this.counters) lines.push(`${key} ${value}`);\n\t\treturn lines.join('\\n') + (lines.length ? '\\n' : '');\n\t}\n}\n\nfunction seriesKey(name: string, labels: Record<string, string>): string {\n\tconst parts = Object.entries(labels).map(([k, v]) => `${k}=\"${String(v).replace(/\"/g, '')}\"`);\n\treturn parts.length ? `${name}{${parts.join(',')}}` : name;\n}\n\n// Middleware that records one `http_requests_total{status_class}` per response.\nexport function withMetrics(registry: MetricsRegistry): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst response = await next();\n\t\tconst cls = `${Math.floor(response.status / 100)}xx`;\n\t\tregistry.inc('http_requests_total', { status_class: cls });\n\t\treturn response;\n\t};\n}\n","export interface IBillingPlan {\n\tname: string;\n\tprice: number | null; // null = custom/enterprise pricing\n\tseats: number | 'unlimited';\n\tinterval?: 'month' | 'year';\n\ttrialDays?: number;\n}\n\nexport interface ISMTPConfig {\n\thost: string;\n\tport: number;\n\tsecure: boolean; // true = TLS, false = STARTTLS\n\tuser: string;\n\tpass: string;\n}\n\nexport interface FonderieConfig {\n\tbasePath?: string; // e.g. '/v1' — prefixes all routes; defaults to ''\n\n\tdb: {\n\t\turl: string; // Standard postgres:// connection string\n\t\t// Future: adapter pattern for other vendors\n\t\t// adapter?: 'pg' | 'mysql2' | 'oracledb' ← v2 concern\n\t};\n\n\tbilling?: {\n\t\tprovider: 'stripe';\n\t\tplans: IBillingPlan[];\n\t\tstripeSecretKey: string;\n\t};\n\n\temail?: {\n\t\tfrom: string;\n\t\t// Credentials from their .env — Fonderie never stores these\n\t\tapiKey?: string;\n\t\tsmtp?: ISMTPConfig;\n\t\tprovider: 'resend' | 'ses' | 'smtp';\n\t};\n\n\t// Fail-closed production boot gate. In production, `boot()` refuses to start\n\t// when any registered module reports an `error`-severity readiness problem\n\t// (e.g. a weak signing secret, a missing at-rest encryption key). Set this to\n\t// true only to deliberately override the gate — not recommended. Outside\n\t// production the gate never runs.\n\tskipProductionReadinessGate?: boolean;\n\n\t// Built-in health endpoints, registered on boot (unprefixed by basePath):\n\t// GET /healthz — liveness, always 200 while the process is up\n\t// GET /readyz — readiness, 200 when checkProductionReadiness() is ok AND\n\t// readyProbe() (if provided) resolves truthy, else 503\n\t// Set false to disable. Point your platform's probes at these.\n\thealthChecks?: boolean;\n\t// Optional dependency probe for /readyz — e.g. `() => store.testConnection()`.\n\t// Throwing or returning false makes /readyz report 503.\n\treadyProbe?: () => boolean | Promise<boolean>;\n\n\t// Enable Prometheus-format metrics: counts requests by status class and\n\t// serves them at GET /metrics (unprefixed). Off by default. Access to\n\t// /metrics should be restricted at your ingress/scraper.\n\tmetrics?: boolean;\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n","export function stringOrEmpty(value: unknown): string {\n\treturn typeof value === 'string' ? value : '';\n}\n\nexport function booleanOrFalse(value: unknown): boolean {\n\tif (typeof value === 'boolean') return value;\n\tif (value === 'true' || value === '1') return true;\n\treturn false;\n}\n\nexport function arrayOrEmpty<T>(value: unknown): T[] {\n\treturn Array.isArray(value) ? (value as T[]) : [];\n}\n\nexport function numberOrZero(value: unknown): number {\n\tconst n = Number(value);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\nexport function dateOrEmpty(value: unknown): string {\n\tif (typeof value === 'string') return value;\n\tif (value instanceof Date) return value.toISOString();\n\treturn '';\n}\n","// One opaque keyset-pagination cursor over (created_at, id) — used by billing's\n// wallet ledger and audit's event log (previously two copies that had drifted:\n// audit's decode skipped field-range checks, so a crafted cursor reached the\n// ::timestamptz cast as a 500 instead of decoding to null → 422). Pure: no DB.\nexport function encodeKeysetCursor(createdAt: string, id: string): string {\n\treturn Buffer.from(JSON.stringify([createdAt, id])).toString('base64url');\n}\n\n// Range-checked (month 01-12, day 01-31, hour 00-23, min/sec 00-59) so a crafted\n// in-shape-but-out-of-range timestamp yields null (→ the caller's 422), never a\n// Postgres cast error (500). Accepts Postgres' own text form\n// ('2026-09-04 18:50:50.888123+00') so cursors survive round-trips without JS\n// millisecond truncation.\nconst CURSOR_TS_RE =\n\t/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[T ]([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function decodeKeysetCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));\n\t\tif (!Array.isArray(parsed) || typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string') {\n\t\t\treturn null;\n\t\t}\n\t\tif (!CURSOR_TS_RE.test(parsed[0]) || !CURSOR_ID_RE.test(parsed[1])) return null;\n\t\treturn { createdAt: parsed[0], id: parsed[1] };\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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","names":["module"]}