castrum 0.7.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -368
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +2763 -0
  5. package/dist/index.js +8707 -0
  6. package/docs/API.md +337 -0
  7. package/docs/ARCHITECTURE.md +507 -0
  8. package/docs/BENCHMARKS.md +343 -0
  9. package/docs/CASE_STUDY.md +240 -0
  10. package/docs/ENVIRONMENT.md +81 -0
  11. package/docs/FFI_BUN_GUIDE.md +327 -0
  12. package/docs/GETTING_STARTED.md +192 -0
  13. package/docs/INGRESS-ROUTER.md +81 -0
  14. package/docs/INGRESS.md +746 -0
  15. package/docs/NATIVE-ROUTE.md +53 -0
  16. package/docs/REPO_MAP.md +279 -0
  17. package/docs/adr/0001-two-wire-formats.md +39 -0
  18. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  19. package/docs/adr/0003-bun-builtins-delegation.md +43 -0
  20. package/docs/bun-builtins-decision-matrix.md +115 -0
  21. package/index.ts +44 -8
  22. package/package.json +98 -33
  23. package/src/ingress/body.ts +289 -0
  24. package/src/ingress/constants.ts +220 -48
  25. package/src/ingress/context.ts +136 -0
  26. package/src/ingress/decode/baked-result.ts +121 -0
  27. package/src/ingress/decode/fast-result.ts +160 -0
  28. package/src/ingress/decode/packed-sections.ts +56 -0
  29. package/src/ingress/decode/result-base.ts +172 -0
  30. package/src/ingress/errors.ts +72 -0
  31. package/src/ingress/fast.ts +152 -1021
  32. package/src/ingress/handlers.ts +639 -1001
  33. package/src/ingress/headers/baked-templates.ts +102 -0
  34. package/src/ingress/headers/cors.ts +44 -0
  35. package/src/ingress/headers/fast-templates.ts +238 -0
  36. package/src/ingress/headers/hsts.ts +83 -0
  37. package/src/ingress/health.ts +68 -0
  38. package/src/ingress/index.ts +142 -405
  39. package/src/ingress/metrics.ts +132 -0
  40. package/src/ingress/options.ts +197 -0
  41. package/src/ingress/packing/gather-raw-headers.ts +139 -0
  42. package/src/ingress/packing/header-packing.ts +75 -0
  43. package/src/ingress/packing/input-packer.ts +164 -0
  44. package/src/ingress/packing/scratch.ts +50 -0
  45. package/src/ingress/packing/select-headers.ts +102 -0
  46. package/src/ingress/response/error-bodies.ts +75 -0
  47. package/src/ingress/response/terminal.ts +59 -0
  48. package/src/ingress/router.ts +237 -0
  49. package/src/ingress/routes/common.ts +79 -0
  50. package/src/ingress/routes/delete.ts +14 -0
  51. package/src/ingress/routes/echo.ts +107 -0
  52. package/src/ingress/routes/fallback.ts +23 -0
  53. package/src/ingress/routes/head.ts +27 -0
  54. package/src/ingress/routes/index.ts +11 -0
  55. package/src/ingress/routes/json-write.ts +129 -0
  56. package/src/ingress/routes/options.ts +26 -0
  57. package/src/ingress/routes/read.ts +35 -0
  58. package/src/ingress/routes/responder.ts +159 -0
  59. package/src/ingress/server-node.ts +376 -0
  60. package/src/ingress/server.ts +475 -0
  61. package/src/ingress/shared.ts +132 -0
  62. package/src/ingress/status.ts +97 -0
  63. package/src/ingress/types.ts +191 -0
  64. package/src/integration/batch.ts +58 -0
  65. package/src/integration/index.ts +29 -0
  66. package/src/integration/pipeline.ts +220 -0
  67. package/src/integration/streaming.ts +78 -0
  68. package/src/integration/websocket.ts +93 -0
  69. package/src/loader/batch.ts +137 -0
  70. package/src/loader/cost.ts +105 -0
  71. package/src/loader/index.ts +745 -0
  72. package/src/loader/ops.ts +657 -0
  73. package/src/loader/state.ts +84 -0
  74. package/src/loader/types.ts +145 -0
  75. package/src/native/ffi/constants.ts +58 -0
  76. package/src/native/ffi/selftest.ts +729 -0
  77. package/src/native/ffi/types.ts +474 -0
  78. package/src/native/ffi.ts +1775 -0
  79. package/src/native/index.ts +8 -207
  80. package/src/native/loader.ts +240 -0
  81. package/src/native/types/instances.ts +274 -0
  82. package/src/native/types.ts +401 -0
  83. package/src/runtime/builtins.ts +131 -0
  84. package/src/runtime/codec.ts +147 -0
  85. package/src/runtime/detect.ts +36 -0
  86. package/src/runtime/index.ts +28 -0
  87. package/src/runtime/native.ts +35 -0
  88. package/src/runtime/server.ts +31 -0
  89. package/src/runtime/transport.ts +64 -0
  90. package/src/runtime/types.ts +173 -0
  91. package/src/runtime/uuid.ts +45 -0
  92. package/src/runtime/websocket.ts +30 -0
  93. package/src/rust-ffi/addon.ts +10 -0
  94. package/src/rust-ffi/batch/build.ts +155 -0
  95. package/src/rust-ffi/batch/index.ts +8 -0
  96. package/src/rust-ffi/batch/types.ts +92 -0
  97. package/src/rust-ffi/client.ts +87 -0
  98. package/src/rust-ffi/context.ts +193 -0
  99. package/src/rust-ffi/index.ts +51 -541
  100. package/src/rust-ffi/into.ts +21 -0
  101. package/src/rust-ffi/options.ts +113 -0
  102. package/src/rust-ffi/packed.ts +253 -0
  103. package/src/rust-ffi/proven.ts +28 -0
  104. package/src/rust-ffi/scalar/crypto.ts +346 -0
  105. package/src/rust-ffi/scalar/factories.ts +459 -0
  106. package/src/rust-ffi/scalar/hashing.ts +74 -0
  107. package/src/rust-ffi/scalar/http.ts +337 -0
  108. package/src/rust-ffi/scalar/index.ts +31 -0
  109. package/src/rust-ffi/scalar/interface.ts +325 -0
  110. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  111. package/src/rust-ffi/scalar/json.ts +61 -0
  112. package/src/rust-ffi/scalar/payload.ts +241 -0
  113. package/src/rust-ffi/text.ts +121 -0
  114. package/src/selection.json +194 -0
  115. package/src/selection.ts +73 -0
  116. package/src/shared/adaptive.ts +67 -0
  117. package/src/shared/buffer-pool.ts +264 -0
  118. package/src/shared/bytes.ts +108 -9
  119. package/src/shared/codec.ts +78 -0
  120. package/src/shared/env.ts +20 -0
  121. package/src/shared/log.ts +75 -0
  122. package/src/shared/metrics.ts +247 -0
  123. package/src/shared/packed/index.ts +10 -0
  124. package/src/shared/packed/parsers.ts +31 -0
  125. package/src/shared/packed/schema.ts +23 -0
  126. package/src/shared/packed/wire.ts +504 -0
  127. package/src/shared/proven.ts +289 -0
  128. package/src/shared/request-id.ts +67 -0
  129. package/src/shared/response.ts +104 -0
  130. package/src/shared/runtime.ts +29 -0
  131. package/src/shared/trace.ts +81 -0
  132. package/src/shared/uuid.ts +15 -0
  133. package/Cargo.toml +0 -73
  134. package/bench.ts +0 -3
  135. package/rust/batch.rs +0 -195
  136. package/rust/cookie_parser.rs +0 -70
  137. package/rust/cors.rs +0 -263
  138. package/rust/hashing.rs +0 -164
  139. package/rust/headers.rs +0 -234
  140. package/rust/hmac_sha256.rs +0 -113
  141. package/rust/http_parser.rs +0 -132
  142. package/rust/ingress.rs +0 -616
  143. package/rust/ingress_constants.rs +0 -89
  144. package/rust/ip_trust.rs +0 -185
  145. package/rust/json_ops.rs +0 -66
  146. package/rust/json_patch_ops.rs +0 -25
  147. package/rust/json_schema.rs +0 -151
  148. package/rust/json_ser.rs +0 -429
  149. package/rust/lib.rs +0 -48
  150. package/rust/method.rs +0 -141
  151. package/rust/mime_lookup.rs +0 -76
  152. package/rust/output.rs +0 -238
  153. package/rust/proxy.rs +0 -90
  154. package/rust/query_parser.rs +0 -141
  155. package/rust/random_token.rs +0 -52
  156. package/rust/rate_limit.rs +0 -203
  157. package/rust/terminal.rs +0 -95
  158. package/rust/unit_tests.rs +0 -843
  159. package/rust/url_codec.rs +0 -133
  160. package/rust/util.rs +0 -537
  161. package/rust/validation.rs +0 -107
  162. package/rust/websocket.rs +0 -23
  163. package/src/baseline/index.ts +0 -12
  164. package/src/baseline/tasks/cookie.ts +0 -22
  165. package/src/baseline/tasks/hashing.ts +0 -18
  166. package/src/baseline/tasks/hmac.ts +0 -38
  167. package/src/baseline/tasks/http.ts +0 -62
  168. package/src/baseline/tasks/json-patch.ts +0 -12
  169. package/src/baseline/tasks/json.ts +0 -50
  170. package/src/baseline/tasks/mime.ts +0 -5
  171. package/src/baseline/tasks/query.ts +0 -14
  172. package/src/baseline/tasks/token.ts +0 -15
  173. package/src/baseline/tasks/url.ts +0 -11
  174. package/src/baseline/tasks/validation.ts +0 -19
  175. package/src/baseline/tasks/websocket.ts +0 -14
  176. package/src/bench/assert.ts +0 -70
  177. package/src/bench/checks.ts +0 -227
  178. package/src/bench/checksum.ts +0 -27
  179. package/src/bench/comparisons.ts +0 -70
  180. package/src/bench/concurrent-worker.ts +0 -135
  181. package/src/bench/fixtures.ts +0 -233
  182. package/src/bench/index.ts +0 -2
  183. package/src/bench/measure.ts +0 -177
  184. package/src/bench/now.ts +0 -3
  185. package/src/bench/report.ts +0 -79
  186. package/src/bench/run.ts +0 -57
  187. package/src/bench/tasks/complex.ts +0 -232
  188. package/src/bench/tasks/concurrent.ts +0 -213
  189. package/src/bench/tasks/cookie.ts +0 -44
  190. package/src/bench/tasks/hashing.ts +0 -33
  191. package/src/bench/tasks/hmac.ts +0 -36
  192. package/src/bench/tasks/http.ts +0 -50
  193. package/src/bench/tasks/index.ts +0 -52
  194. package/src/bench/tasks/json-patch.ts +0 -21
  195. package/src/bench/tasks/json.ts +0 -33
  196. package/src/bench/tasks/mime.ts +0 -21
  197. package/src/bench/tasks/query.ts +0 -44
  198. package/src/bench/tasks/stress.ts +0 -142
  199. package/src/bench/tasks/token.ts +0 -20
  200. package/src/bench/tasks/url.ts +0 -34
  201. package/src/bench/tasks/validation.ts +0 -57
  202. package/src/bench/tasks/websocket.ts +0 -21
  203. package/src/bench/types.ts +0 -60
  204. package/src/data/json-rows.ts +0 -33
  205. package/src/ingress/packed-input.ts +0 -59
  206. package/src/shared/json.ts +0 -18
  207. package/src/shared/packed.ts +0 -316
