castrum 0.9.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 (102) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1374 -1772
  4. package/dist/index.js +5570 -4169
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +106 -14
  7. package/docs/BENCHMARKS.md +83 -47
  8. package/docs/CASE_STUDY.md +240 -0
  9. package/docs/ENVIRONMENT.md +8 -6
  10. package/docs/FFI_BUN_GUIDE.md +327 -0
  11. package/docs/GETTING_STARTED.md +15 -32
  12. package/docs/INGRESS-ROUTER.md +81 -0
  13. package/docs/INGRESS.md +5 -4
  14. package/docs/NATIVE-ROUTE.md +53 -0
  15. package/docs/REPO_MAP.md +46 -26
  16. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  17. package/docs/bun-builtins-decision-matrix.md +25 -18
  18. package/index.ts +9 -16
  19. package/package.json +36 -24
  20. package/src/ingress/body.ts +167 -27
  21. package/src/ingress/handlers.ts +91 -15
  22. package/src/ingress/health.ts +1 -0
  23. package/src/ingress/index.ts +39 -27
  24. package/src/ingress/packing/gather-raw-headers.ts +85 -21
  25. package/src/ingress/packing/header-packing.ts +5 -5
  26. package/src/ingress/packing/input-packer.ts +17 -3
  27. package/src/ingress/packing/select-headers.ts +12 -6
  28. package/src/ingress/router.ts +237 -0
  29. package/src/ingress/routes/common.ts +42 -1
  30. package/src/ingress/routes/fallback.ts +2 -2
  31. package/src/ingress/routes/head.ts +3 -13
  32. package/src/ingress/routes/index.ts +5 -4
  33. package/src/ingress/routes/json-write.ts +7 -31
  34. package/src/ingress/routes/options.ts +2 -12
  35. package/src/ingress/routes/read.ts +3 -13
  36. package/src/ingress/routes/responder.ts +159 -0
  37. package/src/ingress/server-node.ts +6 -6
  38. package/src/ingress/server.ts +45 -13
  39. package/src/ingress/types.ts +35 -1
  40. package/src/integration/pipeline.ts +3 -0
  41. package/src/integration/streaming.ts +2 -3
  42. package/src/integration/websocket.ts +4 -3
  43. package/src/loader/batch.ts +4 -0
  44. package/src/loader/cost.ts +1 -0
  45. package/src/loader/index.ts +43 -228
  46. package/src/loader/state.ts +84 -0
  47. package/src/loader/types.ts +145 -0
  48. package/src/native/ffi/constants.ts +58 -0
  49. package/src/native/ffi/selftest.ts +729 -0
  50. package/src/native/ffi/types.ts +474 -0
  51. package/src/native/ffi.ts +1063 -829
  52. package/src/native/loader.ts +52 -6
  53. package/src/native/types/instances.ts +274 -0
  54. package/src/native/types.ts +41 -238
  55. package/src/runtime/builtins.ts +131 -0
  56. package/src/runtime/codec.ts +147 -0
  57. package/src/runtime/detect.ts +36 -0
  58. package/src/runtime/index.ts +28 -0
  59. package/src/runtime/native.ts +35 -0
  60. package/src/runtime/server.ts +31 -0
  61. package/src/runtime/transport.ts +64 -0
  62. package/src/runtime/types.ts +173 -0
  63. package/src/runtime/uuid.ts +45 -0
  64. package/src/runtime/websocket.ts +30 -0
  65. package/src/rust-ffi/addon.ts +1 -1
  66. package/src/rust-ffi/batch/build.ts +155 -0
  67. package/src/rust-ffi/batch/index.ts +8 -0
  68. package/src/rust-ffi/batch/types.ts +92 -0
  69. package/src/rust-ffi/client.ts +5 -6
  70. package/src/rust-ffi/context.ts +39 -10
  71. package/src/rust-ffi/index.ts +28 -17
  72. package/src/rust-ffi/into.ts +21 -0
  73. package/src/rust-ffi/options.ts +1 -1
  74. package/src/rust-ffi/packed.ts +2 -2
  75. package/src/rust-ffi/proven.ts +16 -30
  76. package/src/rust-ffi/scalar/crypto.ts +188 -107
  77. package/src/rust-ffi/scalar/factories.ts +253 -27
  78. package/src/rust-ffi/scalar/hashing.ts +43 -41
  79. package/src/rust-ffi/scalar/http.ts +208 -77
  80. package/src/rust-ffi/scalar/index.ts +3 -3
  81. package/src/rust-ffi/scalar/interface.ts +88 -213
  82. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  83. package/src/rust-ffi/scalar/json.ts +28 -12
  84. package/src/rust-ffi/scalar/payload.ts +159 -42
  85. package/src/rust-ffi/text.ts +31 -5
  86. package/src/selection.ts +27 -31
  87. package/src/shared/adaptive.ts +1 -2
  88. package/src/shared/buffer-pool.ts +72 -17
  89. package/src/shared/bytes.ts +75 -6
  90. package/src/shared/codec.ts +78 -0
  91. package/src/shared/env.ts +4 -4
  92. package/src/shared/log.ts +1 -0
  93. package/src/shared/packed/index.ts +10 -0
  94. package/src/shared/packed/parsers.ts +31 -0
  95. package/src/shared/packed/schema.ts +23 -0
  96. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  97. package/src/shared/proven.ts +189 -496
  98. package/src/shared/runtime.ts +20 -25
  99. package/src/shared/uuid.ts +12 -28
  100. package/src/data/json-rows.ts +0 -33
  101. package/src/rust-ffi/batch.ts +0 -431
  102. package/src/shared/bench-classify.ts +0 -209
