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,346 @@
1
+ // src/rust-ffi/scalar/crypto.ts — Auth / crypto scalar methods.
2
+ //
3
+ // Mirrors rust/crypto/*: base64/hex codecs, signed cookies, CSRF, random
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()`.
10
+
11
+ import type { PasswordHashOptions } from '../../native'
12
+ import { encoder } from '../../shared/bytes'
13
+ import { writeInto } from '../into'
14
+ import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
15
+
16
+ /** Auth / crypto scalar methods (`Pick<RustScalar, ...>`). */
17
+ export function buildCrypto(ctx: RustClientContext) {
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)
31
+
32
+ return {
33
+ randomToken(byteLen: number): Uint8Array | string {
34
+ // Optimal by default under Bun: `crypto.getRandomValues` + native hex
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
52
+ }
53
+ const bytes = addon.randomToken(byteLen)
54
+ return writeInto('random token', output, bytes)
55
+ },
56
+ base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array | string {
57
+ // Bun's Buffer base64 (SIMD) beats the rust+FFI crossing (~2x, measured)
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
64
+ }
65
+ return resolveNative(ctx, 'base64Encode')(
66
+ input,
67
+ urlSafe ?? undefined,
68
+ padding ?? undefined,
69
+ ) as Uint8Array | string
70
+ },
71
+ base64EncodeInto(
72
+ input: Uint8Array,
73
+ output: Uint8Array,
74
+ urlSafe?: boolean,
75
+ padding?: boolean,
76
+ ): number {
77
+ return resolveNative(ctx, 'base64EncodeInto')(
78
+ input,
79
+ output,
80
+ urlSafe ?? undefined,
81
+ padding ?? undefined,
82
+ ) as number
83
+ },
84
+ base64Decode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array {
85
+ return resolveNative(ctx, 'base64Decode')(
86
+ input,
87
+ urlSafe ?? undefined,
88
+ padding ?? undefined,
89
+ ) as Uint8Array
90
+ },
91
+ base64DecodeInto(
92
+ input: Uint8Array,
93
+ output: Uint8Array,
94
+ urlSafe?: boolean,
95
+ padding?: boolean,
96
+ ): number {
97
+ return resolveNative(ctx, 'base64DecodeInto')(
98
+ input,
99
+ output,
100
+ urlSafe ?? undefined,
101
+ padding ?? undefined,
102
+ ) as number
103
+ },
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)
113
+ return addon.base64UrlEncode(input)
114
+ },
115
+ base64UrlDecode(input: Uint8Array): Uint8Array {
116
+ const f = ffi()
117
+ if (f) return f.base64Decode(input, true, false)
118
+ return addon.base64UrlDecode(input)
119
+ },
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
126
+ },
127
+ hexEncodeInto(input: Uint8Array, output: Uint8Array): number {
128
+ return resolveNative(ctx, 'hexEncodeInto')(input, output) as number
129
+ },
130
+ hexDecode(input: Uint8Array): Uint8Array {
131
+ return resolveNative(ctx, 'hexDecode')(input) as Uint8Array
132
+ },
133
+ hexDecodeInto(input: Uint8Array, output: Uint8Array): number {
134
+ return resolveNative(ctx, 'hexDecodeInto')(input, output) as number
135
+ },
136
+ signCookie(value: Uint8Array, secret: Uint8Array): Uint8Array | string {
137
+ return resolveNative(ctx, 'signCookie')(value, secret) as Uint8Array | string
138
+ },
139
+ signCookieInto(value: Uint8Array, secret: Uint8Array, output: Uint8Array): number {
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)
153
+ },
154
+ csrfToken(secret: Uint8Array): Uint8Array | string {
155
+ return resolveNative(ctx, 'csrfToken')(secret) as Uint8Array | string
156
+ },
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))
161
+ },
162
+ csrfVerify(token: Uint8Array, secret: Uint8Array): boolean {
163
+ return resolveNative(ctx, 'csrfVerify')(token, secret) as boolean
164
+ },
165
+ jwtSign(
166
+ claims: Record<string, unknown>,
167
+ secret: Uint8Array,
168
+ ttlSeconds?: number | null,
169
+ nowSeconds?: number,
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
+ }
188
+ return addon.jwtSign(
189
+ claims,
190
+ secret,
191
+ ttlSeconds ?? null,
192
+ nowSeconds ?? Math.floor(Date.now() / 1000),
193
+ )
194
+ },
195
+ jwtSignBytes(
196
+ claimsJson: Uint8Array,
197
+ secret: Uint8Array,
198
+ ttlSeconds?: number | null,
199
+ nowSeconds?: number,
200
+ ): Uint8Array | string {
201
+ const f = ffi()
202
+ if (f) {
203
+ // The C-ABI `ttl <= 0` sentinel matches napi's Option<i64> (inject only
204
+ // when positive), and nowSeconds defaults identically.
205
+ return f.jwtSignBytes(
206
+ claimsJson,
207
+ secret,
208
+ ttlSeconds ?? 0,
209
+ nowSeconds ?? Math.floor(Date.now() / 1000),
210
+ )
211
+ }
212
+ return addon.jwtSignBytes(
213
+ claimsJson,
214
+ secret,
215
+ ttlSeconds ?? null,
216
+ nowSeconds ?? Math.floor(Date.now() / 1000),
217
+ )
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
+ },
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
+ }
256
+ return addon.jwtVerify(token, secret, nowSeconds ?? Math.floor(Date.now() / 1000))
257
+ },
258
+ passwordHash(
259
+ password: Uint8Array,
260
+ salt: Uint8Array,
261
+ options?: PasswordHashOptions | null,
262
+ ): Uint8Array | string {
263
+ const f = ffi()
264
+ if (f) {
265
+ // Resolve the napi defaults (rust/crypto/argon2.rs resolve_opts).
266
+ const o = options ?? {}
267
+ return f.passwordHash(
268
+ password,
269
+ salt,
270
+ o.mCost ?? 19456,
271
+ o.tCost ?? 2,
272
+ o.pCost ?? 1,
273
+ o.outLen ?? 32,
274
+ )
275
+ }
276
+ return addon.passwordHash(password, salt, options ?? null)
277
+ },
278
+ passwordVerify(password: Uint8Array, phc: Uint8Array): boolean {
279
+ return resolveNative(ctx, 'passwordVerify')(password, phc) as boolean
280
+ },
281
+ passwordHashBcrypt(password: Uint8Array, cost: number): string {
282
+ return resolveNative(ctx, 'passwordHashBcrypt')(password, cost) as string
283
+ },
284
+ passwordVerifyBcrypt(password: Uint8Array, hash: string): boolean {
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)
289
+ return addon.passwordVerifyBcrypt(password, hash)
290
+ },
291
+ pbkdf2Sha256(
292
+ password: Uint8Array,
293
+ salt: Uint8Array,
294
+ rounds: number,
295
+ dkLen: number,
296
+ ): Uint8Array {
297
+ return resolveNative(ctx, 'pbkdf2Sha256')(password, salt, rounds, dkLen) as Uint8Array
298
+ },
299
+ aeadEncrypt(
300
+ key: Uint8Array,
301
+ nonce: Uint8Array,
302
+ plaintext: Uint8Array,
303
+ algorithm?: string | null,
304
+ ): Uint8Array {
305
+ const f = ffi()
306
+ if (f) {
307
+ return f.aeadEncrypt(key, nonce, plaintext, algorithm === 'chacha20-poly1305' ? 1 : 0)
308
+ }
309
+ return addon.aeadEncrypt(key, nonce, plaintext, algorithm ?? null)
310
+ },
311
+ aeadEncryptInto(
312
+ key: Uint8Array,
313
+ nonce: Uint8Array,
314
+ plaintext: Uint8Array,
315
+ output: Uint8Array,
316
+ algorithm?: string | null,
317
+ ): number {
318
+ const f = ffi()
319
+ if (f) {
320
+ return f.aeadEncryptInto(
321
+ key,
322
+ nonce,
323
+ plaintext,
324
+ output,
325
+ algorithm === 'chacha20-poly1305' ? 1 : 0,
326
+ )
327
+ }
328
+ const bytes = addon.aeadEncrypt(key, nonce, plaintext, algorithm ?? null)
329
+ if (output.length < bytes.length) throw new Error('aead encrypt: output buffer too small')
330
+ output.set(bytes)
331
+ return bytes.length
332
+ },
333
+ aeadDecrypt(
334
+ key: Uint8Array,
335
+ nonce: Uint8Array,
336
+ ciphertext: Uint8Array,
337
+ algorithm?: string | null,
338
+ ): Uint8Array | null {
339
+ const f = ffi()
340
+ if (f) {
341
+ return f.aeadDecrypt(key, nonce, ciphertext, algorithm === 'chacha20-poly1305' ? 1 : 0)
342
+ }
343
+ return addon.aeadDecrypt(key, nonce, ciphertext, algorithm ?? null)
344
+ },
345
+ }
346
+ }