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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 castrum contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -14,26 +14,32 @@ A high-performance Bun backend framework with Rust-accelerated FFI functions, pr
14
14
  │ │
15
15
  │ index.ts (entry point) │
16
16
  │ ├── src/rust-ffi/ Raw Rust FFI bindings │
17
- │ ├── src/native/ NAPI native addon loader │
17
+ │ ├── src/native/ bun:ffi (primary on Bun) + NAPI loader │
18
18
  │ ├── src/ingress/ High-performance HTTP ingress pipeline │
19
+ │ ├── src/integration/ Framework-agnostic pipeline/WS/SSE │
20
+ │ ├── src/loader/ Higher-order loader (HFC) │
19
21
  │ ├── src/baseline/ JS baseline (benchmark reference) │
20
- │ ├── src/data/ Data serialization utilities │
21
22
  │ └── src/shared/ Shared constants and helpers │
22
23
  │ │
23
- ├─────────────────── NAPI Bridge (napi-rs) ──────────────────────┤
24
+ ├──────── Native Bridge: bun:ffi (Bun, primary) + NAPI (fallback) ────────┤
24
25
  │ │
25
26
  │ Rust (cdylib) Layer │
26
27
  │ │
27
28
  │ rust/lib.rs │
28
- │ ├── Core Pipeline: ingress, cors, rate_limit, ip_trust │
29
- │ ├── Parsers: http_parser, query_parser, cookie_parser │
30
- │ ├── Validation: validation (email/uuid/IP) │
31
- │ ├── JSON: json_ops, json_patch_ops, json_schema, │
32
- │ │ json_ser │
33
- │ ├── Crypto: hashing, hmac_sha256, random_token │
34
- │ ├── Data: url_codec, mime_lookup, websocket │
35
- │ ├── Batch: batch │
36
- │ ├── Utilities: util, method, headers, output, terminal │
29
+ │ ├── util/ bytes, packed, batch, batch_core, threadpool, │
30
+ │ │ validation │
31
+ │ ├── http/ headers, method, http_parser, cookie/query │
32
+ │ │ parsers, form, media_type, url_codec/join, │
33
+ │ │ etag, accept, mime_lookup, multipart │
34
+ │ ├── crypto/ hmac_sha256, cookie_sign, csrf, jwt, aead, │
35
+ │ │ argon2, base64, hashing, random_token │
36
+ │ ├── json/ json_ops, json_ser, json_patch_ops, │
37
+ │ │ json_schema, fast_schema/ (zero-DOM) │
38
+ │ ├── payload/ compress, sse, ws_frames, websocket, template │
39
+ │ ├── ingress/ mod.rs (napi boundary), pipeline.rs (core), │
40
+ │ │ options/time/packed, cors, proxy, ip_trust, │
41
+ │ │ rate_limit, terminal, output, constants │
42
+ │ └── ffi.rs extern "C" exports (castrum_*) for bun:ffi │
37
43
  └─────────────────────────────────────────────────────────────────┘
38
44
  ```
39
45
 
@@ -46,21 +52,64 @@ bun install
46
52
  # Build Rust native addon (release)
47
53
  bun run build
48
54
 
55
+ # Build the compiled ESM entry for Node.js consumers (dist/)
56
+ bun run build:js
57
+
49
58
  # Run benchmarks
50
59
  bun bench.ts
51
60
 
52
61
  # Run tests
53
62
  bun test
63
+
64
+ # Run the Node.js smoke suite (requires `bun run build:js` first)
65
+ bun run test:node
66
+ ```
67
+
68
+ ## Node.js Support
69
+
70
+ **Bun is the primary target** (raw TypeScript, zero startup cost). The package is
71
+ *also* consumable from Node.js via a compiled ESM entry:
72
+
73
+ - Bun resolves the `bun` exports condition → `index.ts` (native TS execution).
74
+ - Node.js resolves the `node`/`default` condition → `dist/index.js` (compiled ESM,
75
+ requires Node.js >= 20.3). The bundled `dist/index.d.ts` covers the full API.
76
+ - **One cdylib, two transports.** The addon is built with napi-rs (a standard N-API
77
+ binary) but ALSO exports `extern "C"` symbols (`rust/ffi.rs`) that Bun loads directly:
78
+ - **Bun** — `bun:ffi` is the PRIMARY transport. `src/native/ffi.ts` `dlopen`s the same
79
+ `.node` file and JIT-calls the C ABI exports (~10-20ns crossing), covering the whole
80
+ stateless scalar API + the ingress per-request pipeline + the ingress layout
81
+ constants. NAPI is the fallback (Node, `CASTRUM_FFI_MODE=napi`, or a failed bind-time
82
+ self-test).
83
+ - **Node.js** — the N-API addon (`src/native/loader.ts`) is the fallback transport.
84
+ - `rust.transport()` returns `"ffi"` / `"napi"` (the resolved transport) and
85
+ `rust.ffiActive()` reports whether the bun:ffi transport is live; `CASTRUM_FFI_MODE`
86
+ overrides the selection (see docs/ENVIRONMENT.md).
87
+
88
+ ```ts
89
+ // Node.js (ESM)
90
+ import { rust, createIngressHandler, readHandler, createIngressServerNode } from "castrum";
91
+
92
+ rust.crc32(new Uint8Array([104, 105])); // native FFI works identically (napi fallback)
93
+
94
+ // Serve the SAME pre-baked ingress handlers over node:http
95
+ const srv = createIngressServerNode({ port: 3000, routes: { "/health": { read: handler } } });
96
+ await srv.ready; // resolves once the server is listening
54
97
  ```
