castrum 0.9.1 → 0.9.3

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 (49) hide show
  1. package/README.md +1 -1
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +239 -5
  4. package/dist/index.js +3852 -3375
  5. package/docs/ARCHITECTURE.md +11 -9
  6. package/docs/BENCHMARKS.md +70 -38
  7. package/docs/CASE_STUDY.md +10 -7
  8. package/docs/ENVIRONMENT.md +16 -1
  9. package/docs/FFI_BUN_GUIDE.md +18 -10
  10. package/docs/GETTING_STARTED.md +5 -4
  11. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  12. package/docs/INGRESS-ROUTER.md +3 -2
  13. package/docs/INGRESS.md +6 -3
  14. package/docs/NATIVE-ROUTE.md +28 -4
  15. package/docs/REPO_MAP.md +60 -30
  16. package/docs/adr/0001-two-wire-formats.md +1 -1
  17. package/docs/ai/LOCAL_DEV.md +81 -0
  18. package/docs/ai/TREE.md +1070 -0
  19. package/package.json +24 -23
  20. package/src/ingress/decode/result-base.ts +10 -2
  21. package/src/ingress/handlers.ts +45 -290
  22. package/src/ingress/headers/security.ts +41 -0
  23. package/src/ingress/index.ts +12 -148
  24. package/src/ingress/native-route.ts +182 -0
  25. package/src/ingress/packing/gather-raw-headers.ts +26 -2
  26. package/src/ingress/packing/route-wire.ts +197 -0
  27. package/src/ingress/packing/select-headers.ts +8 -1
  28. package/src/ingress/path-matcher.ts +115 -0
  29. package/src/ingress/response/baked-response.ts +302 -0
  30. package/src/ingress/router.ts +42 -2
  31. package/src/ingress/routes/common.ts +9 -1
  32. package/src/ingress/routes/json-write.ts +21 -18
  33. package/src/ingress/routes/native.ts +130 -0
  34. package/src/ingress/server-node.ts +1 -1
  35. package/src/ingress/server.ts +38 -99
  36. package/src/ingress/sync.ts +151 -0
  37. package/src/ingress/types.ts +9 -0
  38. package/src/native/ffi/build/codecs.ts +736 -0
  39. package/src/native/ffi/build/compress.ts +248 -0
  40. package/src/native/ffi/build/instances.ts +598 -0
  41. package/src/native/ffi/build/parse.ts +539 -0
  42. package/src/native/ffi/build/util.ts +140 -0
  43. package/src/native/ffi/build.ts +94 -0
  44. package/src/native/ffi/selftest.ts +11 -718
  45. package/src/native/ffi/types.ts +25 -0
  46. package/src/native/ffi.ts +36 -1320
  47. package/src/native/types/instances.ts +5 -0
  48. package/src/rust-ffi/packed.ts +38 -0
  49. package/src/rust-ffi/scalar/factories.ts +4 -0
