castrum 0.9.0 → 0.9.1

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 (102) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1374 -1772
  4. package/dist/index.js +5570 -4169
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +106 -14
  7. package/docs/BENCHMARKS.md +83 -47
  8. package/docs/CASE_STUDY.md +240 -0
  9. package/docs/ENVIRONMENT.md +8 -6
  10. package/docs/FFI_BUN_GUIDE.md +327 -0
  11. package/docs/GETTING_STARTED.md +15 -32
  12. package/docs/INGRESS-ROUTER.md +81 -0
  13. package/docs/INGRESS.md +5 -4
  14. package/docs/NATIVE-ROUTE.md +53 -0
  15. package/docs/REPO_MAP.md +46 -26
  16. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  17. package/docs/bun-builtins-decision-matrix.md +25 -18
  18. package/index.ts +9 -16
  19. package/package.json +36 -24
  20. package/src/ingress/body.ts +167 -27
  21. package/src/ingress/handlers.ts +91 -15
  22. package/src/ingress/health.ts +1 -0
  23. package/src/ingress/index.ts +39 -27
  24. package/src/ingress/packing/gather-raw-headers.ts +85 -21
  25. package/src/ingress/packing/header-packing.ts +5 -5
  26. package/src/ingress/packing/input-packer.ts +17 -3
  27. package/src/ingress/packing/select-headers.ts +12 -6
  28. package/src/ingress/router.ts +237 -0
  29. package/src/ingress/routes/common.ts +42 -1
  30. package/src/ingress/routes/fallback.ts +2 -2
  31. package/src/ingress/routes/head.ts +3 -13
  32. package/src/ingress/routes/index.ts +5 -4
  33. package/src/ingress/routes/json-write.ts +7 -31
  34. package/src/ingress/routes/options.ts +2 -12
  35. package/src/ingress/routes/read.ts +3 -13
  36. package/src/ingress/routes/responder.ts +159 -0
  37. package/src/ingress/server-node.ts +6 -6
  38. package/src/ingress/server.ts +45 -13
  39. package/src/ingress/types.ts +35 -1
  40. package/src/integration/pipeline.ts +3 -0
  41. package/src/integration/streaming.ts +2 -3
  42. package/src/integration/websocket.ts +4 -3
  43. package/src/loader/batch.ts +4 -0
  44. package/src/loader/cost.ts +1 -0
  45. package/src/loader/index.ts +43 -228
  46. package/src/loader/state.ts +84 -0
  47. package/src/loader/types.ts +145 -0
  48. package/src/native/ffi/constants.ts +58 -0
  49. package/src/native/ffi/selftest.ts +729 -0
  50. package/src/native/ffi/types.ts +474 -0
  51. package/src/native/ffi.ts +1063 -829
  52. package/src/native/loader.ts +52 -6
  53. package/src/native/types/instances.ts +274 -0
  54. package/src/native/types.ts +41 -238
  55. package/src/runtime/builtins.ts +131 -0
  56. package/src/runtime/codec.ts +147 -0
  57. package/src/runtime/detect.ts +36 -0
  58. package/src/runtime/index.ts +28 -0
  59. package/src/runtime/native.ts +35 -0
  60. package/src/runtime/server.ts +31 -0
  61. package/src/runtime/transport.ts +64 -0
  62. package/src/runtime/types.ts +173 -0
  63. package/src/runtime/uuid.ts +45 -0
  64. package/src/runtime/websocket.ts +30 -0
  65. package/src/rust-ffi/addon.ts +1 -1
  66. package/src/rust-ffi/batch/build.ts +155 -0
  67. package/src/rust-ffi/batch/index.ts +8 -0
  68. package/src/rust-ffi/batch/types.ts +92 -0
  69. package/src/rust-ffi/client.ts +5 -6
  70. package/src/rust-ffi/context.ts +39 -10
  71. package/src/rust-ffi/index.ts +28 -17
  72. package/src/rust-ffi/into.ts +21 -0
  73. package/src/rust-ffi/options.ts +1 -1
  74. package/src/rust-ffi/packed.ts +2 -2
  75. package/src/rust-ffi/proven.ts +16 -30
  76. package/src/rust-ffi/scalar/crypto.ts +188 -107
  77. package/src/rust-ffi/scalar/factories.ts +253 -27
  78. package/src/rust-ffi/scalar/hashing.ts +43 -41
  79. package/src/rust-ffi/scalar/http.ts +208 -77
  80. package/src/rust-ffi/scalar/index.ts +3 -3
  81. package/src/rust-ffi/scalar/interface.ts +88 -213
  82. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  83. package/src/rust-ffi/scalar/json.ts +28 -12
  84. package/src/rust-ffi/scalar/payload.ts +159 -42
  85. package/src/rust-ffi/text.ts +31 -5
  86. package/src/selection.ts +27 -31
  87. package/src/shared/adaptive.ts +1 -2
  88. package/src/shared/buffer-pool.ts +72 -17
  89. package/src/shared/bytes.ts +75 -6
  90. package/src/shared/codec.ts +78 -0
  91. package/src/shared/env.ts +4 -4
  92. package/src/shared/log.ts +1 -0
  93. package/src/shared/packed/index.ts +10 -0
  94. package/src/shared/packed/parsers.ts +31 -0
  95. package/src/shared/packed/schema.ts +23 -0
  96. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  97. package/src/shared/proven.ts +189 -496
  98. package/src/shared/runtime.ts +20 -25
  99. package/src/shared/uuid.ts +12 -28
  100. package/src/data/json-rows.ts +0 -33
  101. package/src/rust-ffi/batch.ts +0 -431
  102. package/src/shared/bench-classify.ts +0 -209
