castrum 0.9.0 → 0.9.2

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 (120) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1653 -1817
  4. package/dist/index.js +6172 -4294
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +111 -17
  7. package/docs/BENCHMARKS.md +117 -49
  8. package/docs/CASE_STUDY.md +243 -0
  9. package/docs/ENVIRONMENT.md +24 -7
  10. package/docs/FFI_BUN_GUIDE.md +335 -0
  11. package/docs/GETTING_STARTED.md +18 -34
  12. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  13. package/docs/INGRESS-ROUTER.md +82 -0
  14. package/docs/INGRESS.md +11 -7
  15. package/docs/NATIVE-ROUTE.md +77 -0
  16. package/docs/REPO_MAP.md +103 -53
  17. package/docs/adr/0001-two-wire-formats.md +1 -1
  18. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  19. package/docs/ai/LOCAL_DEV.md +81 -0
  20. package/docs/ai/TREE.md +1070 -0
  21. package/docs/bun-builtins-decision-matrix.md +25 -18
  22. package/index.ts +9 -16
  23. package/package.json +41 -28
  24. package/src/ingress/body.ts +167 -27
  25. package/src/ingress/decode/result-base.ts +10 -2
  26. package/src/ingress/handlers.ts +116 -285
  27. package/src/ingress/headers/security.ts +41 -0
  28. package/src/ingress/health.ts +1 -0
  29. package/src/ingress/index.ts +45 -169
  30. package/src/ingress/native-route.ts +182 -0
  31. package/src/ingress/packing/gather-raw-headers.ts +110 -22
  32. package/src/ingress/packing/header-packing.ts +5 -5
  33. package/src/ingress/packing/input-packer.ts +17 -3
  34. package/src/ingress/packing/route-wire.ts +197 -0
  35. package/src/ingress/packing/select-headers.ts +20 -7
  36. package/src/ingress/path-matcher.ts +115 -0
  37. package/src/ingress/response/baked-response.ts +302 -0
  38. package/src/ingress/router.ts +277 -0
  39. package/src/ingress/routes/common.ts +50 -1
  40. package/src/ingress/routes/fallback.ts +2 -2
  41. package/src/ingress/routes/head.ts +3 -13
  42. package/src/ingress/routes/index.ts +5 -4
  43. package/src/ingress/routes/json-write.ts +26 -47
  44. package/src/ingress/routes/native.ts +130 -0
  45. package/src/ingress/routes/options.ts +2 -12
  46. package/src/ingress/routes/read.ts +3 -13
  47. package/src/ingress/routes/responder.ts +159 -0
  48. package/src/ingress/server-node.ts +7 -7
  49. package/src/ingress/server.ts +83 -112
  50. package/src/ingress/sync.ts +151 -0
  51. package/src/ingress/types.ts +44 -1
  52. package/src/integration/pipeline.ts +3 -0
  53. package/src/integration/streaming.ts +2 -3
  54. package/src/integration/websocket.ts +4 -3
  55. package/src/loader/batch.ts +4 -0
  56. package/src/loader/cost.ts +1 -0
  57. package/src/loader/index.ts +43 -228
  58. package/src/loader/state.ts +84 -0
  59. package/src/loader/types.ts +145 -0
  60. package/src/native/ffi/build/codecs.ts +736 -0
  61. package/src/native/ffi/build/compress.ts +248 -0
  62. package/src/native/ffi/build/instances.ts +598 -0
  63. package/src/native/ffi/build/parse.ts +539 -0
  64. package/src/native/ffi/build/util.ts +140 -0
  65. package/src/native/ffi/build.ts +94 -0
  66. package/src/native/ffi/constants.ts +58 -0
  67. package/src/native/ffi/selftest.ts +22 -0
  68. package/src/native/ffi/types.ts +499 -0
  69. package/src/native/ffi.ts +307 -1357
  70. package/src/native/loader.ts +52 -6
  71. package/src/native/types/instances.ts +279 -0
  72. package/src/native/types.ts +41 -238
  73. package/src/runtime/builtins.ts +131 -0
  74. package/src/runtime/codec.ts +147 -0
  75. package/src/runtime/detect.ts +36 -0
  76. package/src/runtime/index.ts +28 -0
  77. package/src/runtime/native.ts +35 -0
  78. package/src/runtime/server.ts +31 -0
  79. package/src/runtime/transport.ts +64 -0
  80. package/src/runtime/types.ts +173 -0
  81. package/src/runtime/uuid.ts +45 -0
  82. package/src/runtime/websocket.ts +30 -0
  83. package/src/rust-ffi/addon.ts +1 -1
  84. package/src/rust-ffi/batch/build.ts +155 -0
  85. package/src/rust-ffi/batch/index.ts +8 -0
  86. package/src/rust-ffi/batch/types.ts +92 -0
  87. package/src/rust-ffi/client.ts +5 -6
  88. package/src/rust-ffi/context.ts +39 -10
  89. package/src/rust-ffi/index.ts +28 -17
  90. package/src/rust-ffi/into.ts +21 -0
  91. package/src/rust-ffi/options.ts +1 -1
  92. package/src/rust-ffi/packed.ts +40 -2
  93. package/src/rust-ffi/proven.ts +16 -30
  94. package/src/rust-ffi/scalar/crypto.ts +188 -107
  95. package/src/rust-ffi/scalar/factories.ts +257 -27
  96. package/src/rust-ffi/scalar/hashing.ts +43 -41
  97. package/src/rust-ffi/scalar/http.ts +208 -77
  98. package/src/rust-ffi/scalar/index.ts +3 -3
  99. package/src/rust-ffi/scalar/interface.ts +88 -213
  100. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  101. package/src/rust-ffi/scalar/json.ts +28 -12
  102. package/src/rust-ffi/scalar/payload.ts +159 -42
  103. package/src/rust-ffi/text.ts +31 -5
  104. package/src/selection.ts +27 -31
  105. package/src/shared/adaptive.ts +1 -2
  106. package/src/shared/buffer-pool.ts +72 -17
  107. package/src/shared/bytes.ts +75 -6
  108. package/src/shared/codec.ts +78 -0
  109. package/src/shared/env.ts +4 -4
  110. package/src/shared/log.ts +1 -0
  111. package/src/shared/packed/index.ts +10 -0
  112. package/src/shared/packed/parsers.ts +31 -0
  113. package/src/shared/packed/schema.ts +23 -0
  114. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  115. package/src/shared/proven.ts +189 -496
  116. package/src/shared/runtime.ts +20 -25
  117. package/src/shared/uuid.ts +12 -28
  118. package/src/data/json-rows.ts +0 -33
  119. package/src/rust-ffi/batch.ts +0 -431
  120. package/src/shared/bench-classify.ts +0 -209
