castrum 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/castrum.darwin-arm64.node +0 -0
  4. package/castrum.darwin-x64.node +0 -0
  5. package/castrum.linux-arm64-gnu.node +0 -0
  6. package/castrum.linux-arm64-musl.node +0 -0
  7. package/castrum.linux-x64-gnu.node +0 -0
  8. package/castrum.linux-x64-musl.node +0 -0
  9. package/castrum.linux-x64-v3-gnu.node +0 -0
  10. package/castrum.win32-x64-msvc.node +0 -0
  11. package/dist/index.d.ts +664 -8
  12. package/dist/index.js +2014 -1327
  13. package/docs/BENCHMARKS.md +13 -0
  14. package/docs/CASE_STUDY.md +1 -1
  15. package/docs/ENVIRONMENT.md +8 -1
  16. package/docs/FFI_BUN_GUIDE.md +1 -1
  17. package/docs/INGRESS.md +4 -4
  18. package/docs/REPO_MAP.md +7 -5
  19. package/docs/RND-CONCURRENCY.md +20 -4
  20. package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
  21. package/docs/superpowers/plans/2026-09-18-elysia-learnings-adoption.md +634 -0
  22. package/index.ts +6 -0
  23. package/package.json +4 -2
  24. package/rust/crypto/aead.rs +44 -0
  25. package/rust/crypto/argon2.rs +32 -2
  26. package/rust/crypto/jwt/tests.rs +19 -0
  27. package/rust/crypto/jwt/token.rs +43 -28
  28. package/rust/crypto/session.rs +6 -0
  29. package/rust/ffi/crypto.rs +63 -32
  30. package/rust/ffi/hashing.rs +48 -33
  31. package/rust/ffi/http.rs +62 -36
  32. package/rust/ffi/metrics.rs +6 -2
  33. package/rust/ffi/payload.rs +47 -27
  34. package/rust/ffi/task.rs +55 -35
  35. package/rust/ffi/tests.rs +24 -0
  36. package/rust/ffi/util.rs +40 -0
  37. package/rust/http/headers.rs +13 -0
  38. package/rust/http/multipart.rs +139 -0
  39. package/rust/ingress/api.rs +21 -5
  40. package/rust/ingress/mod.rs +7 -0
  41. package/rust/ingress/native_route.rs +25 -5
  42. package/rust/ingress/schema_cache.rs +266 -0
  43. package/rust/task/completion.rs +7 -0
  44. package/rust/task/mod.rs +2 -1
  45. package/rust/task/runtime.rs +55 -7
  46. package/rust/task/tests.rs +128 -1
  47. package/src/ingress/abort.ts +48 -0
  48. package/src/ingress/body.ts +104 -8
  49. package/src/ingress/decode/baked-result.ts +13 -5
  50. package/src/ingress/decode/fast-result.ts +1 -1
  51. package/src/ingress/decode/result-base.ts +16 -2
  52. package/src/ingress/handlers.ts +104 -31
  53. package/src/ingress/headers/memoized-headers.ts +40 -0
  54. package/src/ingress/health.ts +22 -0
  55. package/src/ingress/index.ts +3 -1
  56. package/src/ingress/native-route.ts +25 -6
  57. package/src/ingress/options.ts +26 -2
  58. package/src/ingress/packing/gather-raw-headers.ts +24 -8
  59. package/src/ingress/response/baked-response.ts +118 -28
  60. package/src/ingress/router.ts +50 -3
  61. package/src/ingress/routes/common.ts +35 -11
  62. package/src/ingress/routes/echo.ts +60 -7
  63. package/src/ingress/routes/json-write.ts +32 -8
  64. package/src/ingress/routes/native.ts +24 -5
  65. package/src/ingress/routes/options.ts +6 -2
  66. package/src/ingress/routes/responder.ts +29 -9
  67. package/src/ingress/routes/safe-record.ts +57 -0
  68. package/src/ingress/server-error.ts +111 -0
  69. package/src/ingress/server-node.ts +135 -37
  70. package/src/ingress/server.ts +143 -36
  71. package/src/ingress/shared.ts +1 -1
  72. package/src/ingress/types.ts +15 -0
  73. package/src/integration/pipeline.ts +6 -0
  74. package/src/loader/ops.ts +1 -1
  75. package/src/native/ffi/build/codecs-selftest.ts +305 -0
  76. package/src/native/ffi/build/codecs.ts +0 -296
  77. package/src/native/ffi/build/instances-selftest.ts +264 -0
  78. package/src/native/ffi/build/instances.ts +10 -259
  79. package/src/native/ffi/build/parse-selftest.ts +238 -0
  80. package/src/native/ffi/build/parse.ts +11 -234
  81. package/src/native/ffi/build/task.ts +2 -1
  82. package/src/native/ffi/build.ts +5 -1
  83. package/src/native/ffi/selftest.ts +3 -3
  84. package/src/native/ffi/symbols.ts +460 -0
  85. package/src/native/ffi/types.ts +6 -1
  86. package/src/native/ffi.ts +2 -434
  87. package/src/native/ingress-binding.ts +39 -0
  88. package/src/native/types.ts +6 -0
  89. package/src/runtime/codec.ts +24 -0
  90. package/src/runtime/detect.ts +14 -0
  91. package/src/runtime/types.ts +11 -0
  92. package/src/rust-ffi/batch/build.ts +18 -8
  93. package/src/rust-ffi/batch/types.ts +15 -0
  94. package/src/rust-ffi/context.ts +46 -3
  95. package/src/rust-ffi/options.ts +19 -0
  96. package/src/rust-ffi/scalar/crypto.ts +8 -0
  97. package/src/rust-ffi/scalar/factories.ts +53 -20
  98. package/src/rust-ffi/scalar/http.ts +60 -10
  99. package/src/rust-ffi/scalar/interface.ts +18 -1
  100. package/src/rust-ffi/scalar/payload.ts +27 -1
  101. package/src/rust-ffi/text.ts +5 -4
  102. package/src/shared/buffer-pool.ts +54 -9
  103. package/src/shared/codec.ts +26 -0
  104. package/src/shared/memory.ts +68 -0
  105. package/src/shared/metrics.ts +86 -3
  106. package/src/shared/packed/parsers.ts +64 -7
  107. package/src/shared/runtime.ts +7 -0
  108. package/src/task/runtime.ts +81 -37
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
@@ -534,8 +553,45 @@ export interface PathMatch {
534
553
  * adapter (Bun's native router handles matching; the ingress pipeline does
535
554
  * not echo params in its response). */
536
555
  type RouteHandler = (req: Request, srv?: unknown, params?: Record<string, string>) => Response | Promise<Response>;
556
+ /**
557
+ * A prebuilt constant response for static route promotion.
558
+ *
559
+ * Either a bare `Response` or a factory that builds a FRESH `Response` per
560
+ * request. Behavior differs by runtime:
561
+ *
562
+ * - **Bun**: a bare `Response` is snapshotted into `Bun.serve`'s native route
563
+ * table at bind time (verified on Bun 1.4.2 — the body is NOT consumed by the
564
+ * first request) and served on every request without entering JS.
565
+ * - **Node** (`createIngressServerNode`): the adapter reads the bare `Response`
566
+ * body ONCE and materializes a fresh `Response` per request (a web body is
567
+ * one-shot).
568
+ *
569
+ * A factory is invoked by the runtime on every request, so it MUST return a new
570
+ * `Response` each call. Returning the SAME already-sent instance fails on the
571
+ * second request with `ERR_BODY_ALREADY_USED` (Bun) / a consumed stream
572
+ * (Node) — use a factory only when the response genuinely varies per request.
573
+ */
574
+ export type StaticRoute = Response | (() => Response);
537
575
  /** Route spec for the Bun.serve builder. */
538
576
  export interface BakedRoute {
577
+ /**
578
+ * A prebuilt constant response served for GET, bypassing the ingress
579
+ * pipeline entirely. Takes precedence over `read`/`cookies` for GET (the
580
+ * pipeline handler is never wired); other methods (`write`/`echo`/`delete`)
581
+ * are unaffected. See {@link StaticRoute} for the one-shot-body contract.
582
+ *
583
+ * HEAD is intentionally NOT wired for `static` (a bare `Response` carries a
584
+ * body; static responses are probes/constants served over GET). This matches
585
+ * the raw-`read` probe behavior — but note that a route which previously
586
+ * served HEAD via a native `read` now 405s (Node) on HEAD once `static` is
587
+ * set. When `static` is combined with `write`/`echo`/`delete`, those dynamic
588
+ * methods remain wired.
589
+ *
590
+ * Runtime note: `createIngressRouter` treats `static` differently — there,
591
+ * like `raw`, it OWNS THE WHOLE SPEC (the router has no compiled ingress for
592
+ * the other methods; see `RouterRouteSpec.static` in src/ingress/router.ts).
593
+ */
594
+ static?: StaticRoute;
539
595
  /**
540
596
  * Wires GET + HEAD read handlers. Accepts an optimized ingress handler OR a
541
597
  * raw `RouteHandler` function (health/metrics probes are raw — see
@@ -612,14 +668,20 @@ export interface CreateIngressServerOptions {
612
668
  routes: Record<string, BakedRoute>;
613
669
  fallback?: OptimizedIngressHandler;
614
670
  /**
615
- * Invoked when an unhandled error escapes a request handler (currently wired
616
- * on the Node adapter; the Bun path uses runtime.onResponse). Never throws —
617
- * hook failures are swallowed.
671
+ * Invoked when an unhandled error escapes a request handler (raw `read`
672
+ * functions, `responder`/`native` responders, or the Bun/Node adapter
673
+ * catch-all). The error is masked to a generic 500 on the wire. Never
674
+ * throws — hook failures are swallowed.
618
675
  */
619
676
  onError?: (info: {
620
677
  error: Error;
621
678
  request?: Request;
622
679
  }) => void;
680
+ /**
681
+ * Optional structured-log sink for escaped handler errors (one JSON line per
682
+ * masked 500, message only). Never throws.
683
+ */
684
+ logger?: (line: string) => void;
623
685
  /** Node adapter only: ms to receive the complete request (headers + body).
624
686
  * Guards slowloris/trickling requests. Default: 30_000. */
625
687
  requestTimeoutMs?: number;
@@ -949,6 +1011,453 @@ export interface Argon2HasherInstance {
949
1011
  hash(password: Uint8Array, salt: Uint8Array): Uint8Array;
950
1012
  verify(password: Uint8Array, phc: Uint8Array): boolean;
951
1013
  }
1014
+ /**
1015
+ * The bound bun:ffi C-ABI surface. Each member is a dlopen'd `castrum_*`
1016
+ * symbol wrapped so inputs are `(ptr,len)` byte slices and text outputs are
1017
+ * returned as JS strings via JSC's native transfer (cstring clone) — zero
1018
+ * `TextEncoder`/`TextDecoder` on the Bun path. `*Into` variants write into a
1019
+ * caller buffer and return the byte count; variable-size ops return the exact
1020
+ * required size on a too-small buffer so the transport grows once and retries
1021
+ * exactly once.
1022
+ */
1023
+ export interface BunFFI {
1024
+ crc32(input: Uint8Array): number;
1025
+ fnv1a64(input: Uint8Array): bigint;
1026
+ xxh3(input: Uint8Array): bigint;
1027
+ jsonValid(input: Uint8Array): boolean;
1028
+ /** UTF-8 validity probe (native replacement for a fatal `TextDecoder`). */
1029
+ utf8Valid(input: Uint8Array): boolean;
1030
+ /** Lowercase-hex encode → string (native transfer, zero encode). */
1031
+ hexEncode(input: Uint8Array): string;
1032
+ /** Lowercase-hex encode into `output`; returns bytes written. */
1033
+ hexEncodeInto(input: Uint8Array, output: Uint8Array): number;
1034
+ /** RFC 3986 percent-encode → string. */
1035
+ urlEncode(input: Uint8Array): string;
1036
+ /** RFC 3986 percent-encode into `output`; returns bytes written. */
1037
+ urlEncodeInto(input: Uint8Array, output: Uint8Array): number;
1038
+ validateEmail(input: string): boolean;
1039
+ validateUuid(input: string): boolean;
1040
+ validateIpv4(input: string): boolean;
1041
+ validateIpv6(input: string): boolean;
1042
+ /** Byte-input sibling of {@link validateEmail} (`(ptr,len)` — no decode). */
1043
+ validateEmailBytes(input: Uint8Array): boolean;
1044
+ /** Byte-input sibling of {@link validateUuid} (`(ptr,len)` — no decode). */
1045
+ validateUuidBytes(input: Uint8Array): boolean;
1046
+ /** Byte-input sibling of {@link validateIpv4} (`(ptr,len)` — no decode). */
1047
+ validateIpv4Bytes(input: Uint8Array): boolean;
1048
+ /** Byte-input sibling of {@link validateIpv6} (`(ptr,len)` — no decode). */
1049
+ validateIpv6Bytes(input: Uint8Array): boolean;
1050
+ /** Sum of `id` fields across a JSON array → bigint (throws on non-array input). */
1051
+ jsonSumIds(input: Uint8Array): bigint;
1052
+ hmacSha256Verify(key: Uint8Array, data: Uint8Array, signature: Uint8Array): boolean;
1053
+ csrfVerify(token: Uint8Array, secret: Uint8Array): boolean;
1054
+ passwordVerify(password: Uint8Array, phc: Uint8Array): boolean;
1055
+ /** `phc` is a `cstring` ARG (the engine transcodes the JS PHC string in-engine). */
1056
+ passwordVerifyBcrypt(password: Uint8Array, phc: string): boolean;
1057
+ /** Hex-decode into a fresh buffer (size `input.length / 2`). */
1058
+ hexDecode(input: Uint8Array): Uint8Array;
1059
+ /** Hex-decode into `output`; returns bytes written (throws if too small). */
1060
+ hexDecodeInto(input: Uint8Array, output: Uint8Array): number;
1061
+ /** Percent-decode into a fresh buffer (size `input.length`). */
1062
+ urlDecode(input: Uint8Array): Uint8Array;
1063
+ /** Percent-decode into `output`; returns bytes written (throws if too small). */
1064
+ urlDecodeInto(input: Uint8Array, output: Uint8Array): number;
1065
+ /** base64-decode into a fresh buffer (size `ceil(len/4)*3`). */
1066
+ base64Decode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array;
1067
+ /** base64-decode into `output`; returns bytes written (throws if too small). */
1068
+ base64DecodeInto(input: Uint8Array, output: Uint8Array, urlSafe?: boolean, padding?: boolean): number;
1069
+ /** RFC 6455 Sec-WebSocket-Accept → string (cstring return; `key` is a `cstring` ARG). */
1070
+ wsAcceptKey(key: string): string;
1071
+ /** RFC 6455 Sec-WebSocket-Accept into `output`; returns bytes written. */
1072
+ wsAcceptKeyInto(key: Uint8Array, output: Uint8Array): number;
1073
+ /** crc32 ETag → string (10 strong / 12 weak chars, cstring return). */
1074
+ etag(data: Uint8Array, weak?: boolean): string;
1075
+ /** crc32 ETag into `output`; returns bytes written. */
1076
+ etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number;
1077
+ /** Evaluate a precompiled `ConditionalRequest` (opaque `inner` handle from its
1078
+ * napi `inner_ptr()`) — true → 304. `null` header args = absent (flags encode
1079
+ * presence so a present-but-empty header is distinct). A null (0) handle → false. */
1080
+ conditionalIsNotModified(inner: number, ifNoneMatch: Uint8Array | null, ifModifiedSince: Uint8Array | null): boolean;
1081
+ /** Precompiled `MediaTypeMatcher` wildcard match (opaque `inner` handle). */
1082
+ mediaTypeMatcherMatches(inner: number, actual: Uint8Array): boolean;
1083
+ /** Precompiled `AcceptNegotiator` → best supported encoding (opaque `inner`
1084
+ * handle); `null` = identity. */
1085
+ acceptNegotiatorNegotiate(inner: number, header: Uint8Array): string | null;
1086
+ /** Precompiled `AcceptNegotiator` → best supported encoding with SERVER-
1087
+ * preference tie-breaking (RFC 7231 server semantics, opaque `inner` handle);
1088
+ * `null` = identity. */
1089
+ acceptNegotiatorNegotiateServer(inner: number, header: Uint8Array): string | null;
1090
+ /** Precompiled `JwtSigner` → compact token from pre-serialized claim JSON
1091
+ * (opaque `inner` handle); throws on invalid claims / null handle. */
1092
+ jwtSignerSign(inner: number, claimsJson: Uint8Array, nowSeconds: number): Uint8Array;
1093
+ /** Precompiled `JwtSigner` → claims JSON bytes (opaque `inner` handle);
1094
+ * `null` = invalid signature / expired / malformed. */
1095
+ jwtSignerVerify(inner: number, token: Uint8Array, nowSeconds: number): Uint8Array | null;
1096
+ /** Ed25519 keypair generation → `{ privateKey, publicKey }` (PKCS#8 v1 DER
1097
+ * private + SPKI DER public bytes); throws on CSPRNG failure. */
1098
+ ed25519GenerateKeypair(): {
1099
+ privateKey: Uint8Array;
1100
+ publicKey: Uint8Array;
1101
+ };
1102
+ /** Ed25519 sign → 64-byte signature; throws on invalid private key. */
1103
+ ed25519Sign(msg: Uint8Array, privateKey: Uint8Array): Uint8Array;
1104
+ /** Ed25519 verify → whether `signature` is valid for `msg` under `publicKey`. */
1105
+ ed25519Verify(msg: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
1106
+ /** EdDSA (Ed25519) JWT sign → compact token; `null` = invalid claims/private
1107
+ * key. `ttl <= 0` = no `iat`/`exp` injection. */
1108
+ jwtEdDSASign(claimsJson: Uint8Array, privateKey: Uint8Array, ttl: number, nowSeconds: number): string | null;
1109
+ /** EdDSA (Ed25519) JWT verify → claims JSON string; `null` = invalid. */
1110
+ jwtEdDSAVerify(token: Uint8Array, publicKey: Uint8Array, nowSeconds: number): string | null;
1111
+ /** Compiled `TemplateRenderer` → UTF-8 bytes from a pre-serialized JSON
1112
+ * context (opaque `inner` handle); throws on invalid context / render error. */
1113
+ templateRender(inner: number, contextJson: Uint8Array): Uint8Array;
1114
+ /** Compiled `SchemaValidator` → whether a document is valid (opaque `inner` handle). */
1115
+ schemaValidatorValidate(inner: number, doc: Uint8Array): boolean;
1116
+ /** `RateLimiter` string-key check → { allowed, remaining, resetMs } (opaque
1117
+ * `inner` handle; `key` is a `cstring` ARG); throws on a null handle. */
1118
+ rateLimiterCheck(inner: number, key: string, nowMs: number): RateLimiterVerdict;
1119
+ /** `RateLimiter` pre-hashed key check → { allowed, remaining, resetMs }. */
1120
+ rateLimiterCheckKey(inner: number, key: number, nowMs: number): RateLimiterVerdict;
1121
+ /** `byteLen` random bytes → `byteLen * 2` hex-char string (cstring return). */
1122
+ randomToken(byteLen: number): string;
1123
+ /** base64-encode → string. */
1124
+ base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): string;
1125
+ /** base64-encode into `output`; returns bytes written. */
1126
+ base64EncodeInto(input: Uint8Array, output: Uint8Array, urlSafe?: boolean, padding?: boolean): number;
1127
+ /** HMAC-SHA256 → 64-char lowercase-hex string. */
1128
+ hmacSha256(key: Uint8Array, data: Uint8Array): string;
1129
+ /** HMAC-SHA256 hex into `output`; returns bytes written (throws if too small). */
1130
+ hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number;
1131
+ /** Sign cookie `value` as `value.<64-hex>` → string (cstring return). */
1132
+ signCookie(value: Uint8Array, secret: Uint8Array): string;
1133
+ /** Sign cookie into `output`; returns bytes written (throws if too small). */
1134
+ signCookieInto(value: Uint8Array, secret: Uint8Array, output: Uint8Array): number;
1135
+ /** Verify a signed cookie → value string, or `null` on bad signature. */
1136
+ verifyCookie(signed: Uint8Array, secret: Uint8Array): string | null;
1137
+ /** Verify a signed cookie into `output`; returns value length or `null` on bad signature. */
1138
+ verifyCookieInto(signed: Uint8Array, secret: Uint8Array, output: Uint8Array): number | null;
1139
+ /** CSRF token (129 chars: 64 rnd-hex + '.' + 64 sig-hex) → string. */
1140
+ csrfToken(secret: Uint8Array): string;
1141
+ /** CSRF token into `output`; returns bytes written (throws if too small). */
1142
+ csrfTokenInto(secret: Uint8Array, output: Uint8Array): number;
1143
+ /** Argon2id PHC hash → string (m/t/p/out_len params). */
1144
+ passwordHash(password: Uint8Array, salt: Uint8Array, mCost: number, tCost: number, pCost: number, outLen: number): string;
1145
+ /** bcrypt `$2b$` PHC → string (cost clamped 4..=31). */
1146
+ passwordHashBcrypt(password: Uint8Array, cost: number): string;
1147
+ /** PBKDF2-HMAC-SHA256 → `dkLen` bytes (rounds, dkLen clamped). */
1148
+ pbkdf2Sha256(password: Uint8Array, salt: Uint8Array, rounds: number, dkLen: number): Uint8Array;
1149
+ /** AEAD encrypt (0 = AES-256-GCM, 1 = ChaCha20-Poly1305) → ct+tag. */
1150
+ aeadEncrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, algorithm?: number): Uint8Array;
1151
+ /** AEAD encrypt into `output` (ct + 16-byte tag); returns bytes written. */
1152
+ aeadEncryptInto(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, output: Uint8Array, algorithm?: number): number;
1153
+ /** AEAD decrypt → plaintext, or `null` on auth failure. */
1154
+ aeadDecrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, algorithm?: number): Uint8Array | null;
1155
+ /** RFC 6455 frame encode into a fresh buffer. */
1156
+ wsFrameEncode(opcode: number, payload: Uint8Array, mask: boolean, fin: boolean): Uint8Array;
1157
+ /** RFC 6455 frame encode into `output`; returns bytes written (throws if too small). */
1158
+ wsFrameEncodeInto(opcode: number, payload: Uint8Array, mask: boolean, fin: boolean, output: Uint8Array): number;
1159
+ /** RFC 6902 JSON patch → JSON text string. */
1160
+ jsonPatch(doc: Uint8Array, patch: Uint8Array): string;
1161
+ /** gzip-compress into a fresh buffer (level clamped 0..=9, default 6). */
1162
+ gzipCompress(data: Uint8Array, level?: number): Uint8Array;
1163
+ /** gzip-compress into `output`; returns bytes written (throws if too small). */
1164
+ gzipCompressInto(data: Uint8Array, output: Uint8Array, level?: number): number;
1165
+ /** gzip-decompress into a fresh buffer (capped by `maxDecompressed`). */
1166
+ gzipDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array;
1167
+ /** gzip-decompress into `output`; returns bytes written (throws if too small). */
1168
+ gzipDecompressInto(data: Uint8Array, output: Uint8Array, maxDecompressed?: number): number;
1169
+ /**
1170
+ * gzip ISIZE trailer probe → the uncompressed size for a single-member gzip
1171
+ * stream, or `0` when unknown (invalid stream / multi-member). Used to
1172
+ * PRE-SIZE a destination buffer for the zero-copy task path.
1173
+ */
1174
+ gzipIsize(data: Uint8Array): number;
1175
+ /** brotli-compress into a fresh buffer (quality clamped 0..=11, default 5). */
1176
+ brotliCompress(data: Uint8Array, quality?: number): Uint8Array;
1177
+ /** brotli-compress into `output`; returns bytes written (throws if too small). */
1178
+ brotliCompressInto(data: Uint8Array, output: Uint8Array, quality?: number): number;
1179
+ /** brotli-decompress into a fresh buffer (capped by `maxDecompressed`). */
1180
+ brotliDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array;
1181
+ /** brotli-decompress into `output`; returns bytes written (throws if too small). */
1182
+ brotliDecompressInto(data: Uint8Array, output: Uint8Array, maxDecompressed?: number): number;
1183
+ /** HTTP request parse → packed output into `output`; returns bytes written. */
1184
+ httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number;
1185
+ /** Query string parse → packed output into `output`; returns bytes written. */
1186
+ queryParsePackedInto(input: Uint8Array, output: Uint8Array): number;
1187
+ /** Cookie header parse → packed output into `output`; returns bytes written. */
1188
+ cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number;
1189
+ /** Sign JWT (HS256) from pre-serialized claim JSON (ttl<=0 → no iat/exp) → string. */
1190
+ jwtSignBytes(claimsJson: Uint8Array, secret: Uint8Array, ttl: number, now: number): string;
1191
+ /** Sign JWT into `output`; returns bytes written (throws if too small). */
1192
+ jwtSignBytesInto(claimsJson: Uint8Array, secret: Uint8Array, ttl: number, now: number, output: Uint8Array): number;
1193
+ /** Decode a WS frame into packed `[flags][opcode][u32 len][payload]`; null on malformed. */
1194
+ wsFrameDecodePacked(data: Uint8Array): Uint8Array | null;
1195
+ /**
1196
+ * Decode a WS frame into `output` (sized ≥ `data.length + 6`); returns bytes
1197
+ * written, null on malformed. Pooled sibling of `wsFrameDecodePacked`.
1198
+ */
1199
+ wsFrameDecodePackedInto(data: Uint8Array, output: Uint8Array): number | null;
1200
+ /** Parse multipart/form-data into the packed parts layout. */
1201
+ multipartParsePacked(body: Uint8Array, boundary: Uint8Array): Uint8Array;
1202
+ /** Parse multipart/form-data into `output`; returns bytes written (throws if too small). */
1203
+ multipartParsePackedInto(body: Uint8Array, boundary: Uint8Array, output: Uint8Array): number;
1204
+ /** Parse x-www-form-urlencoded into packed pairs into `output`. */
1205
+ formParsePackedInto(input: Uint8Array, output: Uint8Array): number;
1206
+ /**
1207
+ * Verify an HS256 JWT → claims JSON text (cstring return; `null` on invalid
1208
+ * signature / expired / malformed). `nowSeconds` is an i64 C arg.
1209
+ */
1210
+ jwtVerify(token: Uint8Array, secret: Uint8Array, nowSeconds: number): string | null;
1211
+ /**
1212
+ * Parse JSON → packed token stream bytes (structural, no re-parse). Layout:
1213
+ * `[u32 strCount]{[u32 len][utf8]}... [u32 treeLen][tree]` — a deduplicated
1214
+ * string table followed by a start/end-marker value tree:
1215
+ * `0=null 1=false 2=true 3=number(f64 LE) 4=string(u32 table idx)
1216
+ * 5=array start 6=object start 7=array end 8=object end
1217
+ * 9=object key(u32 table idx)` (object body `6,(9,keyIdx,value)*,8`).
1218
+ * Throws on invalid JSON / too-small output. The scalar wrapper decodes the
1219
+ * table once and assembles the value — no second parse.
1220
+ */
1221
+ jsonParsePacked(input: Uint8Array): Uint8Array;
1222
+ /**
1223
+ * Parse a Content-Type header → packed verdict bytes:
1224
+ * `[u32 mediaTypeLen][mediaType][u32 charsetLen (0xFFFFFFFF = none)][charset]
1225
+ * [u32 boundaryLen][boundary][u32 paramCount]{[u32 keyLen][key][u32 valLen][val]}`.
1226
+ * Throws on invalid media type / too-small output.
1227
+ */
1228
+ parseMediaType(input: Uint8Array): Uint8Array;
1229
+ /** Parse an IMF-fixdate → unix seconds, or `null` on malformed (packed
1230
+ * `[u8 ok][i64 secs LE]` output, decoded here). */
1231
+ parseHttpDate(input: Uint8Array): bigint | null;
1232
+ /**
1233
+ * Parse an Accept-Encoding header → packed verdict bytes:
1234
+ * `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}` (empty header → count 0).
1235
+ * Throws on too-small output.
1236
+ */
1237
+ parseAcceptEncoding(input: Uint8Array): Uint8Array;
1238
+ /** Percent-encode a query from packed pairs (the `packPairs` layout) → string,
1239
+ * keys SORTED (matches napi `BTreeMap` ordering); `null` on malformed input. */
1240
+ urlEncodeQuery(input: Uint8Array): string | null;
1241
+ /** RFC 3986 URL resolution (base + reference) → string; `null` on non-UTF-8. */
1242
+ urlResolve(base: Uint8Array, reference: Uint8Array): string | null;
1243
+ /** Resolve a reference against a `UrlBuilder`'s PRECOMPILED base (opaque
1244
+ * `inner` handle from its napi `inner_ptr()`) → bytes; throws on a null
1245
+ * handle / non-UTF-8 reference / too-small output. */
1246
+ urlBuilderResolve(inner: number, reference: Uint8Array): Uint8Array;
1247
+ /** Extension → MIME type → string (unknown → `application/octet-stream`; `ext` is a `cstring` ARG). */
1248
+ mimeFromExtension(ext: string): string | null;
1249
+ /**
1250
+ * 29-byte HTTP-date (`Sun, 06 Nov 1994 08:49:37 GMT`) into `output`; returns
1251
+ * bytes written (throws on too-small / out-of-range year). FFI sibling of the
1252
+ * napi `httpDateInto` — kills the napi crossing on the hot path.
1253
+ */
1254
+ httpDateInto(secs: number, output: Uint8Array): number;
1255
+ /** 29-byte HTTP-date → string (allocating sibling of httpDateInto). */
1256
+ httpDate(secs?: number): string;
1257
+ /**
1258
+ * Encode one SSE event → bytes (fresh buffer). event/id null = line omitted
1259
+ * (a present-but-empty string emits the line, matching napi `Option`).
1260
+ * NOTE: event/id stay `(ptr,len)` byte args — a cstring-ARG conversion was
1261
+ * attempted and reverted (2026-08-23); see docs/FFI_BUN_GUIDE.md §14 before
1262
+ * touching this signature.
1263
+ * FFI sibling of the napi `sse_encode_event`.
1264
+ */
1265
+ sseEncodeEvent(event: string | null, data: Uint8Array, id: string | null, retry: number | null): Uint8Array;
1266
+ /**
1267
+ * Encode one SSE event into `output` (sized ≥ `data.length + 64`); returns
1268
+ * bytes written (throws if too small / invalid UTF-8). Pooled sibling of
1269
+ * `sseEncodeEvent` — removes the per-call output-buffer alloc.
1270
+ */
1271
+ sseEncodeEventInto(event: string | null, data: Uint8Array, id: string | null, retry: number | null, output: Uint8Array): number;
1272
+ /** Random hex token into `output`; returns bytes written (throws if too small). */
1273
+ randomTokenInto(byteLen: number, output: Uint8Array): number;
1274
+ /**
1275
+ * Run the ingress pipeline on a packed frame via the opaque inner handle from
1276
+ * `Ingress.ingressInnerPtr()` (valid only while the instance is alive — the
1277
+ * caller must hold it). Returns bytes written; throws on error / too-small.
1278
+ */
1279
+ ingressHandlePacked(inner: number, input: Uint8Array, body: Uint8Array | null, output: Uint8Array): number;
1280
+ /**
1281
+ * Run the ingress pipeline from raw request components via the opaque inner
1282
+ * handle (valid only while the instance is alive). `url`/`ip` are JS strings
1283
+ * passed as `bun:ffi` `cstring` ARGs — the engine transcodes them to
1284
+ * call-scoped NUL-terminated UTF-8 buffers in-engine, so the JS side skips
1285
+ * the frame assembly + `Buffer.write` encode for the URL/IP. `rid`/`headers`
1286
+ * are the packed byte slices; `body` is the request body (or null); `output`
1287
+ * receives the packed decision. Same wire format as {@link ingressHandlePacked}.
1288
+ * Returns bytes written; throws on error / too-small.
1289
+ */
1290
+ ingressHandleComponents(inner: number, methodKind: number, url: string, ip: string, rid: Uint8Array, headers: Uint8Array, body: Uint8Array | null, output: Uint8Array): number;
1291
+ /**
1292
+ * Write the ingress binary-layout constants (38 × u32 LE — `rust/ffi.rs`
1293
+ * `IngressLayout`, numeric source `rust/ingress/output.rs`) into `output`;
1294
+ * returns bytes written. Lets `src/ingress/constants.ts` read the layout via
1295
+ * bun:ffi on Bun so importing the package does NOT dlopen the napi addon.
1296
+ */
1297
+ ingressLayout(out: Uint8Array): number;
1298
+ /**
1299
+ * Compile a route descriptor (the `@ignex/native` route-wire v3 format:
1300
+ * magic `ROUT`, version 3, limits, stage tags, draft-07 body schema) into an
1301
+ * opaque handle. Returns the handle (`0` = invalid descriptor / compile
1302
+ * failure / panic — throws). The handle is owned by the caller and must be
1303
+ * released with {@link routeDestroy}.
1304
+ */
1305
+ routeCompile(descriptor: Uint8Array): number;
1306
+ /**
1307
+ * Run a compiled route on one request frame, writing the packed verdict
1308
+ * (`[flags u32][errorCode u32]` + optional query/cookie pair sections) into
1309
+ * `output`. Returns bytes written (`0` = real error — throws; `> output.length`
1310
+ * = the EXACT required size — the caller allocates once and retries, per the
1311
+ * growExact convention).
1312
+ */
1313
+ routeRun(handle: number, frame: Uint8Array, output: Uint8Array): number;
1314
+ /** Destroy a compiled route handle (frees the native `Box<NativeRoute>`). */
1315
+ routeDestroy(handle: number): void;
1316
+ /** Create an empty metrics registry → a caller-owned opaque handle. */
1317
+ metricsCreate(): number;
1318
+ /**
1319
+ * Declare a counter family → its `u32` series id (`0xFFFFFFFF` = error).
1320
+ * `name` and `labelKeys` are `cstring` ARGs; label keys are
1321
+ * `\u001f`-separated (empty string = no labels).
1322
+ */
1323
+ metricsCounter(handle: number, name: string, labelKeys: string): number;
1324
+ /** Declare a gauge family → its series id. Same ABI as {@link metricsCounter}. */
1325
+ metricsGauge(handle: number, name: string, labelKeys: string): number;
1326
+ /**
1327
+ * Declare a histogram family → its series id. `bucketsCsv` is a
1328
+ * comma-separated finite positive list (empty string = default buckets).
1329
+ */
1330
+ metricsHistogram(handle: number, name: string, labelKeys: string, bucketsCsv: string): number;
1331
+ /**
1332
+ * Counter/gauge `+= amount`, or histogram observe `amount`. `values` is the
1333
+ * packed `\u001f`-separated label values. Returns true on success.
1334
+ */
1335
+ metricsRecord(handle: number, series: number, values: Uint8Array, amount: number): boolean;
1336
+ /** Gauge assignment (`= value`). Returns true on success. */
1337
+ metricsGaugeSet(handle: number, series: number, values: Uint8Array, value: number): boolean;
1338
+ /**
1339
+ * Zero-encode sibling of {@link metricsRecord}: `values` is the JOINED
1340
+ * `\u001f`-separated JS string — the engine transcodes it in-engine.
1341
+ */
1342
+ metricsRecordStr(handle: number, series: number, values: string, amount: number): boolean;
1343
+ /** Zero-encode sibling of {@link metricsGaugeSet} (joined string values). */
1344
+ metricsGaugeSetStr(handle: number, series: number, values: string, value: number): boolean;
1345
+ /**
1346
+ * Render the Prometheus text format into `output`; returns bytes written
1347
+ * (`0` = null handle / error; `> output.length` = exact required size).
1348
+ */
1349
+ metricsRender(handle: number, output: Uint8Array): number;
1350
+ /**
1351
+ * Dump the packed v1 series snapshot into `output`; returns bytes written
1352
+ * (`0` = null handle / error; `> output.length` = exact required size).
1353
+ */
1354
+ metricsSnapshot(handle: number, output: Uint8Array): number;
1355
+ /** Destroy a registry handle (frees the native `Box<MetricsRegistry>`). */
1356
+ metricsDestroy(handle: number): void;
1357
+ /**
1358
+ * Record a BATCH of events in one crossing. Packed layout:
1359
+ * `[u32 n]{[u32 series][u32 valsLen][vals][f64 amount]}`; returns true when
1360
+ * all entries applied.
1361
+ */
1362
+ metricsRecordBatch(handle: number, packed: Uint8Array): boolean;
1363
+ /**
1364
+ * Fused wire-level validation: RAW query string → JSON → draft-07 validate
1365
+ * against the compiled schema (`inner` from `SchemaValidator.innerPtr()`).
1366
+ */
1367
+ queryValidate(inner: number, qs: string): boolean;
1368
+ /** Fused cookie-header variant of {@link queryValidate}. */
1369
+ cookieValidate(inner: number, header: string): boolean;
1370
+ /**
1371
+ * Seal a session envelope natively: builds
1372
+ * `{"id":"…","data":<dataJson>,"exp":exp}` and HMAC-signs it →
1373
+ * `payload.<hex>` token string (`null` = failure).
1374
+ */
1375
+ sessionSeal(id: string, dataJson: string, expSecs: number, secret: string): string | null;
1376
+ /**
1377
+ * Open a sealed session token: verify + extract in one crossing into
1378
+ * `output` as `[u8 ok][i64 exp][u32 idLen][id][u32 dataLen][dataJson]`.
1379
+ * Returns bytes written (`0` = bad signature / error; `> output.length` =
1380
+ * exact required size).
1381
+ */
1382
+ sessionOpen(token: string, secret: string, output: Uint8Array): number;
1383
+ /**
1384
+ * Byte-arg `{@link sessionSeal}`: same envelope + wire format, inputs as
1385
+ * bytes. Preferred for user-derived `id`/`dataJson`, because a `cstring` ARG
1386
+ * is engine-transcoded (a per-arg cost) and SILENTLY TRUNCATES at an
1387
+ * embedded `U+0000` before Rust sees it. `null` = failure.
1388
+ */
1389
+ sessionSealBytes(id: Uint8Array, dataJson: Uint8Array, expSecs: number, secret: Uint8Array): string | null;
1390
+ /**
1391
+ * Byte-arg `{@link sessionOpen}`: same output layout and needed-size
1392
+ * convention, `token`/`secret` as bytes (exact length, no transcode).
1393
+ */
1394
+ sessionOpenBytes(token: Uint8Array, secret: Uint8Array, output: Uint8Array): number;
1395
+ /**
1396
+ * Validate NEWLINE-separated lines as fixed-width hex into `output`
1397
+ * (one verdict byte per line); returns bytes written (`0` = bad width /
1398
+ * error; `> output.length` = exact required size).
1399
+ */
1400
+ hexValidateBatchInto(input: Uint8Array, width: number, output: Uint8Array): number;
1401
+ /**
1402
+ * String-input sibling of {@link hexValidateBatchInto}: the NEWLINE-
1403
+ * separated ids cross as a `cstring` ARG (engine-transcoded — no encode).
1404
+ */
1405
+ hexValidateBatchStr(ids: string, width: number, output: Uint8Array): number;
1406
+ /**
1407
+ * Escape JS-RegExp metacharacters into `output`; returns bytes written
1408
+ * (`0` = error; `> output.length` = exact required size).
1409
+ */
1410
+ regexEscapeInto(input: Uint8Array, output: Uint8Array): number;
1411
+ /**
1412
+ * Zero-copy text sibling of {@link regexEscapeInto}: cstring ARG in,
1413
+ * cstring return out (engine-cloned — zero JS decode). `null` = failure.
1414
+ */
1415
+ regexEscapeStr(input: string): string | null;
1416
+ /** Start the shared native task pool; 0 = started, 1 = already running. */
1417
+ taskInit(threads: number): number;
1418
+ /**
1419
+ * Submit a task op with packed `args` under `taskId`. Returns `1` accepted,
1420
+ * `2` overloaded (bounded queue full), `0` invalid args.
1421
+ */
1422
+ taskSubmit(op: number, args: Uint8Array, taskId: number): number;
1423
+ /**
1424
+ * Zero-copy submit: the op writes its result DIRECTLY into `output` on the
1425
+ * pool thread. `output` must be a stable, caller-owned buffer that stays
1426
+ * alive (and untouched) until the completion is drained — the runtime holds
1427
+ * the reference. The completion body is the 8-byte LE written length.
1428
+ * Returns `1` accepted, `2` overloaded, `0` invalid args.
1429
+ */
1430
+ taskSubmitOut(op: number, args: Uint8Array, taskId: number, output: Uint8Array): number;
1431
+ /**
1432
+ * Zero-copy INPUT: only `hdr` is copied; `data` is read in place by the pool
1433
+ * thread, so the caller must keep it alive (and untouched) until completion.
1434
+ * Returns `1` accepted, `2` overloaded, `0` invalid args.
1435
+ */
1436
+ taskSubmitSlice(op: number, hdr: Uint8Array, data: Uint8Array, taskId: number): number;
1437
+ /**
1438
+ * Drain finished tasks into `output` (packed `[u32 count][entry…]`).
1439
+ * Returns bytes written, `0` when nothing is pending, or the EXACT required
1440
+ * size when `output` is too small (nothing is consumed in that case).
1441
+ */
1442
+ taskDrain(output: Uint8Array): number;
1443
+ /** Finished-but-undrained completion count. */
1444
+ taskPending(): number;
1445
+ /** Mark a task cancelled; 1 = newly marked, 0 = unknown/already finished. */
1446
+ taskCancel(taskId: number): number;
1447
+ /** Register the thread-safe doorbell trampoline address (`0` disables it). */
1448
+ taskSetDoorbell(ptr: number): number;
1449
+ /** Stop the task pool and drop pending completions. */
1450
+ taskShutdown(): number;
1451
+ /** Configured task-pool worker count (0 before the pool starts). */
1452
+ taskThreads(): number;
1453
+ }
1454
+ /** Unpacked rate-limit verdict (from the packed
1455
+ * `[u8 allowed][u32 remaining LE][i64 reset_ms LE]` C-ABI output). */
1456
+ export interface RateLimiterVerdict {
1457
+ allowed: boolean;
1458
+ remaining: number;
1459
+ resetMs: number;
1460
+ }
952
1461
  /**
953
1462
  * Create the runtime-appropriate ingress server: `Bun.serve` on Bun, the
954
1463
  * `node:http` adapter on Node. Selected ONCE at module load — the branch
@@ -987,6 +1496,41 @@ export declare function createTraceId(): string;
987
1496
  export declare function createSpanId(): string;
988
1497
  /** Serialize a trace context back to a `traceparent` header value. */