@@ -0,0 +1,729 @@
1
+ // src/native/ffi/selftest.ts — bind-time self-test for the bun:ffi transport.
2
+ //
3
+ // Every bound function is checked against known-good vectors at bind time. If
4
+ // ANY check fails (ABI mismatch, platform quirk, a future Bun regression), the
5
+ // whole ffi layer is disabled and the caller falls back to the napi addon —
6
+ // the public API never sees a wrong result. `bun:ffi` is experimental; this is
7
+ // the safety net. New C-ABI symbols MUST be added to `selfTest` too.
8
+
9
+ import { decodeJsonPacked } from '../../rust-ffi/scalar/json-packed'
10
+ import { decodeUtf8, encodeUtf8 } from '../../shared/codec'
11
+ import { SELFTEST_HEX, SELFTEST_JSON } from './constants'
12
+ import type { BunFFI } from './types'
13
+
14
+ /** Verify every bound function against known-good results; false disables ffi. */
15
+ export function selfTest(b: BunFFI): boolean {
16
+ // Codec-backed codecs — on Bun these use native transfer (Bun.ArrayBufferSink
17
+ // / bun:ffi CString) so no TextEncoder/TextDecoder runs on the Bun path.
18
+ const enc = { encode: encodeUtf8 }
19
+ const dec = { decode: decodeUtf8 }
20
+
21
+ if (b.crc32(enc.encode('123456789')) !== 0xcbf4_3926) {
22
+ return false
23
+ }
24
+ if (b.fnv1a64(enc.encode('foobar')) !== 0x8594_4171_f739_67e8n) {
25
+ return false
26
+ }
27
+ // XXH3-64 of empty input = 0x2d06800538d394c2 (standard reference vector).
28
+ if (b.xxh3(new Uint8Array(0)) !== 0x2d06800538d394c2n) {
29
+ return false
30
+ }
31
+ if (b.jsonValid(SELFTEST_JSON) !== true || b.jsonValid(enc.encode('{not json')) !== false) {
32
+ return false
33
+ }
34
+ // UTF-8 validity probe (native replacement for a fatal TextDecoder on the
35
+ // Bun path — used by the urlDecode wrapper).
36
+ if (!b.utf8Valid(enc.encode('héllo')) || b.utf8Valid(new Uint8Array([0xff, 0xfe]))) {
37
+ return false
38
+ }
39
+ const hexOut = new Uint8Array(SELFTEST_HEX.length * 2)
40
+ if (b.hexEncodeInto(SELFTEST_HEX, hexOut) !== 10 || dec.decode(hexOut) !== '68656c6c6f') {
41
+ return false
42
+ }
43
+ const urlInput = enc.encode('a b/c')
44
+ const urlOut = new Uint8Array(9)
45
+ if (b.urlEncodeInto(urlInput, urlOut) !== 9 || dec.decode(urlOut) !== 'a%20b%2Fc') {
46
+ return false
47
+ }
48
+
49
+ // Ingress layout blob (38 × u32 LE). The pinned values catch a reordered
50
+ // `#[repr(C)] IngressLayout` (drift → self-test fails → napi fallback); the
51
+ // Rust unit test `ingress_layout_c_abi_matches_output_source` pins every
52
+ // field against output.rs. Slot order mirrors the struct field order.
53
+ const layoutBuf = new Uint8Array(38 * 4)
54
+ b.ingressLayout(layoutBuf)
55
+ const layoutView = new DataView(layoutBuf.buffer, layoutBuf.byteOffset, layoutBuf.byteLength)
56
+ if (
57
+ layoutView.getUint32(0, true) !== 0 || // OUT_VERDICT
58
+ layoutView.getUint32(2 * 4, true) !== 2 || // OUT_STATUS
59
+ layoutView.getUint32(12 * 4, true) !== 48 || // OUT_DATA_START
60
+ layoutView.getUint32(13 * 4, true) !== 1 || // FLAG_HAS_COOKIES
61
+ layoutView.getUint32(28 * 4, true) !== 32 || // HV_COUNT
62
+ layoutView.getUint32(37 * 4, true) !== 8 // ERR_INTERNAL
63
+ ) {
64
+ return false
65
+ }
66
+
67
+ // Ingress pipeline C-ABI: with a null (0) inner handle the Rust side returns
68
+ // 0 immediately and the wrapper throws. This exercises the symbol's ABI (arg
69
+ // count/types/return) at bind time — a signature drift would surface here
70
+ // instead of crashing under load. Real frame→output parity is covered by
71
+ // ffi.test.ts against a live napi instance.
72
+ try {
73
+ b.ingressHandlePacked(0, enc.encode('/'), null, new Uint8Array(64))
74
+ return false // a null handle must throw, not return
75
+ } catch {
76
+ // expected: null inner handle → 0 → throw
77
+ }
78
+
79
+ // Ingress raw-components C-ABI: with a null (0) inner handle the Rust side
80
+ // returns 0 immediately and the wrapper throws — exercises the 12-arg ABI
81
+ // (incl. the two `cstring` slots for url/ip) at bind time. Real frame→output
82
+ // parity is covered by ffi.test.ts against a live napi instance.
83
+ try {
84
+ b.ingressHandleComponents(
85
+ 0,
86
+ 0, // GET
87
+ '/',
88
+ '',
89
+ enc.encode('rid'),
90
+ new Uint8Array(2), // empty packed headers [u16 0]
91
+ null,
92
+ new Uint8Array(64),
93
+ )
94
+ return false // a null handle must throw, not return
95
+ } catch {
96
+ // expected: null inner handle → 0 → throw
97
+ }
98
+
99
+ // Per-route native stack (castrum_route_*): compile a parseQuery-only
100
+ // descriptor, run one frame, assert the packed verdict. Exercises all three
101
+ // symbols' ABI at bind time (handle return, run needed-size, destroy).
102
+ const routeDesc = new Uint8Array(33)
103
+ const rd = new DataView(routeDesc.buffer)
104
+ rd.setUint32(0, 0x524f5554, true) // ROUTE_DESC_MAGIC "ROUT"
105
+ rd.setUint32(4, 3, true) // ROUTE_DESC_VERSION
106
+ rd.setUint32(8, 2 * 1024 * 1024, true) // maxBodyBytes
107
+ rd.setUint32(12, 8192, true) // maxQueryBytes
108
+ rd.setUint32(16, 8192, true) // maxCookieBytes
109
+ rd.setUint32(20, 0, true) // maxPairs
110
+ rd.setUint32(24, 1, true) // stageCount
111
+ routeDesc[28] = 0 // parseQuery
112
+ rd.setUint32(29, 0, true) // schemaCount
113
+ const routeHandle = b.routeCompile(routeDesc)
114
+ if (routeHandle === 0) {
115
+ return false
116
+ }
117
+ const routeFrame = new Uint8Array(15)
118
+ const rf = new DataView(routeFrame.buffer)
119
+ rf.setUint32(0, 0, true) // flags (no body)
120
+ rf.setUint32(4, 3, true) // qLen
121
+ routeFrame.set(enc.encode('a=1'), 8)
122
+ rf.setUint32(11, 0, true) // cLen
123
+ const routeOut = new Uint8Array(64)
124
+ const routeW = b.routeRun(routeHandle, routeFrame, routeOut)
125
+ if (routeW <= 8) {
126
+ return false // header + a query pair section must exceed 8 bytes
127
+ }
128
+ const rv = new DataView(routeOut.buffer)
129
+ const rFlags = rv.getUint32(0, true)
130
+ if ((rFlags & 0b1) === 0 || (rFlags & 0b100) === 0) {
131
+ return false // OK + QUERY_VALID bits must be set
132
+ }
133
+ b.routeDestroy(routeHandle)
134
+
135
+ // ── New bindings ───────────────────────────────────────────────────
136
+ // Validators take `cstring` ARGs (the engine transcodes the JS string).
137
+ if (
138
+ !b.validateEmail('a@b.com') ||
139
+ !b.validateUuid('550e8400-e29b-41d4-a716-446655440000') ||
140
+ !b.validateIpv4('192.168.0.1') ||
141
+ !b.validateIpv6('2001:db8::1') ||
142
+ b.validateEmail('not-an-email') ||
143
+ b.validateUuid('not-a-uuid')
144
+ ) {
145
+ return false
146
+ }
147
+ if (b.jsonSumIds(enc.encode(`[{"id":1},{"id":2}]`)) !== 3n) {
148
+ return false
149
+ }
150
+ // The packed [u8 ok][i64 sum LE] ABI: a legit zero-sum is ok, invalid input throws.
151
+ if (b.jsonSumIds(enc.encode(`[{"id":0},{"id":0}]`)) !== 0n) {
152
+ return false
153
+ }
154
+ let sumInvalidThrew = false
155
+ try {
156
+ b.jsonSumIds(enc.encode('nope'))
157
+ } catch {
158
+ sumInvalidThrew = true
159
+ }
160
+ if (!sumInvalidThrew) {
161
+ return false
162
+ }
163
+
164
+ // HMAC RFC 4231 test case 1 (0x0b × 20 key, "Hi There" data).
165
+ const hmacKey = new Uint8Array(20).fill(0x0b)
166
+ const hmacData = enc.encode('Hi There')
167
+ const hmacSig = enc.encode('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7')
168
+ const hmacHex = b.hmacSha256(hmacKey, hmacData)
169
+ if (hmacHex !== 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7') {
170
+ return false
171
+ }
172
+ if (!b.hmacSha256Verify(hmacKey, hmacData, hmacSig)) {
173
+ return false
174
+ }
175
+
176
+ // Decoders round-trip.
177
+ const decoded = b.hexDecode(enc.encode('68656c6c6f'))
178
+ if (
179
+ dec.decode(decoded) !== 'hello' ||
180
+ dec.decode(b.urlDecode(enc.encode('a%20b%2Fc'))) !== 'a b/c'
181
+ ) {
182
+ return false
183
+ }
184
+
185
+ // WebSocket accept key (RFC 6455 sample) — `key` is a `cstring` ARG.
186
+ if (b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==') !== 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=') {
187
+ return false
188
+ }
189
+
190
+ // ETag: strong = 10 bytes, weak = 12 bytes.
191
+ if (b.etag(SELFTEST_HEX).length !== 10 || b.etag(SELFTEST_HEX, true).length !== 12) {
192
+ return false
193
+ }
194
+ // ConditionalRequest opaque-handle: a null (0) handle must return false (the
195
+ // C side never dereferences freed state) — exercises the symbol's ABI. Real
196
+ // verdict parity is covered by test/unit/features/etag.test.ts.
197
+ if (b.conditionalIsNotModified(0, null, null) !== false) {
198
+ return false
199
+ }
200
+ // Phase 6 opaque-handle instances: null (0) handles must fail SAFELY (false /
201
+ // null / throw — never dereference freed state). Real verdict parity is
202
+ // covered by the per-instance JS tests.
203
+ if (b.mediaTypeMatcherMatches(0, enc.encode('x')) !== false) return false
204
+ if (b.acceptNegotiatorNegotiate(0, enc.encode('gzip')) !== null) return false
205
+ if (b.schemaValidatorValidate(0, enc.encode('{}')) !== false) return false
206
+ if (b.jwtSignerVerify(0, enc.encode('a.b.c'), 0) !== null) return false
207
+ let phase6Threw = false
208
+ try {
209
+ b.jwtSignerSign(0, enc.encode('{}'), 0)
210
+ } catch {
211
+ phase6Threw = true
212
+ }
213
+ if (!phase6Threw) return false
214
+ phase6Threw = false
215
+ try {
216
+ b.templateRender(0, enc.encode('{}'))
217
+ } catch {
218
+ phase6Threw = true
219
+ }
220
+ if (!phase6Threw) return false
221
+ phase6Threw = false
222
+ try {
223
+ b.rateLimiterCheck(0, 'k', 0)
224
+ } catch {
225
+ phase6Threw = true
226
+ }
227
+ if (!phase6Threw) return false
228
+
229
+ // base64.
230
+ if (b.base64Encode(SELFTEST_HEX) !== 'aGVsbG8=') {
231
+ return false
232
+ }
233
+ if (dec.decode(b.base64Decode(enc.encode('aGVsbG8='))) !== 'hello') {
234
+ return false
235
+ }
236
+
237
+ // Signed cookie round-trip.
238
+ const secret = enc.encode('s3cr3t-secret')
239
+ const signed = b.signCookie(SELFTEST_HEX, secret)
240
+ const verified = b.verifyCookie(enc.encode(signed), secret)
241
+ if (verified === null || verified !== 'hello') {
242
+ return false
243
+ }
244
+ if (b.verifyCookie(enc.encode('tampered.0000'), secret) !== null) {
245
+ return false
246
+ }
247
+
248
+ // CSRF token round-trip (issued token verifies against the same secret).
249
+ const csrfTokenStr = b.csrfToken(secret)
250
+ if (csrfTokenStr.length !== 129 || !b.csrfVerify(enc.encode(csrfTokenStr), secret)) {
251
+ return false
252
+ }
253
+
254
+ // Argon2id round-trip at minimum cost (fast) — full defaults would take ~50ms.
255
+ const pw = enc.encode('correct horse battery staple')
256
+ const salt = enc.encode('salty-salt-16b')
257
+ const phc = b.passwordHash(pw, salt, 8, 1, 1, 16)
258
+ if (phc.length === 0 || !b.passwordVerify(pw, enc.encode(phc))) {
259
+ return false
260
+ }
261
+
262
+ // bcrypt round-trip at minimum cost (fast) — `phc` is a `cstring` ARG.
263
+ const bcryptPhc = b.passwordHashBcrypt(pw, 4)
264
+ if (bcryptPhc.length === 0 || !b.passwordVerifyBcrypt(pw, bcryptPhc)) {
265
+ return false
266
+ }
267
+
268
+ // PBKDF2-HMAC-SHA256: password="password", salt="salt", c=1, dkLen=32.
269
+ // The C ABI writes the RAW derived key; hex-encode before comparing.
270
+ const dk = b.pbkdf2Sha256(enc.encode('password'), enc.encode('salt'), 1, 32)
271
+ if (b.hexEncode(dk) !== '120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b') {
272
+ return false
273
+ }
274
+
275
+ // AEAD AES-256-GCM round-trip (key 32B, nonce 12B).
276
+ const aeadKey = new Uint8Array(32).fill(0x42)
277
+ const nonce = new Uint8Array(12).fill(0x07)
278
+ const ct = b.aeadEncrypt(aeadKey, nonce, SELFTEST_HEX, 0)
279
+ const pt = b.aeadDecrypt(aeadKey, nonce, ct, 0)
280
+ if (pt === null || dec.decode(pt) !== 'hello') {
281
+ return false
282
+ }
283
+
284
+ // WebSocket frame: text frame, FIN, no mask → first byte 0x81.
285
+ const frame = b.wsFrameEncode(1, SELFTEST_HEX, false, true)
286
+ if (frame.length === 0 || frame[0] !== 0x81) {
287
+ return false
288
+ }
289
+
290
+ // JSON patch: add a key.
291
+ const patched = b.jsonPatch(
292
+ enc.encode(`{"a":"b"}`),
293
+ enc.encode(`[{"op":"add","path":"/c","value":"d"}]`),
294
+ )
295
+ if (!patched.includes(`"c":"d"`)) {
296
+ return false
297
+ }
298
+
299
+ // gzip / brotli round-trips.
300
+ const gz = b.gzipCompress(SELFTEST_HEX)
301
+ if (dec.decode(b.gzipDecompress(gz)) !== 'hello') {
302
+ return false
303
+ }
304
+ const br = b.brotliCompress(SELFTEST_HEX)
305
+ if (dec.decode(b.brotliDecompress(br)) !== 'hello') {
306
+ return false
307
+ }
308
+
309
+ // Needed-size convention: invalid compressed input throws IMMEDIATELY (the C
310
+ // ABI returns 0 = real error, so the JS wrapper does NOT grow-retry re-runs
311
+ // or allocate up to the 64 MiB decompression cap per bad input).
312
+ let decompressThrew = false
313
+ try {
314
+ b.gzipDecompress(enc.encode('not-a-gzip-stream'))
315
+ } catch {
316
+ decompressThrew = true
317
+ }
318
+ if (!decompressThrew) {
319
+ return false
320
+ }
321
+ decompressThrew = false
322
+ try {
323
+ b.brotliDecompress(enc.encode('not-brotli-stream'))
324
+ } catch {
325
+ decompressThrew = true
326
+ }
327
+ if (!decompressThrew) {
328
+ return false
329
+ }
330
+
331
+ // Packed parsers (non-empty output). Packed output is LARGER than input (each
332
+ // component gets a u32 length prefix), so size with the Rust allocator's
333
+ // conservative upper bound (`input.len() * 9 + 16` in query_parser.rs).
334
+ const req = enc.encode('GET /a?b=1 HTTP/1.1\r\nHost: example.com\r\n\r\n')
335
+ const reqOut = new Uint8Array(req.length * 9 + 16)
336
+ if (b.httpParseRequestPackedInto(req, reqOut) === 0) {
337
+ return false
338
+ }
339
+ const qIn = enc.encode('a=1&b=2')
340
+ const qOut = new Uint8Array(qIn.length * 9 + 16)
341
+ if (b.queryParsePackedInto(qIn, qOut) === 0) {
342
+ return false
343
+ }
344
+ const cIn = enc.encode('a=1; b=2')
345
+ const cOut = new Uint8Array(cIn.length * 9 + 16)
346
+ if (b.cookieParsePackedInto(cIn, cOut) === 0) {
347
+ return false
348
+ }
349
+
350
+ // ── Excluded-surface additions ────────────────────────────────────
351
+ // form parse shares the query core → 2 pairs.
352
+ const fIn = enc.encode('a=1&b=2')
353
+ const fOut = new Uint8Array(fIn.length * 9 + 16)
354
+ if (b.formParsePackedInto(fIn, fOut) === 0 || fOut[0] !== 2) {
355
+ return false
356
+ }
357
+
358
+ // multipart parse → 1 part named "field".
359
+ const boundary = enc.encode('----boundary')
360
+ // Wire format is `--{boundary}` — boundary is `----boundary`, so the body
361
+ // must open with `------boundary`.
362
+ const mBody = enc.encode(
363
+ '------boundary\r\nContent-Disposition: form-data; name="field"\r\n\r\nvalue\r\n------boundary--',
364
+ )
365
+ const mOut = b.multipartParsePacked(mBody, boundary)
366
+ if (mOut[0] !== 1) {
367
+ return false
368
+ }
369
+ // Packed layout: [u32 count][u32 name_len][name]... → name_len at offset 4.
370
+ const mNameLen =
371
+ (mOut[4] ?? 0) | ((mOut[5] ?? 0) << 8) | ((mOut[6] ?? 0) << 16) | ((mOut[7] ?? 0) << 24)
372
+ if (dec.decode(mOut.subarray(8, 8 + mNameLen)) !== 'field') {
373
+ return false
374
+ }
375
+
376
+ // WS frame decode: encode("hello") → decode → fin=1, opcode=1, payload="hello".
377
+ const wf = b.wsFrameEncode(1, SELFTEST_HEX, true, true)
378
+ const wd = b.wsFrameDecodePacked(wf)
379
+ if (wd === null || wd[0] !== 1 || wd[1] !== 1 || dec.decode(wd.subarray(6)) !== 'hello') {
380
+ return false
381
+ }
382
+ if (b.wsFrameDecodePacked(enc.encode('\x80')) !== null) {
383
+ return false
384
+ }
385
+ // Pooled sibling: same decode into a caller buffer; malformed → null.
386
+ const wdInto = new Uint8Array(wf.length + 6)
387
+ const wdW = b.wsFrameDecodePackedInto(wf, wdInto)
388
+ if (
389
+ wdW === null ||
390
+ wdInto[0] !== 1 ||
391
+ wdInto[1] !== 1 ||
392
+ dec.decode(wdInto.subarray(6, wdW)) !== 'hello'
393
+ ) {
394
+ return false
395
+ }
396
+ if (b.wsFrameDecodePackedInto(enc.encode('\x80'), wdInto) !== null) {
397
+ return false
398
+ }
399
+
400
+ // Random token pooled Into: writes byteLen*2 hex chars into the buffer.
401
+ const rtOut = new Uint8Array(32)
402
+ const rtW = b.randomTokenInto(16, rtOut)
403
+ if (rtW !== 32 || !/^[0-9a-f]{32}$/.test(dec.decode(rtOut))) {
404
+ return false
405
+ }
406
+ // Too-small buffer → throw (needed-size convention).
407
+ try {
408
+ b.randomTokenInto(16, new Uint8Array(8))
409
+ return false
410
+ } catch {
411
+ // expected
412
+ }
413
+
414
+ // Fixed-size writers pooled Into: ws_accept_key + etag + csrf_token match the
415
+ // cstring path byte-for-byte.
416
+ const waOut = new Uint8Array(28)
417
+ const waW = b.wsAcceptKeyInto(enc.encode('dGhlIHNhbXBsZSBub25jZQ=='), waOut)
418
+ const waExpected = b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==')
419
+ if (waW !== 28 || dec.decode(waOut) !== waExpected) {
420
+ return false
421
+ }
422
+ const etagData = enc.encode('hello')
423
+ const etagOut = new Uint8Array(16)
424
+ const etagW = b.etagInto(etagData, etagOut)
425
+ if (etagW !== 10 || dec.decode(etagOut.subarray(0, etagW)) !== b.etag(etagData)) {
426
+ return false
427
+ }
428
+ const csrfOut = new Uint8Array(129)
429
+ const csrfW = b.csrfTokenInto(enc.encode('csrf-secret'), csrfOut)
430
+ if (csrfW !== 129 || csrfOut[64] !== 46 /* '.' */) {
431
+ return false
432
+ }
433
+ // sign/verify cookie pooled Into: round-trip value bytes.
434
+ const ckVal = enc.encode('session-value')
435
+ const ckSec = enc.encode('s3cr3t-secret')
436
+ const ckOut = new Uint8Array(256)
437
+ const ckW = b.signCookieInto(ckVal, ckSec, ckOut)
438
+ if (ckW !== ckVal.length + 65) {
439
+ return false
440
+ }
441
+ const ckVerifyOut = new Uint8Array(256)
442
+ const ckV = b.verifyCookieInto(ckOut.subarray(0, ckW), ckSec, ckVerifyOut)
443
+ if (ckV !== ckVal.length || dec.decode(ckVerifyOut.subarray(0, ckV)) !== 'session-value') {
444
+ return false
445
+ }
446
+ // JWT sign pooled Into: same token as the cstring path.
447
+ const jwtClaims = enc.encode('{"sub":"user-1"}')
448
+ const jwtSecret2 = enc.encode('my-secret')
449
+ const jwtOut = new Uint8Array(512)
450
+ const jwtW = b.jwtSignBytesInto(jwtClaims, jwtSecret2, 0, 0, jwtOut)
451
+ if (jwtW === 0 || dec.decode(jwtOut.subarray(0, jwtW)).split('.').length !== 3) {
452
+ return false
453
+ }
454
+
455
+ // Multipart parse Into (pooled): 1 part named "field".
456
+ const mOutInto = new Uint8Array(mBody.length + boundary.length + 64)
457
+ const mW = b.multipartParsePackedInto(mBody, boundary, mOutInto)
458
+ if (mW === 0 || mOutInto[0] !== 1) {
459
+ return false
460
+ }
461
+ const mNameLenInto =
462
+ (mOutInto[4] ?? 0) |
463
+ ((mOutInto[5] ?? 0) << 8) |
464
+ ((mOutInto[6] ?? 0) << 16) |
465
+ ((mOutInto[7] ?? 0) << 24)
466
+ if (dec.decode(mOutInto.subarray(8, 8 + mNameLenInto)) !== 'field') {
467
+ return false
468
+ }
469
+
470
+ // JWT sign with ttl=0 (deterministic — no iat/exp), then verify the
471
+ // signature with the FFI HMAC to prove the binding is real.
472
+ const jwtSecret = enc.encode('my-secret')
473
+ const jwt = b.jwtSignBytes(enc.encode('{"sub":"user-1"}'), jwtSecret, 0, 0)
474
+ const jwtStr = jwt
475
+ const segs = jwtStr.split('.')
476
+ if (segs.length !== 3 || segs[0] === '' || segs[1] === '' || segs[2] === '') {
477
+ return false
478
+ }
479
+ const signingInput = enc.encode(`${segs[0]}.${segs[1]}`)
480
+ const sigHex = b.hmacSha256(jwtSecret, signingInput)
481
+ const sigBytes = b.base64Decode(enc.encode(segs[2] ?? ''), true, false)
482
+ if (sigBytes === null || b.hexEncode(sigBytes) !== sigHex) {
483
+ return false
484
+ }
485
+
486
+ // HTTP-date Into: Sun, 06 Nov 1994 08:49:37 GMT (fixed 29 bytes).
487
+ const dateOut = new Uint8Array(32)
488
+ const dateW = b.httpDateInto(784111777, dateOut)
489
+ if (dateW !== 29 || dec.decode(dateOut.subarray(0, dateW)) !== 'Sun, 06 Nov 1994 08:49:37 GMT') {
490
+ return false
491
+ }
492
+
493
+ // SSE encode Into: event/id/retry present + null-omission parity.
494
+ const sseData = enc.encode('hello')
495
+ const sse1 = dec.decode(b.sseEncodeEvent('update', sseData, '42', 3000))
496
+ if (sse1 !== 'id: 42\nevent: update\nretry: 3000\ndata: hello\n\n') {
497
+ return false
498
+ }
499
+ const sse2 = dec.decode(b.sseEncodeEvent(null, sseData, null, null))
500
+ if (sse2 !== 'data: hello\n\n') {
501
+ return false
502
+ }
503
+ // Present-but-empty event string emits the line (Option parity vs napi).
504
+ const sse3 = dec.decode(b.sseEncodeEvent('', sseData, null, null))
505
+ if (sse3 !== 'event: \ndata: hello\n\n') {
506
+ return false
507
+ }
508
+ // Pooled sibling: same bytes into a caller buffer; too-small throws.
509
+ const ssePool = new Uint8Array(128)
510
+ const sseW = b.sseEncodeEventInto('update', sseData, '42', 3000, ssePool)
511
+ if (sseW !== sse1.length || dec.decode(ssePool.subarray(0, sseW)) !== sse1) {
512
+ return false
513
+ }
514
+ try {
515
+ b.sseEncodeEventInto('update', sseData, '42', 3000, new Uint8Array(4))
516
+ return false
517
+ } catch {
518
+ // expected
519
+ }
520
+
521
+ // ── Wired WIP surface: JWT verify ──
522
+ // jwtVerify: sign (ttl=0 → no iat/exp) then verify → claims JSON; tampered → null.
523
+ const vjwtSecret = enc.encode('verify-secret')
524
+ const vjwt = b.jwtSignBytes(enc.encode('{"sub":"u-1"}'), vjwtSecret, 0, 0)
525
+ const vjwtClaims = b.jwtVerify(enc.encode(vjwt), vjwtSecret, 0)
526
+ if (vjwtClaims === null || !vjwtClaims.includes('"sub":"u-1"')) {
527
+ return false
528
+ }
529
+ if (b.jwtVerify(enc.encode('tampered.token.value'), vjwtSecret, 0) !== null) {
530
+ return false
531
+ }
532
+
533
+ // ── Phase 3 — stateless napi-only scalars ─────────────────────────
534
+ // jsonParsePacked: packed token stream decodes (via the REAL public decoder)
535
+ // to the same value as JSON.parse, with NO second text parse; invalid JSON
536
+ // throws (napi parity). Also verifies the needed-size retry path.
537
+ {
538
+ const packed = b.jsonParsePacked(enc.encode('{"a":1,"b":[true,null,"x"],"n":{"v":2.5}}'))
539
+ const v = decodeJsonPacked(packed) as { a: number; b: unknown[]; n: { v: number } }
540
+ if (v.a !== 1 || v.b[0] !== true || v.b[1] !== null || v.b[2] !== 'x' || v.n.v !== 2.5) {
541
+ return false
542
+ }
543
+ try {
544
+ b.jsonParsePacked(enc.encode('nope'))
545
+ return false
546
+ } catch {
547
+ // expected: invalid JSON → growExact throws
548
+ }
549
+ }
550
+
551
+ // parseMediaType: packed verdict, mediaType at [4..4+len].
552
+ const mt = b.parseMediaType(enc.encode('application/json; charset=utf-8'))
553
+ const mtLen = new DataView(mt.buffer, mt.byteOffset, mt.byteLength).getUint32(0, true)
554
+ if (mtLen === 0 || dec.decode(mt.subarray(4, 4 + mtLen)) !== 'application/json') {
555
+ return false
556
+ }
557
+
558
+ // parseHttpDate: RFC 7231 vector → epoch; malformed → null.
559
+ if (b.parseHttpDate(enc.encode('Sun, 06 Nov 1994 08:49:37 GMT')) !== 784111777n) {
560
+ return false
561
+ }
562
+ if (b.parseHttpDate(enc.encode('not a date')) !== null) {
563
+ return false
564
+ }
565
+
566
+ // parseAcceptEncoding: count, first encoding + q (f32 LE at offset 8+encLen).
567
+ const ae = b.parseAcceptEncoding(enc.encode('gzip, deflate;q=0.5'))
568
+ const aeView = new DataView(ae.buffer, ae.byteOffset, ae.byteLength)
569
+ if (aeView.getUint32(0, true) !== 2) {
570
+ return false
571
+ }
572
+ const aeLen = aeView.getUint32(4, true)
573
+ if (dec.decode(ae.subarray(8, 8 + aeLen)) !== 'gzip') {
574
+ return false
575
+ }
576
+ if (aeView.getFloat32(8 + aeLen, true) !== 1.0) {
577
+ return false
578
+ }
579
+
580
+ // urlEncodeQuery (packed pairs → sorted query text). Build the packed pairs
581
+ // inline — no shared/packed import on the bind-time critical path.
582
+ const qpPacked = new Uint8Array(4 + 2 * (4 + 1 + 4 + 1))
583
+ const qpView = new DataView(qpPacked.buffer)
584
+ qpView.setUint32(0, 2, true)
585
+ let qpOff = 4
586
+ for (const [k, v] of [
587
+ ['a', '1'],
588
+ ['b', '2'],
589
+ ] as const) {
590
+ qpView.setUint32(qpOff, k.length, true)
591
+ qpOff += 4
592
+ qpPacked.set(enc.encode(k), qpOff)
593
+ qpOff += k.length
594
+ qpView.setUint32(qpOff, v.length, true)
595
+ qpOff += 4
596
+ qpPacked.set(enc.encode(v), qpOff)
597
+ qpOff += v.length
598
+ }
599
+ if (b.urlEncodeQuery(qpPacked) !== 'a=1&b=2') {
600
+ return false
601
+ }
602
+ // urlResolve: RFC 3986 §5.4.1.
603
+ if (b.urlResolve(enc.encode('http://a/b/c/d;p?q'), enc.encode('g')) !== 'http://a/b/c/g') {
604
+ return false
605
+ }
606
+ // UrlBuilder opaque-handle: a null (0) handle must throw (the C side never
607
+ // dereferences freed state) — exercises the symbol's ABI. Real resolve
608
+ // parity is covered by test/unit/features/url-join.test.ts.
609
+ let urlBuilderThrew = false
610
+ try {
611
+ b.urlBuilderResolve(0, enc.encode('g'))
612
+ } catch {
613
+ urlBuilderThrew = true
614
+ }
615
+ if (!urlBuilderThrew) {
616
+ return false
617
+ }
618
+ // mimeFromExtension: known + unknown fallback.
619
+ if (b.mimeFromExtension('.js') !== 'text/javascript') {
620
+ return false
621
+ }
622
+ if (b.mimeFromExtension('nope') !== 'application/octet-stream') {
623
+ return false
624
+ }
625
+
626
+ // ── Bind-time coverage for the remaining pooled/string wrappers ──
627
+ // (closes the guard gap — every bound BunFFI method is now exercised here,
628
+ // matching the "every bound function is checked at bind time" header claim).
629
+
630
+ // urlEncode string form.
631
+ if (b.urlEncode(enc.encode('a b/c')) !== 'a%20b%2Fc') {
632
+ return false
633
+ }
634
+ // hexDecodeInto.
635
+ const hdInto = new Uint8Array(8)
636
+ if (
637
+ b.hexDecodeInto(enc.encode('68656c6c6f'), hdInto) !== 5 ||
638
+ dec.decode(hdInto.subarray(0, 5)) !== 'hello'
639
+ ) {
640
+ return false
641
+ }
642
+ // urlDecodeInto.
643
+ const udInto = new Uint8Array(16)
644
+ if (
645
+ b.urlDecodeInto(enc.encode('a%20b%2Fc'), udInto) !== 5 ||
646
+ dec.decode(udInto.subarray(0, 5)) !== 'a b/c'
647
+ ) {
648
+ return false
649
+ }
650
+ // base64EncodeInto.
651
+ const b64Into = new Uint8Array(16)
652
+ if (
653
+ b.base64EncodeInto(SELFTEST_HEX, b64Into) !== 8 ||
654
+ dec.decode(b64Into.subarray(0, 8)) !== 'aGVsbG8='
655
+ ) {
656
+ return false
657
+ }
658
+ // base64DecodeInto.
659
+ const bdInto = new Uint8Array(8)
660
+ if (
661
+ b.base64DecodeInto(enc.encode('aGVsbG8='), bdInto) !== 5 ||
662
+ dec.decode(bdInto.subarray(0, 5)) !== 'hello'
663
+ ) {
664
+ return false
665
+ }
666
+ // rateLimiterCheckKey: null (0) handle → throw (ABI exercise).
667
+ let rlKeyThrew = false
668
+ try {
669
+ b.rateLimiterCheckKey(0, 12345, 0)
670
+ } catch {
671
+ rlKeyThrew = true
672
+ }
673
+ if (!rlKeyThrew) {
674
+ return false
675
+ }
676
+ // randomToken string form.
677
+ if (!/^[0-9a-f]{32}$/.test(b.randomToken(16))) {
678
+ return false
679
+ }
680
+ // hmacSha256Into (RFC 4231 test case 1 hex vector).
681
+ const hmacInto = new Uint8Array(64)
682
+ if (
683
+ b.hmacSha256Into(hmacKey, hmacData, hmacInto) !== 64 ||
684
+ dec.decode(hmacInto) !== 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
685
+ ) {
686
+ return false
687
+ }
688
+ // aeadEncryptInto → decrypt round-trip.
689
+ const ctInto = new Uint8Array(64)
690
+ const ctW = b.aeadEncryptInto(aeadKey, nonce, SELFTEST_HEX, ctInto, 0)
691
+ if (ctW === 0 || b.aeadDecrypt(aeadKey, nonce, ctInto.subarray(0, ctW), 0) === null) {
692
+ return false
693
+ }
694
+ // wsFrameEncodeInto.
695
+ const frameInto = new Uint8Array(64)
696
+ if (b.wsFrameEncodeInto(1, SELFTEST_HEX, false, true, frameInto) === 0 || frameInto[0] !== 0x81) {
697
+ return false
698
+ }
699
+ // gzipCompressInto → decompress round-trip.
700
+ const gzInto = new Uint8Array(64)
701
+ const gzW = b.gzipCompressInto(SELFTEST_HEX, gzInto)
702
+ if (gzW === 0 || dec.decode(b.gzipDecompress(gzInto.subarray(0, gzW))) !== 'hello') {
703
+ return false
704
+ }
705
+ // brotliCompressInto → decompress round-trip.
706
+ const brInto = new Uint8Array(64)
707
+ const brW = b.brotliCompressInto(SELFTEST_HEX, brInto)
708
+ if (brW === 0 || dec.decode(b.brotliDecompress(brInto.subarray(0, brW))) !== 'hello') {
709
+ return false
710
+ }
711
+ // gzipDecompressInto → decompress into a CALLER buffer round-trip.
712
+ const gzDst = new Uint8Array(16)
713
+ const gzDW = b.gzipDecompressInto(gzInto.subarray(0, gzW), gzDst)
714
+ if (gzDW === 0 || dec.decode(gzDst.subarray(0, gzDW)) !== 'hello') {
715
+ return false
716
+ }
717
+ // brotliDecompressInto → decompress into a CALLER buffer round-trip.
718
+ const brDst = new Uint8Array(16)
719
+ const brDW = b.brotliDecompressInto(brInto.subarray(0, brW), brDst)
720
+ if (brDW === 0 || dec.decode(brDst.subarray(0, brDW)) !== 'hello') {
721
+ return false
722
+ }
723
+ // httpDate string form (RFC 7231 vector).
724
+ if (b.httpDate(784111777) !== 'Sun, 06 Nov 1994 08:49:37 GMT') {
725
+ return false
726
+ }
727
+
728
+ return true
729
+ }