@@ -1,457 +1,194 @@
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 { readRequestBodyOnce } from './body'
10
+ import { ERR_CODE_BODY_TOO_LARGE, ERR_CODE_REQUEST_TIMEOUT } from './constants'
11
+ import { internalContext, snapshotResult, syntheticContext } from './context'
12
+ import { createIngressFast } from './fast'
13
+ import { buildResponseContext } from './headers/fast-templates'
14
+ import { buildTerminalResponse } from './response/terminal'
15
+ import { assertSyncCallback, DEFAULT_BODY_TIMEOUT_MS, DEFAULT_MAX_BODY_BYTES } from './shared'
16
+ import type { IngressContext, IngressHandler, IngressOptions, SyncIngressHandler } from './types'
17
+
18
+ // Runtime-driven server factory: picks Bun.serve on Bun and the node:http
19
+ // adapter on Node via the runtime adapter (src/runtime/server.ts). This
20
+ // explicit re-export SHADOWS the Bun-only `createIngressServer` from
21
+ // './server' (re-exported via './handlers' above) so the public API serves
22
+ // one backend per runtime. `createIngressServerNode` remains exported for
23
+ // consumers who want the Node backend pinned.
24
+ export { createIngressServer } from '../runtime/index'
25
+ export { generateRequestId } from '../shared/request-id'
26
+ export type { TraceContext } from '../shared/trace'
27
+ // W3C trace context helpers (parse `traceparent` for log/hook correlation).
50
28
  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;
