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
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "castrum",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "description": "Enterprise-grade Bun + Rust runtime: Rust-accelerated FFI primitives, a production-grade HTTP ingress pipeline, validation, hashing, and JSON processing with benchmark-grade performance.",
6
6
  "type": "module",
7
- "main": "index.ts",
8
- "module": "index.ts",
9
- "types": "index.ts",
7
+ "main": "dist/index.js",
8
+ "module": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
10
  "license": "MIT",
11
11
  "keywords": [
12
12
  "bun",
@@ -29,27 +29,58 @@
29
29
  "access": "public"
30
30
  },
31
31
  "exports": {
32
- ".": "./index.ts"
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "bun": "./index.ts",
35
+ "node": "./dist/index.js",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./package.json": "./package.json"
33
39
  },
34
40
  "files": [
35
41
  "index.ts",
36
- "bench.ts",
42
+ "dist",
37
43
  "src",
38
- "rust",
39
- "Cargo.toml",
44
+ "docs",
40
45
  "README.md",
41
- "*.node"
46
+ "*.node",
47
+ "!src/bench",
48
+ "!src/baseline"
42
49
  ],
43
50
  "scripts": {
44
51
  "build": "napi build --release --platform",
52
+ "build:perf": "bash scripts/build-perf.sh",
53
+ "build:js": "bun run build:js:bundle && bun run build:js:types",
54
+ "build:js:bundle": "bun build ./index.ts --target=node --format=esm --outdir=dist",
55
+ "build:js:types": "dts-bundle-generator -o dist/index.d.ts index.ts",
45
56
  "bench": "bun run build && bun bench.ts",
46
57
  "check": "bun bench.ts",
47
58
  "test": "bun test",
59
+ "test:all": "bun test && bun run typecheck && bun run typecheck:test && bun run lint:ci && cargo test --lib",
60
+ "test:coverage": "node scripts/check-coverage.mjs",
61
+ "setup": "bun install && bun run build",
62
+ "test:node": "node --test test/integration/node-smoke.test.mjs test/integration/node-enterprise.test.mjs",
63
+ "verify:install": "node scripts/verify-install.mjs",
48
64
  "build:debug": "napi build --platform",
49
- "artifacts": "napi artifacts",
65
+ "lint": "bunx biome check .",
66
+ "lint:fix": "bunx biome check --write .",
67
+ "lint:ci": "bunx biome check . --formatter-enabled=false --assist-enabled=false --error-on-warnings",
68
+ "format": "bunx biome format --write .",
69
+ "typecheck": "bunx tsc --noEmit",
70
+ "typecheck:test": "bunx tsc --noEmit -p tsconfig.test.json",
71
+ "test:rust": "cargo test",
72
+ "deny": "cargo deny check",
73
+ "check:version": "bun scripts/check-version.mjs",
74
+ "check:jsdoc": "bun scripts/check-jsdoc.ts",
75
+ "audit": "bun audit",
50
76
  "publish:manual": "node scripts/publish-manual.mjs",
51
77
  "publish:manual:dry": "node scripts/publish-manual.mjs --dry-run",
52
- "prepublishOnly": "node scripts/prepublish.mjs",
78
+ "prepublishOnly": "bun run build:js && node scripts/prepublish.mjs",
79
+ "check:proven": "bun scripts/check-proven.ts",
80
+ "check:proven:fail": "bun scripts/check-proven.ts --fail",
81
+ "check:annotate": "bun scripts/annotate-performance.ts",
82
+ "check:annotate:dry": "bun scripts/annotate-performance.ts --dry-run",
83
+ "check:docs": "bun run check && bun run check:annotate && bun run check:proven",
53
84
  "bench:http": "bun run bench/run-bench.ts",
54
85
  "bench:http:smoke": "bun run bench/run-bench.ts smoke",
55
86
  "bench:http:stress": "bun run bench/run-bench.ts stress",
@@ -62,13 +93,22 @@
62
93
  "bench:http:elysia": "bun run bench/run-bench.ts '' elysia",
63
94
  "bench:http:ingress": "bun run bench/run-bench.ts '' ingress",
64
95
  "bench:http:bun-only": "bun run bench/run-bench.ts '' bun",
65
- "bench:http:all-heavy": "bun run bench/run-bench.ts 1"
96
+ "bench:http:all-heavy": "bun run bench/run-bench.ts 1",
97
+ "bench:startup": "bun run bench/startup.ts",
98
+ "bench:ffi": "bun run bench/ffi-all.ts",
99
+ "bench:ffi:load": "bun run bench/ffi-load.ts",
100
+ "bench:ffi:public": "bun run bench/ffi-public.ts",
101
+ "bench:ffi:workers": "bun run bench/ffi-workers.ts",
102
+ "verify:native:batch": "bun run scripts/verify-native-batch.ts"
66
103
  },
