castrum 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -368
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +2763 -0
- package/dist/index.js +8707 -0
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +507 -0
- package/docs/BENCHMARKS.md +343 -0
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +81 -0
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +192 -0
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +746 -0
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +279 -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 +43 -0
- package/docs/bun-builtins-decision-matrix.md +115 -0
- package/index.ts +44 -8
- package/package.json +98 -33
- package/src/ingress/body.ts +289 -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 +639 -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 +68 -0
- package/src/ingress/index.ts +142 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +139 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +164 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +102 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +79 -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 +27 -0
- package/src/ingress/routes/index.ts +11 -0
- package/src/ingress/routes/json-write.ts +129 -0
- package/src/ingress/routes/options.ts +26 -0
- package/src/ingress/routes/read.ts +35 -0
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +475 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +191 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +220 -0
- package/src/integration/streaming.ts +78 -0
- package/src/integration/websocket.ts +93 -0
- package/src/loader/batch.ts +137 -0
- package/src/loader/cost.ts +105 -0
- package/src/loader/index.ts +745 -0
- package/src/loader/ops.ts +657 -0
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1775 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +240 -0
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +401 -0
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +87 -0
- package/src/rust-ffi/context.ts +193 -0
- package/src/rust-ffi/index.ts +51 -541
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +28 -0
- package/src/rust-ffi/scalar/crypto.ts +346 -0
- package/src/rust-ffi/scalar/factories.ts +459 -0
- package/src/rust-ffi/scalar/hashing.ts +74 -0
- package/src/rust-ffi/scalar/http.ts +337 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +325 -0
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +61 -0
- package/src/rust-ffi/scalar/payload.ts +241 -0
- package/src/rust-ffi/text.ts +121 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +73 -0
- package/src/shared/adaptive.ts +67 -0
- package/src/shared/buffer-pool.ts +264 -0
- package/src/shared/bytes.ts +108 -9
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +75 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/packed/wire.ts +504 -0
- package/src/shared/proven.ts +289 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +29 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +15 -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/data/json-rows.ts +0 -33
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
- package/src/shared/packed.ts +0 -316
package/src/ingress/index.ts
CHANGED
|
@@ -1,457 +1,194 @@
|
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 ──────────────────────────────
|
|
7
|
+
import { decoder } from '../shared/bytes'
|
|
8
|
+
import { generateRequestId } from '../shared/request-id'
|
|
9
|
+
import { readRequestBodyOnce } from './body'
|
|
10
|
+
import { ERR_CODE_BODY_TOO_LARGE, ERR_CODE_REQUEST_TIMEOUT } from './constants'
|
|
11
|
+
import { internalContext, snapshotResult, syntheticContext } from './context'
|
|
12
|
+
import { createIngressFast } from './fast'
|
|
13
|
+
import { buildResponseContext } from './headers/fast-templates'
|
|
14
|
+
import { buildTerminalResponse } from './response/terminal'
|
|
15
|
+
import { assertSyncCallback, DEFAULT_BODY_TIMEOUT_MS, DEFAULT_MAX_BODY_BYTES } from './shared'
|
|
16
|
+
import type { IngressContext, IngressHandler, IngressOptions, SyncIngressHandler } from './types'
|
|
17
|
+
|
|
18
|
+
// Runtime-driven server factory: picks Bun.serve on Bun and the node:http
|
|
19
|
+
// adapter on Node via the runtime adapter (src/runtime/server.ts). This
|
|
20
|
+
// explicit re-export SHADOWS the Bun-only `createIngressServer` from
|
|
21
|
+
// './server' (re-exported via './handlers' above) so the public API serves
|
|
22
|
+
// one backend per runtime. `createIngressServerNode` remains exported for
|
|
23
|
+
// consumers who want the Node backend pinned.
|
|
24
|
+
export { createIngressServer } from '../runtime/index'
|
|
25
|
+
export { generateRequestId } from '../shared/request-id'
|
|
26
|
+
export type { TraceContext } from '../shared/trace'
|
|
27
|
+
// W3C trace context helpers (parse `traceparent` for log/hook correlation).
|
|
50
28
|
export {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
export
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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;
|
|
29
|
+
createSpanId,
|
|
30
|
+
createTraceId,
|
|
31
|
+
parseTraceParent,
|
|
32
|
+
serializeTraceParent,
|
|
33
|
+
} from '../shared/trace'
|
|
34
|
+
// ── Re-export the full public ingress API ─────────────────────────
|
|
35
|
+
export * from './constants'
|
|
36
|
+
export * from './errors'
|
|
37
|
+
export { createIngressFast, FastIngressResult } from './fast'
|
|
38
|
+
export * from './handlers'
|
|
39
|
+
export type { CorsOptions, CorsStaticStrings } from './headers/cors'
|
|
40
|
+
export type { HeaderTemplate, ResponseBuildContext } from './headers/fast-templates'
|
|
41
|
+
export { buildResponseContext, headersForResult } from './headers/fast-templates'
|
|
42
|
+
export type { SecurityHeadersOptions } from './headers/hsts'
|
|
43
|
+
export { healthHandler, livenessHandler, readinessHandler } from './health'
|
|
44
|
+
export type { IngressMetrics } from './metrics'
|
|
45
|
+
// ── Observability (zero-dep): metrics, health probes ─────────────
|
|
46
|
+
export { createIngressMetrics, metricsHandler } from './metrics'
|
|
47
|
+
export type { IngressFastHandler, IngressFastOptions } from './options'
|
|
48
|
+
export { buildTerminalResponse } from './response/terminal'
|
|
49
|
+
export type { CreateIngressRouterOptions, IngressRouter, RouterRouteSpec } from './router'
|
|
50
|
+
export { createIngressRouter } from './router'
|
|
51
|
+
export type { HeaderPlan } from './shared'
|
|
52
|
+
export { DEFAULT_BODY_TIMEOUT_MS, DEFAULT_MAX_BODY_BYTES, METHOD_KIND } from './shared'
|
|
53
|
+
export * from './status'
|
|
54
|
+
export type {
|
|
55
|
+
IngressContext,
|
|
56
|
+
IngressHandler,
|
|
57
|
+
IngressOptions,
|
|
58
|
+
NativeRequestContext,
|
|
59
|
+
NativeResponder,
|
|
60
|
+
SyncIngressHandler,
|
|
61
|
+
TerminalStyle,
|
|
62
|
+
} from './types'
|
|
101
63
|
|
|
102
|
-
|
|
103
|
-
queryJson(): string;
|
|
104
|
-
bodyJson(): Uint8Array;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface IngressContext extends IngressResult {
|
|
108
|
-
response: Response | null;
|
|
109
|
-
}
|
|
64
|
+
// ── Sync factory ─────────────────────────────────────────────────
|
|
110
65
|
|
|
111
66
|
/**
|
|
112
|
-
* Synchronous
|
|
67
|
+
* Synchronous convenience wrapper over `createIngressFast` (path 1).
|
|
113
68
|
*
|
|
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).
|
|
69
|
+
* The callback runs on the fast-path result and MUST be synchronous. Use
|
|
70
|
+
* {@link createIngress} when you want automatic body reading + error contexts.
|
|
118
71
|
*/
|
|
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);
|
|
72
|
+
export function createIngressSync(options: IngressOptions = {}): SyncIngressHandler {
|
|
73
|
+
const fast = createIngressFast(options)
|
|
151
74
|
|
|
152
75
|
return {
|
|
153
76
|
run(req, ip, body, requestId, fn) {
|
|
154
77
|
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
|
-
});
|
|
78
|
+
const out = fn(result)
|
|
79
|
+
assertSyncCallback(out, 'createIngressSync().run()')
|
|
80
|
+
return out
|
|
81
|
+
})
|
|
167
82
|
},
|
|
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
83
|
}
|
|
218
|
-
|
|
219
|
-
return concatUint8Arrays(chunks, total);
|
|
220
84
|
}
|
|
221
85
|
|
|
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
|
-
}
|
|
86
|
+
// ── Async factory ────────────────────────────────────────────────
|
|
236
87
|
|
|
237
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Return a copy of `options` without the security-header keys.
|
|
90
|
+
*
|
|
91
|
+
* `createIngress` applies security headers itself (via `buildResponseContext`),
|
|
92
|
+
* so they must not be forwarded to the underlying fast handler, which rejects
|
|
93
|
+
* them (see `createIngressFast`).
|
|
94
|
+
*/
|
|
95
|
+
function stripSecurityOptions(options: IngressOptions): IngressOptions {
|
|
96
|
+
const copy = { ...options }
|
|
97
|
+
delete copy.security
|
|
98
|
+
delete copy.enableSecurityHeaders
|
|
99
|
+
return copy
|
|
238
100
|
}
|
|
239
101
|
|
|
240
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Async ingress factory: reads the request body (size guard + deadline) and
|
|
104
|
+
* returns a snapshot `IngressContext` with a terminal `Response`.
|
|
105
|
+
*
|
|
106
|
+
* Recommended default for serverless/edge-style `fetch` handlers. For maximum
|
|
107
|
+
* throughput use `createIngressFast` / `createIngressSync` directly.
|
|
108
|
+
*/
|
|
241
109
|
export function createIngress(options: IngressOptions = {}): IngressHandler {
|
|
242
|
-
const sync = createIngressSync(options)
|
|
110
|
+
const sync = createIngressSync(stripSecurityOptions(options))
|
|
243
111
|
|
|
244
|
-
const guard = options.enableBodySizeGuard !== false
|
|
245
|
-
const max = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
112
|
+
const guard = options.enableBodySizeGuard !== false
|
|
113
|
+
const max = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
114
|
+
const bodyTimeoutMs = options.bodyTimeoutMs ?? DEFAULT_BODY_TIMEOUT_MS
|
|
246
115
|
|
|
247
116
|
const wantsBody =
|
|
248
117
|
options.readBody === true ||
|
|
249
118
|
options.requireJsonBody === true ||
|
|
250
119
|
options.schema != null ||
|
|
251
|
-
(options.readBody !== false && guard)
|
|
120
|
+
(options.readBody !== false && guard)
|
|
252
121
|
|
|
253
|
-
const responseCtx = buildResponseContext(options)
|
|
122
|
+
const responseCtx = buildResponseContext(options)
|
|
254
123
|
|
|
255
|
-
return async function ingressAsync(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
124
|
+
return async function ingressAsync(req: Request, ip?: string): Promise<IngressContext> {
|
|
125
|
+
const requestId = options.enableRequestIds === false ? '' : decoder.decode(generateRequestId())
|
|
126
|
+
options.onRequest?.(req, requestId, ip)
|
|
127
|
+
|
|
128
|
+
// Every terminal outcome (success, 413, 408, 500) flows through here so a
|
|
129
|
+
// hook consumer sees exactly one onResponse per request.
|
|
130
|
+
const emitResponse = (context: IngressContext): IngressContext => {
|
|
131
|
+
options.onResponse?.(req, context, context.status, requestId)
|
|
132
|
+
return context
|
|
133
|
+
}
|
|
261
134
|
|
|
262
135
|
try {
|
|
263
136
|
if (guard) {
|
|
264
|
-
const rawLen = req.headers.get(
|
|
265
|
-
const contentLength = Number(rawLen ??
|
|
137
|
+
const rawLen = req.headers.get('content-length')
|
|
138
|
+
const contentLength = Number(rawLen ?? '0')
|
|
266
139
|
|
|
267
140
|
if (Number.isFinite(contentLength) && contentLength > max) {
|
|
268
|
-
return
|
|
269
|
-
req,
|
|
270
|
-
|
|
271
|
-
options,
|
|
272
|
-
responseCtx,
|
|
273
|
-
413,
|
|
274
|
-
ERR_CODE_BODY_TOO_LARGE,
|
|
275
|
-
);
|
|
141
|
+
return emitResponse(
|
|
142
|
+
syntheticContext(req, requestId, options, responseCtx, 413, ERR_CODE_BODY_TOO_LARGE),
|
|
143
|
+
)
|
|
276
144
|
}
|
|
277
145
|
}
|
|
278
146
|
|
|
279
|
-
let body: Uint8Array | null = null
|
|
147
|
+
let body: Uint8Array | null = null
|
|
280
148
|
|
|
281
149
|
if (wantsBody && req.body !== null) {
|
|
282
150
|
try {
|
|
283
|
-
body = await readRequestBodyOnce(req, max, guard)
|
|
151
|
+
body = await readRequestBodyOnce(req, max, guard, bodyTimeoutMs)
|
|
284
152
|
} catch (err) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
options,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
153
|
+
const code = (err as { code?: string } | null)?.code
|
|
154
|
+
|
|
155
|
+
if (code === 'BODY_TOO_LARGE') {
|
|
156
|
+
return emitResponse(
|
|
157
|
+
syntheticContext(req, requestId, options, responseCtx, 413, ERR_CODE_BODY_TOO_LARGE),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (code === 'REQUEST_TIMEOUT') {
|
|
162
|
+
// Match the pre-baked route factories: a body-read deadline is a
|
|
163
|
+
// 408, not a 500 (was silently swallowed as 500 before).
|
|
164
|
+
return emitResponse(
|
|
165
|
+
syntheticContext(req, requestId, options, responseCtx, 408, ERR_CODE_REQUEST_TIMEOUT),
|
|
166
|
+
)
|
|
294
167
|
}
|
|
295
168
|
|
|
296
|
-
|
|
169
|
+
// Unknown body-read failure: surface to onError, then fall through
|
|
170
|
+
// to the internal-error context below.
|
|
171
|
+
options.onError?.(err instanceof Error ? err : new Error(String(err)))
|
|
172
|
+
throw err
|
|
297
173
|
}
|
|
298
174
|
}
|
|
299
175
|
|
|
300
|
-
return
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
responseCtx,
|
|
304
|
-
snapshot,
|
|
305
|
-
req,
|
|
306
|
-
requestId,
|
|
307
|
-
);
|
|
176
|
+
return emitResponse(
|
|
177
|
+
sync.run(req, ip, body, requestId, (r) => {
|
|
178
|
+
const snapshot = snapshotResult(r)
|
|
179
|
+
const response = buildTerminalResponse(responseCtx, snapshot, req, requestId)
|
|
308
180
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
181
|
+
return {
|
|
182
|
+
...snapshot,
|
|
183
|
+
response,
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
} catch (err) {
|
|
188
|
+
// Native/other failures: never silent — report to onError and emit a
|
|
189
|
+
// structured 500 context so the request still completes observably.
|
|
190
|
+
options.onError?.(err instanceof Error ? err : new Error(String(err)))
|
|
191
|
+
return emitResponse(internalContext(req, requestId, options, responseCtx))
|
|
316
192
|
}
|
|
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
193
|
}
|
|
435
|
-
|
|
436
|
-
if (list.includes("*")) {
|
|
437
|
-
return cors.allowCredentials !== true;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return list.includes(origin);
|
|
441
194
|
}
|
|
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 };
|