55
98
 
99
+ `createIngressServer` (Bun.serve) is Bun-only; Node users use `createIngressServerNode`
100
+ with the same route handlers.
101
+
56
102
  ## Build
57
103
 
58
104
  ```bash
59
- # Production build
105
+ # Production build (native addon)
60
106
  bun run build
61
107
 
62
108
  # Debug build
63
109
  bun run build:debug
110
+
111
+ # Compiled JS entry for Node (bundle + bundled types -> dist/)
112
+ bun run build:js
64
113
  ```
65
114
 
66
115
  ## Benchmarks
@@ -78,27 +127,121 @@ bun run bench:http:ingress # vs Ingress (native)
78
127
  bun run bench:http:bun-only # vs raw Bun
79
128
 
80
129
  # Load patterns
81
- bun run bench:http:smoke # smoke test
130
+ bun run bench:http:smoke # smoke test (CI wire-format guard)
82
131
  bun run bench:http:stress # stress test
83
132
  bun run bench:http:heavy # heavy JSON
84
133
  bun run bench:http:crud # CRUD operations
134
+ bun run bench:http:spike # spike test
85
135
  bun run bench:http:soak # soak (endurance) test
86
136
  bun run bench:http:storm # storm (burst) test
87
137
  bun run bench:http:boundary # boundary conditions
138
+ bun run bench:http:all-heavy # heavy-JSON scenarios (substring filter "1": 01-smoke, 10-12, 13-20)
88
139
  ```
89
140
 
141
+ ### Bun built-ins comparison ("don't reinvent the wheel")
142
+
143
+ `bun run check` also races castrum ops against Bun's native built-ins
144
+ (`Bun.hash`, `Bun.password`, `Bun.CryptoHasher`, `Bun.gzipSync`,
145
+ `Bun.randomUUIDv7`) as a **diagnostic** set (`diag:` task names — never
146
+ audited by `check:proven`). Findings on Bun 1.4 / release addon:
147
+
148
+ - **Bun wins → `@flux/native` should prefer Bun**: non-crypto hashing
149
+ (`Bun.hash.crc32` ~3x, `xxHash3` ~4x, wyhash ~1-2x), gzip
150
+ (`Bun.gzipSync`/`gunzipSync` ~1.3-2x), `Bun.randomUUIDv7` (~2x), HMAC
151
+ (`Bun.CryptoHasher` ~1.2x).
152
+ - **Rust wins → keep**: argon2id password hashing/verify (~1.8-2x vs
153
+ `Bun.password`), bcrypt verify (~1.5x), and everything with no sync Bun
154
+ equivalent (brotli, PBKDF2, zero-DOM JSON/parsers).
155
+
156
+ Full table + per-op decisions: [`docs/bun-builtins-decision-matrix.md`](docs/bun-builtins-decision-matrix.md).
157
+
158
+ ### New primitives
159
+
160
+ | `rust.*` | Description | Benchmark vs |
161
+ |---|---|---|
162
+ | `xxh3` | XXH3-64 (non-crypto hash) | `Bun.hash.xxHash3` (~4x Bun — prefer Bun under Bun) |
163
+ | `passwordHashBcrypt` / `passwordVerifyBcrypt` | bcrypt `$2b$` PHC | `Bun.password` bcrypt (hash parity, verify rust ~1.5x) |
164
+ | `pbkdf2Sha256` | PBKDF2-HMAC-SHA256 | `node:crypto.pbkdf2Sync` (parity; only sync option in Bun) |
165
+ | `uuidv7()` (TS) | UUIDv7 — delegates to `Bun.randomUUIDv7`, `crypto.randomUUID` on Node | `Bun.randomUUIDv7` |
166
+
90
167
  ## API Reference
91
168
 
92
169
  ### Core Exports
93
170
 
94
171
  ```ts