67
- "dependencies": {
68
- "crc-32": "^1.2.2",
69
- "fast-json-patch": "^3.1.1",
70
- "mime-types": "^3.0.2",
71
- "validator": "^13.15.35"
104
+ "dependencies": {},
105
+ "peerDependencies": {
106
+ "@types/node": ">=20"
107
+ },
108
+ "peerDependenciesMeta": {
109
+ "@types/node": {
110
+ "optional": true
111
+ }
72
112
  },
73
113
  "devDependencies": {
74
114
  "@elysia/cors": "^1.4.2",
@@ -76,19 +116,32 @@
76
116
  "@types/bun": "^1.3.14",
77
117
  "@types/mime-types": "^3.0.1",
78
118
  "@types/validator": "^13.15.10",
79
- "elysia": "^1.4.0"
119
+ "ajv": "^8.20.0",
120
+ "crc-32": "^1.2.2",
121
+ "dts-bundle-generator": "^9.5.1",
122
+ "elysia": "^1.4.0",
123
+ "fast-json-patch": "^3.1.1",
124
+ "mime-types": "^3.0.2",
125
+ "typescript": "5.9.3",
126
+ "validator": "^13.15.35"
127
+ },
128
+ "overrides": {
129
+ "typescript": "5.9.3"
80
130
  },
81
131
  "engines": {
82
- "bun": ">=1.1.0"
132
+ "bun": ">=1.1.0",
133
+ "node": ">=20.3"
83
134
  },
84
135
  "napi": {
85
136
  "binaryName": "castrum",
86
137
  "targets": [
87
138
  "aarch64-apple-darwin",
88
139
  "aarch64-unknown-linux-gnu",
140
+ "aarch64-unknown-linux-musl",
89
141
  "x86_64-apple-darwin",
90
142
  "x86_64-pc-windows-msvc",
91
- "x86_64-unknown-linux-gnu"
143
+ "x86_64-unknown-linux-gnu",
144
+ "x86_64-unknown-linux-musl"
92
145
  ]
93
146
  }
94
147
  }
@@ -1,17 +1,17 @@
1
- import { encoder } from "../shared/bytes";
1
+ import { encoder } from '../shared/bytes'
2
2
 
3
3
  export interface JsonRowNested {
4
- version: number;
5
- createdAt: string;
4
+ version: number
5
+ createdAt: string
6
6
  }
7
7
 
8
8
  export interface JsonRow {
9
- id: number;
10
- name: string;
11
- active: boolean;
12
- score: number;
13
- tags: string[];
14
- nested: JsonRowNested;
9
+ id: number
10
+ name: string
11
+ active: boolean
12
+ score: number
13
+ tags: string[]
14
+ nested: JsonRowNested
15
15
  }
16
16
 
