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/index.ts
CHANGED
|
@@ -1,457 +1,182 @@
|
|
|
1
|
-
// src/ingress/index.ts — Public ingress API
|
|
1
|
+
// src/ingress/index.ts — Public ingress API (barrel + sync/async factories)
|
|
2
2
|
//
|
|
3
3
|
// All constants come from Rust via src/ingress/constants.ts.
|
|
4
4
|
// For maximum performance, use createIngressFast() from ./fast.ts directly.
|
|
5
5
|
// This module wraps createIngressFast with convenience async API (createIngress).
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
type ResponseBuildContext,
|
|
39
|
-
type HeaderTemplate,
|
|
40
|
-
type HeaderPlan,
|
|
41
|
-
type CorsStaticStrings,
|
|
42
|
-
type SecurityHeadersOptions,
|
|
43
|
-
type CorsOptions,
|
|
44
|
-
generateRequestId,
|
|
45
|
-
METHOD_KIND,
|
|
46
|
-
DEFAULT_MAX_BODY_BYTES,
|
|
47
|
-
} from "./fast";
|
|
48
|
-
|
|
49
|
-
// ── Re-export types from constants ──────────────────────────────
|
|
7
|
+
import { decoder } from '../shared/bytes'
|
|
8
|
+
import { generateRequestId } from '../shared/request-id'
|
|
9
|
+
import { assertSyncCallback, DEFAULT_MAX_BODY_BYTES, DEFAULT_BODY_TIMEOUT_MS } from './shared'
|
|
10
|
+
import { createIngressFast } from './fast'
|
|
11
|
+
import { buildResponseContext } from './headers/fast-templates'
|
|
12
|
+
import { buildTerminalResponse } from './response/terminal'
|
|
13
|
+
import { snapshotResult, syntheticContext, internalContext } from './context'
|
|
14
|
+
import { readRequestBodyOnce } from './body'
|
|
15
|
+
import { ERR_CODE_BODY_TOO_LARGE, ERR_CODE_REQUEST_TIMEOUT } from './constants'
|
|
16
|
+
import type { IngressOptions, IngressContext, SyncIngressHandler, IngressHandler } from './types'
|
|
17
|
+
|
|
18
|
+
// ── Re-export the full public ingress API ─────────────────────────
|
|
19
|
+
export * from './constants'
|
|
20
|
+
export * from './status'
|
|
21
|
+
export * from './errors'
|
|
22
|
+
export * from './handlers'
|
|
23
|
+
export { createIngressFast, FastIngressResult } from './fast'
|
|
24
|
+
export { buildTerminalResponse } from './response/terminal'
|
|
25
|
+
export { buildResponseContext, headersForResult } from './headers/fast-templates'
|
|
26
|
+
export { generateRequestId } from '../shared/request-id'
|
|
27
|
+
export { METHOD_KIND, DEFAULT_MAX_BODY_BYTES, DEFAULT_BODY_TIMEOUT_MS } from './shared'
|
|
28
|
+
export type { HeaderPlan } from './shared'
|
|
29
|
+
export type { ResponseBuildContext, HeaderTemplate } from './headers/fast-templates'
|
|
30
|
+
export type { CorsStaticStrings, CorsOptions } from './headers/cors'
|
|
31
|
+
export type { SecurityHeadersOptions } from './headers/hsts'
|
|
32
|
+
export type { IngressFastHandler, IngressFastOptions } from './options'
|
|
33
|
+
// ── Observability (zero-dep): metrics, health probes ─────────────
|
|
34
|
+
export { createIngressMetrics, metricsHandler } from './metrics'
|
|
35
|
+
export type { IngressMetrics } from './metrics'
|
|
36
|
+
export { livenessHandler, readinessHandler, healthHandler } from './health'
|
|
37
|
+
// W3C trace context helpers (parse `traceparent` for log/hook correlation).
|
|
50
38
|
export {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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;
|
|
39
|
+
parseTraceParent,
|
|
40
|
+
createTraceId,
|
|
41
|
+
createSpanId,
|
|
42
|
+
serializeTraceParent,
|
|
43
|
+
} from '../shared/trace'
|
|
44
|
+
export type { TraceContext } from '../shared/trace'
|
|
45
|
+
export type {
|
|
46
|
+
IngressOptions,
|
|
47
|
+
IngressContext,
|
|
48
|
+
SyncIngressHandler,
|
|
49
|
+
IngressHandler,
|
|
50
|
+
} from './types'
|
|
101
51
|
|
|
102
|
-
|
|
103
|
-
queryJson(): string;
|
|
104
|
-
bodyJson(): Uint8Array;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface IngressContext extends IngressResult {
|
|
108
|
-
response: Response | null;
|
|
109
|
-
}
|
|
52
|
+
// ── Sync factory ─────────────────────────────────────────────────
|
|
110
53
|
|
|
111
54
|
/**
|
|
112
|
-
* Synchronous
|
|
55
|
+
* Synchronous convenience wrapper over `createIngressFast` (path 1).
|
|
113
56
|
*
|
|
114
|
-
* The callback runs on the
|
|
115
|
-
*
|
|
116
|
-
* `run()` returns, so capture the fields you need inside the callback (or use
|
|
117
|
-
* the async {@link createIngress} which snapshots for you).
|
|
57
|
+
* The callback runs on the fast-path result and MUST be synchronous. Use
|
|
58
|
+
* {@link createIngress} when you want automatic body reading + error contexts.
|
|
118
59
|
*/
|
|
119
|
-
export
|
|
120
|
-
|
|
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);
|
|
60
|
+
export function createIngressSync(options: IngressOptions = {}): SyncIngressHandler {
|
|
61
|
+
const fast = createIngressFast(options)
|
|
151
62
|
|
|
152
63
|
return {
|
|
153
64
|
run(req, ip, body, requestId, fn) {
|
|
154
65
|
return fast.run(req, ip, body, requestId, (result) => {
|
|
155
|
-
const out = fn(result)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
});
|
|
66
|
+
const out = fn(result)
|
|
67
|
+
assertSyncCallback(out, 'createIngressSync().run()')
|
|
68
|
+
return out
|
|
69
|
+
})
|
|
167
70
|
},
|
|
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
71
|
}
|
|
218
|
-
|
|
219
|
-
return concatUint8Arrays(chunks, total);
|
|
220
72
|
}
|
|
221
73
|
|
|
222
|
-
|
|
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
|
-
}
|
|
74
|
+
// ── Async factory ────────────────────────────────────────────────
|
|
236
75
|
|
|
237
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Return a copy of `options` without the security-header keys.
|
|
78
|
+
*
|
|
79
|
+
* `createIngress` applies security headers itself (via `buildResponseContext`),
|
|
80
|
+
* so they must not be forwarded to the underlying fast handler, which rejects
|
|
81
|
+
* them (see `createIngressFast`).
|
|
82
|
+
*/
|
|
83
|
+
function stripSecurityOptions(options: IngressOptions): IngressOptions {
|
|
84
|
+
const copy = { ...options }
|
|
85
|
+
delete copy.security
|
|
86
|
+
delete copy.enableSecurityHeaders
|
|
87
|
+
return copy
|
|
238
88
|
}
|
|
239
89
|
|
|
240
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Async ingress factory: reads the request body (size guard + deadline) and
|
|
92
|
+
* returns a snapshot `IngressContext` with a terminal `Response`.
|
|
93
|
+
*
|
|
94
|
+
* Recommended default for serverless/edge-style `fetch` handlers. For maximum
|
|
95
|
+
* throughput use `createIngressFast` / `createIngressSync` directly.
|
|
96
|
+
*/
|
|
241
97
|
export function createIngress(options: IngressOptions = {}): IngressHandler {
|
|
242
|
-
const sync = createIngressSync(options)
|
|
98
|
+
const sync = createIngressSync(stripSecurityOptions(options))
|
|
243
99
|
|
|
244
|
-
const guard = options.enableBodySizeGuard !== false
|
|
245
|
-
const max = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
100
|
+
const guard = options.enableBodySizeGuard !== false
|
|
101
|
+
const max = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
102
|
+
const bodyTimeoutMs = options.bodyTimeoutMs ?? DEFAULT_BODY_TIMEOUT_MS
|
|
246
103
|
|
|
247
104
|
const wantsBody =
|
|
248
105
|
options.readBody === true ||
|
|
249
106
|
options.requireJsonBody === true ||
|
|
250
107
|
options.schema != null ||
|
|
251
|
-
(options.readBody !== false && guard)
|
|
108
|
+
(options.readBody !== false && guard)
|
|
252
109
|
|
|
253
|
-
const responseCtx = buildResponseContext(options)
|
|
110
|
+
const responseCtx = buildResponseContext(options)
|
|
254
111
|
|
|
255
|
-
return async function ingressAsync(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
112
|
+
return async function ingressAsync(req: Request, ip?: string): Promise<IngressContext> {
|
|
113
|
+
const requestId = options.enableRequestIds === false ? '' : decoder.decode(generateRequestId())
|
|
114
|
+
options.onRequest?.(req, requestId, ip)
|
|
115
|
+
|
|
116
|
+
// Every terminal outcome (success, 413, 408, 500) flows through here so a
|
|
117
|
+
// hook consumer sees exactly one onResponse per request.
|
|
118
|
+
const emitResponse = (context: IngressContext): IngressContext => {
|
|
119
|
+
options.onResponse?.(req, context, context.status, requestId)
|
|
120
|
+
return context
|
|
121
|
+
}
|
|
261
122
|
|
|
262
123
|
try {
|
|
263
124
|
if (guard) {
|
|
264
|
-
const rawLen = req.headers.get(
|
|
265
|
-
const contentLength = Number(rawLen ??
|
|
125
|
+
const rawLen = req.headers.get('content-length')
|
|
126
|
+
const contentLength = Number(rawLen ?? '0')
|
|
266
127
|
|
|
267
128
|
if (Number.isFinite(contentLength) && contentLength > max) {
|
|
268
|
-
return
|
|
269
|
-
req,
|
|
270
|
-
|
|
271
|
-
options,
|
|
272
|
-
responseCtx,
|
|
273
|
-
413,
|
|
274
|
-
ERR_CODE_BODY_TOO_LARGE,
|
|
275
|
-
);
|
|
129
|
+
return emitResponse(
|
|
130
|
+
syntheticContext(req, requestId, options, responseCtx, 413, ERR_CODE_BODY_TOO_LARGE),
|
|
131
|
+
)
|
|
276
132
|
}
|
|
277
133
|
}
|
|
278
134
|
|
|
279
|
-
let body: Uint8Array | null = null
|
|
135
|
+
let body: Uint8Array | null = null
|
|
280
136
|
|
|
281
137
|
if (wantsBody && req.body !== null) {
|
|
282
138
|
try {
|
|
283
|
-
body = await readRequestBodyOnce(req, max, guard)
|
|
139
|
+
body = await readRequestBodyOnce(req, max, guard, bodyTimeoutMs)
|
|
284
140
|
} catch (err) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
options,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
141
|
+
const code = (err as { code?: string } | null)?.code
|
|
142
|
+
|
|
143
|
+
if (code === 'BODY_TOO_LARGE') {
|
|
144
|
+
return emitResponse(
|
|
145
|
+
syntheticContext(req, requestId, options, responseCtx, 413, ERR_CODE_BODY_TOO_LARGE),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (code === 'REQUEST_TIMEOUT') {
|
|
150
|
+
// Match the pre-baked route factories: a body-read deadline is a
|
|
151
|
+
// 408, not a 500 (was silently swallowed as 500 before).
|
|
152
|
+
return emitResponse(
|
|
153
|
+
syntheticContext(req, requestId, options, responseCtx, 408, ERR_CODE_REQUEST_TIMEOUT),
|
|
154
|
+
)
|
|
294
155
|
}
|
|
295
156
|
|
|
296
|
-
|
|
157
|
+
// Unknown body-read failure: surface to onError, then fall through
|
|
158
|
+
// to the internal-error context below.
|
|
159
|
+
options.onError?.(err instanceof Error ? err : new Error(String(err)))
|
|
160
|
+
throw err
|
|
297
161
|
}
|
|
298
162
|
}
|
|
299
163
|
|
|
300
|
-
return
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
responseCtx,
|
|
304
|
-
snapshot,
|
|
305
|
-
req,
|
|
306
|
-
requestId,
|
|
307
|
-
);
|
|
164
|
+
return emitResponse(
|
|
165
|
+
sync.run(req, ip, body, requestId, (r) => {
|
|
166
|
+
const snapshot = snapshotResult(r)
|
|
167
|
+
const response = buildTerminalResponse(responseCtx, snapshot, req, requestId)
|
|
308
168
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
169
|
+
return {
|
|
170
|
+
...snapshot,
|
|
171
|
+
response,
|
|
172
|
+
}
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// Native/other failures: never silent — report to onError and emit a
|
|
177
|
+
// structured 500 context so the request still completes observably.
|
|
178
|
+
options.onError?.(err instanceof Error ? err : new Error(String(err)))
|
|
179
|
+
return emitResponse(internalContext(req, requestId, options, responseCtx))
|
|
316
180
|
}
|
|
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
181
|
}
|
|
435
|
-
|
|
436
|
-
if (list.includes("*")) {
|
|
437
|
-
return cors.allowCredentials !== true;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return list.includes(origin);
|
|
441
182
|
}
|
|
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 };
|