castrum 0.9.6 → 0.9.8

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 (88) hide show
  1. package/Cargo.lock +4 -3
  2. package/Cargo.toml +1 -1
  3. package/castrum.darwin-arm64.node +0 -0
  4. package/castrum.darwin-x64.node +0 -0
  5. package/castrum.linux-arm64-gnu.node +0 -0
  6. package/castrum.linux-arm64-musl.node +0 -0
  7. package/castrum.linux-x64-gnu.node +0 -0
  8. package/castrum.linux-x64-musl.node +0 -0
  9. package/castrum.linux-x64-v3-gnu.node +0 -0
  10. package/castrum.win32-x64-msvc.node +0 -0
  11. package/dist/index.d.ts +501 -5
  12. package/dist/index.js +1658 -1314
  13. package/docs/ARCHITECTURE.md +1 -1
  14. package/docs/BENCHMARKS.md +13 -0
  15. package/docs/CASE_STUDY.md +2 -2
  16. package/docs/ENVIRONMENT.md +1 -1
  17. package/docs/FFI_BUN_GUIDE.md +13 -1
  18. package/docs/HOW_TO_ADD_AN_OP.md +24 -0
  19. package/docs/INGRESS.md +4 -4
  20. package/docs/REPO_MAP.md +8 -6
  21. package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
  22. package/index.ts +1 -0
  23. package/package.json +3 -2
  24. package/rust/crypto/aead.rs +44 -0
  25. package/rust/crypto/argon2.rs +32 -2
  26. package/rust/crypto/jwt/tests.rs +19 -0
  27. package/rust/crypto/jwt/token.rs +43 -28
  28. package/rust/crypto/session.rs +170 -7
  29. package/rust/ffi/crypto.rs +182 -61
  30. package/rust/ffi/hashing.rs +48 -33
  31. package/rust/ffi/http.rs +62 -36
  32. package/rust/ffi/metrics.rs +6 -2
  33. package/rust/ffi/payload.rs +47 -27
  34. package/rust/ffi/task.rs +51 -32
  35. package/rust/ffi/tests.rs +197 -0
  36. package/rust/ffi/util.rs +40 -0
  37. package/rust/http/headers.rs +13 -0
  38. package/rust/http/multipart.rs +139 -0
  39. package/rust/ingress/native_route.rs +21 -3
  40. package/rust/task/completion.rs +7 -0
  41. package/rust/task/mod.rs +1 -1
  42. package/rust/task/tests.rs +30 -0
  43. package/src/ingress/decode/baked-result.ts +13 -5
  44. package/src/ingress/decode/fast-result.ts +1 -1
  45. package/src/ingress/decode/result-base.ts +16 -2
  46. package/src/ingress/handlers.ts +103 -31
  47. package/src/ingress/headers/memoized-headers.ts +40 -0
  48. package/src/ingress/native-route.ts +25 -6
  49. package/src/ingress/options.ts +26 -2
  50. package/src/ingress/packing/gather-raw-headers.ts +24 -8
  51. package/src/ingress/response/baked-response.ts +118 -28
  52. package/src/ingress/router.ts +14 -2
  53. package/src/ingress/routes/common.ts +26 -10
  54. package/src/ingress/routes/echo.ts +50 -7
  55. package/src/ingress/routes/json-write.ts +21 -8
  56. package/src/ingress/routes/responder.ts +4 -4
  57. package/src/ingress/server-node.ts +33 -14
  58. package/src/ingress/server.ts +3 -4
  59. package/src/ingress/shared.ts +1 -1
  60. package/src/ingress/types.ts +15 -0
  61. package/src/loader/ops.ts +1 -1
  62. package/src/native/ffi/build/codecs-selftest.ts +305 -0
  63. package/src/native/ffi/build/codecs.ts +0 -296
  64. package/src/native/ffi/build/instances-selftest.ts +264 -0
  65. package/src/native/ffi/build/instances.ts +39 -241
  66. package/src/native/ffi/build/metrics.ts +0 -7
  67. package/src/native/ffi/build/parse-selftest.ts +238 -0
  68. package/src/native/ffi/build/parse.ts +11 -234
  69. package/src/native/ffi/build.ts +5 -1
  70. package/src/native/ffi/selftest.ts +3 -3
  71. package/src/native/ffi/symbols.ts +460 -0
  72. package/src/native/ffi/types.ts +17 -0
  73. package/src/native/ffi.ts +2 -422
  74. package/src/native/ingress-binding.ts +39 -0
  75. package/src/runtime/codec.ts +24 -0
  76. package/src/runtime/types.ts +11 -0
  77. package/src/rust-ffi/batch/build.ts +18 -8
  78. package/src/rust-ffi/batch/types.ts +15 -0
  79. package/src/rust-ffi/options.ts +19 -0
  80. package/src/rust-ffi/scalar/crypto.ts +8 -0
  81. package/src/rust-ffi/scalar/factories.ts +47 -20
  82. package/src/rust-ffi/scalar/http.ts +60 -10
  83. package/src/rust-ffi/scalar/interface.ts +4 -1
  84. package/src/rust-ffi/scalar/payload.ts +27 -1
  85. package/src/shared/buffer-pool.ts +44 -9
  86. package/src/shared/codec.ts +26 -0
  87. package/src/shared/packed/parsers.ts +64 -7
  88. package/src/task/runtime.ts +29 -28