989
1498
  export declare function serializeTraceParent(ctx: TraceContext): string;
1499
+ /**
1500
+ * The `err.code` value a body read carries when it is rejected because the
1501
+ * client disconnected (`Request.signal` aborted).
1502
+ */
1503
+ export declare const ABORT_CODE = "REQUEST_ABORTED";
1504
+ /**
1505
+ * Whether a thrown value is the cancellation error produced when `req.signal`
1506
+ * aborts during a body read (see `readBodyWithLimit` in `./body`).
1507
+ *
1508
+ * @param err - The caught value.
1509
+ * @returns `true` when `err.code === 'REQUEST_ABORTED'`.
1510
+ * @example
1511
+ * ```ts
1512
+ * try {
1513
+ * await readBodyWithLimit(req, 1 << 20, true)
1514
+ * } catch (err) {
1515
+ * if (isAbortError(err)) return abortResponse()
1516
+ * throw err
1517
+ * }
1518
+ * ```
1519
+ */
1520
+ export declare function isAbortError(err: unknown): boolean;
1521
+ /**
1522
+ * Build the cancelled-client response: status **499** ("Client Closed
1523
+ * Request", a de-facto nginx status) with no body. This is the only new
1524
+ * outcome added by cancellation — it never replaces an existing success or
1525
+ * error status.
1526
+ *
1527
+ * @returns A bodyless `Response` with status 499.
1528
+ * @example
1529
+ * ```ts
1530
+ * if (req.signal?.aborted) return abortResponse()
1531
+ * ```
1532
+ */
1533
+ export declare function abortResponse(): Response;
990
1534
  /** Offset of the verdict byte (0 = accepted, nonzero = terminal error). */