17
17
  export function createJsonRows(rows: number): JsonRow[] {
@@ -20,14 +20,14 @@ export function createJsonRows(rows: number): JsonRow[] {
20
20
  name: `user_${i}`,
21
21
  active: i % 2 === 0,
22
22
  score: i * 1.25,
23
- tags: ["alpha", "beta", "gamma"],
23
+ tags: ['alpha', 'beta', 'gamma'],
24
24
  nested: {
25
25
  version: i % 10,
26
- createdAt: "2026-01-01T00:00:00Z",
26
+ createdAt: '2026-01-01T00:00:00Z',
27
27
  },
28
- }));
28
+ }))
29
29
  }
30
30
 
31
31
  export function jsonRowsBytes(rows: number): Uint8Array {
32
- return encoder.encode(JSON.stringify(createJsonRows(rows)));
32
+ return encoder.encode(JSON.stringify(createJsonRows(rows)))
33
33
  }
@@ -0,0 +1,149 @@
1
+ // src/ingress/body.ts — Streaming request-body reading for the async ingress
2
+ // API.
3
+
4
+ const EMPTY_BODY = new Uint8Array(0)
5
+
6
+ /** Read the request body once, enforcing the size guard when enabled. */
7
+ export function readRequestBodyOnce(
8
+ req: Request,
9
+ maxBytes: number,
10
+ guard: boolean,
11
+ timeoutMs?: number,
12
+ ): Promise<Uint8Array> {
13
+ if (req.body === null) {
14
+ return Promise.resolve(EMPTY_BODY)
15
+ }
16
+
17
+ return readBodyWithLimit(req, maxBytes, guard, timeoutMs)
18
+ }
19
+
20
+ function bodyTooLargeError(): Error & { code?: string } {
21
+ const err = new Error('BODY_TOO_LARGE') as Error & { code?: string }
22
+ err.code = 'BODY_TOO_LARGE'
23
+ return err
24
+ }
25
+
26
+ /**
27
+ * Stream-read the request body, enforcing the size guard when enabled and an
28
+ * optional overall deadline.
29
+ *
30
+ * Errors carry `err.code`:
31
+ * - `BODY_TOO_LARGE` when `guard` is on and the body exceeds `maxBytes`
32
+ * (thrown as soon as the limit is crossed — the body is never fully
33
+ * buffered first, which is the slowloris/large-body protection).
34
+ * - `REQUEST_TIMEOUT` when `timeoutMs` elapses before the body completes.
35
+ */
36
+ export async function readBodyWithLimit(
37
+ req: Request,
38
+ maxBytes: number,
39
+ guard: boolean,
40
+ timeoutMs?: number,
41
+ ): Promise<Uint8Array> {
42
+ const body = req.body
43
+
44
+ if (!body) {
45
+ return EMPTY_BODY
46
+ }
47
+
48
+ const deadline = timeoutMs && timeoutMs > 0 ? timeoutMs : 0
49
+ let timer: ReturnType<typeof setTimeout> | undefined
50
+ const timeout = new Promise<never>((_resolve, reject) => {
51
+ if (deadline <= 0) return
52
+ timer = setTimeout(() => reject(timeoutError()), deadline)
53
+ // Don't let a pending body timeout keep the event loop alive once the
54
+ // request has otherwise completed (Node; a no-op under Bun).
55
+ timer.unref?.()
56
+ })
57
+
58
+ // Fast path: a declared Content-Length lets us PROVE the body fits the
59
+ // guard before reading, so one native `arrayBuffer()` read replaces the
60
+ // reader + per-chunk `Promise.race` + `concatUint8Arrays` churn (the bench
61
+ // POSTs are single-shot bodies — this removes ~4-6 allocations per write
62
+ // request). The deadline (when set) is still enforced with a single race,
63
+ // not one per chunk. `req.arrayBuffer()` is bounded by the server's
64
+ // `maxRequestBodySize`, and the post-read length re-check catches a client
65
+ // that lies about Content-Length.
66
+ const declared = req.headers.get('content-length')
67
+ if (declared !== null) {
68
+ const declaredLen = Number(declared)
69
+ if (Number.isFinite(declaredLen)) {
70
+ if (guard && declaredLen > maxBytes) {
71
+ throw bodyTooLargeError()
72
+ }
73
+ try {
74
+ const buf =
75
+ deadline > 0 ? await Promise.race([req.arrayBuffer(), timeout]) : await req.arrayBuffer()
76
+ const bytes = new Uint8Array(buf)
77
+ if (guard && bytes.byteLength > maxBytes) {
78
+ throw bodyTooLargeError()
79
+ }
80
+ return bytes
81
+ } finally {
82
+ clearTimeout(timer)
83
+ }
84
+ }
85
+ }
86
+
87
+ const reader = body.getReader()
88
+ const chunks: Uint8Array[] = []
89
+ let total = 0
90
+ // True when the stream was fully read to `done` — on that path the reader is
91
+ // already closed, so the `cancel()` in `finally` is a wasted async round-trip
92
+ // per request. It is still required when the loop exits early (timeout/error).
93
+ let completed = false
94
+
95
+ try {
96
+ for (;;) {
97
+ const { done, value } =
98
+ deadline > 0 ? await Promise.race([reader.read(), timeout]) : await reader.read()
99
+
100
+ if (done) {
101
+ completed = true
102
+ break
103
+ }
104
+ if (!value) continue
105
+
106
+ total += value.byteLength
107
+
108
+ if (guard && total > maxBytes) {
109
+ throw bodyTooLargeError()
110
+ }
111
+
112
+ chunks.push(value)
113
+ }
114
+ } finally {
115
+ clearTimeout(timer)
116
+ // Release the underlying stream. Cancelling while a read() is still
117
+ // pending (the timeout case) can resolve that pending read() as `done`
118
+ // and RACE the timeout rejection — so reject first, cancel only AFTER
119
+ // the loop has settled. The `.catch` keeps the cancel from masking a
120
+ // thrown REQUEST_TIMEOUT / BODY_TOO_LARGE. On the normal `done` path the
121
+ // stream is already closed, so skip the cancel entirely.
122
+ if (!completed) {
123
+ await reader.cancel().catch(() => {})
124
+ }
125
+ }
126
+
127
+ return concatUint8Arrays(chunks, total)
128
+ }
129
+
130
+ function timeoutError(): Error & { code?: string } {
131
+ const err = new Error('REQUEST_TIMEOUT') as Error & { code?: string }
132
+ err.code = 'REQUEST_TIMEOUT'
133
+ return err
134
+ }
135
+
136
+ function concatUint8Arrays(chunks: Uint8Array[], total: number): Uint8Array {
137
+ if (chunks.length === 0) return EMPTY_BODY
138
+ if (chunks.length === 1) return chunks[0] ?? EMPTY_BODY
139
+
140
+ const out = new Uint8Array(total)
141
+ let offset = 0
142
+
143
+ for (const chunk of chunks) {
144
+ out.set(chunk, offset)
145
+ offset += chunk.byteLength
146
+ }
147
+
148
+ return out
149
+ }
@@ -1,56 +1,228 @@
1
1
  // src/ingress/constants.ts — SINGLE SOURCE OF TRUTH for ingress binary layout constants