95
- import { rust, native, rustBatch } from "castrum";
172
+ import { rust, proven } from "castrum";
173
+
174
+ // `rust` — All Rust FFI native implementations (flat, complete API)
175
+ // `proven` — The same full `rust.*` surface, with performance annotations
176
+ ```
177
+
178
+ ## Performance-annotated surface
179
+
180
+ Not every `rust.*` function beats its JavaScript baseline — e.g. `rust.jsonParse`
181
+ loses ~5x to Bun's `JSON.parse` (DOM + napi marshaling), and the native schema
182
+ validator is slower than `ajv` for small documents. `proven` exposes the **full
183
+ `rust.*` surface** (it is literally the same object as `rust`); performance is
184
+ communicated by `@performance` / `@deprecated` JSDoc annotations rather than by
185
+ filtering, so a function's classification is visible in your editor and in
186
+ `PROVEN_SURFACE`:
187
+
188
+ ```ts
189
+ import { proven, PROVEN_SURFACE } from "castrum";
190
+
191
+ proven.jsonValid(bytes); // ✓ proven — Rust wins vs the JS baseline
192
+ proven.fnv1a64(bytes); // ✓ proven — Rust wins ~9-16x
193
+ proven.jsonParse(bytes); // ✗ @deprecated — loses ~5x to JSON.parse
194
+ ```
195
+
196
+ - `proven` is the full `rust.*` client (`export const proven = rust`). Each
197
+ exported function's JSDoc carries its measured performance vs the JS baseline
198
+ (`@performance`) and is marked `@deprecated` when it loses — so the editor
199
+ steers you to the JS/Bun baseline for those. The single source of truth for the
200
+ classifications is `PROVEN_SURFACE` (`src/shared/proven.ts`). Statuses:
201
+ `proven` / `parity` / `not-competitive` / `unmeasured`.
202
+ - The full surface is unchanged — `rust.jsonParse`, `rust.createSchemaValidator`,
203
+ etc. remain available for completeness; they just aren't advertised as
204
+ performance wins.
205
+ - Classifications are based on the **release build on the shipped baseline CPU**
206
+ (published artifacts are baseline — not the local SIMD `build:perf`).
207
+
208
+ Keep the registry honest with the audit:
209
+
210
+ ```bash
211
+ bun run check # writes bench/results/cpu/latest.json (release build)
212
+ bun run check:proven # report-only: proves vs loses per function
213
+ bun run check:proven:fail # CI gate: exits 1 if a "proven" function regresses
214
+ ```
215
+
216
+ The audit compares each registry entry against the latest benchmark report and
217
+ fails (with `--fail`) when a `proven` function loses to its baseline by more
218
+ than the tolerance — catching real performance regressions (and wrong
219
+ classifications) before they ship.
220
+
221
+ ### Native-vs-JS selection surface
222
+
223
+ For framework consumers that want to bind each operation to a fixed
224
+ implementation at load time (instead of calling `rust.*` directly), castrum
225
+ exposes an auto-selected decision surface:
226
+
227
+ ```ts
228
+ import { opImpl, isNativeOp, opDecision } from "castrum";
96
229
 
