castrum 0.7.0 → 0.9.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.
Files changed (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -59
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +3161 -0
  5. package/dist/index.js +7306 -0
  6. package/docs/ARCHITECTURE.md +415 -0
  7. package/docs/BENCHMARKS.md +307 -0
  8. package/docs/ENVIRONMENT.md +79 -0
  9. package/docs/GETTING_STARTED.md +209 -0
  10. package/docs/INGRESS.md +745 -0
  11. package/docs/REPO_MAP.md +259 -0
  12. package/docs/adr/0001-two-wire-formats.md +39 -0
  13. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  14. package/docs/adr/0003-bun-builtins-delegation.md +42 -0
  15. package/docs/bun-builtins-decision-matrix.md +108 -0
  16. package/index.ts +51 -8
  17. package/package.json +73 -20
  18. package/src/data/json-rows.ts +13 -13
  19. package/src/ingress/body.ts +149 -0
  20. package/src/ingress/constants.ts +220 -48
  21. package/src/ingress/context.ts +136 -0
  22. package/src/ingress/decode/baked-result.ts +121 -0
  23. package/src/ingress/decode/fast-result.ts +160 -0
  24. package/src/ingress/decode/packed-sections.ts +56 -0
  25. package/src/ingress/decode/result-base.ts +172 -0
  26. package/src/ingress/errors.ts +72 -0
  27. package/src/ingress/fast.ts +152 -1021
  28. package/src/ingress/handlers.ts +563 -1001
  29. package/src/ingress/headers/baked-templates.ts +102 -0
  30. package/src/ingress/headers/cors.ts +44 -0
  31. package/src/ingress/headers/fast-templates.ts +238 -0
  32. package/src/ingress/headers/hsts.ts +83 -0
  33. package/src/ingress/health.ts +67 -0
  34. package/src/ingress/index.ts +130 -405
  35. package/src/ingress/metrics.ts +132 -0
  36. package/src/ingress/options.ts +197 -0
  37. package/src/ingress/packing/gather-raw-headers.ts +75 -0
  38. package/src/ingress/packing/header-packing.ts +75 -0
  39. package/src/ingress/packing/input-packer.ts +150 -0
  40. package/src/ingress/packing/scratch.ts +50 -0
  41. package/src/ingress/packing/select-headers.ts +96 -0
  42. package/src/ingress/response/error-bodies.ts +75 -0
  43. package/src/ingress/response/terminal.ts +59 -0
  44. package/src/ingress/routes/common.ts +38 -0
  45. package/src/ingress/routes/delete.ts +14 -0
  46. package/src/ingress/routes/echo.ts +107 -0
  47. package/src/ingress/routes/fallback.ts +23 -0
  48. package/src/ingress/routes/head.ts +37 -0
  49. package/src/ingress/routes/index.ts +10 -0
  50. package/src/ingress/routes/json-write.ts +153 -0
  51. package/src/ingress/routes/options.ts +36 -0
  52. package/src/ingress/routes/read.ts +45 -0
  53. package/src/ingress/server-node.ts +376 -0
  54. package/src/ingress/server.ts +443 -0
  55. package/src/ingress/shared.ts +132 -0
  56. package/src/ingress/status.ts +97 -0
  57. package/src/ingress/types.ts +157 -0
  58. package/src/integration/batch.ts +58 -0
  59. package/src/integration/index.ts +29 -0
  60. package/src/integration/pipeline.ts +217 -0
  61. package/src/integration/streaming.ts +79 -0
  62. package/src/integration/websocket.ts +92 -0
  63. package/src/loader/batch.ts +133 -0
  64. package/src/loader/cost.ts +104 -0
  65. package/src/loader/index.ts +930 -0
  66. package/src/loader/ops.ts +657 -0
  67. package/src/native/ffi.ts +1541 -0
  68. package/src/native/index.ts +8 -207
  69. package/src/native/loader.ts +194 -0
  70. package/src/native/types.ts +598 -0
  71. package/src/rust-ffi/addon.ts +10 -0
  72. package/src/rust-ffi/batch.ts +431 -0
  73. package/src/rust-ffi/client.ts +88 -0
  74. package/src/rust-ffi/context.ts +164 -0
  75. package/src/rust-ffi/index.ts +40 -541
  76. package/src/rust-ffi/options.ts +113 -0
  77. package/src/rust-ffi/packed.ts +253 -0
  78. package/src/rust-ffi/proven.ts +42 -0
  79. package/src/rust-ffi/scalar/crypto.ts +265 -0
  80. package/src/rust-ffi/scalar/factories.ts +233 -0
  81. package/src/rust-ffi/scalar/hashing.ts +72 -0
  82. package/src/rust-ffi/scalar/http.ts +206 -0
  83. package/src/rust-ffi/scalar/index.ts +31 -0
  84. package/src/rust-ffi/scalar/interface.ts +450 -0
  85. package/src/rust-ffi/scalar/json.ts +45 -0
  86. package/src/rust-ffi/scalar/payload.ts +124 -0
  87. package/src/rust-ffi/text.ts +95 -0
  88. package/src/selection.json +194 -0
  89. package/src/selection.ts +77 -0
  90. package/src/shared/adaptive.ts +68 -0
  91. package/src/shared/bench-classify.ts +209 -0
  92. package/src/shared/buffer-pool.ts +209 -0
  93. package/src/shared/bytes.ts +35 -5
  94. package/src/shared/env.ts +20 -0
  95. package/src/shared/log.ts +74 -0
  96. package/src/shared/metrics.ts +247 -0
  97. package/src/shared/packed.ts +350 -154
  98. package/src/shared/proven.ts +596 -0
  99. package/src/shared/request-id.ts +67 -0
  100. package/src/shared/response.ts +104 -0
  101. package/src/shared/runtime.ts +34 -0
  102. package/src/shared/trace.ts +81 -0
  103. package/src/shared/uuid.ts +31 -0
  104. package/Cargo.toml +0 -73
  105. package/bench.ts +0 -3
  106. package/rust/batch.rs +0 -195
  107. package/rust/cookie_parser.rs +0 -70
  108. package/rust/cors.rs +0 -263
  109. package/rust/hashing.rs +0 -164
  110. package/rust/headers.rs +0 -234
  111. package/rust/hmac_sha256.rs +0 -113
  112. package/rust/http_parser.rs +0 -132
  113. package/rust/ingress.rs +0 -616
  114. package/rust/ingress_constants.rs +0 -89
  115. package/rust/ip_trust.rs +0 -185
  116. package/rust/json_ops.rs +0 -66
  117. package/rust/json_patch_ops.rs +0 -25
  118. package/rust/json_schema.rs +0 -151
  119. package/rust/json_ser.rs +0 -429
  120. package/rust/lib.rs +0 -48
  121. package/rust/method.rs +0 -141
  122. package/rust/mime_lookup.rs +0 -76
  123. package/rust/output.rs +0 -238
  124. package/rust/proxy.rs +0 -90
  125. package/rust/query_parser.rs +0 -141
  126. package/rust/random_token.rs +0 -52
  127. package/rust/rate_limit.rs +0 -203
  128. package/rust/terminal.rs +0 -95
  129. package/rust/unit_tests.rs +0 -843
  130. package/rust/url_codec.rs +0 -133
  131. package/rust/util.rs +0 -537
  132. package/rust/validation.rs +0 -107
  133. package/rust/websocket.rs +0 -23
  134. package/src/baseline/index.ts +0 -12
  135. package/src/baseline/tasks/cookie.ts +0 -22
  136. package/src/baseline/tasks/hashing.ts +0 -18
  137. package/src/baseline/tasks/hmac.ts +0 -38
  138. package/src/baseline/tasks/http.ts +0 -62
  139. package/src/baseline/tasks/json-patch.ts +0 -12
  140. package/src/baseline/tasks/json.ts +0 -50
  141. package/src/baseline/tasks/mime.ts +0 -5
  142. package/src/baseline/tasks/query.ts +0 -14
  143. package/src/baseline/tasks/token.ts +0 -15
  144. package/src/baseline/tasks/url.ts +0 -11
  145. package/src/baseline/tasks/validation.ts +0 -19
  146. package/src/baseline/tasks/websocket.ts +0 -14
  147. package/src/bench/assert.ts +0 -70
  148. package/src/bench/checks.ts +0 -227
  149. package/src/bench/checksum.ts +0 -27
  150. package/src/bench/comparisons.ts +0 -70
  151. package/src/bench/concurrent-worker.ts +0 -135
  152. package/src/bench/fixtures.ts +0 -233
  153. package/src/bench/index.ts +0 -2
  154. package/src/bench/measure.ts +0 -177
  155. package/src/bench/now.ts +0 -3
  156. package/src/bench/report.ts +0 -79
  157. package/src/bench/run.ts +0 -57
  158. package/src/bench/tasks/complex.ts +0 -232
  159. package/src/bench/tasks/concurrent.ts +0 -213
  160. package/src/bench/tasks/cookie.ts +0 -44
  161. package/src/bench/tasks/hashing.ts +0 -33
  162. package/src/bench/tasks/hmac.ts +0 -36
  163. package/src/bench/tasks/http.ts +0 -50
  164. package/src/bench/tasks/index.ts +0 -52
  165. package/src/bench/tasks/json-patch.ts +0 -21
  166. package/src/bench/tasks/json.ts +0 -33
  167. package/src/bench/tasks/mime.ts +0 -21
  168. package/src/bench/tasks/query.ts +0 -44
  169. package/src/bench/tasks/stress.ts +0 -142
  170. package/src/bench/tasks/token.ts +0 -20
  171. package/src/bench/tasks/url.ts +0 -34
  172. package/src/bench/tasks/validation.ts +0 -57
  173. package/src/bench/tasks/websocket.ts +0 -21
  174. package/src/bench/types.ts +0 -60
  175. package/src/ingress/packed-input.ts +0 -59
  176. package/src/shared/json.ts +0 -18
@@ -1,457 +1,182 @@
1
- // src/ingress/index.ts — Public ingress API
1
+ // src/ingress/index.ts — Public ingress API (barrel + sync/async factories)
2
2
  //
3
3
  // All constants come from Rust via src/ingress/constants.ts.
4
4
  // For maximum performance, use createIngressFast() from ./fast.ts directly.
5
5
  // This module wraps createIngressFast with convenience async API (createIngress).
6
6
 
7
- import addon from "../native";
8
- import { decoder } from "../shared/bytes";
9
- import {
10
- OUT_VERDICT, OUT_ERROR_CODE, OUT_STATUS, OUT_FLAGS,
11
- OUT_RATE_LIMIT, OUT_RATE_REMAINING, OUT_RATE_RESET, OUT_RETRY_AFTER,
12
- OUT_COOKIES_JSON_LEN, OUT_QUERY_JSON_LEN, OUT_HEADER_VARIANT, OUT_BODY_JSON_LEN,
13
- OUT_DATA_START,
14
- FLAG_HAS_COOKIES, FLAG_HAS_QUERY,
15
- FLAG_BODY_VALID_JSON, FLAG_SCHEMA_VALID,
16
- FLAG_CORS_ALLOWED, FLAG_IS_PREFLIGHT, FLAG_RATE_LIMITED,
17
- FLAG_HTTPS, FLAG_TRUSTED_PROXY, FLAG_BODY_TRUNCATED,
18
- HV_JSON, HV_CORS_SIMPLE, HV_CORS_PREFLIGHT, HV_RATE_ACTIVE, HV_RATE_LIMITED,
19
- HV_COUNT,
20
- ERR_CODE_NONE, ERR_CODE_CORS_PREFLIGHT, ERR_CODE_RATE_LIMITED,
21
- ERR_CODE_BODY_TOO_LARGE, ERR_CODE_INVALID_JSON, ERR_CODE_SCHEMA_VALIDATION,
22
- ERR_CODE_BAD_REQUEST, ERR_CODE_REQUEST_TOO_LARGE, ERR_CODE_INTERNAL,
23
- } from "./constants";
24
- import {
25
- createIngressFast,
26
- FastIngressResult,
27
- buildResponseContext,
28
- buildTerminalResponse,
29
- headersForResult,
30
- safeTerminalStatus,
31
- statusForErrorCode,
32
- isValidResponseStatus,
33
- normalizeResponseStatus,
34
- errorCodeName,
35
- errorMessage,
36
- type IngressFastOptions,
37
- type IngressFastHandler,
38
- type ResponseBuildContext,
39
- type HeaderTemplate,
40
- type HeaderPlan,
41
- type CorsStaticStrings,
42
- type SecurityHeadersOptions,
43
- type CorsOptions,
44
- generateRequestId,
45
- METHOD_KIND,
46
- DEFAULT_MAX_BODY_BYTES,
47
- } from "./fast";
48
-
49
- // ── Re-export types from constants ──────────────────────────────
7
+ import { decoder } from '../shared/bytes'
8
+ import { generateRequestId } from '../shared/request-id'
9
+ import { assertSyncCallback, DEFAULT_MAX_BODY_BYTES, DEFAULT_BODY_TIMEOUT_MS } from './shared'
10
+ import { createIngressFast } from './fast'
11
+ import { buildResponseContext } from './headers/fast-templates'
12
+ import { buildTerminalResponse } from './response/terminal'
13
+ import { snapshotResult, syntheticContext, internalContext } from './context'
14
+ import { readRequestBodyOnce } from './body'
15
+ import { ERR_CODE_BODY_TOO_LARGE, ERR_CODE_REQUEST_TIMEOUT } from './constants'
16
+ import type { IngressOptions, IngressContext, SyncIngressHandler, IngressHandler } from './types'
17
+
18
+ // ── Re-export the full public ingress API ─────────────────────────
19
+ export * from './constants'
20
+ export * from './status'
21
+ export * from './errors'
22
+ export * from './handlers'
23
+ export { createIngressFast, FastIngressResult } from './fast'
24
+ export { buildTerminalResponse } from './response/terminal'
25
+ export { buildResponseContext, headersForResult } from './headers/fast-templates'
26
+ export { generateRequestId } from '../shared/request-id'
27
+ export { METHOD_KIND, DEFAULT_MAX_BODY_BYTES, DEFAULT_BODY_TIMEOUT_MS } from './shared'
28
+ export type { HeaderPlan } from './shared'
29
+ export type { ResponseBuildContext, HeaderTemplate } from './headers/fast-templates'
30
+ export type { CorsStaticStrings, CorsOptions } from './headers/cors'
31
+ export type { SecurityHeadersOptions } from './headers/hsts'
32
+ export type { IngressFastHandler, IngressFastOptions } from './options'
33
+ // ── Observability (zero-dep): metrics, health probes ─────────────
34
+ export { createIngressMetrics, metricsHandler } from './metrics'
35
+ export type { IngressMetrics } from './metrics'
36
+ export { livenessHandler, readinessHandler, healthHandler } from './health'
37
+ // W3C trace context helpers (parse `traceparent` for log/hook correlation).
50
38
  export {
51
- OUT_VERDICT, OUT_ERROR_CODE, OUT_STATUS, OUT_FLAGS,
52
- OUT_RATE_LIMIT, OUT_RATE_REMAINING, OUT_RATE_RESET, OUT_RETRY_AFTER,
53
- OUT_COOKIES_JSON_LEN, OUT_QUERY_JSON_LEN, OUT_HEADER_VARIANT, OUT_BODY_JSON_LEN,
54
- OUT_DATA_START,
55
- FLAG_HAS_COOKIES, FLAG_HAS_QUERY,
56
- FLAG_BODY_VALID_JSON, FLAG_SCHEMA_VALID,
57
- FLAG_CORS_ALLOWED, FLAG_IS_PREFLIGHT, FLAG_RATE_LIMITED,
58
- FLAG_HTTPS, FLAG_TRUSTED_PROXY, FLAG_BODY_TRUNCATED,
59
- HV_JSON, HV_CORS_SIMPLE, HV_CORS_PREFLIGHT, HV_RATE_ACTIVE, HV_RATE_LIMITED,
60
- HV_COUNT,
61
- ERR_CODE_NONE, ERR_CODE_CORS_PREFLIGHT, ERR_CODE_RATE_LIMITED,
62
- ERR_CODE_BODY_TOO_LARGE, ERR_CODE_INVALID_JSON, ERR_CODE_SCHEMA_VALIDATION,
63
- ERR_CODE_BAD_REQUEST, ERR_CODE_REQUEST_TOO_LARGE, ERR_CODE_INTERNAL,
64
- };
65
-
66
- // ── Public option types ──────────────────────────────────────────
67
- export type { SecurityHeadersOptions, CorsOptions };
68
-
69
- export interface IngressOptions extends IngressFastOptions {
70
- enableRequestIds?: boolean;
71
- }
72
-
73
- // ── Result types ─────────────────────────────────────────────────
74
- // Note: the exported `FastIngressResult` class (from fast.ts) is the zero-alloc
75
- // implementation; this `IngressResult` interface is the readonly structural view
76
- // used by the public sync/async handler types.
77
- interface IngressResult {
78
- readonly status: number;
79
- readonly verdict: number;
80
- readonly flags: number;
81
- readonly errorCode: number;
82
- readonly terminal: boolean;
83
- readonly ok: boolean;
84
- readonly https: boolean;
85
- readonly trustedProxy: boolean;
86
- readonly hasCookies: boolean;
87
- readonly hasQuery: boolean;
88
- readonly bodyValidJson: boolean;
89
- readonly schemaValid: boolean;
90
- readonly corsAllowed: boolean;
91
- readonly isPreflight: boolean;
92
- readonly rateLimited: boolean;
93
- readonly rateLimit: number;
94
- readonly rateRemaining: number;
95
- readonly rateResetMs: number;
96
- readonly retryAfterMs: number;
97
- readonly body: Uint8Array;
98
- readonly headerVariant: number;
99
- readonly requestId: string;
100
- readonly bodyTruncated: boolean;
39
+ parseTraceParent,
40
+ createTraceId,
41
+ createSpanId,
42
+ serializeTraceParent,
43
+ } from '../shared/trace'
44
+ export type { TraceContext } from '../shared/trace'
45
+ export type {
46
+ IngressOptions,
47
+ IngressContext,
48
+ SyncIngressHandler,
49
+ IngressHandler,
50
+ } from './types'
101
51
 
102
- cookiesJson(): string;
103
- queryJson(): string;
104
- bodyJson(): Uint8Array;
105
- }
106
-
107
- export interface IngressContext extends IngressResult {
108
- response: Response | null;
109
- }
52
+ // ── Sync factory ─────────────────────────────────────────────────
110
53
 
