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,973 +1,102 @@
1
- // src/ingress/fast.ts — High-performance ingress handler for public API
1
+ // src/ingress/fast.ts — High-performance ingress handler (fast packed-input
2
+ // path).
2
3
  //
3
- // Merges the optimizations from bench/servers/ingress-server.ts into the public API.
4
- // Key improvements over old createIngressSync:
5
- // - Counter-based request ID (no crypto.randomUUID)
6
- // - Pre-encoded header name buffers
7
- // - Reusable output buffer
8
- // - Zero-alloc result parsing with lazy decode
9
- // - All constants come from Rust (src/ingress/constants.ts)
10
-
11
- import addon from "../native";
12
- import type { IngressInstance } from "../native";
13
- import { IngressInputPacker } from "./packed-input";
4
+ // Thin factory module. The fast-path pipeline is decomposed into task-focused
5
+ // submodules:
6
+ // - packing/header-packing.ts binary header packing (zero intermediate strings)
7
+ // - packing/input-packer.ts packed-input buffer builder
8
+ // - decode/fast-result.ts FastIngressResult (zero-alloc lazy decode)
9
+ // - headers/fast-templates.ts header-template system
10
+ // - response/terminal.ts buildTerminalResponse
11
+ // - status.ts / errors.ts / options.ts / shared.ts
12
+ //
13
+ // This module keeps `createIngressFast` and re-exports the full fast-path API
14
+ // so existing importers keep working.
15
+
16
+ import { getAddon } from '../native'
17
+ import { getBunFFI } from '../native/ffi'
18
+ import { OUT_DATA_START } from './constants'
19
+ import { FastIngressResult } from './decode/fast-result'
20
+ import type { IngressFastHandler, IngressFastOptions } from './options'
14
21
  import {
15
- OUT_VERDICT, OUT_ERROR_CODE, OUT_STATUS, OUT_FLAGS,
16
- OUT_RATE_LIMIT, OUT_RATE_REMAINING, OUT_RATE_RESET, OUT_RETRY_AFTER,
17
- OUT_COOKIES_JSON_LEN, OUT_QUERY_JSON_LEN, OUT_HEADER_VARIANT, OUT_BODY_JSON_LEN,
18
- OUT_DATA_START,
19
- FLAG_HAS_COOKIES, FLAG_HAS_QUERY,
20
- FLAG_BODY_VALID_JSON, FLAG_SCHEMA_VALID,
21
- FLAG_CORS_ALLOWED, FLAG_IS_PREFLIGHT, FLAG_RATE_LIMITED,
22
- FLAG_HTTPS, FLAG_TRUSTED_PROXY, FLAG_BODY_TRUNCATED,
23
- HV_JSON, HV_CORS_SIMPLE, HV_CORS_PREFLIGHT, HV_RATE_ACTIVE, HV_RATE_LIMITED,
24
- HV_COUNT,
25
- ERR_CODE_NONE, ERR_CODE_CORS_PREFLIGHT, ERR_CODE_RATE_LIMITED,
26
- ERR_CODE_BODY_TOO_LARGE, ERR_CODE_INVALID_JSON, ERR_CODE_SCHEMA_VALIDATION,
27
- ERR_CODE_BAD_REQUEST, ERR_CODE_REQUEST_TOO_LARGE, ERR_CODE_INTERNAL,
28
- } from "./constants";
29
-
30
- // ── Constants ──────────────────────────────────────────────────
31
- export const DEFAULT_MAX_BODY_BYTES = 1_048_576;
32
- // Internal-only default for the reusable output buffer.
33
- const DEFAULT_OUTPUT_BUFFER_SIZE = 262_144;
34
-
35
- const encoder = new TextEncoder();
36
- const decoder = new TextDecoder();
37
- const EMPTY_BODY = new Uint8Array(0);
38
- const EMPTY_IP_BYTES = encoder.encode("0.0.0.0");
39
-
40
- // ── Method mapping ─────────────────────────────────────────────
41
- export const METHOD_KIND: Record<string, number> = {
42
- GET: 0,
43
- HEAD: 1,
44
- POST: 2,
45
- PUT: 3,
46
- PATCH: 4,
47
- DELETE: 5,
48
- OPTIONS: 6,
49
- };
50
-
51
- // ── Header name constants (pre-encoded) ────────────────────────
52
- const HDR_COOKIE = encoder.encode("cookie");
53
- const HDR_ORIGIN = encoder.encode("origin");
54
- const HDR_ACRM = encoder.encode("access-control-request-method");
55
- const HDR_ACRH = encoder.encode("access-control-request-headers");
56
- const HDR_XFF = encoder.encode("x-forwarded-for");
57
- const HDR_XRI = encoder.encode("x-real-ip");
58
- const HDR_XFP = encoder.encode("x-forwarded-proto");
59
-
60
- // ── Header plan ────────────────────────────────────────────────
61
- export interface HeaderPlan {
62
- cookie: boolean;
63
- cors: boolean;
64
- proxy: boolean;
65
- proto: boolean;
66
- }
67
-
68
- // ── Fast request ID generator (counter-based, no crypto) ──────
69
- const REQ_ID_HEX_LOOKUP = new Uint8Array(256 * 2);
70
- for (let i = 0; i < 256; i++) {
71
- REQ_ID_HEX_LOOKUP[i * 2] = "0123456789abcdef".charCodeAt(i >> 4);
72
- REQ_ID_HEX_LOOKUP[i * 2 + 1] = "0123456789abcdef".charCodeAt(i & 0x0f);
73
- }
74
-
75
- const REQ_ID_BINARY = new Uint8Array(8);
76
- const REQ_ID_HEX = new Uint8Array(16);
77
-
78
- const WORKER_ID = Number(process.env.INGRESS_WORKER_ID || 0) & 0xffff;
79
- const BOOT_RANDOM = (typeof crypto !== "undefined"
80
- ? (crypto as any).getRandomValues?.(new Uint32Array(1))?.[0] ?? ((Math.random() * 0xffffffff) >>> 0)
81
- : (Math.random() * 0xffffffff) >>> 0) as number;
82
-
83
- const REQ_ID_COUNTER = {
84
- hi: (BOOT_RANDOM ^ (WORKER_ID << 16)) >>> 0,
85
- lo: 0,
86
- };
87
-
88
- export function generateRequestId(): Uint8Array {
89
- if (REQ_ID_COUNTER.lo === 0xffffffff) {
90
- REQ_ID_COUNTER.lo = 0;
91
- REQ_ID_COUNTER.hi = (REQ_ID_COUNTER.hi + 1) >>> 0;
92
- } else {
93
- REQ_ID_COUNTER.lo++;
94
- }
95
-
96
- REQ_ID_BINARY[0] = (REQ_ID_COUNTER.hi >>> 24) & 0xff;
97
- REQ_ID_BINARY[1] = (REQ_ID_COUNTER.hi >>> 16) & 0xff;
98
- REQ_ID_BINARY[2] = (REQ_ID_COUNTER.hi >>> 8) & 0xff;
99
- REQ_ID_BINARY[3] = REQ_ID_COUNTER.hi & 0xff;
100
- REQ_ID_BINARY[4] = (REQ_ID_COUNTER.lo >>> 24) & 0xff;
101
- REQ_ID_BINARY[5] = (REQ_ID_COUNTER.lo >>> 16) & 0xff;
102
- REQ_ID_BINARY[6] = (REQ_ID_COUNTER.lo >>> 8) & 0xff;
103
- REQ_ID_BINARY[7] = REQ_ID_COUNTER.lo & 0xff;
104
-
105
- for (let i = 0; i < 8; i++) {
106
- const b = REQ_ID_BINARY[i]!;
107
- REQ_ID_HEX[i * 2] = REQ_ID_HEX_LOOKUP[b * 2]!;
108
- REQ_ID_HEX[i * 2 + 1] = REQ_ID_HEX_LOOKUP[b * 2 + 1]!;
109
- }
110
-
111
- return REQ_ID_HEX;
112
- }
113
-
114
- // ── Header packing (directly into Uint8Array, no intermediate strings) ──
115
- const HEADER_BUF_SIZE = 8192;
116
-
117
- // Thread-local header buffer for per-call isolation
118
- const [getHeaderBuf] = (() => {
119
- const tls = new Array<[Uint8Array, DataView]>();
120
- const MAX_CACHED = 256;
121
- let tlsIdx = 0;
122
-
123
- function acquire(): [Uint8Array, DataView] {
124
- const cached = tls[tlsIdx];
125
- tlsIdx = (tlsIdx + 1) % MAX_CACHED;
126
- if (cached) return cached;
127
-
128
- const buf = new Uint8Array(HEADER_BUF_SIZE);
129
- const view = new DataView(buf.buffer);
130
- const pair: [Uint8Array, DataView] = [buf, view];
131
- tls.push(pair);
132
- return pair;
133
- }
134
-
135
- return [acquire];
136
- })();
137
-
138
- function writeHeaderPair(
139
- buf: Uint8Array,
140
- view: DataView,
141
- pos: number,
142
- name: Uint8Array,
143
- value: string,
144
- ): number {
145
- const needed = 2 + name.length + 4 + value.length * 3;
146
-
147
- if (pos + needed > buf.length) {
148
- const next = new Uint8Array(Math.max(buf.length * 2, pos + needed));
149
- next.set(buf.subarray(0, pos));
150
- buf = next;
151
- view = new DataView(buf.buffer);
152
- }
153
-
154
- view.setUint16(pos, name.length, true);
155
- buf.set(name, pos + 2);
156
- pos += 2 + name.length;
157
-
158
- const valueLenPos = pos;
159
- pos += 4;
160
-
161
- const dest = buf.subarray(pos);
162
- const { written } = encoder.encodeInto(value, dest);
163
-
164
- view.setUint32(valueLenPos, written, true);
165
- pos += written;
166
-
167
- return pos;
168
- }
169
-
170
- function packHeaders(req: Request, plan: HeaderPlan): Uint8Array {
171
- const [buf, view] = getHeaderBuf();
172
- let pos = 2;
173
- let count = 0;
174
-
175
- const headers = req.headers;
176
-
177
- if (plan.cookie) {
178
- const v = headers.get("cookie");
179
- if (v !== null) {
180
- pos = writeHeaderPair(buf, view, pos, HDR_COOKIE, v);
181
- count++;
182
- }
183
- }
184
-
185
- if (plan.cors) {
186
- const origin = headers.get("origin");
187
- if (origin !== null) {
188
- pos = writeHeaderPair(buf, view, pos, HDR_ORIGIN, origin);
189
- count++;
190
- }
191
-
192
- if (req.method === "OPTIONS") {
193
- const acrm = headers.get("access-control-request-method");
194
- if (acrm !== null) {
195
- pos = writeHeaderPair(buf, view, pos, HDR_ACRM, acrm);
196
- count++;
197
- }
198
-
199
- const acrh = headers.get("access-control-request-headers");
200
- if (acrh !== null) {
201
- pos = writeHeaderPair(buf, view, pos, HDR_ACRH, acrh);
202
- count++;
203
- }
204
- }
205
- }
206
-
207
- if (plan.proxy) {
208
- const xff = headers.get("x-forwarded-for");
209
- if (xff !== null) {
210
- pos = writeHeaderPair(buf, view, pos, HDR_XFF, xff);
211
- count++;
212
- }
213
-
214
- const xri = headers.get("x-real-ip");
215
- if (xri !== null) {
216
- pos = writeHeaderPair(buf, view, pos, HDR_XRI, xri);
217
- count++;
218
- }
219
- }
220
-
221
- if (plan.proto) {
222
- const xfp = headers.get("x-forwarded-proto");
223
- if (xfp !== null) {
224
- pos = writeHeaderPair(buf, view, pos, HDR_XFP, xfp);
225
- count++;
226
- }
227
- }
228
-
229
- view.setUint16(0, count, true);
230
- return buf.subarray(0, pos);
231
- }
232
-
233
- // ── Fast result (lazy decode, minimal allocations) ──────────────
234
- export class FastIngressResult {
235
- status = 500;
236
- verdict = 1;
237
- flags = 0;
238
- errorCode = ERR_CODE_INTERNAL;
239
- terminal = true;
240
- ok = false;
241
- https = false;
242
- trustedProxy = false;
243
- hasCookies = false;
244
- hasQuery = false;
245
- bodyValidJson = false;
246
- schemaValid = false;
247
- corsAllowed = false;
248
- isPreflight = false;
249
- rateLimited = false;
250
- rateLimit = 0;
251
- rateRemaining = 0;
252
- rateResetMs = 0;
253
- retryAfterMs = 0;
254
- body: Uint8Array = EMPTY_BODY;
255
- headerVariant = 0;
256
- requestId = "";
257
- bodyTruncated = false;
258
-
259
- // Lazy-decoded cookie/query JSON
260
- private _buf: Uint8Array = EMPTY_BODY;
261
- private _bodyJsonStart = OUT_DATA_START;
262
- private _bodyJsonLen = 0;
263
- private _cookiesBuf: Uint8Array | null = null;
264
- private _queryBuf: Uint8Array | null = null;
265
- private _cookiesDecoded: string | null = null;
266
- private _queryDecoded: string | null = null;
267
-
268
- refresh(buf: Uint8Array, body: Uint8Array, requestId: string): void {
269
- this._buf = buf;
270
- this.body = body;
271
- this.requestId = requestId;
272
-
273
- const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
274
-
275
- const rawVerdict = dv.getUint8(OUT_VERDICT);
276
- const rawErrorCode = dv.getUint8(OUT_ERROR_CODE);
277
- const rawStatus = dv.getUint16(OUT_STATUS, true);
278
- const flags = dv.getUint32(OUT_FLAGS, true);
279
-
280
- const rateLimit = dv.getUint32(OUT_RATE_LIMIT, true);
281
- const rateRemaining = dv.getUint32(OUT_RATE_REMAINING, true);
282
-
283
- const cookiesJsonLen = dv.getUint32(OUT_COOKIES_JSON_LEN, true);
284
- const queryJsonLen = dv.getUint32(OUT_QUERY_JSON_LEN, true);
285
-
286
- const headerVariant = dv.getUint8(OUT_HEADER_VARIANT);
287
- const bodyJsonLen = dv.getUint32(OUT_BODY_JSON_LEN, true);
288
-
289
- const preflightAllowed =
290
- (flags & FLAG_IS_PREFLIGHT) !== 0 && (flags & FLAG_CORS_ALLOWED) !== 0;
291
-
292
- const normalizedStatus = normalizeResponseStatus(
293
- rawStatus,
294
- rawErrorCode,
295
- preflightAllowed,
296
- );
297
-
298
- const uninitialized =
299
- rawVerdict === 0 && rawErrorCode === 0 && rawStatus === 0 && flags === 0;
300
-
301
- if (uninitialized) {
302
- this.status = 500;
303
- this.verdict = 1;
304
- this.errorCode = ERR_CODE_INTERNAL;
305
- } else {
306
- this.status = normalizedStatus;
307
- this.verdict = rawVerdict;
308
- this.errorCode = rawErrorCode;
309
- }
310
-
311
- this.flags = flags;
312
- this.rateLimit = rateLimit;
313
- this.rateRemaining = rateRemaining;
314
-
315
- if (rateLimit > 0 || (flags & FLAG_RATE_LIMITED) !== 0) {
316
- this.rateResetMs = Number(dv.getBigUint64(OUT_RATE_RESET, true));
317
- this.retryAfterMs = Number(dv.getBigUint64(OUT_RETRY_AFTER, true));
318
- } else {
319
- this.rateResetMs = 0;
320
- this.retryAfterMs = 0;
321
- }
322
-
323
- this.headerVariant = headerVariant;
324
- this._bodyJsonLen = bodyJsonLen;
325
-
326
- this.terminal = this.verdict !== 0 || this.status >= 400;
327
- this.ok = this.verdict === 0 && this.status >= 200 && this.status < 400;
328
-
329
- this.https = (flags & FLAG_HTTPS) !== 0;
330
- this.trustedProxy = (flags & FLAG_TRUSTED_PROXY) !== 0;
331
- this.hasCookies = (flags & FLAG_HAS_COOKIES) !== 0;
332
- this.hasQuery = (flags & FLAG_HAS_QUERY) !== 0;
333
- this.bodyValidJson = (flags & FLAG_BODY_VALID_JSON) !== 0;
334
- this.schemaValid = (flags & FLAG_SCHEMA_VALID) !== 0;
335
- this.corsAllowed = (flags & FLAG_CORS_ALLOWED) !== 0;
336
- this.isPreflight = (flags & FLAG_IS_PREFLIGHT) !== 0;
337
- this.rateLimited = (flags & FLAG_RATE_LIMITED) !== 0;
338
-
339
- const safeCookiesLen =
340
- cookiesJsonLen > 0 && OUT_DATA_START + cookiesJsonLen <= buf.byteLength
341
- ? cookiesJsonLen
342
- : 0;
343
-
344
- const queryStart = OUT_DATA_START + safeCookiesLen;
345
-
346
- const safeQueryLen =
347
- queryJsonLen > 0 && queryStart + queryJsonLen <= buf.byteLength
348
- ? queryJsonLen
349
- : 0;
350
-
351
- // Store slices for lazy decode instead of eagerly decoding strings
352
- this._cookiesBuf = safeCookiesLen > 0
353
- ? buf.subarray(OUT_DATA_START, OUT_DATA_START + safeCookiesLen)
354
- : null;
355
- this._cookiesDecoded = null;
356
-
357
- this._queryBuf = safeQueryLen > 0
358
- ? buf.subarray(queryStart, queryStart + safeQueryLen)
359
- : null;
360
- this._queryDecoded = null;
361
-
362
- this._bodyJsonStart = queryStart + safeQueryLen;
363
-
364
- this.bodyTruncated =
365
- (flags & FLAG_BODY_TRUNCATED) !== 0 ||
366
- safeCookiesLen !== cookiesJsonLen ||
367
- safeQueryLen !== queryJsonLen;
368
- }
369
-
370
- invalidate(): void {
371
- this.status = 500;
372
- this.verdict = 1;
373
- this.flags = 0;
374
- this.errorCode = ERR_CODE_INTERNAL;
375
- this.terminal = true;
376
- this.ok = false;
377
- this.https = false;
378
- this.trustedProxy = false;
379
- this.hasCookies = false;
380
- this.hasQuery = false;
381
- this.bodyValidJson = false;
382
- this.schemaValid = false;
383
- this.corsAllowed = false;
384
- this.isPreflight = false;
385
- this.rateLimited = false;
386
- this.rateLimit = 0;
387
- this.rateRemaining = 0;
388
- this.rateResetMs = 0;
389
- this.retryAfterMs = 0;
390
- this.body = EMPTY_BODY;
391
- this.headerVariant = 0;
392
- this.requestId = "";
393
- this.bodyTruncated = false;
394
-
395
- this._buf = EMPTY_BODY;
396
- this._bodyJsonLen = 0;
397
- this._bodyJsonStart = OUT_DATA_START;
398
- this._cookiesBuf = null;
399
- this._queryBuf = null;
400
- this._cookiesDecoded = null;
401
- this._queryDecoded = null;
402
- }
403
-
404
- setInternalError(requestId: string): void {
405
- this.invalidate();
406
- this.requestId = requestId;
407
- }
408
-
409
- cookiesJson(): string {
410
- if (this._cookiesDecoded !== null) return this._cookiesDecoded;
411
- if (this._cookiesBuf !== null) {
412
- this._cookiesDecoded = decoder.decode(this._cookiesBuf);
413
- return this._cookiesDecoded;
414
- }
415
- return "{}";
416
- }
417
-
418
- queryJson(): string {
419
- if (this._queryDecoded !== null) return this._queryDecoded;
420
- if (this._queryBuf !== null) {
421
- this._queryDecoded = decoder.decode(this._queryBuf);
422
- return this._queryDecoded;
423
- }
424
- return "{}";
425
- }
426
-
427
- bodyJson(): Uint8Array {
428
- if (this._bodyJsonLen === 0) return EMPTY_BODY;
429
-
430
- const end = this._bodyJsonStart + this._bodyJsonLen;
431
-
432
- if (end > this._buf.byteLength) {
433
- return EMPTY_BODY;
434
- }
435
-
436
- return this._buf.subarray(this._bodyJsonStart, end);
437
- }
438
- }
439
-
440
- // ── Status helpers ──────────────────────────────────────────
441
- export function isValidResponseStatus(status: number): boolean {
442
- return status === 101 || (status >= 200 && status <= 599);
443
- }
444
-
445
- export function statusForErrorCode(
446
- errorCode: number,
447
- isPreflightAllowed: boolean,
448
- ): number {
449
- if (isPreflightAllowed) return 204;
450
-
451
- switch (errorCode) {
452
- case ERR_CODE_CORS_PREFLIGHT: return 403;
453
- case ERR_CODE_RATE_LIMITED: return 429;
454
- case ERR_CODE_BODY_TOO_LARGE: return 413;
455
- case ERR_CODE_INVALID_JSON: return 400;
456
- case ERR_CODE_SCHEMA_VALIDATION: return 422;
457
- case ERR_CODE_BAD_REQUEST: return 400;
458
- case ERR_CODE_REQUEST_TOO_LARGE: return 431;
459
- case ERR_CODE_INTERNAL: return 500;
460
- default: return 500;
461
- }
462
- }
463
-
464
- export function normalizeResponseStatus(
465
- status: number,
466
- errorCode: number,
467
- isPreflightAllowed: boolean,
468
- ): number {
469
- if (isValidResponseStatus(status)) {
470
- return status;
471
- }
472
- return statusForErrorCode(errorCode, isPreflightAllowed);
473
- }
474
-
475
- export function safeTerminalStatus(
476
- r: {
477
- readonly status: number;
478
- readonly errorCode: number;
479
- readonly isPreflight: boolean;
480
- readonly corsAllowed: boolean;
481
- },
482
- ): number {
483
- const preflightAllowed = r.isPreflight && r.corsAllowed;
484
- if (preflightAllowed) return 204;
485
-
486
- const s = normalizeResponseStatus(r.status, r.errorCode, false);
487
-
488
- if (s >= 400) return s;
489
-
490
- if (r.errorCode !== ERR_CODE_NONE) {
491
- return statusForErrorCode(r.errorCode, false);
492
- }
493
-
494
- return 500;
495
- }
496
-
497
- // ── Header template system ─────────────────────────────────
498
- export interface HeaderTemplate {
499
- readonly entries: ReadonlyArray<readonly [string, string]>;
500
- readonly needsOriginEcho: boolean;
501
- readonly needsRateDynamic: boolean;
502
- readonly needsRetryAfter: boolean;
503
- readonly needsDynamicHsts: boolean;
504
- }
505
-
506
- export interface CorsStaticStrings {
507
- readonly allowMethodsJoined: string;
508
- readonly allowHeadersJoined: string;
509
- readonly exposeHeadersJoined: string;
510
- readonly maxAgeString: string | null;
511
- readonly isWildcard: boolean;
512
- readonly credentials: boolean;
513
- }
514
-
515
- export interface ResponseBuildContext {
516
- readonly templates: HeaderTemplate[];
517
- readonly corsStatic: CorsStaticStrings | null;
518
- readonly hstsValue: string | null;
519
- }
520
-
521
- export interface SecurityHeadersOptions {
522
- contentSecurityPolicy?: string;
523
- hsts?: boolean;
524
- hstsMaxAge?: number;
525
- hstsIncludeSubdomains?: boolean;
526
- hstsPreload?: boolean;
527
- frameOptions?: string;
528
- nosniff?: boolean;
529
- referrerPolicy?: string;
530
- coep?: string;
531
- coop?: string;
532
- corp?: string;
533
- xssProtection?: string;
534
- }
535
-
536
- export interface CorsOptions {
537
- allowOrigin?: string[];
538
- allowMethods?: string[];
539
- allowHeaders?: string[];
540
- exposeHeaders?: string[];
541
- allowCredentials?: boolean;
542
- maxAge?: number;
543
- }
544
-
545
- function buildCorsStaticStrings(
546
- cors: CorsOptions | undefined,
547
- ): CorsStaticStrings | null {
548
- if (!cors) return null;
549
-
550
- const isWildcard =
551
- !cors.allowOrigin ||
552
- cors.allowOrigin.length === 0 ||
553
- cors.allowOrigin.includes("*");
554
-
555
- return {
556
- allowMethodsJoined: cors.allowMethods?.length
557
- ? cors.allowMethods.join(", ")
558
- : "GET, HEAD, POST",
559
- allowHeadersJoined: cors.allowHeaders?.length
560
- ? cors.allowHeaders.join(", ")
561
- : "",
562
- exposeHeadersJoined: cors.exposeHeaders?.length
563
- ? cors.exposeHeaders.join(", ")
564
- : "",
565
- maxAgeString: typeof cors.maxAge === "number" ? String(cors.maxAge) : null,
566
- isWildcard,
567
- credentials: cors.allowCredentials === true,
568
- };
569
- }
570
-
571
- function buildHstsValue(sec: SecurityHeadersOptions): string | null {
572
- const wantHsts =
573
- sec.hsts === true ||
574
- sec.hstsMaxAge !== undefined ||
575
- sec.hstsIncludeSubdomains === true ||
576
- sec.hstsPreload === true;
577
-
578
- if (!wantHsts) return null;
579
-
580
- const maxAge = sec.hstsMaxAge ?? 31_536_000;
581
- let value = `max-age=${maxAge}`;
582
-
583
- if (sec.hstsIncludeSubdomains) value += "; includeSubDomains";
584
- if (sec.hstsPreload) value += "; preload";
585
-
586
- return value;
587
- }
588
-
589
- export function buildResponseContext(
590
- options: {
591
- cors?: CorsOptions;
592
- security?: SecurityHeadersOptions;
593
- https?: boolean;
594
- enableSecurityHeaders?: boolean;
595
- rateLimit?: { limit?: number };
596
- },
597
- ): ResponseBuildContext {
598
- const templates = buildHeaderTemplates(options);
599
- const corsStatic = buildCorsStaticStrings(options.cors);
600
- const hstsValue = buildHstsValue(options.security ?? {});
601
-
602
- return { templates, corsStatic, hstsValue };
603
- }
604
-
605
- function buildHeaderTemplates(
606
- options: {
607
- cors?: CorsOptions;
608
- security?: SecurityHeadersOptions;
609
- https?: boolean;
610
- enableSecurityHeaders?: boolean;
611
- rateLimit?: { limit?: number };
612
- },
613
- ): HeaderTemplate[] {
614
- const templates: HeaderTemplate[] = new Array(HV_COUNT);
615
-
616
- const sec = options.security ?? {};
617
- const securityEnabled = options.enableSecurityHeaders !== false;
618
- const corsStatic = buildCorsStaticStrings(options.cors);
619
- const hstsValue = buildHstsValue(sec);
620
- const hstsIsDynamic = options.https === undefined && hstsValue !== null;
621
-
622
- const securityPairs: Array<[string, string]> = [];
623
-
624
- if (securityEnabled) {
625
- if (sec.nosniff !== false) {
626
- securityPairs.push(["x-content-type-options", "nosniff"]);
627
- }
628
-
629
- securityPairs.push(["x-frame-options", sec.frameOptions ?? "DENY"]);
630
- securityPairs.push(["referrer-policy", sec.referrerPolicy ?? "no-referrer"]);
631
-
632
- if (sec.xssProtection) {
633
- securityPairs.push(["x-xss-protection", sec.xssProtection]);
634
- }
635
-
636
- if (sec.contentSecurityPolicy) {
637
- securityPairs.push(["content-security-policy", sec.contentSecurityPolicy]);
638
- }
639
-
640
- if (sec.coep) {
641
- securityPairs.push(["cross-origin-embedder-policy", sec.coep]);
642
- }
643
-
644
- if (sec.coop) {
645
- securityPairs.push(["cross-origin-opener-policy", sec.coop]);
646
- }
647
-
648
- if (sec.corp) {
649
- securityPairs.push(["cross-origin-resource-policy", sec.corp]);
650
- }
651
-
652
- if (hstsValue !== null && options.https === true) {
653
- securityPairs.push(["strict-transport-security", hstsValue]);
654
- }
655
- }
656
-
657
- const corsVaryPairs: Array<[string, string]> = [];
658
-
659
- if (corsStatic) {
660
- corsVaryPairs.push(["vary", "Origin"]);
661
-
662
- if (!corsStatic.isWildcard) {
663
- corsVaryPairs.push(["vary", "Access-Control-Request-Method"]);
664
- corsVaryPairs.push(["vary", "Access-Control-Request-Headers"]);
665
- }
666
- }
667
-
668
- const corsPolicyPairs: Array<[string, string]> = [];
669
-
670
- if (corsStatic) {
671
- if (corsStatic.credentials) {
672
- corsPolicyPairs.push(["access-control-allow-credentials", "true"]);
673
- }
674
-
675
- if (corsStatic.exposeHeadersJoined) {
676
- corsPolicyPairs.push([
677
- "access-control-expose-headers",
678
- corsStatic.exposeHeadersJoined,
679
- ]);
680
- }
681
- }
682
-
683
- const corsPreflightPairs: Array<[string, string]> = [];
684
-
685
- if (corsStatic) {
686
- corsPreflightPairs.push([
687
- "access-control-allow-methods",
688
- corsStatic.allowMethodsJoined,
689
- ]);
690
-
691
- if (corsStatic.allowHeadersJoined) {
692
- corsPreflightPairs.push([
693
- "access-control-allow-headers",
694
- corsStatic.allowHeadersJoined,
695
- ]);
696
- }
697
-
698
- if (corsStatic.maxAgeString) {
699
- corsPreflightPairs.push(["access-control-max-age", corsStatic.maxAgeString]);
700
- }
701
- }
702
-
703
- const rateLimitCeiling: Array<[string, string]> = [];
704
-
705
- if (options.rateLimit?.limit && options.rateLimit.limit > 0) {
706
- rateLimitCeiling.push(["x-ratelimit-limit", String(options.rateLimit.limit)]);
707
- }
708
-
709
- for (let variant = 0; variant < HV_COUNT; variant++) {
710
- const isCorsSimple = (variant & HV_CORS_SIMPLE) !== 0;
711
- const isCorsPreflight = (variant & HV_CORS_PREFLIGHT) !== 0;
712
- const isRateActive = (variant & HV_RATE_ACTIVE) !== 0;
713
- const isRateLimited = (variant & HV_RATE_LIMITED) !== 0;
714
-
715
- const entries: Array<[string, string]> = [];
716
-
717
- if (securityEnabled) {
718
- for (let i = 0; i < securityPairs.length; i++) {
719
- entries.push(securityPairs[i]!);
720
- }
721
- }
722
-
723
- for (let i = 0; i < corsVaryPairs.length; i++) {
724
- entries.push(corsVaryPairs[i]!);
725
- }
726
-
727
- if (isCorsSimple || isCorsPreflight) {
728
- for (let i = 0; i < corsPolicyPairs.length; i++) {
729
- entries.push(corsPolicyPairs[i]!);
730
- }
731
- }
732
-
733
- if (isCorsPreflight) {
734
- for (let i = 0; i < corsPreflightPairs.length; i++) {
735
- entries.push(corsPreflightPairs[i]!);
736
- }
737
- }
738
-
739
- if (isRateActive) {
740
- for (let i = 0; i < rateLimitCeiling.length; i++) {
741
- entries.push(rateLimitCeiling[i]!);
742
- }
743
- }
744
-
745
- templates[variant] = {
746
- entries,
747
- needsOriginEcho: isCorsSimple || isCorsPreflight,
748
- needsRateDynamic: isRateActive,
749
- needsRetryAfter: isRateLimited,
750
- needsDynamicHsts: hstsIsDynamic,
751
- };
752
- }
753
-
754
- return templates;
755
- }
756
-
757
- export function headersForResult(
758
- ctx: ResponseBuildContext,
759
- r: {
760
- readonly headerVariant: number;
761
- readonly corsAllowed: boolean;
762
- readonly rateRemaining: number;
763
- readonly rateResetMs: number;
764
- readonly retryAfterMs: number;
765
- readonly https: boolean;
766
- },
767
- req: Request,
768
- requestId: string,
769
- ): Headers {
770
- const template =
771
- ctx.templates[r.headerVariant & 0x1f] ?? ctx.templates[0]!;
772
-
773
- const headers = new Headers();
774
-
775
- for (const [name, value] of template.entries) {
776
- headers.append(name, value);
777
- }
778
-
779
- if (requestId) {
780
- headers.set("x-request-id", requestId);
781
- }
782
-
783
- if (template.needsOriginEcho && r.corsAllowed && ctx.corsStatic) {
784
- const origin = req.headers.get("origin");
785
-
786
- if (origin) {
787
- if (ctx.corsStatic.isWildcard && !ctx.corsStatic.credentials) {
788
- headers.set("access-control-allow-origin", "*");
789
- } else {
790
- headers.set("access-control-allow-origin", origin);
791
- }
792
- }
793
- }
794
-
795
- if (template.needsRateDynamic) {
796
- headers.set("x-ratelimit-remaining", String(Math.max(0, r.rateRemaining)));
797
-
798
- if (r.rateResetMs > 0) {
799
- headers.set("x-ratelimit-reset", String(Math.ceil(r.rateResetMs / 1000)));
800
- }
801
- }
802
-
803
- if (template.needsRetryAfter && r.retryAfterMs > 0) {
804
- headers.set(
805
- "retry-after",
806
- String(Math.max(1, Math.ceil(r.retryAfterMs / 1000))),
807
- );
808
- }
809
-
810
- if (template.needsDynamicHsts && r.https && ctx.hstsValue) {
811
- headers.set("strict-transport-security", ctx.hstsValue);
812
- }
813
-
814
- return headers;
815
- }
816
-
817
- // ── Error code helpers ─────────────────────────────────
818
- export function errorCodeName(code: number): string {
819
- switch (code) {
820
- case ERR_CODE_NONE: return "none";
821
- case ERR_CODE_CORS_PREFLIGHT: return "cors_preflight";
822
- case ERR_CODE_RATE_LIMITED: return "rate_limited";
823
- case ERR_CODE_BODY_TOO_LARGE: return "body_too_large";
824
- case ERR_CODE_INVALID_JSON: return "invalid_json";
825
- case ERR_CODE_SCHEMA_VALIDATION: return "schema_validation";
826
- case ERR_CODE_BAD_REQUEST: return "bad_request";
827
- case ERR_CODE_REQUEST_TOO_LARGE: return "request_too_large";
828
- case ERR_CODE_INTERNAL: return "internal";
829
- default: return "unknown";
830
- }
831
- }
832
-
833
- export function errorMessage(status: number, code: number): string {
834
- switch (code) {
835
- case ERR_CODE_CORS_PREFLIGHT: return "CORS preflight rejected";
836
- case ERR_CODE_RATE_LIMITED: return "Too many requests";
837
- case ERR_CODE_BODY_TOO_LARGE: return "Request body too large";
838
- case ERR_CODE_INVALID_JSON: return "Invalid JSON body";
839
- case ERR_CODE_SCHEMA_VALIDATION: return "JSON schema validation failed";
840
- case ERR_CODE_BAD_REQUEST: return "Bad request";
841
- case ERR_CODE_REQUEST_TOO_LARGE: return "Request too large";
842
- case ERR_CODE_INTERNAL: return "Internal server error";
843
- default: return status >= 500 ? "Internal server error" : "Request rejected";
844
- }
845
- }
846
-
847
- export function buildTerminalResponse(
848
- ctx: ResponseBuildContext,
849
- r: {
850
- readonly terminal: boolean;
851
- readonly isPreflight: boolean;
852
- readonly corsAllowed: boolean;
853
- readonly errorCode: number;
854
- readonly status: number;
855
- readonly headerVariant: number;
856
- readonly https: boolean;
857
- readonly rateLimit: number;
858
- readonly rateRemaining: number;
859
- readonly rateResetMs: number;
860
- readonly retryAfterMs: number;
861
- },
862
- req: Request,
863
- requestId: string,
864
- ): Response | null {
865
- if (!r.terminal) return null;
866
-
867
- const preflightAllowed = r.isPreflight && r.corsAllowed;
868
- const headers = headersForResult(ctx, r, req, requestId);
869
-
870
- if (preflightAllowed) {
871
- return new Response(null, { status: 204, headers });
872
- }
873
-
874
- const status = safeTerminalStatus(r);
875
-
876
- headers.set("content-type", "application/json; charset=utf-8");
877
- headers.set("cache-control", "no-store");
878
-
879
- const payload = JSON.stringify({
880
- error: {
881
- code: errorCodeName(r.errorCode),
882
- status,
883
- message: errorMessage(status, r.errorCode),
884
- requestId: requestId || undefined,
885
- },
886
- });
887
-
888
- return new Response(payload, { status, headers });
889
- }
890
-
891
- // ── Ingress options ────────────────────────────────────
892
- export interface IngressFastOptions {
893
- trustProxy?: boolean;
894
- trustedProxies?: { enabled?: boolean; networks?: string[] };
895
- parseCookies?: boolean;
896
- parseQuery?: boolean;
897
- requireJsonBody?: boolean;
898
- schema?: Uint8Array;
899
- cors?: CorsOptions;
900
- rateLimit?: { limit?: number; windowMs?: number; maxEntries?: number };
901
- security?: SecurityHeadersOptions;
902
- https?: boolean;
903
- maxBodyBytes?: number;
904
- enableSecurityHeaders?: boolean;
905
- enableRequestIds?: boolean;
906
- enableBodySizeGuard?: boolean;
907
- emitMetadataJson?: boolean;
908
- readBody?: boolean;
909
- outputBufferSize?: number;
910
- limits?: {
911
- maxUrlBytes?: number;
912
- maxQueryBytes?: number;
913
- maxCookieBytes?: number;
914
- maxHeadersBytes?: number;
915
- maxHeaders?: number;
916
- maxPairs?: number;
917
- };
918
- }
919
-
920
- export interface IngressFastHandler {
921
- run<T>(
922
- req: Request,
923
- ip: string | undefined,
924
- body: Uint8Array | null,
925
- requestId: string,
926
- fn: (result: FastIngressResult) => T,
927
- ): T;
928
- }
22
+ assertIngressOptionValues,
23
+ assertKnownIngressOptions,
24
+ warnTrustProxyDeprecated,
25
+ } from './options'
26
+ import { packHeaders } from './packing/header-packing'
27
+ import { IngressInputPacker } from './packing/input-packer'
28
+ import {
29
+ assertSyncCallback,
30
+ buildHeaderPlan,
31
+ clampIngressBufferSize,
32
+ DEFAULT_FAST_OUTPUT_BUFFER_SIZE,
33
+ DEFAULT_MAX_BODY_BYTES,
34
+ type HeaderPlan,
35
+ METHOD_KIND,
36
+ METHOD_KIND_UNKNOWN,
37
+ } from './shared'
38
+
39
+ export { generateRequestId } from '../shared/request-id'
40
+ export { FastIngressResult } from './decode/fast-result'
41
+ export { errorCodeName, errorMessage } from './errors'
42
+ export type { CorsOptions, CorsStaticStrings } from './headers/cors'
43
+ export type {
44
+ HeaderTemplate,
45
+ ResponseBuildContext,
46
+ } from './headers/fast-templates'
47
+ export {
48
+ buildResponseContext,
49
+ headersForResult,
50
+ } from './headers/fast-templates'
51
+ export type { SecurityHeadersOptions } from './headers/hsts'
52
+ export type { IngressFastHandler, IngressFastOptions } from './options'
53
+ export { buildTerminalResponse } from './response/terminal'
54
+ export type { HeaderPlan } from './shared'
55
+ export { DEFAULT_MAX_BODY_BYTES, METHOD_KIND } from './shared'
56
+ // ── Re-exports (back-compat: preserve fast.ts's original public surface) ──
57
+ export {
58
+ isValidResponseStatus,
59
+ normalizeResponseStatus,
60
+ safeTerminalStatus,
61
+ statusForErrorCode,
62
+ } from './status'
63
+
64
+ // ── Local constants ───────────────────────────────────────────
65
+ const EMPTY_BODY = new Uint8Array(0)
929
66
 