97
- // `rust` — All Rust FFI native implementations (flat, complete API)
98
- // `native` — JavaScript/Bun baseline implementations for benchmarking
99
- // `rustBatch` — Back-compat alias === rust.batch
230
+ opImpl("gzipCompress"); // "native" | "js" | null
231
+ isNativeOp("crc32"); // boolean
232
+ opDecision("fnv1a64"); // { impl, note? } | null
100
233
  ```
101
234
 
235
+ - The single source of truth is `rust/selection.rs`, which embeds the
236
+ benchmark-generated `src/selection.json` (produced by
237
+ `scripts/select-native.ts --write`, audited by `--check` in CI). Under Bun,
238
+ ops where the Bun built-in beats the Rust addon (gzip, crc32, xxh3, HMAC,
239
+ random tokens — see `docs/bun-builtins-decision-matrix.md`) are selected as
240
+ `"js"` (a JS path that delegates to the Bun built-in); under Node the base
241
+ benchmark decision stands.
242
+ - Consumers read `opImpl(op)` **once at startup** and bind each op to a fixed
243
+ implementation — they do not swap native↔js per call.
244
+
102
245
  ### Rust Utility API
103
246
 
104
247
  `rust` is a single flat object exposing **every** native Rust utility with
@@ -122,8 +265,10 @@ string variants under `rust.text`.
122
265
  | `hmacSha256(key, data)` | `Uint8Array` — HMAC-SHA256 signature |
123
266
  | `hmacSha256Verify(key, data, sig)` | `boolean` |
124
267
  | `jsonValid(input)` | `boolean` — JSON validity check |
268
+ | `jsonParse(input)` | `unknown` — JSON → JS value (native sonic-rs DOM; throws on invalid) |
125
269
  | `jsonSumIds(input)` | `bigint` — sum of numeric `id` fields |
126
270
  | `jsonPatch(doc, patch)` | `Uint8Array` — RFC 6902 result |
271
+ | `createSchemaValidator(schema)` | `SchemaValidator` — compile a JSON Schema (draft-07); `.validate(doc): boolean`, `.validateDetailed(doc): SchemaError[]`, `.validateFirstError(doc): SchemaError | null`, `.validateBatchPackedCount(packed): number`, … |
127
272
  | `mimeFromExtension(ext)` | `Uint8Array` — MIME type |
128
273
  | `randomToken(byteLen)` | `Uint8Array` — CSPRNG token |
129
274
  | `urlEncode(input)` / `urlDecode(input)` | `Uint8Array` — percent encode/decode |
@@ -131,8 +276,36 @@ string variants under `rust.text`.
131
276
  | `validateEmail/Uuid/Ipv4/Ipv6(input)` | `boolean` |
132
277
  | `wsAcceptKey(key)` | `Uint8Array` — WebSocket accept key |
133
278
 
279
+ #### Framework actions (HTTP semantics + security)
280
+
281
+ | Function | Returns |
282
+ |----------|---------|
283
+ | `formParsePacked(body)` | `Uint8Array` — packed x-www-form-urlencoded pairs |
284
+ | `createFormParser(capacity?)` | `FormParser` — reusable-buffer form parser (`.parse`, `.parseInto`) |
285
+ | `parseMediaType(header)` | `{ mediaType, charset, boundary, params }` — Content-Type parse |
286
+ | `createMediaTypeParser()` | `MediaTypeParser` — `.parse`, `.matches(actual, expected)` (wildcards) |
287
+ | `etag(data, weak?)` | `Uint8Array` — strong/weak ETag (crc32-based) |
288
+ | `httpDate(secs?)` / `parseHttpDate(input)` | `Uint8Array` / `bigint | null` — IMF-fixdate format/parse |
289
+ | `createConditionalRequest(etag, lastModifiedSecs?)` | `ConditionalRequest` — `.isNotModified(ifNoneMatch, ifModifiedSince)` → 304 |
290
+ | `parseAcceptEncoding(header)` | `{ encoding, q, order }[]` |
291
+ | `createAcceptNegotiator(supported[])` | `AcceptNegotiator` — `.negotiate(header)` → best encoding |
292
+ | `base64Encode/Decode(input, urlSafe?, padding?)`, `base64UrlEncode/Decode(input)` | `Uint8Array` |
293
+ | `createBase64Codec(urlSafe?, padding?)` | `Base64Codec` — `.encode`, `.decode` |
294
+ | `hexEncode(input)` / `hexDecode(input)` | `Uint8Array` — lowercase hex |
295
+ | `signCookie(value, secret)` / `verifyCookie(signed, secret)` | `Uint8Array` / `Uint8Array | null` — signed cookies |
296
+ | `createCookieSigner(secret)` | `CookieSigner` — `.sign`, `.verify` (HMAC key compiled once) |
297
+ | `csrfToken(secret)` / `csrfVerify(token, secret)` | `Uint8Array` / `boolean` |
298
+ | `createCsrfProtector(secret)` | `CsrfProtector` — `.create`, `.verify` |
299
+ | `urlResolve(base, reference)` | `Uint8Array` — RFC 3986 resolution |
300
+ | `urlEncodeQuery(params)` | `Uint8Array` — percent-encoded query (sorted keys) |
301
+ | `createUrlBuilder(base)` | `UrlBuilder` — `.resolve(reference)` (base parsed once) |
302
+ | `createJwtSigner(secret, ttlSeconds?)` | `JwtSigner` — `.sign(claims, now)`, `.verify(token, now)` (HS256 key + ttl compiled once) |
303
+ | `createAeadCipher(key, algorithm?)` | `AeadCipher` — `.encrypt(nonce, pt)`, `.decrypt(nonce, ct)` (key compiled once) |
304
+ | `createArgon2Hasher(options?)` | `Argon2Hasher` — `.hash(password, salt)`, `.verify(password, phc)` (params compiled once) |
305
+ | `createMediaTypeMatcher(expected)` | `MediaTypeMatcher` — `.matches(actual)` (expected precompiled once) |
306
+
134
307
  Low-level / packed variants: `rust.httpParseRequestPacked`, `rust.queryParsePacked`,
135
- `rust.cookieParsePacked`.
308
+ `rust.cookieParsePacked`, `rust.formParsePacked`.
136
309
 
137
310
  #### String ergonomics (`rust.text`)
138
311
 
@@ -156,13 +329,80 @@ rust.batch.jsonValid(docs); // Uint8Array bitset (1 per doc)
156
329
  rust.batch.crc32(docs); // Uint32Array
157
330
  rust.batch.jsonSumIds(docs); // BigInt64Array
158
331
  rust.batch.queryParse(docs); // Uint8Array[]
332
+ rust.batch.formParse(docs); // Uint8Array[] (packed pairs per doc)
333
+ rust.batch.signCookie(docs, secret); // Uint8Array[] (signed)
334
+ rust.batch.verifyCookie(docs, secret); // Uint8Array bitset (valid?)
335
+ rust.batch.csrfVerify(tokens, secret); // Uint8Array bitset (valid?)
159
336
  rust.batch.schemaValidate(validator, docs); // bitset
337
+
338
+ // Hashing / url / mime / ws / base64url
339
+ rust.batch.fnv1a64(docs); // BigUint64Array (unsigned hashes)
340
+ rust.batch.etag(docs); // Uint8Array[]
341
+ rust.batch.urlEncode(docs); // Uint8Array[]
342
+ rust.batch.urlDecode(docs); // Uint8Array[]
343
+ rust.batch.base64UrlEncode(docs); // Uint8Array[]
344
+ rust.batch.wsAcceptKey(keys); // Uint8Array[]
345
+ rust.batch.mimeFromExtension(exts); // Uint8Array[]
346
+
347
+ // Backend-framework batches (zipped lists / shared args)
348
+ rust.batch.passwordVerify(pwds, phcs); // Uint8Array bitset (zipped)
349
+ rust.batch.urlResolve(bases, refs); // Uint8Array[] (zipped)
350
+ rust.batch.jwtSign(claimDocs, secret); // Uint8Array[] (sign N JSON claim docs)
351
+ rust.batch.jwtVerify(tokens, secret, now); // Uint8Array bitset
352
+ rust.batch.sseEncode(items, "event"); // Uint8Array[]
353
+ rust.batch.wsFrameEncode(items, 1, false, true); // Uint8Array[]
354
+ rust.batch.wsFrameDecode(frames); // Uint8Array[] (payloads)
355
+ rust.batch.multipartParse(bodies, boundary); // MultipartPart[][]
160
356
  ```
