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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -368
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +2763 -0
  5. package/dist/index.js +8707 -0
  6. package/docs/API.md +337 -0
  7. package/docs/ARCHITECTURE.md +507 -0
  8. package/docs/BENCHMARKS.md +343 -0
  9. package/docs/CASE_STUDY.md +240 -0
  10. package/docs/ENVIRONMENT.md +81 -0
  11. package/docs/FFI_BUN_GUIDE.md +327 -0
  12. package/docs/GETTING_STARTED.md +192 -0
  13. package/docs/INGRESS-ROUTER.md +81 -0
  14. package/docs/INGRESS.md +746 -0
  15. package/docs/NATIVE-ROUTE.md +53 -0
  16. package/docs/REPO_MAP.md +279 -0
  17. package/docs/adr/0001-two-wire-formats.md +39 -0
  18. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  19. package/docs/adr/0003-bun-builtins-delegation.md +43 -0
  20. package/docs/bun-builtins-decision-matrix.md +115 -0
  21. package/index.ts +44 -8
  22. package/package.json +98 -33
  23. package/src/ingress/body.ts +289 -0
  24. package/src/ingress/constants.ts +220 -48
  25. package/src/ingress/context.ts +136 -0
  26. package/src/ingress/decode/baked-result.ts +121 -0
  27. package/src/ingress/decode/fast-result.ts +160 -0
  28. package/src/ingress/decode/packed-sections.ts +56 -0
  29. package/src/ingress/decode/result-base.ts +172 -0
  30. package/src/ingress/errors.ts +72 -0
  31. package/src/ingress/fast.ts +152 -1021
  32. package/src/ingress/handlers.ts +639 -1001
  33. package/src/ingress/headers/baked-templates.ts +102 -0
  34. package/src/ingress/headers/cors.ts +44 -0
  35. package/src/ingress/headers/fast-templates.ts +238 -0
  36. package/src/ingress/headers/hsts.ts +83 -0
  37. package/src/ingress/health.ts +68 -0
  38. package/src/ingress/index.ts +142 -405
  39. package/src/ingress/metrics.ts +132 -0
  40. package/src/ingress/options.ts +197 -0
  41. package/src/ingress/packing/gather-raw-headers.ts +139 -0
  42. package/src/ingress/packing/header-packing.ts +75 -0
  43. package/src/ingress/packing/input-packer.ts +164 -0
  44. package/src/ingress/packing/scratch.ts +50 -0
  45. package/src/ingress/packing/select-headers.ts +102 -0
  46. package/src/ingress/response/error-bodies.ts +75 -0
  47. package/src/ingress/response/terminal.ts +59 -0
  48. package/src/ingress/router.ts +237 -0
  49. package/src/ingress/routes/common.ts +79 -0
  50. package/src/ingress/routes/delete.ts +14 -0
  51. package/src/ingress/routes/echo.ts +107 -0
  52. package/src/ingress/routes/fallback.ts +23 -0
  53. package/src/ingress/routes/head.ts +27 -0
  54. package/src/ingress/routes/index.ts +11 -0
  55. package/src/ingress/routes/json-write.ts +129 -0
  56. package/src/ingress/routes/options.ts +26 -0
  57. package/src/ingress/routes/read.ts +35 -0
  58. package/src/ingress/routes/responder.ts +159 -0
  59. package/src/ingress/server-node.ts +376 -0
  60. package/src/ingress/server.ts +475 -0
  61. package/src/ingress/shared.ts +132 -0
  62. package/src/ingress/status.ts +97 -0
  63. package/src/ingress/types.ts +191 -0
  64. package/src/integration/batch.ts +58 -0
  65. package/src/integration/index.ts +29 -0
  66. package/src/integration/pipeline.ts +220 -0
  67. package/src/integration/streaming.ts +78 -0
  68. package/src/integration/websocket.ts +93 -0
  69. package/src/loader/batch.ts +137 -0
  70. package/src/loader/cost.ts +105 -0
  71. package/src/loader/index.ts +745 -0
  72. package/src/loader/ops.ts +657 -0
  73. package/src/loader/state.ts +84 -0
  74. package/src/loader/types.ts +145 -0
  75. package/src/native/ffi/constants.ts +58 -0
  76. package/src/native/ffi/selftest.ts +729 -0
  77. package/src/native/ffi/types.ts +474 -0
  78. package/src/native/ffi.ts +1775 -0
  79. package/src/native/index.ts +8 -207
  80. package/src/native/loader.ts +240 -0
  81. package/src/native/types/instances.ts +274 -0
  82. package/src/native/types.ts +401 -0
  83. package/src/runtime/builtins.ts +131 -0
  84. package/src/runtime/codec.ts +147 -0
  85. package/src/runtime/detect.ts +36 -0
  86. package/src/runtime/index.ts +28 -0
  87. package/src/runtime/native.ts +35 -0
  88. package/src/runtime/server.ts +31 -0
  89. package/src/runtime/transport.ts +64 -0
  90. package/src/runtime/types.ts +173 -0
  91. package/src/runtime/uuid.ts +45 -0
  92. package/src/runtime/websocket.ts +30 -0
  93. package/src/rust-ffi/addon.ts +10 -0
  94. package/src/rust-ffi/batch/build.ts +155 -0
  95. package/src/rust-ffi/batch/index.ts +8 -0
  96. package/src/rust-ffi/batch/types.ts +92 -0
  97. package/src/rust-ffi/client.ts +87 -0
  98. package/src/rust-ffi/context.ts +193 -0
  99. package/src/rust-ffi/index.ts +51 -541
  100. package/src/rust-ffi/into.ts +21 -0
  101. package/src/rust-ffi/options.ts +113 -0
  102. package/src/rust-ffi/packed.ts +253 -0
  103. package/src/rust-ffi/proven.ts +28 -0
  104. package/src/rust-ffi/scalar/crypto.ts +346 -0
  105. package/src/rust-ffi/scalar/factories.ts +459 -0
  106. package/src/rust-ffi/scalar/hashing.ts +74 -0
  107. package/src/rust-ffi/scalar/http.ts +337 -0
  108. package/src/rust-ffi/scalar/index.ts +31 -0
  109. package/src/rust-ffi/scalar/interface.ts +325 -0
  110. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  111. package/src/rust-ffi/scalar/json.ts +61 -0
  112. package/src/rust-ffi/scalar/payload.ts +241 -0
  113. package/src/rust-ffi/text.ts +121 -0
  114. package/src/selection.json +194 -0
  115. package/src/selection.ts +73 -0
  116. package/src/shared/adaptive.ts +67 -0
  117. package/src/shared/buffer-pool.ts +264 -0
  118. package/src/shared/bytes.ts +108 -9
  119. package/src/shared/codec.ts +78 -0
  120. package/src/shared/env.ts +20 -0
  121. package/src/shared/log.ts +75 -0
  122. package/src/shared/metrics.ts +247 -0
  123. package/src/shared/packed/index.ts +10 -0
  124. package/src/shared/packed/parsers.ts +31 -0
  125. package/src/shared/packed/schema.ts +23 -0
  126. package/src/shared/packed/wire.ts +504 -0
  127. package/src/shared/proven.ts +289 -0
  128. package/src/shared/request-id.ts +67 -0
  129. package/src/shared/response.ts +104 -0
  130. package/src/shared/runtime.ts +29 -0
  131. package/src/shared/trace.ts +81 -0
  132. package/src/shared/uuid.ts +15 -0
  133. package/Cargo.toml +0 -73
  134. package/bench.ts +0 -3
  135. package/rust/batch.rs +0 -195
  136. package/rust/cookie_parser.rs +0 -70
  137. package/rust/cors.rs +0 -263
  138. package/rust/hashing.rs +0 -164
  139. package/rust/headers.rs +0 -234
  140. package/rust/hmac_sha256.rs +0 -113
  141. package/rust/http_parser.rs +0 -132
  142. package/rust/ingress.rs +0 -616
  143. package/rust/ingress_constants.rs +0 -89
  144. package/rust/ip_trust.rs +0 -185
  145. package/rust/json_ops.rs +0 -66
  146. package/rust/json_patch_ops.rs +0 -25
  147. package/rust/json_schema.rs +0 -151
  148. package/rust/json_ser.rs +0 -429
  149. package/rust/lib.rs +0 -48
  150. package/rust/method.rs +0 -141
  151. package/rust/mime_lookup.rs +0 -76
  152. package/rust/output.rs +0 -238
  153. package/rust/proxy.rs +0 -90
  154. package/rust/query_parser.rs +0 -141
  155. package/rust/random_token.rs +0 -52
  156. package/rust/rate_limit.rs +0 -203
  157. package/rust/terminal.rs +0 -95
  158. package/rust/unit_tests.rs +0 -843
  159. package/rust/url_codec.rs +0 -133
  160. package/rust/util.rs +0 -537
  161. package/rust/validation.rs +0 -107
  162. package/rust/websocket.rs +0 -23
  163. package/src/baseline/index.ts +0 -12
  164. package/src/baseline/tasks/cookie.ts +0 -22
  165. package/src/baseline/tasks/hashing.ts +0 -18
  166. package/src/baseline/tasks/hmac.ts +0 -38
  167. package/src/baseline/tasks/http.ts +0 -62
  168. package/src/baseline/tasks/json-patch.ts +0 -12
  169. package/src/baseline/tasks/json.ts +0 -50
  170. package/src/baseline/tasks/mime.ts +0 -5
  171. package/src/baseline/tasks/query.ts +0 -14
  172. package/src/baseline/tasks/token.ts +0 -15
  173. package/src/baseline/tasks/url.ts +0 -11
  174. package/src/baseline/tasks/validation.ts +0 -19
  175. package/src/baseline/tasks/websocket.ts +0 -14
  176. package/src/bench/assert.ts +0 -70
  177. package/src/bench/checks.ts +0 -227
  178. package/src/bench/checksum.ts +0 -27
  179. package/src/bench/comparisons.ts +0 -70
  180. package/src/bench/concurrent-worker.ts +0 -135
  181. package/src/bench/fixtures.ts +0 -233
  182. package/src/bench/index.ts +0 -2
  183. package/src/bench/measure.ts +0 -177
  184. package/src/bench/now.ts +0 -3
  185. package/src/bench/report.ts +0 -79
  186. package/src/bench/run.ts +0 -57
  187. package/src/bench/tasks/complex.ts +0 -232
  188. package/src/bench/tasks/concurrent.ts +0 -213
  189. package/src/bench/tasks/cookie.ts +0 -44
  190. package/src/bench/tasks/hashing.ts +0 -33
  191. package/src/bench/tasks/hmac.ts +0 -36
  192. package/src/bench/tasks/http.ts +0 -50
  193. package/src/bench/tasks/index.ts +0 -52
  194. package/src/bench/tasks/json-patch.ts +0 -21
  195. package/src/bench/tasks/json.ts +0 -33
  196. package/src/bench/tasks/mime.ts +0 -21
  197. package/src/bench/tasks/query.ts +0 -44
  198. package/src/bench/tasks/stress.ts +0 -142
  199. package/src/bench/tasks/token.ts +0 -20
  200. package/src/bench/tasks/url.ts +0 -34
  201. package/src/bench/tasks/validation.ts +0 -57
  202. package/src/bench/tasks/websocket.ts +0 -21
  203. package/src/bench/types.ts +0 -60
  204. package/src/data/json-rows.ts +0 -33
  205. package/src/ingress/packed-input.ts +0 -59
  206. package/src/shared/json.ts +0 -18
  207. 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
+ }