@@ -0,0 +1,94 @@
1
+ // src/native/ffi/build.ts — the `build()` orchestrator for the BunFFI surface.
2
+ //
3
+ // `build(sym, useBufferLength)` sets up the shared per-bind state — the
4
+ // probe-gated `lenOrView`/`oneArg` argument adapters and the pooled scratch
5
+ // buffers — then delegates to the four domain builders (each receives the raw
6
+ // dlopen'd symbols + this context) and merges their method objects into the
7
+ // final `BunFFI`. Extracted from the former monolithic `ffi.ts` so the
8
+ // per-call wrapper surface is navigable by domain:
9
+ // - `build/codecs.ts` hashing / codec / crypto / auth / ws-key / etag
10
+ // - `build/compress.ts` gzip / brotli / json patch
11
+ // - `build/parse.ts` packed parsers / media-type / http-date / sse
12
+ // - `build/instances.ts` opaque-handle evals + ingress + route stack
13
+
14
+ import { buildCodecs } from './build/codecs'
15
+ import { buildCompress } from './build/compress'
16
+ import { buildInstances } from './build/instances'
17
+ import { buildParse } from './build/parse'
18
+ import type { BuildCtx } from './build/util'
19
+ import type { BunFFI } from './types'
20
+
21
+ /**
22
+ * Assemble the `BunFFI` surface from the raw dlopen'd symbols.
23
+ *
24
+ * @param sym The `symbols` object from `dlopen(path, {...})`.
25
+ * @param useBufferLength `true` when the engine's `buffer`/`buffer_length`
26
+ * ABI pair is live (see the probe in `ffi.ts`).
27
+ */
28
+ export function build(
29
+ sym: Record<string, (...a: unknown[]) => unknown>,
30
+ useBufferLength: boolean,
31
+ ): BunFFI {
32
+ // Single-buffer input fns: with the probe-gated `buffer/buffer_length` ABI the
33
+ // engine reads ptr + byteLength off the SAME TypedArray at call time (an
34
+ // atomic snapshot — pass the view twice, per Bun's docs); with `(ptr,len)` we
35
+ // pass the length explicitly. The branch is on a bind-time constant, so the
36
+ // JIT folds it away at the call site.
37
+ const oneArg = (raw: (...a: unknown[]) => number | bigint, v: Uint8Array): number | bigint =>
38
+ useBufferLength ? raw(v, v) : raw(v, v.length)
39
+ // Phase 1.1: under `buffer`/`buffer_length` the engine reads ptr + byteLength
40
+ // off the SAME view, so every `(ptr,len)` pair passes the view itself for the
41
+ // length slot (atomic snapshot, one JS arg fewer); under `(ptr,usize)` it's
42
+ // the explicit length. The branch is a bind-time constant → JIT folds it.
43
+ const lenOrView = (v: Uint8Array): Uint8Array | number => (useBufferLength ? v : v.length)
44
+
45
+ // ── Reusable scratch for the STRING-returning encode wrappers ────────
46
+ // Each wrapper writes its result and decodes it into an immutable JS string
47
+ // (or primitive) SYNCHRONOUSLY before the next call reuses the buffer — the
48
+ // same safety rationale as `jsonSumOut` / `generateRequestId`. Per-Worker
49
+ // module state (single-threaded event loop); never shared across Workers.
50
+ // NOT used for byte-returning ops (hexDecode/urlDecode/base64Decode/aead/
51
+ // wsFrame/packed parsers): their returned `Uint8Array` aliases the buffer
52
+ // and escapes to the caller — those stay on `*Into` (caller-owned).
53
+ let encodeScratch = new Uint8Array(0)
54
+ const scratchFor = (size: number): Uint8Array => {
55
+ if (encodeScratch.byteLength < size) encodeScratch = new Uint8Array(size)
56
+ return encodeScratch
57
+ }
58
+ // Pooled 9-byte scratch for jsonSumIds (the packed `[u8 ok][i64 sum LE]`
59
+ // output). The wrapper returns the BigInt VALUE (no byte aliasing escapes),
60
+ // so a single reused buffer + cached DataView is safe — removes the per-call
61
+ // Uint8Array + DataView allocs (measured ~400ns of the FFI wrapper cost).
62
+ const jsonSumOut = new Uint8Array(9)
63
+ const jsonSumView = new DataView(jsonSumOut.buffer)
64
+ // Fixed-size packed-verdict scratch + cached DataViews (parseHttpDate 9 B,
65
+ // rate-limit check 13 B) — same pooled pattern as `jsonSumView`. Kept
66
+ // separate from the growable `encodeScratch` so the cached DataViews are
67
+ // never invalidated by a buffer replacement.
68
+ const dateScratch = new Uint8Array(9)
69
+ const dateScratchView = new DataView(dateScratch.buffer)
70
+ const rateScratch = new Uint8Array(13)
71
+ const rateScratchView = new DataView(rateScratch.buffer)
72
+
73
+ const ctx: BuildCtx = {
74
+ useBufferLength,
75
+ lenOrView,
76
+ oneArg,
77
+ scratchFor,
78
+ jsonSumOut,
79
+ jsonSumView,
80
+ dateScratch,
81
+ dateScratchView,
82
+ rateScratch,
83
+ rateScratchView,
84
+ }
85
+
86
+ // Merge the domain surfaces. Each builder's method bodies are verbatim from
87
+ // the original single return object; the cast asserts the merged shape.
88
+ return {
89
+ ...buildCodecs(sym, ctx),
90
+ ...buildCompress(sym, ctx),
91
+ ...buildParse(sym, ctx),
92
+ ...buildInstances(sym, ctx),
93
+ } as BunFFI
94
+ }
@@ -0,0 +1,58 @@
1
+ // src/native/ffi/constants.ts — caps / sizing heuristics / self-test
2
+ // vectors for the bun:ffi transport (pure data, no logic).
3
+ //
4
+ // The caps mirror the Rust napi layer (decompression-bomb guard, JSON-patch
5
+ // output bound). The sizing heuristics are ALLOCATION caps only — `growExact`
6
+ // in the core covers any residual miss with one exact-size retry, so a guess
7
+ // that is too small costs one extra native pass, never a wrong result.
8
+
9
+ // ── Caps mirrored from the Rust napi layer ────────────────────────
10
+ /** rust/payload/compress.rs DEFAULT_MAX_DECOMPRESSED (64 MiB decompression-bomb
11
+ * guard — must stay in sync; do not raise without benchmarking). */
12
+ export const MAX_DECOMPRESSED = 64 * 1024 * 1024
13
+ /** rust/json/json_patch_ops.rs MAX_JSON_PATCH_OUTPUT (128 MiB). */
14
+ export const MAX_JSON_PATCH_OUTPUT = 128 * 1024 * 1024
15
+
16
+ // ── Output-buffer sizing heuristics (measured) ─────────────────────
17
+ // These are ALLOCATION caps only, not correctness bounds: `growExact` covers
18
+ // any residual miss with at most one exact-size retry (no re-run loop), so a
19
+ // guess that is too small costs one extra native pass, never a wrong result.
20
+ //
21
+ // Compress: a 75 KiB input compresses to <1 KiB, so a 16 KiB initial is
22
+ // plenty and single-pass; the 1 MiB ceiling bounds incompressible data.
23
+ /** gzip/brotli compress: initial output-buffer size (bytes). */
24
+ export const COMPRESS_INITIAL_CAP = 16 * 1024
25
+ /** gzip/brotli compress: ceiling that bounds incompressible input (bytes). */
26
+ export const COMPRESS_MAX_CAP = 1024 * 1024
27
+ /** gzip/brotli compress: small-input slack added above `data.length` (bytes). */
28
+ export const COMPRESS_HEADROOM = 64
29
+ // Decompress (no trailer / expensive trailer): typical JSON/text ratios.
30
+ /** gzip decompress: output-size guess multiplier over the input length. */
31
+ export const DECOMPRESS_GUESS_MULTIPLIER_GZIP = 16
32
+ /** brotli decompress: output-size guess multiplier over the input length. */
33
+ export const DECOMPRESS_GUESS_MULTIPLIER_BROTLI = 32
34
+ /** decompress: 4 MiB over-alloc bound used as a last-resort initial size. */
35
+ export const DECOMPRESS_FALLBACK_CAP = 4 * 1024 * 1024
36
+ /** decompress: minimum initial output size (bytes). */
37
+ export const DECOMPRESS_MIN_INITIAL = 1024
38
+ // JWT: token ≈ header(~36) + payload(≈4/3× claims) + sig(~43) + 2 dots. The
39
+ // old `claims.length + 128` under-sized a typical token (measured 168 B for a
40
+ // 30 B claim → grow-retry double-run, making ffi slower than napi). 2×+128
41
+ // with a 256-byte floor covers typical claims in one pass.
42
+ /** jwt sign: claims-length multiplier for the initial output-size guess. */
43
+ export const JWT_INITIAL_MULTIPLIER = 2
44
+ /** jwt sign: fixed byte slack added to the claims-length guess. */
45
+ export const JWT_INITIAL_EXTRA = 128
46
+ /** jwt sign: minimum initial output size (bytes). */
47
+ export const JWT_INITIAL_FLOOR = 256
48
+
49
+ // ── Known-good vectors for the bind-time self-test ───────────────
50
+ // These mirror the Rust `#[cfg(test)]` vectors in rust/ffi.rs.
51
+ /** Self-test vector: ASCII "hello" (hex-encodes to `68656c6c6f`). */
52
+ export const SELFTEST_HEX = new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f]) // "hello" -> 68656c6c6f
53
+ /** Self-test vector: JSON `{"a":1}` (validates via the packed JSON ops). */
54
+ export const SELFTEST_JSON = new Uint8Array([0x7b, 0x22, 0x61, 0x22, 0x3a, 0x31, 0x7d]) // "{\"a\":1}"
55
+
56
+ /** Empty view for `null` body slots in (ptr, len) pairs — the C side treats
57
+ * len 0 as "no body" regardless of the pointer value. */
58
+ export const EMPTY_VIEW = new Uint8Array(0)
@@ -0,0 +1,22 @@
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 the self-test for their
8
+ // domain: each check lives in the same `ffi/build/` file where the method is
9
+ // bound (`selfTestCodecs` / `selfTestCompress` / `selfTestParse` /
10
+ // `selfTestInstances`), and this entry point ANDs them together so any single
11
+ // failure disables the whole layer.
12
+
13
+ import { selfTestCodecs } from './build/codecs'
14
+ import { selfTestCompress } from './build/compress'
15
+ import { selfTestInstances } from './build/instances'
16
+ import { selfTestParse } from './build/parse'
17
+ import type { BunFFI } from './types'
18
+
19
+ /** Verify every bound function against known-good results; false disables ffi. */
20
+ export function selfTest(b: BunFFI): boolean {
21
+ return selfTestCodecs(b) && selfTestCompress(b) && selfTestParse(b) && selfTestInstances(b)
22
+ }
@@ -0,0 +1,499 @@
1
+ // src/native/ffi/types.ts — bun:ffi C-ABI type surface (pure types).
2
+ //
3
+ // The bound-function contract (`BunFFI`), the transport selection
4
+ // (`FfiMode`), and the raw `(ptr,len)` symbol signatures (`Raw2..Raw10`)
5
+ // used by the transport core. No runtime code — erased at compile time.
6
+
7
+ /**
8
+ * The bound bun:ffi C-ABI surface. Each member is a dlopen'd `castrum_*`
9
+ * symbol wrapped so inputs are `(ptr,len)` byte slices and text outputs are
10
+ * returned as JS strings via JSC's native transfer (cstring clone) — zero
11
+ * `TextEncoder`/`TextDecoder` on the Bun path. `*Into` variants write into a
12
+ * caller buffer and return the byte count; variable-size ops return the exact
13
+ * required size on a too-small buffer so the transport grows once and retries
14
+ * exactly once.
15
+ */
16
+ export interface BunFFI {
17
+ crc32(input: Uint8Array): number
18
+ fnv1a64(input: Uint8Array): bigint
19
+ xxh3(input: Uint8Array): bigint
20
+ jsonValid(input: Uint8Array): boolean
21
+ /** UTF-8 validity probe (native replacement for a fatal `TextDecoder`). */
22
+ utf8Valid(input: Uint8Array): boolean
23
+ /** Lowercase-hex encode → string (native transfer, zero encode). */
24
+ hexEncode(input: Uint8Array): string
25
+ /** Lowercase-hex encode into `output`; returns bytes written. */
26
+ hexEncodeInto(input: Uint8Array, output: Uint8Array): number
27
+ /** RFC 3986 percent-encode → string. */
28
+ urlEncode(input: Uint8Array): string
29
+ /** RFC 3986 percent-encode into `output`; returns bytes written. */
30
+ urlEncodeInto(input: Uint8Array, output: Uint8Array): number
31
+
32
+ // ── Validators (u8 → boolean) — `cstring` ARG ────────────────────
33
+ // The input crosses as a `cstring` ARG: the engine transcodes the JS string
34
+ // to a call-scoped NUL-terminated UTF-8 buffer in-engine, so the JS side does
35
+ // ZERO `encoder.encode` work and Rust borrows via `CStr::from_ptr`.
36
+ validateEmail(input: string): boolean
37
+ validateUuid(input: string): boolean
38
+ validateIpv4(input: string): boolean
39
+ validateIpv6(input: string): boolean
40
+ /** Sum of `id` fields across a JSON array → bigint (throws on non-array input). */
41
+ jsonSumIds(input: Uint8Array): bigint
42
+
43
+ // ── Constant-time verify (u8 → boolean) ───────────────────────────
44
+ hmacSha256Verify(key: Uint8Array, data: Uint8Array, signature: Uint8Array): boolean
45
+ csrfVerify(token: Uint8Array, secret: Uint8Array): boolean
46
+ passwordVerify(password: Uint8Array, phc: Uint8Array): boolean
47
+ /** `phc` is a `cstring` ARG (the engine transcodes the JS PHC string in-engine). */
48
+ passwordVerifyBcrypt(password: Uint8Array, phc: string): boolean
49
+
50
+ // ── Decoders (throw on malformed input — napi `Result` parity) ──
51
+ /** Hex-decode into a fresh buffer (size `input.length / 2`). */
52
+ hexDecode(input: Uint8Array): Uint8Array
53
+ /** Hex-decode into `output`; returns bytes written (throws if too small). */
54
+ hexDecodeInto(input: Uint8Array, output: Uint8Array): number
55
+ /** Percent-decode into a fresh buffer (size `input.length`). */
56
+ urlDecode(input: Uint8Array): Uint8Array
57
+ /** Percent-decode into `output`; returns bytes written (throws if too small). */
58
+ urlDecodeInto(input: Uint8Array, output: Uint8Array): number
59
+ /** base64-decode into a fresh buffer (size `ceil(len/4)*3`). */
60
+ base64Decode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array
61
+ /** base64-decode into `output`; returns bytes written (throws if too small). */
62
+ base64DecodeInto(
63
+ input: Uint8Array,
64
+ output: Uint8Array,
65
+ urlSafe?: boolean,
66
+ padding?: boolean,
67
+ ): number
68
+
69
+ // ── Fixed-size output writers ─────────────────────────────────────
70
+ /** RFC 6455 Sec-WebSocket-Accept → string (cstring return; `key` is a `cstring` ARG). */
71
+ wsAcceptKey(key: string): string
72
+ /** RFC 6455 Sec-WebSocket-Accept into `output`; returns bytes written. */
73
+ wsAcceptKeyInto(key: Uint8Array, output: Uint8Array): number
74
+ /** crc32 ETag → string (10 strong / 12 weak chars, cstring return). */
75
+ etag(data: Uint8Array, weak?: boolean): string
76
+ /** crc32 ETag into `output`; returns bytes written. */
77
+ etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number
78
+ /** Evaluate a precompiled `ConditionalRequest` (opaque `inner` handle from its
79
+ * napi `inner_ptr()`) — true → 304. `null` header args = absent (flags encode
80
+ * presence so a present-but-empty header is distinct). A null (0) handle → false. */
81
+ conditionalIsNotModified(
82
+ inner: number,
83
+ ifNoneMatch: Uint8Array | null,
84
+ ifModifiedSince: Uint8Array | null,
85
+ ): boolean
86
+ /** Precompiled `MediaTypeMatcher` wildcard match (opaque `inner` handle). */
87
+ mediaTypeMatcherMatches(inner: number, actual: Uint8Array): boolean
88
+ /** Precompiled `AcceptNegotiator` → best supported encoding (opaque `inner`
89
+ * handle); `null` = identity. */
90
+ acceptNegotiatorNegotiate(inner: number, header: Uint8Array): string | null
91
+ /** Precompiled `AcceptNegotiator` → best supported encoding with SERVER-
92
+ * preference tie-breaking (RFC 7231 server semantics, opaque `inner` handle);
93
+ * `null` = identity. */
94
+ acceptNegotiatorNegotiateServer(inner: number, header: Uint8Array): string | null
95
+ /** Precompiled `JwtSigner` → compact token from pre-serialized claim JSON
96
+ * (opaque `inner` handle); throws on invalid claims / null handle. */
97
+ jwtSignerSign(inner: number, claimsJson: Uint8Array, nowSeconds: number): Uint8Array
98
+ /** Precompiled `JwtSigner` → claims JSON bytes (opaque `inner` handle);
99
+ * `null` = invalid signature / expired / malformed. */
100
+ jwtSignerVerify(inner: number, token: Uint8Array, nowSeconds: number): Uint8Array | null
101
+ /** Ed25519 keypair generation → `{ privateKey, publicKey }` (PKCS#8 v1 DER
102
+ * private + SPKI DER public bytes); throws on CSPRNG failure. */
103
+ ed25519GenerateKeypair(): { privateKey: Uint8Array; publicKey: Uint8Array }
104
+ /** Ed25519 sign → 64-byte signature; throws on invalid private key. */
105
+ ed25519Sign(msg: Uint8Array, privateKey: Uint8Array): Uint8Array
106
+ /** Ed25519 verify → whether `signature` is valid for `msg` under `publicKey`. */
107
+ ed25519Verify(msg: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean
108
+ /** EdDSA (Ed25519) JWT sign → compact token; `null` = invalid claims/private
109
+ * key. `ttl <= 0` = no `iat`/`exp` injection. */
110
+ jwtEdDSASign(
111
+ claimsJson: Uint8Array,
112
+ privateKey: Uint8Array,
113
+ ttl: number,
114
+ nowSeconds: number,
115
+ ): string | null
116
+ /** EdDSA (Ed25519) JWT verify → claims JSON string; `null` = invalid. */
117
+ jwtEdDSAVerify(
118
+ token: Uint8Array,
119
+ publicKey: Uint8Array,
120
+ nowSeconds: number,
121
+ ): string | null
122
+ /** Compiled `TemplateRenderer` → UTF-8 bytes from a pre-serialized JSON
123
+ * context (opaque `inner` handle); throws on invalid context / render error. */
124
+ templateRender(inner: number, contextJson: Uint8Array): Uint8Array
125
+ /** Compiled `SchemaValidator` → whether a document is valid (opaque `inner` handle). */
126
+ schemaValidatorValidate(inner: number, doc: Uint8Array): boolean
127
+ /** `RateLimiter` string-key check → { allowed, remaining, resetMs } (opaque
128
+ * `inner` handle; `key` is a `cstring` ARG); throws on a null handle. */
129
+ rateLimiterCheck(inner: number, key: string, nowMs: number): RateLimiterVerdict
130
+ /** `RateLimiter` pre-hashed key check → { allowed, remaining, resetMs }. */
131
+ rateLimiterCheckKey(inner: number, key: number, nowMs: number): RateLimiterVerdict
132
+ /** `byteLen` random bytes → `byteLen * 2` hex-char string (cstring return). */
133
+ randomToken(byteLen: number): string
134
+ /** base64-encode → string. */
135
+ base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): string
136
+ /** base64-encode into `output`; returns bytes written. */
137
+ base64EncodeInto(
138
+ input: Uint8Array,
139
+ output: Uint8Array,
140
+ urlSafe?: boolean,
141
+ padding?: boolean,
142
+ ): number
143
+ /** HMAC-SHA256 → 64-char lowercase-hex string. */
144
+ hmacSha256(key: Uint8Array, data: Uint8Array): string
145
+ /** HMAC-SHA256 hex into `output`; returns bytes written (throws if too small). */
146
+ hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number
147
+ /** Sign cookie `value` as `value.<64-hex>` → string (cstring return). */
148
+ signCookie(value: Uint8Array, secret: Uint8Array): string
149
+ /** Sign cookie into `output`; returns bytes written (throws if too small). */
150
+ signCookieInto(value: Uint8Array, secret: Uint8Array, output: Uint8Array): number
151
+ /** Verify a signed cookie → value string, or `null` on bad signature. */
152
+ verifyCookie(signed: Uint8Array, secret: Uint8Array): string | null
153
+ /** Verify a signed cookie into `output`; returns value length or `null` on bad signature. */
154
+ verifyCookieInto(signed: Uint8Array, secret: Uint8Array, output: Uint8Array): number | null
155
+ /** CSRF token (129 chars: 64 rnd-hex + '.' + 64 sig-hex) → string. */
156
+ csrfToken(secret: Uint8Array): string
157
+ /** CSRF token into `output`; returns bytes written (throws if too small). */
158
+ csrfTokenInto(secret: Uint8Array, output: Uint8Array): number
159
+ /** Argon2id PHC hash → string (m/t/p/out_len params). */
160
+ passwordHash(
161
+ password: Uint8Array,
162
+ salt: Uint8Array,
163
+ mCost: number,
164
+ tCost: number,
165
+ pCost: number,
166
+ outLen: number,
167
+ ): string
168
+ /** bcrypt `$2b$` PHC → string (cost clamped 4..=31). */
169
+ passwordHashBcrypt(password: Uint8Array, cost: number): string
170
+ /** PBKDF2-HMAC-SHA256 → `dkLen` bytes (rounds, dkLen clamped). */
171
+ pbkdf2Sha256(password: Uint8Array, salt: Uint8Array, rounds: number, dkLen: number): Uint8Array
172
+ /** AEAD encrypt (0 = AES-256-GCM, 1 = ChaCha20-Poly1305) → ct+tag. */
173
+ aeadEncrypt(
174
+ key: Uint8Array,
175
+ nonce: Uint8Array,
176
+ plaintext: Uint8Array,
177
+ algorithm?: number,
178
+ ): Uint8Array
179
+ /** AEAD encrypt into `output` (ct + 16-byte tag); returns bytes written. */
180
+ aeadEncryptInto(
181
+ key: Uint8Array,
182
+ nonce: Uint8Array,
183
+ plaintext: Uint8Array,
184
+ output: Uint8Array,
185
+ algorithm?: number,
186
+ ): number
187
+ /** AEAD decrypt → plaintext, or `null` on auth failure. */
188
+ aeadDecrypt(
189
+ key: Uint8Array,
190
+ nonce: Uint8Array,
191
+ ciphertext: Uint8Array,
192
+ algorithm?: number,
193
+ ): Uint8Array | null
194
+
195
+ // ── Frame / patch / compression (variable-size → grow-retry) ─────
196
+ /** RFC 6455 frame encode into a fresh buffer. */
197
+ wsFrameEncode(opcode: number, payload: Uint8Array, mask: boolean, fin: boolean): Uint8Array
198
+ /** RFC 6455 frame encode into `output`; returns bytes written (throws if too small). */
199
+ wsFrameEncodeInto(
200
+ opcode: number,
201
+ payload: Uint8Array,
202
+ mask: boolean,
203
+ fin: boolean,
204
+ output: Uint8Array,
205
+ ): number
206
+ /** RFC 6902 JSON patch → JSON text string. */
207
+ jsonPatch(doc: Uint8Array, patch: Uint8Array): string
208
+ /** gzip-compress into a fresh buffer (level clamped 0..=9, default 6). */
209
+ gzipCompress(data: Uint8Array, level?: number): Uint8Array
210
+ /** gzip-compress into `output`; returns bytes written (throws if too small). */
211
+ gzipCompressInto(data: Uint8Array, output: Uint8Array, level?: number): number
212
+ /** gzip-decompress into a fresh buffer (capped by `maxDecompressed`). */
213
+ gzipDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array
214
+ /** gzip-decompress into `output`; returns bytes written (throws if too small). */
215
+ gzipDecompressInto(data: Uint8Array, output: Uint8Array, maxDecompressed?: number): number
216
+ /** brotli-compress into a fresh buffer (quality clamped 0..=11, default 5). */
217
+ brotliCompress(data: Uint8Array, quality?: number): Uint8Array
218
+ /** brotli-compress into `output`; returns bytes written (throws if too small). */
219
+ brotliCompressInto(data: Uint8Array, output: Uint8Array, quality?: number): number
220
+ /** brotli-decompress into a fresh buffer (capped by `maxDecompressed`). */
221
+ brotliDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array
222
+ /** brotli-decompress into `output`; returns bytes written (throws if too small). */
223
+ brotliDecompressInto(data: Uint8Array, output: Uint8Array, maxDecompressed?: number): number
224
+
225
+ // ── Packed parsers (into caller buffers) ─────────────────────────
226
+ /** HTTP request parse → packed output into `output`; returns bytes written. */
227
+ httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number
228
+ /** Query string parse → packed output into `output`; returns bytes written. */
229
+ queryParsePackedInto(input: Uint8Array, output: Uint8Array): number
230
+ /** Cookie header parse → packed output into `output`; returns bytes written. */
231
+ cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number
232
+
233
+ // ── Excluded-surface additions (packed / opaque-handle) ─────────
234
+ /** Sign JWT (HS256) from pre-serialized claim JSON (ttl<=0 → no iat/exp) → string. */
235
+ jwtSignBytes(claimsJson: Uint8Array, secret: Uint8Array, ttl: number, now: number): string
236
+ /** Sign JWT into `output`; returns bytes written (throws if too small). */
237
+ jwtSignBytesInto(
238
+ claimsJson: Uint8Array,
239
+ secret: Uint8Array,
240
+ ttl: number,
241
+ now: number,
242
+ output: Uint8Array,
243
+ ): number
244
+ /** Decode a WS frame into packed `[flags][opcode][u32 len][payload]`; null on malformed. */
245
+ wsFrameDecodePacked(data: Uint8Array): Uint8Array | null
246
+ /**
247
+ * Decode a WS frame into `output` (sized ≥ `data.length + 6`); returns bytes
248
+ * written, null on malformed. Pooled sibling of `wsFrameDecodePacked`.
249
+ */
250
+ wsFrameDecodePackedInto(data: Uint8Array, output: Uint8Array): number | null
251
+ /** Parse multipart/form-data into the packed parts layout. */
252
+ multipartParsePacked(body: Uint8Array, boundary: Uint8Array): Uint8Array
253
+ /** Parse multipart/form-data into `output`; returns bytes written (throws if too small). */
254
+ multipartParsePackedInto(body: Uint8Array, boundary: Uint8Array, output: Uint8Array): number
255
+ /** Parse x-www-form-urlencoded into packed pairs into `output`. */
256
+ formParsePackedInto(input: Uint8Array, output: Uint8Array): number
257
+ /**
258
+ * Verify an HS256 JWT → claims JSON text (cstring return; `null` on invalid
259
+ * signature / expired / malformed). `nowSeconds` is an i64 C arg.
260
+ */
261
+ jwtVerify(token: Uint8Array, secret: Uint8Array, nowSeconds: number): string | null
262
+ /**
263
+ * Parse JSON → packed token stream bytes (structural, no re-parse). Layout:
264
+ * `[u32 strCount]{[u32 len][utf8]}... [u32 treeLen][tree]` — a deduplicated
265
+ * string table followed by a start/end-marker value tree:
266
+ * `0=null 1=false 2=true 3=number(f64 LE) 4=string(u32 table idx)
267
+ * 5=array start 6=object start 7=array end 8=object end
268
+ * 9=object key(u32 table idx)` (object body `6,(9,keyIdx,value)*,8`).
269
+ * Throws on invalid JSON / too-small output. The scalar wrapper decodes the
270
+ * table once and assembles the value — no second parse.
271
+ */
272
+ jsonParsePacked(input: Uint8Array): Uint8Array
273
+ /**
274
+ * Parse a Content-Type header → packed verdict bytes:
275
+ * `[u32 mediaTypeLen][mediaType][u32 charsetLen (0xFFFFFFFF = none)][charset]
276
+ * [u32 boundaryLen][boundary][u32 paramCount]{[u32 keyLen][key][u32 valLen][val]}`.
277
+ * Throws on invalid media type / too-small output.
278
+ */
279
+ parseMediaType(input: Uint8Array): Uint8Array
280
+ /** Parse an IMF-fixdate → unix seconds, or `null` on malformed (packed
281
+ * `[u8 ok][i64 secs LE]` output, decoded here). */
282
+ parseHttpDate(input: Uint8Array): bigint | null
283
+ /**
284
+ * Parse an Accept-Encoding header → packed verdict bytes:
285
+ * `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}` (empty header → count 0).
286
+ * Throws on too-small output.
287
+ */
288
+ parseAcceptEncoding(input: Uint8Array): Uint8Array
289
+ /** Percent-encode a query from packed pairs (the `packPairs` layout) → string,
290
+ * keys SORTED (matches napi `BTreeMap` ordering); `null` on malformed input. */
291
+ urlEncodeQuery(input: Uint8Array): string | null
292
+ /** RFC 3986 URL resolution (base + reference) → string; `null` on non-UTF-8. */
293
+ urlResolve(base: Uint8Array, reference: Uint8Array): string | null
294
+ /** Resolve a reference against a `UrlBuilder`'s PRECOMPILED base (opaque
295
+ * `inner` handle from its napi `inner_ptr()`) → bytes; throws on a null
296
+ * handle / non-UTF-8 reference / too-small output. */
297
+ urlBuilderResolve(inner: number, reference: Uint8Array): Uint8Array
298
+ /** Extension → MIME type → string (unknown → `application/octet-stream`; `ext` is a `cstring` ARG). */
299
+ mimeFromExtension(ext: string): string | null
300
+ /**
301
+ * 29-byte HTTP-date (`Sun, 06 Nov 1994 08:49:37 GMT`) into `output`; returns
302
+ * bytes written (throws on too-small / out-of-range year). FFI sibling of the
303
+ * napi `httpDateInto` — kills the napi crossing on the hot path.
304
+ */
305
+ httpDateInto(secs: number, output: Uint8Array): number
306
+ /** 29-byte HTTP-date → string (allocating sibling of httpDateInto). */
307
+ httpDate(secs?: number): string
308
+ /**
309
+ * Encode one SSE event → bytes (fresh buffer). event/id null = line omitted
310
+ * (a present-but-empty string emits the line, matching napi `Option`).
311
+ * FFI sibling of the napi `sse_encode_event` — kills the napi crossing.
312
+ */
313
+ sseEncodeEvent(
314
+ event: string | null,
315
+ data: Uint8Array,
316
+ id: string | null,
317
+ retry: number | null,
318
+ ): Uint8Array
319
+ /**
320
+ * Encode one SSE event into `output` (sized ≥ `data.length + 64`); returns
321
+ * bytes written (throws if too small / invalid UTF-8). Pooled sibling of
322
+ * `sseEncodeEvent` — removes the per-call output-buffer alloc.
323
+ */
324
+ sseEncodeEventInto(
325
+ event: string | null,
326
+ data: Uint8Array,
327
+ id: string | null,
328
+ retry: number | null,
329
+ output: Uint8Array,
330
+ ): number
331
+ /** Random hex token into `output`; returns bytes written (throws if too small). */
332
+ randomTokenInto(byteLen: number, output: Uint8Array): number
333
+ /**
334
+ * Run the ingress pipeline on a packed frame via the opaque inner handle from
335
+ * `Ingress.ingressInnerPtr()` (valid only while the instance is alive — the
336
+ * caller must hold it). Returns bytes written; throws on error / too-small.
337
+ */
338
+ ingressHandlePacked(
339
+ inner: number,
340
+ input: Uint8Array,
341
+ body: Uint8Array | null,
342
+ output: Uint8Array,
343
+ ): number
344
+ /**
345
+ * Run the ingress pipeline from raw request components via the opaque inner
346
+ * handle (valid only while the instance is alive). `url`/`ip` are JS strings
347
+ * passed as `bun:ffi` `cstring` ARGs — the engine transcodes them to
348
+ * call-scoped NUL-terminated UTF-8 buffers in-engine, so the JS side skips
349
+ * the frame assembly + `Buffer.write` encode for the URL/IP. `rid`/`headers`
350
+ * are the packed byte slices; `body` is the request body (or null); `output`
351
+ * receives the packed decision. Same wire format as {@link ingressHandlePacked}.
352
+ * Returns bytes written; throws on error / too-small.
353
+ */
354
+ ingressHandleComponents(
355
+ inner: number,
356
+ methodKind: number,
357
+ url: string,
358
+ ip: string,
359
+ rid: Uint8Array,
360
+ headers: Uint8Array,
361
+ body: Uint8Array | null,
362
+ output: Uint8Array,
363
+ ): number
364
+ /**
365
+ * Write the ingress binary-layout constants (38 × u32 LE — `rust/ffi.rs`
366
+ * `IngressLayout`, numeric source `rust/ingress/output.rs`) into `output`;
367
+ * returns bytes written. Lets `src/ingress/constants.ts` read the layout via
368
+ * bun:ffi on Bun so importing the package does NOT dlopen the napi addon.
369
+ */
370
+ ingressLayout(out: Uint8Array): number
371
+
372
+ // ── Per-route native stack (`castrum_route_*`) ──────────────────
373
+ /**
374
+ * Compile a route descriptor (the `@ignex/native` route-wire v3 format:
375
+ * magic `ROUT`, version 3, limits, stage tags, draft-07 body schema) into an
376
+ * opaque handle. Returns the handle (`0` = invalid descriptor / compile
377
+ * failure / panic — throws). The handle is owned by the caller and must be
378
+ * released with {@link routeDestroy}.
379
+ */
380
+ routeCompile(descriptor: Uint8Array): number
381
+ /**
382
+ * Run a compiled route on one request frame, writing the packed verdict
383
+ * (`[flags u32][errorCode u32]` + optional query/cookie pair sections) into
384
+ * `output`. Returns bytes written (`0` = real error — throws; `> output.length`
385
+ * = the EXACT required size — the caller allocates once and retries, per the
386
+ * growExact convention).
387
+ */
388
+ routeRun(handle: number, frame: Uint8Array, output: Uint8Array): number
389
+ /** Destroy a compiled route handle (frees the native `Box<NativeRoute>`). */
390
+ routeDestroy(handle: number): void
391
+ }
392
+
393
+ /**
394
+ * bun:ffi transport selection (from `CASTRUM_FFI_MODE`; legacy `RUST_FFI_MODE`
395
+ * accepted):
396
+ *
397
+ * - `auto` (default): use bun:ffi on Bun, silently fall back to napi when the
398
+ * bind or self-test fails (and always on Node).
399
+ * - `ffi`: force bun:ffi on Bun — throw a clear error if it can't bind or the
400
+ * self-test fails (use in benches/CI that MUST run ffi).
401
+ * - `napi`: never bind — every call goes through the napi addon (the fallback;
402
+ * useful for exercising the fallback path on Bun).
403
+ */
404
+ export type FfiMode = 'auto' | 'ffi' | 'napi'
405
+
406
+ /** Unpacked rate-limit verdict (from the packed
407
+ * `[u8 allowed][u32 remaining LE][i64 reset_ms LE]` C-ABI output). */
408
+ export interface RateLimiterVerdict {
409
+ allowed: boolean
410
+ remaining: number
411
+ resetMs: number
412
+ }
413
+
414
+ /** Raw 2-arg C-ABI symbol signature (`(ptr,len)` pairs → scalar result). */
415
+ export type Raw2 = (a: unknown, b: unknown) => number | bigint
416
+ /** Raw 3-arg C-ABI symbol signature. */
417
+ export type Raw3 = (a: unknown, b: unknown, c: unknown) => number | bigint
418
+ /** Raw 4-arg C-ABI symbol signature. */
419
+ export type Raw4 = (a: unknown, b: unknown, c: unknown, d: unknown) => number | bigint
420
+ /** Raw 5-arg C-ABI symbol signature. */
421
+ export type Raw5 = (a: unknown, b: unknown, c: unknown, d: unknown, e: unknown) => number | bigint
422
+ /** Raw 6-arg C-ABI symbol signature. */
423
+ export type Raw6 = (
424
+ a: unknown,
425
+ b: unknown,
426
+ c: unknown,
427
+ d: unknown,
428
+ e: unknown,
429
+ f: unknown,
430
+ ) => number | bigint
431
+ /** Raw 7-arg C-ABI symbol signature. */
432
+ export type Raw7 = (
433
+ a: unknown,
434
+ b: unknown,
435
+ c: unknown,
436
+ d: unknown,
437
+ e: unknown,
438
+ f: unknown,
439
+ g: unknown,
440
+ ) => number | bigint
441
+ /** Raw 8-arg C-ABI symbol signature. */
442
+ export type Raw8 = (
443
+ a: unknown,
444
+ b: unknown,
445
+ c: unknown,
446
+ d: unknown,
447
+ e: unknown,
448
+ f: unknown,
449
+ g: unknown,
450
+ h: unknown,
451
+ ) => number | bigint
452
+ /** Raw 9-arg C-ABI symbol signature. */
453
+ export type Raw9 = (
454
+ a: unknown,
455
+ b: unknown,
456
+ c: unknown,
457
+ d: unknown,
458
+ e: unknown,
459
+ f: unknown,
460
+ g: unknown,
461
+ h: unknown,
462
+ i: unknown,
463
+ ) => number | bigint
464
+ /** Raw 10-arg C-ABI symbol signature. */
465
+ export type Raw10 = (
466
+ a: unknown,
467
+ b: unknown,
468
+ c: unknown,
469
+ d: unknown,
470
+ e: unknown,
471
+ f: unknown,
472
+ g: unknown,
473
+ h: unknown,
474
+ i: unknown,
475
+ j: unknown,
476
+ ) => number | bigint
477
+
478
+ /** Raw 12-arg C-ABI symbol signature (`castrum_ingress_handle_components`). */
479
+ export type Raw12 = (
480
+ a: unknown,
481
+ b: unknown,
482
+ c: unknown,
483
+ d: unknown,
484
+ e: unknown,
485
+ f: unknown,
486
+ g: unknown,
487
+ h: unknown,
488
+ i: unknown,
489
+ j: unknown,
490
+ k: unknown,
491
+ l: unknown,
492
+ ) => number | bigint
493
+
494
+ /** A raw C-ABI symbol whose return type is `cstring` — the engine clones the
495
+ * null-terminated string at return (`null` = C NULL, i.e. a failure sentinel).
496
+ * Used by the fixed-size ASCII writers (ws_accept_key, etag, random_token,
497
+ * sign/verify_cookie, csrf_token, jwt_sign_bytes).
498
+ */
499
+ export type RawCStr = (...a: unknown[]) => string | null