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.
- package/Cargo.toml +73 -0
- package/README.md +432 -0
- package/bench.ts +3 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/index.ts +24 -0
- package/package.json +94 -0
- package/rust/batch.rs +195 -0
- package/rust/cookie_parser.rs +70 -0
- package/rust/cors.rs +263 -0
- package/rust/hashing.rs +164 -0
- package/rust/headers.rs +234 -0
- package/rust/hmac_sha256.rs +113 -0
- package/rust/http_parser.rs +132 -0
- package/rust/ingress.rs +616 -0
- package/rust/ingress_constants.rs +89 -0
- package/rust/ip_trust.rs +185 -0
- package/rust/json_ops.rs +66 -0
- package/rust/json_patch_ops.rs +25 -0
- package/rust/json_schema.rs +151 -0
- package/rust/json_ser.rs +429 -0
- package/rust/lib.rs +48 -0
- package/rust/method.rs +141 -0
- package/rust/mime_lookup.rs +76 -0
- package/rust/output.rs +238 -0
- package/rust/proxy.rs +90 -0
- package/rust/query_parser.rs +141 -0
- package/rust/random_token.rs +52 -0
- package/rust/rate_limit.rs +203 -0
- package/rust/terminal.rs +95 -0
- package/rust/unit_tests.rs +843 -0
- package/rust/url_codec.rs +133 -0
- package/rust/util.rs +537 -0
- package/rust/validation.rs +107 -0
- package/rust/websocket.rs +23 -0
- package/src/baseline/index.ts +12 -0
- package/src/baseline/tasks/cookie.ts +22 -0
- package/src/baseline/tasks/hashing.ts +18 -0
- package/src/baseline/tasks/hmac.ts +38 -0
- package/src/baseline/tasks/http.ts +62 -0
- package/src/baseline/tasks/json-patch.ts +12 -0
- package/src/baseline/tasks/json.ts +50 -0
- package/src/baseline/tasks/mime.ts +5 -0
- package/src/baseline/tasks/query.ts +14 -0
- package/src/baseline/tasks/token.ts +15 -0
- package/src/baseline/tasks/url.ts +11 -0
- package/src/baseline/tasks/validation.ts +19 -0
- package/src/baseline/tasks/websocket.ts +14 -0
- package/src/bench/assert.ts +70 -0
- package/src/bench/checks.ts +227 -0
- package/src/bench/checksum.ts +27 -0
- package/src/bench/comparisons.ts +70 -0
- package/src/bench/concurrent-worker.ts +135 -0
- package/src/bench/fixtures.ts +233 -0
- package/src/bench/index.ts +2 -0
- package/src/bench/measure.ts +177 -0
- package/src/bench/now.ts +3 -0
- package/src/bench/report.ts +79 -0
- package/src/bench/run.ts +57 -0
- package/src/bench/tasks/complex.ts +232 -0
- package/src/bench/tasks/concurrent.ts +213 -0
- package/src/bench/tasks/cookie.ts +44 -0
- package/src/bench/tasks/hashing.ts +33 -0
- package/src/bench/tasks/hmac.ts +36 -0
- package/src/bench/tasks/http.ts +50 -0
- package/src/bench/tasks/index.ts +52 -0
- package/src/bench/tasks/json-patch.ts +21 -0
- package/src/bench/tasks/json.ts +33 -0
- package/src/bench/tasks/mime.ts +21 -0
- package/src/bench/tasks/query.ts +44 -0
- package/src/bench/tasks/stress.ts +142 -0
- package/src/bench/tasks/token.ts +20 -0
- package/src/bench/tasks/url.ts +34 -0
- package/src/bench/tasks/validation.ts +57 -0
- package/src/bench/tasks/websocket.ts +21 -0
- package/src/bench/types.ts +60 -0
- package/src/data/json-rows.ts +33 -0
- package/src/ingress/constants.ts +56 -0
- package/src/ingress/fast.ts +1082 -0
- package/src/ingress/handlers.ts +1202 -0
- package/src/ingress/index.ts +457 -0
- package/src/ingress/packed-input.ts +59 -0
- package/src/native/index.ts +207 -0
- package/src/rust-ffi/index.ts +555 -0
- package/src/shared/bytes.ts +36 -0
- package/src/shared/json.ts +18 -0
- package/src/shared/packed.ts +316 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
// src/ingress/index.ts — Public ingress API
|
|
2
|
+
//
|
|
3
|
+
// All constants come from Rust via src/ingress/constants.ts.
|
|
4
|
+
// For maximum performance, use createIngressFast() from ./fast.ts directly.
|
|
5
|
+
// This module wraps createIngressFast with convenience async API (createIngress).
|
|
6
|
+
|
|
7
|
+
import addon from "../native";
|
|
8
|
+
import { decoder } from "../shared/bytes";
|
|
9
|
+
import {
|
|
10
|
+
OUT_VERDICT, OUT_ERROR_CODE, OUT_STATUS, OUT_FLAGS,
|
|
11
|
+
OUT_RATE_LIMIT, OUT_RATE_REMAINING, OUT_RATE_RESET, OUT_RETRY_AFTER,
|
|
12
|
+
OUT_COOKIES_JSON_LEN, OUT_QUERY_JSON_LEN, OUT_HEADER_VARIANT, OUT_BODY_JSON_LEN,
|
|
13
|
+
OUT_DATA_START,
|
|
14
|
+
FLAG_HAS_COOKIES, FLAG_HAS_QUERY,
|
|
15
|
+
FLAG_BODY_VALID_JSON, FLAG_SCHEMA_VALID,
|
|
16
|
+
FLAG_CORS_ALLOWED, FLAG_IS_PREFLIGHT, FLAG_RATE_LIMITED,
|
|
17
|
+
FLAG_HTTPS, FLAG_TRUSTED_PROXY, FLAG_BODY_TRUNCATED,
|
|
18
|
+
HV_JSON, HV_CORS_SIMPLE, HV_CORS_PREFLIGHT, HV_RATE_ACTIVE, HV_RATE_LIMITED,
|
|
19
|
+
HV_COUNT,
|
|
20
|
+
ERR_CODE_NONE, ERR_CODE_CORS_PREFLIGHT, ERR_CODE_RATE_LIMITED,
|
|
21
|
+
ERR_CODE_BODY_TOO_LARGE, ERR_CODE_INVALID_JSON, ERR_CODE_SCHEMA_VALIDATION,
|
|
22
|
+
ERR_CODE_BAD_REQUEST, ERR_CODE_REQUEST_TOO_LARGE, ERR_CODE_INTERNAL,
|
|
23
|
+
} from "./constants";
|
|
24
|
+
import {
|
|
25
|
+
createIngressFast,
|
|
26
|
+
FastIngressResult,
|
|
27
|
+
buildResponseContext,
|
|
28
|
+
buildTerminalResponse,
|
|
29
|
+
headersForResult,
|
|
30
|
+
safeTerminalStatus,
|
|
31
|
+
statusForErrorCode,
|
|
32
|
+
isValidResponseStatus,
|
|
33
|
+
normalizeResponseStatus,
|
|
34
|
+
errorCodeName,
|
|
35
|
+
errorMessage,
|
|
36
|
+
type IngressFastOptions,
|
|
37
|
+
type IngressFastHandler,
|
|
38
|
+
type ResponseBuildContext,
|
|
39
|
+
type HeaderTemplate,
|
|
40
|
+
type HeaderPlan,
|
|
41
|
+
type CorsStaticStrings,
|
|
42
|
+
type SecurityHeadersOptions,
|
|
43
|
+
type CorsOptions,
|
|
44
|
+
generateRequestId,
|
|
45
|
+
METHOD_KIND,
|
|
46
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
47
|
+
} from "./fast";
|
|
48
|
+
|
|
49
|
+
// ── Re-export types from constants ──────────────────────────────
|
|
50
|
+
export {
|
|
51
|
+
OUT_VERDICT, OUT_ERROR_CODE, OUT_STATUS, OUT_FLAGS,
|
|
52
|
+
OUT_RATE_LIMIT, OUT_RATE_REMAINING, OUT_RATE_RESET, OUT_RETRY_AFTER,
|
|
53
|
+
OUT_COOKIES_JSON_LEN, OUT_QUERY_JSON_LEN, OUT_HEADER_VARIANT, OUT_BODY_JSON_LEN,
|
|
54
|
+
OUT_DATA_START,
|
|
55
|
+
FLAG_HAS_COOKIES, FLAG_HAS_QUERY,
|
|
56
|
+
FLAG_BODY_VALID_JSON, FLAG_SCHEMA_VALID,
|
|
57
|
+
FLAG_CORS_ALLOWED, FLAG_IS_PREFLIGHT, FLAG_RATE_LIMITED,
|
|
58
|
+
FLAG_HTTPS, FLAG_TRUSTED_PROXY, FLAG_BODY_TRUNCATED,
|
|
59
|
+
HV_JSON, HV_CORS_SIMPLE, HV_CORS_PREFLIGHT, HV_RATE_ACTIVE, HV_RATE_LIMITED,
|
|
60
|
+
HV_COUNT,
|
|
61
|
+
ERR_CODE_NONE, ERR_CODE_CORS_PREFLIGHT, ERR_CODE_RATE_LIMITED,
|
|
62
|
+
ERR_CODE_BODY_TOO_LARGE, ERR_CODE_INVALID_JSON, ERR_CODE_SCHEMA_VALIDATION,
|
|
63
|
+
ERR_CODE_BAD_REQUEST, ERR_CODE_REQUEST_TOO_LARGE, ERR_CODE_INTERNAL,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ── Public option types ──────────────────────────────────────────
|
|
67
|
+
export type { SecurityHeadersOptions, CorsOptions };
|
|
68
|
+
|
|
69
|
+
export interface IngressOptions extends IngressFastOptions {
|
|
70
|
+
enableRequestIds?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Result types ─────────────────────────────────────────────────
|
|
74
|
+
// Note: the exported `FastIngressResult` class (from fast.ts) is the zero-alloc
|
|
75
|
+
// implementation; this `IngressResult` interface is the readonly structural view
|
|
76
|
+
// used by the public sync/async handler types.
|
|
77
|
+
interface IngressResult {
|
|
78
|
+
readonly status: number;
|
|
79
|
+
readonly verdict: number;
|
|
80
|
+
readonly flags: number;
|
|
81
|
+
readonly errorCode: number;
|
|
82
|
+
readonly terminal: boolean;
|
|
83
|
+
readonly ok: boolean;
|
|
84
|
+
readonly https: boolean;
|
|
85
|
+
readonly trustedProxy: boolean;
|
|
86
|
+
readonly hasCookies: boolean;
|
|
87
|
+
readonly hasQuery: boolean;
|
|
88
|
+
readonly bodyValidJson: boolean;
|
|
89
|
+
readonly schemaValid: boolean;
|
|
90
|
+
readonly corsAllowed: boolean;
|
|
91
|
+
readonly isPreflight: boolean;
|
|
92
|
+
readonly rateLimited: boolean;
|
|
93
|
+
readonly rateLimit: number;
|
|
94
|
+
readonly rateRemaining: number;
|
|
95
|
+
readonly rateResetMs: number;
|
|
96
|
+
readonly retryAfterMs: number;
|
|
97
|
+
readonly body: Uint8Array;
|
|
98
|
+
readonly headerVariant: number;
|
|
99
|
+
readonly requestId: string;
|
|
100
|
+
readonly bodyTruncated: boolean;
|
|
101
|
+
|
|
102
|
+
cookiesJson(): string;
|
|
103
|
+
queryJson(): string;
|
|
104
|
+
bodyJson(): Uint8Array;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface IngressContext extends IngressResult {
|
|
108
|
+
response: Response | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Synchronous ingress handler.
|
|
113
|
+
*
|
|
114
|
+
* The callback runs on the zero-alloc result object and MUST be synchronous:
|
|
115
|
+
* returning a Promise (or any thenable) throws. The result is invalidated after
|
|
116
|
+
* `run()` returns, so capture the fields you need inside the callback (or use
|
|
117
|
+
* the async {@link createIngress} which snapshots for you).
|
|
118
|
+
*/
|
|
119
|
+
export interface SyncIngressHandler {
|
|
120
|
+
run<T>(
|
|
121
|
+
req: Request,
|
|
122
|
+
ip: string | undefined,
|
|
123
|
+
body: Uint8Array | null,
|
|
124
|
+
requestId: string,
|
|
125
|
+
fn: (result: IngressResult) => T,
|
|
126
|
+
): T;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface IngressHandler {
|
|
130
|
+
(req: Request, ip?: string): Promise<IngressContext>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── HTTP status helpers (re-exported from fast.ts) ──────────────
|
|
134
|
+
export { isValidResponseStatus, statusForErrorCode, normalizeResponseStatus, safeTerminalStatus };
|
|
135
|
+
|
|
136
|
+
// ── Header template system (re-exported from fast.ts) ────────────
|
|
137
|
+
export { buildResponseContext, headersForResult };
|
|
138
|
+
export type { ResponseBuildContext, HeaderTemplate, CorsStaticStrings };
|
|
139
|
+
|
|
140
|
+
// ── Pre-baked handler functions (route factories + Bun.serve builder) ──
|
|
141
|
+
export * from "./handlers";
|
|
142
|
+
|
|
143
|
+
// ── Constants ────────────────────────────────────────────────────
|
|
144
|
+
const EMPTY_BODY = new Uint8Array(0);
|
|
145
|
+
|
|
146
|
+
// ── Sync factory ─────────────────────────────────────────────────
|
|
147
|
+
export function createIngressSync(
|
|
148
|
+
options: IngressOptions = {},
|
|
149
|
+
): SyncIngressHandler {
|
|
150
|
+
const fast = createIngressFast(options);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
run(req, ip, body, requestId, fn) {
|
|
154
|
+
return fast.run(req, ip, body, requestId, (result) => {
|
|
155
|
+
const out = fn(result);
|
|
156
|
+
|
|
157
|
+
if (
|
|
158
|
+
out !== null &&
|
|
159
|
+
(typeof out === "object" || typeof out === "function") &&
|
|
160
|
+
typeof (out as any).then === "function"
|
|
161
|
+
) {
|
|
162
|
+
throw new Error("createIngressSync().run() callback must be synchronous.");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return out;
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Body reading helpers (used by async API) ────────────────────
|
|
172
|
+
function readRequestBodyOnce(
|
|
173
|
+
req: Request,
|
|
174
|
+
maxBytes: number,
|
|
175
|
+
guard: boolean,
|
|
176
|
+
): Promise<Uint8Array> {
|
|
177
|
+
if (req.body === null) {
|
|
178
|
+
return Promise.resolve(EMPTY_BODY);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return readBodyWithLimit(req, maxBytes, guard);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function readBodyWithLimit(
|
|
185
|
+
req: Request,
|
|
186
|
+
maxBytes: number,
|
|
187
|
+
guard: boolean,
|
|
188
|
+
): Promise<Uint8Array> {
|
|
189
|
+
const body = req.body;
|
|
190
|
+
|
|
191
|
+
if (!body) {
|
|
192
|
+
return EMPTY_BODY;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const reader = body.getReader();
|
|
196
|
+
const chunks: Uint8Array[] = [];
|
|
197
|
+
let total = 0;
|
|
198
|
+
|
|
199
|
+
for (;;) {
|
|
200
|
+
const { done, value } = await reader.read();
|
|
201
|
+
|
|
202
|
+
if (done) break;
|
|
203
|
+
if (!value) continue;
|
|
204
|
+
|
|
205
|
+
total += value.byteLength;
|
|
206
|
+
|
|
207
|
+
if (guard && total > maxBytes) {
|
|
208
|
+
await reader.cancel().catch(() => {});
|
|
209
|
+
|
|
210
|
+
const err = new Error("BODY_TOO_LARGE");
|
|
211
|
+
(err as any).code = "BODY_TOO_LARGE";
|
|
212
|
+
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
chunks.push(value);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return concatUint8Arrays(chunks, total);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function concatUint8Arrays(
|
|
223
|
+
chunks: Uint8Array[],
|
|
224
|
+
total: number,
|
|
225
|
+
): Uint8Array {
|
|
226
|
+
if (chunks.length === 0) return EMPTY_BODY;
|
|
227
|
+
if (chunks.length === 1) return chunks[0]!;
|
|
228
|
+
|
|
229
|
+
const out = new Uint8Array(total);
|
|
230
|
+
let offset = 0;
|
|
231
|
+
|
|
232
|
+
for (const chunk of chunks) {
|
|
233
|
+
out.set(chunk, offset);
|
|
234
|
+
offset += chunk.byteLength;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Async factory ────────────────────────────────────────────────
|
|
241
|
+
export function createIngress(options: IngressOptions = {}): IngressHandler {
|
|
242
|
+
const sync = createIngressSync(options);
|
|
243
|
+
|
|
244
|
+
const guard = options.enableBodySizeGuard !== false;
|
|
245
|
+
const max = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
246
|
+
|
|
247
|
+
const wantsBody =
|
|
248
|
+
options.readBody === true ||
|
|
249
|
+
options.requireJsonBody === true ||
|
|
250
|
+
options.schema != null ||
|
|
251
|
+
(options.readBody !== false && guard);
|
|
252
|
+
|
|
253
|
+
const responseCtx = buildResponseContext(options);
|
|
254
|
+
|
|
255
|
+
return async function ingressAsync(
|
|
256
|
+
req: Request,
|
|
257
|
+
ip?: string,
|
|
258
|
+
): Promise<IngressContext> {
|
|
259
|
+
const requestId =
|
|
260
|
+
options.enableRequestIds === false ? "" : decoder.decode(generateRequestId());
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
if (guard) {
|
|
264
|
+
const rawLen = req.headers.get("content-length");
|
|
265
|
+
const contentLength = Number(rawLen ?? "0");
|
|
266
|
+
|
|
267
|
+
if (Number.isFinite(contentLength) && contentLength > max) {
|
|
268
|
+
return syntheticContext(
|
|
269
|
+
req,
|
|
270
|
+
requestId,
|
|
271
|
+
options,
|
|
272
|
+
responseCtx,
|
|
273
|
+
413,
|
|
274
|
+
ERR_CODE_BODY_TOO_LARGE,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let body: Uint8Array | null = null;
|
|
280
|
+
|
|
281
|
+
if (wantsBody && req.body !== null) {
|
|
282
|
+
try {
|
|
283
|
+
body = await readRequestBodyOnce(req, max, guard);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if ((err as any)?.code === "BODY_TOO_LARGE") {
|
|
286
|
+
return syntheticContext(
|
|
287
|
+
req,
|
|
288
|
+
requestId,
|
|
289
|
+
options,
|
|
290
|
+
responseCtx,
|
|
291
|
+
413,
|
|
292
|
+
ERR_CODE_BODY_TOO_LARGE,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
throw err;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return sync.run(req, ip, body, requestId, (r) => {
|
|
301
|
+
const snapshot = snapshotResult(r);
|
|
302
|
+
const response = buildTerminalResponse(
|
|
303
|
+
responseCtx,
|
|
304
|
+
snapshot,
|
|
305
|
+
req,
|
|
306
|
+
requestId,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
...snapshot,
|
|
311
|
+
response,
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
} catch {
|
|
315
|
+
return internalContext(req, requestId, options, responseCtx);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Result helpers ───────────────────────────────────────────────
|
|
321
|
+
function snapshotResult(r: IngressResult): IngressResult {
|
|
322
|
+
const cookies = r.cookiesJson();
|
|
323
|
+
const query = r.queryJson();
|
|
324
|
+
const bodyJson = r.bodyJson().slice();
|
|
325
|
+
const body = r.body;
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
status: r.status,
|
|
329
|
+
verdict: r.verdict,
|
|
330
|
+
flags: r.flags,
|
|
331
|
+
errorCode: r.errorCode,
|
|
332
|
+
terminal: r.terminal,
|
|
333
|
+
ok: r.ok,
|
|
334
|
+
https: r.https,
|
|
335
|
+
trustedProxy: r.trustedProxy,
|
|
336
|
+
hasCookies: r.hasCookies,
|
|
337
|
+
hasQuery: r.hasQuery,
|
|
338
|
+
bodyValidJson: r.bodyValidJson,
|
|
339
|
+
schemaValid: r.schemaValid,
|
|
340
|
+
corsAllowed: r.corsAllowed,
|
|
341
|
+
isPreflight: r.isPreflight,
|
|
342
|
+
rateLimited: r.rateLimited,
|
|
343
|
+
rateLimit: r.rateLimit,
|
|
344
|
+
rateRemaining: r.rateRemaining,
|
|
345
|
+
rateResetMs: r.rateResetMs,
|
|
346
|
+
retryAfterMs: r.retryAfterMs,
|
|
347
|
+
body,
|
|
348
|
+
headerVariant: r.headerVariant,
|
|
349
|
+
requestId: r.requestId,
|
|
350
|
+
bodyTruncated: r.bodyTruncated,
|
|
351
|
+
|
|
352
|
+
cookiesJson: () => cookies,
|
|
353
|
+
queryJson: () => query,
|
|
354
|
+
bodyJson: () => bodyJson,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function syntheticContext(
|
|
359
|
+
req: Request,
|
|
360
|
+
requestId: string,
|
|
361
|
+
options: IngressOptions,
|
|
362
|
+
responseCtx: ResponseBuildContext,
|
|
363
|
+
status: number,
|
|
364
|
+
errorCode: number,
|
|
365
|
+
): IngressContext {
|
|
366
|
+
const corsAllowed = staticCorsAllowed(options, req);
|
|
367
|
+
const variant = HV_JSON | (corsAllowed ? HV_CORS_SIMPLE : 0);
|
|
368
|
+
|
|
369
|
+
const base: IngressResult = {
|
|
370
|
+
status,
|
|
371
|
+
verdict: 1,
|
|
372
|
+
flags: 0,
|
|
373
|
+
errorCode,
|
|
374
|
+
terminal: true,
|
|
375
|
+
ok: false,
|
|
376
|
+
https: options.https === true,
|
|
377
|
+
trustedProxy:
|
|
378
|
+
options.trustProxy === true ||
|
|
379
|
+
options.trustedProxies?.enabled === true,
|
|
380
|
+
hasCookies: false,
|
|
381
|
+
hasQuery: false,
|
|
382
|
+
bodyValidJson: false,
|
|
383
|
+
schemaValid: false,
|
|
384
|
+
corsAllowed,
|
|
385
|
+
isPreflight: false,
|
|
386
|
+
rateLimited: errorCode === ERR_CODE_RATE_LIMITED,
|
|
387
|
+
rateLimit: options.rateLimit?.limit ?? 0,
|
|
388
|
+
rateRemaining: 0,
|
|
389
|
+
rateResetMs: 0,
|
|
390
|
+
retryAfterMs: 0,
|
|
391
|
+
body: EMPTY_BODY,
|
|
392
|
+
headerVariant: variant,
|
|
393
|
+
requestId,
|
|
394
|
+
bodyTruncated: false,
|
|
395
|
+
|
|
396
|
+
cookiesJson: () => "{}",
|
|
397
|
+
queryJson: () => "{}",
|
|
398
|
+
bodyJson: () => EMPTY_BODY,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
...base,
|
|
403
|
+
response: buildTerminalResponse(responseCtx, base, req, requestId),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function internalContext(
|
|
408
|
+
req: Request,
|
|
409
|
+
requestId: string,
|
|
410
|
+
options: IngressOptions,
|
|
411
|
+
responseCtx: ResponseBuildContext,
|
|
412
|
+
): IngressContext {
|
|
413
|
+
return syntheticContext(
|
|
414
|
+
req,
|
|
415
|
+
requestId,
|
|
416
|
+
options,
|
|
417
|
+
responseCtx,
|
|
418
|
+
500,
|
|
419
|
+
ERR_CODE_INTERNAL,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function staticCorsAllowed(options: IngressOptions, req: Request): boolean {
|
|
424
|
+
const cors = options.cors;
|
|
425
|
+
if (!cors) return false;
|
|
426
|
+
|
|
427
|
+
const origin = req.headers.get("origin");
|
|
428
|
+
if (!origin) return false;
|
|
429
|
+
|
|
430
|
+
const list = cors.allowOrigin;
|
|
431
|
+
|
|
432
|
+
if (!list || list.length === 0) {
|
|
433
|
+
return cors.allowCredentials !== true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (list.includes("*")) {
|
|
437
|
+
return cors.allowCredentials !== true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return list.includes(origin);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── Re-export fast.ts API for direct use ─────────────────────────
|
|
444
|
+
// Error helpers (errorCodeName/errorMessage/buildTerminalResponse), METHOD_KIND
|
|
445
|
+
// and DEFAULT_MAX_BODY_BYTES were previously internal-only; exposing them makes
|
|
446
|
+
// the numeric verdict/errorCode contract usable without reaching into internals.
|
|
447
|
+
export {
|
|
448
|
+
createIngressFast,
|
|
449
|
+
FastIngressResult,
|
|
450
|
+
errorCodeName,
|
|
451
|
+
errorMessage,
|
|
452
|
+
buildTerminalResponse,
|
|
453
|
+
generateRequestId,
|
|
454
|
+
METHOD_KIND,
|
|
455
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
456
|
+
};
|
|
457
|
+
export type { IngressFastHandler, IngressFastOptions, HeaderPlan };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export class IngressInputPacker {
|
|
2
|
+
private buf: Uint8Array;
|
|
3
|
+
private view: DataView;
|
|
4
|
+
private pos = 0;
|
|
5
|
+
|
|
6
|
+
constructor(initialSize = 65536) {
|
|
7
|
+
this.buf = new Uint8Array(Math.max(1024, initialSize));
|
|
8
|
+
this.view = new DataView(this.buf.buffer);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private ensure(additional: number): void {
|
|
12
|
+
const needed = this.pos + additional;
|
|
13
|
+
if (needed <= this.buf.byteLength) return;
|
|
14
|
+
|
|
15
|
+
let nextSize = this.buf.byteLength * 2;
|
|
16
|
+
while (nextSize < needed) {
|
|
17
|
+
nextSize *= 2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const next = new Uint8Array(nextSize);
|
|
21
|
+
next.set(this.buf.subarray(0, this.pos));
|
|
22
|
+
|
|
23
|
+
this.buf = next;
|
|
24
|
+
this.view = new DataView(next.buffer);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private writeU8(value: number): void {
|
|
28
|
+
this.ensure(1);
|
|
29
|
+
this.buf[this.pos++] = value & 0xff;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private writeLenPrefixed(bytes: Uint8Array): void {
|
|
33
|
+
this.ensure(4 + bytes.byteLength);
|
|
34
|
+
|
|
35
|
+
this.view.setUint32(this.pos, bytes.byteLength, true);
|
|
36
|
+
this.pos += 4;
|
|
37
|
+
|
|
38
|
+
this.buf.set(bytes, this.pos);
|
|
39
|
+
this.pos += bytes.byteLength;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pack(
|
|
43
|
+
methodKind: number,
|
|
44
|
+
urlBytes: Uint8Array,
|
|
45
|
+
ipBytes: Uint8Array,
|
|
46
|
+
requestIdBytes: Uint8Array,
|
|
47
|
+
headers: Uint8Array,
|
|
48
|
+
): Uint8Array {
|
|
49
|
+
this.pos = 0;
|
|
50
|
+
|
|
51
|
+
this.writeU8(methodKind);
|
|
52
|
+
this.writeLenPrefixed(urlBytes);
|
|
53
|
+
this.writeLenPrefixed(ipBytes);
|
|
54
|
+
this.writeLenPrefixed(requestIdBytes);
|
|
55
|
+
this.writeLenPrefixed(headers);
|
|
56
|
+
|
|
57
|
+
return this.buf.subarray(0, this.pos);
|
|
58
|
+
}
|
|
59
|
+
}
|