111
54
  /**
112
- * Synchronous ingress handler.
55
+ * Synchronous convenience wrapper over `createIngressFast` (path 1).
113
56
  *
114
- * The callback runs on the zero-alloc result object and MUST be synchronous:
115
- * returning a Promise (or any thenable) throws. The result is invalidated after
116
- * `run()` returns, so capture the fields you need inside the callback (or use
117
- * the async {@link createIngress} which snapshots for you).
57
+ * The callback runs on the fast-path result and MUST be synchronous. Use
58
+ * {@link createIngress} when you want automatic body reading + error contexts.
118
59
  */
119
- export interface SyncIngressHandler {
120
- run<T>(
121
- req: Request,
122
- ip: string | undefined,
123
- body: Uint8Array | null,
124
- requestId: string,
125
- fn: (result: IngressResult) => T,
126
- ): T;
127
- }
128
-
129
- export interface IngressHandler {
130
- (req: Request, ip?: string): Promise<IngressContext>;
131
- }
132
-
133
- // ── HTTP status helpers (re-exported from fast.ts) ──────────────
134
- export { isValidResponseStatus, statusForErrorCode, normalizeResponseStatus, safeTerminalStatus };
135
-
136
- // ── Header template system (re-exported from fast.ts) ────────────
137
- export { buildResponseContext, headersForResult };
138
- export type { ResponseBuildContext, HeaderTemplate, CorsStaticStrings };
139
-
140
- // ── Pre-baked handler functions (route factories + Bun.serve builder) ──
141
- export * from "./handlers";
142
-
143
- // ── Constants ────────────────────────────────────────────────────
144
- const EMPTY_BODY = new Uint8Array(0);
145
-
146
- // ── Sync factory ─────────────────────────────────────────────────
147
- export function createIngressSync(
148
- options: IngressOptions = {},
149
- ): SyncIngressHandler {
150
- const fast = createIngressFast(options);
60
+ export function createIngressSync(options: IngressOptions = {}): SyncIngressHandler {
61
+ const fast = createIngressFast(options)
151
62
 
152
63
  return {
153
64
  run(req, ip, body, requestId, fn) {
154
65
  return fast.run(req, ip, body, requestId, (result) => {
155
- const out = fn(result);
156
-
157
- if (
158
- out !== null &&
159
- (typeof out === "object" || typeof out === "function") &&
160
- typeof (out as any).then === "function"
161
- ) {
162
- throw new Error("createIngressSync().run() callback must be synchronous.");
163
- }
164
-
165
- return out;
166
- });
66
+ const out = fn(result)
67
+ assertSyncCallback(out, 'createIngressSync().run()')
68
+ return out
69
+ })
167
70
  },
168
- };
169
- }
170
-
171
- // ── Body reading helpers (used by async API) ────────────────────
172
- function readRequestBodyOnce(
173
- req: Request,
174
- maxBytes: number,
175
- guard: boolean,
176
- ): Promise<Uint8Array> {
177
- if (req.body === null) {
178
- return Promise.resolve(EMPTY_BODY);
179
- }
180
-
181
- return readBodyWithLimit(req, maxBytes, guard);
182
- }
183
-
184
- async function readBodyWithLimit(
185
- req: Request,
186
- maxBytes: number,
187
- guard: boolean,
188
- ): Promise<Uint8Array> {
189
- const body = req.body;
190
-
191
- if (!body) {
192
- return EMPTY_BODY;
193
- }
194
-
195
- const reader = body.getReader();
196
- const chunks: Uint8Array[] = [];
197
- let total = 0;
198
-
199
- for (;;) {
200
- const { done, value } = await reader.read();
201
-
202
- if (done) break;
203
- if (!value) continue;
204
-
205
- total += value.byteLength;
206
-
207
- if (guard && total > maxBytes) {
208
- await reader.cancel().catch(() => {});
209
-
210
- const err = new Error("BODY_TOO_LARGE");
211
- (err as any).code = "BODY_TOO_LARGE";
212
-
213
- throw err;
214
- }
215
-
216
- chunks.push(value);
217
71
  }
218
-
219
- return concatUint8Arrays(chunks, total);
220
72
  }