161
357
 
162
358
  Raw packed + metadata variants (advanced): `rust.packed.*` mirrors the native
163
359
  functions 1:1 (`jsonValidBatchPacked`, `jsonValidBatchCountPacked`,
164
360
  `queryParseBatchTotalLenPacked`, …).
165
361
 
362
+ #### Higher-order data loader (`castrum.loader`)
363
+
364
+ `loader` is a callable higher-order function over the whole batch surface. It
365
+ pre-binds an op so repeated calls skip registry dispatch, and it routes small
366
+ vs. large workloads automatically: a single item → one scalar native call, a
367
+ bulk → ONE packed batch call, and `load()` coalesces N same-tick calls into one
368
+ packed call (DataLoader-style) with a bounded LRU cache.
369
+
370
+ ```ts
371
+ import { loader } from "castrum";
372
+
373
+ const isEmail = loader("validateEmail");
374
+ isEmail(emailBytes); // scalar → boolean
375
+ isEmail([a, b, c]); // bulk → Uint8Array bitset (one packed call)
376
+ await isEmail.load(a); // coalesced + cached (auto batch/strategy)
377
+
378
+ // Covered op families (44 ops):
379
+ // hash crc32, fnv1a64
380
+ // json jsonValid, jsonSumIds, jsonPatch*, schemaValidate
381
+ // validate email, uuid, ipv4, ipv6
382
+ // parse query, cookie, form, httpParseRequest, urlResolve*
383
+ // encode hex, base64, base64Url, urlEncode, urlDecode, urlDecodeBytes, etag
384
+ // compress gzip, brotli
385
+ // crypto hmacSha256, hmacSha256Verify*, signCookie, verifyCookie, csrfVerify,
386
+ // passwordHash, passwordVerify*, aeadEncrypt, aeadDecrypt, jwtSign, jwtVerify
387
+ // web wsAcceptKey, wsFrameEncode, sseEncode, mimeFromExtension
388
+ // (* = paired: per-item companion array, e.g. loader.run("jsonPatch", docs, patches))
389
+
390
+ // Paired ops take a companion array:
391
+ loader.run("jsonPatch", docs, patches); // bulk
392
+ loader.run("jsonPatch", doc, patch); // single
393
+
394
+ // verifyCookie / jwtVerify are boolean-validity ops (valid/invalid);
395
+ // use rust.verifyCookie / rust.jwtVerify for the decoded value.
396
+
397
+ // Bind a schema validator for repeated single/bulk validation + count:
398
+ const schema = loader.schema(rust.createSchemaValidator(schemaBytes));
399
+ schema(doc); // boolean
400
+ schema(docs); // Uint8Array bitset
401
+ schema.count(docs); // number of valid docs
402
+
403
+ // Fine-grained control: createLoader({ adaptive, batchMin, maxCacheKeys, ... })
404
+ ```
405
+
166
406
  #### Configuring defaults
