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,459 @@
1
+ // src/rust-ffi/scalar/factories.ts — Compiled-once instance factories + rayon runtime.
2
+ //
3
+ // Every `create*` here constructs a compiled-once native instance (key / schema
4
+ // / template precompiled at construction) plus the rayon thread-pool controls.
5
+
6
+ import type {
7
+ AcceptNegotiatorInstance,
8
+ AeadCipherInstance,
9
+ Argon2HasherInstance,
10
+ Base64CodecInstance,
11
+ ConditionalRequestInstance,
12
+ CookieSignerInstance,
13
+ CsrfProtectorInstance,
14
+ FormParserInstance,
15
+ HmacSignerInstance,
16
+ JwtSignerInstance,
17
+ MediaTypeMatcherInstance,
18
+ MediaTypeParserInstance,
19
+ PasswordHashOptions,
20
+ RateLimiterInstance,
21
+ SchemaValidatorInstance,
22
+ TemplateRendererInstance,
23
+ UrlBuilderInstance,
24
+ } from '../../native'
25
+ import type { BunFFI } from '../../native/ffi'
26
+ import { encoder } from '../../shared/bytes'
27
+ import { decodeUtf8, encodeUtf8 } from '../../shared/codec'
28
+ import type { RustClientContext } from '../context'
29
+ import { asNumber } from '../options'
30
+
31
+ // ── FFI-backed instance wrappers ───────────────────────────────────────────
32
+ // On Bun, instances whose methods have a STATELESS C-ABI sibling are backed by
33
+ // the `bun:ffi` fast path instead of a NAPI instance — cutting the per-call
34
+ // NAPI crossing (~300ns) to ~20ns. The precompiled-state benefit is mostly
35
+ // lost (e.g. the HMAC key is rebuilt per call, ~100ns), but that is still far
36
+ // cheaper than a NAPI crossing, so these win under Bun. Byte-for-byte output is
37
+ // guaranteed by the ffi cross-check suite. NAPI instances remain the fallback
38
+ // (Node / CASTRUM_FFI_MODE=napi / failed self-test). Instances with genuinely
39
+ // stateful precompiled cores (SchemaValidator, TemplateRenderer,
40
+ // AcceptNegotiator, ConditionalRequest, MediaTypeMatcher/Parser, UrlBuilder,
41
+ // RateLimiter) stay NAPI — they need opaque handles (deferred).
42
+
43
+ function ffiHmacSigner(key: Uint8Array, ffi: BunFFI): HmacSignerInstance {
44
+ return {
45
+ sign(data) {
46
+ // Pooled `hmacSha256Into`: writes the 64 hex chars directly into a fresh
47
+ // buffer — no cstring return → no string clone → no encodeUtf8 round-trip
48
+ // (the old path cloned the hex string then re-encoded it to bytes).
49
+ const out = new Uint8Array(64)
50
+ const w = ffi.hmacSha256Into(key, data, out)
51
+ return out.subarray(0, w)
52
+ },
53
+ verify(data, sig) {
54
+ return ffi.hmacSha256Verify(key, data, sig)
55
+ },
56
+ }
57
+ }
58
+
59
+ function ffiBase64Codec(
60
+ urlSafe: boolean | undefined,
61
+ padding: boolean | undefined,
62
+ ffi: BunFFI,
63
+ ): Base64CodecInstance {
64
+ return {
65
+ encode(input) {
66
+ // Pooled `base64EncodeInto`: writes the base64 bytes directly into a
67
+ // fresh buffer sized for the input — no cstring return / re-encode.
68
+ const out = new Uint8Array(Math.ceil(input.length / 3) * 4 + 3)
69
+ const w = ffi.base64EncodeInto(input, out, urlSafe, padding)
70
+ return out.subarray(0, w)
71
+ },
72
+ decode(input) {
73
+ return ffi.base64Decode(input, urlSafe, padding)
74
+ },
75
+ }
76
+ }
77
+
78
+ function ffiCookieSigner(secret: Uint8Array, ffi: BunFFI): CookieSignerInstance {
79
+ return {
80
+ sign(value) {
81
+ // Pooled `signCookieInto`: writes `value.<64-hex>` directly into a fresh
82
+ // buffer — no cstring return / re-encode round-trip.
83
+ const out = new Uint8Array(value.length + 65)
84
+ const w = ffi.signCookieInto(value, secret, out)
85
+ return out.subarray(0, w)
86
+ },
87
+ verify(signed) {
88
+ // Pooled `verifyCookieInto`: writes the verified value directly (the
89
+ // value is a substring of `signed`, so `signed.length + 1` always fits).
90
+ const out = new Uint8Array(signed.length + 1)
91
+ const w = ffi.verifyCookieInto(signed, secret, out)
92
+ return w === null ? null : out.subarray(0, w)
93
+ },
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Opaque-handle `ConditionalRequest`: the napi instance compiles the resource
99
+ * state once (construction); per-call `isNotModified` evaluates it through the
100
+ * C-ABI (`castrum_conditional_is_not_modified`) via the inner handle — no napi
101
+ * crossing. The napi instance MUST stay alive for the handle's lifetime (same
102
+ * contract as the ingress handler / `Ingress::inner_ptr`) — `keepAlive` is
103
+ * referenced by the returned method so the native state is never GC'd while
104
+ * the handle is in use.
105
+ */
106
+ function ffiConditionalRequest(
107
+ napi: ConditionalRequestInstance,
108
+ inner: number,
109
+ ffi: BunFFI,
110
+ ): ConditionalRequestInstance {
111
+ // Referenced by the method below → captured → prevents GC of the native
112
+ // state the opaque handle points into.
113
+ const keepAlive = napi
114
+ return {
115
+ isNotModified(ifNoneMatch, ifModifiedSince) {
116
+ void keepAlive
117
+ return ffi.conditionalIsNotModified(inner, ifNoneMatch ?? null, ifModifiedSince ?? null)
118
+ },
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Opaque-handle `UrlBuilder`: the napi instance parses the base ONCE; per-call
124
+ * `resolve` evaluates it through the C-ABI (`castrum_url_builder_resolve`) via
125
+ * the inner handle — no napi crossing. `keepAlive` references the napi instance
126
+ * so the precompiled base is never GC'd while the handle is in use.
127
+ */
128
+ function ffiUrlBuilder(napi: UrlBuilderInstance, inner: number, ffi: BunFFI): UrlBuilderInstance {
129
+ const keepAlive = napi
130
+ return {
131
+ resolve(reference) {
132
+ void keepAlive
133
+ return ffi.urlBuilderResolve(inner, reference)
134
+ },
135
+ }
136
+ }
137
+
138
+ /** Opaque-handle `MediaTypeMatcher` (expected precompiled once). */
139
+ function ffiMediaTypeMatcher(
140
+ napi: MediaTypeMatcherInstance,
141
+ inner: number,
142
+ ffi: BunFFI,
143
+ ): MediaTypeMatcherInstance {
144
+ const keepAlive = napi
145
+ return {
146
+ matches(actual) {
147
+ void keepAlive
148
+ return ffi.mediaTypeMatcherMatches(inner, actual)
149
+ },
150
+ }
151
+ }
152
+
153
+ /** Opaque-handle `AcceptNegotiator` (supported list precompiled once). */
154
+ function ffiAcceptNegotiator(
155
+ napi: AcceptNegotiatorInstance,
156
+ inner: number,
157
+ ffi: BunFFI,
158
+ ): AcceptNegotiatorInstance {
159
+ const keepAlive = napi
160
+ return {
161
+ negotiate(header) {
162
+ void keepAlive
163
+ return ffi.acceptNegotiatorNegotiate(inner, header)
164
+ },
165
+ }
166
+ }
167
+
168
+ /** Opaque-handle `JwtSigner` (HS256 key + ttl precompiled once). */
169
+ function ffiJwtSigner(napi: JwtSignerInstance, inner: number, ffi: BunFFI): JwtSignerInstance {
170
+ const keepAlive = napi
171
+ return {
172
+ sign(claims, nowSeconds) {
173
+ void keepAlive
174
+ return ffi.jwtSignerSign(inner, encoder.encode(JSON.stringify(claims)), nowSeconds)
175
+ },
176
+ signBytes(claimsJson, nowSeconds) {
177
+ void keepAlive
178
+ return ffi.jwtSignerSign(inner, claimsJson, nowSeconds)
179
+ },
180
+ verify(token, nowSeconds) {
181
+ void keepAlive
182
+ const claims = ffi.jwtSignerVerify(inner, token, nowSeconds)
183
+ return claims === null ? null : (JSON.parse(decodeUtf8(claims)) as unknown)
184
+ },
185
+ }
186
+ }
187
+
188
+ /** Opaque-handle `TemplateRenderer` (template compiled once). */
189
+ function ffiTemplateRenderer(
190
+ napi: TemplateRendererInstance,
191
+ inner: number,
192
+ ffi: BunFFI,
193
+ ): TemplateRendererInstance {
194
+ const keepAlive = napi
195
+ return {
196
+ render(context) {
197
+ void keepAlive
198
+ return ffi.templateRender(inner, encoder.encode(JSON.stringify(context)))
199
+ },
200
+ renderBytes(contextJson) {
201
+ void keepAlive
202
+ return ffi.templateRender(inner, contextJson)
203
+ },
204
+ renderBatchPacked(data) {
205
+ void keepAlive
206
+ return napi.renderBatchPacked(data)
207
+ },
208
+ }
209
+ }
210
+
211
+ /** Opaque-handle `SchemaValidator` (schema compiled once); the hot `validate`
212
+ * is a C-ABI eval; the detailed/batch/derive ops stay on the napi instance. */
213
+ function ffiSchemaValidator(
214
+ napi: SchemaValidatorInstance,
215
+ inner: number,
216
+ ffi: BunFFI,
217
+ ): SchemaValidatorInstance {
218
+ const keepAlive = napi
219
+ return {
220
+ validate(input) {
221
+ void keepAlive
222
+ return ffi.schemaValidatorValidate(inner, input)
223
+ },
224
+ validateDetailed: napi.validateDetailed.bind(napi),
225
+ validateFirstError: napi.validateFirstError.bind(napi),
226
+ validateBatchPackedCount: napi.validateBatchPackedCount.bind(napi),
227
+ validateBatchPackedBitset: napi.validateBatchPackedBitset.bind(napi),
228
+ derive: napi.derive.bind(napi),
229
+ }
230
+ }
231
+
232
+ /** Opaque-handle `RateLimiter` (sharded state compiled once). */
233
+ function ffiRateLimiter(
234
+ napi: RateLimiterInstance,
235
+ inner: number,
236
+ ffi: BunFFI,
237
+ ): RateLimiterInstance {
238
+ const keepAlive = napi
239
+ return {
240
+ check(key, nowMs) {
241
+ void keepAlive
242
+ // `key` is a `cstring` ARG — the engine transcodes the rate-limit key
243
+ // in-engine (no JS-side encode).
244
+ return ffi.rateLimiterCheck(inner, key, nowMs)
245
+ },
246
+ checkKey(key, nowMs) {
247
+ void keepAlive
248
+ return ffi.rateLimiterCheckKey(inner, key, nowMs)
249
+ },
250
+ }
251
+ }
252
+
253
+ function ffiCsrfProtector(secret: Uint8Array, ffi: BunFFI): CsrfProtectorInstance {
254
+ return {
255
+ create() {
256
+ // Pooled `csrfTokenInto`: writes the 129-char `hex.hex` directly into a
257
+ // fresh buffer — no cstring return / re-encode round-trip.
258
+ const out = new Uint8Array(129)
259
+ const w = ffi.csrfTokenInto(secret, out)
260
+ return out.subarray(0, w)
261
+ },
262
+ verify(token) {
263
+ return ffi.csrfVerify(token, secret)
264
+ },
265
+ }
266
+ }
267
+
268
+ function ffiArgon2Hasher(
269
+ options: PasswordHashOptions | null | undefined,
270
+ ffi: BunFFI,
271
+ ): Argon2HasherInstance {
272
+ // Resolve the napi defaults (rust/crypto/argon2.rs resolve_opts) — mirrors
273
+ // the scalar passwordHash FFI wrapper (crypto.ts).
274
+ const o = options ?? {}
275
+ const mCost = o.mCost ?? 19456
276
+ const tCost = o.tCost ?? 2
277
+ const pCost = o.pCost ?? 1
278
+ const outLen = o.outLen ?? 32
279
+ return {
280
+ hash(password, salt) {
281
+ return encodeUtf8(ffi.passwordHash(password, salt, mCost, tCost, pCost, outLen))
282
+ },
283
+ verify(password, phc) {
284
+ return ffi.passwordVerify(password, phc)
285
+ },
286
+ }
287
+ }
288
+
289
+ function ffiAeadCipher(
290
+ key: Uint8Array,
291
+ algorithm: string | undefined,
292
+ ffi: BunFFI,
293
+ ): AeadCipherInstance {
294
+ // Mirror the napi constructor's validation (rust/crypto/aead.rs
295
+ // `resolve_algorithm`): only aes-256-gcm (default) and chacha20-poly1305 are
296
+ // accepted; anything else throws at construction, NOT at first use.
297
+ if (algorithm !== undefined && algorithm !== 'aes-256-gcm' && algorithm !== 'chacha20-poly1305') {
298
+ throw new Error(
299
+ `unsupported aead algorithm: ${algorithm} (expected aes-256-gcm | chacha20-poly1305)`,
300
+ )
301
+ }
302
+ const alg = algorithm === 'chacha20-poly1305' ? 1 : 0
303
+ return {
304
+ encrypt(nonce, plaintext) {
305
+ return ffi.aeadEncrypt(key, nonce, plaintext, alg)
306
+ },
307
+ decrypt(nonce, ciphertext) {
308
+ return ffi.aeadDecrypt(key, nonce, ciphertext, alg)
309
+ },
310
+ }
311
+ }
312
+
313
+ function ffiFormParser(ffi: BunFFI): FormParserInstance {
314
+ return {
315
+ parse(input) {
316
+ // Sizing matches the scalar formParsePacked path (Rust allocator bound
317
+ // `input.len() * 9 + 16`).
318
+ const out = new Uint8Array(input.length * 9 + 16)
319
+ const w = ffi.formParsePackedInto(input, out)
320
+ return out.subarray(0, w)
321
+ },
322
+ parseInto(input, output) {
323
+ return ffi.formParsePackedInto(input, output)
324
+ },
325
+ }
326
+ }
327
+
328
+ /** Compiled-once factory + runtime-control methods (`Pick<RustScalar, ...>`). */
329
+ export function buildFactories(ctx: RustClientContext) {
330
+ const { transport } = ctx.runtime
331
+ const { addon } = ctx
332
+
333
+ return {
334
+ createSchemaValidator(schema: Uint8Array): SchemaValidatorInstance {
335
+ const ffi = transport.ffi
336
+ if (ffi) {
337
+ const napi = new addon.SchemaValidator(schema)
338
+ return ffiSchemaValidator(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
339
+ }
340
+ return new addon.SchemaValidator(schema)
341
+ },
342
+ createHmacSigner(key: Uint8Array): HmacSignerInstance {
343
+ const ffi = transport.ffi
344
+ if (ffi) return ffiHmacSigner(key, ffi)
345
+ return new addon.HmacSigner(key)
346
+ },
347
+ createTemplateRenderer(source: string): TemplateRendererInstance {
348
+ const ffi = transport.ffi
349
+ if (ffi) {
350
+ const napi = new addon.TemplateRenderer(source)
351
+ return ffiTemplateRenderer(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
352
+ }
353
+ return new addon.TemplateRenderer(source)
354
+ },
355
+ createFormParser(capacity?: number): FormParserInstance {
356
+ const ffi = transport.ffi
357
+ if (ffi) return ffiFormParser(ffi)
358
+ return new addon.FormParser(capacity)
359
+ },
360
+ createMediaTypeParser(): MediaTypeParserInstance {
361
+ return new addon.MediaTypeParser()
362
+ },
363
+ createConditionalRequest(
364
+ etagValue: Uint8Array,
365
+ lastModifiedSecs?: number,
366
+ ): ConditionalRequestInstance {
367
+ // FFI-first via the opaque inner handle (the napi instance compiles the
368
+ // state once; per-call `isNotModified` is a C-ABI eval). The wrapper
369
+ // holds the napi instance for the handle's lifetime.
370
+ const ffi = transport.ffi
371
+ if (ffi) {
372
+ const napi = new addon.ConditionalRequest(etagValue, lastModifiedSecs ?? undefined)
373
+ return ffiConditionalRequest(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
374
+ }
375
+ return new addon.ConditionalRequest(etagValue, lastModifiedSecs ?? undefined)
376
+ },
377
+ createAcceptNegotiator(supported: string[]): AcceptNegotiatorInstance {
378
+ const ffi = transport.ffi
379
+ if (ffi) {
380
+ const napi = new addon.AcceptNegotiator(supported)
381
+ return ffiAcceptNegotiator(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
382
+ }
383
+ return new addon.AcceptNegotiator(supported)
384
+ },
385
+ createBase64Codec(urlSafe?: boolean, padding?: boolean): Base64CodecInstance {
386
+ const ffi = transport.ffi
387
+ if (ffi) return ffiBase64Codec(urlSafe, padding, ffi)
388
+ return new addon.Base64Codec(urlSafe ?? undefined, padding ?? undefined)
389
+ },
390
+ createCookieSigner(secret: Uint8Array): CookieSignerInstance {
391
+ const ffi = transport.ffi
392
+ if (ffi) return ffiCookieSigner(secret, ffi)
393
+ return new addon.CookieSigner(secret)
394
+ },
395
+ createCsrfProtector(secret: Uint8Array): CsrfProtectorInstance {
396
+ const ffi = transport.ffi
397
+ if (ffi) return ffiCsrfProtector(secret, ffi)
398
+ return new addon.CsrfProtector(secret)
399
+ },
400
+ createUrlBuilder(base: Uint8Array): UrlBuilderInstance {
401
+ // FFI-first via the opaque inner handle (the napi instance parses the
402
+ // base ONCE; per-call `resolve` is a C-ABI eval). The wrapper holds the
403
+ // napi instance for the handle's lifetime.
404
+ const ffi = transport.ffi
405
+ if (ffi) {
406
+ const napi = new addon.UrlBuilder(base)
407
+ return ffiUrlBuilder(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
408
+ }
409
+ return new addon.UrlBuilder(base)
410
+ },
411
+ createJwtSigner(secret: Uint8Array, ttlSeconds?: number): JwtSignerInstance {
412
+ const ffi = transport.ffi
413
+ if (ffi) {
414
+ const napi = new addon.JwtSigner(secret, ttlSeconds ?? undefined)
415
+ return ffiJwtSigner(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
416
+ }
417
+ return new addon.JwtSigner(secret, ttlSeconds ?? undefined)
418
+ },
419
+ createAeadCipher(key: Uint8Array, algorithm?: string): AeadCipherInstance {
420
+ const ffi = transport.ffi
421
+ if (ffi) return ffiAeadCipher(key, algorithm, ffi)
422
+ return new addon.AeadCipher(key, algorithm ?? undefined)
423
+ },
424
+ createArgon2Hasher(options?: PasswordHashOptions | null): Argon2HasherInstance {
425
+ const ffi = transport.ffi
426
+ if (ffi) return ffiArgon2Hasher(options, ffi)
427
+ return new addon.Argon2Hasher(options ?? undefined)
428
+ },
429
+ createMediaTypeMatcher(expected: Uint8Array): MediaTypeMatcherInstance {
430
+ const ffi = transport.ffi
431
+ if (ffi) {
432
+ const napi = new addon.MediaTypeMatcher(expected)
433
+ return ffiMediaTypeMatcher(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
434
+ }
435
+ return new addon.MediaTypeMatcher(expected)
436
+ },
437
+ createRateLimiter(
438
+ limit: number,
439
+ windowMs: number,
440
+ maxEntries?: number | null,
441
+ ): RateLimiterInstance {
442
+ const ffi = transport.ffi
443
+ if (ffi) {
444
+ const napi = new addon.RateLimiter(limit, windowMs, maxEntries ?? undefined)
445
+ return ffiRateLimiter(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
446
+ }
447
+ return new addon.RateLimiter(limit, windowMs, maxEntries ?? undefined)
448
+ },
449
+ initThreadPool(threads?: number): void {
450
+ // Explicit user call also establishes the pool state locally.
451
+ ctx.markPoolInitialized()
452
+ if (threads !== undefined) ctx.setPendingThreads(threads)
453
+ addon.initThreadPool(threads)
454
+ },
455
+ rayonNumThreads(): number {
456
+ return asNumber(addon.rayonNumThreads() as unknown)
457
+ },
458
+ }
459
+ }
@@ -0,0 +1,74 @@
1
+ // src/rust-ffi/scalar/hashing.ts — Hashing scalar methods.
2
+ //
3
+ // Mirrors rust/crypto/hashing.rs + hmac_sha256.rs: crc32 / fnv1a64 / HMAC.
4
+ // (HMAC reuses the context-cached signer to avoid re-constructing the key.)
5
+ //
6
+ // Runtime dispatch is centralized in the adapter (`ctx.runtime`): the Bun
7
+ // built-in delegations (crc32/xxh3/hmacSha256) come from `builtins.has(op)`
8
+ // and the native call from `transport.resolve(op)` / `transport.ffi`
9
+ // (bun:ffi first, napi fallback) — no inline `isBun()` / `getBunFFI()`.
10
+
11
+ import { toBytes } from '../../shared/bytes'
12
+ import { writeInto } from '../into'
13
+ import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
14
+ import { asBigInt, asNumber } from '../options'
15
+
16
+ /** Hashing scalar methods (`Pick<RustScalar, ...>`). */
17
+ export function buildHashing(ctx: RustClientContext) {
18
+ const { builtins, transport } = ctx.runtime
19
+ // Hoist the immutable per-runtime builtin-delegation decisions into a
20
+ // bind-time map (BUILTIN_OPS is a module constant — never mutated after
21
+ // load) so the hot path reads object properties instead of `Set.has`.
22
+ const HAS = {
23
+ crc32: builtins.has('crc32'),
24
+ xxh3: builtins.has('xxh3'),
25
+ hmacSha256: builtins.has('hmacSha256'),
26
+ }
27
+ // Lazy-memoized ffi surface: binds on first call, single local read after.
28
+ const ffi = memoizeFfi(transport)
29
+ // bun:ffi JIT-calls the C ABI exports with ~1/4 the crossing cost of napi
30
+ // for these sub-µs hashes; the transport resolves ffi-first and caches.
31
+ return {
32
+ crc32(input: Uint8Array): number {
33
+ // Optimal by default under Bun: `Bun.hash.crc32` (native SIMD) beats the
34
+ // rust+FFI crossing (~2.8-8.4x, decision matrix) and matches the
35
+ // `opImpl` recommendation. Node keeps the addon.
36
+ if (HAS.crc32) return builtins.crc32(input)
37
+ return asNumber(resolveNative(ctx, 'crc32')(input)) >>> 0
38
+ },
39
+ fnv1a64(input: Uint8Array): bigint {
40
+ return asBigInt(resolveNative(ctx, 'fnv1a64')(input))
41
+ },
42
+ xxh3(input: Uint8Array): bigint {
43
+ // Optimal by default under Bun: `Bun.hash.xxHash3` beats the rust+FFI
44
+ // crossing (~4x, decision matrix). Node keeps the addon.
45
+ if (HAS.xxh3) return builtins.xxh3(input)
46
+ return asBigInt(resolveNative(ctx, 'xxh3')(input))
47
+ },
48
+ hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array | string {
49
+ // Optimal by default under Bun: `Bun.CryptoHasher` (native) beats the
50
+ // rust+FFI crossing (~1.2x, decision matrix). Bun returns the 64-char
51
+ // lowercase-hex STRING (native transfer — no TextEncoder round-trip); the
52
+ // addon returns the same hex as bytes. Node keeps the FFI path.
53
+ if (HAS.hmacSha256) return builtins.hmacSha256(key, data)
54
+ const f = ffi()
55
+ if (f) return f.hmacSha256(key, data)
56
+ return ctx.hmacSigner(key).sign(data)
57
+ },
58
+ hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number {
59
+ const f = ffi()
60
+ if (f) return f.hmacSha256Into(key, data, output)
61
+ return writeInto('hmac sha256', output, ctx.hmacSigner(key).sign(data))
62
+ },
63
+ hmacSha256Verify(key: Uint8Array, data: Uint8Array, sig: Uint8Array | string): boolean {
64
+ // `hmacSha256` returns the signature as a hex STRING under Bun (built-in
65
+ // delegation / cstring) and as hex bytes under napi. The verifier accepts
66
+ // both — normalize the string to hex bytes so the ffi/napi cores see the
67
+ // same input (sign → verify is self-consistent on every transport).
68
+ const sigBytes = toBytes(sig)
69
+ const f = ffi()
70
+ if (f) return f.hmacSha256Verify(key, data, sigBytes)
71
+ return ctx.hmacSigner(key).verify(data, sigBytes)
72
+ },
73
+ }
74
+ }