castrum 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/Cargo.toml +73 -0
  2. package/README.md +432 -0
  3. package/bench.ts +3 -0
  4. package/castrum.linux-x64-gnu.node +0 -0
  5. package/index.ts +24 -0
  6. package/package.json +94 -0
  7. package/rust/batch.rs +195 -0
  8. package/rust/cookie_parser.rs +70 -0
  9. package/rust/cors.rs +263 -0
  10. package/rust/hashing.rs +164 -0
  11. package/rust/headers.rs +234 -0
  12. package/rust/hmac_sha256.rs +113 -0
  13. package/rust/http_parser.rs +132 -0
  14. package/rust/ingress.rs +616 -0
  15. package/rust/ingress_constants.rs +89 -0
  16. package/rust/ip_trust.rs +185 -0
  17. package/rust/json_ops.rs +66 -0
  18. package/rust/json_patch_ops.rs +25 -0
  19. package/rust/json_schema.rs +151 -0
  20. package/rust/json_ser.rs +429 -0
  21. package/rust/lib.rs +48 -0
  22. package/rust/method.rs +141 -0
  23. package/rust/mime_lookup.rs +76 -0
  24. package/rust/output.rs +238 -0
  25. package/rust/proxy.rs +90 -0
  26. package/rust/query_parser.rs +141 -0
  27. package/rust/random_token.rs +52 -0
  28. package/rust/rate_limit.rs +203 -0
  29. package/rust/terminal.rs +95 -0
  30. package/rust/unit_tests.rs +843 -0
  31. package/rust/url_codec.rs +133 -0
  32. package/rust/util.rs +537 -0
  33. package/rust/validation.rs +107 -0
  34. package/rust/websocket.rs +23 -0
  35. package/src/baseline/index.ts +12 -0
  36. package/src/baseline/tasks/cookie.ts +22 -0
  37. package/src/baseline/tasks/hashing.ts +18 -0
  38. package/src/baseline/tasks/hmac.ts +38 -0
  39. package/src/baseline/tasks/http.ts +62 -0
  40. package/src/baseline/tasks/json-patch.ts +12 -0
  41. package/src/baseline/tasks/json.ts +50 -0
  42. package/src/baseline/tasks/mime.ts +5 -0
  43. package/src/baseline/tasks/query.ts +14 -0
  44. package/src/baseline/tasks/token.ts +15 -0
  45. package/src/baseline/tasks/url.ts +11 -0
  46. package/src/baseline/tasks/validation.ts +19 -0
  47. package/src/baseline/tasks/websocket.ts +14 -0
  48. package/src/bench/assert.ts +70 -0
  49. package/src/bench/checks.ts +227 -0
  50. package/src/bench/checksum.ts +27 -0
  51. package/src/bench/comparisons.ts +70 -0
  52. package/src/bench/concurrent-worker.ts +135 -0
  53. package/src/bench/fixtures.ts +233 -0
  54. package/src/bench/index.ts +2 -0
  55. package/src/bench/measure.ts +177 -0
  56. package/src/bench/now.ts +3 -0
  57. package/src/bench/report.ts +79 -0
  58. package/src/bench/run.ts +57 -0
  59. package/src/bench/tasks/complex.ts +232 -0
  60. package/src/bench/tasks/concurrent.ts +213 -0
  61. package/src/bench/tasks/cookie.ts +44 -0
  62. package/src/bench/tasks/hashing.ts +33 -0
  63. package/src/bench/tasks/hmac.ts +36 -0
  64. package/src/bench/tasks/http.ts +50 -0
  65. package/src/bench/tasks/index.ts +52 -0
  66. package/src/bench/tasks/json-patch.ts +21 -0
  67. package/src/bench/tasks/json.ts +33 -0
  68. package/src/bench/tasks/mime.ts +21 -0
  69. package/src/bench/tasks/query.ts +44 -0
  70. package/src/bench/tasks/stress.ts +142 -0
  71. package/src/bench/tasks/token.ts +20 -0
  72. package/src/bench/tasks/url.ts +34 -0
  73. package/src/bench/tasks/validation.ts +57 -0
  74. package/src/bench/tasks/websocket.ts +21 -0
  75. package/src/bench/types.ts +60 -0
  76. package/src/data/json-rows.ts +33 -0
  77. package/src/ingress/constants.ts +56 -0
  78. package/src/ingress/fast.ts +1082 -0
  79. package/src/ingress/handlers.ts +1202 -0
  80. package/src/ingress/index.ts +457 -0
  81. package/src/ingress/packed-input.ts +59 -0
  82. package/src/native/index.ts +207 -0
  83. package/src/rust-ffi/index.ts +555 -0
  84. package/src/shared/bytes.ts +36 -0
  85. package/src/shared/json.ts +18 -0
  86. package/src/shared/packed.ts +316 -0
@@ -0,0 +1,1082 @@
1
+ // src/ingress/fast.ts — High-performance ingress handler for public API
2
+ //
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";
14
+ 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
+ }
929
+
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
+ ]);
954
+
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
+ }
963
+ }
964
+ }
965
+
966
+ // ── Fast ingress factory ─────────────────────────────
967
+ export function createIngressFast(
968
+ options: IngressFastOptions = {},
969
+ ): IngressFastHandler {
970
+ assertKnownIngressOptions(options);
971
+
972
+ const rustOptions: Record<string, unknown> = {
973
+ trustProxy: options.trustProxy,
974
+ trustedProxies: options.trustedProxies
975
+ ? {
976
+ enabled: options.trustedProxies.enabled,
977
+ networks: options.trustedProxies.networks,
978
+ }
979
+ : undefined,
980
+ parseCookies: options.parseCookies,
981
+ parseQuery: options.parseQuery,
982
+ requireJsonBody: options.requireJsonBody,
983
+ schema: options.schema,
984
+ cors: options.cors
985
+ ? {
986
+ allowOrigin: options.cors.allowOrigin,
987
+ allowMethods: options.cors.allowMethods,
988
+ allowHeaders: options.cors.allowHeaders,
989
+ exposeHeaders: options.cors.exposeHeaders,
990
+ allowCredentials: options.cors.allowCredentials,
991
+ maxAge: options.cors.maxAge,
992
+ }
993
+ : undefined,
994
+ rateLimit: options.rateLimit
995
+ ? {
996
+ limit: options.rateLimit.limit,
997
+ windowMs: options.rateLimit.windowMs,
998
+ maxEntries: options.rateLimit.maxEntries,
999
+ }
1000
+ : undefined,
1001
+ https: options.https,
1002
+ maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
1003
+ enableBodySizeGuard: options.enableBodySizeGuard !== false,
1004
+ emitMetadataJson: options.emitMetadataJson,
1005
+ limits: options.limits
1006
+ ? {
1007
+ maxUrlBytes: options.limits.maxUrlBytes,
1008
+ maxQueryBytes: options.limits.maxQueryBytes,
1009
+ maxCookieBytes: options.limits.maxCookieBytes,
1010
+ maxHeadersBytes: options.limits.maxHeadersBytes,
1011
+ maxHeaders: options.limits.maxHeaders,
1012
+ maxPairs: options.limits.maxPairs,
1013
+ }
1014
+ : 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
+ };
1026
+
1027
+ const outputBufSize = Math.max(
1028
+ 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();
1034
+
1035
+ return {
1036
+ run(req, ip, body, requestId, fn) {
1037
+ 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);
1061
+ }
1062
+
1063
+ 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;
1077
+ } finally {
1078
+ result.invalidate();
1079
+ }
1080
+ },
1081
+ };
1082
+ }