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/constants.ts
CHANGED
|
@@ -1,56 +1,228 @@
|
|
|
1
1
|
// src/ingress/constants.ts — SINGLE SOURCE OF TRUTH for ingress binary layout constants
|
|
2
|
-
// These are read from
|
|
2
|
+
// These are read from Rust to eliminate duplication across:
|
|
3
3
|
// - src/ingress/index.ts
|
|
4
4
|
// - bench/servers/ingress-server.ts
|
|
5
5
|
// - Any future consumers
|
|
6
6
|
//
|
|
7
|
-
// IMPORTANT: Do NOT hardcode these values. They come from Rust's
|
|
8
|
-
//
|
|
7
|
+
// IMPORTANT: Do NOT hardcode these values. They come from Rust's
|
|
8
|
+
// `ingress/output.rs` (the single numeric source), projected to JS via either
|
|
9
|
+
// `ingress_constants.rs` (NAPI) or the C-ABI layout blob `rust/ffi.rs`
|
|
10
|
+
// `IngressLayout` (`castrum_ingress_layout`). Both read the same numbers, so
|
|
11
|
+
// they cannot drift.
|
|
9
12
|
|
|
10
|
-
import
|
|
13
|
+
import { getAddon } from '../native'
|
|
14
|
+
import { getBunFFI, type BunFFI } from '../native/ffi'
|
|
15
|
+
|
|
16
|
+
// Transport: bun:ffi is PRIMARY on Bun, so the layout is read through the
|
|
17
|
+
// C-ABI blob WITHOUT dlopening the napi addon. Node — or forced
|
|
18
|
+
// `CASTRUM_FFI_MODE=napi`, a missing symbol, or a failed bind-time self-test —
|
|
19
|
+
// falls back to the napi addon. This is the ONE module that touches native at
|
|
20
|
+
// import time; everything else in the package stays lazy.
|
|
21
|
+
const ffi = getBunFFI()
|
|
22
|
+
|
|
23
|
+
// Slot order mirrors the `#[repr(C)] IngressLayout` struct in rust/ffi.rs
|
|
24
|
+
// (38 × u32 LE). Drift is guarded by the Rust unit test
|
|
25
|
+
// `ingress_layout_c_abi_matches_output_source` and by the pinned values in the
|
|
26
|
+
// bun:ffi bind-time self-test (a reorder → self-test fails → napi fallback).
|
|
27
|
+
const SLOT = {
|
|
28
|
+
OUT_VERDICT: 0,
|
|
29
|
+
OUT_ERROR_CODE: 1,
|
|
30
|
+
OUT_STATUS: 2,
|
|
31
|
+
OUT_FLAGS: 3,
|
|
32
|
+
OUT_RATE_LIMIT: 4,
|
|
33
|
+
OUT_RATE_REMAINING: 5,
|
|
34
|
+
OUT_RATE_RESET: 6,
|
|
35
|
+
OUT_RETRY_AFTER: 7,
|
|
36
|
+
OUT_COOKIES_JSON_LEN: 8,
|
|
37
|
+
OUT_QUERY_JSON_LEN: 9,
|
|
38
|
+
OUT_HEADER_VARIANT: 10,
|
|
39
|
+
OUT_BODY_JSON_LEN: 11,
|
|
40
|
+
OUT_DATA_START: 12,
|
|
41
|
+
FLAG_HAS_COOKIES: 13,
|
|
42
|
+
FLAG_HAS_QUERY: 14,
|
|
43
|
+
FLAG_BODY_VALID_JSON: 15,
|
|
44
|
+
FLAG_SCHEMA_VALID: 16,
|
|
45
|
+
FLAG_CORS_ALLOWED: 17,
|
|
46
|
+
FLAG_IS_PREFLIGHT: 18,
|
|
47
|
+
FLAG_RATE_LIMITED: 19,
|
|
48
|
+
FLAG_HTTPS: 20,
|
|
49
|
+
FLAG_TRUSTED_PROXY: 21,
|
|
50
|
+
FLAG_BODY_TRUNCATED: 22,
|
|
51
|
+
HV_JSON: 23,
|
|
52
|
+
HV_CORS_SIMPLE: 24,
|
|
53
|
+
HV_CORS_PREFLIGHT: 25,
|
|
54
|
+
HV_RATE_ACTIVE: 26,
|
|
55
|
+
HV_RATE_LIMITED: 27,
|
|
56
|
+
HV_COUNT: 28,
|
|
57
|
+
ERR_NONE: 29,
|
|
58
|
+
ERR_CORS_PREFLIGHT: 30,
|
|
59
|
+
ERR_RATE_LIMITED: 31,
|
|
60
|
+
ERR_BODY_TOO_LARGE: 32,
|
|
61
|
+
ERR_INVALID_JSON: 33,
|
|
62
|
+
ERR_SCHEMA_VALIDATION: 34,
|
|
63
|
+
ERR_BAD_REQUEST: 35,
|
|
64
|
+
ERR_REQUEST_TOO_LARGE: 36,
|
|
65
|
+
ERR_INTERNAL: 37,
|
|
66
|
+
} as const
|
|
67
|
+
|
|
68
|
+
type Layout = Record<keyof typeof SLOT, number>
|
|
69
|
+
|
|
70
|
+
/** Read the layout via the bun:ffi C-ABI blob (Bun primary path). */
|
|
71
|
+
function readLayoutViaFfi(ffiLive: BunFFI): Layout {
|
|
72
|
+
const buf = new Uint8Array(38 * 4)
|
|
73
|
+
ffiLive.ingressLayout(buf)
|
|
74
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
75
|
+
const out = {} as Layout
|
|
76
|
+
for (const key of Object.keys(SLOT) as (keyof typeof SLOT)[]) {
|
|
77
|
+
out[key] = view.getUint32(SLOT[key] * 4, true)
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read the layout via the napi addon (Node / fallback). */
|
|
83
|
+
function readLayoutViaNapi(): Layout {
|
|
84
|
+
const addon = getAddon()
|
|
85
|
+
return {
|
|
86
|
+
OUT_VERDICT: addon.INGRESS_OUT_VERDICT,
|
|
87
|
+
OUT_ERROR_CODE: addon.INGRESS_OUT_ERROR_CODE,
|
|
88
|
+
OUT_STATUS: addon.INGRESS_OUT_STATUS,
|
|
89
|
+
OUT_FLAGS: addon.INGRESS_OUT_FLAGS,
|
|
90
|
+
OUT_RATE_LIMIT: addon.INGRESS_OUT_RATE_LIMIT,
|
|
91
|
+
OUT_RATE_REMAINING: addon.INGRESS_OUT_RATE_REMAINING,
|
|
92
|
+
OUT_RATE_RESET: addon.INGRESS_OUT_RATE_RESET,
|
|
93
|
+
OUT_RETRY_AFTER: addon.INGRESS_OUT_RETRY_AFTER,
|
|
94
|
+
OUT_COOKIES_JSON_LEN: addon.INGRESS_OUT_COOKIES_JSON_LEN,
|
|
95
|
+
OUT_QUERY_JSON_LEN: addon.INGRESS_OUT_QUERY_JSON_LEN,
|
|
96
|
+
OUT_HEADER_VARIANT: addon.INGRESS_OUT_HEADER_VARIANT,
|
|
97
|
+
OUT_BODY_JSON_LEN: addon.INGRESS_OUT_BODY_JSON_LEN,
|
|
98
|
+
OUT_DATA_START: addon.INGRESS_OUT_DATA_START,
|
|
99
|
+
FLAG_HAS_COOKIES: addon.INGRESS_FLAG_HAS_COOKIES,
|
|
100
|
+
FLAG_HAS_QUERY: addon.INGRESS_FLAG_HAS_QUERY,
|
|
101
|
+
FLAG_BODY_VALID_JSON: addon.INGRESS_FLAG_BODY_VALID_JSON,
|
|
102
|
+
FLAG_SCHEMA_VALID: addon.INGRESS_FLAG_SCHEMA_VALID,
|
|
103
|
+
FLAG_CORS_ALLOWED: addon.INGRESS_FLAG_CORS_ALLOWED,
|
|
104
|
+
FLAG_IS_PREFLIGHT: addon.INGRESS_FLAG_IS_PREFLIGHT,
|
|
105
|
+
FLAG_RATE_LIMITED: addon.INGRESS_FLAG_RATE_LIMITED,
|
|
106
|
+
FLAG_HTTPS: addon.INGRESS_FLAG_HTTPS,
|
|
107
|
+
FLAG_TRUSTED_PROXY: addon.INGRESS_FLAG_TRUSTED_PROXY,
|
|
108
|
+
FLAG_BODY_TRUNCATED: addon.INGRESS_FLAG_BODY_TRUNCATED,
|
|
109
|
+
HV_JSON: addon.INGRESS_HV_JSON,
|
|
110
|
+
HV_CORS_SIMPLE: addon.INGRESS_HV_CORS_SIMPLE,
|
|
111
|
+
HV_CORS_PREFLIGHT: addon.INGRESS_HV_CORS_PREFLIGHT,
|
|
112
|
+
HV_RATE_ACTIVE: addon.INGRESS_HV_RATE_ACTIVE,
|
|
113
|
+
HV_RATE_LIMITED: addon.INGRESS_HV_RATE_LIMITED,
|
|
114
|
+
HV_COUNT: addon.INGRESS_HV_COUNT,
|
|
115
|
+
ERR_NONE: addon.INGRESS_ERR_NONE,
|
|
116
|
+
ERR_CORS_PREFLIGHT: addon.INGRESS_ERR_CORS_PREFLIGHT,
|
|
117
|
+
ERR_RATE_LIMITED: addon.INGRESS_ERR_RATE_LIMITED,
|
|
118
|
+
ERR_BODY_TOO_LARGE: addon.INGRESS_ERR_BODY_TOO_LARGE,
|
|
119
|
+
ERR_INVALID_JSON: addon.INGRESS_ERR_INVALID_JSON,
|
|
120
|
+
ERR_SCHEMA_VALIDATION: addon.INGRESS_ERR_SCHEMA_VALIDATION,
|
|
121
|
+
ERR_BAD_REQUEST: addon.INGRESS_ERR_BAD_REQUEST,
|
|
122
|
+
ERR_REQUEST_TOO_LARGE: addon.INGRESS_ERR_REQUEST_TOO_LARGE,
|
|
123
|
+
ERR_INTERNAL: addon.INGRESS_ERR_INTERNAL,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const L: Layout = ffi !== null ? readLayoutViaFfi(ffi) : readLayoutViaNapi()
|
|
11
128
|
|
|
12
129
|
// ── Output buffer layout ───────────────────────────────────
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
export const
|
|
22
|
-
|
|
23
|
-
export const
|
|
24
|
-
|
|
25
|
-
export const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export const
|
|
30
|
-
|
|
31
|
-
export const
|
|
32
|
-
|
|
33
|
-
export const
|
|
34
|
-
|
|
35
|
-
export const
|
|
36
|
-
|
|
37
|
-
export const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export const
|
|
42
|
-
|
|
43
|
-
export const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
export const
|
|
50
|
-
|
|
51
|
-
export const
|
|
52
|
-
|
|
53
|
-
export const
|
|
54
|
-
|
|
55
|
-
export const
|
|
56
|
-
|
|
130
|
+
// Byte offsets into the fixed header of the native ingress OUTPUT buffer.
|
|
131
|
+
// The header occupies [0, OUT_DATA_START); the cookie/query/body JSON sections
|
|
132
|
+
// follow at OUT_DATA_START (see decode/packed-sections.ts). Read through the
|
|
133
|
+
// C-ABI layout blob (Bun) or napi (Node) — never hardcoded.
|
|
134
|
+
|
|
135
|
+
/** Offset of the verdict byte (0 = accepted, nonzero = terminal error). */
|
|
136
|
+
export const OUT_VERDICT: number = L.OUT_VERDICT
|
|
137
|
+
/** Offset of the error-code byte (see ERR_CODE_* below). */
|
|
138
|
+
export const OUT_ERROR_CODE: number = L.OUT_ERROR_CODE
|
|
139
|
+
/** Offset of the raw u16 status. */
|
|
140
|
+
export const OUT_STATUS: number = L.OUT_STATUS
|
|
141
|
+
/** Offset of the u32 flags word (see FLAG_* below). */
|
|
142
|
+
export const OUT_FLAGS: number = L.OUT_FLAGS
|
|
143
|
+
/** Offset of the u32 rate-limit budget (U32_MAX = disabled). */
|
|
144
|
+
export const OUT_RATE_LIMIT: number = L.OUT_RATE_LIMIT
|
|
145
|
+
/** Offset of the u32 rate-limit remaining count. */
|
|
146
|
+
export const OUT_RATE_REMAINING: number = L.OUT_RATE_REMAINING
|
|
147
|
+
/** Offset of the u64 rate-limit window reset (ms epoch). */
|
|
148
|
+
export const OUT_RATE_RESET: number = L.OUT_RATE_RESET
|
|
149
|
+
/** Offset of the u64 Retry-After value (ms). */
|
|
150
|
+
export const OUT_RETRY_AFTER: number = L.OUT_RETRY_AFTER
|
|
151
|
+
/** Offset of the u32 cookies-JSON section length. */
|
|
152
|
+
export const OUT_COOKIES_JSON_LEN: number = L.OUT_COOKIES_JSON_LEN
|
|
153
|
+
/** Offset of the u32 query-JSON section length. */
|
|
154
|
+
export const OUT_QUERY_JSON_LEN: number = L.OUT_QUERY_JSON_LEN
|
|
155
|
+
/** Offset of the header-template variant byte (see HV_* below). */
|
|
156
|
+
export const OUT_HEADER_VARIANT: number = L.OUT_HEADER_VARIANT
|
|
157
|
+
/** Offset of the u32 body-JSON section length. */
|
|
158
|
+
export const OUT_BODY_JSON_LEN: number = L.OUT_BODY_JSON_LEN
|
|
159
|
+
/** Byte offset where the variable-length JSON sections begin. */
|
|
160
|
+
export const OUT_DATA_START: number = L.OUT_DATA_START
|
|
161
|
+
|
|
162
|
+
// ── Flags (bits within the OUT_FLAGS u32) ──────────────────
|
|
163
|
+
/** Request carried a cookie header (cookie-JSON section present). */
|
|
164
|
+
export const FLAG_HAS_COOKIES: number = L.FLAG_HAS_COOKIES
|
|
165
|
+
/** Request carried a query string (query-JSON section present). */
|
|
166
|
+
export const FLAG_HAS_QUERY: number = L.FLAG_HAS_QUERY
|
|
167
|
+
/** The request body parsed as valid JSON. */
|
|
168
|
+
export const FLAG_BODY_VALID_JSON: number = L.FLAG_BODY_VALID_JSON
|
|
169
|
+
/** The request body validated against the ingress schema. */
|
|
170
|
+
export const FLAG_SCHEMA_VALID: number = L.FLAG_SCHEMA_VALID
|
|
171
|
+
/** The CORS engine allowed the request origin. */
|
|
172
|
+
export const FLAG_CORS_ALLOWED: number = L.FLAG_CORS_ALLOWED
|
|
173
|
+
/** The request was a CORS preflight (OPTIONS + request-method). */
|
|
174
|
+
export const FLAG_IS_PREFLIGHT: number = L.FLAG_IS_PREFLIGHT
|
|
175
|
+
/** The request was rate-limited. */
|
|
176
|
+
export const FLAG_RATE_LIMITED: number = L.FLAG_RATE_LIMITED
|
|
177
|
+
/** The request arrived over HTTPS (trusted-proxy scheme). */
|
|
178
|
+
export const FLAG_HTTPS: number = L.FLAG_HTTPS
|
|
179
|
+
/** The request came from a trusted proxy (per the proxy config). */
|
|
180
|
+
export const FLAG_TRUSTED_PROXY: number = L.FLAG_TRUSTED_PROXY
|
|
181
|
+
/** A declared output section overran the buffer (truncated). */
|
|
182
|
+
export const FLAG_BODY_TRUNCATED: number = L.FLAG_BODY_TRUNCATED
|
|
183
|
+
|
|
184
|
+
// ── Header variant bits (bits within the OUT_HEADER_VARIANT byte) ──
|
|
185
|
+
/** Response body is JSON (`content-type: application/json`). */
|
|
186
|
+
export const HV_JSON: number = L.HV_JSON
|
|
187
|
+
/** CORS simple-request variant (Origin echoed + vary headers). */
|
|
188
|
+
export const HV_CORS_SIMPLE: number = L.HV_CORS_SIMPLE
|
|
189
|
+
/** CORS preflight variant (allow-methods/headers/max-age). */
|
|
190
|
+
export const HV_CORS_PREFLIGHT: number = L.HV_CORS_PREFLIGHT
|
|
191
|
+
/** Rate-limit active variant (`ratelimit-limit` header). */
|
|
192
|
+
export const HV_RATE_ACTIVE: number = L.HV_RATE_ACTIVE
|
|
193
|
+
/** Rate-limited variant (Retry-After emitted). */
|
|
194
|
+
export const HV_RATE_LIMITED: number = L.HV_RATE_LIMITED
|
|
195
|
+
/** Total number of header-variant combinations (32). */
|
|
196
|
+
export const HV_COUNT: number = L.HV_COUNT
|
|
197
|
+
|
|
198
|
+
// ── Error codes (bytes in OUT_ERROR_CODE) ──────────────────
|
|
199
|
+
/** No error (accepted). */
|
|
200
|
+
export const ERR_CODE_NONE: number = L.ERR_NONE
|
|
201
|
+
/** CORS preflight rejected. */
|
|
202
|
+
export const ERR_CODE_CORS_PREFLIGHT: number = L.ERR_CORS_PREFLIGHT
|
|
203
|
+
/** Rate limit exceeded. */
|
|
204
|
+
export const ERR_CODE_RATE_LIMITED: number = L.ERR_RATE_LIMITED
|
|
205
|
+
/** Request body exceeded the size guard. */
|
|
206
|
+
export const ERR_CODE_BODY_TOO_LARGE: number = L.ERR_BODY_TOO_LARGE
|
|
207
|
+
/** Request body is not valid JSON. */
|
|
208
|
+
export const ERR_CODE_INVALID_JSON: number = L.ERR_INVALID_JSON
|
|
209
|
+
/** Request body failed schema validation. */
|
|
210
|
+
export const ERR_CODE_SCHEMA_VALIDATION: number = L.ERR_SCHEMA_VALIDATION
|
|
211
|
+
/** Malformed request (bad request line / headers). */
|
|
212
|
+
export const ERR_CODE_BAD_REQUEST: number = L.ERR_BAD_REQUEST
|
|
213
|
+
/** Request headers/URL exceeded the configured limits. */
|
|
214
|
+
export const ERR_CODE_REQUEST_TOO_LARGE: number = L.ERR_REQUEST_TOO_LARGE
|
|
215
|
+
/** Internal pipeline failure (maps to 500). */
|
|
216
|
+
export const ERR_CODE_INTERNAL: number = L.ERR_INTERNAL
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* JS-only error-code sentinel for a request-body read timeout in the async
|
|
220
|
+
* `createIngress` path.
|
|
221
|
+
*
|
|
222
|
+
* The native pipeline never emits this value — it is a JS body-reader error
|
|
223
|
+
* (`REQUEST_TIMEOUT` from `readBodyWithLimit`) mapped onto the numeric
|
|
224
|
+
* error-code contract so the shared status/name/message helpers stay
|
|
225
|
+
* consistent. Deliberately not sourced from Rust (see the header comment:
|
|
226
|
+
* numeric layout values come from the addon; this one is a JS-level error).
|
|
227
|
+
*/
|
|
228
|
+
export const ERR_CODE_REQUEST_TIMEOUT: number = -1
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/ingress/context.ts — Result snapshotting + synthetic/error context
|
|
2
|
+
// builders for the async ingress API.
|
|
3
|
+
|
|
4
|
+
import type { ResponseBuildContext } from './headers/fast-templates'
|
|
5
|
+
import { buildTerminalResponse } from './response/terminal'
|
|
6
|
+
import { ERR_CODE_INTERNAL, ERR_CODE_RATE_LIMITED, HV_JSON, HV_CORS_SIMPLE } from './constants'
|
|
7
|
+
import type { IngressOptions, IngressResult, IngressContext } from './types'
|
|
8
|
+
|
|
9
|
+
const EMPTY_BODY = new Uint8Array(0)
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Deep-snapshot a result so the caller can use it after `run()` returns.
|
|
13
|
+
*
|
|
14
|
+
* Note on aliasing: the returned `body` field aliases the request body buffer
|
|
15
|
+
* (`r.body`) — it is NOT copied — while `bodyJson()` returns a stable copy
|
|
16
|
+
* (captured once at snapshot time, safe after the live result is invalidated).
|
|
17
|
+
* In the async `createIngress` path the aliased buffer is the fully-
|
|
18
|
+
* materialized request body, which lives as long as the request and is never
|
|
19
|
+
* reused, so the alias is safe. Treat `body` as read-only; use `bodyJson()`
|
|
20
|
+
* for a defensive copy.
|
|
21
|
+
*/
|
|
22
|
+
export function snapshotResult(r: IngressResult): IngressResult {
|
|
23
|
+
const cookies = r.cookiesJson()
|
|
24
|
+
const query = r.queryJson()
|
|
25
|
+
const bodyJson = r.bodyJson().slice()
|
|
26
|
+
const body = r.body
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
status: r.status,
|
|
30
|
+
verdict: r.verdict,
|
|
31
|
+
flags: r.flags,
|
|
32
|
+
errorCode: r.errorCode,
|
|
33
|
+
terminal: r.terminal,
|
|
34
|
+
ok: r.ok,
|
|
35
|
+
https: r.https,
|
|
36
|
+
trustedProxy: r.trustedProxy,
|
|
37
|
+
hasCookies: r.hasCookies,
|
|
38
|
+
hasQuery: r.hasQuery,
|
|
39
|
+
bodyValidJson: r.bodyValidJson,
|
|
40
|
+
schemaValid: r.schemaValid,
|
|
41
|
+
corsAllowed: r.corsAllowed,
|
|
42
|
+
isPreflight: r.isPreflight,
|
|
43
|
+
rateLimited: r.rateLimited,
|
|
44
|
+
rateLimit: r.rateLimit,
|
|
45
|
+
rateRemaining: r.rateRemaining,
|
|
46
|
+
rateResetMs: r.rateResetMs,
|
|
47
|
+
retryAfterMs: r.retryAfterMs,
|
|
48
|
+
body,
|
|
49
|
+
headerVariant: r.headerVariant,
|
|
50
|
+
requestId: r.requestId,
|
|
51
|
+
bodyTruncated: r.bodyTruncated,
|
|
52
|
+
|
|
53
|
+
cookiesJson: () => cookies,
|
|
54
|
+
queryJson: () => query,
|
|
55
|
+
bodyJson: () => bodyJson,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Build a synthetic (terminal) context for body-size / error short-circuits. */
|
|
60
|
+
export function syntheticContext(
|
|
61
|
+
req: Request,
|
|
62
|
+
requestId: string,
|
|
63
|
+
options: IngressOptions,
|
|
64
|
+
responseCtx: ResponseBuildContext,
|
|
65
|
+
status: number,
|
|
66
|
+
errorCode: number,
|
|
67
|
+
): IngressContext {
|
|
68
|
+
const corsAllowed = staticCorsAllowed(options, req)
|
|
69
|
+
const variant = HV_JSON | (corsAllowed ? HV_CORS_SIMPLE : 0)
|
|
70
|
+
|
|
71
|
+
const base: IngressResult = {
|
|
72
|
+
status,
|
|
73
|
+
verdict: 1,
|
|
74
|
+
flags: 0,
|
|
75
|
+
errorCode,
|
|
76
|
+
terminal: true,
|
|
77
|
+
ok: false,
|
|
78
|
+
https: options.https === true,
|
|
79
|
+
trustedProxy: options.trustProxy === true || options.trustedProxies?.enabled === true,
|
|
80
|
+
hasCookies: false,
|
|
81
|
+
hasQuery: false,
|
|
82
|
+
bodyValidJson: false,
|
|
83
|
+
schemaValid: false,
|
|
84
|
+
corsAllowed,
|
|
85
|
+
isPreflight: false,
|
|
86
|
+
rateLimited: errorCode === ERR_CODE_RATE_LIMITED,
|
|
87
|
+
rateLimit: options.rateLimit?.limit ?? 0,
|
|
88
|
+
rateRemaining: 0,
|
|
89
|
+
rateResetMs: 0,
|
|
90
|
+
retryAfterMs: 0,
|
|
91
|
+
body: EMPTY_BODY,
|
|
92
|
+
headerVariant: variant,
|
|
93
|
+
requestId,
|
|
94
|
+
bodyTruncated: false,
|
|
95
|
+
|
|
96
|
+
cookiesJson: () => '{}',
|
|
97
|
+
queryJson: () => '{}',
|
|
98
|
+
bodyJson: () => EMPTY_BODY,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
...base,
|
|
103
|
+
response: buildTerminalResponse(responseCtx, base, req, requestId),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Build the internal-error context (status 500). */
|
|
108
|
+
export function internalContext(
|
|
109
|
+
req: Request,
|
|
110
|
+
requestId: string,
|
|
111
|
+
options: IngressOptions,
|
|
112
|
+
responseCtx: ResponseBuildContext,
|
|
113
|
+
): IngressContext {
|
|
114
|
+
return syntheticContext(req, requestId, options, responseCtx, 500, ERR_CODE_INTERNAL)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Statically determine whether the configured CORS policy allows `req`. */
|
|
118
|
+
export function staticCorsAllowed(options: IngressOptions, req: Request): boolean {
|
|
119
|
+
const cors = options.cors
|
|
120
|
+
if (!cors) return false
|
|
121
|
+
|
|
122
|
+
const origin = req.headers.get('origin')
|
|
123
|
+
if (!origin) return false
|
|
124
|
+
|
|
125
|
+
const list = cors.allowOrigin
|
|
126
|
+
|
|
127
|
+
if (!list || list.length === 0) {
|
|
128
|
+
return cors.allowCredentials !== true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (list.includes('*')) {
|
|
132
|
+
return cors.allowCredentials !== true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return list.includes(origin)
|
|
136
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/ingress/decode/baked-result.ts — Zero-alloc result decoder (pre-baked
|
|
2
|
+
// path).
|
|
3
|
+
//
|
|
4
|
+
// Decodes the native Ingress packed output (OUT_* layout in ./constants.ts)
|
|
5
|
+
// into the pre-baked handler result, which carries the benchmark wire format
|
|
6
|
+
// ({"ok":false,"error":{...}}).
|
|
7
|
+
//
|
|
8
|
+
// NOTE: this is the pre-baked-path decoder with its own status normalization.
|
|
9
|
+
// The fast path has its own decoder (./fast-result.ts) — do not unify them
|
|
10
|
+
// (see AGENTS.md). The two decoders DO share their section/rate-limit plumbing
|
|
11
|
+
// via ./result-base.ts.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
OUT_VERDICT,
|
|
15
|
+
OUT_FLAGS,
|
|
16
|
+
OUT_RATE_LIMIT,
|
|
17
|
+
OUT_RATE_REMAINING,
|
|
18
|
+
OUT_COOKIES_JSON_LEN,
|
|
19
|
+
OUT_QUERY_JSON_LEN,
|
|
20
|
+
OUT_HEADER_VARIANT,
|
|
21
|
+
OUT_BODY_JSON_LEN,
|
|
22
|
+
OUT_DATA_START,
|
|
23
|
+
FLAG_BODY_VALID_JSON,
|
|
24
|
+
FLAG_SCHEMA_VALID,
|
|
25
|
+
FLAG_CORS_ALLOWED,
|
|
26
|
+
FLAG_IS_PREFLIGHT,
|
|
27
|
+
FLAG_RATE_LIMITED,
|
|
28
|
+
FLAG_TRUSTED_PROXY,
|
|
29
|
+
FLAG_BODY_TRUNCATED,
|
|
30
|
+
HV_JSON,
|
|
31
|
+
ERR_CODE_INTERNAL as ERROR_CODE_INTERNAL,
|
|
32
|
+
} from '../constants'
|
|
33
|
+
import { sectionLayout } from './packed-sections'
|
|
34
|
+
import { isValidResponseStatus } from '../status'
|
|
35
|
+
import { IngressResultBase } from './result-base'
|
|
36
|
+
|
|
37
|
+
/** Zero-alloc, reusable result decoder for the pre-baked handler path. */
|
|
38
|
+
export class BakedIngressResult extends IngressResultBase {
|
|
39
|
+
refresh(buf: Uint8Array, body: Uint8Array, view: DataView): void {
|
|
40
|
+
// Defensive: the native core always writes the full fixed header
|
|
41
|
+
// (>= OUT_DATA_START bytes) before returning `written`. The cached
|
|
42
|
+
// whole-buffer DataView (see viewForArrayBuffer) would otherwise decode
|
|
43
|
+
// stale bytes if this contract is ever violated — treat as internal error.
|
|
44
|
+
if (buf.byteLength < OUT_DATA_START) {
|
|
45
|
+
this.setInternalError()
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
this.body = body
|
|
49
|
+
|
|
50
|
+
const h0 = view.getUint32(OUT_VERDICT, true)
|
|
51
|
+
const h1 = view.getUint32(OUT_FLAGS, true)
|
|
52
|
+
const h2 = view.getUint32(OUT_RATE_LIMIT, true)
|
|
53
|
+
const h3 = view.getUint32(OUT_RATE_REMAINING, true)
|
|
54
|
+
|
|
55
|
+
const cookiesLenRaw = view.getUint32(OUT_COOKIES_JSON_LEN, true)
|
|
56
|
+
const queryLenRaw = view.getUint32(OUT_QUERY_JSON_LEN, true)
|
|
57
|
+
const headerVariant = view.getUint8(OUT_HEADER_VARIANT)
|
|
58
|
+
const bodyJsonLenRaw = view.getUint32(OUT_BODY_JSON_LEN, true)
|
|
59
|
+
|
|
60
|
+
// Bounds-checked section offsets shared with the fast decoder: a
|
|
61
|
+
// malformed/truncated buffer can never produce slices past its end.
|
|
62
|
+
const layout = sectionLayout(buf.byteLength, cookiesLenRaw, queryLenRaw, bodyJsonLenRaw)
|
|
63
|
+
|
|
64
|
+
if (h0 === 0 && h1 === 0) {
|
|
65
|
+
this.verdict = 1
|
|
66
|
+
this.errorCode = ERROR_CODE_INTERNAL
|
|
67
|
+
this.status = 500
|
|
68
|
+
} else {
|
|
69
|
+
this.verdict = h0 & 0xff
|
|
70
|
+
this.errorCode = (h0 >>> 8) & 0xff
|
|
71
|
+
|
|
72
|
+
const rawStatus = (h0 >>> 16) & 0xffff
|
|
73
|
+
// Reuse the shared status-validity helper instead of re-inlining the
|
|
74
|
+
// 101/200-599 check (single source of truth in ../status.ts).
|
|
75
|
+
this.status = isValidResponseStatus(rawStatus) ? rawStatus : 500
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const flags = h1
|
|
79
|
+
|
|
80
|
+
this.rateRemaining = h3
|
|
81
|
+
|
|
82
|
+
this.setRateWindow(h2, (flags & FLAG_RATE_LIMITED) !== 0, view)
|
|
83
|
+
|
|
84
|
+
this.headerVariant = headerVariant
|
|
85
|
+
|
|
86
|
+
// The baked path trusts the already-clamped section lengths, so pass the
|
|
87
|
+
// safe body len here (the fast path re-checks bounds on read instead).
|
|
88
|
+
this.setSections(buf, layout, layout.safeBodyJsonLen)
|
|
89
|
+
|
|
90
|
+
this.updateOkTerminal()
|
|
91
|
+
|
|
92
|
+
this.isPreflight = (flags & FLAG_IS_PREFLIGHT) !== 0
|
|
93
|
+
this.corsAllowed = (flags & FLAG_CORS_ALLOWED) !== 0
|
|
94
|
+
this.rateLimited = (flags & FLAG_RATE_LIMITED) !== 0
|
|
95
|
+
this.trustedProxy = (flags & FLAG_TRUSTED_PROXY) !== 0
|
|
96
|
+
this.bodyValidJson = (flags & FLAG_BODY_VALID_JSON) !== 0
|
|
97
|
+
this.schemaValid = (flags & FLAG_SCHEMA_VALID) !== 0
|
|
98
|
+
|
|
99
|
+
this.bodyTruncated = (flags & FLAG_BODY_TRUNCATED) !== 0 || layout.truncated
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
invalidate(): void {
|
|
103
|
+
// The baked empty state is neutral (0/0/0) — exactly what resetShared sets.
|
|
104
|
+
this.resetShared()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setInternalError(): void {
|
|
108
|
+
this.invalidate()
|
|
109
|
+
this.status = 500
|
|
110
|
+
this.verdict = 1
|
|
111
|
+
this.errorCode = ERROR_CODE_INTERNAL
|
|
112
|
+
this.headerVariant = HV_JSON
|
|
113
|
+
this.terminal = true
|
|
114
|
+
this.ok = false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
bodyJson(copy: boolean): Uint8Array {
|
|
118
|
+
const slice = this.bodyJsonSlice()
|
|
119
|
+
return (copy ? slice.slice() : slice) as Uint8Array
|
|
120
|
+
}
|
|
121
|
+
}
|