221
73
 
222
- function concatUint8Arrays(
223
- chunks: Uint8Array[],
224
- total: number,
225
- ): Uint8Array {
226
- if (chunks.length === 0) return EMPTY_BODY;
227
- if (chunks.length === 1) return chunks[0]!;
228
-
229
- const out = new Uint8Array(total);
230
- let offset = 0;
231
-
232
- for (const chunk of chunks) {
233
- out.set(chunk, offset);
234
- offset += chunk.byteLength;
235
- }
74
+ // ── Async factory ────────────────────────────────────────────────
236
75
 
237
- return out;
76
+ /**
77
+ * Return a copy of `options` without the security-header keys.
78
+ *
79
+ * `createIngress` applies security headers itself (via `buildResponseContext`),
80
+ * so they must not be forwarded to the underlying fast handler, which rejects
81
+ * them (see `createIngressFast`).
82
+ */
83
+ function stripSecurityOptions(options: IngressOptions): IngressOptions {
84
+ const copy = { ...options }
85
+ delete copy.security
86
+ delete copy.enableSecurityHeaders
87
+ return copy
238
88
  }
239
89
 
240
- // ── Async factory ────────────────────────────────────────────────
90
+ /**
91
+ * Async ingress factory: reads the request body (size guard + deadline) and
92
+ * returns a snapshot `IngressContext` with a terminal `Response`.
93
+ *
94
+ * Recommended default for serverless/edge-style `fetch` handlers. For maximum
95
+ * throughput use `createIngressFast` / `createIngressSync` directly.
96
+ */
241
97
  export function createIngress(options: IngressOptions = {}): IngressHandler {
242
- const sync = createIngressSync(options);
98
+ const sync = createIngressSync(stripSecurityOptions(options))
243
99
 
244
- const guard = options.enableBodySizeGuard !== false;
245
- const max = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
100
+ const guard = options.enableBodySizeGuard !== false
101
+ const max = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
102
+ const bodyTimeoutMs = options.bodyTimeoutMs ?? DEFAULT_BODY_TIMEOUT_MS
246
103
 
247
104
  const wantsBody =
248
105
  options.readBody === true ||
249
106
  options.requireJsonBody === true ||
250
107
  options.schema != null ||
251
- (options.readBody !== false && guard);
108
+ (options.readBody !== false && guard)
252
109
 
253
- const responseCtx = buildResponseContext(options);
110
+ const responseCtx = buildResponseContext(options)
254
111
 
255
- return async function ingressAsync(
256
- req: Request,
257
- ip?: string,
258
- ): Promise<IngressContext> {
259
- const requestId =
260
- options.enableRequestIds === false ? "" : decoder.decode(generateRequestId());
112
+ return async function ingressAsync(req: Request, ip?: string): Promise<IngressContext> {
113
+ const requestId = options.enableRequestIds === false ? '' : decoder.decode(generateRequestId())
114
+ options.onRequest?.(req, requestId, ip)
115
+
116
+ // Every terminal outcome (success, 413, 408, 500) flows through here so a
117
+ // hook consumer sees exactly one onResponse per request.
118
+ const emitResponse = (context: IngressContext): IngressContext => {
119
+ options.onResponse?.(req, context, context.status, requestId)
120
+ return context
121
+ }
261
122
 
262
123
  try {
263
124
  if (guard) {
264
- const rawLen = req.headers.get("content-length");
265
- const contentLength = Number(rawLen ?? "0");
125
+ const rawLen = req.headers.get('content-length')
126
+ const contentLength = Number(rawLen ?? '0')
266
127
 
267
128
  if (Number.isFinite(contentLength) && contentLength > max) {
268
- return syntheticContext(
269
- req,
270
- requestId,
271
- options,
272
- responseCtx,
273
- 413,
274
- ERR_CODE_BODY_TOO_LARGE,
275
- );
129
+ return emitResponse(
130
+ syntheticContext(req, requestId, options, responseCtx, 413, ERR_CODE_BODY_TOO_LARGE),
131
+ )
276
132
  }
277
133
  }
278
134
 
279
- let body: Uint8Array | null = null;
135
+ let body: Uint8Array | null = null
280
136
 
281
137
  if (wantsBody && req.body !== null) {
282
138
  try {
283
- body = await readRequestBodyOnce(req, max, guard);
139
+ body = await readRequestBodyOnce(req, max, guard, bodyTimeoutMs)
284
140
  } catch (err) {
285
- if ((err as any)?.code === "BODY_TOO_LARGE") {
286
- return syntheticContext(
287
- req,
288
- requestId,
289
- options,
290
- responseCtx,
291
- 413,
292
- ERR_CODE_BODY_TOO_LARGE,
293
- );
141
+ const code = (err as { code?: string } | null)?.code
142
+
143
+ if (code === 'BODY_TOO_LARGE') {
144
+ return emitResponse(
145
+ syntheticContext(req, requestId, options, responseCtx, 413, ERR_CODE_BODY_TOO_LARGE),
146
+ )
147
+ }
148
+
149
+ if (code === 'REQUEST_TIMEOUT') {
150
+ // Match the pre-baked route factories: a body-read deadline is a
151
+ // 408, not a 500 (was silently swallowed as 500 before).
152
+ return emitResponse(
153
+ syntheticContext(req, requestId, options, responseCtx, 408, ERR_CODE_REQUEST_TIMEOUT),
154
+ )
294
155
  }
295
156
 
296
- throw err;
157
+ // Unknown body-read failure: surface to onError, then fall through
158
+ // to the internal-error context below.
159
+ options.onError?.(err instanceof Error ? err : new Error(String(err)))
160
+ throw err
297
161
  }
298
162
  }
299
163
 
300
- return sync.run(req, ip, body, requestId, (r) => {
301
- const snapshot = snapshotResult(r);
302
- const response = buildTerminalResponse(
303
- responseCtx,
304
- snapshot,
305
- req,
306
- requestId,
307
- );
164
+ return emitResponse(
165
+ sync.run(req, ip, body, requestId, (r) => {
166
+ const snapshot = snapshotResult(r)
167
+ const response = buildTerminalResponse(responseCtx, snapshot, req, requestId)
308
168
 
309
- return {
310
- ...snapshot,
311
- response,
312
- };
313
- });
314
- } catch {
315
- return internalContext(req, requestId, options, responseCtx);
169
+ return {
170
+ ...snapshot,
171
+ response,
172
+ }
173
+ }),
174
+ )
175
+ } catch (err) {
176
+ // Native/other failures: never silent — report to onError and emit a
177
+ // structured 500 context so the request still completes observably.
178
+ options.onError?.(err instanceof Error ? err : new Error(String(err)))
179
+ return emitResponse(internalContext(req, requestId, options, responseCtx))
316
180
  }
317
- };
318
- }
319
-
320
- // ── Result helpers ───────────────────────────────────────────────
321
- function snapshotResult(r: IngressResult): IngressResult {
322
- const cookies = r.cookiesJson();
323
- const query = r.queryJson();
324
- const bodyJson = r.bodyJson().slice();
325
- const body = r.body;
326
-
327
- return {
328
- status: r.status,
329
- verdict: r.verdict,
330
- flags: r.flags,
331
- errorCode: r.errorCode,
332
- terminal: r.terminal,
333
- ok: r.ok,
334
- https: r.https,
335
- trustedProxy: r.trustedProxy,
336
- hasCookies: r.hasCookies,
337
- hasQuery: r.hasQuery,
338
- bodyValidJson: r.bodyValidJson,
339
- schemaValid: r.schemaValid,
340
- corsAllowed: r.corsAllowed,
341
- isPreflight: r.isPreflight,
342
- rateLimited: r.rateLimited,
343
- rateLimit: r.rateLimit,
344
- rateRemaining: r.rateRemaining,
345
- rateResetMs: r.rateResetMs,
346
- retryAfterMs: r.retryAfterMs,
347
- body,
348
- headerVariant: r.headerVariant,
349
- requestId: r.requestId,
350
- bodyTruncated: r.bodyTruncated,
351
-
352
- cookiesJson: () => cookies,
353
- queryJson: () => query,
354
- bodyJson: () => bodyJson,
355
- };
356
- }
357
-
358
- function syntheticContext(
359
- req: Request,
360
- requestId: string,
361
- options: IngressOptions,
362
- responseCtx: ResponseBuildContext,
363
- status: number,
364
- errorCode: number,
365
- ): IngressContext {
366
- const corsAllowed = staticCorsAllowed(options, req);
367
- const variant = HV_JSON | (corsAllowed ? HV_CORS_SIMPLE : 0);
368
-
369
- const base: IngressResult = {
370
- status,
371
- verdict: 1,
372
- flags: 0,
373
- errorCode,
374
- terminal: true,
375
- ok: false,
376
- https: options.https === true,
377
- trustedProxy:
378
- options.trustProxy === true ||
379
- options.trustedProxies?.enabled === true,
380
- hasCookies: false,
381
- hasQuery: false,
382
- bodyValidJson: false,
383
- schemaValid: false,
384
- corsAllowed,
385
- isPreflight: false,
386
- rateLimited: errorCode === ERR_CODE_RATE_LIMITED,
387
- rateLimit: options.rateLimit?.limit ?? 0,
388
- rateRemaining: 0,
389
- rateResetMs: 0,
390
- retryAfterMs: 0,
391
- body: EMPTY_BODY,
392
- headerVariant: variant,
393
- requestId,
394
- bodyTruncated: false,
395
-
396
- cookiesJson: () => "{}",
397
- queryJson: () => "{}",
398
- bodyJson: () => EMPTY_BODY,
399
- };
400
-
401
- return {
402
- ...base,
403
- response: buildTerminalResponse(responseCtx, base, req, requestId),
404
- };
405
- }
406
-
407
- function internalContext(
408
- req: Request,
409
- requestId: string,
410
- options: IngressOptions,
411
- responseCtx: ResponseBuildContext,
412
- ): IngressContext {
413
- return syntheticContext(
414
- req,
415
- requestId,
416
- options,
417
- responseCtx,
418
- 500,
419
- ERR_CODE_INTERNAL,
420
- );
421
- }
422
-
423
- function staticCorsAllowed(options: IngressOptions, req: Request): boolean {
424
- const cors = options.cors;
425
- if (!cors) return false;
426
-
427
- const origin = req.headers.get("origin");
428
- if (!origin) return false;
429
-
430
- const list = cors.allowOrigin;
431
-
432
- if (!list || list.length === 0) {
433
- return cors.allowCredentials !== true;
434
181
  }
435
-
436
- if (list.includes("*")) {
437
- return cors.allowCredentials !== true;
438
- }
439
-
440
- return list.includes(origin);
441
182
  }
442
-
443
- // ── Re-export fast.ts API for direct use ─────────────────────────
444
- // Error helpers (errorCodeName/errorMessage/buildTerminalResponse), METHOD_KIND
445
- // and DEFAULT_MAX_BODY_BYTES were previously internal-only; exposing them makes
446
- // the numeric verdict/errorCode contract usable without reaching into internals.
447
- export {
448
- createIngressFast,
449
- FastIngressResult,
450
- errorCodeName,
451
- errorMessage,
452
- buildTerminalResponse,
453
- generateRequestId,
454
- METHOD_KIND,
455
- DEFAULT_MAX_BODY_BYTES,
456
- };
457
- export type { IngressFastHandler, IngressFastOptions, HeaderPlan };