@@ -2,48 +2,71 @@
2
2
  //
3
3
  // Mirrors rust/crypto/*: base64/hex codecs, signed cookies, CSRF, random
4
4
  // tokens, JWT, password hashing and AEAD.
5
+ //
6
+ // Runtime dispatch is centralized in the adapter (`ctx.runtime`): Bun built-in
7
+ // delegations (randomToken/base64*/hexEncode) come from `builtins.has(op)` and
8
+ // the native call from `transport.resolve(op)` / `transport.ffi` (bun:ffi
9
+ // first, napi fallback) — no inline `isBun()` / `getBunFFI()`.
5
10
 
6
11
  import type { PasswordHashOptions } from '../../native'
7
- import { getBunFFI } from '../../native/ffi'
8
- import { isBun } from '../../shared/runtime'
9
- import type { RustClientContext } from '../context'
10
-
11
- const encoder = new TextEncoder()
12
- const decoder = new TextDecoder()
12
+ import { encoder } from '../../shared/bytes'
13
+ import { writeInto } from '../into'
14
+ import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
13
15
 
14
16
  /** Auth / crypto scalar methods (`Pick<RustScalar, ...>`). */
15
17
  export function buildCrypto(ctx: RustClientContext) {
16
18
  const { addon } = ctx
19
+ const { builtins, transport } = ctx.runtime
20
+ // Hoist the immutable per-runtime builtin-delegation decisions into a
21
+ // bind-time map (BUILTIN_OPS is a module constant — never mutated after
22
+ // load) so the hot path reads object properties instead of `Set.has`.
23
+ const HAS = {
24
+ randomToken: builtins.has('randomToken'),
25
+ base64Encode: builtins.has('base64Encode'),
26
+ base64UrlEncode: builtins.has('base64UrlEncode'),
27
+ hexEncode: builtins.has('hexEncode'),
28
+ }
29
+ // Lazy-memoized ffi surface: binds on first call, single local read after.
30
+ const ffi = memoizeFfi(transport)
17
31
 
18
32
  return {
19
- randomToken(byteLen: number): Uint8Array {
33
+ randomToken(byteLen: number): Uint8Array | string {
20
34
  // Optimal by default under Bun: `crypto.getRandomValues` + native hex
21
- // beats the rust+FFI crossing for token-sized draws (decision matrix).
22
- // Preserves the native 16 MiB allocation guard and the hex format
23
- // (2n bytes). Node keeps the addon.
24
- if (isBun()) {
25
- if (byteLen > 16 * 1024 * 1024) {
26
- throw new RangeError('randomToken: byteLen exceeds 16 MiB limit')
27
- }
28
- return encoder.encode(
29
- Buffer.from(crypto.getRandomValues(new Uint8Array(byteLen))).toString('hex'),
30
- )
35
+ // beats the rust+FFI crossing for token-sized draws (decision matrix);
36
+ // the builtin carries the 16 MiB guard and returns the hex STRING. Node
37
+ // keeps the addon.
38
+ if (HAS.randomToken) return builtins.randomToken(byteLen)
39
+ return resolveNative(ctx, 'randomToken')(byteLen) as Uint8Array | string
40
+ },
41
+ randomTokenInto(byteLen: number, output: Uint8Array): number {
42
+ // Pooled sibling — writes `byteLen*2` hex chars into `output`. FFI path
43
+ // uses the native `_into` (no cstring round-trip); without ffi the Bun
44
+ // builtin (or addon) hex is written into the caller buffer.
45
+ const f = ffi()
46
+ if (f) return f.randomTokenInto(byteLen, output)
47
+ if (HAS.randomToken) {
48
+ const need = byteLen * 2
49
+ if (output.length < need) throw new Error('random token: output buffer too small')
50
+ output.set(encoder.encode(builtins.randomToken(byteLen) as string))
51
+ return need
31
52
  }
32
- const ffi = getBunFFI()
33
- if (ffi) return ffi.randomToken(byteLen)
34
- return addon.randomToken(byteLen)
53
+ const bytes = addon.randomToken(byteLen)
54
+ return writeInto('random token', output, bytes)
35
55
  },
36
- base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array {
56
+ base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array | string {
37
57
  // Bun's Buffer base64 (SIMD) beats the rust+FFI crossing (~2x, measured)
38
- // for the standard (url-safe=false, padded) case; byte-parity pinned by
39
- // test/unit/features/encoding.test.ts. url-safe/unpadded falls through to
40
- // the native path (Buffer can't express base64url without string surgery).
41
- if (isBun() && !urlSafe && padding !== false) {
42
- return encoder.encode(Buffer.from(input).toString('base64'))
58
+ // for the standard (url-safe=false, padded) case; the builtin returns
59
+ // `undefined` for url-safe/unpadded so those fall through to native.
60
+ // Parity pinned by test/unit/features/encoding.test.ts.
61
+ if (HAS.base64Encode) {
62
+ const encoded = builtins.base64Encode(input, urlSafe, padding)
63
+ if (encoded !== undefined) return encoded
43
64
  }
44
- const ffi = getBunFFI()
45
- if (ffi) return ffi.base64Encode(input, urlSafe, padding)
46
- return addon.base64Encode(input, urlSafe ?? undefined, padding ?? undefined)
65
+ return resolveNative(ctx, 'base64Encode')(
66
+ input,
67
+ urlSafe ?? undefined,
68
+ padding ?? undefined,
69
+ ) as Uint8Array | string
47
70
  },
48
71
  base64EncodeInto(
49
72
  input: Uint8Array,
@@ -51,14 +74,19 @@ export function buildCrypto(ctx: RustClientContext) {
51
74
  urlSafe?: boolean,
52
75
  padding?: boolean,
53
76
  ): number {
54
- const ffi = getBunFFI()
55
- if (ffi) return ffi.base64EncodeInto(input, output, urlSafe, padding)
56
- return addon.base64EncodeInto(input, output, urlSafe ?? undefined, padding ?? undefined)
77
+ return resolveNative(ctx, 'base64EncodeInto')(
78
+ input,
79
+ output,
80
+ urlSafe ?? undefined,
81
+ padding ?? undefined,
82
+ ) as number
57
83
  },
58
84
  base64Decode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array {
59
- const ffi = getBunFFI()
60
- if (ffi) return ffi.base64Decode(input, urlSafe, padding)
61
- return addon.base64Decode(input, urlSafe ?? undefined, padding ?? undefined)
85
+ return resolveNative(ctx, 'base64Decode')(
86
+ input,
87
+ urlSafe ?? undefined,
88
+ padding ?? undefined,
89
+ ) as Uint8Array
62
90
  },
63
91
  base64DecodeInto(
64
92
  input: Uint8Array,
@@ -66,69 +94,73 @@ export function buildCrypto(ctx: RustClientContext) {
66
94
  urlSafe?: boolean,
67
95
  padding?: boolean,
68
96
  ): number {
69
- const ffi = getBunFFI()
70
- if (ffi) return ffi.base64DecodeInto(input, output, urlSafe, padding)
71
- return addon.base64DecodeInto(input, output, urlSafe ?? undefined, padding ?? undefined)
97
+ return resolveNative(ctx, 'base64DecodeInto')(
98
+ input,
99
+ output,
100
+ urlSafe ?? undefined,
101
+ padding ?? undefined,
102
+ ) as number
72
103
  },
73
- base64UrlEncode(input: Uint8Array): Uint8Array {
74
- const ffi = getBunFFI()
75
- if (ffi) return ffi.base64Encode(input, true, false)
104
+ base64UrlEncode(input: Uint8Array): Uint8Array | string {
105
+ // Bun's native `Buffer.toString('base64url')` (SIMD) beats the rust+FFI
106
+ // crossing — same delegation rationale as `base64Encode`; verified
107
+ // byte-parity with the Rust `URL_SAFE_NO_PAD` engine
108
+ // (test/unit/features/encoding.test.ts). The ffi path reuses
109
+ // `base64Encode(input, true, false)` (no dedicated base64UrlEncode symbol).
110
+ if (HAS.base64UrlEncode) return builtins.base64UrlEncode(input)
111
+ const f = ffi()
112
+ if (f) return f.base64Encode(input, true, false)
76
113
  return addon.base64UrlEncode(input)
77
114
  },
78
115
  base64UrlDecode(input: Uint8Array): Uint8Array {
79
- const ffi = getBunFFI()
80
- if (ffi) return ffi.base64Decode(input, true, false)
116
+ const f = ffi()
117
+ if (f) return f.base64Decode(input, true, false)
81
118
  return addon.base64UrlDecode(input)
82
119
  },
83
- hexEncode(input: Uint8Array): Uint8Array {
84
- // Bun fast path writes straight into a fresh buffer — no napi Buffer
85
- // alloc + copy (~13x cheaper crossing for small inputs).
86
- const ffi = getBunFFI()
87
- if (ffi) return ffi.hexEncode(input)
88
- return addon.hexEncode(input)
120
+ hexEncode(input: Uint8Array): Uint8Array | string {
121
+ // Bun's native `Buffer.toString('hex')` (SIMD) beats the rust+FFI
122
+ // crossing, especially for large inputs. Verified byte-parity with the
123
+ // Rust hex encoder. Bun returns the hex STRING; Node keeps the addon bytes.
124
+ if (HAS.hexEncode) return builtins.hexEncode(input)
125
+ return resolveNative(ctx, 'hexEncode')(input) as Uint8Array | string
89
126
  },
90
127
  hexEncodeInto(input: Uint8Array, output: Uint8Array): number {
91
- const ffi = getBunFFI()
92
- if (ffi) return ffi.hexEncodeInto(input, output)
93
- return addon.hexEncodeInto(input, output)
128
+ return resolveNative(ctx, 'hexEncodeInto')(input, output) as number
94
129
  },
95
130
  hexDecode(input: Uint8Array): Uint8Array {
96
- const ffi = getBunFFI()
97
- if (ffi) return ffi.hexDecode(input)
98
- return addon.hexDecode(input)
131
+ return resolveNative(ctx, 'hexDecode')(input) as Uint8Array
99
132
  },
100
133
  hexDecodeInto(input: Uint8Array, output: Uint8Array): number {
101
- const ffi = getBunFFI()
102
- if (ffi) return ffi.hexDecodeInto(input, output)
103
- return addon.hexDecodeInto(input, output)
134
+ return resolveNative(ctx, 'hexDecodeInto')(input, output) as number
104
135
  },
105
- signCookie(value: Uint8Array, secret: Uint8Array): Uint8Array {
106
- const ffi = getBunFFI()
107
- if (ffi) return ffi.signCookie(value, secret)
108
- return addon.signCookie(value, secret)
136
+ signCookie(value: Uint8Array, secret: Uint8Array): Uint8Array | string {
137
+ return resolveNative(ctx, 'signCookie')(value, secret) as Uint8Array | string
109
138
  },
110
139
  signCookieInto(value: Uint8Array, secret: Uint8Array, output: Uint8Array): number {
111
- const ffi = getBunFFI()
112
- if (ffi) return ffi.signCookieInto(value, secret, output)
113
- const bytes = addon.signCookie(value, secret)
114
- if (output.length < bytes.length) throw new Error('sign cookie: output buffer too small')
115
- output.set(bytes)
116
- return bytes.length
140
+ const f = ffi()
141
+ if (f) return f.signCookieInto(value, secret, output)
142
+ return writeInto('sign cookie', output, addon.signCookie(value, secret))
143
+ },
144
+ verifyCookie(signed: Uint8Array, secret: Uint8Array): Uint8Array | string | null {
145
+ return resolveNative(ctx, 'verifyCookie')(signed, secret) as Uint8Array | string | null
146
+ },
147
+ verifyCookieInto(signed: Uint8Array, secret: Uint8Array, output: Uint8Array): number | null {
148
+ const f = ffi()
149
+ if (f) return f.verifyCookieInto(signed, secret, output)
150
+ const value = addon.verifyCookie(signed, secret)
151
+ if (value === null) return null
152
+ return writeInto('verify cookie', output, value)
117
153
  },
118
- verifyCookie(signed: Uint8Array, secret: Uint8Array): Uint8Array | null {
119
- const ffi = getBunFFI()
120
- if (ffi) return ffi.verifyCookie(signed, secret)
121
- return addon.verifyCookie(signed, secret)
154
+ csrfToken(secret: Uint8Array): Uint8Array | string {
155
+ return resolveNative(ctx, 'csrfToken')(secret) as Uint8Array | string
122
156
  },
123
- csrfToken(secret: Uint8Array): Uint8Array {
124
- const ffi = getBunFFI()
125
- if (ffi) return ffi.csrfToken(secret)
126
- return addon.csrfToken(secret)
157
+ csrfTokenInto(secret: Uint8Array, output: Uint8Array): number {
158
+ const f = ffi()
159
+ if (f) return f.csrfTokenInto(secret, output)
160
+ return writeInto('csrf token', output, addon.csrfToken(secret))
127
161
  },
128
162
  csrfVerify(token: Uint8Array, secret: Uint8Array): boolean {
129
- const ffi = getBunFFI()
130
- if (ffi) return ffi.csrfVerify(token, secret)
131
- return addon.csrfVerify(token, secret)
163
+ return resolveNative(ctx, 'csrfVerify')(token, secret) as boolean
132
164
  },
133
165
  jwtSign(
134
166
  claims: Record<string, unknown>,
@@ -136,6 +168,23 @@ export function buildCrypto(ctx: RustClientContext) {
136
168
  ttlSeconds?: number | null,
137
169
  nowSeconds?: number,
138
170
  ): Uint8Array {
171
+ // FFI-first: route the object claims through the C-ABI `jwtSignBytes`
172
+ // (pre-serialized JSON). The engine returns the token as a string
173
+ // (native transfer); re-encode to bytes to keep the public BYTE contract
174
+ // (batch/benchmark consumers compare bytes). NOTE: JSON.stringify
175
+ // preserves JS insertion order while napi's serde_json sorts keys — for
176
+ // multi-key claims the two transports emit different-but-equally-valid
177
+ // tokens (both verify; parity tests cross-verify semantically).
178
+ const f = ffi()
179
+ if (f) {
180
+ const token = f.jwtSignBytes(
181
+ encoder.encode(JSON.stringify(claims)),
182
+ secret,
183
+ ttlSeconds ?? 0,
184
+ nowSeconds ?? Math.floor(Date.now() / 1000),
185
+ )
186
+ return encoder.encode(token)
187
+ }
139
188
  return addon.jwtSign(
140
189
  claims,
141
190
  secret,
@@ -148,12 +197,12 @@ export function buildCrypto(ctx: RustClientContext) {
148
197
  secret: Uint8Array,
149
198
  ttlSeconds?: number | null,
150
199
  nowSeconds?: number,
151
- ): Uint8Array {
152
- const ffi = getBunFFI()
153
- if (ffi) {
200
+ ): Uint8Array | string {
201
+ const f = ffi()
202
+ if (f) {
154
203
  // The C-ABI `ttl <= 0` sentinel matches napi's Option<i64> (inject only
155
204
  // when positive), and nowSeconds defaults identically.
156
- return ffi.jwtSignBytes(
205
+ return f.jwtSignBytes(
157
206
  claimsJson,
158
207
  secret,
159
208
  ttlSeconds ?? 0,
@@ -167,19 +216,55 @@ export function buildCrypto(ctx: RustClientContext) {
167
216
  nowSeconds ?? Math.floor(Date.now() / 1000),
168
217
  )
169
218
  },
219
+ jwtSignBytesInto(
220
+ claimsJson: Uint8Array,
221
+ secret: Uint8Array,
222
+ output: Uint8Array,
223
+ ttlSeconds?: number | null,
224
+ nowSeconds?: number,
225
+ ): number {
226
+ const f = ffi()
227
+ if (f) {
228
+ return f.jwtSignBytesInto(
229
+ claimsJson,
230
+ secret,
231
+ ttlSeconds ?? 0,
232
+ nowSeconds ?? Math.floor(Date.now() / 1000),
233
+ output,
234
+ )
235
+ }
236
+ return writeInto(
237
+ 'jwt sign',
238
+ output,
239
+ addon.jwtSignBytes(
240
+ claimsJson,
241
+ secret,
242
+ ttlSeconds ?? null,
243
+ nowSeconds ?? Math.floor(Date.now() / 1000),
244
+ ),
245
+ )
246
+ },
170
247
  jwtVerify(token: Uint8Array, secret: Uint8Array, nowSeconds?: number): unknown {
248
+ const f = ffi()
249
+ if (f) {
250
+ // Verify via the FFI cstring path (castrum_jwt_verify): the engine
251
+ // clones the claims JSON string at return (zero decode), and `null` =
252
+ // invalid signature / expired / malformed → null (napi Option parity).
253
+ const claims = f.jwtVerify(token, secret, nowSeconds ?? Math.floor(Date.now() / 1000))
254
+ return claims === null ? null : (JSON.parse(claims) as unknown)
255
+ }
171
256
  return addon.jwtVerify(token, secret, nowSeconds ?? Math.floor(Date.now() / 1000))
172
257
  },
173
258
  passwordHash(
174
259
  password: Uint8Array,
175
260
  salt: Uint8Array,
176
261
  options?: PasswordHashOptions | null,
177
- ): Uint8Array {
178
- const ffi = getBunFFI()
179
- if (ffi) {
262
+ ): Uint8Array | string {
263
+ const f = ffi()
264
+ if (f) {
180
265
  // Resolve the napi defaults (rust/crypto/argon2.rs resolve_opts).
181
266
  const o = options ?? {}
182
- return ffi.passwordHash(
267
+ return f.passwordHash(
183
268
  password,
184
269
  salt,
185
270
  o.mCost ?? 19456,
@@ -191,18 +276,16 @@ export function buildCrypto(ctx: RustClientContext) {
191
276
  return addon.passwordHash(password, salt, options ?? null)
192
277
  },
193
278
  passwordVerify(password: Uint8Array, phc: Uint8Array): boolean {
194
- const ffi = getBunFFI()
195
- if (ffi) return ffi.passwordVerify(password, phc)
196
- return addon.passwordVerify(password, phc)
279
+ return resolveNative(ctx, 'passwordVerify')(password, phc) as boolean
197
280
  },
198
281
  passwordHashBcrypt(password: Uint8Array, cost: number): string {
199
- const ffi = getBunFFI()
200
- if (ffi) return decoder.decode(ffi.passwordHashBcrypt(password, cost))
201
- return addon.passwordHashBcrypt(password, cost)
282
+ return resolveNative(ctx, 'passwordHashBcrypt')(password, cost) as string
202
283
  },
203
284
  passwordVerifyBcrypt(password: Uint8Array, hash: string): boolean {
204
- const ffi = getBunFFI()
205
- if (ffi) return ffi.passwordVerifyBcrypt(password, encoder.encode(hash))
285
+ // FFI-first: `hash` is a `cstring` ARG (the engine transcodes the PHC
286
+ // string in-engine — no JS encode); `password` stays `(ptr,len)` bytes.
287
+ const f = ffi()
288
+ if (f) return f.passwordVerifyBcrypt(password, hash)
206
289
  return addon.passwordVerifyBcrypt(password, hash)
207
290
  },
208
291
  pbkdf2Sha256(
@@ -211,9 +294,7 @@ export function buildCrypto(ctx: RustClientContext) {
211
294
  rounds: number,
212
295
  dkLen: number,
213
296
  ): Uint8Array {
214
- const ffi = getBunFFI()
215
- if (ffi) return ffi.pbkdf2Sha256(password, salt, rounds, dkLen)
216
- return addon.pbkdf2Sha256(password, salt, rounds, dkLen)
297
+ return resolveNative(ctx, 'pbkdf2Sha256')(password, salt, rounds, dkLen) as Uint8Array
217
298
  },
218
299
  aeadEncrypt(
219
300
  key: Uint8Array,
@@ -221,9 +302,9 @@ export function buildCrypto(ctx: RustClientContext) {
221
302
  plaintext: Uint8Array,
222
303
  algorithm?: string | null,
223
304
  ): Uint8Array {
224
- const ffi = getBunFFI()
225
- if (ffi) {
226
- return ffi.aeadEncrypt(key, nonce, plaintext, algorithm === 'chacha20-poly1305' ? 1 : 0)
305
+ const f = ffi()
306
+ if (f) {
307
+ return f.aeadEncrypt(key, nonce, plaintext, algorithm === 'chacha20-poly1305' ? 1 : 0)
227
308
  }
228
309
  return addon.aeadEncrypt(key, nonce, plaintext, algorithm ?? null)
229
310
  },
@@ -234,9 +315,9 @@ export function buildCrypto(ctx: RustClientContext) {
234
315
  output: Uint8Array,
235
316
  algorithm?: string | null,
236
317
  ): number {
237
- const ffi = getBunFFI()
238
- if (ffi) {
239
- return ffi.aeadEncryptInto(
318
+ const f = ffi()
319
+ if (f) {
320
+ return f.aeadEncryptInto(
240
321
  key,
241
322
  nonce,
242
323
  plaintext,
@@ -255,9 +336,9 @@ export function buildCrypto(ctx: RustClientContext) {
255
336
  ciphertext: Uint8Array,
256
337
  algorithm?: string | null,
257
338
  ): Uint8Array | null {
258
- const ffi = getBunFFI()
259
- if (ffi) {
260
- return ffi.aeadDecrypt(key, nonce, ciphertext, algorithm === 'chacha20-poly1305' ? 1 : 0)
339
+ const f = ffi()
340
+ if (f) {
341
+ return f.aeadDecrypt(key, nonce, ciphertext, algorithm === 'chacha20-poly1305' ? 1 : 0)
261
342
  }
262
343
  return addon.aeadDecrypt(key, nonce, ciphertext, algorithm ?? null)
263
344
  },