29
+ createSpanId,
30
+ createTraceId,
31
+ parseTraceParent,
32
+ serializeTraceParent,
33
+ } from '../shared/trace'
34
+ // ── Re-export the full public ingress API ─────────────────────────
35
+ export * from './constants'
36
+ export * from './errors'
37
+ export { createIngressFast, FastIngressResult } from './fast'
38
+ export * from './handlers'
39
+ export type { CorsOptions, CorsStaticStrings } from './headers/cors'
40
+ export type { HeaderTemplate, ResponseBuildContext } from './headers/fast-templates'
41
+ export { buildResponseContext, headersForResult } from './headers/fast-templates'
42
+ export type { SecurityHeadersOptions } from './headers/hsts'
43
+ export { healthHandler, livenessHandler, readinessHandler } from './health'
44
+ export type { IngressMetrics } from './metrics'
45
+ // ── Observability (zero-dep): metrics, health probes ─────────────
46
+ export { createIngressMetrics, metricsHandler } from './metrics'
47
+ export type { IngressFastHandler, IngressFastOptions } from './options'
48
+ export { buildTerminalResponse } from './response/terminal'
49
+ export type { CreateIngressRouterOptions, IngressRouter, RouterRouteSpec } from './router'
50
+ export { createIngressRouter } from './router'
51
+ export type { HeaderPlan } from './shared'
52
+ export { DEFAULT_BODY_TIMEOUT_MS, DEFAULT_MAX_BODY_BYTES, METHOD_KIND } from './shared'
53
+ export * from './status'
54
+ export type {
55
+ IngressContext,
56
+ IngressHandler,
57
+ IngressOptions,
58
+ NativeRequestContext,
59
+ NativeResponder,
60
+ SyncIngressHandler,
61
+ TerminalStyle,
62
+ } from './types'
101
63
 