167
407
 
168
408
  Defaults are selected automatically, and can be overridden:
@@ -197,8 +437,7 @@ const myRust = createRust({ mimeCache: false });
197
437
  | `import { encoder, decoder } from "castrum"` | Text encode/decode utilities |
198
438
  | `import { packBatch, packPairs, unpackBitset, unpackByteResults, unpackI64ArrayAsBigInt, unpackU32Array } from "castrum"` | Batch/pair packing + unpacking utilities |
199
439
  | `import { readPairsPacked, pairsToObject, readHttpPacked } from "castrum"` | Decode packed pairs / parsed HTTP buffers |
200
- | `import { parseQueryString, parseCookieHeader } from "castrum"` | High-level string parsers (no hand-packing) |
201
- | `import { jsonRowsBytes, createJsonRows } from "castrum"` | JSON row serialization |
440
+ | `import { parseQueryString, parseCookieHeader, parseFormBody } from "castrum"` | High-level parsers (no hand-packing) |
202
441
  | `import { createIngress, createIngressSync, createIngressFast } from "castrum"` | HTTP ingress pipeline (low-level) |
203
442
  | `import { createIngressHandler, createIngressServer, readHandler, jsonWriteHandler, echoHandler, fallbackHandler } from "castrum"` | **Pre-baked** ingress handlers — route factories + Bun.serve builder |
204
443
 
@@ -299,46 +538,60 @@ castrum/
299
538
  ├── package.json # Node/Bun package config
300
539
  ├── tsconfig.json # TypeScript config
301
540
  │
