castrum 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/Cargo.lock +4 -3
  2. package/Cargo.toml +1 -1
  3. package/castrum.darwin-arm64.node +0 -0
  4. package/castrum.darwin-x64.node +0 -0
  5. package/castrum.linux-arm64-gnu.node +0 -0
  6. package/castrum.linux-arm64-musl.node +0 -0
  7. package/castrum.linux-x64-gnu.node +0 -0
  8. package/castrum.linux-x64-musl.node +0 -0
  9. package/castrum.linux-x64-v3-gnu.node +0 -0
  10. package/castrum.win32-x64-msvc.node +0 -0
  11. package/dist/index.d.ts +501 -5
  12. package/dist/index.js +1658 -1314
  13. package/docs/ARCHITECTURE.md +1 -1
  14. package/docs/BENCHMARKS.md +13 -0
  15. package/docs/CASE_STUDY.md +2 -2
  16. package/docs/ENVIRONMENT.md +1 -1
  17. package/docs/FFI_BUN_GUIDE.md +13 -1
  18. package/docs/HOW_TO_ADD_AN_OP.md +24 -0
  19. package/docs/INGRESS.md +4 -4
  20. package/docs/REPO_MAP.md +8 -6
  21. package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
  22. package/index.ts +1 -0
  23. package/package.json +3 -2
  24. package/rust/crypto/aead.rs +44 -0
  25. package/rust/crypto/argon2.rs +32 -2
  26. package/rust/crypto/jwt/tests.rs +19 -0
  27. package/rust/crypto/jwt/token.rs +43 -28
  28. package/rust/crypto/session.rs +170 -7
  29. package/rust/ffi/crypto.rs +182 -61
  30. package/rust/ffi/hashing.rs +48 -33
  31. package/rust/ffi/http.rs +62 -36
  32. package/rust/ffi/metrics.rs +6 -2
  33. package/rust/ffi/payload.rs +47 -27
  34. package/rust/ffi/task.rs +51 -32
  35. package/rust/ffi/tests.rs +197 -0
  36. package/rust/ffi/util.rs +40 -0
  37. package/rust/http/headers.rs +13 -0
  38. package/rust/http/multipart.rs +139 -0
  39. package/rust/ingress/native_route.rs +21 -3
  40. package/rust/task/completion.rs +7 -0
  41. package/rust/task/mod.rs +1 -1
  42. package/rust/task/tests.rs +30 -0
  43. package/src/ingress/decode/baked-result.ts +13 -5
  44. package/src/ingress/decode/fast-result.ts +1 -1
  45. package/src/ingress/decode/result-base.ts +16 -2
  46. package/src/ingress/handlers.ts +103 -31
  47. package/src/ingress/headers/memoized-headers.ts +40 -0
  48. package/src/ingress/native-route.ts +25 -6
  49. package/src/ingress/options.ts +26 -2
  50. package/src/ingress/packing/gather-raw-headers.ts +24 -8
  51. package/src/ingress/response/baked-response.ts +118 -28
  52. package/src/ingress/router.ts +14 -2
  53. package/src/ingress/routes/common.ts +26 -10
  54. package/src/ingress/routes/echo.ts +50 -7
  55. package/src/ingress/routes/json-write.ts +21 -8
  56. package/src/ingress/routes/responder.ts +4 -4
  57. package/src/ingress/server-node.ts +33 -14
  58. package/src/ingress/server.ts +3 -4
  59. package/src/ingress/shared.ts +1 -1
  60. package/src/ingress/types.ts +15 -0
  61. package/src/loader/ops.ts +1 -1
  62. package/src/native/ffi/build/codecs-selftest.ts +305 -0
  63. package/src/native/ffi/build/codecs.ts +0 -296
  64. package/src/native/ffi/build/instances-selftest.ts +264 -0
  65. package/src/native/ffi/build/instances.ts +39 -241
  66. package/src/native/ffi/build/metrics.ts +0 -7
  67. package/src/native/ffi/build/parse-selftest.ts +238 -0
  68. package/src/native/ffi/build/parse.ts +11 -234
  69. package/src/native/ffi/build.ts +5 -1
  70. package/src/native/ffi/selftest.ts +3 -3
  71. package/src/native/ffi/symbols.ts +460 -0
  72. package/src/native/ffi/types.ts +17 -0
  73. package/src/native/ffi.ts +2 -422
  74. package/src/native/ingress-binding.ts +39 -0
  75. package/src/runtime/codec.ts +24 -0
  76. package/src/runtime/types.ts +11 -0
  77. package/src/rust-ffi/batch/build.ts +18 -8
  78. package/src/rust-ffi/batch/types.ts +15 -0
  79. package/src/rust-ffi/options.ts +19 -0
  80. package/src/rust-ffi/scalar/crypto.ts +8 -0
  81. package/src/rust-ffi/scalar/factories.ts +47 -20
  82. package/src/rust-ffi/scalar/http.ts +60 -10
  83. package/src/rust-ffi/scalar/interface.ts +4 -1
  84. package/src/rust-ffi/scalar/payload.ts +27 -1
  85. package/src/shared/buffer-pool.ts +44 -9
  86. package/src/shared/codec.ts +26 -0
  87. package/src/shared/packed/parsers.ts +64 -7
  88. package/src/task/runtime.ts +29 -28
package/Cargo.lock CHANGED
@@ -233,7 +233,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
233
233
 
234
234
  [[package]]
235
235
  name = "castrum"
