castrum 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -59
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +3161 -0
  5. package/dist/index.js +7306 -0
  6. package/docs/ARCHITECTURE.md +415 -0
  7. package/docs/BENCHMARKS.md +307 -0
  8. package/docs/ENVIRONMENT.md +79 -0
  9. package/docs/GETTING_STARTED.md +209 -0
  10. package/docs/INGRESS.md +745 -0
  11. package/docs/REPO_MAP.md +259 -0
  12. package/docs/adr/0001-two-wire-formats.md +39 -0
  13. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  14. package/docs/adr/0003-bun-builtins-delegation.md +42 -0
  15. package/docs/bun-builtins-decision-matrix.md +108 -0
  16. package/index.ts +51 -8
  17. package/package.json +73 -20
  18. package/src/data/json-rows.ts +13 -13
  19. package/src/ingress/body.ts +149 -0
  20. package/src/ingress/constants.ts +220 -48
  21. package/src/ingress/context.ts +136 -0
  22. package/src/ingress/decode/baked-result.ts +121 -0
  23. package/src/ingress/decode/fast-result.ts +160 -0
  24. package/src/ingress/decode/packed-sections.ts +56 -0
  25. package/src/ingress/decode/result-base.ts +172 -0
  26. package/src/ingress/errors.ts +72 -0
  27. package/src/ingress/fast.ts +152 -1021
  28. package/src/ingress/handlers.ts +563 -1001
  29. package/src/ingress/headers/baked-templates.ts +102 -0
  30. package/src/ingress/headers/cors.ts +44 -0
  31. package/src/ingress/headers/fast-templates.ts +238 -0
  32. package/src/ingress/headers/hsts.ts +83 -0
  33. package/src/ingress/health.ts +67 -0
  34. package/src/ingress/index.ts +130 -405
  35. package/src/ingress/metrics.ts +132 -0
  36. package/src/ingress/options.ts +197 -0
  37. package/src/ingress/packing/gather-raw-headers.ts +75 -0
  38. package/src/ingress/packing/header-packing.ts +75 -0
  39. package/src/ingress/packing/input-packer.ts +150 -0
  40. package/src/ingress/packing/scratch.ts +50 -0
  41. package/src/ingress/packing/select-headers.ts +96 -0
  42. package/src/ingress/response/error-bodies.ts +75 -0
  43. package/src/ingress/response/terminal.ts +59 -0
  44. package/src/ingress/routes/common.ts +38 -0
  45. package/src/ingress/routes/delete.ts +14 -0
  46. package/src/ingress/routes/echo.ts +107 -0
  47. package/src/ingress/routes/fallback.ts +23 -0
  48. package/src/ingress/routes/head.ts +37 -0
  49. package/src/ingress/routes/index.ts +10 -0
  50. package/src/ingress/routes/json-write.ts +153 -0
  51. package/src/ingress/routes/options.ts +36 -0
  52. package/src/ingress/routes/read.ts +45 -0
  53. package/src/ingress/server-node.ts +376 -0
  54. package/src/ingress/server.ts +443 -0
  55. package/src/ingress/shared.ts +132 -0
  56. package/src/ingress/status.ts +97 -0
  57. package/src/ingress/types.ts +157 -0
  58. package/src/integration/batch.ts +58 -0
  59. package/src/integration/index.ts +29 -0
  60. package/src/integration/pipeline.ts +217 -0
  61. package/src/integration/streaming.ts +79 -0
  62. package/src/integration/websocket.ts +92 -0
  63. package/src/loader/batch.ts +133 -0
  64. package/src/loader/cost.ts +104 -0
  65. package/src/loader/index.ts +930 -0
  66. package/src/loader/ops.ts +657 -0
  67. package/src/native/ffi.ts +1541 -0
  68. package/src/native/index.ts +8 -207
  69. package/src/native/loader.ts +194 -0
  70. package/src/native/types.ts +598 -0
  71. package/src/rust-ffi/addon.ts +10 -0
  72. package/src/rust-ffi/batch.ts +431 -0
  73. package/src/rust-ffi/client.ts +88 -0
  74. package/src/rust-ffi/context.ts +164 -0
  75. package/src/rust-ffi/index.ts +40 -541
  76. package/src/rust-ffi/options.ts +113 -0
  77. package/src/rust-ffi/packed.ts +253 -0
  78. package/src/rust-ffi/proven.ts +42 -0
  79. package/src/rust-ffi/scalar/crypto.ts +265 -0
  80. package/src/rust-ffi/scalar/factories.ts +233 -0
  81. package/src/rust-ffi/scalar/hashing.ts +72 -0
  82. package/src/rust-ffi/scalar/http.ts +206 -0
  83. package/src/rust-ffi/scalar/index.ts +31 -0
  84. package/src/rust-ffi/scalar/interface.ts +450 -0
  85. package/src/rust-ffi/scalar/json.ts +45 -0
  86. package/src/rust-ffi/scalar/payload.ts +124 -0
  87. package/src/rust-ffi/text.ts +95 -0
  88. package/src/selection.json +194 -0
  89. package/src/selection.ts +77 -0
  90. package/src/shared/adaptive.ts +68 -0
  91. package/src/shared/bench-classify.ts +209 -0
  92. package/src/shared/buffer-pool.ts +209 -0
  93. package/src/shared/bytes.ts +35 -5
  94. package/src/shared/env.ts +20 -0
  95. package/src/shared/log.ts +74 -0
  96. package/src/shared/metrics.ts +247 -0
  97. package/src/shared/packed.ts +350 -154
  98. package/src/shared/proven.ts +596 -0
  99. package/src/shared/request-id.ts +67 -0
  100. package/src/shared/response.ts +104 -0
  101. package/src/shared/runtime.ts +34 -0
  102. package/src/shared/trace.ts +81 -0
  103. package/src/shared/uuid.ts +31 -0
  104. package/Cargo.toml +0 -73
  105. package/bench.ts +0 -3
  106. package/rust/batch.rs +0 -195
  107. package/rust/cookie_parser.rs +0 -70
  108. package/rust/cors.rs +0 -263
  109. package/rust/hashing.rs +0 -164
  110. package/rust/headers.rs +0 -234
  111. package/rust/hmac_sha256.rs +0 -113
  112. package/rust/http_parser.rs +0 -132
  113. package/rust/ingress.rs +0 -616
  114. package/rust/ingress_constants.rs +0 -89
  115. package/rust/ip_trust.rs +0 -185
  116. package/rust/json_ops.rs +0 -66
  117. package/rust/json_patch_ops.rs +0 -25
  118. package/rust/json_schema.rs +0 -151
  119. package/rust/json_ser.rs +0 -429
  120. package/rust/lib.rs +0 -48
  121. package/rust/method.rs +0 -141
  122. package/rust/mime_lookup.rs +0 -76
  123. package/rust/output.rs +0 -238
  124. package/rust/proxy.rs +0 -90
  125. package/rust/query_parser.rs +0 -141
  126. package/rust/random_token.rs +0 -52
  127. package/rust/rate_limit.rs +0 -203
  128. package/rust/terminal.rs +0 -95
  129. package/rust/unit_tests.rs +0 -843
  130. package/rust/url_codec.rs +0 -133
  131. package/rust/util.rs +0 -537
  132. package/rust/validation.rs +0 -107
  133. package/rust/websocket.rs +0 -23
  134. package/src/baseline/index.ts +0 -12
  135. package/src/baseline/tasks/cookie.ts +0 -22
  136. package/src/baseline/tasks/hashing.ts +0 -18
  137. package/src/baseline/tasks/hmac.ts +0 -38
  138. package/src/baseline/tasks/http.ts +0 -62
  139. package/src/baseline/tasks/json-patch.ts +0 -12
  140. package/src/baseline/tasks/json.ts +0 -50
  141. package/src/baseline/tasks/mime.ts +0 -5
  142. package/src/baseline/tasks/query.ts +0 -14
  143. package/src/baseline/tasks/token.ts +0 -15
  144. package/src/baseline/tasks/url.ts +0 -11
  145. package/src/baseline/tasks/validation.ts +0 -19
  146. package/src/baseline/tasks/websocket.ts +0 -14
  147. package/src/bench/assert.ts +0 -70
  148. package/src/bench/checks.ts +0 -227
  149. package/src/bench/checksum.ts +0 -27
  150. package/src/bench/comparisons.ts +0 -70
  151. package/src/bench/concurrent-worker.ts +0 -135
  152. package/src/bench/fixtures.ts +0 -233
  153. package/src/bench/index.ts +0 -2
  154. package/src/bench/measure.ts +0 -177
  155. package/src/bench/now.ts +0 -3
  156. package/src/bench/report.ts +0 -79
  157. package/src/bench/run.ts +0 -57
  158. package/src/bench/tasks/complex.ts +0 -232
  159. package/src/bench/tasks/concurrent.ts +0 -213
  160. package/src/bench/tasks/cookie.ts +0 -44
  161. package/src/bench/tasks/hashing.ts +0 -33
  162. package/src/bench/tasks/hmac.ts +0 -36
  163. package/src/bench/tasks/http.ts +0 -50
  164. package/src/bench/tasks/index.ts +0 -52
  165. package/src/bench/tasks/json-patch.ts +0 -21
  166. package/src/bench/tasks/json.ts +0 -33
  167. package/src/bench/tasks/mime.ts +0 -21
  168. package/src/bench/tasks/query.ts +0 -44
  169. package/src/bench/tasks/stress.ts +0 -142
  170. package/src/bench/tasks/token.ts +0 -20
  171. package/src/bench/tasks/url.ts +0 -34
  172. package/src/bench/tasks/validation.ts +0 -57
  173. package/src/bench/tasks/websocket.ts +0 -21
  174. package/src/bench/types.ts +0 -60
  175. package/src/ingress/packed-input.ts +0 -59
  176. package/src/shared/json.ts +0 -18
@@ -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
+ }
@@ -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
+ }