castrum 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +322 -59
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +3161 -0
- package/dist/index.js +7306 -0
- package/docs/ARCHITECTURE.md +415 -0
- package/docs/BENCHMARKS.md +307 -0
- package/docs/ENVIRONMENT.md +79 -0
- package/docs/GETTING_STARTED.md +209 -0
- package/docs/INGRESS.md +745 -0
- package/docs/REPO_MAP.md +259 -0
- package/docs/adr/0001-two-wire-formats.md +39 -0
- package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
- package/docs/adr/0003-bun-builtins-delegation.md +42 -0
- package/docs/bun-builtins-decision-matrix.md +108 -0
- package/index.ts +51 -8
- package/package.json +73 -20
- package/src/data/json-rows.ts +13 -13
- package/src/ingress/body.ts +149 -0
- package/src/ingress/constants.ts +220 -48
- package/src/ingress/context.ts +136 -0
- package/src/ingress/decode/baked-result.ts +121 -0
- package/src/ingress/decode/fast-result.ts +160 -0
- package/src/ingress/decode/packed-sections.ts +56 -0
- package/src/ingress/decode/result-base.ts +172 -0
- package/src/ingress/errors.ts +72 -0
- package/src/ingress/fast.ts +152 -1021
- package/src/ingress/handlers.ts +563 -1001
- package/src/ingress/headers/baked-templates.ts +102 -0
- package/src/ingress/headers/cors.ts +44 -0
- package/src/ingress/headers/fast-templates.ts +238 -0
- package/src/ingress/headers/hsts.ts +83 -0
- package/src/ingress/health.ts +67 -0
- package/src/ingress/index.ts +130 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +75 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +150 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +96 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/routes/common.ts +38 -0
- package/src/ingress/routes/delete.ts +14 -0
- package/src/ingress/routes/echo.ts +107 -0
- package/src/ingress/routes/fallback.ts +23 -0
- package/src/ingress/routes/head.ts +37 -0
- package/src/ingress/routes/index.ts +10 -0
- package/src/ingress/routes/json-write.ts +153 -0
- package/src/ingress/routes/options.ts +36 -0
- package/src/ingress/routes/read.ts +45 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +443 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +157 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +217 -0
- package/src/integration/streaming.ts +79 -0
- package/src/integration/websocket.ts +92 -0
- package/src/loader/batch.ts +133 -0
- package/src/loader/cost.ts +104 -0
- package/src/loader/index.ts +930 -0
- package/src/loader/ops.ts +657 -0
- package/src/native/ffi.ts +1541 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +194 -0
- package/src/native/types.ts +598 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch.ts +431 -0
- package/src/rust-ffi/client.ts +88 -0
- package/src/rust-ffi/context.ts +164 -0
- package/src/rust-ffi/index.ts +40 -541
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +42 -0
- package/src/rust-ffi/scalar/crypto.ts +265 -0
- package/src/rust-ffi/scalar/factories.ts +233 -0
- package/src/rust-ffi/scalar/hashing.ts +72 -0
- package/src/rust-ffi/scalar/http.ts +206 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +450 -0
- package/src/rust-ffi/scalar/json.ts +45 -0
- package/src/rust-ffi/scalar/payload.ts +124 -0
- package/src/rust-ffi/text.ts +95 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +77 -0
- package/src/shared/adaptive.ts +68 -0
- package/src/shared/bench-classify.ts +209 -0
- package/src/shared/buffer-pool.ts +209 -0
- package/src/shared/bytes.ts +35 -5
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +74 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed.ts +350 -154
- package/src/shared/proven.ts +596 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +34 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +31 -0
- package/Cargo.toml +0 -73
- package/bench.ts +0 -3
- package/rust/batch.rs +0 -195
- package/rust/cookie_parser.rs +0 -70
- package/rust/cors.rs +0 -263
- package/rust/hashing.rs +0 -164
- package/rust/headers.rs +0 -234
- package/rust/hmac_sha256.rs +0 -113
- package/rust/http_parser.rs +0 -132
- package/rust/ingress.rs +0 -616
- package/rust/ingress_constants.rs +0 -89
- package/rust/ip_trust.rs +0 -185
- package/rust/json_ops.rs +0 -66
- package/rust/json_patch_ops.rs +0 -25
- package/rust/json_schema.rs +0 -151
- package/rust/json_ser.rs +0 -429
- package/rust/lib.rs +0 -48
- package/rust/method.rs +0 -141
- package/rust/mime_lookup.rs +0 -76
- package/rust/output.rs +0 -238
- package/rust/proxy.rs +0 -90
- package/rust/query_parser.rs +0 -141
- package/rust/random_token.rs +0 -52
- package/rust/rate_limit.rs +0 -203
- package/rust/terminal.rs +0 -95
- package/rust/unit_tests.rs +0 -843
- package/rust/url_codec.rs +0 -133
- package/rust/util.rs +0 -537
- package/rust/validation.rs +0 -107
- package/rust/websocket.rs +0 -23
- package/src/baseline/index.ts +0 -12
- package/src/baseline/tasks/cookie.ts +0 -22
- package/src/baseline/tasks/hashing.ts +0 -18
- package/src/baseline/tasks/hmac.ts +0 -38
- package/src/baseline/tasks/http.ts +0 -62
- package/src/baseline/tasks/json-patch.ts +0 -12
- package/src/baseline/tasks/json.ts +0 -50
- package/src/baseline/tasks/mime.ts +0 -5
- package/src/baseline/tasks/query.ts +0 -14
- package/src/baseline/tasks/token.ts +0 -15
- package/src/baseline/tasks/url.ts +0 -11
- package/src/baseline/tasks/validation.ts +0 -19
- package/src/baseline/tasks/websocket.ts +0 -14
- package/src/bench/assert.ts +0 -70
- package/src/bench/checks.ts +0 -227
- package/src/bench/checksum.ts +0 -27
- package/src/bench/comparisons.ts +0 -70
- package/src/bench/concurrent-worker.ts +0 -135
- package/src/bench/fixtures.ts +0 -233
- package/src/bench/index.ts +0 -2
- package/src/bench/measure.ts +0 -177
- package/src/bench/now.ts +0 -3
- package/src/bench/report.ts +0 -79
- package/src/bench/run.ts +0 -57
- package/src/bench/tasks/complex.ts +0 -232
- package/src/bench/tasks/concurrent.ts +0 -213
- package/src/bench/tasks/cookie.ts +0 -44
- package/src/bench/tasks/hashing.ts +0 -33
- package/src/bench/tasks/hmac.ts +0 -36
- package/src/bench/tasks/http.ts +0 -50
- package/src/bench/tasks/index.ts +0 -52
- package/src/bench/tasks/json-patch.ts +0 -21
- package/src/bench/tasks/json.ts +0 -33
- package/src/bench/tasks/mime.ts +0 -21
- package/src/bench/tasks/query.ts +0 -44
- package/src/bench/tasks/stress.ts +0 -142
- package/src/bench/tasks/token.ts +0 -20
- package/src/bench/tasks/url.ts +0 -34
- package/src/bench/tasks/validation.ts +0 -57
- package/src/bench/tasks/websocket.ts +0 -21
- package/src/bench/types.ts +0 -60
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
package/src/ingress/fast.ts
CHANGED
|
@@ -1,973 +1,102 @@
|
|
|
1
|
-
// src/ingress/fast.ts — High-performance ingress handler
|
|
1
|
+
// src/ingress/fast.ts — High-performance ingress handler (fast packed-input
|
|
2
|
+
// path).
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
7
|
-
// -
|
|
8
|
-
// -
|
|
9
|
-
// -
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
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
|
-
// ──
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1030
|
-
)
|
|
1031
|
-
const
|
|
1032
|
-
const
|
|
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] ??
|
|
1039
|
-
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
)
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
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
|
+
}
|