castrum 0.7.0 → 0.9.0

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 (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -59
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +3161 -0
  5. package/dist/index.js +7306 -0
  6. package/docs/ARCHITECTURE.md +415 -0
  7. package/docs/BENCHMARKS.md +307 -0
  8. package/docs/ENVIRONMENT.md +79 -0
  9. package/docs/GETTING_STARTED.md +209 -0
  10. package/docs/INGRESS.md +745 -0
  11. package/docs/REPO_MAP.md +259 -0
  12. package/docs/adr/0001-two-wire-formats.md +39 -0
  13. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  14. package/docs/adr/0003-bun-builtins-delegation.md +42 -0
  15. package/docs/bun-builtins-decision-matrix.md +108 -0
  16. package/index.ts +51 -8
  17. package/package.json +73 -20
  18. package/src/data/json-rows.ts +13 -13
  19. package/src/ingress/body.ts +149 -0
  20. package/src/ingress/constants.ts +220 -48
  21. package/src/ingress/context.ts +136 -0
  22. package/src/ingress/decode/baked-result.ts +121 -0
  23. package/src/ingress/decode/fast-result.ts +160 -0
  24. package/src/ingress/decode/packed-sections.ts +56 -0
  25. package/src/ingress/decode/result-base.ts +172 -0
  26. package/src/ingress/errors.ts +72 -0
  27. package/src/ingress/fast.ts +152 -1021
  28. package/src/ingress/handlers.ts +563 -1001
  29. package/src/ingress/headers/baked-templates.ts +102 -0
  30. package/src/ingress/headers/cors.ts +44 -0
  31. package/src/ingress/headers/fast-templates.ts +238 -0
  32. package/src/ingress/headers/hsts.ts +83 -0
  33. package/src/ingress/health.ts +67 -0
  34. package/src/ingress/index.ts +130 -405
  35. package/src/ingress/metrics.ts +132 -0
  36. package/src/ingress/options.ts +197 -0
  37. package/src/ingress/packing/gather-raw-headers.ts +75 -0
  38. package/src/ingress/packing/header-packing.ts +75 -0
  39. package/src/ingress/packing/input-packer.ts +150 -0
  40. package/src/ingress/packing/scratch.ts +50 -0
  41. package/src/ingress/packing/select-headers.ts +96 -0
  42. package/src/ingress/response/error-bodies.ts +75 -0
  43. package/src/ingress/response/terminal.ts +59 -0
  44. package/src/ingress/routes/common.ts +38 -0
  45. package/src/ingress/routes/delete.ts +14 -0
  46. package/src/ingress/routes/echo.ts +107 -0
  47. package/src/ingress/routes/fallback.ts +23 -0
  48. package/src/ingress/routes/head.ts +37 -0
  49. package/src/ingress/routes/index.ts +10 -0
  50. package/src/ingress/routes/json-write.ts +153 -0
  51. package/src/ingress/routes/options.ts +36 -0
  52. package/src/ingress/routes/read.ts +45 -0
  53. package/src/ingress/server-node.ts +376 -0
  54. package/src/ingress/server.ts +443 -0
  55. package/src/ingress/shared.ts +132 -0
  56. package/src/ingress/status.ts +97 -0
  57. package/src/ingress/types.ts +157 -0
  58. package/src/integration/batch.ts +58 -0
  59. package/src/integration/index.ts +29 -0
  60. package/src/integration/pipeline.ts +217 -0
  61. package/src/integration/streaming.ts +79 -0
  62. package/src/integration/websocket.ts +92 -0
  63. package/src/loader/batch.ts +133 -0
  64. package/src/loader/cost.ts +104 -0
  65. package/src/loader/index.ts +930 -0
  66. package/src/loader/ops.ts +657 -0
  67. package/src/native/ffi.ts +1541 -0
  68. package/src/native/index.ts +8 -207
  69. package/src/native/loader.ts +194 -0
  70. package/src/native/types.ts +598 -0
  71. package/src/rust-ffi/addon.ts +10 -0
  72. package/src/rust-ffi/batch.ts +431 -0
  73. package/src/rust-ffi/client.ts +88 -0
  74. package/src/rust-ffi/context.ts +164 -0
  75. package/src/rust-ffi/index.ts +40 -541
  76. package/src/rust-ffi/options.ts +113 -0
  77. package/src/rust-ffi/packed.ts +253 -0
  78. package/src/rust-ffi/proven.ts +42 -0
  79. package/src/rust-ffi/scalar/crypto.ts +265 -0
  80. package/src/rust-ffi/scalar/factories.ts +233 -0
  81. package/src/rust-ffi/scalar/hashing.ts +72 -0
  82. package/src/rust-ffi/scalar/http.ts +206 -0
  83. package/src/rust-ffi/scalar/index.ts +31 -0
  84. package/src/rust-ffi/scalar/interface.ts +450 -0
  85. package/src/rust-ffi/scalar/json.ts +45 -0
  86. package/src/rust-ffi/scalar/payload.ts +124 -0
  87. package/src/rust-ffi/text.ts +95 -0
  88. package/src/selection.json +194 -0
  89. package/src/selection.ts +77 -0
  90. package/src/shared/adaptive.ts +68 -0
  91. package/src/shared/bench-classify.ts +209 -0
  92. package/src/shared/buffer-pool.ts +209 -0
  93. package/src/shared/bytes.ts +35 -5
  94. package/src/shared/env.ts +20 -0
  95. package/src/shared/log.ts +74 -0
  96. package/src/shared/metrics.ts +247 -0
  97. package/src/shared/packed.ts +350 -154
  98. package/src/shared/proven.ts +596 -0
  99. package/src/shared/request-id.ts +67 -0
  100. package/src/shared/response.ts +104 -0
  101. package/src/shared/runtime.ts +34 -0
  102. package/src/shared/trace.ts +81 -0
  103. package/src/shared/uuid.ts +31 -0
  104. package/Cargo.toml +0 -73
  105. package/bench.ts +0 -3
  106. package/rust/batch.rs +0 -195
  107. package/rust/cookie_parser.rs +0 -70
  108. package/rust/cors.rs +0 -263
  109. package/rust/hashing.rs +0 -164
  110. package/rust/headers.rs +0 -234
  111. package/rust/hmac_sha256.rs +0 -113
  112. package/rust/http_parser.rs +0 -132
  113. package/rust/ingress.rs +0 -616
  114. package/rust/ingress_constants.rs +0 -89
  115. package/rust/ip_trust.rs +0 -185
  116. package/rust/json_ops.rs +0 -66
  117. package/rust/json_patch_ops.rs +0 -25
  118. package/rust/json_schema.rs +0 -151
  119. package/rust/json_ser.rs +0 -429
  120. package/rust/lib.rs +0 -48
  121. package/rust/method.rs +0 -141
  122. package/rust/mime_lookup.rs +0 -76
  123. package/rust/output.rs +0 -238
  124. package/rust/proxy.rs +0 -90
  125. package/rust/query_parser.rs +0 -141
  126. package/rust/random_token.rs +0 -52
  127. package/rust/rate_limit.rs +0 -203
  128. package/rust/terminal.rs +0 -95
  129. package/rust/unit_tests.rs +0 -843
  130. package/rust/url_codec.rs +0 -133
  131. package/rust/util.rs +0 -537
  132. package/rust/validation.rs +0 -107
  133. package/rust/websocket.rs +0 -23
  134. package/src/baseline/index.ts +0 -12
  135. package/src/baseline/tasks/cookie.ts +0 -22
  136. package/src/baseline/tasks/hashing.ts +0 -18
  137. package/src/baseline/tasks/hmac.ts +0 -38
  138. package/src/baseline/tasks/http.ts +0 -62
  139. package/src/baseline/tasks/json-patch.ts +0 -12
  140. package/src/baseline/tasks/json.ts +0 -50
  141. package/src/baseline/tasks/mime.ts +0 -5
  142. package/src/baseline/tasks/query.ts +0 -14
  143. package/src/baseline/tasks/token.ts +0 -15
  144. package/src/baseline/tasks/url.ts +0 -11
  145. package/src/baseline/tasks/validation.ts +0 -19
  146. package/src/baseline/tasks/websocket.ts +0 -14
  147. package/src/bench/assert.ts +0 -70
  148. package/src/bench/checks.ts +0 -227
  149. package/src/bench/checksum.ts +0 -27
  150. package/src/bench/comparisons.ts +0 -70
  151. package/src/bench/concurrent-worker.ts +0 -135
  152. package/src/bench/fixtures.ts +0 -233
  153. package/src/bench/index.ts +0 -2
  154. package/src/bench/measure.ts +0 -177
  155. package/src/bench/now.ts +0 -3
  156. package/src/bench/report.ts +0 -79
  157. package/src/bench/run.ts +0 -57
  158. package/src/bench/tasks/complex.ts +0 -232
  159. package/src/bench/tasks/concurrent.ts +0 -213
  160. package/src/bench/tasks/cookie.ts +0 -44
  161. package/src/bench/tasks/hashing.ts +0 -33
  162. package/src/bench/tasks/hmac.ts +0 -36
  163. package/src/bench/tasks/http.ts +0 -50
  164. package/src/bench/tasks/index.ts +0 -52
  165. package/src/bench/tasks/json-patch.ts +0 -21
  166. package/src/bench/tasks/json.ts +0 -33
  167. package/src/bench/tasks/mime.ts +0 -21
  168. package/src/bench/tasks/query.ts +0 -44
  169. package/src/bench/tasks/stress.ts +0 -142
  170. package/src/bench/tasks/token.ts +0 -20
  171. package/src/bench/tasks/url.ts +0 -34
  172. package/src/bench/tasks/validation.ts +0 -57
  173. package/src/bench/tasks/websocket.ts +0 -21
  174. package/src/bench/types.ts +0 -60
  175. package/src/ingress/packed-input.ts +0 -59
  176. package/src/shared/json.ts +0 -18
@@ -0,0 +1,253 @@
1
+ // src/rust-ffi/packed.ts — Raw packed-wire FFI namespace.
2
+ //
3
+ // `rust.packed.*` is the zero-copy API: callers pass already-packed buffers and
4
+ // receive the raw packed output, plus the batch metadata/count helpers.
5
+
6
+ import { asNumber } from './options'
7
+ import { resolvePoolNative, type RustClientContext } from './context'
8
+ import type { PasswordHashOptions } from '../native'
9
+
10
+ /** Raw packed-wire FFI namespace. */
11
+ export interface RustPacked {
12
+ // ── Sync batch (packed in → packed out) ──
13
+ crc32BatchPacked(input: Uint8Array): Uint8Array
14
+ jsonValidBatchPacked(input: Uint8Array): Uint8Array
15
+ validateEmailBatchPacked(input: Uint8Array): Uint8Array
16
+ validateUuidBatchPacked(input: Uint8Array): Uint8Array
17
+ validateIpv4BatchPacked(input: Uint8Array): Uint8Array
18
+ validateIpv6BatchPacked(input: Uint8Array): Uint8Array
19
+ jsonSumBatchPacked(input: Uint8Array): Uint8Array
20
+ queryParseBatchPacked(input: Uint8Array): Uint8Array
21
+ cookieParseBatchPacked(input: Uint8Array): Uint8Array
22
+ httpParseRequestBatchPacked(input: Uint8Array): Uint8Array
23
+ /** Batch RFC 6902 JSON Patch (two packed lists, zipped) → packed results. */
24
+ jsonPatchBatchPacked(docs: Uint8Array, patches: Uint8Array): Uint8Array
25
+ /** Packed FNV-1a 64 batch (i64 per item). */
26
+ fnv1A64BatchPacked(input: Uint8Array): Uint8Array
27
+ /** Packed ETag batch (10 strong / 12 weak bytes per item). */
28
+ etagBatchPacked(input: Uint8Array, weak?: boolean | null): Uint8Array
29
+ urlEncodeBatchPacked(input: Uint8Array): Uint8Array
30
+ urlDecodeBatchPacked(input: Uint8Array): Uint8Array
31
+ urlDecodeBytesBatchPacked(input: Uint8Array): Uint8Array
32
+ mimeFromExtensionBatchPacked(input: Uint8Array): Uint8Array
33
+ wsAcceptKeyBatchPacked(input: Uint8Array): Uint8Array
34
+
35
+ // ── Batch metadata / counts ──
36
+ jsonValidBatchCountPacked(input: Uint8Array): number
37
+ validateEmailBatchCountPacked(input: Uint8Array): number
38
+ validateUuidBatchCountPacked(input: Uint8Array): number
39
+ validateIpv4BatchCountPacked(input: Uint8Array): number
40
+ validateIpv6BatchCountPacked(input: Uint8Array): number
41
+ jsonSumBatchTotalPacked(input: Uint8Array): number
42
+ queryParseBatchTotalLenPacked(input: Uint8Array): number
43
+ cookieParseBatchTotalLenPacked(input: Uint8Array): number
44
+ httpParseRequestBatchTotalLenPacked(input: Uint8Array): number
45
+
46
+ // ── Backend-framework features ──
47
+ passwordHashBatchPacked(
48
+ input: Uint8Array,
49
+ salt: Uint8Array,
50
+ options?: PasswordHashOptions | null,
51
+ ): Uint8Array
52
+ /** Packed password-verify batch (two packed lists, zipped) → bitset. */
53
+ passwordVerifyBatchPacked(passwords: Uint8Array, phcs: Uint8Array): Uint8Array
54
+ /** Packed URL-resolve batch (two packed lists, zipped) → packed results. */
55
+ urlResolveBatchPacked(bases: Uint8Array, references: Uint8Array): Uint8Array
56
+ aeadEncryptBatchPacked(
57
+ input: Uint8Array,
58
+ key: Uint8Array,
59
+ nonce: Uint8Array,
60
+ algorithm?: string | null,
61
+ ): Uint8Array
62
+ aeadDecryptBatchPacked(
63
+ input: Uint8Array,
64
+ key: Uint8Array,
65
+ nonce: Uint8Array,
66
+ algorithm?: string | null,
67
+ ): Uint8Array
68
+ gzipCompressBatchPacked(input: Uint8Array, level?: number | null): Uint8Array
69
+ gzipDecompressBatchPacked(input: Uint8Array): Uint8Array
70
+ brotliCompressBatchPacked(input: Uint8Array, quality?: number | null): Uint8Array
71
+ brotliDecompressBatchPacked(input: Uint8Array): Uint8Array
72
+ jwtSignBatchPacked(
73
+ input: Uint8Array,
74
+ secret: Uint8Array,
75
+ ttlSeconds: number | null,
76
+ nowSeconds: number,
77
+ ): Uint8Array
78
+ jwtVerifyBatchPacked(input: Uint8Array, secret: Uint8Array, nowSeconds: number): Uint8Array
79
+ /** Batch HMAC-SHA256 sign (packed in → hex-signature byte results). */
80
+ hmacSha256BatchPacked(input: Uint8Array, key: Uint8Array): Uint8Array
81
+ /** Batch HMAC-SHA256 verify (two packed lists: data + hex sigs, zipped) → bitset. */
82
+ hmacSha256VerifyBatchPacked(input: Uint8Array, sigs: Uint8Array, key: Uint8Array): Uint8Array
83
+ multipartParseBatchPacked(input: Uint8Array, boundary: Uint8Array): Uint8Array
84
+ templateRenderBatchPacked(input: Uint8Array, source: string): Uint8Array
85
+ sseEncodeBatchPacked(
86
+ input: Uint8Array,
87
+ event: string | null,
88
+ id: string | null,
89
+ retry: number | null,
90
+ ): Uint8Array
91
+ wsFrameEncodeBatchPacked(
92
+ input: Uint8Array,
93
+ opcode: number,
94
+ mask: boolean,
95
+ fin: boolean,
96
+ ): Uint8Array
97
+ wsFrameDecodeBatchPacked(input: Uint8Array): Uint8Array
98
+ }
99
+
100
+ /** Build the `packed` namespace for a client context. */
101
+ export function buildPacked(ctx: RustClientContext): RustPacked {
102
+ // Packed ops are rayon-backed NAPI calls (no C ABI). Resolve each native fn
103
+ // on first use — ensuring the process-wide pool initializes at that point so
104
+ // `rust.configure({ rayonThreads })` still takes effect — and cache it via
105
+ // `resolvePoolNative` (shared with `resolveNative`/batch, context.ts). This
106
+ // removes BOTH the lazy-addon Proxy `get` and the old `withPoolInit`
107
+ // per-access Proxy from the packed hot path. The object keeps the
108
+ // `NativeAddon` type so the method bodies below typecheck unchanged.
109
+ const addon = new Proxy(ctx.addon, {
110
+ get: (_target, name) => resolvePoolNative(ctx, String(name)),
111
+ }) as typeof ctx.addon
112
+
113
+ return {
114
+ crc32BatchPacked(input) {
115
+ return addon.crc32BatchPacked(input)
116
+ },
117
+ jsonValidBatchPacked(input) {
118
+ return addon.jsonValidBatchPacked(input)
119
+ },
120
+ validateEmailBatchPacked(input) {
121
+ return addon.validateEmailBatchPacked(input)
122
+ },
123
+ validateUuidBatchPacked(input) {
124
+ return addon.validateUuidBatchPacked(input)
125
+ },
126
+ validateIpv4BatchPacked(input) {
127
+ return addon.validateIpv4BatchPacked(input)
128
+ },
129
+ validateIpv6BatchPacked(input) {
130
+ return addon.validateIpv6BatchPacked(input)
131
+ },
132
+ jsonSumBatchPacked(input) {
133
+ return addon.jsonSumBatchPacked(input)
134
+ },
135
+ queryParseBatchPacked(input) {
136
+ return addon.queryParseBatchPacked(input)
137
+ },
138
+ cookieParseBatchPacked(input) {
139
+ return addon.cookieParseBatchPacked(input)
140
+ },
141
+ httpParseRequestBatchPacked(input) {
142
+ return addon.httpParseRequestBatchPacked(input)
143
+ },
144
+ jsonPatchBatchPacked(docs, patches) {
145
+ return addon.jsonPatchBatchPacked(docs, patches)
146
+ },
147
+ fnv1A64BatchPacked(input) {
148
+ return addon.fnv1A64BatchPacked(input)
149
+ },
150
+ etagBatchPacked(input, weak) {
151
+ return addon.etagBatchPacked(input, weak ?? null)
152
+ },
153
+ urlEncodeBatchPacked(input) {
154
+ return addon.urlEncodeBatchPacked(input)
155
+ },
156
+ urlDecodeBatchPacked(input) {
157
+ return addon.urlDecodeBatchPacked(input)
158
+ },
159
+ urlDecodeBytesBatchPacked(input) {
160
+ return addon.urlDecodeBytesBatchPacked(input)
161
+ },
162
+ mimeFromExtensionBatchPacked(input) {
163
+ return addon.mimeFromExtensionBatchPacked(input)
164
+ },
165
+ wsAcceptKeyBatchPacked(input) {
166
+ return addon.wsAcceptKeyBatchPacked(input)
167
+ },
168
+
169
+ jsonValidBatchCountPacked(input) {
170
+ return asNumber(addon.jsonValidBatchCountPacked(input))
171
+ },
172
+ validateEmailBatchCountPacked(input) {
173
+ return asNumber(addon.validateEmailBatchCountPacked(input))
174
+ },
175
+ validateUuidBatchCountPacked(input) {
176
+ return asNumber(addon.validateUuidBatchCountPacked(input))
177
+ },
178
+ validateIpv4BatchCountPacked(input) {
179
+ return asNumber(addon.validateIpv4BatchCountPacked(input))
180
+ },
181
+ validateIpv6BatchCountPacked(input) {
182
+ return asNumber(addon.validateIpv6BatchCountPacked(input))
183
+ },
184
+ jsonSumBatchTotalPacked(input) {
185
+ return asNumber(addon.jsonSumBatchTotalPacked(input))
186
+ },
187
+ queryParseBatchTotalLenPacked(input) {
188
+ return asNumber(addon.queryParseBatchTotalLenPacked(input))
189
+ },
190
+ cookieParseBatchTotalLenPacked(input) {
191
+ return asNumber(addon.cookieParseBatchTotalLenPacked(input))
192
+ },
193
+ httpParseRequestBatchTotalLenPacked(input) {
194
+ return asNumber(addon.httpParseRequestBatchTotalLenPacked(input))
195
+ },
196
+
197
+ // ── Backend-framework features ──
198
+ passwordHashBatchPacked(input, salt, options) {
199
+ return addon.passwordHashBatchPacked(input, salt, options ?? null)
200
+ },
201
+ passwordVerifyBatchPacked(passwords, phcs) {
202
+ return addon.passwordVerifyBatchPacked(passwords, phcs)
203
+ },
204
+ urlResolveBatchPacked(bases, references) {
205
+ return addon.urlResolveBatchPacked(bases, references)
206
+ },
207
+ aeadEncryptBatchPacked(input, key, nonce, algorithm) {
208
+ return addon.aeadEncryptBatchPacked(input, key, nonce, algorithm ?? null)
209
+ },
210
+ aeadDecryptBatchPacked(input, key, nonce, algorithm) {
211
+ return addon.aeadDecryptBatchPacked(input, key, nonce, algorithm ?? null)
212
+ },
213
+ gzipCompressBatchPacked(input, level) {
214
+ return addon.gzipCompressBatchPacked(input, level ?? null)
215
+ },
216
+ gzipDecompressBatchPacked(input) {
217
+ return addon.gzipDecompressBatchPacked(input)
218
+ },
219
+ brotliCompressBatchPacked(input, quality) {
220
+ return addon.brotliCompressBatchPacked(input, quality ?? null)
221
+ },
222
+ brotliDecompressBatchPacked(input) {
223
+ return addon.brotliDecompressBatchPacked(input)
224
+ },
225
+ jwtSignBatchPacked(input, secret, ttlSeconds, nowSeconds) {
226
+ return addon.jwtSignBatchPacked(input, secret, ttlSeconds ?? null, nowSeconds)
227
+ },
228
+ jwtVerifyBatchPacked(input, secret, nowSeconds) {
229
+ return addon.jwtVerifyBatchPacked(input, secret, nowSeconds)
230
+ },
231
+ hmacSha256BatchPacked(input, key) {
232
+ return addon.hmacSha256BatchPacked(input, key)
233
+ },
234
+ hmacSha256VerifyBatchPacked(input, sigs, key) {
235
+ return addon.hmacSha256VerifyBatchPacked(input, sigs, key)
236
+ },
237
+ multipartParseBatchPacked(input, boundary) {
238
+ return addon.multipartParseBatchPacked(input, boundary)
239
+ },
240
+ templateRenderBatchPacked(input, source) {
241
+ return addon.templateRenderBatchPacked(input, source)
242
+ },
243
+ sseEncodeBatchPacked(input, event, id, retry) {
244
+ return addon.sseEncodeBatchPacked(input, event ?? null, id ?? null, retry ?? null)
245
+ },
246
+ wsFrameEncodeBatchPacked(input, opcode, mask, fin) {
247
+ return addon.wsFrameEncodeBatchPacked(input, opcode, mask, fin)
248
+ },
249
+ wsFrameDecodeBatchPacked(input) {
250
+ return addon.wsFrameDecodeBatchPacked(input)
251
+ },
252
+ }
253
+ }
@@ -0,0 +1,42 @@
1
+ // src/rust-ffi/proven.ts — The performance-annotated export surface.
2
+ //
3
+ // `proven` exposes the FULL `rust.*` surface. Each exported function's JSDoc
4
+ // carries its measured performance vs the native JS baseline, annotated from
5
+ // the CPU benchmark report by `scripts/annotate-performance.ts` (run via
6
+ // `bun run check:annotate`). Functions that are SLOWER than the JS baseline
7
+ // (e.g. `jsonParse` vs Bun's JSON.parse, or schema validation vs ajv) are
8
+ // marked `@deprecated`.
9
+ //
10
+ // The registry (`src/shared/proven.ts`) is the single source of truth for the
11
+ // annotations; `bun run check:proven` audits it against the latest benchmark
12
+ // report (with `--fail` to gate CI).
13
+
14
+ import { rust, type RustClient } from './client'
15
+
16
+ /**
17
+ * The performance-annotated client: the full {@link RustClient} surface.
18
+ * Each exported function's JSDoc notes its measured performance vs the JS
19
+ * baseline; functions slower than the baseline carry `@deprecated`.
20
+ */
21
+ export type ProvenClient = RustClient
22
+
23
+ /**
24
+ * The full `rust.*` surface with benchmark-annotated JSDoc. Identical to
25
+ * `rust` — the annotations and `@deprecated` markers are what communicate
26
+ * performance now, not a curated subset.
27
+ */
28
+ export const proven: ProvenClient = rust
29
+
30
+ // Re-export the registry + helpers so consumers can audit the surface.
31
+ export {
32
+ PROVEN_SURFACE,
33
+ provenStatus,
34
+ isProven,
35
+ provenSurface,
36
+ provenSummary,
37
+ } from '../shared/proven'
38
+ export type {
39
+ PerformanceStatus,
40
+ ProvenEntry,
41
+ ProvenKey,
42
+ } from '../shared/proven'
@@ -0,0 +1,265 @@
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
+ 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()
13
+
14
+ /** Auth / crypto scalar methods (`Pick<RustScalar, ...>`). */
15
+ export function buildCrypto(ctx: RustClientContext) {
16
+ const { addon } = ctx
17
+
18
+ return {
19
+ randomToken(byteLen: number): Uint8Array {
20
+ // 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
+ )
31
+ }
32
+ const ffi = getBunFFI()
33
+ if (ffi) return ffi.randomToken(byteLen)
34
+ return addon.randomToken(byteLen)
35
+ },
36
+ base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array {
37
+ // 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'))
43
+ }
44
+ const ffi = getBunFFI()
45
+ if (ffi) return ffi.base64Encode(input, urlSafe, padding)
46
+ return addon.base64Encode(input, urlSafe ?? undefined, padding ?? undefined)
47
+ },
48
+ base64EncodeInto(
49
+ input: Uint8Array,
50
+ output: Uint8Array,
51
+ urlSafe?: boolean,
52
+ padding?: boolean,
53
+ ): 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)
57
+ },
58
+ 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)
62
+ },
63
+ base64DecodeInto(
64
+ input: Uint8Array,
65
+ output: Uint8Array,
66
+ urlSafe?: boolean,
67
+ padding?: boolean,
68
+ ): 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)
72
+ },
73
+ base64UrlEncode(input: Uint8Array): Uint8Array {
74
+ const ffi = getBunFFI()
75
+ if (ffi) return ffi.base64Encode(input, true, false)
76
+ return addon.base64UrlEncode(input)
77
+ },
78
+ base64UrlDecode(input: Uint8Array): Uint8Array {
79
+ const ffi = getBunFFI()
80
+ if (ffi) return ffi.base64Decode(input, true, false)
81
+ return addon.base64UrlDecode(input)
82
+ },
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)
89
+ },
90
+ hexEncodeInto(input: Uint8Array, output: Uint8Array): number {
91
+ const ffi = getBunFFI()
92
+ if (ffi) return ffi.hexEncodeInto(input, output)
93
+ return addon.hexEncodeInto(input, output)
94
+ },
95
+ hexDecode(input: Uint8Array): Uint8Array {
96
+ const ffi = getBunFFI()
97
+ if (ffi) return ffi.hexDecode(input)
98
+ return addon.hexDecode(input)
99
+ },
100
+ hexDecodeInto(input: Uint8Array, output: Uint8Array): number {
101
+ const ffi = getBunFFI()
102
+ if (ffi) return ffi.hexDecodeInto(input, output)
103
+ return addon.hexDecodeInto(input, output)
104
+ },
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)
109
+ },
110
+ 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
117
+ },
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)
122
+ },
123
+ csrfToken(secret: Uint8Array): Uint8Array {
124
+ const ffi = getBunFFI()
125
+ if (ffi) return ffi.csrfToken(secret)
126
+ return addon.csrfToken(secret)
127
+ },
128
+ csrfVerify(token: Uint8Array, secret: Uint8Array): boolean {
129
+ const ffi = getBunFFI()
130
+ if (ffi) return ffi.csrfVerify(token, secret)
131
+ return addon.csrfVerify(token, secret)
132
+ },
133
+ jwtSign(
134
+ claims: Record<string, unknown>,
135
+ secret: Uint8Array,
136
+ ttlSeconds?: number | null,
137
+ nowSeconds?: number,
138
+ ): Uint8Array {
139
+ return addon.jwtSign(
140
+ claims,
141
+ secret,
142
+ ttlSeconds ?? null,
143
+ nowSeconds ?? Math.floor(Date.now() / 1000),
144
+ )
145
+ },
146
+ jwtSignBytes(
147
+ claimsJson: Uint8Array,
148
+ secret: Uint8Array,
149
+ ttlSeconds?: number | null,
150
+ nowSeconds?: number,
151
+ ): Uint8Array {
152
+ const ffi = getBunFFI()
153
+ if (ffi) {
154
+ // The C-ABI `ttl <= 0` sentinel matches napi's Option<i64> (inject only
155
+ // when positive), and nowSeconds defaults identically.
156
+ return ffi.jwtSignBytes(
157
+ claimsJson,
158
+ secret,
159
+ ttlSeconds ?? 0,
160
+ nowSeconds ?? Math.floor(Date.now() / 1000),
161
+ )
162
+ }
163
+ return addon.jwtSignBytes(
164
+ claimsJson,
165
+ secret,
166
+ ttlSeconds ?? null,
167
+ nowSeconds ?? Math.floor(Date.now() / 1000),
168
+ )
169
+ },
170
+ jwtVerify(token: Uint8Array, secret: Uint8Array, nowSeconds?: number): unknown {
171
+ return addon.jwtVerify(token, secret, nowSeconds ?? Math.floor(Date.now() / 1000))
172
+ },
173
+ passwordHash(
174
+ password: Uint8Array,
175
+ salt: Uint8Array,
176
+ options?: PasswordHashOptions | null,
177
+ ): Uint8Array {
178
+ const ffi = getBunFFI()
179
+ if (ffi) {
180
+ // Resolve the napi defaults (rust/crypto/argon2.rs resolve_opts).
181
+ const o = options ?? {}
182
+ return ffi.passwordHash(
183
+ password,
184
+ salt,
185
+ o.mCost ?? 19456,
186
+ o.tCost ?? 2,
187
+ o.pCost ?? 1,
188
+ o.outLen ?? 32,
189
+ )
190
+ }
191
+ return addon.passwordHash(password, salt, options ?? null)
192
+ },
193
+ passwordVerify(password: Uint8Array, phc: Uint8Array): boolean {
194
+ const ffi = getBunFFI()
195
+ if (ffi) return ffi.passwordVerify(password, phc)
196
+ return addon.passwordVerify(password, phc)
197
+ },
198
+ 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)
202
+ },
203
+ passwordVerifyBcrypt(password: Uint8Array, hash: string): boolean {
204
+ const ffi = getBunFFI()
205
+ if (ffi) return ffi.passwordVerifyBcrypt(password, encoder.encode(hash))
206
+ return addon.passwordVerifyBcrypt(password, hash)
207
+ },
208
+ pbkdf2Sha256(
209
+ password: Uint8Array,
210
+ salt: Uint8Array,
211
+ rounds: number,
212
+ dkLen: number,
213
+ ): Uint8Array {
214
+ const ffi = getBunFFI()
215
+ if (ffi) return ffi.pbkdf2Sha256(password, salt, rounds, dkLen)
216
+ return addon.pbkdf2Sha256(password, salt, rounds, dkLen)
217
+ },
218
+ aeadEncrypt(
219
+ key: Uint8Array,
220
+ nonce: Uint8Array,
221
+ plaintext: Uint8Array,
222
+ algorithm?: string | null,
223
+ ): Uint8Array {
224
+ const ffi = getBunFFI()
225
+ if (ffi) {
226
+ return ffi.aeadEncrypt(key, nonce, plaintext, algorithm === 'chacha20-poly1305' ? 1 : 0)
227
+ }
228
+ return addon.aeadEncrypt(key, nonce, plaintext, algorithm ?? null)
229
+ },
230
+ aeadEncryptInto(
231
+ key: Uint8Array,
232
+ nonce: Uint8Array,
233
+ plaintext: Uint8Array,
234
+ output: Uint8Array,
235
+ algorithm?: string | null,
236
+ ): number {
237
+ const ffi = getBunFFI()
238
+ if (ffi) {
239
+ return ffi.aeadEncryptInto(
240
+ key,
241
+ nonce,
242
+ plaintext,
243
+ output,
244
+ algorithm === 'chacha20-poly1305' ? 1 : 0,
245
+ )
246
+ }
247
+ const bytes = addon.aeadEncrypt(key, nonce, plaintext, algorithm ?? null)
248
+ if (output.length < bytes.length) throw new Error('aead encrypt: output buffer too small')
249
+ output.set(bytes)
250
+ return bytes.length
251
+ },
252
+ aeadDecrypt(
253
+ key: Uint8Array,
254
+ nonce: Uint8Array,
255
+ ciphertext: Uint8Array,
256
+ algorithm?: string | null,
257
+ ): Uint8Array | null {
258
+ const ffi = getBunFFI()
259
+ if (ffi) {
260
+ return ffi.aeadDecrypt(key, nonce, ciphertext, algorithm === 'chacha20-poly1305' ? 1 : 0)
261
+ }
262
+ return addon.aeadDecrypt(key, nonce, ciphertext, algorithm ?? null)
263
+ },
264
+ }
265
+ }