@@ -0,0 +1,460 @@
1
+ // src/native/ffi/symbols.ts — the bun:ffi dlopen symbol map for the castrum C ABI.
2
+ //
3
+ // Every `#[no_mangle] extern "C"` export in rust/ffi/ is declared here with its
4
+ // exact ABI. Extracted from ffi.ts so the (large, declarative) ABI contract
5
+ // lives apart from the bind-time orchestration: `abi()` rewrites each `ptr`
6
+ // (plus its paired `usize`) to the atomic `buffer`/`buffer_length` pair when the
7
+ // engine supports it, and `u64_fast` avoids BigInt boxing on byte counts.
8
+
9
+ import type { FFITypeOrString } from 'bun:ffi'
10
+
11
+ /** One dlopen symbol entry: its argument ABI and return type. */
12
+ export type FFISymbolSpec = {
13
+ args: readonly FFITypeOrString[]
14
+ returns: FFITypeOrString
15
+ }
16
+
17
+ /**
18
+ * Build the dlopen symbol map for the castrum C ABI.
19
+ *
20
+ * @param useBufferLength - When true, `(ptr,len)` pairs become the atomic
21
+ * `buffer`/`buffer_length` ABI pair; otherwise they stay `(ptr,usize)`.
22
+ * @returns The symbol-name to ABI-spec map passed to `dlopen`.
23
+ */
24
+ export function buildFFISymbolMap(useBufferLength: boolean): Record<string, FFISymbolSpec> {
25
+ // String ABI specs ("ptr"/"usize"/"u32"...): accepted by bun:ffi and typed
26
+ // via `FFITypeOrString` in bun-types. usize == uint64 on the host ABI.
27
+ // Symbol keys must be the exact `#[no_mangle] extern "C"` names in
28
+ // rust/ffi/. Every `(ptr,len)` pair is passed as `(view, view.length)`.
29
+ // The 8 input-only single-buffer fns use `inputAbi`; everything else keeps
30
+ // `(ptr,len)` because the output length is the caller's write cap.
31
+ const inputAbi: readonly FFITypeOrString[] = useBufferLength
32
+ ? (['buffer', 'buffer_length'] as unknown as readonly FFITypeOrString[])
33
+ : ['ptr', 'usize']
34
+ // Phase 1.1: extend `buffer`/`buffer_length` to EVERY `(ptr,len)` pair, not
35
+ // just the input-only fns. The engine reads ptr + byteLength off the SAME
36
+ // view at call time (atomic snapshot), so call sites pass the view twice
37
+ // (see `lenOrView` below). Conversion is POSITIONAL and pair-aware: each
38
+ // `ptr` consumes its following `usize` as a length. Scalar args that happen
39
+ // to be `usize` (the opaque `inner` handle of `castrum_ingress_handle_packed`
40
+ // and the `max` param of gzip/brotli decompress) are passed through
41
+ // unchanged — a blind `usize`→`buffer_length` map would corrupt them.
42
+ const abi = (shape: readonly string[]): readonly FFITypeOrString[] => {
43
+ if (!useBufferLength) return shape as readonly FFITypeOrString[]
44
+ const out: FFITypeOrString[] = []
45
+ for (let i = 0; i < shape.length; i++) {
46
+ const t = shape[i]
47
+ if (t === 'ptr') {
48
+ out.push('buffer' as unknown as FFITypeOrString)
49
+ out.push('buffer_length' as unknown as FFITypeOrString)
50
+ i++ // consume the paired `usize`
51
+ } else {
52
+ out.push(t as FFITypeOrString)
53
+ }
54
+ }
55
+ return out
56
+ }
57
+ // `u64_fast` returns a plain `number` when the value fits (< 2^53) instead
58
+ // of a BigInt — bun-types lacks the literal, so bind it via a typed const
59
+ // (same cast pattern as the `buffer_length` probe above). Every bytes-written
60
+ // return fits in a double, so this removes the per-call BigInt boxing on the
61
+ // hot scalar / `*_into` / packed-parser / ingress calls.
62
+ const U64_FAST = 'u64_fast' as unknown as FFITypeOrString
63
+
64
+ return {
65
+ castrum_crc32: { args: inputAbi, returns: 'u32' },
66
+ castrum_fnv1a64: { args: inputAbi, returns: 'u64' },
67
+ castrum_xxh3: { args: inputAbi, returns: 'u64' },
68
+ castrum_json_valid: { args: inputAbi, returns: 'u8' },
69
+ castrum_utf8_valid: { args: inputAbi, returns: 'u8' },
70
+ castrum_hex_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
71
+ castrum_hex_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
72
+ castrum_url_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
73
+ castrum_url_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
74
+ // String-input validators: `cstring` ARG (the engine transcodes the JS
75
+ // string in-engine — no JS-side encode; Rust borrows via CStr::from_ptr).
76
+ // The `_bytes` siblings take a `(ptr,len)` pair — zero transcode when
77
+ // the caller already holds bytes (a cstring arg would force a decode +
78
+ // engine re-encode; docs/FFI_BUN_GUIDE.md §3 shape 4).
79
+ //
80
+ // SAFETY RULE for every `cstring` ARG added here: the transcode is
81
+ // NUL-TERMINATED, so an embedded U+0000 truncates the value in-engine. A
82
+ // verdict op bound this way can be fooled (`validateEmail` on
83
+ // 'a@b.com\0<script>' returned TRUE before the byte re-route). Either
84
+ // give the op a `_bytes` sibling and route byte callers to it, or guard
85
+ // the string form with `hasNul` — see docs/FFI_BUN_GUIDE.md §6.1,
86
+ // pinned by test/unit/native/cstring-nul.test.ts.
87
+ castrum_validate_email: { args: ['cstring'], returns: 'u8' },
88
+ castrum_validate_uuid: { args: ['cstring'], returns: 'u8' },
89
+ castrum_validate_ipv4: { args: ['cstring'], returns: 'u8' },
90
+ castrum_validate_ipv6: { args: ['cstring'], returns: 'u8' },
91
+ castrum_validate_email_bytes: { args: inputAbi, returns: 'u8' },
92
+ castrum_validate_uuid_bytes: { args: inputAbi, returns: 'u8' },
93
+ castrum_validate_ipv4_bytes: { args: inputAbi, returns: 'u8' },
94
+ castrum_validate_ipv6_bytes: { args: inputAbi, returns: 'u8' },
95
+ castrum_json_sum_ids: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
96
+ castrum_hmac_sha256_verify: {
97
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
98
+ returns: 'u8',
99
+ },
100
+ castrum_csrf_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
101
+ castrum_password_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
102
+ castrum_password_verify_bcrypt: {
103
+ // `phc` crosses as a `cstring` ARG; `password` stays a `(ptr,len)` pair.
104
+ args: abi(['ptr', 'usize', 'cstring']),
105
+ returns: 'u8',
106
+ },
107
+ castrum_ws_accept_key: { args: ['cstring'], returns: 'cstring' },
108
+ castrum_ws_accept_key_into: {
109
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
110
+ returns: U64_FAST,
111
+ },
112
+ castrum_etag: { args: abi(['ptr', 'usize', 'u8']), returns: 'cstring' },
113
+ castrum_etag_into: { args: abi(['ptr', 'usize', 'u8', 'ptr', 'usize']), returns: U64_FAST },
114
+ castrum_conditional_is_not_modified: {
115
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'u8']),
116
+ returns: 'u8',
117
+ },
118
+ castrum_media_type_matcher_matches: {
119
+ args: abi(['usize', 'ptr', 'usize']),
120
+ returns: 'u8',
121
+ },
122
+ castrum_accept_negotiator_negotiate: {
123
+ args: abi(['usize', 'ptr', 'usize']),
124
+ returns: 'cstring',
125
+ },
126
+ // Server-preference sibling (RFC 7231 server semantics): `header` is a
127
+ // `cstring` ARG — the engine transcodes the JS header string in-engine.
128
+ castrum_accept_negotiator_negotiate_server: {
129
+ args: ['usize', 'cstring'],
130
+ returns: 'cstring',
131
+ },
132
+ castrum_jwt_signer_sign: {
133
+ args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
134
+ returns: U64_FAST,
135
+ },
136
+ castrum_jwt_signer_verify: {
137
+ args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
138
+ returns: U64_FAST,
139
+ },
140
+ castrum_template_render: {
141
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
142
+ returns: U64_FAST,
143
+ },
144
+ castrum_schema_validator_validate: {
145
+ args: abi(['usize', 'ptr', 'usize']),
146
+ returns: 'u8',
147
+ },
148
+ castrum_rate_limiter_check: {
149
+ // `key` crosses as a `cstring` ARG (text, no NUL in the intended use).
150
+ args: abi(['usize', 'cstring', 'i64', 'ptr', 'usize']),
151
+ returns: U64_FAST,
152
+ },
153
+ castrum_rate_limiter_check_key: {
154
+ args: abi(['usize', 'i64', 'i64', 'ptr', 'usize']),
155
+ returns: U64_FAST,
156
+ },
157
+ castrum_random_token: { args: ['u32'], returns: 'cstring' },
158
+ castrum_random_token_into: { args: ['u32', ...abi(['ptr', 'usize'])], returns: U64_FAST },
159
+ castrum_base64_encode: {
160
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
161
+ returns: U64_FAST,
162
+ },
163
+ castrum_base64_decode: {
164
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
165
+ returns: U64_FAST,
166
+ },
167
+ castrum_hmac_sha256: {
168
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
169
+ returns: U64_FAST,
170
+ },
171
+ castrum_sign_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
172
+ castrum_sign_cookie_into: {
173
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
174
+ returns: U64_FAST,
175
+ },
176
+ castrum_verify_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
177
+ castrum_verify_cookie_into: {
178
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
179
+ returns: U64_FAST,
180
+ },
181
+ castrum_csrf_token: { args: abi(['ptr', 'usize']), returns: 'cstring' },
182
+ castrum_csrf_token_into: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
183
+ castrum_password_hash: {
184
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'u32', 'u32', 'ptr', 'usize']),
185
+ returns: U64_FAST,
186
+ },
187
+ castrum_password_hash_bcrypt: {
188
+ args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
189
+ returns: U64_FAST,
190
+ },
191
+ castrum_pbkdf2_sha256: {
192
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'ptr', 'usize']),
193
+ returns: U64_FAST,
194
+ },
195
+ castrum_aead_encrypt: {
196
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
197
+ returns: U64_FAST,
198
+ },
199
+ castrum_aead_decrypt: {
200
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
201
+ returns: U64_FAST,
202
+ },
203
+ castrum_ws_frame_encode: {
204
+ args: abi(['u8', 'ptr', 'usize', 'u8', 'u8', 'ptr', 'usize']),
205
+ returns: U64_FAST,
206
+ },
207
+ castrum_json_patch: {
208
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
209
+ returns: U64_FAST,
210
+ },
211
+ castrum_gzip_compress: {
212
+ args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
213
+ returns: U64_FAST,
214
+ },
215
+ castrum_gzip_decompress: {
216
+ args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
217
+ returns: U64_FAST,
218
+ },
219
+ castrum_brotli_compress: {
220
+ args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
221
+ returns: U64_FAST,
222
+ },
223
+ castrum_brotli_decompress: {
224
+ args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
225
+ returns: U64_FAST,
226
+ },
227
+ castrum_gzip_isize: { args: abi(['ptr', 'usize']), returns: 'u32' },
228
+ castrum_http_parse_request_packed: {
229
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
230
+ returns: U64_FAST,
231
+ },
232
+ castrum_query_parse_packed: {
233
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
234
+ returns: U64_FAST,
235
+ },
236
+ castrum_cookie_parse_packed: {
237
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
238
+ returns: U64_FAST,
239
+ },
240
+ castrum_http_date_into: { args: ['f64', ...abi(['ptr', 'usize'])], returns: U64_FAST },
241
+ castrum_sse_encode_into: {
242
+ // event/id stay `(ptr,len)` byte args. A cstring-ARG conversion was
243
+ // tried (2026-08-23) and reverted: the win was unproven once the
244
+ // codec encode path got faster, and ABI-desync segfaults during the
245
+ // experiment were misread as an engine bug — see docs/FFI_BUN_GUIDE.md
246
+ // §14 before touching this signature.
247
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'u32', 'ptr', 'usize']),
248
+ returns: U64_FAST,
249
+ },
250
+ // ── Excluded-surface additions ──
251
+ castrum_jwt_sign_bytes: {
252
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64']),
253
+ returns: 'cstring',
254
+ },
255
+ castrum_jwt_sign_bytes_into: {
256
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64', 'ptr', 'usize']),
257
+ returns: U64_FAST,
258
+ },
259
+ // ── Ed25519 / EdDSA JWT (RBAC auth) — bound + self-tested; the public
260
+ // surface is deferred to the auth-module consumer ────────────────
261
+ castrum_ed25519_generate_keypair: {
262
+ args: abi(['ptr', 'usize']),
263
+ returns: U64_FAST,
264
+ },
265
+ castrum_ed25519_sign: {
266
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
267
+ returns: U64_FAST,
268
+ },
269
+ castrum_ed25519_verify: {
270
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
271
+ returns: 'u32',
272
+ },
273
+ castrum_jwt_eddsa_sign: {
274
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64']),
275
+ returns: 'cstring',
276
+ },
277
+ castrum_jwt_eddsa_verify: {
278
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64']),
279
+ returns: 'cstring',
280
+ },
281
+ castrum_ws_frame_decode_packed: {
282
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
283
+ returns: U64_FAST,
284
+ },
285
+ castrum_multipart_parse_packed: {
286
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
287
+ returns: U64_FAST,
288
+ },
289
+ castrum_form_parse_packed: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
290
+ // ── Phase 3 — stateless napi-only scalars (now FFI) ──────────
291
+ // Structural parse: parses ONCE, emits a packed token stream with a
292
+ // deduped string table so JS assembles the value with NO second parse.
293
+ castrum_json_parse_packed: {
294
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
295
+ returns: U64_FAST,
296
+ },
297
+ castrum_parse_media_type: {
298
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
299
+ returns: U64_FAST,
300
+ },
301
+ castrum_parse_http_date: {
302
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
303
+ returns: U64_FAST,
304
+ },
305
+ castrum_parse_accept_encoding: {
306
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
307
+ returns: U64_FAST,
308
+ },
309
+ castrum_url_encode_query: { args: abi(['ptr', 'usize']), returns: 'cstring' },
310
+ castrum_url_resolve: {
311
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
312
+ returns: 'cstring',
313
+ },
314
+ castrum_url_builder_resolve: {
315
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
316
+ returns: U64_FAST,
317
+ },
318
+ castrum_mime_from_extension: { args: ['cstring'], returns: 'cstring' },
319
+ castrum_jwt_verify: {
320
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64']),
321
+ returns: 'cstring',
322
+ },
323
+ castrum_ingress_handle_packed: {
324
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
325
+ returns: U64_FAST,
326
+ },
327
+ // Raw-components sibling of `castrum_ingress_handle_packed`: `url`/`ip`
328
+ // are `cstring` ARGs (Bun transcodes the JS strings to call-scoped
329
+ // NUL-terminated buffers in-engine — no JS-side Buffer.write encode);
330
+ // rid/headers/body are `(ptr,len)` byte slices; out is `(ptr,len)`.
331
+ castrum_ingress_handle_components: {
332
+ args: abi([
333
+ 'usize',
334
+ 'u8',
335
+ 'cstring',
336
+ 'cstring',
337
+ 'ptr',
338
+ 'usize',
339
+ 'ptr',
340
+ 'usize',
341
+ 'ptr',
342
+ 'usize',
343
+ 'ptr',
344
+ 'usize',
345
+ ]),
346
+ returns: U64_FAST,
347
+ },
348
+ castrum_ingress_layout: { args: abi(['ptr', 'usize']), returns: U64_FAST },
349
+ // Per-route native stack (`@ignex/native` also dlopens these exact names
350
+ // itself; castrum binds them for the bind-time self-test + parity guard).
351
+ castrum_route_compile: { args: abi(['ptr', 'usize']), returns: U64_FAST },
352
+ castrum_route_run: {
353
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
354
+ returns: U64_FAST,
355
+ },
356
+ castrum_route_destroy: { args: ['usize'], returns: 'void' },
357
+ // ── Batch fixed-width hex validation (ObjectId-shaped ids, etc.) ──
358
+ // NEWLINE-separated lines in; one verdict byte (1/0) per line out.
359
+ castrum_hex_validate_batch: {
360
+ args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
361
+ returns: U64_FAST,
362
+ },
363
+ // ── JS-RegExp metacharacter escaping (needed-size convention) ──
364
+ castrum_regex_escape: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
365
+ // ── Metrics registry (`castrum_metrics_*`) ──────────────────────
366
+ // Caller-owned handle (route-stack ownership model): create returns a
367
+ // Box<Registry> as usize; declare fns take name/label-keys as cstring
368
+ // ARGS (label keys `\x1f`-separated) and return the u32 series id
369
+ // (0xFFFFFFFF = error); record/set cross the packed label VALUES as a
370
+ // `(ptr,len)` pair + the amount as f64.
371
+ castrum_metrics_create: { args: [], returns: U64_FAST },
372
+ castrum_metrics_counter: { args: ['usize', 'cstring', 'cstring'], returns: 'u32' },
373
+ castrum_metrics_gauge: { args: ['usize', 'cstring', 'cstring'], returns: 'u32' },
374
+ castrum_metrics_histogram: {
375
+ args: ['usize', 'cstring', 'cstring', 'cstring'],
376
+ returns: 'u32',
377
+ },
378
+ castrum_metrics_record: {
379
+ args: abi(['usize', 'u32', 'ptr', 'usize', 'f64']),
380
+ returns: 'u8',
381
+ },
382
+ castrum_metrics_gauge_set: {
383
+ args: abi(['usize', 'u32', 'ptr', 'usize', 'f64']),
384
+ returns: 'u8',
385
+ },
386
+ // Zero-encode siblings: the joined label values cross as ONE `cstring`
387
+ // ARG — the engine transcodes the JS string in-engine (no TextEncoder).
388
+ castrum_metrics_record_str: {
389
+ args: ['usize', 'u32', 'cstring', 'f64'],
390
+ returns: 'u8',
391
+ },
392
+ castrum_metrics_gauge_set_str: {
393
+ args: ['usize', 'u32', 'cstring', 'f64'],
394
+ returns: 'u8',
395
+ },
396
+ // Zero-copy text escape: cstring ARG in, cstring return out — the JS
397
+ // side does zero encode AND zero decode (engine clones at return).
398
+ castrum_regex_escape_str: { args: ['cstring'], returns: 'cstring' },
399
+ // String-input batch hex validation: `ids.join('\n')` crosses as a
400
+ // cstring ARG.
401
+ castrum_hex_validate_batch_str: {
402
+ args: abi(['cstring', 'u32', 'ptr', 'usize']),
403
+ returns: U64_FAST,
404
+ },
405
+ castrum_metrics_render: { args: abi(['usize', 'ptr', 'usize']), returns: U64_FAST },
406
+ castrum_metrics_snapshot: { args: abi(['usize', 'ptr', 'usize']), returns: U64_FAST },
407
+ castrum_metrics_record_batch: {
408
+ args: abi(['usize', 'ptr', 'usize']),
409
+ returns: 'u8',
410
+ },
411
+ // Fused wire-level validation: RAW query/cookie → JSON → draft-07 gate.
412
+ // `inner` is the SchemaValidator inner handle; strings cross as cstring
413
+ // ARGs (engine-transcoded).
414
+ castrum_query_validate: { args: ['usize', 'cstring'], returns: 'u8' },
415
+ castrum_cookie_validate: { args: ['usize', 'cstring'], returns: 'u8' },
416
+ // Fused session envelope: seal builds+signs; open verifies + extracts.
417
+ castrum_session_seal: {
418
+ args: ['cstring', 'cstring', 'i64', 'cstring'],
419
+ returns: 'cstring',
420
+ },
421
+ castrum_session_open: {
422
+ args: abi(['cstring', 'cstring', 'ptr', 'usize']),
423
+ returns: U64_FAST,
424
+ },
425
+ // Byte-arg siblings of the pair above: identical core and wire format,
426
+ // but every INPUT crosses as `(ptr,len)`. A `cstring` ARG costs the
427
+ // engine a transcode per arg AND silently truncates at an embedded
428
+ // U+0000 — reachable here because `id`/`data_json` are user-derived.
429
+ castrum_session_seal_bytes: {
430
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
431
+ returns: 'cstring',
432
+ },
433
+ castrum_session_open_bytes: {
434
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
435
+ returns: U64_FAST,
436
+ },
437
+ // Off-thread task runtime: `submit` takes a packed `(ptr,len)` arg blob
438
+ // plus a JS-assigned id; `drain` writes the packed completion batch into
439
+ // a caller buffer (needed-size convention). `set_doorbell` takes the raw
440
+ // `JSCallback({threadsafe:true}).ptr` as a bare pointer (`abi` leaves a
441
+ // lone `ptr` untouched), so the pool can wake JS once per batch.
442
+ castrum_task_init: { args: ['u32'], returns: 'u32' },
443
+ castrum_task_submit: { args: abi(['u32', 'ptr', 'usize', 'usize']), returns: 'u32' },
444
+ castrum_task_submit_slice: {
445
+ args: abi(['u32', 'ptr', 'usize', 'ptr', 'usize', 'usize']),
446
+ returns: 'u32',
447
+ },
448
+ castrum_task_submit_out: {
449
+ args: abi(['u32', 'ptr', 'usize', 'usize', 'ptr', 'usize']),
450
+ returns: 'u32',
451
+ },
452
+ castrum_task_drain: { args: abi(['ptr', 'usize']), returns: U64_FAST },
453
+ castrum_task_pending: { args: [], returns: 'u32' },
454
+ castrum_task_cancel: { args: ['usize'], returns: 'u32' },
455
+ castrum_task_set_doorbell: { args: ['ptr'], returns: 'u32' },
456
+ castrum_task_shutdown: { args: [], returns: 'u32' },
457
+ castrum_task_threads: { args: [], returns: 'u32' },
458
+ castrum_metrics_destroy: { args: ['usize'], returns: 'void' },
459
+ }
460
+ }
@@ -471,6 +471,23 @@ export interface BunFFI {
471
471
  * exact required size).
472
472
  */
473
473
  sessionOpen(token: string, secret: string, output: Uint8Array): number
474
+ /**
475
+ * Byte-arg `{@link sessionSeal}`: same envelope + wire format, inputs as
476
+ * bytes. Preferred for user-derived `id`/`dataJson`, because a `cstring` ARG
477
+ * is engine-transcoded (a per-arg cost) and SILENTLY TRUNCATES at an
478
+ * embedded `U+0000` before Rust sees it. `null` = failure.
479
+ */
480
+ sessionSealBytes(
481
+ id: Uint8Array,
482
+ dataJson: Uint8Array,
483
+ expSecs: number,
484
+ secret: Uint8Array,
485
+ ): string | null
486
+ /**
487
+ * Byte-arg `{@link sessionOpen}`: same output layout and needed-size
488
+ * convention, `token`/`secret` as bytes (exact length, no transcode).
489
+ */
490
+ sessionOpenBytes(token: Uint8Array, secret: Uint8Array, output: Uint8Array): number
474
491
 
475
492
  // ── Batch fixed-width hex validation / RegExp escaping ─────────────
476
493
  /**