991
1535
  export declare const OUT_VERDICT: number;
992
1536
  /** Offset of the error-code byte (see ERR_CODE_* below). */
@@ -1227,7 +1771,7 @@ export declare function readHandler(ingress: OptimizedIngressHandler, opts?: Bak
1227
1771
  export declare const deleteHandler: typeof readHandler;
1228
1772
  /**
1229
1773
  * Pre-baked echo handler: streams the request body back with the client's
1230
- * Content-Type, bounded by `maxBodyBytes`.
1774
+ * Content-Type (coerced to a non-active type), bounded by `maxBodyBytes`.
1231
1775
  */
1232
1776
  export declare function echoHandler(ingress: OptimizedIngressHandler, opts?: BakedHandlerOptions): (req: Request, srv?: unknown) => Promise<Response>;
1233
1777
  /**
@@ -1368,6 +1912,20 @@ export declare function createIngressHandler(options?: IngressHandlerOptions, ru
1368
1912
  * Never touches dependencies.
1369
1913
  */
1370
1914
  export declare function livenessHandler(): (_req: Request) => Response;
1915
+ /**
1916
+ * Liveness probe as a PREBUILT `Response`, for static route promotion.
1917
+ *
1918
+ * Wire it as `{ static: staticLiveness() }` so `Bun.serve` snapshots the
1919
+ * constant body into its native route table and serves `/livez` without
1920
+ * entering the JavaScript ingress pipeline at all (see `BakedRoute.static` in
1921
+ * src/ingress/server.ts). On the node:http adapter the bare `Response` is
1922
+ * re-materialized per request from cached body bytes, so the one-shot body is
1923
+ * never replayed. Unlike {@link readinessHandler}, it never runs a dependency
1924
+ * check.
1925
+ *
1926
+ * @returns A fresh 200 `{"status":"ok"}` `Response`.
1927
+ */
1928
+ export declare function staticLiveness(): Response;
1371
1929
  /**
1372
1930
  * Readiness probe: runs the optional async `check` (dependency readiness).
1373
1931
  * Returns `200 {"status":"ready"}` when the check passes (or no check is
@@ -1494,6 +2052,20 @@ export interface RouterRouteSpec {
1494
2052
  /** Terminal envelope style for this route's responder (default: router-level
1495
2053
  * `terminalStyle` or `'castrum'`). */
1496
2054
  terminalStyle?: TerminalStyle;
2055
+ /**
2056
+ * A prebuilt constant response served directly for GET, OUTSIDE the ingress
2057
+ * pipeline (static route promotion). Like `raw`, it OWNS THE WHOLE SPEC: the
2058
+ * router compiles no `IngressInner` for a `static` route, so `read`/`write`/
2059
+ * `echo`/etc. on the same spec are ignored.
2060
+ *
2061
+ * This differs from `BakedRoute.static` in src/ingress/server.ts, where
2062
+ * `static` only takes GET precedence and leaves `write`/`echo`/`delete`
2063
+ * wired — the router cannot mirror that because a `static`-only spec has no
2064
+ * compiled ingress to hand those methods. Do not unify the two semantics.
2065
+ *
2066
+ * See `BakedRoute.static` for the one-shot-body contract.
2067
+ */
2068
+ static?: StaticRoute;
1497
2069
  /**
1498
2070
  * A raw request→Response handler served directly for GET, OUTSIDE the
1499
2071
  * ingress pipeline (health/metrics probes, /metrics). When set, `read`/
@@ -1586,6 +2158,23 @@ export declare function createIngressSync(options?: IngressOptions): SyncIngress
1586
2158
  * @returns an async handler returning a snapshot context
1587
2159
  */
1588
2160
  export declare function createIngress(options?: IngressOptions): IngressHandler;
2161
+ /**
2162
+ * Synchronous ingress writers backed by castrum's process-lifetime FFI binding.
2163
+ * Handles MUST originate from addonPath and remain owned/alive while in use.
2164
+ * Writers throw on native failure; a return larger than output.byteLength is
2165
+ * the exact required capacity, not a completed write. No buffer is retained.
2166
+ */
2167
+ export type IngressBinding = Pick<BunFFI, "ingressHandleComponents" | "ingressHandlePacked" | "ingressLayout"> & {
2168
+ readonly addonPath: string;
2169
+ };
2170
+ /**
2171
+ * Share castrum's self-tested ingress writers without a second dlopen/map.
2172
+ * @returns The binding and actual binary path, or null when FFI is unavailable.
2173
+ * @example
2174
+ * const binding = getIngressBinding()
2175
+ * // Verify binding.addonPath matches the addon that owns your ingress handle.
2176
+ */
2177
+ export declare function getIngressBinding(): IngressBinding | null;
1589
2178
  /** Parse a query string (`a=1&b=2`) into an object via the native parser. */
1590
2179
  export declare function parseQueryString(query: string): Record<string, string | string[]>;
1591
2180
  /** Parse a cookie header (`a=1; b=2`) into an object via the native parser. */
@@ -2513,8 +3102,23 @@ export interface RustBatch {
2513
3102
  wsAcceptKey(items: Uint8Array[]): Uint8Array[];
2514
3103
  base64UrlEncode(items: Uint8Array[]): Uint8Array[];
2515
3104
  base64UrlDecode(items: Uint8Array[]): Uint8Array[];
3105
+ /**
3106
+ * Parallel password hashing. WARNING: the SAME `salt` is applied to every
3107
+ * item. Reusing one salt across real user passwords enables a single
3108
+ * precomputation to attack them all — generate a unique salt per password in
3109
+ * production (this batch form is for precomputed/benchmark workloads).
3110
+ */
2516
3111
  passwordHash(passwords: Uint8Array[], salt: Uint8Array, options?: PasswordHashOptions | null): Uint8Array[];
3112
+ /**
3113
+ * Parallel AEAD encrypt. `nonce` MUST be exactly 12 bytes and MUST be fresh
3114
+ * for every batch call with the same key: a unique per-item nonce is derived
3115
+ * by XOR-ing the item index into the low 8 bytes, so reusing a base nonce
3116
+ * across calls reuses keystream/tag material for colliding indices
3117
+ * (catastrophic for AES-GCM / ChaCha20-Poly1305). A non-12-byte nonce is
3118
+ * rejected with an error.
3119
+ */
2517
3120
  aeadEncrypt(key: Uint8Array, nonce: Uint8Array, items: Uint8Array[], algorithm?: string | null): Uint8Array[];
3121
+ /** Parallel AEAD decrypt. Pass the SAME base nonce and item order as the encrypt call. */
2518
3122
  aeadDecrypt(key: Uint8Array, nonce: Uint8Array, items: Uint8Array[], algorithm?: string | null): Uint8Array[];
2519
3123
  gzipCompress(items: Uint8Array[], level?: number | null): Uint8Array[];
2520
3124
  gzipDecompress(items: Uint8Array[], maxDecompressed?: number | null): Uint8Array[];
@@ -2781,7 +3385,10 @@ export interface RustScalar {
2781
3385
  createMediaTypeMatcher(expected: Uint8Array): MediaTypeMatcherInstance;
2782
3386
  /**
2783
3387
  * Sharded fixed-window per-key rate limiter. `maxEntries` clamps internally
2784
- * (default 1,048,576). Each instance owns an independent budget.
3388
+ * (default 1,048,576) and has an effective floor of 16,384
3389
+ * (`max_per_shard = max(maxEntries / 256, 64)` × 256 shards) — values below
3390
+ * that are silently raised, so don't rely on it to bound a tiny key space.
3391
+ * Each instance owns an independent budget.
2785
3392
  * @remarks Scalar per-check cost: ~3-14x SLOWER than the JS Map baseline —
2786
3393
  * prefer the ingress pipeline (one FFI for all stages) for native rate
2787
3394
  * limiting in a request path. [measured 2026-08]
@@ -2796,6 +3403,20 @@ export interface RustScalar {
2796
3403
  createMetricsRegistry(): MetricsRegistryInstance;
2797
3404
  initThreadPool(rayonThreads?: number): void;
2798
3405
  rayonNumThreads(): number;
3406
+ /**
3407
+ * Drop the process-wide compiled request-body schema cache. Identical schema
3408
+ * bytes are compiled once and shared across `Ingress` / `NativeRoute`
3409
+ * instances; this releases those compiled validators (already-constructed
3410
+ * instances keep their own shared reference and are unaffected). Idempotent,
3411
+ * and a safe no-op when the native addon is absent or predates the symbol
3412
+ * (the public `flushMemory()` relies on that).
3413
+ *
3414
+ * Maintenance-only escape hatch — NOT a benchmarked op, so it is intentionally
3415
+ * exempt from `PROVEN_SELECTION` / `selection.json` (there is no native-vs-JS
3416
+ * winner to classify). `test/unit/contract/proven.test.ts` only requires
3417
+ * registry coverage for selection.json ops + Bun built-ins.
3418
+ */
3419
+ clearSchemaCache(): void;
2799
3420
  jwtSign(claims: Record<string, unknown>, secret: Uint8Array, ttlSeconds?: number | null, nowSeconds?: number): Uint8Array;
2800
3421
  /**
2801
3422
  * Sign a JWT from pre-serialized claim JSON bytes — skips the napi
@@ -3198,6 +3819,41 @@ export declare const encoder: {
3198
3819
  export declare const decoder: {
3199
3820
  decode(bytes: Uint8Array | string): string;
3200
3821
  };
3822
+ /** Options for {@link flushMemory}. */
3823
+ export interface FlushMemoryOptions {
3824
+ /**
3825
+ * When true (the default), also request a full garbage collection from the
3826
+ * runtime: `Bun.gc(true)` on Bun, `globalThis.gc?.()` on Node when the
3827
+ * process runs with `--expose-gc`. Set false to flush caches only.
3828
+ */
3829
+ gc?: boolean;
3830
+ }
3831
+ /**
3832
+ * Flush process-level caches and (by default) request a full GC. Intended for
3833
+ * explicit memory-pressure handling — e.g. after a burst or before a snapshot.
3834
+ *
3835
+ * Clears:
3836
+ * - the global loader LRU cache (`loader.clear()`),
3837
+ * - the process-wide MIME caches (`clearMimeCaches()`),
3838
+ * - the native compiled-schema cache (`rust.clearSchemaCache()`).
3839
+ *
3840
+ * The metrics registry is intentionally left untouched.
3841
+ *
3842
+ * Idempotent, and safe to call before the native addon has ever been loaded
3843
+ * (every cache starts empty, the loader cache is a plain Map, and the native
3844
+ * schema-cache clear is a defensive no-op when the addon/symbol is absent).
3845
+ *
3846
+ * @param options - `gc: false` skips the runtime GC request. Defaults to `{}`
3847
+ * (GC requested).
3848
+ * @example
3849
+ * ```ts
3850
+ * import { flushMemory } from 'castrum'
3851
+ *
3852
+ * flushMemory() // drop caches + request GC
3853
+ * flushMemory({ gc: false }) // drop caches only
3854
+ * ```
3855
+ */
3856
+ export declare function flushMemory(options?: FlushMemoryOptions): void;
3201
3857
  /**
3202
3858
  * Generate a UUIDv7 (time-ordered) string, preferring Bun's native built-in.
3203
3859
  * Under Node this falls back to `crypto.randomUUID` (v4).