930
- // ── Option validation (fail fast on misspelled keys) ─────────────
931
- // Options are forwarded to the native addon as a plain object; a misspelled
932
- // key would otherwise be silently ignored. Validate against the known set so
933
- // misconfiguration fails loudly at construction time.
934
- const KNOWN_INGRESS_OPTION_KEYS: ReadonlySet<string> = new Set([
935
- "trustProxy",
936
- "trustedProxies",
937
- "parseCookies",
938
- "parseQuery",
939
- "requireJsonBody",
940
- "schema",
941
- "cors",
942
- "rateLimit",
943
- "security",
944
- "https",
945
- "maxBodyBytes",
946
- "enableSecurityHeaders",
947
- "enableRequestIds",
948
- "enableBodySizeGuard",
949
- "emitMetadataJson",
950
- "readBody",
951
- "outputBufferSize",
952
- "limits",
953
- ]);
67
+ // ── Fast ingress factory ─────────────────────────────
954
68
 
955
- function assertKnownIngressOptions(options: IngressFastOptions): void {
956
- for (const key of Object.keys(options)) {
957
- if (!KNOWN_INGRESS_OPTION_KEYS.has(key)) {
958
- throw new TypeError(
959
- `createIngressFast: unknown option '${key}'. ` +
960
- `Known options: ${[...KNOWN_INGRESS_OPTION_KEYS].sort().join(", ")}`,
961
- );
962
- }
69
+ /**
70
+ * Create a fast packed-input ingress handler (path 1).
71
+ *
72
+ * JS packs the request headers into a binary buffer (`IngressInputPacker`) and
73
+ * the native `Ingress.handleRequestPacked` decodes them. Responses use the
74
+ * `{"error":{...}}` wire format with `x-ratelimit-*` headers.
75
+ *
76
+ * Prefer this when you want the lowest-overhead pipeline and will call `run()`
77
+ * yourself with a **synchronous** callback. For ready-made route handlers and a
78
+ * server builder, use `createIngressHandler` (path 2).
79
+ */
80
+ export function createIngressFast(options: IngressFastOptions = {}): IngressFastHandler {
81
+ assertKnownIngressOptions(options)
82
+ assertIngressOptionValues(options)
83
+ if (options.trustProxy === true) {
84
+ warnTrustProxyDeprecated()
85
+ }
86
+ // The raw fast path returns a decoded result, not a Response — it cannot
87
+ // apply security headers. `security`/`enableSecurityHeaders` were previously
88
+ // accepted and silently ignored here (a dead option). Fail loudly instead:
89
+ // use `createIngress` (async) / `createIngressHandler` (pre-baked), or emit
90
+ // the headers yourself via `buildResponseContext` + `headersForResult`.
91
+ if (options.security !== undefined || options.enableSecurityHeaders !== undefined) {
92
+ throw new TypeError(
93
+ 'createIngressFast: `security` / `enableSecurityHeaders` are not ' +
94
+ 'supported on the raw fast path (it returns a decoded result, not a ' +
95
+ 'Response). Use `createIngress` or `createIngressHandler` to apply ' +
96
+ 'security headers, or emit them yourself via `buildResponseContext` ' +
97
+ 'and `headersForResult`.',
98
+ )
963
99
  }
964
- }
965
-
966
- // ── Fast ingress factory ─────────────────────────────
967
- export function createIngressFast(
968
- options: IngressFastOptions = {},
969
- ): IngressFastHandler {
970
- assertKnownIngressOptions(options);
971
100
 
972
101
  const rustOptions: Record<string, unknown> = {
973
102
  trustProxy: options.trustProxy,
@@ -1012,71 +141,73 @@ export function createIngressFast(
1012
141
  maxPairs: options.limits.maxPairs,
1013
142
  }
1014
143
  : undefined,
1015
- };
1016
-
1017
- const NativeIngress = (addon as any).Ingress as new (opts: Record<string, unknown>) => IngressInstance;
1018
- const handler = new NativeIngress(rustOptions);
1019
-
1020
- const headerPlan: HeaderPlan = {
1021
- cookie: options.parseCookies === true,
1022
- cors: options.cors != null,
1023
- proxy: options.trustProxy === true || options.trustedProxies?.enabled === true,
1024
- proto: (options.trustProxy === true || options.trustedProxies?.enabled === true) && options.https === undefined,
1025
- };
144
+ }
1026
145
 
1027
- const outputBufSize = Math.max(
146
+ // Lazy: the native addon is only needed once a handler is created.
147
+ const addon = getAddon()
148
+ const NativeIngress = addon.Ingress
149
+ const handler = new NativeIngress(rustOptions)
150
+
151
+ // Bun fast path: run the native pipeline through `bun:ffi` (opaque inner
152
+ // handle) to cut the per-request N-API crossing. The handle is valid only
153
+ // while `handler` is alive — this closure holds it, so it can never dangle.
154
+ // Falls back to napi when ffi is unavailable / the pointer method is absent.
155
+ const bunFFI = getBunFFI()
156
+ // Opaque u64 handle → JS number ONCE (bun:ffi converts a BigInt argument to
157
+ // a number per call anyway; number is cheaper on the hot path).
158
+ const ingressPtr =
159
+ bunFFI !== null && typeof handler.ingressInnerPtr === 'function'
160
+ ? Number(handler.ingressInnerPtr())
161
+ : 0
162
+
163
+ // Shared with the pre-baked handler path so cookie/cors/proxy/proto
164
+ // extraction decisions can never silently diverge between the two paths.
165
+ const headerPlan: HeaderPlan = buildHeaderPlan(options)
166
+
167
+ const outputBufSize = clampIngressBufferSize(
168
+ options.outputBufferSize ?? DEFAULT_FAST_OUTPUT_BUFFER_SIZE,
1028
169
  OUT_DATA_START,
1029
- options.outputBufferSize ?? DEFAULT_OUTPUT_BUFFER_SIZE,
1030
- );
1031
- const outputBuf = new Uint8Array(outputBufSize);
1032
- const inputPacker = new IngressInputPacker();
1033
- const result = new FastIngressResult();
170
+ )
171
+ const outputBuf = new Uint8Array(outputBufSize)
172
+ const inputPacker = new IngressInputPacker()
173
+ const result = new FastIngressResult()
1034
174
 
1035
175
  return {
1036
176
  run(req, ip, body, requestId, fn) {
1037
177
  try {
1038
- const methodKind = METHOD_KIND[req.method] ?? 7;
1039
-
1040
- const urlBytes = encoder.encode(req.url);
1041
- const ipBytes =
1042
- ip && ip.length > 0 ? encoder.encode(ip) : EMPTY_IP_BYTES;
1043
- const ridBytes = requestId
1044
- ? encoder.encode(requestId)
1045
- : new Uint8Array(0);
1046
-
1047
- const headers = packHeaders(req, headerPlan);
1048
-
1049
- const input = inputPacker.pack(
1050
- methodKind,
1051
- urlBytes,
1052
- ipBytes,
1053
- ridBytes,
1054
- headers,
1055
- );
1056
-
1057
- handler.handleRequestPacked(input, body, outputBuf);
1058
- result.refresh(outputBuf, body ?? EMPTY_BODY, requestId);
1059
- } catch {
1060
- result.setInternalError(requestId);
178
+ const methodKind = METHOD_KIND[req.method] ?? METHOD_KIND_UNKNOWN
179
+
180
+ const headers = packHeaders(req, headerPlan)
181
+
182
+ const input = inputPacker.packFromStrings(methodKind, req.url, ip, requestId, headers)
183
+
184
+ // handleRequestPacked returns the number of bytes it wrote; decode only
185
+ // the written prefix (mirrors handlers.ts) so stale bytes past `written`
186
+ // in the reused buffer can never be misread. Under Bun with a live ffi
187
+ // handle this runs the pipeline through `bun:ffi` instead of napi.
188
+ const written =
189
+ bunFFI !== null && ingressPtr !== 0
190
+ ? bunFFI.ingressHandlePacked(ingressPtr, input, body, outputBuf)
191
+ : handler.handleRequestPacked(input, body, outputBuf)
192
+ result.refresh(outputBuf.subarray(0, written), body ?? EMPTY_BODY, requestId)
193
+ } catch (err) {
194
+ result.setInternalError(requestId)
195
+ if (options.onError) {
196
+ try {
197
+ options.onError(err instanceof Error ? err : new Error(String(err)))
198
+ } catch {
199
+ // hook must never crash the handler
200
+ }
201
+ }
1061
202
  }
1062
203
 
1063
204
  try {
1064
- const out = fn(result);
1065
-
1066
- if (
1067
- out !== null &&
1068
- (typeof out === "object" || typeof out === "function") &&
1069
- typeof (out as any).then === "function"
1070
- ) {
1071
- throw new Error(
1072
- "createIngressFast().run() callback must be synchronous.",
1073
- );
1074
- }
1075
-
1076
- return out;
205
+ const out = fn(result)
206
+ assertSyncCallback(out, 'createIngressFast().run()')
207
+ return out
1077
208
  } finally {
1078
- result.invalidate();
209
+ result.invalidate()
1079
210
  }
1080
211
  },
1081
- };
1082
- }
212
+ }
213
+ }