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,233 @@
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 { asNumber } from '../options'
7
+ import { getBunFFI, type BunFFI } from '../../native/ffi'
8
+ import type { RustClientContext } from '../context'
9
+ import type {
10
+ HmacSignerInstance,
11
+ SchemaValidatorInstance,
12
+ TemplateRendererInstance,
13
+ FormParserInstance,
14
+ MediaTypeParserInstance,
15
+ MediaTypeMatcherInstance,
16
+ ConditionalRequestInstance,
17
+ AcceptNegotiatorInstance,
18
+ Base64CodecInstance,
19
+ CookieSignerInstance,
20
+ CsrfProtectorInstance,
21
+ UrlBuilderInstance,
22
+ JwtSignerInstance,
23
+ AeadCipherInstance,
24
+ Argon2HasherInstance,
25
+ RateLimiterInstance,
26
+ PasswordHashOptions,
27
+ } from '../../native'
28
+
29
+ // ── FFI-backed instance wrappers ───────────────────────────────────────────
30
+ // On Bun, instances whose methods have a STATELESS C-ABI sibling are backed by
31
+ // the `bun:ffi` fast path instead of a NAPI instance — cutting the per-call
32
+ // NAPI crossing (~300ns) to ~20ns. The precompiled-state benefit is mostly
33
+ // lost (e.g. the HMAC key is rebuilt per call, ~100ns), but that is still far
34
+ // cheaper than a NAPI crossing, so these win under Bun. Byte-for-byte output is
35
+ // guaranteed by the ffi cross-check suite. NAPI instances remain the fallback
36
+ // (Node / CASTRUM_FFI_MODE=napi / failed self-test). Instances with genuinely
37
+ // stateful precompiled cores (SchemaValidator, TemplateRenderer,
38
+ // AcceptNegotiator, ConditionalRequest, MediaTypeMatcher/Parser, UrlBuilder,
39
+ // RateLimiter) stay NAPI — they need opaque handles (deferred).
40
+
41
+ function ffiHmacSigner(key: Uint8Array, ffi: BunFFI): HmacSignerInstance {
42
+ return {
43
+ sign(data) {
44
+ return ffi.hmacSha256(key, data)
45
+ },
46
+ verify(data, sig) {
47
+ return ffi.hmacSha256Verify(key, data, sig)
48
+ },
49
+ }
50
+ }
51
+
52
+ function ffiBase64Codec(
53
+ urlSafe: boolean | undefined,
54
+ padding: boolean | undefined,
55
+ ffi: BunFFI,
56
+ ): Base64CodecInstance {
57
+ return {
58
+ encode(input) {
59
+ return ffi.base64Encode(input, urlSafe, padding)
60
+ },
61
+ decode(input) {
62
+ return ffi.base64Decode(input, urlSafe, padding)
63
+ },
64
+ }
65
+ }
66
+
67
+ function ffiCookieSigner(secret: Uint8Array, ffi: BunFFI): CookieSignerInstance {
68
+ return {
69
+ sign(value) {
70
+ return ffi.signCookie(value, secret)
71
+ },
72
+ verify(signed) {
73
+ return ffi.verifyCookie(signed, secret)
74
+ },
75
+ }
76
+ }
77
+
78
+ function ffiCsrfProtector(secret: Uint8Array, ffi: BunFFI): CsrfProtectorInstance {
79
+ return {
80
+ create() {
81
+ return ffi.csrfToken(secret)
82
+ },
83
+ verify(token) {
84
+ return ffi.csrfVerify(token, secret)
85
+ },
86
+ }
87
+ }
88
+
89
+ function ffiArgon2Hasher(
90
+ options: PasswordHashOptions | null | undefined,
91
+ ffi: BunFFI,
92
+ ): Argon2HasherInstance {
93
+ // Resolve the napi defaults (rust/crypto/argon2.rs resolve_opts) — mirrors
94
+ // the scalar passwordHash FFI wrapper (crypto.ts).
95
+ const o = options ?? {}
96
+ const mCost = o.mCost ?? 19456
97
+ const tCost = o.tCost ?? 2
98
+ const pCost = o.pCost ?? 1
99
+ const outLen = o.outLen ?? 32
100
+ return {
101
+ hash(password, salt) {
102
+ return ffi.passwordHash(password, salt, mCost, tCost, pCost, outLen)
103
+ },
104
+ verify(password, phc) {
105
+ return ffi.passwordVerify(password, phc)
106
+ },
107
+ }
108
+ }
109
+
110
+ function ffiAeadCipher(
111
+ key: Uint8Array,
112
+ algorithm: string | undefined,
113
+ ffi: BunFFI,
114
+ ): AeadCipherInstance {
115
+ // Mirror the napi constructor's validation (rust/crypto/aead.rs
116
+ // `resolve_algorithm`): only aes-256-gcm (default) and chacha20-poly1305 are
117
+ // accepted; anything else throws at construction, NOT at first use.
118
+ if (algorithm !== undefined && algorithm !== 'aes-256-gcm' && algorithm !== 'chacha20-poly1305') {
119
+ throw new Error(
120
+ `unsupported aead algorithm: ${algorithm} (expected aes-256-gcm | chacha20-poly1305)`,
121
+ )
122
+ }
123
+ const alg = algorithm === 'chacha20-poly1305' ? 1 : 0
124
+ return {
125
+ encrypt(nonce, plaintext) {
126
+ return ffi.aeadEncrypt(key, nonce, plaintext, alg)
127
+ },
128
+ decrypt(nonce, ciphertext) {
129
+ return ffi.aeadDecrypt(key, nonce, ciphertext, alg)
130
+ },
131
+ }
132
+ }
133
+
134
+ function ffiFormParser(ffi: BunFFI): FormParserInstance {
135
+ return {
136
+ parse(input) {
137
+ // Sizing matches the scalar formParsePacked path (Rust allocator bound
138
+ // `input.len() * 9 + 16`).
139
+ const out = new Uint8Array(input.length * 9 + 16)
140
+ const w = ffi.formParsePackedInto(input, out)
141
+ return out.subarray(0, w)
142
+ },
143
+ parseInto(input, output) {
144
+ return ffi.formParsePackedInto(input, output)
145
+ },
146
+ }
147
+ }
148
+
149
+ /** Compiled-once factory + runtime-control methods (`Pick<RustScalar, ...>`). */
150
+ export function buildFactories(ctx: RustClientContext) {
151
+ const { addon } = ctx
152
+
153
+ return {
154
+ createSchemaValidator(schema: Uint8Array): SchemaValidatorInstance {
155
+ return new addon.SchemaValidator(schema)
156
+ },
157
+ createHmacSigner(key: Uint8Array): HmacSignerInstance {
158
+ const ffi = getBunFFI()
159
+ if (ffi) return ffiHmacSigner(key, ffi)
160
+ return new addon.HmacSigner(key)
161
+ },
162
+ createTemplateRenderer(source: string): TemplateRendererInstance {
163
+ return new addon.TemplateRenderer(source)
164
+ },
165
+ createFormParser(capacity?: number): FormParserInstance {
166
+ const ffi = getBunFFI()
167
+ if (ffi) return ffiFormParser(ffi)
168
+ return new addon.FormParser(capacity)
169
+ },
170
+ createMediaTypeParser(): MediaTypeParserInstance {
171
+ return new addon.MediaTypeParser()
172
+ },
173
+ createConditionalRequest(
174
+ etagValue: Uint8Array,
175
+ lastModifiedSecs?: number,
176
+ ): ConditionalRequestInstance {
177
+ return new addon.ConditionalRequest(etagValue, lastModifiedSecs ?? undefined)
178
+ },
179
+ createAcceptNegotiator(supported: string[]): AcceptNegotiatorInstance {
180
+ return new addon.AcceptNegotiator(supported)
181
+ },
182
+ createBase64Codec(urlSafe?: boolean, padding?: boolean): Base64CodecInstance {
183
+ const ffi = getBunFFI()
184
+ if (ffi) return ffiBase64Codec(urlSafe, padding, ffi)
185
+ return new addon.Base64Codec(urlSafe ?? undefined, padding ?? undefined)
186
+ },
187
+ createCookieSigner(secret: Uint8Array): CookieSignerInstance {
188
+ const ffi = getBunFFI()
189
+ if (ffi) return ffiCookieSigner(secret, ffi)
190
+ return new addon.CookieSigner(secret)
191
+ },
192
+ createCsrfProtector(secret: Uint8Array): CsrfProtectorInstance {
193
+ const ffi = getBunFFI()
194
+ if (ffi) return ffiCsrfProtector(secret, ffi)
195
+ return new addon.CsrfProtector(secret)
196
+ },
197
+ createUrlBuilder(base: Uint8Array): UrlBuilderInstance {
198
+ return new addon.UrlBuilder(base)
199
+ },
200
+ createJwtSigner(secret: Uint8Array, ttlSeconds?: number): JwtSignerInstance {
201
+ return new addon.JwtSigner(secret, ttlSeconds ?? undefined)
202
+ },
203
+ createAeadCipher(key: Uint8Array, algorithm?: string): AeadCipherInstance {
204
+ const ffi = getBunFFI()
205
+ if (ffi) return ffiAeadCipher(key, algorithm, ffi)
206
+ return new addon.AeadCipher(key, algorithm ?? undefined)
207
+ },
208
+ createArgon2Hasher(options?: PasswordHashOptions | null): Argon2HasherInstance {
209
+ const ffi = getBunFFI()
210
+ if (ffi) return ffiArgon2Hasher(options, ffi)
211
+ return new addon.Argon2Hasher(options ?? undefined)
212
+ },
213
+ createMediaTypeMatcher(expected: Uint8Array): MediaTypeMatcherInstance {
214
+ return new addon.MediaTypeMatcher(expected)
215
+ },
216
+ createRateLimiter(
217
+ limit: number,
218
+ windowMs: number,
219
+ maxEntries?: number | null,
220
+ ): RateLimiterInstance {
221
+ return new addon.RateLimiter(limit, windowMs, maxEntries ?? undefined)
222
+ },
223
+ initThreadPool(threads?: number): void {
224
+ // Explicit user call also establishes the pool state locally.
225
+ ctx.markPoolInitialized()
226
+ if (threads !== undefined) ctx.setPendingThreads(threads)
227
+ addon.initThreadPool(threads)
228
+ },
229
+ rayonNumThreads(): number {
230
+ return asNumber(addon.rayonNumThreads() as unknown)
231
+ },
232
+ }
233
+ }
@@ -0,0 +1,72 @@
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
+ import { asBigInt, asNumber } from '../options'
7
+ import { resolveNative, type RustClientContext } from '../context'
8
+ import { getBunFFI } from '../../native/ffi'
9
+ import { isBun } from '../../shared/runtime'
10
+
11
+ const hexEncoder = new TextEncoder()
12
+
13
+ /** Hashing scalar methods (`Pick<RustScalar, ...>`). */
14
+ export function buildHashing(ctx: RustClientContext) {
15
+ // Bun fast path: `bun:ffi` JIT-calls the C ABI exports with ~1/4 the crossing
16
+ // cost of napi for these sub-µs hashes. `getBunFFI()` is lazy (binds on first
17
+ // use, ~2ns cached check) and null when unavailable / self-test fails.
18
+ return {
19
+ crc32(input: Uint8Array): number {
20
+ // Optimal by default under Bun: `Bun.hash.crc32` (native SIMD) beats the
21
+ // rust+FFI crossing (~2.8-8.4x, decision matrix) and matches the
22
+ // `BUN_WINS`/`opImpl` recommendation. Node keeps the addon.
23
+ // `>>> 0` preserves the unsigned-32 contract.
24
+ if (isBun()) return Bun.hash.crc32(input) >>> 0
25
+ const ffi = getBunFFI()
26
+ if (ffi) return ffi.crc32(input)
27
+ return asNumber(resolveNative(ctx, 'crc32')(input)) >>> 0
28
+ },
29
+ fnv1a64(input: Uint8Array): bigint {
30
+ const ffi = getBunFFI()
31
+ if (ffi) return ffi.fnv1a64(input)
32
+ return asBigInt(resolveNative(ctx, 'fnv1a64')(input))
33
+ },
34
+ xxh3(input: Uint8Array): bigint {
35
+ // Optimal by default under Bun: `Bun.hash.xxHash3` beats the rust+FFI
36
+ // crossing (~4x, decision matrix). Node keeps the addon.
37
+ if (isBun()) return Bun.hash.xxHash3(input)
38
+ const ffi = getBunFFI()
39
+ if (ffi) return ffi.xxh3(input)
40
+ return asBigInt(resolveNative(ctx, 'xxh3')(input))
41
+ },
42
+ hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array {
43
+ // Optimal by default under Bun: `Bun.CryptoHasher` (native) beats the
44
+ // rust+FFI crossing (~1.2x, decision matrix). Re-encoded to hex to match
45
+ // castrum's hex-return contract (64 lowercase-hex bytes — same as the
46
+ // addon). Node keeps the FFI path (`hmacSha256` is classified "proven"
47
+ // vs the node:crypto baseline). Napi fallback keeps the precompiled-key
48
+ // `HmacSigner` (identity semantics).
49
+ if (isBun()) {
50
+ const hasher = new Bun.CryptoHasher('sha256', key)
51
+ hasher.update(data)
52
+ return hexEncoder.encode(Buffer.from(hasher.digest()).toString('hex'))
53
+ }
54
+ const ffi = getBunFFI()
55
+ if (ffi) return ffi.hmacSha256(key, data)
56
+ return ctx.hmacSigner(key).sign(data)
57
+ },
58
+ hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number {
59
+ const ffi = getBunFFI()
60
+ if (ffi) return ffi.hmacSha256Into(key, data, output)
61
+ const bytes = ctx.hmacSigner(key).sign(data)
62
+ if (output.length < bytes.length) throw new Error('hmac sha256: output buffer too small')
63
+ output.set(bytes)
64
+ return bytes.length
65
+ },
66
+ hmacSha256Verify(key: Uint8Array, data: Uint8Array, sig: Uint8Array): boolean {
67
+ const ffi = getBunFFI()
68
+ if (ffi) return ffi.hmacSha256Verify(key, data, sig)
69
+ return ctx.hmacSigner(key).verify(data, sig)
70
+ },
71
+ }
72
+ }
@@ -0,0 +1,206 @@
1
+ // src/rust-ffi/scalar/http.ts — HTTP wire-format + parsing scalar methods.
2
+ //
3
+ // Mirrors rust/http/* (packed parsers, media type, etag, HTTP dates,
4
+ // Accept-Encoding, URL codec/join, MIME lookup) plus the `util/validation`
5
+ // validators and the WebSocket accept-key.
6
+
7
+ import type { EncodingPrefResult, MediaTypeResult } from '../../native'
8
+ import { getBunFFI } from '../../native/ffi'
9
+ import { decoder, encoder } from '../../shared/bytes'
10
+ import { isBun } from '../../shared/runtime'
11
+ import { type RustClientContext, resolveNative } from '../context'
12
+
13
+ // Mirror napi `url_decode`'s UTF-8 validation (rust/http/url_codec.rs): only
14
+ // validate when a high byte is present (the ASCII fast path skips it — same as
15
+ // Rust's saw_high check). `url_decode_bytes` / `url_decode_into` skip the check
16
+ // entirely, so those ffi bindings are byte-faithful with no extra work.
17
+ const FATAL_UTF8 = new TextDecoder('utf-8', { fatal: true })
18
+ function ensureValidUtf8(bytes: Uint8Array): void {
19
+ for (let i = 0; i < bytes.length; i++) {
20
+ if ((bytes[i] ?? 0) >= 0x80) {
21
+ try {
22
+ FATAL_UTF8.decode(bytes)
23
+ } catch {
24
+ throw new Error('url decode: invalid UTF-8 in decoded output')
25
+ }
26
+ return
27
+ }
28
+ }
29
+ }
30
+
31
+ /** HTTP / parsing scalar methods (`Pick<RustScalar, ...>`). */
32
+ export function buildHttp(ctx: RustClientContext) {
33
+ // Resolve the native fns through the shared first-use cache so the hot
34
+ // scalar path skips the lazy-addon Proxy get on every call.
35
+ const n = (name: string) => resolveNative(ctx, name)
36
+
37
+ return {
38
+ mimeFromExtension(ext: Uint8Array): Uint8Array {
39
+ return ctx.cachedMime(ext)
40
+ },
41
+ urlEncode(input: Uint8Array): Uint8Array {
42
+ // Under Bun, Bun's native `encodeURIComponent` beats the rust+FFI/NAPI
43
+ // crossing (~4x on the string path, ~1.3x on bytes) and skips the C-ABI
44
+ // call entirely — see docs/bun-builtins-decision-matrix.md. Parity is
45
+ // pinned by test/unit/features/url.test.ts (byte-for-byte, incl.
46
+ // unicode/emoji); `urlEncode` is always applied to text, so the
47
+ // bytes→string→encode→bytes round-trip is lossless.
48
+ if (isBun()) return encoder.encode(encodeURIComponent(decoder.decode(input)))
49
+ const ffi = getBunFFI()
50
+ if (ffi) return ffi.urlEncode(input)
51
+ return n('urlEncode')(input) as Uint8Array
52
+ },
53
+ urlDecode(input: Uint8Array): Uint8Array {
54
+ // Same rationale as `urlEncode`; the strict `urlDecode` already throws on
55
+ // invalid UTF-8 (ensureValidUtf8 below), matching decodeURIComponent's
56
+ // URIError (a subclass of Error), so the JS path is behavior-equivalent.
57
+ if (isBun()) return encoder.encode(decodeURIComponent(decoder.decode(input)))
58
+ const ffi = getBunFFI()
59
+ if (ffi) {
60
+ const out = ffi.urlDecode(input)
61
+ // napi `url_decode` validates UTF-8 (only when a high byte is present);
62
+ // `url_decode_bytes`/`url_decode_into` do not. This is the strict one.
63
+ ensureValidUtf8(out)
64
+ return out
65
+ }
66
+ return n('urlDecode')(input) as Uint8Array
67
+ },
68
+ urlDecodeBytes(input: Uint8Array): Uint8Array {
69
+ const ffi = getBunFFI()
70
+ if (ffi) return ffi.urlDecode(input) // byte semantics match url_decode_bytes
71
+ return n('urlDecodeBytes')(input) as Uint8Array
72
+ },
73
+ urlEncodeInto(input: Uint8Array, output: Uint8Array): number {
74
+ const ffi = getBunFFI()
75
+ if (ffi) return ffi.urlEncodeInto(input, output)
76
+ return n('urlEncodeInto')(input, output) as number
77
+ },
78
+ urlDecodeInto(input: Uint8Array, output: Uint8Array): number {
79
+ const ffi = getBunFFI()
80
+ if (ffi) return ffi.urlDecodeInto(input, output) // no UTF-8 check, matches napi
81
+ return n('urlDecodeInto')(input, output) as number
82
+ },
83
+ validateEmail(input: Uint8Array): boolean {
84
+ const ffi = getBunFFI()
85
+ if (ffi) return ffi.validateEmail(input)
86
+ return n('validateEmail')(input) as boolean
87
+ },
88
+ validateUuid(input: Uint8Array): boolean {
89
+ const ffi = getBunFFI()
90
+ if (ffi) return ffi.validateUuid(input)
91
+ return n('validateUuid')(input) as boolean
92
+ },
93
+ validateIpv4(input: Uint8Array): boolean {
94
+ const ffi = getBunFFI()
95
+ if (ffi) return ffi.validateIpv4(input)
96
+ return n('validateIpv4')(input) as boolean
97
+ },
98
+ validateIpv6(input: Uint8Array): boolean {
99
+ const ffi = getBunFFI()
100
+ if (ffi) return ffi.validateIpv6(input)
101
+ return n('validateIpv6')(input) as boolean
102
+ },
103
+ wsAcceptKey(key: Uint8Array): Uint8Array {
104
+ const ffi = getBunFFI()
105
+ if (ffi) return ffi.wsAcceptKey(key)
106
+ return n('wsAcceptKey')(key) as Uint8Array
107
+ },
108
+ httpParseRequestPacked(input: Uint8Array): Uint8Array {
109
+ const ffi = getBunFFI()
110
+ if (ffi) {
111
+ const out = new Uint8Array(input.length * 9 + 16)
112
+ const w = ffi.httpParseRequestPackedInto(input, out)
113
+ return out.subarray(0, w)
114
+ }
115
+ return n('httpParseRequestPacked')(input) as Uint8Array
116
+ },
117
+ httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number {
118
+ const ffi = getBunFFI()
119
+ if (ffi) return ffi.httpParseRequestPackedInto(input, output)
120
+ return n('httpParseRequestPackedInto')(input, output) as number
121
+ },
122
+ queryParsePacked(input: Uint8Array): Uint8Array {
123
+ const ffi = getBunFFI()
124
+ if (ffi) {
125
+ const out = new Uint8Array(input.length * 9 + 16)
126
+ const w = ffi.queryParsePackedInto(input, out)
127
+ return out.subarray(0, w)
128
+ }
129
+ return n('queryParsePacked')(input) as Uint8Array
130
+ },
131
+ queryParsePackedInto(input: Uint8Array, output: Uint8Array): number {
132
+ const ffi = getBunFFI()
133
+ if (ffi) return ffi.queryParsePackedInto(input, output)
134
+ return n('queryParsePackedInto')(input, output) as number
135
+ },
136
+ cookieParsePacked(input: Uint8Array): Uint8Array {
137
+ const ffi = getBunFFI()
138
+ if (ffi) {
139
+ const out = new Uint8Array(input.length * 9 + 16)
140
+ const w = ffi.cookieParsePackedInto(input, out)
141
+ return out.subarray(0, w)
142
+ }
143
+ return n('cookieParsePacked')(input) as Uint8Array
144
+ },
145
+ cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number {
146
+ const ffi = getBunFFI()
147
+ if (ffi) return ffi.cookieParsePackedInto(input, output)
148
+ return n('cookieParsePackedInto')(input, output) as number
149
+ },
150
+ formParsePacked(input: Uint8Array): Uint8Array {
151
+ const ffi = getBunFFI()
152
+ if (ffi) {
153
+ // x-www-form-urlencoded uses the SAME packed-pairs core as query parse.
154
+ const out = new Uint8Array(input.length * 9 + 16)
155
+ const w = ffi.formParsePackedInto(input, out)
156
+ return out.subarray(0, w)
157
+ }
158
+ return n('formParsePacked')(input) as Uint8Array
159
+ },
160
+ parseMediaType(input: Uint8Array): MediaTypeResult {
161
+ const r = n('parseMediaType')(input) as MediaTypeResult
162
+ // napi serializes Option fields as undefined; normalize to null to match
163
+ // the declared `string | null` public type.
164
+ return {
165
+ mediaType: r.mediaType,
166
+ charset: r.charset ?? null,
167
+ boundary: r.boundary ?? null,
168
+ params: r.params,
169
+ }
170
+ },
171
+ etag(data: Uint8Array, weak?: boolean): Uint8Array {
172
+ const ffi = getBunFFI()
173
+ if (ffi) return ffi.etag(data, weak)
174
+ return n('etag')(data, weak ?? undefined) as Uint8Array
175
+ },
176
+ etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number {
177
+ const ffi = getBunFFI()
178
+ if (ffi) return ffi.etagInto(data, output, weak)
179
+ return n('etagInto')(data, output, weak ?? undefined) as number
180
+ },
181
+ httpDate(secs?: number): Uint8Array {
182
+ // Bun's Date.toUTCString (RFC 1123 IMF-fixdate) beats the rust+FFI
183
+ // crossing (~3.7x, measured); output is byte-identical for HTTP timestamps.
184
+ if (isBun()) {
185
+ const t = secs ?? Math.floor(Date.now() / 1000)
186
+ return encoder.encode(new Date(t * 1000).toUTCString())
187
+ }
188
+ return n('httpDate')(secs ?? undefined) as Uint8Array
189
+ },
190
+ httpDateInto(secs: number | undefined, output: Uint8Array): number {
191
+ return n('httpDateInto')(secs ?? undefined, output) as number
192
+ },
193
+ parseHttpDate(input: Uint8Array): bigint | null {
194
+ return n('parseHttpDate')(input) as bigint | null
195
+ },
196
+ parseAcceptEncoding(input: Uint8Array): EncodingPrefResult[] {
197
+ return n('parseAcceptEncoding')(input) as EncodingPrefResult[]
198
+ },
199
+ urlResolve(base: Uint8Array, reference: Uint8Array): Uint8Array {
200
+ return n('urlResolve')(base, reference) as Uint8Array
201
+ },
202
+ urlEncodeQuery(params: Record<string, string>): Uint8Array {
203
+ return n('urlEncodeQuery')(params) as Uint8Array
204
+ },
205
+ }
206
+ }
@@ -0,0 +1,31 @@
1
+ // src/rust-ffi/scalar/index.ts — Scalar + feature FFI methods (barrel).
2
+ //
3
+ // The byte-level single-value methods of the Rust client (crc32, hmac, json,
4
+ // jwt, password, aead, compress, ... plus the class factories and runtime
5
+ // controls). Composed into the client by ./client.ts.
6
+ //
7
+ // The `RustScalar` interface (the public surface, with its perf JSDoc) lives
8
+ // in `interface.ts`; the implementation is split into domain builders mirroring
9
+ // `rust/` (hashing, json, http, crypto, payload, factories).
10
+
11
+ import type { RustClientContext } from '../context'
12
+ import { buildHashing } from './hashing'
13
+ import { buildJson } from './json'
14
+ import { buildHttp } from './http'
15
+ import { buildCrypto } from './crypto'
16
+ import { buildPayload } from './payload'
17
+ import { buildFactories } from './factories'
18
+
19
+ export type { RustScalar } from './interface'
20
+
21
+ /** Build the scalar/feature method set for a client context. */
22
+ export function buildScalar(ctx: RustClientContext): import('./interface').RustScalar {
23
+ return {
24
+ ...buildHashing(ctx),
25
+ ...buildJson(ctx),
26
+ ...buildHttp(ctx),
27
+ ...buildCrypto(ctx),
28
+ ...buildPayload(ctx),
29
+ ...buildFactories(ctx),
30
+ }
31
+ }