castrum 0.7.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -368
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +2763 -0
  5. package/dist/index.js +8707 -0
  6. package/docs/API.md +337 -0
  7. package/docs/ARCHITECTURE.md +507 -0
  8. package/docs/BENCHMARKS.md +343 -0
  9. package/docs/CASE_STUDY.md +240 -0
  10. package/docs/ENVIRONMENT.md +81 -0
  11. package/docs/FFI_BUN_GUIDE.md +327 -0
  12. package/docs/GETTING_STARTED.md +192 -0
  13. package/docs/INGRESS-ROUTER.md +81 -0
  14. package/docs/INGRESS.md +746 -0
  15. package/docs/NATIVE-ROUTE.md +53 -0
  16. package/docs/REPO_MAP.md +279 -0
  17. package/docs/adr/0001-two-wire-formats.md +39 -0
  18. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  19. package/docs/adr/0003-bun-builtins-delegation.md +43 -0
  20. package/docs/bun-builtins-decision-matrix.md +115 -0
  21. package/index.ts +44 -8
  22. package/package.json +98 -33
  23. package/src/ingress/body.ts +289 -0
  24. package/src/ingress/constants.ts +220 -48
  25. package/src/ingress/context.ts +136 -0
  26. package/src/ingress/decode/baked-result.ts +121 -0
  27. package/src/ingress/decode/fast-result.ts +160 -0
  28. package/src/ingress/decode/packed-sections.ts +56 -0
  29. package/src/ingress/decode/result-base.ts +172 -0
  30. package/src/ingress/errors.ts +72 -0
  31. package/src/ingress/fast.ts +152 -1021
  32. package/src/ingress/handlers.ts +639 -1001
  33. package/src/ingress/headers/baked-templates.ts +102 -0
  34. package/src/ingress/headers/cors.ts +44 -0
  35. package/src/ingress/headers/fast-templates.ts +238 -0
  36. package/src/ingress/headers/hsts.ts +83 -0
  37. package/src/ingress/health.ts +68 -0
  38. package/src/ingress/index.ts +142 -405
  39. package/src/ingress/metrics.ts +132 -0
  40. package/src/ingress/options.ts +197 -0
  41. package/src/ingress/packing/gather-raw-headers.ts +139 -0
  42. package/src/ingress/packing/header-packing.ts +75 -0
  43. package/src/ingress/packing/input-packer.ts +164 -0
  44. package/src/ingress/packing/scratch.ts +50 -0
  45. package/src/ingress/packing/select-headers.ts +102 -0
  46. package/src/ingress/response/error-bodies.ts +75 -0
  47. package/src/ingress/response/terminal.ts +59 -0
  48. package/src/ingress/router.ts +237 -0
  49. package/src/ingress/routes/common.ts +79 -0
  50. package/src/ingress/routes/delete.ts +14 -0
  51. package/src/ingress/routes/echo.ts +107 -0
  52. package/src/ingress/routes/fallback.ts +23 -0
  53. package/src/ingress/routes/head.ts +27 -0
  54. package/src/ingress/routes/index.ts +11 -0
  55. package/src/ingress/routes/json-write.ts +129 -0
  56. package/src/ingress/routes/options.ts +26 -0
  57. package/src/ingress/routes/read.ts +35 -0
  58. package/src/ingress/routes/responder.ts +159 -0
  59. package/src/ingress/server-node.ts +376 -0
  60. package/src/ingress/server.ts +475 -0
  61. package/src/ingress/shared.ts +132 -0
  62. package/src/ingress/status.ts +97 -0
  63. package/src/ingress/types.ts +191 -0
  64. package/src/integration/batch.ts +58 -0
  65. package/src/integration/index.ts +29 -0
  66. package/src/integration/pipeline.ts +220 -0
  67. package/src/integration/streaming.ts +78 -0
  68. package/src/integration/websocket.ts +93 -0
  69. package/src/loader/batch.ts +137 -0
  70. package/src/loader/cost.ts +105 -0
  71. package/src/loader/index.ts +745 -0
  72. package/src/loader/ops.ts +657 -0
  73. package/src/loader/state.ts +84 -0
  74. package/src/loader/types.ts +145 -0
  75. package/src/native/ffi/constants.ts +58 -0
  76. package/src/native/ffi/selftest.ts +729 -0
  77. package/src/native/ffi/types.ts +474 -0
  78. package/src/native/ffi.ts +1775 -0
  79. package/src/native/index.ts +8 -207
  80. package/src/native/loader.ts +240 -0
  81. package/src/native/types/instances.ts +274 -0
  82. package/src/native/types.ts +401 -0
  83. package/src/runtime/builtins.ts +131 -0
  84. package/src/runtime/codec.ts +147 -0
  85. package/src/runtime/detect.ts +36 -0
  86. package/src/runtime/index.ts +28 -0
  87. package/src/runtime/native.ts +35 -0
  88. package/src/runtime/server.ts +31 -0
  89. package/src/runtime/transport.ts +64 -0
  90. package/src/runtime/types.ts +173 -0
  91. package/src/runtime/uuid.ts +45 -0
  92. package/src/runtime/websocket.ts +30 -0
  93. package/src/rust-ffi/addon.ts +10 -0
  94. package/src/rust-ffi/batch/build.ts +155 -0
  95. package/src/rust-ffi/batch/index.ts +8 -0
  96. package/src/rust-ffi/batch/types.ts +92 -0
  97. package/src/rust-ffi/client.ts +87 -0
  98. package/src/rust-ffi/context.ts +193 -0
  99. package/src/rust-ffi/index.ts +51 -541
  100. package/src/rust-ffi/into.ts +21 -0
  101. package/src/rust-ffi/options.ts +113 -0
  102. package/src/rust-ffi/packed.ts +253 -0
  103. package/src/rust-ffi/proven.ts +28 -0
  104. package/src/rust-ffi/scalar/crypto.ts +346 -0
  105. package/src/rust-ffi/scalar/factories.ts +459 -0
  106. package/src/rust-ffi/scalar/hashing.ts +74 -0
  107. package/src/rust-ffi/scalar/http.ts +337 -0
  108. package/src/rust-ffi/scalar/index.ts +31 -0
  109. package/src/rust-ffi/scalar/interface.ts +325 -0
  110. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  111. package/src/rust-ffi/scalar/json.ts +61 -0
  112. package/src/rust-ffi/scalar/payload.ts +241 -0
  113. package/src/rust-ffi/text.ts +121 -0
  114. package/src/selection.json +194 -0
  115. package/src/selection.ts +73 -0
  116. package/src/shared/adaptive.ts +67 -0
  117. package/src/shared/buffer-pool.ts +264 -0
  118. package/src/shared/bytes.ts +108 -9
  119. package/src/shared/codec.ts +78 -0
  120. package/src/shared/env.ts +20 -0
  121. package/src/shared/log.ts +75 -0
  122. package/src/shared/metrics.ts +247 -0
  123. package/src/shared/packed/index.ts +10 -0
  124. package/src/shared/packed/parsers.ts +31 -0
  125. package/src/shared/packed/schema.ts +23 -0
  126. package/src/shared/packed/wire.ts +504 -0
  127. package/src/shared/proven.ts +289 -0
  128. package/src/shared/request-id.ts +67 -0
  129. package/src/shared/response.ts +104 -0
  130. package/src/shared/runtime.ts +29 -0
  131. package/src/shared/trace.ts +81 -0
  132. package/src/shared/uuid.ts +15 -0
  133. package/Cargo.toml +0 -73
  134. package/bench.ts +0 -3
  135. package/rust/batch.rs +0 -195
  136. package/rust/cookie_parser.rs +0 -70
  137. package/rust/cors.rs +0 -263
  138. package/rust/hashing.rs +0 -164
  139. package/rust/headers.rs +0 -234
  140. package/rust/hmac_sha256.rs +0 -113
  141. package/rust/http_parser.rs +0 -132
  142. package/rust/ingress.rs +0 -616
  143. package/rust/ingress_constants.rs +0 -89
  144. package/rust/ip_trust.rs +0 -185
  145. package/rust/json_ops.rs +0 -66
  146. package/rust/json_patch_ops.rs +0 -25
  147. package/rust/json_schema.rs +0 -151
  148. package/rust/json_ser.rs +0 -429
  149. package/rust/lib.rs +0 -48
  150. package/rust/method.rs +0 -141
  151. package/rust/mime_lookup.rs +0 -76
  152. package/rust/output.rs +0 -238
  153. package/rust/proxy.rs +0 -90
  154. package/rust/query_parser.rs +0 -141
  155. package/rust/random_token.rs +0 -52
  156. package/rust/rate_limit.rs +0 -203
  157. package/rust/terminal.rs +0 -95
  158. package/rust/unit_tests.rs +0 -843
  159. package/rust/url_codec.rs +0 -133
  160. package/rust/util.rs +0 -537
  161. package/rust/validation.rs +0 -107
  162. package/rust/websocket.rs +0 -23
  163. package/src/baseline/index.ts +0 -12
  164. package/src/baseline/tasks/cookie.ts +0 -22
  165. package/src/baseline/tasks/hashing.ts +0 -18
  166. package/src/baseline/tasks/hmac.ts +0 -38
  167. package/src/baseline/tasks/http.ts +0 -62
  168. package/src/baseline/tasks/json-patch.ts +0 -12
  169. package/src/baseline/tasks/json.ts +0 -50
  170. package/src/baseline/tasks/mime.ts +0 -5
  171. package/src/baseline/tasks/query.ts +0 -14
  172. package/src/baseline/tasks/token.ts +0 -15
  173. package/src/baseline/tasks/url.ts +0 -11
  174. package/src/baseline/tasks/validation.ts +0 -19
  175. package/src/baseline/tasks/websocket.ts +0 -14
  176. package/src/bench/assert.ts +0 -70
  177. package/src/bench/checks.ts +0 -227
  178. package/src/bench/checksum.ts +0 -27
  179. package/src/bench/comparisons.ts +0 -70
  180. package/src/bench/concurrent-worker.ts +0 -135
  181. package/src/bench/fixtures.ts +0 -233
  182. package/src/bench/index.ts +0 -2
  183. package/src/bench/measure.ts +0 -177
  184. package/src/bench/now.ts +0 -3
  185. package/src/bench/report.ts +0 -79
  186. package/src/bench/run.ts +0 -57
  187. package/src/bench/tasks/complex.ts +0 -232
  188. package/src/bench/tasks/concurrent.ts +0 -213
  189. package/src/bench/tasks/cookie.ts +0 -44
  190. package/src/bench/tasks/hashing.ts +0 -33
  191. package/src/bench/tasks/hmac.ts +0 -36
  192. package/src/bench/tasks/http.ts +0 -50
  193. package/src/bench/tasks/index.ts +0 -52
  194. package/src/bench/tasks/json-patch.ts +0 -21
  195. package/src/bench/tasks/json.ts +0 -33
  196. package/src/bench/tasks/mime.ts +0 -21
  197. package/src/bench/tasks/query.ts +0 -44
  198. package/src/bench/tasks/stress.ts +0 -142
  199. package/src/bench/tasks/token.ts +0 -20
  200. package/src/bench/tasks/url.ts +0 -34
  201. package/src/bench/tasks/validation.ts +0 -57
  202. package/src/bench/tasks/websocket.ts +0 -21
  203. package/src/bench/types.ts +0 -60
  204. package/src/data/json-rows.ts +0 -33
  205. package/src/ingress/packed-input.ts +0 -59
  206. package/src/shared/json.ts +0 -18
  207. package/src/shared/packed.ts +0 -316