@@ -0,0 +1,598 @@
1
+ // src/native/ffi/build/instances.ts — opaque-handle (compiled-instance) BunFFI methods.
2
+ //
3
+ // Every method here evaluates against a PRECOMPILED native instance via its
4
+ // opaque `inner_ptr()` handle (JWT signer, template renderer, schema validator,
5
+ // rate limiter, media-type matcher, accept negotiator, conditional request, URL
6
+ // builder) or drives a compiled route / the full ingress pipeline. Receives the
7
+ // raw dlopen'd symbols and the per-bind context from `build()`.
8
+
9
+ import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
10
+ import { EMPTY_VIEW } from '../constants'
11
+ import type { BunFFI, Raw2, Raw3, Raw5, Raw6, Raw7, Raw8, Raw12, RawCStr } from '../types'
12
+ import type { BuildCtx } from './util'
13
+ import { cstr, growExact, unpackRateCheck } from './util'
14
+
15
+ /**
16
+ * Build the opaque-handle methods of the BunFFI surface. `ctx` is destructured
17
+ * so the method bodies read exactly as the original `build()`.
18
+ */
19
+ export function buildInstances(
20
+ sym: Record<string, (...a: unknown[]) => unknown>,
21
+ ctx: BuildCtx,
22
+ ): Partial<BunFFI> {
23
+ const { lenOrView, rateScratch, rateScratchView } = ctx
24
+
25
+ const conditionalIsNotModifiedRaw = sym.castrum_conditional_is_not_modified as Raw6
26
+ const mediaTypeMatcherMatchesRaw = sym.castrum_media_type_matcher_matches as Raw3
27
+ const acceptNegotiatorNegotiateRaw = sym.castrum_accept_negotiator_negotiate as RawCStr
28
+ const acceptNegotiatorNegotiateServerRaw = sym
29
+ .castrum_accept_negotiator_negotiate_server as (...a: unknown[]) => string | null
30
+ const ed25519GenerateKeypairRaw = sym.castrum_ed25519_generate_keypair as Raw2
31
+ const ed25519SignRaw = sym.castrum_ed25519_sign as Raw6
32
+ const ed25519VerifyRaw = sym.castrum_ed25519_verify as Raw6
33
+ const jwtEdDSASignSym = sym.castrum_jwt_eddsa_sign as (...a: unknown[]) => string | null
34
+ const jwtEdDSAVerifySym = sym.castrum_jwt_eddsa_verify as (...a: unknown[]) => string | null
35
+ const jwtSignerSignRaw = sym.castrum_jwt_signer_sign as Raw6
36
+ const jwtSignerVerifyRaw = sym.castrum_jwt_signer_verify as Raw6
37
+ const templateRenderRaw = sym.castrum_template_render as Raw5
38
+ const schemaValidatorValidateRaw = sym.castrum_schema_validator_validate as Raw3
39
+ const rateLimiterCheckRaw = sym.castrum_rate_limiter_check as Raw5
40
+ const rateLimiterCheckKeyRaw = sym.castrum_rate_limiter_check_key as Raw5
41
+ const jwtSignBytes = sym.castrum_jwt_sign_bytes as RawCStr
42
+ const jwtSignBytesInto = sym.castrum_jwt_sign_bytes_into as Raw8
43
+ const jwtVerifySym = sym.castrum_jwt_verify as (...a: unknown[]) => string | null
44
+ const urlBuilderResolveRaw = sym.castrum_url_builder_resolve as Raw5
45
+ const ingressHandlePacked = sym.castrum_ingress_handle_packed as Raw7
46
+ const ingressHandleComponentsSym = sym.castrum_ingress_handle_components as Raw12
47
+ const ingressLayoutSym = sym.castrum_ingress_layout as Raw2
48
+ const routeCompileSym = sym.castrum_route_compile as Raw2
49
+ const routeRunSym = sym.castrum_route_run as Raw5
50
+ const routeDestroySym = sym.castrum_route_destroy as (a: unknown) => void
51
+
52
+ return {
53
+ conditionalIsNotModified(inner, ifNoneMatch, ifModifiedSince) {
54
+ // Opaque-handle eval of the precompiled `ConditionalRequest` state.
55
+ // flags bit0 = If-None-Match present, bit1 = If-Modified-Since present
56
+ // (present-but-empty is distinct from absent — napi Option parity). A
57
+ // null handle (0) → 0 (never dereferences freed state).
58
+ const flags = (ifNoneMatch === null ? 0 : 1) | (ifModifiedSince === null ? 0 : 2)
59
+ const inm = ifNoneMatch ?? EMPTY_VIEW
60
+ const ims = ifModifiedSince ?? EMPTY_VIEW
61
+ return (
62
+ Number(
63
+ conditionalIsNotModifiedRaw(inner, inm, lenOrView(inm), ims, lenOrView(ims), flags),
64
+ ) === 1
65
+ )
66
+ },
67
+ mediaTypeMatcherMatches(inner, actual) {
68
+ // Precompiled expected-type match → u8.
69
+ return Number(mediaTypeMatcherMatchesRaw(inner, actual, lenOrView(actual))) === 1
70
+ },
71
+ acceptNegotiatorNegotiate(inner, header) {
72
+ // cstring best-supported encoding; `null` = identity (napi Option parity).
73
+ return acceptNegotiatorNegotiateRaw(inner, header, lenOrView(header))
74
+ },
75
+ acceptNegotiatorNegotiateServer(inner, header) {
76
+ // Server-preference tie-breaking (RFC 7231 server semantics). The C ABI
77
+ // takes `header` as a `cstring` ARG, so decode the bytes to a JS string
78
+ // (the engine transcodes it to the call-scoped NUL-terminated buffer).
79
+ // `null` = identity (napi Option parity).
80
+ return acceptNegotiatorNegotiateServerRaw(inner, decodeUtf8(header))
81
+ },
82
+ jwtSignerSign(inner, claimsJson, nowSeconds) {
83
+ // Precompiled key + ttl → compact token. 0 = invalid claims JSON (real
84
+ // error → growExact throws); w > output.length = exact needed size.
85
+ return growExact(
86
+ (out) =>
87
+ Number(
88
+ jwtSignerSignRaw(
89
+ inner,
90
+ claimsJson,
91
+ lenOrView(claimsJson),
92
+ BigInt(nowSeconds),
93
+ out,
94
+ lenOrView(out),
95
+ ),
96
+ ),
97
+ Math.min(claimsJson.length + 128, 64 * 1024),
98
+ 1024 * 1024,
99
+ 'jwt signer: invalid claims JSON or output buffer too small',
100
+ )
101
+ },
102
+ jwtSignerVerify(inner, token, nowSeconds) {
103
+ // Precompiled key → claims JSON bytes; 0 = invalid / expired → null.
104
+ const out = new Uint8Array(Math.min(token.length + 256, 64 * 1024))
105
+ const w = Number(
106
+ jwtSignerVerifyRaw(inner, token, lenOrView(token), BigInt(nowSeconds), out, lenOrView(out)),
107
+ )
108
+ if (w === 0) return null
109
+ if (w > out.length) {
110
+ const out2 = new Uint8Array(w)
111
+ const w2 = Number(
112
+ jwtSignerVerifyRaw(
113
+ inner,
114
+ token,
115
+ lenOrView(token),
116
+ BigInt(nowSeconds),
117
+ out2,
118
+ lenOrView(out2),
119
+ ),
120
+ )
121
+ return w2 === 0 ? null : out2.subarray(0, w2)
122
+ }
123
+ return out.subarray(0, w)
124
+ },
125
+ templateRender(inner, contextJson) {
126
+ // Compiled template + pre-serialized JSON context → UTF-8 bytes. 0 =
127
+ // invalid context / render error (real error → growExact throws).
128
+ return growExact(
129
+ (out) =>
130
+ Number(
131
+ templateRenderRaw(inner, contextJson, lenOrView(contextJson), out, lenOrView(out)),
132
+ ),
133
+ Math.min(contextJson.length + 128, 64 * 1024),
134
+ 1024 * 1024,
135
+ 'template render: invalid context JSON or render failed',
136
+ )
137
+ },
138
+ schemaValidatorValidate(inner, doc) {
139
+ return Number(schemaValidatorValidateRaw(inner, doc, lenOrView(doc))) === 1
140
+ },
141
+ rateLimiterCheck(inner, key, nowMs) {
142
+ // Packed [u8 allowed][u32 remaining LE][i64 reset_ms LE] (13 bytes).
143
+ // Reused scratch + cached DataView (no per-call allocs). `key` is a
144
+ // `cstring` ARG (the engine transcodes the JS string in-engine).
145
+ const out = rateScratch
146
+ const w = Number(
147
+ rateLimiterCheckRaw(inner, key, BigInt(Math.trunc(nowMs)), out, lenOrView(out)),
148
+ )
149
+ if (w === 0) throw new Error('rate limiter check: null handle')
150
+ return unpackRateCheck(out, rateScratchView)
151
+ },
152
+ rateLimiterCheckKey(inner, key, nowMs) {
153
+ // Packed [u8 allowed][u32 remaining LE][i64 reset_ms LE] (13 bytes).
154
+ // Reused scratch + cached DataView (no per-call allocs).
155
+ const out = rateScratch
156
+ const w = Number(
157
+ rateLimiterCheckKeyRaw(inner, BigInt(key), BigInt(Math.trunc(nowMs)), out, lenOrView(out)),
158
+ )
159
+ if (w === 0) throw new Error('rate limiter check: null handle')
160
+ return unpackRateCheck(out, rateScratchView)
161
+ },
162
+ jwtSignBytes(claimsJson, secret, ttl, now) {
163
+ // Compact HS256 token returned as a cstring (engine-cloned) — the Rust
164
+ // side builds the whole token and the JS pays zero decode. ttl<=0 = no
165
+ // iat/exp (napi Option<i64> sentinel). i64 args must be BigInt.
166
+ return cstr(
167
+ jwtSignBytes(
168
+ claimsJson,
169
+ lenOrView(claimsJson),
170
+ secret,
171
+ lenOrView(secret),
172
+ BigInt(ttl),
173
+ BigInt(now),
174
+ ),
175
+ 'jwt sign: invalid claims JSON',
176
+ )
177
+ },
178
+ jwtSignBytesInto(claimsJson, secret, ttl, now, output) {
179
+ // Native pooled `_into`: writes the compact token directly into the
180
+ // caller buffer (no cstring round-trip). Needed-size convention: a write
181
+ // larger than `output.length` reports the exact required size → throw;
182
+ // 0 = invalid claims JSON.
183
+ const w = Number(
184
+ jwtSignBytesInto(
185
+ claimsJson,
186
+ lenOrView(claimsJson),
187
+ secret,
188
+ lenOrView(secret),
189
+ BigInt(ttl),
190
+ BigInt(now),
191
+ output,
192
+ lenOrView(output),
193
+ ),
194
+ )
195
+ if (w === 0) {
196
+ throw new Error('jwt sign: invalid claims JSON')
197
+ }
198
+ if (w > output.length) {
199
+ throw new Error('jwt sign: output buffer too small')
200
+ }
201
+ return w
202
+ },
203
+ jwtVerify(token, secret, nowSeconds) {
204
+ // Verify an HS256 JWT → claims as a JSON cstring; `null` = invalid
205
+ // signature / expired / malformed (napi Option parity). `now` is an i64
206
+ // C arg → must be passed as BigInt.
207
+ return jwtVerifySym(token, lenOrView(token), secret, lenOrView(secret), BigInt(nowSeconds))
208
+ },
209
+ ed25519GenerateKeypair() {
210
+ // Packed `[u32 privLen][priv PKCS#8 v1 DER][u32 pubLen][pub SPKI DER]`
211
+ // (100 bytes). Needed-size convention: `0` = CSPRNG failure (throw), a
212
+ // write larger than the buffer = the exact required size (one exact
213
+ // retry). Decode the packed blob into the two DER byte slices.
214
+ const blob = growExact(
215
+ (out) => Number(ed25519GenerateKeypairRaw(out, lenOrView(out))),
216
+ 100,
217
+ 1024,
218
+ 'ed25519 keypair generation failed',
219
+ )
220
+ const privLen =
221
+ (blob[0] ?? 0) | ((blob[1] ?? 0) << 8) | ((blob[2] ?? 0) << 16) | ((blob[3] ?? 0) << 24)
222
+ const privateKey = blob.subarray(4, 4 + privLen)
223
+ const pubStart = 4 + privLen
224
+ const pubLen =
225
+ (blob[pubStart] ?? 0) |
226
+ ((blob[pubStart + 1] ?? 0) << 8) |
227
+ ((blob[pubStart + 2] ?? 0) << 16) |
228
+ ((blob[pubStart + 3] ?? 0) << 24)
229
+ return { privateKey, publicKey: blob.subarray(pubStart + 4, pubStart + 4 + pubLen) }
230
+ },
231
+ ed25519Sign(msg, privateKey) {
232
+ // 64-byte signature (needed-size convention: `0` = invalid private key →
233
+ // growExact throws).
234
+ return growExact(
235
+ (out) =>
236
+ Number(
237
+ ed25519SignRaw(
238
+ privateKey,
239
+ lenOrView(privateKey),
240
+ msg,
241
+ lenOrView(msg),
242
+ out,
243
+ lenOrView(out),
244
+ ),
245
+ ),
246
+ 64,
247
+ 64,
248
+ 'ed25519 sign failed (invalid private key)',
249
+ )
250
+ },
251
+ ed25519Verify(msg, signature, publicKey) {
252
+ // u8 → boolean. C ABI arg order is (key, msg, sig).
253
+ return (
254
+ Number(
255
+ ed25519VerifyRaw(
256
+ publicKey,
257
+ lenOrView(publicKey),
258
+ msg,
259
+ lenOrView(msg),
260
+ signature,
261
+ lenOrView(signature),
262
+ ),
263
+ ) === 1
264
+ )
265
+ },
266
+ jwtEdDSASign(claimsJson, privateKey, ttl, nowSeconds) {
267
+ // Compact EdDSA token as a cstring (engine clone); `null` = invalid
268
+ // claims JSON / invalid private key (the napi `jwt_sign_eddsa` throws,
269
+ // so a future public consumer maps null → throw). `ttl <= 0` = no
270
+ // `iat`/`exp` injection; `now`/`ttl` are i64 C args → BigInt.
271
+ return jwtEdDSASignSym(
272
+ claimsJson,
273
+ lenOrView(claimsJson),
274
+ privateKey,
275
+ lenOrView(privateKey),
276
+ BigInt(ttl),
277
+ BigInt(nowSeconds),
278
+ )
279
+ },
280
+ jwtEdDSAVerify(token, publicKey, nowSeconds) {
281
+ // Claims JSON as a cstring; `null` = invalid signature / expired /
282
+ // malformed (napi Option parity).
283
+ return jwtEdDSAVerifySym(
284
+ token,
285
+ lenOrView(token),
286
+ publicKey,
287
+ lenOrView(publicKey),
288
+ BigInt(nowSeconds),
289
+ )
290
+ },
291
+ urlBuilderResolve(inner, reference) {
292
+ // Opaque-handle resolve against a `UrlBuilder`'s PRECOMPILED base. 0 =
293
+ // null handle / non-UTF-8 reference (real error → growExact throws);
294
+ // w > output.length = exact needed size (one exact retry).
295
+ return growExact(
296
+ (out) =>
297
+ Number(urlBuilderResolveRaw(inner, reference, lenOrView(reference), out, lenOrView(out))),
298
+ Math.min(reference.length * 2 + 128, 64 * 1024),
299
+ 1024 * 1024,
300
+ 'url builder resolve: invalid reference or output buffer too small',
301
+ )
302
+ },
303
+ ingressHandlePacked(inner, input, body, output) {
304
+ const w = Number(
305
+ ingressHandlePacked(
306
+ inner,
307
+ input,
308
+ lenOrView(input),
309
+ body ?? EMPTY_VIEW,
310
+ // Under `buffer_length` the length slot must be a view (the engine
311
+ // reads byteLength off it) — `EMPTY_VIEW` has byteLength 0, matching
312
+ // a null body; under `(ptr,usize)` it's the explicit length 0.
313
+ body ? lenOrView(body) : lenOrView(EMPTY_VIEW),
314
+ output,
315
+ lenOrView(output),
316
+ ),
317
+ )
318
+ if (w === 0) {
319
+ throw new Error('ingress handle: output buffer too small or pipeline error')
320
+ }
321
+ return w
322
+ },
323
+ ingressHandleComponents(inner, methodKind, url, ip, rid, headers, body, output) {
324
+ // `url`/`ip` are passed as JS strings to `cstring` args — the engine
325
+ // transcodes them to call-scoped NUL-terminated UTF-8 buffers in-engine
326
+ // (no JS-side `Buffer.write` encode, no frame assembly for URL/IP).
327
+ const w = Number(
328
+ ingressHandleComponentsSym(
329
+ inner,
330
+ methodKind,
331
+ url,
332
+ ip,
333
+ rid,
334
+ lenOrView(rid),
335
+ headers,
336
+ lenOrView(headers),
337
+ body ?? EMPTY_VIEW,
338
+ body ? lenOrView(body) : lenOrView(EMPTY_VIEW),
339
+ output,
340
+ lenOrView(output),
341
+ ),
342
+ )
343
+ if (w === 0) {
344
+ throw new Error('ingress components: output buffer too small or pipeline error')
345
+ }
346
+ return w
347
+ },
348
+ ingressLayout(out) {
349
+ const w = Number(ingressLayoutSym(out, lenOrView(out)))
350
+ if (w !== out.length) {
351
+ throw new Error('ingress layout: output buffer too small')
352
+ }
353
+ return w
354
+ },
355
+ routeCompile(descriptor) {
356
+ const handle = Number(routeCompileSym(descriptor, lenOrView(descriptor)))
357
+ if (handle === 0) {
358
+ throw new Error('route compile: invalid route descriptor')
359
+ }
360
+ return handle
361
+ },
362
+ routeRun(handle, frame, output) {
363
+ // Needed-size convention: `0` = real error (malformed frame / panic); a
364
+ // write larger than `output.length` is the EXACT required size (caller
365
+ // allocates once and retries) — only a `0` write throws here.
366
+ const w = Number(routeRunSym(handle, frame, lenOrView(frame), output, lenOrView(output)))
367
+ if (w === 0) {
368
+ throw new Error('route run: malformed frame or pipeline error')
369
+ }
370
+ return w
371
+ },
372
+ routeDestroy(handle) {
373
+ routeDestroySym(handle)
374
+ },
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Bind-time self-test for the opaque-handle surface (the methods built in
380
+ * `buildInstances`). `false` disables the ffi layer and forces the napi
381
+ * fallback.
382
+ */
383
+ export function selfTestInstances(b: BunFFI): boolean {
384
+ const enc = { encode: encodeUtf8 }
385
+ const dec = { decode: decodeUtf8 }
386
+
387
+ // Ingress layout blob (38 × u32 LE). The pinned values catch a reordered
388
+ // `#[repr(C)] IngressLayout` (drift → self-test fails → napi fallback); the
389
+ // Rust unit test `ingress_layout_c_abi_matches_output_source` pins every
390
+ // field against output.rs. Slot order mirrors the struct field order.
391
+ const layoutBuf = new Uint8Array(38 * 4)
392
+ b.ingressLayout(layoutBuf)
393
+ const layoutView = new DataView(layoutBuf.buffer, layoutBuf.byteOffset, layoutBuf.byteLength)
394
+ if (
395
+ layoutView.getUint32(0, true) !== 0 || // OUT_VERDICT
396
+ layoutView.getUint32(2 * 4, true) !== 2 || // OUT_STATUS
397
+ layoutView.getUint32(12 * 4, true) !== 48 || // OUT_DATA_START
398
+ layoutView.getUint32(13 * 4, true) !== 1 || // FLAG_HAS_COOKIES
399
+ layoutView.getUint32(28 * 4, true) !== 32 || // HV_COUNT
400
+ layoutView.getUint32(37 * 4, true) !== 8 // ERR_INTERNAL
401
+ ) {
402
+ return false
403
+ }
404
+
405
+ // Ingress pipeline C-ABI: with a null (0) inner handle the Rust side returns
406
+ // 0 immediately and the wrapper throws. This exercises the symbol's ABI (arg
407
+ // count/types/return) at bind time — a signature drift would surface here
408
+ // instead of crashing under load. Real frame→output parity is covered by
409
+ // ffi.test.ts against a live napi instance.
410
+ try {
411
+ b.ingressHandlePacked(0, enc.encode('/'), null, new Uint8Array(64))
412
+ return false // a null handle must throw, not return
413
+ } catch {
414
+ // expected: null inner handle → 0 → throw
415
+ }
416
+
417
+ // Ingress raw-components C-ABI: with a null (0) inner handle the Rust side
418
+ // returns 0 immediately and the wrapper throws — exercises the 12-arg ABI
419
+ // (incl. the two `cstring` slots for url/ip) at bind time. Real frame→output
420
+ // parity is covered by ffi.test.ts against a live napi instance.
421
+ try {
422
+ b.ingressHandleComponents(
423
+ 0,
424
+ 0, // GET
425
+ '/',
426
+ '',
427
+ enc.encode('rid'),
428
+ new Uint8Array(2), // empty packed headers [u16 0]
429
+ null,
430
+ new Uint8Array(64),
431
+ )
432
+ return false // a null handle must throw, not return
433
+ } catch {
434
+ // expected: null inner handle → 0 → throw
435
+ }
436
+
437
+ // Per-route native stack (castrum_route_*): compile a parseQuery-only
438
+ // descriptor, run one frame, assert the packed verdict. Exercises all three
439
+ // symbols' ABI at bind time (handle return, run needed-size, destroy).
440
+ const routeDesc = new Uint8Array(33)
441
+ const rd = new DataView(routeDesc.buffer)
442
+ rd.setUint32(0, 0x524f5554, true) // ROUTE_DESC_MAGIC "ROUT"
443
+ rd.setUint32(4, 3, true) // ROUTE_DESC_VERSION
444
+ rd.setUint32(8, 2 * 1024 * 1024, true) // maxBodyBytes
445
+ rd.setUint32(12, 8192, true) // maxQueryBytes
446
+ rd.setUint32(16, 8192, true) // maxCookieBytes
447
+ rd.setUint32(20, 0, true) // maxPairs
448
+ rd.setUint32(24, 1, true) // stageCount
449
+ routeDesc[28] = 0 // parseQuery
450
+ rd.setUint32(29, 0, true) // schemaCount
451
+ const routeHandle = b.routeCompile(routeDesc)
452
+ if (routeHandle === 0) {
453
+ return false
454
+ }
455
+ const routeFrame = new Uint8Array(15)
456
+ const rf = new DataView(routeFrame.buffer)
457
+ rf.setUint32(0, 0, true) // flags (no body)
458
+ rf.setUint32(4, 3, true) // qLen
459
+ routeFrame.set(enc.encode('a=1'), 8)
460
+ rf.setUint32(11, 0, true) // cLen
461
+ const routeOut = new Uint8Array(64)
462
+ const routeW = b.routeRun(routeHandle, routeFrame, routeOut)
463
+ if (routeW <= 8) {
464
+ return false // header + a query pair section must exceed 8 bytes
465
+ }
466
+ const rv = new DataView(routeOut.buffer)
467
+ const rFlags = rv.getUint32(0, true)
468
+ if ((rFlags & 0b1) === 0 || (rFlags & 0b100) === 0) {
469
+ return false // OK + QUERY_VALID bits must be set
470
+ }
471
+ b.routeDestroy(routeHandle)
472
+
473
+ // ConditionalRequest opaque-handle: a null (0) handle must return false (the
474
+ // C side never dereferences freed state) — exercises the symbol's ABI. Real
475
+ // verdict parity is covered by test/unit/features/etag.test.ts.
476
+ if (b.conditionalIsNotModified(0, null, null) !== false) {
477
+ return false
478
+ }
479
+
480
+ // Phase 6 opaque-handle instances: null (0) handles must fail SAFELY (false /
481
+ // null / throw — never dereference freed state). Real verdict parity is
482
+ // covered by the per-instance JS tests.
483
+ if (b.mediaTypeMatcherMatches(0, enc.encode('x')) !== false) return false
484
+ if (b.acceptNegotiatorNegotiate(0, enc.encode('gzip')) !== null) return false
485
+ if (b.acceptNegotiatorNegotiateServer(0, enc.encode('gzip')) !== null) return false
486
+ if (b.schemaValidatorValidate(0, enc.encode('{}')) !== false) return false
487
+ if (b.jwtSignerVerify(0, enc.encode('a.b.c'), 0) !== null) return false
488
+ let phase6Threw = false
489
+ try {
490
+ b.jwtSignerSign(0, enc.encode('{}'), 0)
491
+ } catch {
492
+ phase6Threw = true
493
+ }
494
+ if (!phase6Threw) return false
495
+ phase6Threw = false
496
+ try {
497
+ b.templateRender(0, enc.encode('{}'))
498
+ } catch {
499
+ phase6Threw = true
500
+ }
501
+ if (!phase6Threw) return false
502
+ phase6Threw = false
503
+ try {
504
+ b.rateLimiterCheck(0, 'k', 0)
505
+ } catch {
506
+ phase6Threw = true
507
+ }
508
+ if (!phase6Threw) return false
509
+
510
+ // JWT sign pooled Into: same token as the cstring path.
511
+ const jwtClaims = enc.encode('{"sub":"user-1"}')
512
+ const jwtSecret2 = enc.encode('my-secret')
513
+ const jwtOut = new Uint8Array(512)
514
+ const jwtW = b.jwtSignBytesInto(jwtClaims, jwtSecret2, 0, 0, jwtOut)
515
+ if (jwtW === 0 || dec.decode(jwtOut.subarray(0, jwtW)).split('.').length !== 3) {
516
+ return false
517
+ }
518
+
519
+ // JWT sign with ttl=0 (deterministic — no iat/exp), then verify the
520
+ // signature with the FFI HMAC to prove the binding is real.
521
+ const jwtSecret = enc.encode('my-secret')
522
+ const jwt = b.jwtSignBytes(enc.encode('{"sub":"user-1"}'), jwtSecret, 0, 0)
523
+ const jwtStr = jwt
524
+ const segs = jwtStr.split('.')
525
+ if (segs.length !== 3 || segs[0] === '' || segs[1] === '' || segs[2] === '') {
526
+ return false
527
+ }
528
+ const signingInput = enc.encode(`${segs[0]}.${segs[1]}`)
529
+ const sigHex = b.hmacSha256(jwtSecret, signingInput)
530
+ const sigBytes = b.base64Decode(enc.encode(segs[2] ?? ''), true, false)
531
+ if (sigBytes === null || b.hexEncode(sigBytes) !== sigHex) {
532
+ return false
533
+ }
534
+
535
+ // jwtVerify: sign (ttl=0 → no iat/exp) then verify → claims JSON; tampered → null.
536
+ const vjwtSecret = enc.encode('verify-secret')
537
+ const vjwt = b.jwtSignBytes(enc.encode('{"sub":"u-1"}'), vjwtSecret, 0, 0)
538
+ const vjwtClaims = b.jwtVerify(enc.encode(vjwt), vjwtSecret, 0)
539
+ if (vjwtClaims === null || !vjwtClaims.includes('"sub":"u-1"')) {
540
+ return false
541
+ }
542
+ if (b.jwtVerify(enc.encode('tampered.token.value'), vjwtSecret, 0) !== null) {
543
+ return false
544
+ }
545
+
546
+ // Ed25519 round-trip: generate → sign → verify (and a tampered signature
547
+ // must be rejected). The generate wrapper also parses the packed blob, so
548
+ // this pins the `[u32 privLen][priv][u32 pubLen][pub]` layout.
549
+ const edKp = b.ed25519GenerateKeypair()
550
+ const edMsg = enc.encode('ed25519 bind-time self-test')
551
+ const edSig = b.ed25519Sign(edMsg, edKp.privateKey)
552
+ if (edSig.byteLength !== 64 || !b.ed25519Verify(edMsg, edSig, edKp.publicKey)) {
553
+ return false
554
+ }
555
+ const edTampered = edSig.slice()
556
+ edTampered[0] = (edTampered[0] ?? 0) ^ 0xff
557
+ if (b.ed25519Verify(edMsg, edTampered, edKp.publicKey)) {
558
+ return false
559
+ }
560
+ // EdDSA JWT round-trip on the SAME keypair (ttl=0 → no iat/exp, so verify
561
+ // succeeds at any `now`).
562
+ const edToken = b.jwtEdDSASign(enc.encode('{"sub":"ffi-self-test"}'), edKp.privateKey, 0, 0)
563
+ if (edToken === null || edToken.split('.').length !== 3) {
564
+ return false
565
+ }
566
+ const edClaims = b.jwtEdDSAVerify(enc.encode(edToken), edKp.publicKey, 0)
567
+ if (edClaims === null || !edClaims.includes('"sub":"ffi-self-test"')) {
568
+ return false
569
+ }
570
+ if (b.jwtEdDSAVerify(enc.encode('tampered.token.value'), edKp.publicKey, 0) !== null) {
571
+ return false
572
+ }
573
+
574
+ // UrlBuilder opaque-handle: a null (0) handle must throw (the C side never
575
+ // dereferences freed state) — exercises the symbol's ABI. Real resolve
576
+ // parity is covered by test/unit/features/url-join.test.ts.
577
+ let urlBuilderThrew = false
578
+ try {
579
+ b.urlBuilderResolve(0, enc.encode('g'))
580
+ } catch {
581
+ urlBuilderThrew = true
582
+ }
583
+ if (!urlBuilderThrew) {
584
+ return false
585
+ }
586
+ // rateLimiterCheckKey: null (0) handle → throw (ABI exercise).
587
+ let rlKeyThrew = false
588
+ try {
589
+ b.rateLimiterCheckKey(0, 12345, 0)
590
+ } catch {
591
+ rlKeyThrew = true
592
+ }
593
+ if (!rlKeyThrew) {
594
+ return false
595
+ }
596
+
597
+ return true
598
+ }