102
- cookiesJson(): string;
103
- queryJson(): string;
104
- bodyJson(): Uint8Array;
105
- }
106
-
107
- export interface IngressContext extends IngressResult {
108
- response: Response | null;
109
- }
64
+ // ── Sync factory ─────────────────────────────────────────────────
110
65
 
111
66
  /**
112
- * Synchronous ingress handler.
67
+ * Synchronous convenience wrapper over `createIngressFast` (path 1).
113
68
  *
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).
69
+ * The callback runs on the fast-path result and MUST be synchronous. Use
70
+ * {@link createIngress} when you want automatic body reading + error contexts.
118
71
  */
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);
72
+ export function createIngressSync(options: IngressOptions = {}): SyncIngressHandler {
73
+ const fast = createIngressFast(options)
151
74
 
152
75
  return {
153
76
  run(req, ip, body, requestId, fn) {
154
77
  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
- });
78
+ const out = fn(result)
79
+ assertSyncCallback(out, 'createIngressSync().run()')
80
+ return out
81
+ })
167
82
  },
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
83
  }
218
-
219
- return concatUint8Arrays(chunks, total);
220
84
  }
221
85
 
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
- }
86
+ // ── Async factory ────────────────────────────────────────────────
236
87
 
237
- return out;
88
+ /**
89
+ * Return a copy of `options` without the security-header keys.
90
+ *
91
+ * `createIngress` applies security headers itself (via `buildResponseContext`),
92
+ * so they must not be forwarded to the underlying fast handler, which rejects
93
+ * them (see `createIngressFast`).
94
+ */
95
+ function stripSecurityOptions(options: IngressOptions): IngressOptions {
96
+ const copy = { ...options }
97
+ delete copy.security
98
+ delete copy.enableSecurityHeaders
99
+ return copy
238
100
  }
239
101
 
