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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// src/ingress/decode/fast-result.ts — Zero-alloc result decoder (fast path).
|
|
2
|
+
//
|
|
3
|
+
// Decodes the native Ingress packed output (OUT_* layout in ./constants.ts)
|
|
4
|
+
// lazily: slices are stored, strings are decoded only on demand via
|
|
5
|
+
// cookiesJson()/queryJson()/bodyJson().
|
|
6
|
+
//
|
|
7
|
+
// NOTE: this is the fast-path decoder ({"error":{...}} wire format). The
|
|
8
|
+
// pre-baked path has its own decoder (./baked-result.ts) with a different
|
|
9
|
+
// wire format and status normalization — do not unify them (see AGENTS.md).
|
|
10
|
+
// The two decoders DO share their section/rate-limit plumbing via
|
|
11
|
+
// ./result-base.ts.
|
|
12
|
+
|
|
13
|
+
import { viewForArrayBuffer } from '../../shared/bytes'
|
|
14
|
+
import {
|
|
15
|
+
OUT_VERDICT,
|
|
16
|
+
OUT_ERROR_CODE,
|
|
17
|
+
OUT_STATUS,
|
|
18
|
+
OUT_FLAGS,
|
|
19
|
+
OUT_RATE_LIMIT,
|
|
20
|
+
OUT_RATE_REMAINING,
|
|
21
|
+
OUT_COOKIES_JSON_LEN,
|
|
22
|
+
OUT_QUERY_JSON_LEN,
|
|
23
|
+
OUT_HEADER_VARIANT,
|
|
24
|
+
OUT_BODY_JSON_LEN,
|
|
25
|
+
OUT_DATA_START,
|
|
26
|
+
FLAG_HAS_COOKIES,
|
|
27
|
+
FLAG_HAS_QUERY,
|
|
28
|
+
FLAG_BODY_VALID_JSON,
|
|
29
|
+
FLAG_SCHEMA_VALID,
|
|
30
|
+
FLAG_CORS_ALLOWED,
|
|
31
|
+
FLAG_IS_PREFLIGHT,
|
|
32
|
+
FLAG_RATE_LIMITED,
|
|
33
|
+
FLAG_HTTPS,
|
|
34
|
+
FLAG_TRUSTED_PROXY,
|
|
35
|
+
FLAG_BODY_TRUNCATED,
|
|
36
|
+
ERR_CODE_INTERNAL,
|
|
37
|
+
} from '../constants'
|
|
38
|
+
import { normalizeResponseStatus } from '../status'
|
|
39
|
+
import { sectionLayout } from './packed-sections'
|
|
40
|
+
import { IngressResultBase } from './result-base'
|
|
41
|
+
|
|
42
|
+
/** Zero-alloc, lazy-decoding view over a native Ingress output buffer. */
|
|
43
|
+
export class FastIngressResult extends IngressResultBase {
|
|
44
|
+
// Fast path's empty state is an internal-error shape (500 / verdict 1 /
|
|
45
|
+
// internal error-code). The baked decoder's empty state is neutral (0/0/0).
|
|
46
|
+
override status = 500
|
|
47
|
+
override verdict = 1
|
|
48
|
+
override errorCode = ERR_CODE_INTERNAL
|
|
49
|
+
flags = 0
|
|
50
|
+
https = false
|
|
51
|
+
hasCookies = false
|
|
52
|
+
hasQuery = false
|
|
53
|
+
rateLimit = 0
|
|
54
|
+
requestId = ''
|
|
55
|
+
|
|
56
|
+
refresh(buf: Uint8Array, body: Uint8Array, requestId: string): void {
|
|
57
|
+
// Defensive: the native core always writes the full fixed header
|
|
58
|
+
// (>= OUT_DATA_START bytes) before returning `written`. The cached
|
|
59
|
+
// whole-buffer DataView below would otherwise decode stale bytes if this
|
|
60
|
+
// contract is ever violated — treat it as an internal error instead.
|
|
61
|
+
if (buf.byteLength < OUT_DATA_START) {
|
|
62
|
+
this.setInternalError(requestId)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
this._buf = buf
|
|
66
|
+
this.body = body
|
|
67
|
+
this.requestId = requestId
|
|
68
|
+
|
|
69
|
+
// Cached per-ArrayBuffer DataView (respects the subarray byteOffset): no
|
|
70
|
+
// per-request view alloc on the offset-0 production buffers.
|
|
71
|
+
const dv = viewForArrayBuffer(buf.buffer, buf.byteOffset)
|
|
72
|
+
|
|
73
|
+
const rawVerdict = dv.getUint8(OUT_VERDICT)
|
|
74
|
+
const rawErrorCode = dv.getUint8(OUT_ERROR_CODE)
|
|
75
|
+
const rawStatus = dv.getUint16(OUT_STATUS, true)
|
|
76
|
+
const flags = dv.getUint32(OUT_FLAGS, true)
|
|
77
|
+
|
|
78
|
+
const rateLimit = dv.getUint32(OUT_RATE_LIMIT, true)
|
|
79
|
+
const rateRemaining = dv.getUint32(OUT_RATE_REMAINING, true)
|
|
80
|
+
|
|
81
|
+
const cookiesJsonLen = dv.getUint32(OUT_COOKIES_JSON_LEN, true)
|
|
82
|
+
const queryJsonLen = dv.getUint32(OUT_QUERY_JSON_LEN, true)
|
|
83
|
+
|
|
84
|
+
const headerVariant = dv.getUint8(OUT_HEADER_VARIANT)
|
|
85
|
+
const bodyJsonLen = dv.getUint32(OUT_BODY_JSON_LEN, true)
|
|
86
|
+
|
|
87
|
+
const preflightAllowed = (flags & FLAG_IS_PREFLIGHT) !== 0 && (flags & FLAG_CORS_ALLOWED) !== 0
|
|
88
|
+
|
|
89
|
+
const normalizedStatus = normalizeResponseStatus(rawStatus, rawErrorCode, preflightAllowed)
|
|
90
|
+
|
|
91
|
+
const uninitialized = rawVerdict === 0 && rawErrorCode === 0 && rawStatus === 0 && flags === 0
|
|
92
|
+
|
|
93
|
+
if (uninitialized) {
|
|
94
|
+
this.status = 500
|
|
95
|
+
this.verdict = 1
|
|
96
|
+
this.errorCode = ERR_CODE_INTERNAL
|
|
97
|
+
} else {
|
|
98
|
+
this.status = normalizedStatus
|
|
99
|
+
this.verdict = rawVerdict
|
|
100
|
+
this.errorCode = rawErrorCode
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.flags = flags
|
|
104
|
+
this.rateLimit = rateLimit
|
|
105
|
+
this.rateRemaining = rateRemaining
|
|
106
|
+
|
|
107
|
+
this.setRateWindow(rateLimit, (flags & FLAG_RATE_LIMITED) !== 0, dv)
|
|
108
|
+
|
|
109
|
+
this.headerVariant = headerVariant
|
|
110
|
+
|
|
111
|
+
this.updateOkTerminal()
|
|
112
|
+
|
|
113
|
+
this.https = (flags & FLAG_HTTPS) !== 0
|
|
114
|
+
this.trustedProxy = (flags & FLAG_TRUSTED_PROXY) !== 0
|
|
115
|
+
this.hasCookies = (flags & FLAG_HAS_COOKIES) !== 0
|
|
116
|
+
this.hasQuery = (flags & FLAG_HAS_QUERY) !== 0
|
|
117
|
+
this.bodyValidJson = (flags & FLAG_BODY_VALID_JSON) !== 0
|
|
118
|
+
this.schemaValid = (flags & FLAG_SCHEMA_VALID) !== 0
|
|
119
|
+
this.corsAllowed = (flags & FLAG_CORS_ALLOWED) !== 0
|
|
120
|
+
this.isPreflight = (flags & FLAG_IS_PREFLIGHT) !== 0
|
|
121
|
+
this.rateLimited = (flags & FLAG_RATE_LIMITED) !== 0
|
|
122
|
+
|
|
123
|
+
// Bounds-checked section offsets shared with the pre-baked decoder: a
|
|
124
|
+
// malformed/truncated buffer can never produce slices past its end.
|
|
125
|
+
const layout = sectionLayout(buf.byteLength, cookiesJsonLen, queryJsonLen, bodyJsonLen)
|
|
126
|
+
|
|
127
|
+
// Store slices for lazy decode instead of eagerly decoding strings. The
|
|
128
|
+
// fast path trusts the RAW declared body length and re-checks bounds on
|
|
129
|
+
// read (bodyJson()), so pass the raw declared length here.
|
|
130
|
+
this.setSections(buf, layout, bodyJsonLen)
|
|
131
|
+
|
|
132
|
+
// Parity with the baked decoder: any declared section (cookies, query,
|
|
133
|
+
// OR body) that overran the buffer counts as truncated.
|
|
134
|
+
this.bodyTruncated = (flags & FLAG_BODY_TRUNCATED) !== 0 || layout.truncated
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
invalidate(): void {
|
|
138
|
+
this.resetShared()
|
|
139
|
+
// Fast path's empty state is an internal-error shape (500/1/internal).
|
|
140
|
+
this.status = 500
|
|
141
|
+
this.verdict = 1
|
|
142
|
+
this.errorCode = ERR_CODE_INTERNAL
|
|
143
|
+
this.flags = 0
|
|
144
|
+
this.https = false
|
|
145
|
+
this.hasCookies = false
|
|
146
|
+
this.hasQuery = false
|
|
147
|
+
this.rateLimit = 0
|
|
148
|
+
this.requestId = ''
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
setInternalError(requestId: string): void {
|
|
152
|
+
this.invalidate()
|
|
153
|
+
this.requestId = requestId
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Bounds-checked body-JSON section, or an empty view when absent. */
|
|
157
|
+
bodyJson(): Uint8Array {
|
|
158
|
+
return this.bodyJsonSlice()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/ingress/decode/packed-sections.ts — Shared bounds-checked section layout.
|
|
2
|
+
//
|
|
3
|
+
// Both decoders (fast-result.ts and baked-result.ts) walk the same OUT_* data
|
|
4
|
+
// region: [cookies JSON][query JSON][body JSON], each u32-length-prefixed in
|
|
5
|
+
// the fixed header. This helper centralizes the bounds-checked offset math so
|
|
6
|
+
// a malformed/truncated buffer can never produce slices past its end.
|
|
7
|
+
|
|
8
|
+
import { OUT_DATA_START } from '../constants'
|
|
9
|
+
|
|
10
|
+
/** Bounds-checked section offsets for one ingress output buffer. */
|
|
11
|
+
export interface PackedSectionLayout {
|
|
12
|
+
/** Cookie-JSON length clamped to the buffer (0 if none / out of range). */
|
|
13
|
+
safeCookiesLen: number
|
|
14
|
+
/** Byte offset where the query-JSON section begins. */
|
|
15
|
+
queryStart: number
|
|
16
|
+
/** Query-JSON length clamped to the buffer (0 if none / out of range). */
|
|
17
|
+
safeQueryLen: number
|
|
18
|
+
/** Byte offset where the body-JSON section begins. */
|
|
19
|
+
bodyJsonStart: number
|
|
20
|
+
/** Body-JSON length clamped to the buffer (0 if none / out of range). */
|
|
21
|
+
safeBodyJsonLen: number
|
|
22
|
+
/** Whether any declared section length overran the buffer. */
|
|
23
|
+
truncated: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compute bounds-checked section offsets for an ingress output buffer.
|
|
28
|
+
*
|
|
29
|
+
* Given the raw u32 section lengths (already read from the fixed header) and
|
|
30
|
+
* the actual buffer length, returns safe (clamped-to-buffer) lengths and the
|
|
31
|
+
* body-JSON start offset. Any length that would run past the end of the
|
|
32
|
+
* buffer is clamped to 0 and reported via `truncated`, so callers never read
|
|
33
|
+
* stale or out-of-bounds bytes.
|
|
34
|
+
*/
|
|
35
|
+
export function sectionLayout(
|
|
36
|
+
bufLen: number,
|
|
37
|
+
cookiesLen: number,
|
|
38
|
+
queryLen: number,
|
|
39
|
+
bodyLen: number,
|
|
40
|
+
): PackedSectionLayout {
|
|
41
|
+
const safeCookiesLen = cookiesLen > 0 && OUT_DATA_START + cookiesLen <= bufLen ? cookiesLen : 0
|
|
42
|
+
const queryStart = OUT_DATA_START + safeCookiesLen
|
|
43
|
+
const safeQueryLen = queryLen > 0 && queryStart + queryLen <= bufLen ? queryLen : 0
|
|
44
|
+
const bodyJsonStart = queryStart + safeQueryLen
|
|
45
|
+
const safeBodyJsonLen = bodyLen > 0 && bodyJsonStart + bodyLen <= bufLen ? bodyLen : 0
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
safeCookiesLen,
|
|
49
|
+
queryStart,
|
|
50
|
+
safeQueryLen,
|
|
51
|
+
bodyJsonStart,
|
|
52
|
+
safeBodyJsonLen,
|
|
53
|
+
truncated:
|
|
54
|
+
safeCookiesLen !== cookiesLen || safeQueryLen !== queryLen || safeBodyJsonLen !== bodyLen,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/ingress/decode/result-base.ts — Shared zero-alloc decoder plumbing.
|
|
2
|
+
//
|
|
3
|
+
// Both ingress result decoders (fast-result.ts, baked-result.ts) walk the SAME
|
|
4
|
+
// native OUT_* layout and share: lazy cookie/query JSON section decoding, the
|
|
5
|
+
// bounds-checked body-JSON slice, the rate-limit window fields, and the
|
|
6
|
+
// flag-derived booleans. This base holds that identical plumbing so the two
|
|
7
|
+
// paths cannot drift on section handling.
|
|
8
|
+
//
|
|
9
|
+
// The WIRE FORMAT and STATUS NORMALIZATION intentionally differ between the
|
|
10
|
+
// two decoders ({"error":{...}} fast path vs {"ok":false,...} baked path) —
|
|
11
|
+
// those stay in the subclasses (see AGENTS.md; do not unify them).
|
|
12
|
+
|
|
13
|
+
import { decoder } from '../../shared/bytes'
|
|
14
|
+
import { OUT_DATA_START, OUT_RATE_RESET, OUT_RETRY_AFTER } from '../constants'
|
|
15
|
+
import type { PackedSectionLayout } from './packed-sections'
|
|
16
|
+
|
|
17
|
+
const EMPTY_BODY = new Uint8Array(0)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Zero-alloc, reusable result decoder shared base.
|
|
21
|
+
*
|
|
22
|
+
* Holds the fields and lazy-decoding helpers that both the fast-path
|
|
23
|
+
* (`FastIngressResult`) and pre-baked (`BakedIngressResult`) decoders consume
|
|
24
|
+
* identically from the native OUT_* output buffer. Concrete subclasses own the
|
|
25
|
+
* status normalization, the exposed extras (requestId, flags, ...) and the
|
|
26
|
+
* wire-format-specific body/error accessors.
|
|
27
|
+
*/
|
|
28
|
+
export abstract class IngressResultBase {
|
|
29
|
+
/** Raw native verdict byte (0 = accepted). */
|
|
30
|
+
verdict = 0
|
|
31
|
+
/** Raw native error-code byte. */
|
|
32
|
+
errorCode = 0
|
|
33
|
+
/** Normalized HTTP status. */
|
|
34
|
+
status = 0
|
|
35
|
+
/** Header template variant byte emitted by the native core. */
|
|
36
|
+
headerVariant = 0
|
|
37
|
+
/** Remaining rate-limit budget (count). */
|
|
38
|
+
rateRemaining = 0
|
|
39
|
+
/** Rate-limit window reset (ms epoch). */
|
|
40
|
+
rateResetMs = 0
|
|
41
|
+
/** Retry-After value (ms). */
|
|
42
|
+
retryAfterMs = 0
|
|
43
|
+
/** True when the pipeline returned a terminal (error) verdict. */
|
|
44
|
+
terminal = true
|
|
45
|
+
/** True when the pipeline accepted the request (verdict 0 + 2xx/3xx). */
|
|
46
|
+
ok = false
|
|
47
|
+
/** True when the request was a CORS preflight. */
|
|
48
|
+
isPreflight = false
|
|
49
|
+
/** True when the CORS engine allowed the request origin. */
|
|
50
|
+
corsAllowed = false
|
|
51
|
+
/** True when the request was rate-limited. */
|
|
52
|
+
rateLimited = false
|
|
53
|
+
/** True when the request came from a trusted proxy. */
|
|
54
|
+
trustedProxy = false
|
|
55
|
+
/** True when the request body parsed as valid JSON. */
|
|
56
|
+
bodyValidJson = false
|
|
57
|
+
/** True when the request body validated against the ingress schema. */
|
|
58
|
+
schemaValid = false
|
|
59
|
+
/** True when a declared output section overran the buffer. */
|
|
60
|
+
bodyTruncated = false
|
|
61
|
+
/** Request body bytes (may be an empty view). */
|
|
62
|
+
body: Uint8Array = EMPTY_BODY
|
|
63
|
+
|
|
64
|
+
protected _buf: Uint8Array = EMPTY_BODY
|
|
65
|
+
protected _bodyJsonStart = OUT_DATA_START
|
|
66
|
+
protected _bodyJsonLen = 0
|
|
67
|
+
private _cookiesBuf: Uint8Array | null = null
|
|
68
|
+
private _queryBuf: Uint8Array | null = null
|
|
69
|
+
private _cookiesDecoded: string | null = null
|
|
70
|
+
private _queryDecoded: string | null = null
|
|
71
|
+
|
|
72
|
+
/** Reset the shared fields to their empty state (call from subclass `invalidate`). */
|
|
73
|
+
protected resetShared(): void {
|
|
74
|
+
this.verdict = 0
|
|
75
|
+
this.errorCode = 0
|
|
76
|
+
this.status = 0
|
|
77
|
+
this.headerVariant = 0
|
|
78
|
+
this.rateRemaining = 0
|
|
79
|
+
this.rateResetMs = 0
|
|
80
|
+
this.retryAfterMs = 0
|
|
81
|
+
this.terminal = true
|
|
82
|
+
this.ok = false
|
|
83
|
+
this.isPreflight = false
|
|
84
|
+
this.corsAllowed = false
|
|
85
|
+
this.rateLimited = false
|
|
86
|
+
this.trustedProxy = false
|
|
87
|
+
this.bodyValidJson = false
|
|
88
|
+
this.schemaValid = false
|
|
89
|
+
this.bodyTruncated = false
|
|
90
|
+
this.body = EMPTY_BODY
|
|
91
|
+
this._buf = EMPTY_BODY
|
|
92
|
+
this._bodyJsonStart = OUT_DATA_START
|
|
93
|
+
this._bodyJsonLen = 0
|
|
94
|
+
this._cookiesBuf = null
|
|
95
|
+
this._queryBuf = null
|
|
96
|
+
this._cookiesDecoded = null
|
|
97
|
+
this._queryDecoded = null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Recompute `terminal`/`ok` from the (subclass-set) verdict + status. */
|
|
101
|
+
protected updateOkTerminal(): void {
|
|
102
|
+
this.terminal = this.verdict !== 0 || this.status >= 400
|
|
103
|
+
this.ok = this.verdict === 0 && this.status >= 200 && this.status < 400
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Store the rate-limit window fields. Both decoders read the same OUT_*
|
|
108
|
+
* slots and gate them on the same conditions (a positive budget OR the
|
|
109
|
+
* rate-limited flag), so the read cannot drift between paths.
|
|
110
|
+
*/
|
|
111
|
+
protected setRateWindow(rateLimit: number, rateLimited: boolean, view: DataView): void {
|
|
112
|
+
if (rateLimit > 0 || rateLimited) {
|
|
113
|
+
this.rateResetMs = Number(view.getBigUint64(OUT_RATE_RESET, true))
|
|
114
|
+
this.retryAfterMs = Number(view.getBigUint64(OUT_RETRY_AFTER, true))
|
|
115
|
+
} else {
|
|
116
|
+
this.rateResetMs = 0
|
|
117
|
+
this.retryAfterMs = 0
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Store the bounds-checked cookie/query/body JSON sections from `layout`.
|
|
123
|
+
*
|
|
124
|
+
* `bodyJsonLen` is the body-JSON length to trust for `bodyJson()`: the fast
|
|
125
|
+
* path passes the RAW declared length (it re-checks bounds on read), the
|
|
126
|
+
* baked path passes the already-clamped `layout.safeBodyJsonLen`. Sections
|
|
127
|
+
* are stored as slices and decoded lazily by `cookiesJson()`/`queryJson()`.
|
|
128
|
+
*/
|
|
129
|
+
protected setSections(buf: Uint8Array, layout: PackedSectionLayout, bodyJsonLen: number): void {
|
|
130
|
+
this._buf = buf
|
|
131
|
+
this._bodyJsonStart = layout.bodyJsonStart
|
|
132
|
+
this._bodyJsonLen = bodyJsonLen
|
|
133
|
+
this._cookiesBuf =
|
|
134
|
+
layout.safeCookiesLen > 0
|
|
135
|
+
? buf.subarray(OUT_DATA_START, OUT_DATA_START + layout.safeCookiesLen)
|
|
136
|
+
: null
|
|
137
|
+
this._cookiesDecoded = null
|
|
138
|
+
this._queryBuf =
|
|
139
|
+
layout.safeQueryLen > 0
|
|
140
|
+
? buf.subarray(layout.queryStart, layout.queryStart + layout.safeQueryLen)
|
|
141
|
+
: null
|
|
142
|
+
this._queryDecoded = null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Decoded cookies-JSON section, or `"{}"` when absent. */
|
|
146
|
+
cookiesJson(): string {
|
|
147
|
+
if (this._cookiesDecoded !== null) return this._cookiesDecoded
|
|
148
|
+
if (this._cookiesBuf !== null) {
|
|
149
|
+
this._cookiesDecoded = decoder.decode(this._cookiesBuf)
|
|
150
|
+
return this._cookiesDecoded
|
|
151
|
+
}
|
|
152
|
+
return '{}'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Decoded query-JSON section, or `"{}"` when absent. */
|
|
156
|
+
queryJson(): string {
|
|
157
|
+
if (this._queryDecoded !== null) return this._queryDecoded
|
|
158
|
+
if (this._queryBuf !== null) {
|
|
159
|
+
this._queryDecoded = decoder.decode(this._queryBuf)
|
|
160
|
+
return this._queryDecoded
|
|
161
|
+
}
|
|
162
|
+
return '{}'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Bounds-checked body-JSON slice (empty when absent / out of range). */
|
|
166
|
+
protected bodyJsonSlice(): Uint8Array {
|
|
167
|
+
if (this._bodyJsonLen === 0) return EMPTY_BODY
|
|
168
|
+
const end = this._bodyJsonStart + this._bodyJsonLen
|
|
169
|
+
if (end > this._buf.byteLength) return EMPTY_BODY
|
|
170
|
+
return this._buf.subarray(this._bodyJsonStart, end)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/ingress/errors.ts — Fast-path error-code → name/message mapping.
|
|
2
|
+
//
|
|
3
|
+
// The pre-baked path keeps its own error bodies in ./response/error-bodies.ts
|
|
4
|
+
// because it emits a different wire format ({"ok":false,"error":{...}}), so
|
|
5
|
+
// the two mappings are intentionally not unified (see AGENTS.md).
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ERR_CODE_NONE,
|
|
9
|
+
ERR_CODE_CORS_PREFLIGHT,
|
|
10
|
+
ERR_CODE_RATE_LIMITED,
|
|
11
|
+
ERR_CODE_BODY_TOO_LARGE,
|
|
12
|
+
ERR_CODE_INVALID_JSON,
|
|
13
|
+
ERR_CODE_SCHEMA_VALIDATION,
|
|
14
|
+
ERR_CODE_BAD_REQUEST,
|
|
15
|
+
ERR_CODE_REQUEST_TOO_LARGE,
|
|
16
|
+
ERR_CODE_REQUEST_TIMEOUT,
|
|
17
|
+
ERR_CODE_INTERNAL,
|
|
18
|
+
} from './constants'
|
|
19
|
+
|
|
20
|
+
/** Map a native error code to its stable string name. */
|
|
21
|
+
export function errorCodeName(code: number): string {
|
|
22
|
+
switch (code) {
|
|
23
|
+
case ERR_CODE_NONE:
|
|
24
|
+
return 'none'
|
|
25
|
+
case ERR_CODE_CORS_PREFLIGHT:
|
|
26
|
+
return 'cors_preflight'
|
|
27
|
+
case ERR_CODE_RATE_LIMITED:
|
|
28
|
+
return 'rate_limited'
|
|
29
|
+
case ERR_CODE_BODY_TOO_LARGE:
|
|
30
|
+
return 'body_too_large'
|
|
31
|
+
case ERR_CODE_INVALID_JSON:
|
|
32
|
+
return 'invalid_json'
|
|
33
|
+
case ERR_CODE_SCHEMA_VALIDATION:
|
|
34
|
+
return 'schema_validation'
|
|
35
|
+
case ERR_CODE_BAD_REQUEST:
|
|
36
|
+
return 'bad_request'
|
|
37
|
+
case ERR_CODE_REQUEST_TOO_LARGE:
|
|
38
|
+
return 'request_too_large'
|
|
39
|
+
case ERR_CODE_REQUEST_TIMEOUT:
|
|
40
|
+
return 'request_timeout'
|
|
41
|
+
case ERR_CODE_INTERNAL:
|
|
42
|
+
return 'internal'
|
|
43
|
+
default:
|
|
44
|
+
return 'unknown'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Map a native error code (plus status context) to a human message. */
|
|
49
|
+
export function errorMessage(status: number, code: number): string {
|
|
50
|
+
switch (code) {
|
|
51
|
+
case ERR_CODE_CORS_PREFLIGHT:
|
|
52
|
+
return 'CORS preflight rejected'
|
|
53
|
+
case ERR_CODE_RATE_LIMITED:
|
|
54
|
+
return 'Too many requests'
|
|
55
|
+
case ERR_CODE_BODY_TOO_LARGE:
|
|
56
|
+
return 'Request body too large'
|
|
57
|
+
case ERR_CODE_INVALID_JSON:
|
|
58
|
+
return 'Invalid JSON body'
|
|
59
|
+
case ERR_CODE_SCHEMA_VALIDATION:
|
|
60
|
+
return 'JSON schema validation failed'
|
|
61
|
+
case ERR_CODE_BAD_REQUEST:
|
|
62
|
+
return 'Bad request'
|
|
63
|
+
case ERR_CODE_REQUEST_TOO_LARGE:
|
|
64
|
+
return 'Request too large'
|
|
65
|
+
case ERR_CODE_REQUEST_TIMEOUT:
|
|
66
|
+
return 'Request body read timed out'
|
|
67
|
+
case ERR_CODE_INTERNAL:
|
|
68
|
+
return 'Internal server error'
|
|
69
|
+
default:
|
|
70
|
+
return status >= 500 ? 'Internal server error' : 'Request rejected'
|
|
71
|
+
}
|
|
72
|
+
}
|