2
- // These are read from the Rust native addon to eliminate duplication across:
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 ingress_constants.rs
8
- // and are typed on the NativeAddon interface (src/native/index.ts).
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 addon from "../native";
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
- export const OUT_VERDICT: number = addon.INGRESS_OUT_VERDICT;
14
- export const OUT_ERROR_CODE: number = addon.INGRESS_OUT_ERROR_CODE;
15
- export const OUT_STATUS: number = addon.INGRESS_OUT_STATUS;
16
- export const OUT_FLAGS: number = addon.INGRESS_OUT_FLAGS;
17
- export const OUT_RATE_LIMIT: number = addon.INGRESS_OUT_RATE_LIMIT;
18
- export const OUT_RATE_REMAINING: number = addon.INGRESS_OUT_RATE_REMAINING;
19
- export const OUT_RATE_RESET: number = addon.INGRESS_OUT_RATE_RESET;
20
- export const OUT_RETRY_AFTER: number = addon.INGRESS_OUT_RETRY_AFTER;
21
- export const OUT_COOKIES_JSON_LEN: number = addon.INGRESS_OUT_COOKIES_JSON_LEN;
22
- export const OUT_QUERY_JSON_LEN: number = addon.INGRESS_OUT_QUERY_JSON_LEN;
23
- export const OUT_HEADER_VARIANT: number = addon.INGRESS_OUT_HEADER_VARIANT;
24
- export const OUT_BODY_JSON_LEN: number = addon.INGRESS_OUT_BODY_JSON_LEN;
25
- export const OUT_DATA_START: number = addon.INGRESS_OUT_DATA_START;
26
-
27
- // ── Flags ──────────────────────────────────────────────────
28
- export const FLAG_HAS_COOKIES: number = addon.INGRESS_FLAG_HAS_COOKIES;
29
- export const FLAG_HAS_QUERY: number = addon.INGRESS_FLAG_HAS_QUERY;
30
- export const FLAG_BODY_VALID_JSON: number = addon.INGRESS_FLAG_BODY_VALID_JSON;
31
- export const FLAG_SCHEMA_VALID: number = addon.INGRESS_FLAG_SCHEMA_VALID;
32
- export const FLAG_CORS_ALLOWED: number = addon.INGRESS_FLAG_CORS_ALLOWED;
33
- export const FLAG_IS_PREFLIGHT: number = addon.INGRESS_FLAG_IS_PREFLIGHT;
34
- export const FLAG_RATE_LIMITED: number = addon.INGRESS_FLAG_RATE_LIMITED;
35
- export const FLAG_HTTPS: number = addon.INGRESS_FLAG_HTTPS;
36
- export const FLAG_TRUSTED_PROXY: number = addon.INGRESS_FLAG_TRUSTED_PROXY;
37
- export const FLAG_BODY_TRUNCATED: number = addon.INGRESS_FLAG_BODY_TRUNCATED;
38
-
39
- // ── Header variant bits ────────────────────────────────────
40
- export const HV_JSON: number = addon.INGRESS_HV_JSON;
41
- export const HV_CORS_SIMPLE: number = addon.INGRESS_HV_CORS_SIMPLE;
42
- export const HV_CORS_PREFLIGHT: number = addon.INGRESS_HV_CORS_PREFLIGHT;
43
- export const HV_RATE_ACTIVE: number = addon.INGRESS_HV_RATE_ACTIVE;
44
- export const HV_RATE_LIMITED: number = addon.INGRESS_HV_RATE_LIMITED;
45
- export const HV_COUNT: number = addon.INGRESS_HV_COUNT;
46
-
47
- // ── Error codes ────────────────────────────────────────────
48
- export const ERR_CODE_NONE: number = addon.INGRESS_ERR_NONE;
49
- export const ERR_CODE_CORS_PREFLIGHT: number = addon.INGRESS_ERR_CORS_PREFLIGHT;
50
- export const ERR_CODE_RATE_LIMITED: number = addon.INGRESS_ERR_RATE_LIMITED;
51
- export const ERR_CODE_BODY_TOO_LARGE: number = addon.INGRESS_ERR_BODY_TOO_LARGE;
52
- export const ERR_CODE_INVALID_JSON: number = addon.INGRESS_ERR_INVALID_JSON;
53
- export const ERR_CODE_SCHEMA_VALIDATION: number = addon.INGRESS_ERR_SCHEMA_VALIDATION;
54
- export const ERR_CODE_BAD_REQUEST: number = addon.INGRESS_ERR_BAD_REQUEST;
55
- export const ERR_CODE_REQUEST_TOO_LARGE: number = addon.INGRESS_ERR_REQUEST_TOO_LARGE;
56
- export const ERR_CODE_INTERNAL: number = addon.INGRESS_ERR_INTERNAL;
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