240
- // ── Async factory ────────────────────────────────────────────────
102
+ /**
103
+ * Async ingress factory: reads the request body (size guard + deadline) and
104
+ * returns a snapshot `IngressContext` with a terminal `Response`.
105
+ *
106
+ * Recommended default for serverless/edge-style `fetch` handlers. For maximum
107
+ * throughput use `createIngressFast` / `createIngressSync` directly.
108
+ */
241
109
  export function createIngress(options: IngressOptions = {}): IngressHandler {
242
- const sync = createIngressSync(options);
110
+ const sync = createIngressSync(stripSecurityOptions(options))
243
111
 
244
- const guard = options.enableBodySizeGuard !== false;
245
- const max = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
112
+ const guard = options.enableBodySizeGuard !== false
113
+ const max = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
114
+ const bodyTimeoutMs = options.bodyTimeoutMs ?? DEFAULT_BODY_TIMEOUT_MS
246
115
 
247
116
  const wantsBody =
248
117
  options.readBody === true ||
249
118
  options.requireJsonBody === true ||
250
119
  options.schema != null ||
251
- (options.readBody !== false && guard);
120
+ (options.readBody !== false && guard)
252
121
 
253
- const responseCtx = buildResponseContext(options);
122
+ const responseCtx = buildResponseContext(options)
254
123
 
255
- return async function ingressAsync(
256
- req: Request,
257
- ip?: string,
258
- ): Promise<IngressContext> {
259
- const requestId =
260
- options.enableRequestIds === false ? "" : decoder.decode(generateRequestId());
124
+ return async function ingressAsync(req: Request, ip?: string): Promise<IngressContext> {
125
+ const requestId = options.enableRequestIds === false ? '' : decoder.decode(generateRequestId())
126
+ options.onRequest?.(req, requestId, ip)
127
+
128
+ // Every terminal outcome (success, 413, 408, 500) flows through here so a
129
+ // hook consumer sees exactly one onResponse per request.
130
+ const emitResponse = (context: IngressContext): IngressContext => {
131
+ options.onResponse?.(req, context, context.status, requestId)
132
+ return context
133
+ }
261
134
 
262
135
  try {
263
136
  if (guard) {
264
- const rawLen = req.headers.get("content-length");
265
- const contentLength = Number(rawLen ?? "0");
137
+ const rawLen = req.headers.get('content-length')
138
+ const contentLength = Number(rawLen ?? '0')
266
139
 
267
140
  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
- );
141
+ return emitResponse(
142
+ syntheticContext(req, requestId, options, responseCtx, 413, ERR_CODE_BODY_TOO_LARGE),
143
+ )
276
144
  }
277
145
  }
278
146
 
279
- let body: Uint8Array | null = null;
147
+ let body: Uint8Array | null = null
280
148
 
281
149
  if (wantsBody && req.body !== null) {
282
150
  try {
283
- body = await readRequestBodyOnce(req, max, guard);
151
+ body = await readRequestBodyOnce(req, max, guard, bodyTimeoutMs)
284
152
  } 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
- );
153
+ const code = (err as { code?: string } | null)?.code
154
+
155
+ if (code === 'BODY_TOO_LARGE') {
156
+ return emitResponse(
157
+ syntheticContext(req, requestId, options, responseCtx, 413, ERR_CODE_BODY_TOO_LARGE),
158
+ )
159
+ }
160
+
161
+ if (code === 'REQUEST_TIMEOUT') {
162
+ // Match the pre-baked route factories: a body-read deadline is a
163
+ // 408, not a 500 (was silently swallowed as 500 before).
164
+ return emitResponse(
165
+ syntheticContext(req, requestId, options, responseCtx, 408, ERR_CODE_REQUEST_TIMEOUT),
166
+ )
294
167
  }
295
168
 
296
- throw err;
169
+ // Unknown body-read failure: surface to onError, then fall through
170
+ // to the internal-error context below.
171
+ options.onError?.(err instanceof Error ? err : new Error(String(err)))
172
+ throw err
297
173
  }
298
174
  }
299
175
 
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
- );
176
+ return emitResponse(
177
+ sync.run(req, ip, body, requestId, (r) => {
178
+ const snapshot = snapshotResult(r)
179
+ const response = buildTerminalResponse(responseCtx, snapshot, req, requestId)
308
180
 
309
- return {
310
- ...snapshot,
311
- response,
312
- };
313
- });
314
- } catch {
315
- return internalContext(req, requestId, options, responseCtx);
181
+ return {
182
+ ...snapshot,
183
+ response,
184
+ }
185
+ }),
186
+ )
187
+ } catch (err) {
188
+ // Native/other failures: never silent — report to onError and emit a
189
+ // structured 500 context so the request still completes observably.
190
+ options.onError?.(err instanceof Error ? err : new Error(String(err)))
191
+ return emitResponse(internalContext(req, requestId, options, responseCtx))
316
192
  }
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
193
  }
435
-
436
- if (list.includes("*")) {
437
- return cors.allowCredentials !== true;
438
- }
439
-
440
- return list.includes(origin);
441
194
  }
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 };