@fonderie/core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brain/signatures.md +2 -1
- package/dist/body-parser-dk4YTT5g.d.ts +19 -0
- package/dist/body-parser-hOh-q32S.d.cts +19 -0
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.cjs +141 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -7
- package/dist/index.d.ts +2 -7
- package/dist/index.js +141 -73
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +73 -17
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +2 -3
- package/dist/middlewares/index.d.ts +2 -3
- package/dist/middlewares/index.js +71 -17
- package/dist/middlewares/index.js.map +1 -1
- package/dist/response.cjs +1 -0
- package/dist/response.cjs.map +1 -1
- package/dist/response.d.cts +1 -0
- package/dist/response.d.ts +1 -0
- package/dist/response.js +1 -0
- package/dist/response.js.map +1 -1
- package/package.json +1 -1
package/brain/signatures.md
CHANGED
|
@@ -161,6 +161,7 @@ interface FonderieConfig {
|
|
|
161
161
|
skipProductionReadinessGate?: boolean;
|
|
162
162
|
healthChecks?: boolean;
|
|
163
163
|
readyProbe?: () => boolean | Promise<boolean>;
|
|
164
|
+
exposeReadyzDetails?: boolean;
|
|
164
165
|
metrics?: boolean;
|
|
165
166
|
maxBodyBytes?: number;
|
|
166
167
|
onError?: (err: unknown) => Response;
|
|
@@ -200,7 +201,7 @@ interface IApiError {
|
|
|
200
201
|
|
|
201
202
|
type HttpStatus = (typeof HTTP)[keyof typeof HTTP];
|
|
202
203
|
|
|
203
|
-
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; }
|
|
204
|
+
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 PAYLOAD_TOO_LARGE: 413; readonly UNPROCESSABLE: 422; readonly TOO_MANY_REQUESTS: 429; readonly SERVER_ERROR: 500; readonly NOT_IMPLEMENTED: 501; readonly BAD_GATEWAY: 502; readonly SERVICE_UNAVAILABLE: 503; }
|
|
204
205
|
|
|
205
206
|
function setApiResponse<T>(status: number, reason: string, explanation: string, payload?: T | undefined): Response
|
|
206
207
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Middleware } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default cap on the request body the pipeline will buffer, in bytes (5 MiB).
|
|
5
|
+
* Enforced HERE, in the body parser, so EVERY entry point inherits it — the
|
|
6
|
+
* built-in listen() server, and all adapters' buildContext()/handle() paths.
|
|
7
|
+
* (An uncapped parser was a memory-exhaustion DoS on any adapter whose
|
|
8
|
+
* transport didn't add its own cap, e.g. adapter-hono on node-server.)
|
|
9
|
+
*/
|
|
10
|
+
declare const DEFAULT_MAX_BODY_BYTES: number;
|
|
11
|
+
/**
|
|
12
|
+
* Build the body-parsing middleware with an explicit byte cap. The core app
|
|
13
|
+
* wires this with `config.maxBodyBytes`; the bare `withBody` export below
|
|
14
|
+
* keeps the default cap for direct users.
|
|
15
|
+
*/
|
|
16
|
+
declare function bodyParser(maxBytes?: number): Middleware;
|
|
17
|
+
declare const withBody: Middleware;
|
|
18
|
+
|
|
19
|
+
export { DEFAULT_MAX_BODY_BYTES as D, bodyParser as b, withBody as w };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Middleware } from './types.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default cap on the request body the pipeline will buffer, in bytes (5 MiB).
|
|
5
|
+
* Enforced HERE, in the body parser, so EVERY entry point inherits it — the
|
|
6
|
+
* built-in listen() server, and all adapters' buildContext()/handle() paths.
|
|
7
|
+
* (An uncapped parser was a memory-exhaustion DoS on any adapter whose
|
|
8
|
+
* transport didn't add its own cap, e.g. adapter-hono on node-server.)
|
|
9
|
+
*/
|
|
10
|
+
declare const DEFAULT_MAX_BODY_BYTES: number;
|
|
11
|
+
/**
|
|
12
|
+
* Build the body-parsing middleware with an explicit byte cap. The core app
|
|
13
|
+
* wires this with `config.maxBodyBytes`; the bare `withBody` export below
|
|
14
|
+
* keeps the default cap for direct users.
|
|
15
|
+
*/
|
|
16
|
+
declare function bodyParser(maxBytes?: number): Middleware;
|
|
17
|
+
declare const withBody: Middleware;
|
|
18
|
+
|
|
19
|
+
export { DEFAULT_MAX_BODY_BYTES as D, bodyParser as b, withBody as w };
|
package/dist/config.cjs.map
CHANGED
|
@@ -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\t// Fail-closed production boot gate. In production, `boot()` refuses to start\n\t// when any registered module reports an `error`-severity readiness problem\n\t// (e.g. a weak signing secret, a missing at-rest encryption key). Set this to\n\t// true only to deliberately override the gate — not recommended. Outside\n\t// production the gate never runs.\n\tskipProductionReadinessGate?: boolean;\n\n\t// Built-in health endpoints, registered on boot (unprefixed by basePath):\n\t// GET /healthz — liveness, always 200 while the process is up\n\t// GET /readyz — readiness, 200 when checkProductionReadiness() is ok AND\n\t// readyProbe() (if provided) resolves truthy, else 503\n\t// Set false to disable. Point your platform's probes at these.\n\thealthChecks?: boolean;\n\t// Optional dependency probe for /readyz — e.g. `() => store.testConnection()`.\n\t// Throwing or returning false makes /readyz report 503.\n\treadyProbe?: () => boolean | Promise<boolean>;\n\n\t// Enable Prometheus-format metrics: counts requests by status class and\n\t// serves them at GET /metrics (unprefixed). Off by default. Access to\n\t// /metrics should be restricted at your ingress/scraper.\n\tmetrics?: boolean;\n\n\t// Cap on the request body the built-in `listen()` server will buffer, in\n\t// bytes. Defaults to 5 MiB (matching the adapters). Without a cap, an\n\t// unauthenticated request could stream an arbitrarily large body fully into\n\t// memory before any handler runs — a memory-exhaustion DoS. Oversize\n\t// requests get 413. Raise for large uploads (e.g. @fonderie/media images);\n\t// adapter deployments configure this on the adapter instead.\n\tmaxBodyBytes?: number;\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
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\t// /readyz's `problems` list names weak secrets and placeholder tokens — a\n\t// security-posture map — so in production it is omitted unless this is\n\t// explicitly true. Probes only need the status code. Non-production always\n\t// includes details.\n\texposeReadyzDetails?: boolean;\n\n\t// Enable Prometheus-format metrics: counts requests by status class and\n\t// serves them at GET /metrics (unprefixed). Off by default. Access to\n\t// /metrics should be restricted at your ingress/scraper.\n\tmetrics?: boolean;\n\n\t// Cap on the request body the built-in `listen()` server will buffer, in\n\t// bytes. Defaults to 5 MiB (matching the adapters). Without a cap, an\n\t// unauthenticated request could stream an arbitrarily large body fully into\n\t// memory before any handler runs — a memory-exhaustion DoS. Oversize\n\t// requests get 413. Raise for large uploads (e.g. @fonderie/media images);\n\t// adapter deployments configure this on the adapter instead.\n\tmaxBodyBytes?: number;\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAuFO,SAAS,aAAa,QAAwC;AACpE,SAAO;AACR;","names":[]}
|
package/dist/config.d.cts
CHANGED
|
@@ -31,6 +31,7 @@ interface FonderieConfig {
|
|
|
31
31
|
skipProductionReadinessGate?: boolean;
|
|
32
32
|
healthChecks?: boolean;
|
|
33
33
|
readyProbe?: () => boolean | Promise<boolean>;
|
|
34
|
+
exposeReadyzDetails?: boolean;
|
|
34
35
|
metrics?: boolean;
|
|
35
36
|
maxBodyBytes?: number;
|
|
36
37
|
onError?: (err: unknown) => Response;
|
package/dist/config.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ interface FonderieConfig {
|
|
|
31
31
|
skipProductionReadinessGate?: boolean;
|
|
32
32
|
healthChecks?: boolean;
|
|
33
33
|
readyProbe?: () => boolean | Promise<boolean>;
|
|
34
|
+
exposeReadyzDetails?: boolean;
|
|
34
35
|
metrics?: boolean;
|
|
35
36
|
maxBodyBytes?: number;
|
|
36
37
|
onError?: (err: unknown) => Response;
|
package/dist/config.js.map
CHANGED
|
@@ -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\t// Fail-closed production boot gate. In production, `boot()` refuses to start\n\t// when any registered module reports an `error`-severity readiness problem\n\t// (e.g. a weak signing secret, a missing at-rest encryption key). Set this to\n\t// true only to deliberately override the gate — not recommended. Outside\n\t// production the gate never runs.\n\tskipProductionReadinessGate?: boolean;\n\n\t// Built-in health endpoints, registered on boot (unprefixed by basePath):\n\t// GET /healthz — liveness, always 200 while the process is up\n\t// GET /readyz — readiness, 200 when checkProductionReadiness() is ok AND\n\t// readyProbe() (if provided) resolves truthy, else 503\n\t// Set false to disable. Point your platform's probes at these.\n\thealthChecks?: boolean;\n\t// Optional dependency probe for /readyz — e.g. `() => store.testConnection()`.\n\t// Throwing or returning false makes /readyz report 503.\n\treadyProbe?: () => boolean | Promise<boolean>;\n\n\t// Enable Prometheus-format metrics: counts requests by status class and\n\t// serves them at GET /metrics (unprefixed). Off by default. Access to\n\t// /metrics should be restricted at your ingress/scraper.\n\tmetrics?: boolean;\n\n\t// Cap on the request body the built-in `listen()` server will buffer, in\n\t// bytes. Defaults to 5 MiB (matching the adapters). Without a cap, an\n\t// unauthenticated request could stream an arbitrarily large body fully into\n\t// memory before any handler runs — a memory-exhaustion DoS. Oversize\n\t// requests get 413. Raise for large uploads (e.g. @fonderie/media images);\n\t// adapter deployments configure this on the adapter instead.\n\tmaxBodyBytes?: number;\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n"],"mappings":";
|
|
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\t// /readyz's `problems` list names weak secrets and placeholder tokens — a\n\t// security-posture map — so in production it is omitted unless this is\n\t// explicitly true. Probes only need the status code. Non-production always\n\t// includes details.\n\texposeReadyzDetails?: boolean;\n\n\t// Enable Prometheus-format metrics: counts requests by status class and\n\t// serves them at GET /metrics (unprefixed). Off by default. Access to\n\t// /metrics should be restricted at your ingress/scraper.\n\tmetrics?: boolean;\n\n\t// Cap on the request body the built-in `listen()` server will buffer, in\n\t// bytes. Defaults to 5 MiB (matching the adapters). Without a cap, an\n\t// unauthenticated request could stream an arbitrarily large body fully into\n\t// memory before any handler runs — a memory-exhaustion DoS. Oversize\n\t// requests get 413. Raise for large uploads (e.g. @fonderie/media images);\n\t// adapter deployments configure this on the adapter instead.\n\tmaxBodyBytes?: number;\n\n\tonError?: (err: unknown) => Response;\n\n\t// Transform every JSON response body just before it is sent. Return the new\n\t// body, or `undefined` to leave that response untouched. This is the single,\n\t// adapter-agnostic seam for adapting Fonderie's `{ reason, explanation, result }`\n\t// envelope to an app's own contract (e.g. flat shapes an existing frontend\n\t// expects) WITHOUT editing handlers. Applied at the one egress point, so it\n\t// covers every route and every adapter. Status code, headers, and cookies are\n\t// preserved; only the body shape changes. Non-JSON responses pass through.\n\t// Opt-in: unset = current behaviour, unchanged.\n\tonResponse?: (body: unknown, info: { status: number; request: Request }) => unknown;\n}\n\nexport function defineConfig(config: FonderieConfig): FonderieConfig {\n\treturn config; // typed identity — same pattern as defineConfig in Vite/Nuxt\n}\n"],"mappings":";AAuFO,SAAS,aAAa,QAAwC;AACpE,SAAO;AACR;","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -134,6 +134,7 @@ var HTTP = {
|
|
|
134
134
|
NOT_FOUND: 404,
|
|
135
135
|
CONFLICT: 409,
|
|
136
136
|
GONE: 410,
|
|
137
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
137
138
|
UNPROCESSABLE: 422,
|
|
138
139
|
TOO_MANY_REQUESTS: 429,
|
|
139
140
|
SERVER_ERROR: 500,
|
|
@@ -155,25 +156,75 @@ function notFoundMiddleware() {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
// src/middlewares/body-parser.ts
|
|
158
|
-
var
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
var DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
160
|
+
var PayloadTooLargeError = class extends Error {
|
|
161
|
+
fonderiePayloadTooLarge = true;
|
|
162
|
+
};
|
|
163
|
+
async function readTextCapped(req, maxBytes) {
|
|
164
|
+
const declared = Number(req.headers.get("content-length"));
|
|
165
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
166
|
+
throw new PayloadTooLargeError();
|
|
162
167
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
if (!req.body) return null;
|
|
169
|
+
const reader = req.body.getReader();
|
|
170
|
+
const chunks = [];
|
|
171
|
+
let total = 0;
|
|
172
|
+
for (; ; ) {
|
|
173
|
+
const { done, value } = await reader.read();
|
|
174
|
+
if (done) break;
|
|
175
|
+
total += value.byteLength;
|
|
176
|
+
if (total > maxBytes) {
|
|
177
|
+
await reader.cancel().catch(() => void 0);
|
|
178
|
+
throw new PayloadTooLargeError();
|
|
171
179
|
}
|
|
172
|
-
|
|
173
|
-
return setApiResponse(HTTP.BAD_REQUEST, "INVALID_REQUEST", "Invalid request body");
|
|
180
|
+
chunks.push(value);
|
|
174
181
|
}
|
|
175
|
-
|
|
176
|
-
|
|
182
|
+
const merged = new Uint8Array(total);
|
|
183
|
+
let offset = 0;
|
|
184
|
+
for (const c of chunks) {
|
|
185
|
+
merged.set(c, offset);
|
|
186
|
+
offset += c.byteLength;
|
|
187
|
+
}
|
|
188
|
+
return new TextDecoder().decode(merged);
|
|
189
|
+
}
|
|
190
|
+
function bodyParser(maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
191
|
+
return async (ctx, next) => {
|
|
192
|
+
const method = ctx.request.method.toUpperCase();
|
|
193
|
+
if (method === "GET" || method === "HEAD") {
|
|
194
|
+
return next();
|
|
195
|
+
}
|
|
196
|
+
const ct = ctx.request.headers.get("content-type") ?? "";
|
|
197
|
+
try {
|
|
198
|
+
if (ct.includes("application/json") || ct.includes("application/x-www-form-urlencoded")) {
|
|
199
|
+
const raw = await readTextCapped(ctx.request, maxBytes);
|
|
200
|
+
if (raw !== null) {
|
|
201
|
+
ctx.request = new Request(ctx.request.url, {
|
|
202
|
+
method: ctx.request.method,
|
|
203
|
+
headers: ctx.request.headers,
|
|
204
|
+
body: raw.length > 0 ? raw : null
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (ct.includes("application/json")) {
|
|
208
|
+
const text = raw?.trim() ?? "";
|
|
209
|
+
ctx.meta.body = text ? JSON.parse(text) : {};
|
|
210
|
+
} else {
|
|
211
|
+
ctx.meta.body = Object.fromEntries(new URLSearchParams(raw ?? ""));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (err?.fonderiePayloadTooLarge) {
|
|
216
|
+
return setApiResponse(
|
|
217
|
+
HTTP.PAYLOAD_TOO_LARGE,
|
|
218
|
+
"PAYLOAD_TOO_LARGE",
|
|
219
|
+
"Request body too large"
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return setApiResponse(HTTP.BAD_REQUEST, "INVALID_REQUEST", "Invalid request body");
|
|
223
|
+
}
|
|
224
|
+
return next();
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
var withBody = bodyParser();
|
|
177
228
|
|
|
178
229
|
// src/middlewares/security-headers.ts
|
|
179
230
|
function withSecurityHeaders(options = {}) {
|
|
@@ -269,8 +320,7 @@ function withMetrics(registry) {
|
|
|
269
320
|
}
|
|
270
321
|
|
|
271
322
|
// src/app.ts
|
|
272
|
-
var
|
|
273
|
-
var PayloadTooLargeError = class extends Error {
|
|
323
|
+
var PayloadTooLargeError2 = class extends Error {
|
|
274
324
|
fonderiePayloadTooLarge = true;
|
|
275
325
|
};
|
|
276
326
|
function payloadTooLarge(res) {
|
|
@@ -288,7 +338,7 @@ var FonderieApp = class {
|
|
|
288
338
|
constructor(config) {
|
|
289
339
|
this.config = config;
|
|
290
340
|
this.prefix = (config.basePath ?? "").replace(/\/$/, "");
|
|
291
|
-
this.middlewares = [
|
|
341
|
+
this.middlewares = [bodyParser(config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES), withSecurityHeaders()];
|
|
292
342
|
if (config.metrics) this.middlewares.push(withMetrics(this.metrics));
|
|
293
343
|
}
|
|
294
344
|
listen(port, options = {}) {
|
|
@@ -300,66 +350,79 @@ var FonderieApp = class {
|
|
|
300
350
|
} = options;
|
|
301
351
|
const maxBodyBytes = this.config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
302
352
|
const server = (0, import_node_http.createServer)(async (req, res) => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
353
|
+
try {
|
|
354
|
+
const host = req.headers.host ?? "localhost";
|
|
355
|
+
const url = `http://${host}${req.url ?? "/"}`;
|
|
356
|
+
const headers = new Headers();
|
|
357
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
358
|
+
if (!value) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (Array.isArray(value)) {
|
|
362
|
+
for (const v of value) headers.append(key, v);
|
|
363
|
+
} else {
|
|
364
|
+
headers.set(key, value);
|
|
365
|
+
}
|
|
309
366
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
367
|
+
const method = req.method ?? "GET";
|
|
368
|
+
const hasBody = !["GET", "HEAD"].includes(method.toUpperCase());
|
|
369
|
+
const declared = Number(req.headers["content-length"]);
|
|
370
|
+
if (hasBody && Number.isFinite(declared) && declared > maxBodyBytes) {
|
|
371
|
+
payloadTooLarge(res);
|
|
372
|
+
res.once("close", () => req.destroy());
|
|
373
|
+
return;
|
|
314
374
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (total > maxBodyBytes) {
|
|
332
|
-
reject(new PayloadTooLargeError());
|
|
333
|
-
req.destroy();
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
chunks.push(chunk);
|
|
375
|
+
let body;
|
|
376
|
+
try {
|
|
377
|
+
body = await new Promise((resolve, reject) => {
|
|
378
|
+
const chunks = [];
|
|
379
|
+
let total = 0;
|
|
380
|
+
req.on("data", (chunk) => {
|
|
381
|
+
total += chunk.length;
|
|
382
|
+
if (total > maxBodyBytes) {
|
|
383
|
+
reject(new PayloadTooLargeError2());
|
|
384
|
+
req.destroy();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
chunks.push(chunk);
|
|
388
|
+
});
|
|
389
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
390
|
+
req.on("error", reject);
|
|
337
391
|
});
|
|
338
|
-
|
|
339
|
-
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (err instanceof PayloadTooLargeError2) {
|
|
394
|
+
payloadTooLarge(res);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
res.statusCode = 400;
|
|
398
|
+
res.end();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const request = new Request(url, {
|
|
402
|
+
method,
|
|
403
|
+
headers,
|
|
404
|
+
body: hasBody && body.length > 0 ? new Uint8Array(body) : null
|
|
405
|
+
});
|
|
406
|
+
const response = await this.handle(request);
|
|
407
|
+
res.statusCode = response.status;
|
|
408
|
+
const setCookies = response.headers.getSetCookie?.() ?? [];
|
|
409
|
+
if (setCookies.length) res.setHeader("Set-Cookie", setCookies);
|
|
410
|
+
response.headers.forEach((v, k) => {
|
|
411
|
+
if (k.toLowerCase() !== "set-cookie") res.setHeader(k, v);
|
|
340
412
|
});
|
|
413
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
341
414
|
} catch (err) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
415
|
+
console.error("[fonderie] request handling failed:", err?.message);
|
|
416
|
+
try {
|
|
417
|
+
if (!res.headersSent) {
|
|
418
|
+
res.statusCode = 400;
|
|
419
|
+
res.setHeader("content-type", "application/json");
|
|
420
|
+
}
|
|
421
|
+
res.end(JSON.stringify({ reason: "BAD_REQUEST", explanation: "Malformed request" }));
|
|
422
|
+
} catch {
|
|
423
|
+
req.destroy();
|
|
345
424
|
}
|
|
346
|
-
res.statusCode = 400;
|
|
347
|
-
res.end();
|
|
348
|
-
return;
|
|
349
425
|
}
|
|
350
|
-
const request = new Request(url, {
|
|
351
|
-
method,
|
|
352
|
-
headers,
|
|
353
|
-
body: hasBody && body.length > 0 ? new Uint8Array(body) : null
|
|
354
|
-
});
|
|
355
|
-
const response = await this.handle(request);
|
|
356
|
-
res.statusCode = response.status;
|
|
357
|
-
const setCookies = response.headers.getSetCookie?.() ?? [];
|
|
358
|
-
if (setCookies.length) res.setHeader("Set-Cookie", setCookies);
|
|
359
|
-
response.headers.forEach((v, k) => {
|
|
360
|
-
if (k.toLowerCase() !== "set-cookie") res.setHeader(k, v);
|
|
361
|
-
});
|
|
362
|
-
res.end(Buffer.from(await response.arrayBuffer()));
|
|
363
426
|
}).listen(port, () => {
|
|
364
427
|
if (quiet) return;
|
|
365
428
|
const ip = getLocalIPv4();
|
|
@@ -442,8 +505,13 @@ var FonderieApp = class {
|
|
|
442
505
|
}
|
|
443
506
|
}
|
|
444
507
|
const ready = report.ok && dependencies;
|
|
508
|
+
const exposeDetails = process.env["NODE_ENV"] !== "production" || this.config.exposeReadyzDetails === true;
|
|
445
509
|
return Response.json(
|
|
446
|
-
{
|
|
510
|
+
{
|
|
511
|
+
status: ready ? "ready" : "not_ready",
|
|
512
|
+
dependencies,
|
|
513
|
+
...exposeDetails ? { problems: report.problems } : {}
|
|
514
|
+
},
|
|
447
515
|
{ status: ready ? 200 : 503 }
|
|
448
516
|
);
|
|
449
517
|
}
|