@@ -0,0 +1,337 @@
1
+ // src/rust-ffi/scalar/http.ts — HTTP wire-format + parsing scalar methods.
2
+ //
3
+ // Mirrors rust/http/* (packed parsers, media type, etag, HTTP dates,
4
+ // Accept-Encoding, URL codec/join, MIME lookup) plus the `util/validation`
5
+ // validators and the WebSocket accept-key.
6
+ //
7
+ // Runtime dispatch is centralized in the adapter (`ctx.runtime`): Bun built-in
8
+ // delegations (urlEncode/urlDecode/httpDate) come from `builtins.has(op)` and
9
+ // the native call from `transport.resolve(op)` / `transport.ffi` (bun:ffi
10
+ // first, napi fallback) — no inline `isBun()` / `getBunFFI()`.
11
+
12
+ import type { EncodingPrefResult, MediaTypeResult } from '../../native'
13
+ import type { BunFFI } from '../../native/ffi'
14
+ import { decoder } from '../../shared/bytes'
15
+ import { decodeUtf8Fatal } from '../../shared/codec'
16
+ import { packPairs } from '../../shared/packed'
17
+ import { writeInto } from '../into'
18
+ import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
19
+
20
+ // Mirror napi `url_decode`'s UTF-8 validation (rust/http/url_codec.rs): only
21
+ // validate when a high byte is present (the ASCII fast path skips it — same as
22
+ // Rust's saw_high check). On the Bun path the check is a native C-ABI probe
23
+ // (`castrum_utf8_valid`); under Node (no ffi) the codec's fatal TextDecoder is
24
+ // the fallback — no TextDecoder runs on the Bun path.
25
+ function ensureValidUtf8(bytes: Uint8Array, ffi: BunFFI | null): void {
26
+ for (let i = 0; i < bytes.length; i++) {
27
+ if ((bytes[i] ?? 0) >= 0x80) {
28
+ if (ffi) {
29
+ if (!ffi.utf8Valid(bytes)) {
30
+ throw new Error('url decode: invalid UTF-8 in decoded output')
31
+ }
32
+ } else {
33
+ try {
34
+ decodeUtf8Fatal(bytes)
35
+ } catch {
36
+ throw new Error('url decode: invalid UTF-8 in decoded output')
37
+ }
38
+ }
39
+ return
40
+ }
41
+ }
42
+ }
43
+
44
+ /** Read an unsigned little-endian u32 at `off` (0 past the end). */
45
+ function u32LE(b: Uint8Array, off: number): number {
46
+ return (
47
+ ((b[off] ?? 0) |
48
+ ((b[off + 1] ?? 0) << 8) |
49
+ ((b[off + 2] ?? 0) << 16) |
50
+ ((b[off + 3] ?? 0) << 24)) >>>
51
+ 0
52
+ )
53
+ }
54
+
55
+ /**
56
+ * Unpack the `castrum_parse_media_type` verdict into the napi-shaped result.
57
+ * Layout: `[u32 mediaTypeLen][mediaType][u32 charsetLen (0xFFFFFFFF = none)]
58
+ * [charset][u32 boundaryLen][boundary][u32 paramCount]{[u32 keyLen][key]
59
+ * [u32 valLen][val]}` — charset/boundary also appear in `params`, matching the
60
+ * napi `HashMap` (which keeps every param including charset/boundary).
61
+ */
62
+ function unpackMediaType(packed: Uint8Array): MediaTypeResult {
63
+ let off = 0
64
+ const mtLen = u32LE(packed, off)
65
+ off += 4
66
+ const mediaType = decoder.decode(packed.subarray(off, off + mtLen))
67
+ off += mtLen
68
+ let charset: string | null = null
69
+ const csLen = u32LE(packed, off)
70
+ off += 4
71
+ if (csLen !== 0xffffffff) {
72
+ charset = decoder.decode(packed.subarray(off, off + csLen))
73
+ off += csLen
74
+ }
75
+ let boundary: string | null = null
76
+ const bLen = u32LE(packed, off)
77
+ off += 4
78
+ if (bLen !== 0xffffffff) {
79
+ boundary = decoder.decode(packed.subarray(off, off + bLen))
80
+ off += bLen
81
+ }
82
+ const count = u32LE(packed, off)
83
+ off += 4
84
+ const params: Record<string, string> = {}
85
+ for (let i = 0; i < count; i++) {
86
+ const kLen = u32LE(packed, off)
87
+ off += 4
88
+ const key = decoder.decode(packed.subarray(off, off + kLen))
89
+ off += kLen
90
+ const vLen = u32LE(packed, off)
91
+ off += 4
92
+ const val = decoder.decode(packed.subarray(off, off + vLen))
93
+ off += vLen
94
+ params[key] = val
95
+ }
96
+ return { mediaType, charset, boundary, params }
97
+ }
98
+
99
+ /** Unpack the `castrum_parse_accept_encoding` verdict (f32 q-values) into the
100
+ * napi-shaped array. Layout: `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}`. */
101
+ function unpackAcceptEncoding(packed: Uint8Array): EncodingPrefResult[] {
102
+ const count = u32LE(packed, 0)
103
+ let off = 4
104
+ const out: EncodingPrefResult[] = []
105
+ const view = new DataView(packed.buffer, packed.byteOffset, packed.byteLength)
106
+ for (let i = 0; i < count; i++) {
107
+ const encLen = u32LE(packed, off)
108
+ off += 4
109
+ const encoding = decoder.decode(packed.subarray(off, off + encLen))
110
+ off += encLen
111
+ const q = view.getFloat32(off, true)
112
+ off += 4
113
+ const order = u32LE(packed, off)
114
+ off += 4
115
+ out.push({ encoding, q, order })
116
+ }
117
+ return out
118
+ }
119
+
120
+ /** HTTP / parsing scalar methods (`Pick<RustScalar, ...>`). */
121
+ export function buildHttp(ctx: RustClientContext) {
122
+ const { builtins, transport } = ctx.runtime
123
+ // Resolve the native fns through the shared first-use cache so the hot
124
+ // scalar path skips the lazy-addon Proxy get on every call. `resolveNative`
125
+ // is FFI-first (bun:ffi when live, napi fallback) via the adapter.
126
+ const n = (name: string) => resolveNative(ctx, name)
127
+ // Hoist the immutable per-runtime builtin-delegation decisions into a
128
+ // bind-time map (BUILTIN_OPS is a module constant, never mutated after
129
+ // load) so the hot path reads an object property instead of a `Set.has`.
130
+ const HAS = {
131
+ urlEncode: builtins.has('urlEncode'),
132
+ urlDecode: builtins.has('urlDecode'),
133
+ httpDate: builtins.has('httpDate'),
134
+ }
135
+ // Lazy-memoized ffi surface: binds on first call (no eager dlopen at
136
+ // import), then a single local read per call — no getter hop.
137
+ const ffi = memoizeFfi(transport)
138
+
139
+ return {
140
+ mimeFromExtension(ext: Uint8Array): Uint8Array | string {
141
+ // FFI-first: the `cstring` ARG is string-only, so bytes are decoded once
142
+ // (cheap CString clone) before the call. napi keeps the memoized-bytes
143
+ // path (cachedMime).
144
+ const f = ffi()
145
+ if (f) return f.mimeFromExtension(decoder.decode(ext)) ?? 'application/octet-stream'
146
+ return ctx.cachedMime(ext)
147
+ },
148
+ urlEncode(input: Uint8Array): Uint8Array | string {
149
+ // Under Bun, Bun's native `encodeURIComponent` beats the rust+FFI/NAPI
150
+ // crossing (~4x on the string path, ~1.3x on bytes) and skips the C-ABI
151
+ // call entirely — see docs/bun-builtins-decision-matrix.md. Bun returns
152
+ // the percent-encoded STRING (native transfer). Parity is pinned by
153
+ // test/unit/features/url.test.ts.
154
+ if (HAS.urlEncode) return builtins.urlEncode(input)
155
+ return n('urlEncode')(input) as Uint8Array
156
+ },
157
+ urlDecode(input: Uint8Array): Uint8Array {
158
+ // Same rationale as `urlEncode`; the strict `urlDecode` already throws on
159
+ // invalid UTF-8 (ensureValidUtf8 below), matching decodeURIComponent's
160
+ // URIError (a subclass of Error), so the JS path is behavior-equivalent.
161
+ if (HAS.urlDecode) return builtins.urlDecode(input)
162
+ const f = ffi()
163
+ if (f) {
164
+ const out = f.urlDecode(input)
165
+ // napi `url_decode` validates UTF-8 (only when a high byte is present);
166
+ // `url_decode_bytes`/`url_decode_into` do not. This is the strict one.
167
+ ensureValidUtf8(out, transport.ffi)
168
+ return out
169
+ }
170
+ return n('urlDecode')(input) as Uint8Array
171
+ },
172
+ urlDecodeBytes(input: Uint8Array): Uint8Array {
173
+ const f = ffi()
174
+ if (f) return f.urlDecode(input) // byte semantics match url_decode_bytes
175
+ return n('urlDecodeBytes')(input) as Uint8Array
176
+ },
177
+ urlEncodeInto(input: Uint8Array, output: Uint8Array): number {
178
+ return n('urlEncodeInto')(input, output) as number
179
+ },
180
+ urlDecodeInto(input: Uint8Array, output: Uint8Array): number {
181
+ // no UTF-8 check — matches napi url_decode_into on both transports
182
+ return n('urlDecodeInto')(input, output) as number
183
+ },
184
+ validateEmail(input: Uint8Array): boolean {
185
+ // FFI-first: the `cstring` ARG is string-only, so bytes are decoded once
186
+ // (cheap CString clone) before the call — still faster than the old
187
+ // `(ptr,len)` view path (see bench:margin cstring scenario). napi
188
+ // fallback keeps the byte path.
189
+ const f = ffi()
190
+ if (f) return f.validateEmail(decoder.decode(input))
191
+ return n('validateEmail')(input) as boolean
192
+ },
193
+ validateUuid(input: Uint8Array): boolean {
194
+ const f = ffi()
195
+ if (f) return f.validateUuid(decoder.decode(input))
196
+ return n('validateUuid')(input) as boolean
197
+ },
198
+ validateIpv4(input: Uint8Array): boolean {
199
+ const f = ffi()
200
+ if (f) return f.validateIpv4(decoder.decode(input))
201
+ return n('validateIpv4')(input) as boolean
202
+ },
203
+ validateIpv6(input: Uint8Array): boolean {
204
+ const f = ffi()
205
+ if (f) return f.validateIpv6(decoder.decode(input))
206
+ return n('validateIpv6')(input) as boolean
207
+ },
208
+ wsAcceptKey(key: Uint8Array): Uint8Array | string {
209
+ // FFI-first: the `cstring` ARG is string-only, so bytes are decoded once
210
+ // (cheap CString clone) before the call. napi fallback keeps the bytes.
211
+ const f = ffi()
212
+ if (f) return f.wsAcceptKey(decoder.decode(key))
213
+ return n('wsAcceptKey')(key) as Uint8Array | string
214
+ },
215
+ wsAcceptKeyInto(key: Uint8Array, output: Uint8Array): number {
216
+ const f = ffi()
217
+ if (f) return f.wsAcceptKeyInto(key, output)
218
+ return writeInto('ws accept key', output, n('wsAcceptKey')(key) as Uint8Array)
219
+ },
220
+ httpParseRequestPacked(input: Uint8Array): Uint8Array {
221
+ const f = ffi()
222
+ if (f) {
223
+ const out = new Uint8Array(input.length * 9 + 16)
224
+ const w = f.httpParseRequestPackedInto(input, out)
225
+ return out.subarray(0, w)
226
+ }
227
+ return n('httpParseRequestPacked')(input) as Uint8Array
228
+ },
229
+ httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number {
230
+ return n('httpParseRequestPackedInto')(input, output) as number
231
+ },
232
+ queryParsePacked(input: Uint8Array): Uint8Array {
233
+ const f = ffi()
234
+ if (f) {
235
+ const out = new Uint8Array(input.length * 9 + 16)
236
+ const w = f.queryParsePackedInto(input, out)
237
+ return out.subarray(0, w)
238
+ }
239
+ return n('queryParsePacked')(input) as Uint8Array
240
+ },
241
+ queryParsePackedInto(input: Uint8Array, output: Uint8Array): number {
242
+ return n('queryParsePackedInto')(input, output) as number
243
+ },
244
+ cookieParsePacked(input: Uint8Array): Uint8Array {
245
+ const f = ffi()
246
+ if (f) {
247
+ const out = new Uint8Array(input.length * 9 + 16)
248
+ const w = f.cookieParsePackedInto(input, out)
249
+ return out.subarray(0, w)
250
+ }
251
+ return n('cookieParsePacked')(input) as Uint8Array
252
+ },
253
+ cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number {
254
+ return n('cookieParsePackedInto')(input, output) as number
255
+ },
256
+ formParsePacked(input: Uint8Array): Uint8Array {
257
+ const f = ffi()
258
+ if (f) {
259
+ // x-www-form-urlencoded uses the SAME packed-pairs core as query parse.
260
+ const out = new Uint8Array(input.length * 9 + 16)
261
+ const w = f.formParsePackedInto(input, out)
262
+ return out.subarray(0, w)
263
+ }
264
+ return n('formParsePacked')(input) as Uint8Array
265
+ },
266
+ formParsePackedInto(input: Uint8Array, output: Uint8Array): number {
267
+ // Pooled sibling of formParsePacked — caller-owned output buffer (the
268
+ // napi addon has no packed `Into` for form, so copy via the allocating
269
+ // call on that path; same wire output, so the copy is exact).
270
+ const f = ffi()
271
+ if (f) return f.formParsePackedInto(input, output)
272
+ return writeInto('form parse', output, n('formParsePacked')(input) as Uint8Array)
273
+ },
274
+ parseMediaType(input: Uint8Array): MediaTypeResult {
275
+ const f = ffi()
276
+ if (f) return unpackMediaType(f.parseMediaType(input))
277
+ const r = n('parseMediaType')(input) as MediaTypeResult
278
+ // napi serializes Option fields as undefined; normalize to null to match
279
+ // the declared `string | null` public type.
280
+ return {
281
+ mediaType: r.mediaType,
282
+ charset: r.charset ?? null,
283
+ boundary: r.boundary ?? null,
284
+ params: r.params,
285
+ }
286
+ },
287
+ etag(data: Uint8Array, weak?: boolean): Uint8Array | string {
288
+ return n('etag')(data, weak ?? undefined) as Uint8Array | string
289
+ },
290
+ etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number {
291
+ return n('etagInto')(data, output, weak ?? undefined) as number
292
+ },
293
+ httpDate(secs?: number): Uint8Array | string {
294
+ // Bun's Date.toUTCString (RFC 1123 IMF-fixdate) beats the rust+FFI
295
+ // crossing (~3.7x, measured); Bun returns the STRING (native transfer).
296
+ if (HAS.httpDate) return builtins.httpDate(secs)
297
+ return n('httpDate')(secs ?? undefined) as Uint8Array
298
+ },
299
+ httpDateInto(secs: number | undefined, output: Uint8Array): number {
300
+ // FFI-first: `castrum_http_date_into` writes directly into the caller
301
+ // buffer (no napi crossing). `undefined` → 0 (matches the napi
302
+ // `Option<f64>` default). napi fallback keeps the old path.
303
+ const f = ffi()
304
+ if (f) return f.httpDateInto(secs ?? 0, output)
305
+ return n('httpDateInto')(secs ?? undefined, output) as number
306
+ },
307
+ parseHttpDate(input: Uint8Array): bigint | null {
308
+ return n('parseHttpDate')(input) as bigint | null
309
+ },
310
+ parseAcceptEncoding(input: Uint8Array): EncodingPrefResult[] {
311
+ const f = ffi()
312
+ if (f) return unpackAcceptEncoding(f.parseAcceptEncoding(input))
313
+ return n('parseAcceptEncoding')(input) as EncodingPrefResult[]
314
+ },
315
+ urlResolve(base: Uint8Array, reference: Uint8Array): Uint8Array | string {
316
+ // FFI-first: cstring resolution (native transfer — zero encode).
317
+ const f = ffi()
318
+ if (f) {
319
+ const s = f.urlResolve(base, reference)
320
+ if (s === null) throw new Error('url resolve: invalid UTF-8 in input')
321
+ return s
322
+ }
323
+ return n('urlResolve')(base, reference) as Uint8Array
324
+ },
325
+ urlEncodeQuery(params: Record<string, string>): Uint8Array | string {
326
+ // FFI-first: pack the params into the shared pairs layout, then the C
327
+ // side sorts keys (BTreeMap) and percent-encodes → cstring.
328
+ const f = ffi()
329
+ if (f) {
330
+ const s = f.urlEncodeQuery(packPairs(Object.entries(params)))
331
+ if (s === null) throw new Error('url encode query: invalid input')
332
+ return s
333
+ }
334
+ return n('urlEncodeQuery')(params) as Uint8Array
335
+ },
336
+ }
337
+ }
@@ -0,0 +1,31 @@
1
+ // src/rust-ffi/scalar/index.ts — Scalar + feature FFI methods (barrel).
2
+ //
3
+ // The byte-level single-value methods of the Rust client (crc32, hmac, json,
4
+ // jwt, password, aead, compress, ... plus the class factories and runtime
5
+ // controls). Composed into the client by ./client.ts.
6
+ //
7
+ // The `RustScalar` interface (the public surface, with its perf JSDoc) lives
8
+ // in `interface.ts`; the implementation is split into domain builders mirroring
9
+ // `rust/` (hashing, json, http, crypto, payload, factories).
10
+
11
+ import type { RustClientContext } from '../context'
12
+ import { buildCrypto } from './crypto'
13
+ import { buildFactories } from './factories'
14
+ import { buildHashing } from './hashing'
15
+ import { buildHttp } from './http'
16
+ import { buildJson } from './json'
17
+ import { buildPayload } from './payload'
18
+
19
+ export type { RustScalar } from './interface'
20
+
21
+ /** Build the scalar/feature method set for a client context. */
22
+ export function buildScalar(ctx: RustClientContext): import('./interface').RustScalar {
23
+ return {
24
+ ...buildHashing(ctx),
25
+ ...buildJson(ctx),
26
+ ...buildHttp(ctx),
27
+ ...buildCrypto(ctx),
28
+ ...buildPayload(ctx),
29
+ ...buildFactories(ctx),
30
+ }
31
+ }
@@ -0,0 +1,325 @@
1
+ // src/rust-ffi/scalar/interface.ts — The `RustScalar` method surface.
2
+ //
3
+ // One file per public method. The implementation is split into domain
4
+ // builders (hashing, json, http, crypto, payload, factories) composed by
5
+ // `buildScalar` in index.ts.
6
+
7
+ import type {
8
+ AcceptNegotiatorInstance,
9
+ AeadCipherInstance,
10
+ Argon2HasherInstance,
11
+ Base64CodecInstance,
12
+ ConditionalRequestInstance,
13
+ CookieSignerInstance,
14
+ CsrfProtectorInstance,
15
+ EncodingPrefResult,
16
+ FormParserInstance,
17
+ HmacSignerInstance,
18
+ JwtSignerInstance,
19
+ MediaTypeMatcherInstance,
20
+ MediaTypeParserInstance,
21
+ MediaTypeResult,
22
+ MultipartPart,
23
+ PasswordHashOptions,
24
+ RateLimiterInstance,
25
+ SchemaValidatorInstance,
26
+ TemplateRendererInstance,
27
+ UrlBuilderInstance,
28
+ WsFrame,
29
+ } from '../../native'
30
+
31
+ /**
32
+ * The flat `rust.*` scalar method surface: bytes in → normalized out. Built by
33
+ * the scalar builders from the native addon + Bun built-in delegations.
34
+ */
35
+ export interface RustScalar {
36
+ // ── Scalar utilities (bytes in → normalized out) ──
37
+ crc32(input: Uint8Array): number
38
+ fnv1a64(input: Uint8Array): bigint
39
+ /**
40
+ * XXH3-64 over raw bytes (high-throughput non-cryptographic hash).
41
+ * Compare against `Bun.hash.xxHash3` — see docs/bun-builtins-decision-matrix.md.
42
+ */
43
+ xxh3(input: Uint8Array): bigint
44
+ hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array | string
45
+ hmacSha256Verify(key: Uint8Array, data: Uint8Array, sig: Uint8Array | string): boolean
46
+ jsonValid(input: Uint8Array): boolean
47
+ /**
48
+ * Parse JSON to a JS value (FFI packed structural parse on Bun — sonic-rs
49
+ * parses ONCE, JS assembles from a token stream with no re-parse; napi
50
+ * sonic-rs DOM fallback). Throws on invalid JSON.
51
+ */
52
+ jsonParse(input: Uint8Array): unknown
53
+ jsonSumIds(input: Uint8Array): bigint
54
+ jsonPatch(doc: Uint8Array, patch: Uint8Array): Uint8Array | string
55
+ mimeFromExtension(ext: Uint8Array): Uint8Array | string
56
+ randomToken(byteLen: number): Uint8Array | string
57
+ randomTokenInto(byteLen: number, output: Uint8Array): number
58
+ urlEncode(input: Uint8Array): Uint8Array | string
59
+ urlDecode(input: Uint8Array): Uint8Array
60
+ /**
61
+ * Strict percent-decode without UTF-8 validation.
62
+ */
63
+ urlDecodeBytes(input: Uint8Array): Uint8Array
64
+ urlEncodeInto(input: Uint8Array, output: Uint8Array): number
65
+ urlDecodeInto(input: Uint8Array, output: Uint8Array): number
66
+ validateEmail(input: Uint8Array): boolean
67
+ validateUuid(input: Uint8Array): boolean
68
+ validateIpv4(input: Uint8Array): boolean
69
+ validateIpv6(input: Uint8Array): boolean
70
+ wsAcceptKey(key: Uint8Array): Uint8Array | string
71
+ wsAcceptKeyInto(key: Uint8Array, output: Uint8Array): number
72
+
73
+ // ── Low-level / packed scalar ──
74
+ httpParseRequestPacked(input: Uint8Array): Uint8Array
75
+ queryParsePacked(input: Uint8Array): Uint8Array
76
+ cookieParsePacked(input: Uint8Array): Uint8Array
77
+ httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number
78
+ queryParsePackedInto(input: Uint8Array, output: Uint8Array): number
79
+ cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number
80
+ /**
81
+ * Parse an application/x-www-form-urlencoded body into packed pairs.
82
+ */
83
+ formParsePacked(input: Uint8Array): Uint8Array
84
+ formParsePackedInto(input: Uint8Array, output: Uint8Array): number
85
+ /**
86
+ * Parse a Content-Type header into a structured media type.
87
+ */
88
+ parseMediaType(input: Uint8Array): MediaTypeResult
89
+ /**
90
+ * Generate a strong/weak ETag (crc32-based).
91
+ */
92
+ etag(data: Uint8Array, weak?: boolean): Uint8Array | string
93
+ etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number
94
+ /**
95
+ * Format a unix timestamp as an IMF-fixdate HTTP-date.
96
+ */
97
+ httpDate(secs?: number): Uint8Array | string
98
+ httpDateInto(secs: number | undefined, output: Uint8Array): number
99
+ parseHttpDate(input: Uint8Array): bigint | null
100
+ parseAcceptEncoding(input: Uint8Array): EncodingPrefResult[]
101
+ /**
102
+ * Base64 encode (standard by default; url-safe/padding configurable).
103
+ */
104
+ base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array | string
105
+ /**
106
+ * Base64 decode (standard); throws on invalid input.
107
+ */
108
+ base64Decode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array
109
+ base64EncodeInto(
110
+ input: Uint8Array,
111
+ output: Uint8Array,
112
+ urlSafe?: boolean,
113
+ padding?: boolean,
114
+ ): number
115
+ base64DecodeInto(
116
+ input: Uint8Array,
117
+ output: Uint8Array,
118
+ urlSafe?: boolean,
119
+ padding?: boolean,
120
+ ): number
121
+ base64UrlEncode(input: Uint8Array): Uint8Array | string
122
+ base64UrlDecode(input: Uint8Array): Uint8Array
123
+ /**
124
+ * Lowercase hex encode.
125
+ */
126
+ hexEncode(input: Uint8Array): Uint8Array | string
127
+ /**
128
+ * Hex decode; throws on odd length or invalid digits.
129
+ */
130
+ hexDecode(input: Uint8Array): Uint8Array
131
+ hexEncodeInto(input: Uint8Array, output: Uint8Array): number
132
+ hexDecodeInto(input: Uint8Array, output: Uint8Array): number
133
+ hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number
134
+ signCookieInto(value: Uint8Array, secret: Uint8Array, output: Uint8Array): number
135
+ verifyCookieInto(signed: Uint8Array, secret: Uint8Array, output: Uint8Array): number | null
136
+ csrfTokenInto(secret: Uint8Array, output: Uint8Array): number
137
+ aeadEncryptInto(
138
+ key: Uint8Array,
139
+ nonce: Uint8Array,
140
+ plaintext: Uint8Array,
141
+ output: Uint8Array,
142
+ algorithm?: string | null,
143
+ ): number
144
+ wsFrameEncodeInto(
145
+ opcode: number,
146
+ payload: Uint8Array,
147
+ mask: boolean,
148
+ fin: boolean,
149
+ output: Uint8Array,
150
+ ): number
151
+ gzipCompressInto(data: Uint8Array, output: Uint8Array, level?: number | null): number
152
+ brotliCompressInto(data: Uint8Array, output: Uint8Array, quality?: number | null): number
153
+ /**
154
+ * Sign a cookie value as `value.signature` (HMAC-SHA256 hex).
155
+ */
156
+ signCookie(value: Uint8Array, secret: Uint8Array): Uint8Array | string
157
+ /**
158
+ * Verify a signed cookie; returns the value or null.
159
+ */
160
+ verifyCookie(signed: Uint8Array, secret: Uint8Array): Uint8Array | string | null
161
+ /**
162
+ * Create a CSRF token (32-byte random hex + HMAC signature).
163
+ */
164
+ csrfToken(secret: Uint8Array): Uint8Array | string
165
+ /**
166
+ * Constant-time verify a CSRF token.
167
+ */
168
+ csrfVerify(token: Uint8Array, secret: Uint8Array): boolean
169
+ /**
170
+ * Resolve a URL reference against a base (RFC 3986).
171
+ */
172
+ urlResolve(base: Uint8Array, reference: Uint8Array): Uint8Array | string
173
+ /**
174
+ * Build a percent-encoded query string from params (sorted keys).
175
+ */
176
+ urlEncodeQuery(params: Record<string, string>): Uint8Array | string
177
+
178
+ // ── Factories / runtime ──
179
+ createSchemaValidator(schema: Uint8Array): SchemaValidatorInstance
180
+ createHmacSigner(key: Uint8Array): HmacSignerInstance
181
+ createTemplateRenderer(source: string): TemplateRendererInstance
182
+ createFormParser(capacity?: number): FormParserInstance
183
+ createMediaTypeParser(): MediaTypeParserInstance
184
+ /**
185
+ * Higher-order conditional-request check: per-resource 304 decision (setup once).
186
+ */
187
+ createConditionalRequest(
188
+ etagValue: Uint8Array,
189
+ lastModifiedSecs?: number,
190
+ ): ConditionalRequestInstance
191
+ /**
192
+ * Higher-order negotiator: precompiles supported encodings once.
193
+ */
194
+ createAcceptNegotiator(supported: string[]): AcceptNegotiatorInstance
195
+ createBase64Codec(urlSafe?: boolean, padding?: boolean): Base64CodecInstance
196
+ createCookieSigner(secret: Uint8Array): CookieSignerInstance
197
+ createCsrfProtector(secret: Uint8Array): CsrfProtectorInstance
198
+ createUrlBuilder(base: Uint8Array): UrlBuilderInstance
199
+ createJwtSigner(secret: Uint8Array, ttlSeconds?: number): JwtSignerInstance
200
+ createAeadCipher(key: Uint8Array, algorithm?: string): AeadCipherInstance
201
+ createArgon2Hasher(options?: PasswordHashOptions | null): Argon2HasherInstance
202
+ createMediaTypeMatcher(expected: Uint8Array): MediaTypeMatcherInstance
203
+ /**
204
+ * Sharded fixed-window per-key rate limiter. `maxEntries` clamps internally
205
+ * (default 1,048,576). Each instance owns an independent budget.
206
+ * @remarks Scalar per-check cost: ~3-14x SLOWER than the JS Map baseline —
207
+ * prefer the ingress pipeline (one FFI for all stages) for native rate
208
+ * limiting in a request path. [measured 2026-08]
209
+ */
210
+ createRateLimiter(
211
+ limit: number,
212
+ windowMs: number,
213
+ maxEntries?: number | null,
214
+ ): RateLimiterInstance
215
+ initThreadPool(rayonThreads?: number): void
216
+ rayonNumThreads(): number
217
+
218
+ // ── Backend-framework scalar features ──
219
+ jwtSign(
220
+ claims: Record<string, unknown>,
221
+ secret: Uint8Array,
222
+ ttlSeconds?: number | null,
223
+ nowSeconds?: number,
224
+ ): Uint8Array
225
+ /**
226
+ * Sign a JWT from pre-serialized claim JSON bytes — skips the napi
227
+ * `serde_json::Value` marshal of `jwtSign` for callers that already hold the
228
+ * bytes (same injection semantics).
229
+ */
230
+ jwtSignBytes(
231
+ claimsJson: Uint8Array,
232
+ secret: Uint8Array,
233
+ ttlSeconds?: number | null,
234
+ nowSeconds?: number,
235
+ ): Uint8Array | string
236
+ jwtSignBytesInto(
237
+ claimsJson: Uint8Array,
238
+ secret: Uint8Array,
239
+ output: Uint8Array,
240
+ ttlSeconds?: number | null,
241
+ nowSeconds?: number,
242
+ ): number
243
+ jwtVerify(token: Uint8Array, secret: Uint8Array, nowSeconds?: number): unknown
244
+ passwordHash(
245
+ password: Uint8Array,
246
+ salt: Uint8Array,
247
+ options?: PasswordHashOptions | null,
248
+ ): Uint8Array | string
249
+ passwordVerify(password: Uint8Array, phc: Uint8Array): boolean
250
+ /**
251
+ * bcrypt password hash → `$2b$` PHC string (cost clamped 4..=31).
252
+ */
253
+ passwordHashBcrypt(password: Uint8Array, cost: number): string
254
+ /**
255
+ * Verify a password against a bcrypt `$2b$` PHC string.
256
+ */
257
+ passwordVerifyBcrypt(password: Uint8Array, hash: string): boolean
258
+ /**
259
+ * PBKDF2-HMAC-SHA256 key derivation (dkLen clamped to 1 MiB).
260
+ */
261
+ pbkdf2Sha256(password: Uint8Array, salt: Uint8Array, rounds: number, dkLen: number): Uint8Array
262
+ aeadEncrypt(
263
+ key: Uint8Array,
264
+ nonce: Uint8Array,
265
+ plaintext: Uint8Array,
266
+ algorithm?: string | null,
267
+ ): Uint8Array
268
+ aeadDecrypt(
269
+ key: Uint8Array,
270
+ nonce: Uint8Array,
271
+ ciphertext: Uint8Array,
272
+ algorithm?: string | null,
273
+ ): Uint8Array | null
274
+ gzipCompress(data: Uint8Array, level?: number | null): Uint8Array
275
+ /**
276
+ * @param maxDecompressed Caps decompressed output (default 64 MiB) as a
277
+ * decompression-bomb guard; a larger value errors.
278
+ */
279
+ gzipDecompress(data: Uint8Array, maxDecompressed?: number | null): Uint8Array
280
+ /**
281
+ * gzip-decompress into `output` (pooled — caller-owned buffer); returns bytes
282
+ * written. Throws when `output` is too small (the exact required size is
283
+ * reported) or the input is invalid / exceeds `maxDecompressed`.
284
+ * @param maxDecompressed Caps decompressed output (default 64 MiB) as a
285
+ * decompression-bomb guard; a larger value errors.
286
+ */
287
+ gzipDecompressInto(data: Uint8Array, output: Uint8Array, maxDecompressed?: number | null): number
288
+ brotliCompress(data: Uint8Array, quality?: number | null): Uint8Array
289
+ /**
290
+ * @param maxDecompressed Caps decompressed output (default 64 MiB) as a
291
+ * decompression-bomb guard; a larger value errors.
292
+ */
293
+ brotliDecompress(data: Uint8Array, maxDecompressed?: number | null): Uint8Array
294
+ /**
295
+ * brotli-decompress into `output` (pooled — caller-owned buffer); returns
296
+ * bytes written. Throws when `output` is too small (the exact required size
297
+ * is reported) or the input is invalid / exceeds `maxDecompressed`.
298
+ * @param maxDecompressed Caps decompressed output (default 64 MiB) as a
299
+ * decompression-bomb guard; a larger value errors.
300
+ */
301
+ brotliDecompressInto(
302
+ data: Uint8Array,
303
+ output: Uint8Array,
304
+ maxDecompressed?: number | null,
305
+ ): number
306
+ multipartParse(body: Uint8Array, boundary: Uint8Array): MultipartPart[]
307
+ multipartParsePacked(body: Uint8Array, boundary: Uint8Array): Uint8Array
308
+ multipartParsePackedInto(body: Uint8Array, boundary: Uint8Array, output: Uint8Array): number
309
+ wsFrameEncode(opcode: number, payload: Uint8Array, mask: boolean, fin: boolean): Uint8Array
310
+ wsFrameDecode(data: Uint8Array): WsFrame | null
311
+ wsFrameDecodePackedInto(data: Uint8Array, output: Uint8Array): number | null
312
+ sseEncodeEvent(
313
+ event: string | null,
314
+ data: Uint8Array,
315
+ id: string | null,
316
+ retry: number | null,
317
+ ): Uint8Array
318
+ sseEncodeEventInto(
319
+ event: string | null,
320
+ data: Uint8Array,
321
+ id: string | null,
322
+ retry: number | null,
323
+ output: Uint8Array,
324
+ ): number
325
+ }