236
- version = "0.9.6"
236
+ version = "0.9.8"
237
237
  dependencies = [
238
238
  "argon2",
239
239
  "aws-lc-rs",
@@ -1075,13 +1075,14 @@ dependencies = [
1075
1075
 
1076
1076
  [[package]]
1077
1077
  name = "napi"
1078
- version = "3.11.0"
1078
+ version = "3.12.2"
1079
1079
  source = "registry+https://github.com/rust-lang/crates.io-index"
1080
- checksum = "de33522036981030a75c231829566bc63414e08101a6f5ff4ac6cef19c8e0941"
1080
+ checksum = "58c5f4d5375213fdb7be2655e152386e82f026f9a5ba36a75556e11359aafe09"
1081
1081
  dependencies = [
1082
1082
  "bitflags",
1083
1083
  "ctor",
1084
1084
  "futures",
1085
+ "libc",
1085
1086
  "napi-build",
1086
1087
  "napi-sys",
1087
1088
  "nohash-hasher",
package/Cargo.toml CHANGED
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "castrum"
3
3
  # Keep in sync with package.json "version" (single source of truth: the npm package).
4
- version = "0.9.6"
4
+ version = "0.9.8"
5
5
  edition = "2021"
6
6
  # Keep in sync with package.json "license" (cargo-deny rejects a crate with no
7
7
  # license expression).
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/index.d.ts CHANGED
@@ -116,7 +116,11 @@ export interface NativeRoute {
116
116
  runFrame(frame: Uint8Array): RouteWireResult;
117
117
  /** Convenience: pack a `(query, cookie, body)` frame then {@link runFrame}. */
118
118
  run(query: string, cookie: string, body: Uint8Array | null): RouteWireResult;
119
- /** Free the native handle (idempotent; safe to call once at shutdown). */
119
+ /**
120
+ * Free the native handle. Idempotent: a second call is a no-op. After it,
121
+ * {@link runFrame}/{@link run} throw rather than pass a freed handle into the
122
+ * native stack.
123
+ */
120
124
  destroy(): void;
121
125
  }
122
126
  /**
@@ -182,6 +186,13 @@ declare abstract class IngressResultBase {
182
186
  /** Request body bytes (may be an empty view). */
183
187
  body: Uint8Array;
184
188
  protected _buf: Uint8Array;
189
+ /**
190
+ * Valid byte length of `_buf` (the native `written` count). `_buf` may be the
191
+ * WHOLE pooled output buffer (`outputBufferSize` bytes), so bounds checks must
192
+ * use this, not `_buf.byteLength` — that lets `run()` skip a per-request
193
+ * `buffer.subarray(0, written)` view.
194
+ */
195
+ protected _len: number;
185
196
  protected _bodyJsonStart: number;
186
197
  protected _bodyJsonLen: number;
187
198
  private _cookiesBuf;
@@ -206,7 +217,7 @@ declare abstract class IngressResultBase {
206
217
  * baked path passes the already-clamped `layout.safeBodyJsonLen`. Sections
207
218
  * are stored as slices and decoded lazily by `cookiesJson()`/`queryJson()`.
208
219
  */
209
- protected setSections(buf: Uint8Array, layout: PackedSectionLayout, bodyJsonLen: number): void;
220
+ protected setSections(buf: Uint8Array, layout: PackedSectionLayout, bodyJsonLen: number, len?: number): void;
210
221
  /** Decoded cookies-JSON section, or `"{}"` when absent. */
211
222
  cookiesJson(): string;
212
223
  /** Decoded query-JSON section, or `"{}"` when absent. */
@@ -216,7 +227,7 @@ declare abstract class IngressResultBase {
216
227
  }
217
228
  /** Zero-alloc, reusable result decoder for the pre-baked handler path. */
218
229
  export declare class BakedIngressResult extends IngressResultBase {
219
- refresh(buf: Uint8Array, body: Uint8Array, view: DataView): void;
230
+ refresh(buf: Uint8Array, body: Uint8Array, view: DataView, len?: number): void;
220
231
  invalidate(): void;
221
232
  setInternalError(): void;
222
233
  bodyJson(copy: boolean): Uint8Array;
@@ -427,6 +438,14 @@ export interface OptimizedIngressHandler {
427
438
  string,
428
439
  string
429
440
  ][];
441
+ /**
442
+ * Success-path headers as a ready `Headers` instance: the memoized base
443
+ * (template + reflected origin) plus per-request rate/request-id extras,
444
+ * without rebuilding `Headers` from an array on every response. Optional so
445
+ * hand-rolled mocks need not provide it; callers fall back to
446
+ * `memoizedHeaders(responseHeaders(...))`.
447
+ */
448
+ successHeaders?(variant: number, requestIdHeader: string | null, origin: string | null, rateRemaining?: number, rateResetSecs?: number): Headers;
430
449
  terminalHeaders(variant: number, ctx: BakedContext, result: BakedIngressResult | null): [
431
450
  string,
432
451
  string
@@ -949,6 +968,448 @@ export interface Argon2HasherInstance {
949
968
  hash(password: Uint8Array, salt: Uint8Array): Uint8Array;
950
969
  verify(password: Uint8Array, phc: Uint8Array): boolean;
951
970
  }
971
+ /**
972
+ * The bound bun:ffi C-ABI surface. Each member is a dlopen'd `castrum_*`
973
+ * symbol wrapped so inputs are `(ptr,len)` byte slices and text outputs are
974
+ * returned as JS strings via JSC's native transfer (cstring clone) — zero
975
+ * `TextEncoder`/`TextDecoder` on the Bun path. `*Into` variants write into a
976
+ * caller buffer and return the byte count; variable-size ops return the exact
977
+ * required size on a too-small buffer so the transport grows once and retries
978
+ * exactly once.
979
+ */
980
+ export interface BunFFI {
981
+ crc32(input: Uint8Array): number;
982
+ fnv1a64(input: Uint8Array): bigint;
983
+ xxh3(input: Uint8Array): bigint;
984
+ jsonValid(input: Uint8Array): boolean;
985
+ /** UTF-8 validity probe (native replacement for a fatal `TextDecoder`). */
986
+ utf8Valid(input: Uint8Array): boolean;
987
+ /** Lowercase-hex encode → string (native transfer, zero encode). */
988
+ hexEncode(input: Uint8Array): string;
989
+ /** Lowercase-hex encode into `output`; returns bytes written. */
990
+ hexEncodeInto(input: Uint8Array, output: Uint8Array): number;
991
+ /** RFC 3986 percent-encode → string. */
992
+ urlEncode(input: Uint8Array): string;
993
+ /** RFC 3986 percent-encode into `output`; returns bytes written. */
994
+ urlEncodeInto(input: Uint8Array, output: Uint8Array): number;
995
+ validateEmail(input: string): boolean;
996
+ validateUuid(input: string): boolean;
997
+ validateIpv4(input: string): boolean;
998
+ validateIpv6(input: string): boolean;
999
+ /** Byte-input sibling of {@link validateEmail} (`(ptr,len)` — no decode). */
1000
+ validateEmailBytes(input: Uint8Array): boolean;
1001
+ /** Byte-input sibling of {@link validateUuid} (`(ptr,len)` — no decode). */
1002
+ validateUuidBytes(input: Uint8Array): boolean;
1003
+ /** Byte-input sibling of {@link validateIpv4} (`(ptr,len)` — no decode). */
1004
+ validateIpv4Bytes(input: Uint8Array): boolean;
1005
+ /** Byte-input sibling of {@link validateIpv6} (`(ptr,len)` — no decode). */
1006
+ validateIpv6Bytes(input: Uint8Array): boolean;
1007
+ /** Sum of `id` fields across a JSON array → bigint (throws on non-array input). */
1008
+ jsonSumIds(input: Uint8Array): bigint;
1009
+ hmacSha256Verify(key: Uint8Array, data: Uint8Array, signature: Uint8Array): boolean;
1010
+ csrfVerify(token: Uint8Array, secret: Uint8Array): boolean;
1011
+ passwordVerify(password: Uint8Array, phc: Uint8Array): boolean;
1012
+ /** `phc` is a `cstring` ARG (the engine transcodes the JS PHC string in-engine). */
1013
+ passwordVerifyBcrypt(password: Uint8Array, phc: string): boolean;
1014
+ /** Hex-decode into a fresh buffer (size `input.length / 2`). */
1015
+ hexDecode(input: Uint8Array): Uint8Array;
1016
+ /** Hex-decode into `output`; returns bytes written (throws if too small). */
1017
+ hexDecodeInto(input: Uint8Array, output: Uint8Array): number;
1018
+ /** Percent-decode into a fresh buffer (size `input.length`). */
1019
+ urlDecode(input: Uint8Array): Uint8Array;
1020
+ /** Percent-decode into `output`; returns bytes written (throws if too small). */
1021
+ urlDecodeInto(input: Uint8Array, output: Uint8Array): number;
1022
+ /** base64-decode into a fresh buffer (size `ceil(len/4)*3`). */
1023
+ base64Decode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array;
1024
+ /** base64-decode into `output`; returns bytes written (throws if too small). */
1025
+ base64DecodeInto(input: Uint8Array, output: Uint8Array, urlSafe?: boolean, padding?: boolean): number;
1026
+ /** RFC 6455 Sec-WebSocket-Accept → string (cstring return; `key` is a `cstring` ARG). */
1027
+ wsAcceptKey(key: string): string;
1028
+ /** RFC 6455 Sec-WebSocket-Accept into `output`; returns bytes written. */
1029
+ wsAcceptKeyInto(key: Uint8Array, output: Uint8Array): number;
1030
+ /** crc32 ETag → string (10 strong / 12 weak chars, cstring return). */
1031
+ etag(data: Uint8Array, weak?: boolean): string;
1032
+ /** crc32 ETag into `output`; returns bytes written. */
1033
+ etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number;
1034
+ /** Evaluate a precompiled `ConditionalRequest` (opaque `inner` handle from its
1035
+ * napi `inner_ptr()`) — true → 304. `null` header args = absent (flags encode
1036
+ * presence so a present-but-empty header is distinct). A null (0) handle → false. */
1037
+ conditionalIsNotModified(inner: number, ifNoneMatch: Uint8Array | null, ifModifiedSince: Uint8Array | null): boolean;
1038
+ /** Precompiled `MediaTypeMatcher` wildcard match (opaque `inner` handle). */
1039
+ mediaTypeMatcherMatches(inner: number, actual: Uint8Array): boolean;
1040
+ /** Precompiled `AcceptNegotiator` → best supported encoding (opaque `inner`
1041
+ * handle); `null` = identity. */
1042
+ acceptNegotiatorNegotiate(inner: number, header: Uint8Array): string | null;
1043
+ /** Precompiled `AcceptNegotiator` → best supported encoding with SERVER-
1044
+ * preference tie-breaking (RFC 7231 server semantics, opaque `inner` handle);
1045
+ * `null` = identity. */
1046
+ acceptNegotiatorNegotiateServer(inner: number, header: Uint8Array): string | null;
1047
+ /** Precompiled `JwtSigner` → compact token from pre-serialized claim JSON
1048
+ * (opaque `inner` handle); throws on invalid claims / null handle. */
1049
+ jwtSignerSign(inner: number, claimsJson: Uint8Array, nowSeconds: number): Uint8Array;
1050
+ /** Precompiled `JwtSigner` → claims JSON bytes (opaque `inner` handle);
1051
+ * `null` = invalid signature / expired / malformed. */
1052
+ jwtSignerVerify(inner: number, token: Uint8Array, nowSeconds: number): Uint8Array | null;
1053
+ /** Ed25519 keypair generation → `{ privateKey, publicKey }` (PKCS#8 v1 DER
1054
+ * private + SPKI DER public bytes); throws on CSPRNG failure. */
1055
+ ed25519GenerateKeypair(): {
1056
+ privateKey: Uint8Array;
1057
+ publicKey: Uint8Array;
1058
+ };
1059
+ /** Ed25519 sign → 64-byte signature; throws on invalid private key. */
1060
+ ed25519Sign(msg: Uint8Array, privateKey: Uint8Array): Uint8Array;
1061
+ /** Ed25519 verify → whether `signature` is valid for `msg` under `publicKey`. */
1062
+ ed25519Verify(msg: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
1063
+ /** EdDSA (Ed25519) JWT sign → compact token; `null` = invalid claims/private
1064
+ * key. `ttl <= 0` = no `iat`/`exp` injection. */
1065
+ jwtEdDSASign(claimsJson: Uint8Array, privateKey: Uint8Array, ttl: number, nowSeconds: number): string | null;
1066
+ /** EdDSA (Ed25519) JWT verify → claims JSON string; `null` = invalid. */
1067
+ jwtEdDSAVerify(token: Uint8Array, publicKey: Uint8Array, nowSeconds: number): string | null;
1068
+ /** Compiled `TemplateRenderer` → UTF-8 bytes from a pre-serialized JSON
1069
+ * context (opaque `inner` handle); throws on invalid context / render error. */
1070
+ templateRender(inner: number, contextJson: Uint8Array): Uint8Array;
1071
+ /** Compiled `SchemaValidator` → whether a document is valid (opaque `inner` handle). */
1072
+ schemaValidatorValidate(inner: number, doc: Uint8Array): boolean;
1073
+ /** `RateLimiter` string-key check → { allowed, remaining, resetMs } (opaque
1074
+ * `inner` handle; `key` is a `cstring` ARG); throws on a null handle. */
1075
+ rateLimiterCheck(inner: number, key: string, nowMs: number): RateLimiterVerdict;
1076
+ /** `RateLimiter` pre-hashed key check → { allowed, remaining, resetMs }. */
1077
+ rateLimiterCheckKey(inner: number, key: number, nowMs: number): RateLimiterVerdict;
1078
+ /** `byteLen` random bytes → `byteLen * 2` hex-char string (cstring return). */
1079
+ randomToken(byteLen: number): string;
1080
+ /** base64-encode → string. */
1081
+ base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): string;
1082
+ /** base64-encode into `output`; returns bytes written. */
1083
+ base64EncodeInto(input: Uint8Array, output: Uint8Array, urlSafe?: boolean, padding?: boolean): number;
1084
+ /** HMAC-SHA256 → 64-char lowercase-hex string. */
1085
+ hmacSha256(key: Uint8Array, data: Uint8Array): string;
1086
+ /** HMAC-SHA256 hex into `output`; returns bytes written (throws if too small). */
1087
+ hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number;
1088
+ /** Sign cookie `value` as `value.<64-hex>` → string (cstring return). */
1089
+ signCookie(value: Uint8Array, secret: Uint8Array): string;
1090
+ /** Sign cookie into `output`; returns bytes written (throws if too small). */
1091
+ signCookieInto(value: Uint8Array, secret: Uint8Array, output: Uint8Array): number;
1092
+ /** Verify a signed cookie → value string, or `null` on bad signature. */
1093
+ verifyCookie(signed: Uint8Array, secret: Uint8Array): string | null;
1094
+ /** Verify a signed cookie into `output`; returns value length or `null` on bad signature. */
1095
+ verifyCookieInto(signed: Uint8Array, secret: Uint8Array, output: Uint8Array): number | null;
1096
+ /** CSRF token (129 chars: 64 rnd-hex + '.' + 64 sig-hex) → string. */
1097
+ csrfToken(secret: Uint8Array): string;
1098
+ /** CSRF token into `output`; returns bytes written (throws if too small). */
1099
+ csrfTokenInto(secret: Uint8Array, output: Uint8Array): number;
1100
+ /** Argon2id PHC hash → string (m/t/p/out_len params). */
1101
+ passwordHash(password: Uint8Array, salt: Uint8Array, mCost: number, tCost: number, pCost: number, outLen: number): string;
1102
+ /** bcrypt `$2b$` PHC → string (cost clamped 4..=31). */
1103
+ passwordHashBcrypt(password: Uint8Array, cost: number): string;
1104
+ /** PBKDF2-HMAC-SHA256 → `dkLen` bytes (rounds, dkLen clamped). */
1105
+ pbkdf2Sha256(password: Uint8Array, salt: Uint8Array, rounds: number, dkLen: number): Uint8Array;
1106
+ /** AEAD encrypt (0 = AES-256-GCM, 1 = ChaCha20-Poly1305) → ct+tag. */
1107
+ aeadEncrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, algorithm?: number): Uint8Array;
1108
+ /** AEAD encrypt into `output` (ct + 16-byte tag); returns bytes written. */
1109
+ aeadEncryptInto(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, output: Uint8Array, algorithm?: number): number;
1110
+ /** AEAD decrypt → plaintext, or `null` on auth failure. */
1111
+ aeadDecrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, algorithm?: number): Uint8Array | null;
1112
+ /** RFC 6455 frame encode into a fresh buffer. */
1113
+ wsFrameEncode(opcode: number, payload: Uint8Array, mask: boolean, fin: boolean): Uint8Array;
1114
+ /** RFC 6455 frame encode into `output`; returns bytes written (throws if too small). */
1115
+ wsFrameEncodeInto(opcode: number, payload: Uint8Array, mask: boolean, fin: boolean, output: Uint8Array): number;
1116
+ /** RFC 6902 JSON patch → JSON text string. */
1117
+ jsonPatch(doc: Uint8Array, patch: Uint8Array): string;
1118
+ /** gzip-compress into a fresh buffer (level clamped 0..=9, default 6). */
1119
+ gzipCompress(data: Uint8Array, level?: number): Uint8Array;
1120
+ /** gzip-compress into `output`; returns bytes written (throws if too small). */
1121
+ gzipCompressInto(data: Uint8Array, output: Uint8Array, level?: number): number;
1122
+ /** gzip-decompress into a fresh buffer (capped by `maxDecompressed`). */
1123
+ gzipDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array;
1124
+ /** gzip-decompress into `output`; returns bytes written (throws if too small). */
1125
+ gzipDecompressInto(data: Uint8Array, output: Uint8Array, maxDecompressed?: number): number;
1126
+ /**
1127
+ * gzip ISIZE trailer probe → the uncompressed size for a single-member gzip
1128
+ * stream, or `0` when unknown (invalid stream / multi-member). Used to
1129
+ * PRE-SIZE a destination buffer for the zero-copy task path.
1130
+ */
1131
+ gzipIsize(data: Uint8Array): number;
1132
+ /** brotli-compress into a fresh buffer (quality clamped 0..=11, default 5). */
1133
+ brotliCompress(data: Uint8Array, quality?: number): Uint8Array;
1134
+ /** brotli-compress into `output`; returns bytes written (throws if too small). */
1135
+ brotliCompressInto(data: Uint8Array, output: Uint8Array, quality?: number): number;
1136
+ /** brotli-decompress into a fresh buffer (capped by `maxDecompressed`). */
1137
+ brotliDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array;
1138
+ /** brotli-decompress into `output`; returns bytes written (throws if too small). */
1139
+ brotliDecompressInto(data: Uint8Array, output: Uint8Array, maxDecompressed?: number): number;
1140
+ /** HTTP request parse → packed output into `output`; returns bytes written. */
1141
+ httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number;
1142
+ /** Query string parse → packed output into `output`; returns bytes written. */
1143
+ queryParsePackedInto(input: Uint8Array, output: Uint8Array): number;
1144
+ /** Cookie header parse → packed output into `output`; returns bytes written. */
1145
+ cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number;
1146
+ /** Sign JWT (HS256) from pre-serialized claim JSON (ttl<=0 → no iat/exp) → string. */
1147
+ jwtSignBytes(claimsJson: Uint8Array, secret: Uint8Array, ttl: number, now: number): string;
1148
+ /** Sign JWT into `output`; returns bytes written (throws if too small). */
1149
+ jwtSignBytesInto(claimsJson: Uint8Array, secret: Uint8Array, ttl: number, now: number, output: Uint8Array): number;
1150
+ /** Decode a WS frame into packed `[flags][opcode][u32 len][payload]`; null on malformed. */
1151
+ wsFrameDecodePacked(data: Uint8Array): Uint8Array | null;
1152
+ /**
1153
+ * Decode a WS frame into `output` (sized ≥ `data.length + 6`); returns bytes
1154
+ * written, null on malformed. Pooled sibling of `wsFrameDecodePacked`.
1155
+ */
1156
+ wsFrameDecodePackedInto(data: Uint8Array, output: Uint8Array): number | null;
1157
+ /** Parse multipart/form-data into the packed parts layout. */
1158
+ multipartParsePacked(body: Uint8Array, boundary: Uint8Array): Uint8Array;
1159
+ /** Parse multipart/form-data into `output`; returns bytes written (throws if too small). */
1160
+ multipartParsePackedInto(body: Uint8Array, boundary: Uint8Array, output: Uint8Array): number;
1161
+ /** Parse x-www-form-urlencoded into packed pairs into `output`. */
1162
+ formParsePackedInto(input: Uint8Array, output: Uint8Array): number;
1163
+ /**
1164
+ * Verify an HS256 JWT → claims JSON text (cstring return; `null` on invalid
1165
+ * signature / expired / malformed). `nowSeconds` is an i64 C arg.
1166
+ */
1167
+ jwtVerify(token: Uint8Array, secret: Uint8Array, nowSeconds: number): string | null;
1168
+ /**
1169
+ * Parse JSON → packed token stream bytes (structural, no re-parse). Layout:
1170
+ * `[u32 strCount]{[u32 len][utf8]}... [u32 treeLen][tree]` — a deduplicated
1171
+ * string table followed by a start/end-marker value tree:
1172
+ * `0=null 1=false 2=true 3=number(f64 LE) 4=string(u32 table idx)
1173
+ * 5=array start 6=object start 7=array end 8=object end
1174
+ * 9=object key(u32 table idx)` (object body `6,(9,keyIdx,value)*,8`).
1175
+ * Throws on invalid JSON / too-small output. The scalar wrapper decodes the
1176
+ * table once and assembles the value — no second parse.
1177
+ */
1178
+ jsonParsePacked(input: Uint8Array): Uint8Array;
1179
+ /**
1180
+ * Parse a Content-Type header → packed verdict bytes:
1181
+ * `[u32 mediaTypeLen][mediaType][u32 charsetLen (0xFFFFFFFF = none)][charset]
1182
+ * [u32 boundaryLen][boundary][u32 paramCount]{[u32 keyLen][key][u32 valLen][val]}`.
1183
+ * Throws on invalid media type / too-small output.
1184
+ */
1185
+ parseMediaType(input: Uint8Array): Uint8Array;
1186
+ /** Parse an IMF-fixdate → unix seconds, or `null` on malformed (packed
1187
+ * `[u8 ok][i64 secs LE]` output, decoded here). */
1188
+ parseHttpDate(input: Uint8Array): bigint | null;
1189
+ /**
1190
+ * Parse an Accept-Encoding header → packed verdict bytes:
1191
+ * `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}` (empty header → count 0).
1192
+ * Throws on too-small output.
1193
+ */
1194
+ parseAcceptEncoding(input: Uint8Array): Uint8Array;
1195
+ /** Percent-encode a query from packed pairs (the `packPairs` layout) → string,
1196
+ * keys SORTED (matches napi `BTreeMap` ordering); `null` on malformed input. */
1197
+ urlEncodeQuery(input: Uint8Array): string | null;
1198
+ /** RFC 3986 URL resolution (base + reference) → string; `null` on non-UTF-8. */
1199
+ urlResolve(base: Uint8Array, reference: Uint8Array): string | null;
1200
+ /** Resolve a reference against a `UrlBuilder`'s PRECOMPILED base (opaque
1201
+ * `inner` handle from its napi `inner_ptr()`) → bytes; throws on a null
1202
+ * handle / non-UTF-8 reference / too-small output. */
1203
+ urlBuilderResolve(inner: number, reference: Uint8Array): Uint8Array;
1204
+ /** Extension → MIME type → string (unknown → `application/octet-stream`; `ext` is a `cstring` ARG). */
1205
+ mimeFromExtension(ext: string): string | null;
1206
+ /**
1207
+ * 29-byte HTTP-date (`Sun, 06 Nov 1994 08:49:37 GMT`) into `output`; returns
1208
+ * bytes written (throws on too-small / out-of-range year). FFI sibling of the
1209
+ * napi `httpDateInto` — kills the napi crossing on the hot path.
1210
+ */
1211
+ httpDateInto(secs: number, output: Uint8Array): number;
1212
+ /** 29-byte HTTP-date → string (allocating sibling of httpDateInto). */
1213
+ httpDate(secs?: number): string;
1214
+ /**
1215
+ * Encode one SSE event → bytes (fresh buffer). event/id null = line omitted
1216
+ * (a present-but-empty string emits the line, matching napi `Option`).
1217
+ * NOTE: event/id stay `(ptr,len)` byte args — a cstring-ARG conversion was
1218
+ * attempted and reverted (2026-08-23); see docs/FFI_BUN_GUIDE.md §14 before
1219
+ * touching this signature.
1220
+ * FFI sibling of the napi `sse_encode_event`.
1221
+ */
1222
+ sseEncodeEvent(event: string | null, data: Uint8Array, id: string | null, retry: number | null): Uint8Array;
1223
+ /**
1224
+ * Encode one SSE event into `output` (sized ≥ `data.length + 64`); returns
1225
+ * bytes written (throws if too small / invalid UTF-8). Pooled sibling of
1226
+ * `sseEncodeEvent` — removes the per-call output-buffer alloc.
1227
+ */
1228
+ sseEncodeEventInto(event: string | null, data: Uint8Array, id: string | null, retry: number | null, output: Uint8Array): number;
1229
+ /** Random hex token into `output`; returns bytes written (throws if too small). */
1230
+ randomTokenInto(byteLen: number, output: Uint8Array): number;
1231
+ /**
1232
+ * Run the ingress pipeline on a packed frame via the opaque inner handle from
1233
+ * `Ingress.ingressInnerPtr()` (valid only while the instance is alive — the
1234
+ * caller must hold it). Returns bytes written; throws on error / too-small.
1235
+ */
1236
+ ingressHandlePacked(inner: number, input: Uint8Array, body: Uint8Array | null, output: Uint8Array): number;
1237
+ /**
1238
+ * Run the ingress pipeline from raw request components via the opaque inner
1239
+ * handle (valid only while the instance is alive). `url`/`ip` are JS strings
1240
+ * passed as `bun:ffi` `cstring` ARGs — the engine transcodes them to
1241
+ * call-scoped NUL-terminated UTF-8 buffers in-engine, so the JS side skips
1242
+ * the frame assembly + `Buffer.write` encode for the URL/IP. `rid`/`headers`
1243
+ * are the packed byte slices; `body` is the request body (or null); `output`
1244
+ * receives the packed decision. Same wire format as {@link ingressHandlePacked}.
1245
+ * Returns bytes written; throws on error / too-small.
1246
+ */
1247
+ ingressHandleComponents(inner: number, methodKind: number, url: string, ip: string, rid: Uint8Array, headers: Uint8Array, body: Uint8Array | null, output: Uint8Array): number;
1248
+ /**
1249
+ * Write the ingress binary-layout constants (38 × u32 LE — `rust/ffi.rs`
1250
+ * `IngressLayout`, numeric source `rust/ingress/output.rs`) into `output`;
1251
+ * returns bytes written. Lets `src/ingress/constants.ts` read the layout via
1252
+ * bun:ffi on Bun so importing the package does NOT dlopen the napi addon.
1253
+ */
1254
+ ingressLayout(out: Uint8Array): number;
1255
+ /**
1256
+ * Compile a route descriptor (the `@ignex/native` route-wire v3 format:
1257
+ * magic `ROUT`, version 3, limits, stage tags, draft-07 body schema) into an
1258
+ * opaque handle. Returns the handle (`0` = invalid descriptor / compile
1259
+ * failure / panic — throws). The handle is owned by the caller and must be
1260
+ * released with {@link routeDestroy}.
1261
+ */
1262
+ routeCompile(descriptor: Uint8Array): number;
1263
+ /**
1264
+ * Run a compiled route on one request frame, writing the packed verdict
1265
+ * (`[flags u32][errorCode u32]` + optional query/cookie pair sections) into
1266
+ * `output`. Returns bytes written (`0` = real error — throws; `> output.length`
1267
+ * = the EXACT required size — the caller allocates once and retries, per the
1268
+ * growExact convention).
1269
+ */
1270
+ routeRun(handle: number, frame: Uint8Array, output: Uint8Array): number;
1271
+ /** Destroy a compiled route handle (frees the native `Box<NativeRoute>`). */
1272
+ routeDestroy(handle: number): void;
1273
+ /** Create an empty metrics registry → a caller-owned opaque handle. */
1274
+ metricsCreate(): number;
1275
+ /**
1276
+ * Declare a counter family → its `u32` series id (`0xFFFFFFFF` = error).
1277
+ * `name` and `labelKeys` are `cstring` ARGs; label keys are
1278
+ * `\u001f`-separated (empty string = no labels).
1279
+ */
1280
+ metricsCounter(handle: number, name: string, labelKeys: string): number;
1281
+ /** Declare a gauge family → its series id. Same ABI as {@link metricsCounter}. */
1282
+ metricsGauge(handle: number, name: string, labelKeys: string): number;
1283
+ /**
1284
+ * Declare a histogram family → its series id. `bucketsCsv` is a
1285
+ * comma-separated finite positive list (empty string = default buckets).
1286
+ */
1287
+ metricsHistogram(handle: number, name: string, labelKeys: string, bucketsCsv: string): number;
1288
+ /**
1289
+ * Counter/gauge `+= amount`, or histogram observe `amount`. `values` is the
1290
+ * packed `\u001f`-separated label values. Returns true on success.
1291
+ */
1292
+ metricsRecord(handle: number, series: number, values: Uint8Array, amount: number): boolean;
1293
+ /** Gauge assignment (`= value`). Returns true on success. */
1294
+ metricsGaugeSet(handle: number, series: number, values: Uint8Array, value: number): boolean;
1295
+ /**
1296
+ * Zero-encode sibling of {@link metricsRecord}: `values` is the JOINED
1297
+ * `\u001f`-separated JS string — the engine transcodes it in-engine.
1298
+ */
1299
+ metricsRecordStr(handle: number, series: number, values: string, amount: number): boolean;
1300
+ /** Zero-encode sibling of {@link metricsGaugeSet} (joined string values). */
1301
+ metricsGaugeSetStr(handle: number, series: number, values: string, value: number): boolean;
1302
+ /**
1303
+ * Render the Prometheus text format into `output`; returns bytes written
1304
+ * (`0` = null handle / error; `> output.length` = exact required size).
1305
+ */
1306
+ metricsRender(handle: number, output: Uint8Array): number;
1307
+ /**
1308
+ * Dump the packed v1 series snapshot into `output`; returns bytes written
1309
+ * (`0` = null handle / error; `> output.length` = exact required size).
1310
+ */
1311
+ metricsSnapshot(handle: number, output: Uint8Array): number;
1312
+ /** Destroy a registry handle (frees the native `Box<MetricsRegistry>`). */
1313
+ metricsDestroy(handle: number): void;
1314
+ /**
1315
+ * Record a BATCH of events in one crossing. Packed layout:
1316
+ * `[u32 n]{[u32 series][u32 valsLen][vals][f64 amount]}`; returns true when
1317
+ * all entries applied.
1318
+ */
1319
+ metricsRecordBatch(handle: number, packed: Uint8Array): boolean;
1320
+ /**
1321
+ * Fused wire-level validation: RAW query string → JSON → draft-07 validate
1322
+ * against the compiled schema (`inner` from `SchemaValidator.innerPtr()`).
1323
+ */
1324
+ queryValidate(inner: number, qs: string): boolean;
1325
+ /** Fused cookie-header variant of {@link queryValidate}. */
1326
+ cookieValidate(inner: number, header: string): boolean;
1327
+ /**
1328
+ * Seal a session envelope natively: builds
1329
+ * `{"id":"…","data":<dataJson>,"exp":exp}` and HMAC-signs it →
1330
+ * `payload.<hex>` token string (`null` = failure).
1331
+ */
1332
+ sessionSeal(id: string, dataJson: string, expSecs: number, secret: string): string | null;
1333
+ /**
1334
+ * Open a sealed session token: verify + extract in one crossing into
1335
+ * `output` as `[u8 ok][i64 exp][u32 idLen][id][u32 dataLen][dataJson]`.
1336
+ * Returns bytes written (`0` = bad signature / error; `> output.length` =
1337
+ * exact required size).
1338
+ */
1339
+ sessionOpen(token: string, secret: string, output: Uint8Array): number;
1340
+ /**
1341
+ * Byte-arg `{@link sessionSeal}`: same envelope + wire format, inputs as
1342
+ * bytes. Preferred for user-derived `id`/`dataJson`, because a `cstring` ARG
1343
+ * is engine-transcoded (a per-arg cost) and SILENTLY TRUNCATES at an
1344
+ * embedded `U+0000` before Rust sees it. `null` = failure.
1345
+ */
1346
+ sessionSealBytes(id: Uint8Array, dataJson: Uint8Array, expSecs: number, secret: Uint8Array): string | null;
1347
+ /**
1348
+ * Byte-arg `{@link sessionOpen}`: same output layout and needed-size
1349
+ * convention, `token`/`secret` as bytes (exact length, no transcode).
1350
+ */
1351
+ sessionOpenBytes(token: Uint8Array, secret: Uint8Array, output: Uint8Array): number;
1352
+ /**
1353
+ * Validate NEWLINE-separated lines as fixed-width hex into `output`
1354
+ * (one verdict byte per line); returns bytes written (`0` = bad width /
1355
+ * error; `> output.length` = exact required size).
1356
+ */
1357
+ hexValidateBatchInto(input: Uint8Array, width: number, output: Uint8Array): number;
1358
+ /**
1359
+ * String-input sibling of {@link hexValidateBatchInto}: the NEWLINE-
1360
+ * separated ids cross as a `cstring` ARG (engine-transcoded — no encode).
1361
+ */
1362
+ hexValidateBatchStr(ids: string, width: number, output: Uint8Array): number;
1363
+ /**
1364
+ * Escape JS-RegExp metacharacters into `output`; returns bytes written
1365
+ * (`0` = error; `> output.length` = exact required size).
1366
+ */
1367
+ regexEscapeInto(input: Uint8Array, output: Uint8Array): number;
1368
+ /**
1369
+ * Zero-copy text sibling of {@link regexEscapeInto}: cstring ARG in,
1370
+ * cstring return out (engine-cloned — zero JS decode). `null` = failure.
1371
+ */
1372
+ regexEscapeStr(input: string): string | null;
1373
+ /** Start the shared native task pool; 0 = started, 1 = already running. */
1374
+ taskInit(threads: number): number;
1375
+ /** Submit a task op with packed `args` under `taskId`; 1 = accepted. */
1376
+ taskSubmit(op: number, args: Uint8Array, taskId: number): number;
1377
+ /**
1378
+ * Zero-copy submit: the op writes its result DIRECTLY into `output` on the
1379
+ * pool thread. `output` must be a stable, caller-owned buffer that stays
1380
+ * alive (and untouched) until the completion is drained — the runtime holds
1381
+ * the reference. The completion body is the 8-byte LE written length.
1382
+ */
1383
+ taskSubmitOut(op: number, args: Uint8Array, taskId: number, output: Uint8Array): number;
1384
+ /**
1385
+ * Zero-copy INPUT: only `hdr` is copied; `data` is read in place by the pool
1386
+ * thread, so the caller must keep it alive (and untouched) until completion.
1387
+ */
1388
+ taskSubmitSlice(op: number, hdr: Uint8Array, data: Uint8Array, taskId: number): number;
1389
+ /**
1390
+ * Drain finished tasks into `output` (packed `[u32 count][entry…]`).
1391
+ * Returns bytes written, `0` when nothing is pending, or the EXACT required
1392
+ * size when `output` is too small (nothing is consumed in that case).
1393
+ */
1394
+ taskDrain(output: Uint8Array): number;
1395
+ /** Finished-but-undrained completion count. */
1396
+ taskPending(): number;
1397
+ /** Mark a task cancelled; 1 = newly marked, 0 = unknown/already finished. */
1398
+ taskCancel(taskId: number): number;
1399
+ /** Register the thread-safe doorbell trampoline address (`0` disables it). */
1400
+ taskSetDoorbell(ptr: number): number;
1401
+ /** Stop the task pool and drop pending completions. */
1402
+ taskShutdown(): number;
1403
+ /** Configured task-pool worker count (0 before the pool starts). */
1404
+ taskThreads(): number;
1405
+ }
1406
+ /** Unpacked rate-limit verdict (from the packed
1407
+ * `[u8 allowed][u32 remaining LE][i64 reset_ms LE]` C-ABI output). */
1408
+ export interface RateLimiterVerdict {
1409
+ allowed: boolean;
1410
+ remaining: number;
1411
+ resetMs: number;
1412
+ }
952
1413
  /**
953
1414
  * Create the runtime-appropriate ingress server: `Bun.serve` on Bun, the
954
1415
  * `node:http` adapter on Node. Selected ONCE at module load — the branch
@@ -1227,7 +1688,7 @@ export declare function readHandler(ingress: OptimizedIngressHandler, opts?: Bak
1227
1688
  export declare const deleteHandler: typeof readHandler;
1228
1689
  /**
1229
1690
  * Pre-baked echo handler: streams the request body back with the client's
1230
- * Content-Type, bounded by `maxBodyBytes`.
1691
+ * Content-Type (coerced to a non-active type), bounded by `maxBodyBytes`.
1231
1692
  */
1232
1693
  export declare function echoHandler(ingress: OptimizedIngressHandler, opts?: BakedHandlerOptions): (req: Request, srv?: unknown) => Promise<Response>;
1233
1694
  /**
@@ -1586,6 +2047,23 @@ export declare function createIngressSync(options?: IngressOptions): SyncIngress
1586
2047
  * @returns an async handler returning a snapshot context
1587
2048
  */
1588
2049
  export declare function createIngress(options?: IngressOptions): IngressHandler;
2050
+ /**
2051
+ * Synchronous ingress writers backed by castrum's process-lifetime FFI binding.
2052
+ * Handles MUST originate from addonPath and remain owned/alive while in use.
2053
+ * Writers throw on native failure; a return larger than output.byteLength is
2054
+ * the exact required capacity, not a completed write. No buffer is retained.
2055
+ */
2056
+ export type IngressBinding = Pick<BunFFI, "ingressHandleComponents" | "ingressHandlePacked" | "ingressLayout"> & {
2057
+ readonly addonPath: string;
2058
+ };
2059
+ /**
2060
+ * Share castrum's self-tested ingress writers without a second dlopen/map.
2061
+ * @returns The binding and actual binary path, or null when FFI is unavailable.
2062
+ * @example
2063
+ * const binding = getIngressBinding()
2064
+ * // Verify binding.addonPath matches the addon that owns your ingress handle.
2065
+ */
2066
+ export declare function getIngressBinding(): IngressBinding | null;
1589
2067
  /** Parse a query string (`a=1&b=2`) into an object via the native parser. */
1590
2068
  export declare function parseQueryString(query: string): Record<string, string | string[]>;
1591
2069
  /** Parse a cookie header (`a=1; b=2`) into an object via the native parser. */
@@ -2513,8 +2991,23 @@ export interface RustBatch {
2513
2991
  wsAcceptKey(items: Uint8Array[]): Uint8Array[];
2514
2992
  base64UrlEncode(items: Uint8Array[]): Uint8Array[];
2515
2993
  base64UrlDecode(items: Uint8Array[]): Uint8Array[];
2994
+ /**
2995
+ * Parallel password hashing. WARNING: the SAME `salt` is applied to every
2996
+ * item. Reusing one salt across real user passwords enables a single
2997
+ * precomputation to attack them all — generate a unique salt per password in
2998
+ * production (this batch form is for precomputed/benchmark workloads).
2999
+ */
2516
3000
  passwordHash(passwords: Uint8Array[], salt: Uint8Array, options?: PasswordHashOptions | null): Uint8Array[];
3001
+ /**
3002
+ * Parallel AEAD encrypt. `nonce` MUST be exactly 12 bytes and MUST be fresh
3003
+ * for every batch call with the same key: a unique per-item nonce is derived
3004
+ * by XOR-ing the item index into the low 8 bytes, so reusing a base nonce
3005
+ * across calls reuses keystream/tag material for colliding indices
3006
+ * (catastrophic for AES-GCM / ChaCha20-Poly1305). A non-12-byte nonce is
3007
+ * rejected with an error.
3008
+ */
2517
3009
  aeadEncrypt(key: Uint8Array, nonce: Uint8Array, items: Uint8Array[], algorithm?: string | null): Uint8Array[];
3010
+ /** Parallel AEAD decrypt. Pass the SAME base nonce and item order as the encrypt call. */
2518
3011
  aeadDecrypt(key: Uint8Array, nonce: Uint8Array, items: Uint8Array[], algorithm?: string | null): Uint8Array[];
2519
3012
  gzipCompress(items: Uint8Array[], level?: number | null): Uint8Array[];
2520
3013
  gzipDecompress(items: Uint8Array[], maxDecompressed?: number | null): Uint8Array[];
@@ -2781,7 +3274,10 @@ export interface RustScalar {
2781
3274
  createMediaTypeMatcher(expected: Uint8Array): MediaTypeMatcherInstance;
2782
3275
  /**
2783
3276
  * Sharded fixed-window per-key rate limiter. `maxEntries` clamps internally
2784
- * (default 1,048,576). Each instance owns an independent budget.
3277
+ * (default 1,048,576) and has an effective floor of 16,384
3278
+ * (`max_per_shard = max(maxEntries / 256, 64)` × 256 shards) — values below
3279
+ * that are silently raised, so don't rely on it to bound a tiny key space.
3280
+ * Each instance owns an independent budget.
2785
3281
  * @remarks Scalar per-check cost: ~3-14x SLOWER than the JS Map baseline —
2786
3282
  * prefer the ingress pipeline (one FFI for all stages) for native rate
2787
3283
  * limiting in a request path. [measured 2026-08]