302
- ├── rust/ # Rust source (cdylib)
303
- │ ├── lib.rs # Crate root, module declarations
304
- │ ├── ingress.rs # NAPI ingress class
305
- │ ├── cors.rs # CORS engine
306
- │ ├── rate_limit.rs # Keyed rate limiter
307
- │ ├── ip_trust.rs # IP trust & proxy resolution
308
- │ ├── http_parser.rs # HTTP request parser
309
- │ ├── query_parser.rs # Query string parser
310
- │ ├── cookie_parser.rs # Cookie header parser
311
- │ ├── validation.rs # Email/UUID/IP validation
312
- │ ├── hashing.rs # FNV-1a & fast hashing
313
- │ ├── hmac_sha256.rs # HMAC-SHA256
314
- │ ├── json_ops.rs # JSON operations
315
- │ ├── json_patch_ops.rs # JSON Patch operations
316
- │ ├── json_schema.rs # JSON Schema validation
317
- │ ├── json_ser.rs # JSON serialization utilities
318
- │ ├── url_codec.rs # URL encode/decode
319
- │ ├── mime_lookup.rs # MIME type lookup
320
- │ ├── websocket.rs # WebSocket utilities
321
- │ ├── random_token.rs # Secure random token generation
322
- │ ├── batch.rs # Batch processing
323
- │ ├── method.rs # HTTP method enum
324
- │ ├── headers.rs # Header packing
325
- │ ├── output.rs # Output buffer layout
326
- │ ├── terminal.rs # Terminal response writers
327
- │ ├── proxy.rs # Proxy detection utilities
328
- │ ├── util.rs # Core utilities
329
- │ └── ingress_constants.rs # NAPI-exported constants
541
+ ├── rust/ # Rust source (one cdylib, domain folders)
542
+ │ ├── lib.rs # Crate root: folder declarations + module map
543
+ │ ├── util/ # Shared infra: bytes, packed, batch(+core), threadpool, validation
544
+ │ ├── http/ # Wire formats: headers, method, http/query/cookie parsers,
545
+ │ │ # form, media_type, url_codec/join, etag, accept, mime_lookup, multipart
546
+ │ ├── crypto/ # Auth & hashing: hmac, cookie_sign, csrf, jwt, aead, argon2,
547
+ │ │ # base64, hashing, random_token
548
+ │ ├── json/ # JSON & schema: json_ops, json_ser, json_patch_ops, json_schema,
549
+ │ │ # fast_schema/ (zero-DOM draft-07 engine: types/cursor/compile/
550
+ │ │ # validate/errors — incl. pattern, enum/const, multipleOf,
551
+ │ │ # combinators, $ref, uniqueItems, detailed errors)
552
+ │ ├── payload/ # Output & streaming: compress, sse, ws_frames, websocket, template
553
+ │ ├── ingress/ # The ingress pipeline: mod.rs (napi boundary), pipeline.rs (core),
554
+ │ │ # tests.rs, options/time/packed, cors, proxy, ip_trust,
555
+ │ │ # rate_limit, terminal, output, ingress_constants
556
+ │ ├── test_support.rs # Shared test helpers
557
+ │ └── unit_tests.rs # Cross-module test suite
330
558
  │
331
559
  ├── src/ # TypeScript source
332
- │ ├── ingress/ # Ingress pipeline (TS layer)
333
- │ │ ├── index.ts # Public API + async factory
334
- │ │ ├── fast.ts # Fast synchronous handler (packed input)
335
- │ │ ├── handlers.ts # PRE-BAKED handlers + createIngressServer
560
+ │ ├── ingress/ # Ingress pipeline (TS layer), decomposed by task
561
+ │ │ ├── index.ts # Public API barrel + async factory
562
+ │ │ ├── fast.ts # Thin: createIngressFast (packed input)
563
+ │ │ ├── handlers.ts # Thin: createIngressHandler (JS-packs frame → handleRequestPacked, pre-baked)
564
+ │ │ ├── server.ts # createIngressServer (Bun.serve builder)
565
+ │ │ ├── server-node.ts # createIngressServerNode (node:http adapter)
336
566
  │ │ ├── constants.ts # Layout constants (from Rust)
