castrum 0.9.7 → 0.9.9

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 (108) hide show
  1. package/Cargo.lock +1 -1
  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 +664 -8
  12. package/dist/index.js +2014 -1327
  13. package/docs/BENCHMARKS.md +13 -0
  14. package/docs/CASE_STUDY.md +1 -1
  15. package/docs/ENVIRONMENT.md +8 -1
  16. package/docs/FFI_BUN_GUIDE.md +1 -1
  17. package/docs/INGRESS.md +4 -4
  18. package/docs/REPO_MAP.md +7 -5
  19. package/docs/RND-CONCURRENCY.md +20 -4
  20. package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
  21. package/docs/superpowers/plans/2026-09-18-elysia-learnings-adoption.md +634 -0
  22. package/index.ts +6 -0
  23. package/package.json +4 -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 +6 -0
  29. package/rust/ffi/crypto.rs +63 -32
  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 +55 -35
  35. package/rust/ffi/tests.rs +24 -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/api.rs +21 -5
  40. package/rust/ingress/mod.rs +7 -0
  41. package/rust/ingress/native_route.rs +25 -5
  42. package/rust/ingress/schema_cache.rs +266 -0
  43. package/rust/task/completion.rs +7 -0
  44. package/rust/task/mod.rs +2 -1
  45. package/rust/task/runtime.rs +55 -7
  46. package/rust/task/tests.rs +128 -1
  47. package/src/ingress/abort.ts +48 -0
  48. package/src/ingress/body.ts +104 -8
  49. package/src/ingress/decode/baked-result.ts +13 -5
  50. package/src/ingress/decode/fast-result.ts +1 -1
  51. package/src/ingress/decode/result-base.ts +16 -2
  52. package/src/ingress/handlers.ts +104 -31
  53. package/src/ingress/headers/memoized-headers.ts +40 -0
  54. package/src/ingress/health.ts +22 -0
  55. package/src/ingress/index.ts +3 -1
  56. package/src/ingress/native-route.ts +25 -6
  57. package/src/ingress/options.ts +26 -2
  58. package/src/ingress/packing/gather-raw-headers.ts +24 -8
  59. package/src/ingress/response/baked-response.ts +118 -28
  60. package/src/ingress/router.ts +50 -3
  61. package/src/ingress/routes/common.ts +35 -11
  62. package/src/ingress/routes/echo.ts +60 -7
  63. package/src/ingress/routes/json-write.ts +32 -8
  64. package/src/ingress/routes/native.ts +24 -5
  65. package/src/ingress/routes/options.ts +6 -2
  66. package/src/ingress/routes/responder.ts +29 -9
  67. package/src/ingress/routes/safe-record.ts +57 -0
  68. package/src/ingress/server-error.ts +111 -0
  69. package/src/ingress/server-node.ts +135 -37
  70. package/src/ingress/server.ts +143 -36
  71. package/src/ingress/shared.ts +1 -1
  72. package/src/ingress/types.ts +15 -0
  73. package/src/integration/pipeline.ts +6 -0
  74. package/src/loader/ops.ts +1 -1
  75. package/src/native/ffi/build/codecs-selftest.ts +305 -0
  76. package/src/native/ffi/build/codecs.ts +0 -296
  77. package/src/native/ffi/build/instances-selftest.ts +264 -0
  78. package/src/native/ffi/build/instances.ts +10 -259
  79. package/src/native/ffi/build/parse-selftest.ts +238 -0
  80. package/src/native/ffi/build/parse.ts +11 -234
  81. package/src/native/ffi/build/task.ts +2 -1
  82. package/src/native/ffi/build.ts +5 -1
  83. package/src/native/ffi/selftest.ts +3 -3
  84. package/src/native/ffi/symbols.ts +460 -0
  85. package/src/native/ffi/types.ts +6 -1
  86. package/src/native/ffi.ts +2 -434
  87. package/src/native/ingress-binding.ts +39 -0
  88. package/src/native/types.ts +6 -0
  89. package/src/runtime/codec.ts +24 -0
  90. package/src/runtime/detect.ts +14 -0
  91. package/src/runtime/types.ts +11 -0
  92. package/src/rust-ffi/batch/build.ts +18 -8
  93. package/src/rust-ffi/batch/types.ts +15 -0
  94. package/src/rust-ffi/context.ts +46 -3
  95. package/src/rust-ffi/options.ts +19 -0
  96. package/src/rust-ffi/scalar/crypto.ts +8 -0
  97. package/src/rust-ffi/scalar/factories.ts +53 -20
  98. package/src/rust-ffi/scalar/http.ts +60 -10
  99. package/src/rust-ffi/scalar/interface.ts +18 -1
  100. package/src/rust-ffi/scalar/payload.ts +27 -1
  101. package/src/rust-ffi/text.ts +5 -4
  102. package/src/shared/buffer-pool.ts +54 -9
  103. package/src/shared/codec.ts +26 -0
  104. package/src/shared/memory.ts +68 -0
  105. package/src/shared/metrics.ts +86 -3
  106. package/src/shared/packed/parsers.ts +64 -7
  107. package/src/shared/runtime.ts +7 -0
  108. package/src/task/runtime.ts +81 -37
@@ -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
+ }
@@ -515,18 +515,23 @@ export interface BunFFI {
515
515
  // ── Off-thread task runtime (castrum_task_*) ──────────────────────
516
516
  /** Start the shared native task pool; 0 = started, 1 = already running. */
517
517
  taskInit(threads: number): number
518
- /** Submit a task op with packed `args` under `taskId`; 1 = accepted. */
518
+ /**
519
+ * Submit a task op with packed `args` under `taskId`. Returns `1` accepted,
520
+ * `2` overloaded (bounded queue full), `0` invalid args.
521
+ */
519
522
  taskSubmit(op: number, args: Uint8Array, taskId: number): number
520
523
  /**
521
524
  * Zero-copy submit: the op writes its result DIRECTLY into `output` on the
522
525
  * pool thread. `output` must be a stable, caller-owned buffer that stays
523
526
  * alive (and untouched) until the completion is drained — the runtime holds
524
527
  * the reference. The completion body is the 8-byte LE written length.
528
+ * Returns `1` accepted, `2` overloaded, `0` invalid args.
525
529
  */
526
530
  taskSubmitOut(op: number, args: Uint8Array, taskId: number, output: Uint8Array): number
527
531
  /**
528
532
  * Zero-copy INPUT: only `hdr` is copied; `data` is read in place by the pool
529
533
  * thread, so the caller must keep it alive (and untouched) until completion.
534
+ * Returns `1` accepted, `2` overloaded, `0` invalid args.
530
535
  */
531
536
  taskSubmitSlice(op: number, hdr: Uint8Array, data: Uint8Array, taskId: number): number
532
537
  /**