337
- │ │ └── packed-input.ts # Input buffer packing
338
- │ ├── native/ # Native addon loader
339
- │ │ └── index.ts # Module loading + type definitions
567
+ │ │ ├── shared.ts # JS constants shared by both paths
568
+ │ │ ├── status.ts # Status normalization helpers
569
+ │ │ ├── errors.ts # Fast-path error-code mapping
570
+ │ │ ├── options.ts # IngressFast option types + validation
571
+ │ │ ├── types.ts # Public API types
572
+ │ │ ├── body.ts # Streaming request-body reader
573
+ │ │ ├── context.ts # Result snapshot + synthetic contexts
574
+ │ │ ├── packing/ # header-packing, input-packer, gather-raw-headers
575
+ │ │ ├── headers/ # cors, hsts, fast-templates, baked-templates
576
+ │ │ ├── decode/ # fast-result, baked-result
577
+ │ │ ├── response/ # terminal, error-bodies
578
+ │ │ └── routes/ # read/head/json-write/echo/delete/options/fallback factories
579
+ │ ├── native/ # Native transport: bun:ffi (primary on Bun) + NAPI fallback
580
+ │ │ ├── types.ts # NativeAddon interface + instance types
581
+ │ │ ├── loader.ts # getAddon/lazyAddon path resolution
582
+ │ │ ├── ffi.ts # bun:ffi C-ABI bindings (Bun-only; bind-time self-test)
583
+ │ │ └── index.ts # Barrel
340
584
  │ ├── rust-ffi/ # Rust FFI bindings
341
- │ │ └── index.ts # Flat, complete FFI API
585
+ │ │ ├── options.ts # RustOptions + input-normalization helpers
586
+ │ │ ├── addon.ts # shared lazy addon proxy
587
+ │ │ ├── context.ts # per-instance state + namespace helpers
588
+ │ │ ├── text.ts # string namespace
589
+ │ │ ├── batch.ts # array-of-bytes namespace
590
+ │ │ ├── packed.ts # raw packed-wire namespace
591
+ │ │ ├── scalar/ # scalar/feature methods (interface + per-domain builders)
592
+ │ │ ├── client.ts # createRust factory + default `rust`
593
+ │ │ ├── proven.ts # `proven` client (full surface, annotated)
594
+ │ │ └── index.ts # Barrel
342
595
  │ ├── baseline/ # JS baseline implementations
343
596
  │ │ ├── index.ts # Aggregator
344
597
  │ │ └── tasks/ # Individual baseline implementations
@@ -353,6 +606,12 @@ castrum/
353
606
  │
354
607
  ├── test/ # Tests (see CONTRIBUTING.md → Testing Requirements)
355
608
  ├── docs/ # Documentation
609
+ │ ├── REPO_MAP.md # ← START HERE: what is where & why
610
+ │ ├── GETTING_STARTED.md # Intern-friendly tutorial
611
+ │ ├── ARCHITECTURE.md # Deep-dive internals
612
+ │ ├── INGRESS.md # Pre-baked ingress API
613
+ │ ├── BENCHMARKS.md # Benchmark scenarios
614
+ │ └── ENVIRONMENT.md # Env vars
356
615
  └── scripts/ # Build/utility scripts
357
616
  ```
358
617
 
@@ -365,7 +624,8 @@ All Rust FFI functions are designed for **zero-allocation** paths where possible
365
624
  - **Ingress pipeline**: Single output buffer written by Rust, read by JS — minimal copies
366
625
  - **Validation functions**: Pure CPU-bound, 10-100x faster than JS equivalents
367
626
  - **Hashing**: SIMD-optimized crates under the hood
368
- - **Batch operations**: Packed binary format minimizes NAPI boundary crossings
627
+ - **Batch operations**: Packed binary format minimizes boundary crossings
628
+ (the batch/packed namespaces use the NAPI fallback — no C-ABI batch symbols)
369
629
  - **JSON parsing**: Uses `sonic-rs` (Rust) — one of the fastest JSON parsers available
370
630
 
371
631
  ---
@@ -397,9 +657,12 @@ Bump `version` in `package.json` (and `Cargo.toml`), add a `CHANGELOG.md` entry,
397
657
  then:
398
658
 
399
659
  ```bash
400
- git tag v0.6.0 && git push origin v0.6.0
660
+ git tag v<version> && git push origin v<version>
401
661
  ```
402
662
 
663
+ > Run `bun run check:version` first — it verifies `package.json`, `Cargo.toml`,
664
+ > and `CHANGELOG.md` agree. See [docs/REPO_MAP.md](./docs/REPO_MAP.md) §6.
665
+
403
666
  ### Manual
404
667
 
405
668
  To build and publish a release by hand (no CI needed; npm logged in or `NPM_TOKEN`
Binary file