castrum 0.9.3 → 0.9.6

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 (206) hide show
  1. package/Cargo.lock +2242 -0
  2. package/Cargo.toml +155 -0
  3. package/README.md +14 -6
  4. package/build.rs +5 -0
  5. package/castrum.darwin-arm64.node +0 -0
  6. package/castrum.darwin-x64.node +0 -0
  7. package/castrum.linux-arm64-gnu.node +0 -0
  8. package/castrum.linux-arm64-musl.node +0 -0
  9. package/castrum.linux-x64-gnu.node +0 -0
  10. package/castrum.linux-x64-musl.node +0 -0
  11. package/castrum.linux-x64-v3-gnu.node +0 -0
  12. package/castrum.win32-x64-msvc.node +0 -0
  13. package/dist/index.d.ts +1508 -1291
  14. package/dist/index.js +3391 -2069
  15. package/docs/API.md +2 -1
  16. package/docs/ARCHITECTURE.md +3 -2
  17. package/docs/CASE_STUDY.md +5 -3
  18. package/docs/ENVIRONMENT.md +25 -2
  19. package/docs/FFI_BUN_GUIDE.md +242 -14
  20. package/docs/HOW_TO_ADD_AN_OP.md +10 -0
  21. package/docs/REPO_MAP.md +17 -6
  22. package/docs/RND-CONCURRENCY.md +772 -0
  23. package/docs/bun-builtins-decision-matrix.md +4 -1
  24. package/index.ts +24 -25
  25. package/package.json +173 -156
  26. package/rust/crypto/aead.rs +378 -0
  27. package/rust/crypto/argon2.rs +265 -0
  28. package/rust/crypto/base64.rs +420 -0
  29. package/rust/crypto/bcrypt.rs +89 -0
  30. package/rust/crypto/cookie_sign.rs +169 -0
  31. package/rust/crypto/csrf.rs +106 -0
  32. package/rust/crypto/ed25519.rs +215 -0
  33. package/rust/crypto/hashing.rs +140 -0
  34. package/rust/crypto/hmac_sha256.rs +88 -0
  35. package/rust/crypto/jwt/api.rs +286 -0
  36. package/rust/crypto/jwt/mod.rs +22 -0
  37. package/rust/crypto/jwt/tests.rs +312 -0
  38. package/rust/crypto/jwt/token.rs +360 -0
  39. package/rust/crypto/mod.rs +27 -0
  40. package/rust/crypto/pbkdf2.rs +105 -0
  41. package/rust/crypto/random_token.rs +85 -0
  42. package/rust/crypto/session.rs +222 -0
  43. package/rust/ffi/crypto.rs +708 -0
  44. package/rust/ffi/hashing.rs +285 -0
  45. package/rust/ffi/http.rs +752 -0
  46. package/rust/ffi/ingress.rs +303 -0
  47. package/rust/ffi/json.rs +517 -0
  48. package/rust/ffi/jwt.rs +419 -0
  49. package/rust/ffi/metrics.rs +373 -0
  50. package/rust/ffi/mod.rs +69 -0
  51. package/rust/ffi/payload.rs +275 -0
  52. package/rust/ffi/probe.rs +60 -0
  53. package/rust/ffi/rate_limit.rs +77 -0
  54. package/rust/ffi/route.rs +85 -0
  55. package/rust/ffi/task.rs +172 -0
  56. package/rust/ffi/tests.rs +2333 -0
  57. package/rust/ffi/util.rs +152 -0
  58. package/rust/ffi/validators.rs +123 -0
  59. package/rust/http/accept.rs +606 -0
  60. package/rust/http/cookie_parser.rs +140 -0
  61. package/rust/http/etag.rs +289 -0
  62. package/rust/http/form.rs +136 -0
  63. package/rust/http/headers.rs +267 -0
  64. package/rust/http/http_date.rs +286 -0
  65. package/rust/http/http_parser.rs +252 -0
  66. package/rust/http/media_type.rs +355 -0
  67. package/rust/http/method.rs +200 -0
  68. package/rust/http/mime_lookup.rs +130 -0
  69. package/rust/http/mod.rs +32 -0
  70. package/rust/http/multipart.rs +685 -0
  71. package/rust/http/query_parser.rs +211 -0
  72. package/rust/http/url_codec.rs +296 -0
  73. package/rust/http/url_join.rs +324 -0
  74. package/rust/ingress/api.rs +355 -0
  75. package/rust/ingress/cors.rs +403 -0
  76. package/rust/ingress/ingress_constants.rs +209 -0
  77. package/rust/ingress/ip_trust.rs +312 -0
  78. package/rust/ingress/mod.rs +48 -0
  79. package/rust/ingress/native_route.rs +770 -0
  80. package/rust/ingress/options.rs +176 -0
  81. package/rust/ingress/output.rs +406 -0
  82. package/rust/ingress/packed.rs +191 -0
  83. package/rust/ingress/pipeline.rs +534 -0
  84. package/rust/ingress/proxy.rs +138 -0
  85. package/rust/ingress/rate_limit.rs +538 -0
  86. package/rust/ingress/terminal.rs +114 -0
  87. package/rust/ingress/tests.rs +567 -0
  88. package/rust/ingress/time.rs +70 -0
  89. package/rust/json/fast_schema/capture.rs +336 -0
  90. package/rust/json/fast_schema/compile.rs +556 -0
  91. package/rust/json/fast_schema/cursor.rs +368 -0
  92. package/rust/json/fast_schema/email.rs +282 -0
  93. package/rust/json/fast_schema/errors.rs +247 -0
  94. package/rust/json/fast_schema/mod.rs +57 -0
  95. package/rust/json/fast_schema/tests.rs +1143 -0
  96. package/rust/json/fast_schema/types.rs +140 -0
  97. package/rust/json/fast_schema/validate.rs +931 -0
  98. package/rust/json/json_ops.rs +121 -0
  99. package/rust/json/json_schema.rs +837 -0
  100. package/rust/json/json_ser.rs +883 -0
  101. package/rust/json/mod.rs +19 -0
  102. package/rust/json/napi_marshal.rs +318 -0
  103. package/rust/json/patch/api.rs +60 -0
  104. package/rust/json/patch/engine.rs +153 -0
  105. package/rust/json/patch/mod.rs +47 -0
  106. package/rust/json/patch/ops.rs +270 -0
  107. package/rust/json/patch/pointer.rs +49 -0
  108. package/rust/json/patch/tests.rs +499 -0
  109. package/rust/lib.rs +73 -0
  110. package/rust/metrics/api.rs +148 -0
  111. package/rust/metrics/mod.rs +21 -0
  112. package/rust/metrics/registry.rs +961 -0
  113. package/rust/panic_safety.rs +40 -0
  114. package/rust/payload/compress.rs +512 -0
  115. package/rust/payload/mod.rs +13 -0
  116. package/rust/payload/sse.rs +214 -0
  117. package/rust/payload/template.rs +253 -0
  118. package/rust/payload/websocket.rs +69 -0
  119. package/rust/payload/ws_frames.rs +354 -0
  120. package/rust/proptest_suite.rs +139 -0
  121. package/rust/selection.rs +95 -0
  122. package/rust/src/bin/native-bench.rs +495 -0
  123. package/rust/task/completion.rs +119 -0
  124. package/rust/task/mod.rs +65 -0
  125. package/rust/task/ops.rs +503 -0
  126. package/rust/task/runtime.rs +220 -0
  127. package/rust/task/tests.rs +231 -0
  128. package/rust/test_support.rs +71 -0
  129. package/rust/util/batch/api.rs +266 -0
  130. package/rust/util/batch/core.rs +473 -0
  131. package/rust/util/batch/mod.rs +23 -0
  132. package/rust/util/batch/tests.rs +445 -0
  133. package/rust/util/batch_core.rs +184 -0
  134. package/rust/util/bytes.rs +594 -0
  135. package/rust/util/mod.rs +34 -0
  136. package/rust/util/packed.rs +473 -0
  137. package/rust/util/text.rs +130 -0
  138. package/rust/util/threadpool.rs +128 -0
  139. package/rust/util/validation.rs +419 -0
  140. package/rust-toolchain.toml +17 -0
  141. package/scripts/postinstall.mjs +199 -0
  142. package/src/ingress/constants.ts +1 -1
  143. package/src/ingress/context.ts +2 -2
  144. package/src/ingress/decode/baked-result.ts +13 -13
  145. package/src/ingress/decode/fast-result.ts +17 -17
  146. package/src/ingress/errors.ts +7 -7
  147. package/src/ingress/headers/baked-templates.ts +1 -1
  148. package/src/ingress/headers/fast-templates.ts +3 -3
  149. package/src/ingress/health.ts +1 -2
  150. package/src/ingress/index.ts +1 -1
  151. package/src/ingress/native-route.ts +9 -5
  152. package/src/ingress/options.ts +1 -1
  153. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  154. package/src/ingress/packing/route-wire.ts +1 -5
  155. package/src/ingress/response/error-bodies.ts +5 -5
  156. package/src/ingress/response/terminal.ts +1 -1
  157. package/src/ingress/routes/echo.ts +4 -4
  158. package/src/ingress/routes/fallback.ts +2 -2
  159. package/src/ingress/routes/head.ts +2 -2
  160. package/src/ingress/routes/native.ts +2 -1
  161. package/src/ingress/routes/options.ts +1 -1
  162. package/src/ingress/routes/read.ts +2 -2
  163. package/src/ingress/server.ts +10 -6
  164. package/src/ingress/status.ts +7 -7
  165. package/src/integration/batch.ts +1 -1
  166. package/src/integration/index.ts +9 -9
  167. package/src/integration/pipeline.ts +6 -6
  168. package/src/integration/websocket.ts +2 -2
  169. package/src/loader/index.ts +1 -5
  170. package/src/loader/ops.ts +1 -1
  171. package/src/native/ffi/build/codecs.ts +130 -12
  172. package/src/native/ffi/build/compress.ts +22 -10
  173. package/src/native/ffi/build/instances.ts +46 -4
  174. package/src/native/ffi/build/metrics.ts +247 -0
  175. package/src/native/ffi/build/parse.ts +59 -23
  176. package/src/native/ffi/build/task.ts +79 -0
  177. package/src/native/ffi/build/util.ts +32 -1
  178. package/src/native/ffi/build.ts +4 -0
  179. package/src/native/ffi/selftest.ts +12 -3
  180. package/src/native/ffi/types.ts +151 -9
  181. package/src/native/ffi.ts +160 -15
  182. package/src/native/index.ts +1 -1
  183. package/src/native/loader.ts +55 -5
  184. package/src/native/types/instances.ts +34 -0
  185. package/src/native/types.ts +11 -0
  186. package/src/runtime/codec.ts +53 -9
  187. package/src/runtime/types.ts +7 -0
  188. package/src/rust-ffi/batch/build.ts +16 -9
  189. package/src/rust-ffi/index.ts +7 -6
  190. package/src/rust-ffi/options.ts +1 -3
  191. package/src/rust-ffi/proven.ts +3 -3
  192. package/src/rust-ffi/scalar/crypto.ts +1 -1
  193. package/src/rust-ffi/scalar/factories.ts +73 -0
  194. package/src/rust-ffi/scalar/hashing.ts +1 -1
  195. package/src/rust-ffi/scalar/http.ts +71 -27
  196. package/src/rust-ffi/scalar/index.ts +2 -0
  197. package/src/rust-ffi/scalar/interface.ts +20 -0
  198. package/src/rust-ffi/scalar/payload.ts +11 -3
  199. package/src/rust-ffi/scalar/validation.ts +121 -0
  200. package/src/rust-ffi/text.ts +25 -3
  201. package/src/selection.ts +1 -1
  202. package/src/shared/bytes.ts +24 -4
  203. package/src/shared/codec.ts +18 -3
  204. package/src/task/index.ts +24 -0
  205. package/src/task/op.ts +171 -0
  206. package/src/task/runtime.ts +568 -0
@@ -0,0 +1,420 @@
1
+ // rust/crypto/base64.rs — base64 (standard/url-safe) + hex encode/decode.
2
+ //
3
+ // Uses the `base64` crate engines (Cargo dep already present). The
4
+ // `Base64Codec` higher-order instance precompiles the alphabet/decoding
5
+ // configuration once in its constructor and reuses it across calls.
6
+
7
+ use napi::bindgen_prelude::*;
8
+ use napi_derive::napi;
9
+
10
+ use crate::util::bytes::{hex_val, HEX_LOWER};
11
+ use base64::Engine as _;
12
+
13
+ fn engine(url_safe: bool, padding: bool) -> base64::engine::general_purpose::GeneralPurpose {
14
+ use base64::engine::general_purpose::*;
15
+ match (url_safe, padding) {
16
+ (false, true) => STANDARD,
17
+ (false, false) => STANDARD_NO_PAD,
18
+ (true, true) => URL_SAFE,
19
+ (true, false) => URL_SAFE_NO_PAD,
20
+ }
21
+ }
22
+
23
+ #[napi]
24
+ pub fn base64_encode(input: Uint8Array, url_safe: Option<bool>, padding: Option<bool>) -> Buffer {
25
+ let out = engine(url_safe.unwrap_or(false), padding.unwrap_or(true)).encode(input.as_ref());
26
+ Buffer::from(out.into_bytes())
27
+ }
28
+
29
+ #[napi]
30
+ pub fn base64_decode(
31
+ input: Uint8Array,
32
+ url_safe: Option<bool>,
33
+ padding: Option<bool>,
34
+ ) -> Result<Buffer> {
35
+ engine(url_safe.unwrap_or(false), padding.unwrap_or(true))
36
+ .decode(input.as_ref())
37
+ .map(Buffer::from)
38
+ .map_err(|e| Error::from_reason(e.to_string()))
39
+ }
40
+
41
+ #[napi]
42
+ pub fn base64url_encode(input: Uint8Array) -> Buffer {
43
+ Buffer::from(
44
+ base64::engine::general_purpose::URL_SAFE_NO_PAD
45
+ .encode(input.as_ref())
46
+ .into_bytes(),
47
+ )
48
+ }
49
+
50
+ #[napi]
51
+ pub fn base64url_decode(input: Uint8Array) -> Result<Buffer> {
52
+ base64::engine::general_purpose::URL_SAFE_NO_PAD
53
+ .decode(input.as_ref())
54
+ .map(Buffer::from)
55
+ .map_err(|e| Error::from_reason(e.to_string()))
56
+ }
57
+
58
+ /// Base64-encode to raw bytes (no Buffer wrapping). Shared by the scalar and
59
+ /// packed-batch paths.
60
+ pub fn base64_encode_bytes(input: &[u8], url_safe: bool, padding: bool) -> Vec<u8> {
61
+ engine(url_safe, padding).encode(input).into_bytes()
62
+ }
63
+
64
+ /// Base64-decode from raw bytes. Errors on invalid input. Shared by the scalar
65
+ /// and packed-batch paths.
66
+ pub fn base64_decode_bytes(input: &[u8], url_safe: bool, padding: bool) -> Result<Vec<u8>> {
67
+ engine(url_safe, padding)
68
+ .decode(input)
69
+ .map_err(|e| Error::from_reason(e.to_string()))
70
+ }
71
+
72
+ /// Base64url (RFC 7515 §2 — URL-safe alphabet, NO padding) encode to raw
73
+ /// bytes. The single shared source of truth for JWT segment encoding
74
+ /// (`crate::crypto::jwt`) and Ed25519 keypair serialization
75
+ /// (`crate::crypto::ed25519`) — keeps the url-safe alphabet defined in exactly
76
+ /// one place.
77
+ #[inline]
78
+ pub fn base64url_encode_bytes(data: &[u8]) -> Vec<u8> {
79
+ base64::engine::general_purpose::URL_SAFE_NO_PAD
80
+ .encode(data)
81
+ .into_bytes()
82
+ }
83
+
84
+ /// Base64url (RFC 7515 §2 — URL-safe alphabet, NO padding) decode from raw
85
+ /// bytes. Returns `None` on invalid UTF-8 or non-base64url input.
86
+ #[inline]
87
+ pub fn base64url_decode_bytes(data: &[u8]) -> Option<Vec<u8>> {
88
+ let text = std::str::from_utf8(data).ok()?;
89
+ base64::engine::general_purpose::URL_SAFE_NO_PAD
90
+ .decode(text)
91
+ .ok()
92
+ }
93
+
94
+ // ── hex ──────────────────────────────────────────────────────────
95
+
96
+ /// Encode bytes as lowercase hex (uses the shared `HEX_LOWER` table — direct
97
+ /// byte writes, no per-nibble `char` push).
98
+ ///
99
+ /// # Safety
100
+ ///
101
+ /// Hex output is always ASCII (`HEX_LOWER` is a byte string of `0-9a-f`), so
102
+ /// the built `Vec<u8>` is valid UTF-8 by construction; `from_utf8_unchecked`
103
+ /// skips a redundant validation pass on the allocating hot path.
104
+ pub fn hex_encode_bytes(input: &[u8]) -> String {
105
+ let mut out = Vec::with_capacity(input.len() * 2);
106
+ for &b in input {
107
+ out.push(HEX_LOWER[(b >> 4) as usize]);
108
+ out.push(HEX_LOWER[(b & 0x0f) as usize]);
109
+ }
110
+ // SAFETY: every byte pushed is an ASCII hex digit from `HEX_LOWER`.
111
+ unsafe { String::from_utf8_unchecked(out) }
112
+ }
113
+
114
+ /// Decode lowercase/uppercase hex to bytes.
115
+ pub fn hex_decode_bytes(input: &[u8]) -> std::result::Result<Vec<u8>, &'static str> {
116
+ if !input.len().is_multiple_of(2) {
117
+ return Err("odd hex length");
118
+ }
119
+ let mut out = Vec::with_capacity(input.len() / 2);
120
+ for i in (0..input.len()).step_by(2) {
121
+ let hi = hex_val(input[i]).ok_or("invalid hex digit")?;
122
+ let lo = hex_val(input[i + 1]).ok_or("invalid hex digit")?;
123
+ out.push((hi << 4) | lo);
124
+ }
125
+ Ok(out)
126
+ }
127
+
128
+ #[napi]
129
+ pub fn hex_encode(input: Uint8Array) -> Buffer {
130
+ Buffer::from(hex_encode_bytes(input.as_ref()).into_bytes())
131
+ }
132
+
133
+ #[napi]
134
+ pub fn hex_decode(input: Uint8Array) -> Result<Buffer> {
135
+ hex_decode_bytes(input.as_ref())
136
+ .map(Buffer::from)
137
+ .map_err(|e| Error::from_reason(e.to_string()))
138
+ }
139
+
140
+ // ── Reusable-output (_into) variants ────────────────────────────
141
+ //
142
+ // These write into a caller-provided output buffer and return the number of
143
+ // bytes written (u32), so hot loops can pool the buffer instead of allocating
144
+ // a fresh Vec + napi Buffer per call. They error on a buffer that is too small;
145
+ // input/output overlap is handled by `crate::util::run_packed_into`.
146
+
147
+ /// Lowercase-hex encode directly into `out`. Returns bytes written
148
+ /// (`input.len() * 2`). Errors if `out` is too small.
149
+ #[inline]
150
+ pub fn hex_encode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
151
+ let needed = input
152
+ .len()
153
+ .checked_mul(2)
154
+ .ok_or_else(|| Error::from_reason("hex encode: input too large (length overflow)"))?;
155
+ if out.len() < needed {
156
+ return Err(Error::from_reason("hex encode: output buffer too small"));
157
+ }
158
+ let mut pos = 0usize;
159
+ for &b in input {
160
+ out[pos] = HEX_LOWER[(b >> 4) as usize];
161
+ out[pos + 1] = HEX_LOWER[(b & 0x0f) as usize];
162
+ pos += 2;
163
+ }
164
+ Ok(pos)
165
+ }
166
+
167
+ #[napi]
168
+ pub fn hex_encode_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
169
+ crate::util::run_packed_into(&input, &mut output, hex_encode_into_slice)
170
+ }
171
+
172
+ /// Hex-decode directly into `out`. Returns bytes written (`input.len() / 2`).
173
+ /// Errors on odd length, invalid digits, or an output buffer that is too small.
174
+ #[inline]
175
+ pub fn hex_decode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
176
+ if !input.len().is_multiple_of(2) {
177
+ return Err(Error::from_reason("odd hex length"));
178
+ }
179
+ let needed = input.len() / 2;
180
+ if out.len() < needed {
181
+ return Err(Error::from_reason("hex decode: output buffer too small"));
182
+ }
183
+ let mut pos = 0usize;
184
+ for i in (0..input.len()).step_by(2) {
185
+ let hi = hex_val(input[i]).ok_or_else(|| Error::from_reason("invalid hex digit"))?;
186
+ let lo = hex_val(input[i + 1]).ok_or_else(|| Error::from_reason("invalid hex digit"))?;
187
+ out[pos] = (hi << 4) | lo;
188
+ pos += 1;
189
+ }
190
+ Ok(pos)
191
+ }
192
+
193
+ #[napi]
194
+ pub fn hex_decode_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
195
+ crate::util::run_packed_into(&input, &mut output, hex_decode_into_slice)
196
+ }
197
+
198
+ /// Pure core: base64-encode `input` into `out` (zero-alloc `encode_slice`).
199
+ /// Returns bytes written; errors if `out` is too small. Shared by the `_into`
200
+ /// napi path and the pure-core unit tests.
201
+ pub fn base64_encode_into_slice(
202
+ input: &[u8],
203
+ out: &mut [u8],
204
+ url_safe: bool,
205
+ padding: bool,
206
+ ) -> Result<usize> {
207
+ engine(url_safe, padding)
208
+ .encode_slice(input, out)
209
+ .map_err(|e| Error::from_reason(format!("base64 encode: {e}")))
210
+ }
211
+
212
+ /// Base64-encode into a caller-provided output buffer (zero-alloc via
213
+ /// `Engine::encode_slice`). Returns bytes written; errors if `output` is too
214
+ /// small.
215
+ #[napi]
216
+ pub fn base64_encode_into(
217
+ input: Uint8Array,
218
+ mut output: Uint8Array,
219
+ url_safe: Option<bool>,
220
+ padding: Option<bool>,
221
+ ) -> Result<u32> {
222
+ crate::util::run_packed_into(&input, &mut output, move |inp, out| {
223
+ base64_encode_into_slice(inp, out, url_safe.unwrap_or(false), padding.unwrap_or(true))
224
+ })
225
+ }
226
+
227
+ /// Pure core: base64-decode `input` into `out` (zero-alloc `decode_slice`).
228
+ /// Returns bytes written; errors on invalid input or a too-small output
229
+ /// buffer. Shared by the `_into` napi path and the pure-core unit tests.
230
+ pub fn base64_decode_into_slice(
231
+ input: &[u8],
232
+ out: &mut [u8],
233
+ url_safe: bool,
234
+ padding: bool,
235
+ ) -> Result<usize> {
236
+ engine(url_safe, padding)
237
+ .decode_slice(input, out)
238
+ .map_err(|e| Error::from_reason(format!("base64 decode: {e}")))
239
+ }
240
+
241
+ /// Base64-decode into a caller-provided output buffer (zero-alloc via
242
+ /// `Engine::decode_slice`). Returns bytes written; errors on invalid input or
243
+ /// an output buffer that is too small.
244
+ #[napi]
245
+ pub fn base64_decode_into(
246
+ input: Uint8Array,
247
+ mut output: Uint8Array,
248
+ url_safe: Option<bool>,
249
+ padding: Option<bool>,
250
+ ) -> Result<u32> {
251
+ crate::util::run_packed_into(&input, &mut output, move |inp, out| {
252
+ base64_decode_into_slice(inp, out, url_safe.unwrap_or(false), padding.unwrap_or(true))
253
+ })
254
+ }
255
+
256
+ /// Higher-order instance: the alphabet/padding engine is selected ONCE at
257
+ /// construction and stored concretely, so `encode`/`decode` never re-run the
258
+ /// 4-way engine match on the per-call path.
259
+ #[napi]
260
+ pub struct Base64Codec {
261
+ engine: base64::engine::general_purpose::GeneralPurpose,
262
+ }
263
+
264
+ #[napi]
265
+ impl Base64Codec {
266
+ #[napi(constructor)]
267
+ pub fn new(url_safe: Option<bool>, padding: Option<bool>) -> Self {
268
+ Self {
269
+ engine: engine(url_safe.unwrap_or(false), padding.unwrap_or(true)),
270
+ }
271
+ }
272
+
273
+ #[napi]
274
+ pub fn encode(&self, input: Uint8Array) -> Buffer {
275
+ Buffer::from(self.engine.encode(input.as_ref()).into_bytes())
276
+ }
277
+
278
+ #[napi]
279
+ pub fn decode(&self, input: Uint8Array) -> Result<Buffer> {
280
+ self.engine
281
+ .decode(input.as_ref())
282
+ .map(Buffer::from)
283
+ .map_err(|e| Error::from_reason(e.to_string()))
284
+ }
285
+ }
286
+
287
+ #[cfg(test)]
288
+ mod tests {
289
+ use super::*;
290
+
291
+ #[test]
292
+ fn base64_roundtrip_standard() {
293
+ let data = b"hello world!";
294
+ let enc = base64::engine::general_purpose::STANDARD.encode(data);
295
+ assert_eq!(
296
+ base64_decode(Uint8Array::new(enc.into_bytes()), None, None)
297
+ .unwrap()
298
+ .as_ref(),
299
+ data
300
+ );
301
+ }
302
+
303
+ #[test]
304
+ fn base64url_no_padding() {
305
+ // 0xfb → 111110 11 → "-w" (url-safe, no padding)
306
+ let enc = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0xfb]);
307
+ assert_eq!(enc, "-w");
308
+ let dec = base64url_decode(Uint8Array::new(enc.into_bytes())).unwrap();
309
+ assert_eq!(dec.as_ref(), [0xfb]);
310
+ }
311
+
312
+ #[test]
313
+ fn base64_decode_rejects_invalid() {
314
+ assert!(base64_decode(Uint8Array::new(b"!!!".to_vec()), None, None).is_err());
315
+ }
316
+
317
+ #[test]
318
+ fn hex_roundtrip() {
319
+ let data = b"\x00\x01\xfe\xffAB";
320
+ let enc = hex_encode_bytes(data);
321
+ assert_eq!(enc, "0001feff4142");
322
+ assert_eq!(hex_decode_bytes(enc.as_bytes()).unwrap(), data);
323
+ }
324
+
325
+ #[test]
326
+ fn hex_rejects_odd_and_bad() {
327
+ assert!(hex_decode_bytes(b"abc").is_err());
328
+ assert!(hex_decode_bytes(b"zz").is_err());
329
+ }
330
+
331
+ #[test]
332
+ fn codec_instance_encodes_urlsafe() {
333
+ let c = Base64Codec::new(Some(true), Some(false));
334
+ let enc = c.encode(Uint8Array::new(b"\xfb".to_vec()));
335
+ assert_eq!(enc.as_ref(), b"-w");
336
+ }
337
+
338
+ // ── reusable-output (_into) variants ──
339
+
340
+ #[test]
341
+ fn hex_encode_into_slice_matches_allocating() {
342
+ let data = b"\x00\x01\xfe\xffAB";
343
+ let expected = hex_encode_bytes(data);
344
+ let mut out = [0u8; 32];
345
+ let n = hex_encode_into_slice(data, &mut out).unwrap();
346
+ assert_eq!(n, expected.len());
347
+ assert_eq!(&out[..n], expected.as_bytes());
348
+ }
349
+
350
+ #[test]
351
+ fn hex_encode_into_reports_length() {
352
+ let data = b"hello";
353
+ let out = Uint8Array::new(vec![0u8; 16]);
354
+ let n = hex_encode_into(Uint8Array::new(data.to_vec()), out).unwrap();
355
+ assert_eq!(n as usize, data.len() * 2);
356
+ }
357
+
358
+ #[test]
359
+ fn hex_encode_into_small_buffer_errors() {
360
+ let mut out = [0u8; 2];
361
+ assert!(hex_encode_into_slice(b"abc", &mut out).is_err());
362
+ let out2 = Uint8Array::new(vec![0u8; 4]);
363
+ assert!(hex_encode_into(Uint8Array::new(b"abcdef".to_vec()), out2).is_err());
364
+ }
365
+
366
+ #[test]
367
+ fn hex_decode_into_roundtrips() {
368
+ let enc = hex_encode_bytes(b"\x00\x01\xfe\xffAB");
369
+ let mut out = [0u8; 16];
370
+ let n = hex_decode_into_slice(enc.as_bytes(), &mut out).unwrap();
371
+ assert_eq!(&out[..n], b"\x00\x01\xfe\xffAB");
372
+ }
373
+
374
+ #[test]
375
+ fn hex_decode_into_rejects_bad_input() {
376
+ let mut out = [0u8; 4];
377
+ assert!(hex_decode_into_slice(b"abc", &mut out).is_err());
378
+ assert!(hex_decode_into_slice(b"zz", &mut out).is_err());
379
+ }
380
+
381
+ #[test]
382
+ fn hex_decode_into_small_buffer_errors() {
383
+ let mut out = [0u8; 2];
384
+ assert!(hex_decode_into_slice(b"0001020304", &mut out).is_err());
385
+ }
386
+
387
+ #[test]
388
+ fn base64_encode_into_reports_length() {
389
+ let data = b"hello world!";
390
+ let out = Uint8Array::new(vec![0u8; 64]);
391
+ let n = base64_encode_into(Uint8Array::new(data.to_vec()), out, None, None).unwrap();
392
+ assert_eq!(
393
+ n as usize,
394
+ base64::engine::general_purpose::STANDARD.encode(data).len()
395
+ );
396
+ }
397
+
398
+ #[test]
399
+ fn base64_encode_into_small_buffer_errors() {
400
+ let out = Uint8Array::new(vec![0u8; 4]);
401
+ assert!(
402
+ base64_encode_into(Uint8Array::new(b"hello world!".to_vec()), out, None, None).is_err()
403
+ );
404
+ }
405
+
406
+ #[test]
407
+ fn base64_decode_into_roundtrips() {
408
+ let data = b"hello world!";
409
+ let enc = base64::engine::general_purpose::STANDARD.encode(data);
410
+ let out = Uint8Array::new(vec![0u8; 32]);
411
+ let n = base64_decode_into(Uint8Array::new(enc.into_bytes()), out, None, None).unwrap();
412
+ assert_eq!(n as usize, data.len());
413
+ }
414
+
415
+ #[test]
416
+ fn base64_decode_into_rejects_invalid() {
417
+ let out = Uint8Array::new(vec![0u8; 32]);
418
+ assert!(base64_decode_into(Uint8Array::new(b"!!!".to_vec()), out, None, None).is_err());
419
+ }
420
+ }
@@ -0,0 +1,89 @@
1
+ // rust/crypto/bcrypt.rs — bcrypt password hashing.
2
+ //
3
+ // Pure-Rust (bcrypt crate, Blowfish-based). Unlike argon2id (memory-hard), a
4
+ // bcrypt cost of `2^cost` is CPU-bound. We expose the standard `$2b$` PHC
5
+ // string format so the output is interchangeable with Bun.password / bcryptjs.
6
+ //
7
+ // Pure-Rust core stays unit-testable; only the entry points use napi types.
8
+
9
+ use napi::bindgen_prelude::*;
10
+ use napi_derive::napi;
11
+
12
+ /// bcrypt cost lower bound (bcrypt crate enforces 4..=31).
13
+ pub const BCRYPT_MIN_COST: u32 = 4;
14
+ /// bcrypt cost upper bound.
15
+ pub const BCRYPT_MAX_COST: u32 = 31;
16
+
17
+ /// Clamp a caller cost into the bcrypt-legal range (4..=31).
18
+ #[inline]
19
+ pub fn clamp_cost(cost: u32) -> u32 {
20
+ cost.clamp(BCRYPT_MIN_COST, BCRYPT_MAX_COST)
21
+ }
22
+
23
+ /// Hash a password with bcrypt at `cost`, returning a `$2b$` PHC string.
24
+ #[napi]
25
+ pub fn password_hash_bcrypt(password: Uint8Array, cost: u32) -> Result<String> {
26
+ let cost = clamp_cost(cost);
27
+ bcrypt::hash(password.as_ref(), cost).map_err(|e| Error::from_reason(e.to_string()))
28
+ }
29
+
30
+ /// Verify a password against a bcrypt `$2b$` PHC string.
31
+ /// Returns false on ANY malformed hash (parity with argon2 `password_verify`).
32
+ #[napi]
33
+ pub fn password_verify_bcrypt(password: Uint8Array, hash: String) -> bool {
34
+ bcrypt::verify(password.as_ref(), &hash).unwrap_or(false)
35
+ }
36
+
37
+ #[cfg(test)]
38
+ mod tests {
39
+ use super::*;
40
+
41
+ #[test]
42
+ fn clamp_cost_bounds() {
43
+ assert_eq!(clamp_cost(0), BCRYPT_MIN_COST);
44
+ assert_eq!(clamp_cost(3), BCRYPT_MIN_COST);
45
+ assert_eq!(clamp_cost(4), 4);
46
+ assert_eq!(clamp_cost(10), 10);
47
+ assert_eq!(clamp_cost(31), 31);
48
+ assert_eq!(clamp_cost(99), BCRYPT_MAX_COST);
49
+ }
50
+
51
+ #[test]
52
+ fn bcrypt_roundtrip_and_wrong_password() {
53
+ let h = bcrypt::hash(b"password".as_slice(), 4).unwrap();
54
+ assert!(h.starts_with("$2b$04$"));
55
+ assert_eq!(h.len(), 60); // PHC bcrypt strings are exactly 60 chars
56
+ assert!(bcrypt::verify(b"password".as_slice(), &h).unwrap());
57
+ assert!(!bcrypt::verify(b"wrong".as_slice(), &h).unwrap());
58
+ }
59
+
60
+ #[test]
61
+ fn bcrypt_is_cost_sensitive_and_self_contained() {
62
+ // Same password, two hashes differ (random salt) but both verify.
63
+ let a = bcrypt::hash(b"pw".as_slice(), 4).unwrap();
64
+ let b = bcrypt::hash(b"pw".as_slice(), 4).unwrap();
65
+ assert_ne!(a, b);
66
+ assert!(bcrypt::verify(b"pw".as_slice(), &a).unwrap());
67
+ assert!(bcrypt::verify(b"pw".as_slice(), &b).unwrap());
68
+ }
69
+
70
+ #[test]
71
+ fn napi_wrapper_clamps_and_roundtrips() {
72
+ // cost 3 is clamped up to 4 (cheap) — exercises the napi boundary.
73
+ let h = password_hash_bcrypt(Uint8Array::new(b"pw".to_vec()), 3).unwrap();
74
+ assert!(h.starts_with("$2b$04$"));
75
+ assert!(password_verify_bcrypt(
76
+ Uint8Array::new(b"pw".to_vec()),
77
+ h.clone()
78
+ ));
79
+ assert!(!password_verify_bcrypt(
80
+ Uint8Array::new(b"nope".to_vec()),
81
+ h
82
+ ));
83
+ // Malformed hash -> false (not a throw), matching argon2 semantics.
84
+ assert!(!password_verify_bcrypt(
85
+ Uint8Array::new(b"pw".to_vec()),
86
+ "garbage".to_string()
87
+ ));
88
+ }
89
+ }
@@ -0,0 +1,169 @@
1
+ // rust/crypto/cookie_sign.rs — signed cookies (`value.signature`, HMAC-SHA256).
2
+ //
3
+ // Composes the existing HMAC core + hex tables. The `CookieSigner`
4
+ // higher-order instance precomputes the HMAC key once in its constructor and
5
+ // reuses it across sign/verify calls.
6
+
7
+ use aws_lc_rs::hmac;
8
+ use napi::bindgen_prelude::*;
9
+ use napi_derive::napi;
10
+
11
+ use crate::util::bytes::{hex_decode_32, hex_encode_32};
12
+
13
+ /// Zero-alloc core: `value` ++ "." ++ 64-hex into `out`. Returns the bytes
14
+ /// written, or `None` on a too-small buffer. Used by the C-ABI (`bun:ffi`)
15
+ /// path to avoid the signed-cookie Vec + copy.
16
+ pub fn sign_cookie_into(value: &[u8], key: &hmac::Key, out: &mut [u8]) -> Option<usize> {
17
+ let need = value.len() + 1 + 64;
18
+ if out.len() < need {
19
+ return None;
20
+ }
21
+ let tag = hmac::sign(key, value);
22
+ let mut sig = [0u8; 64];
23
+ hex_encode_32(tag.as_ref(), &mut sig);
24
+ out[..value.len()].copy_from_slice(value);
25
+ out[value.len()] = b'.';
26
+ out[value.len() + 1..need].copy_from_slice(&sig);
27
+ Some(need)
28
+ }
29
+
30
+ /// `value` ++ "." ++ lowercase-hex(HMAC-SHA256(secret, value)).
31
+ pub fn sign_cookie_bytes(value: &[u8], key: &hmac::Key) -> Vec<u8> {
32
+ let tag = hmac::sign(key, value);
33
+ let mut sig = [0u8; 64];
34
+ hex_encode_32(tag.as_ref(), &mut sig);
35
+ let mut out = Vec::with_capacity(value.len() + 1 + 64);
36
+ out.extend_from_slice(value);
37
+ out.push(b'.');
38
+ out.extend_from_slice(&sig);
39
+ out
40
+ }
41
+
42
+ /// Zero-alloc core: constant-time verify into `out` (the value without its
43
+ /// signature). Returns the value length, or `None` on invalid signature /
44
+ /// too-small buffer. Used by the C-ABI (`bun:ffi`) path to avoid the Vec.
45
+ pub fn verify_cookie_into(signed: &[u8], key: &hmac::Key, out: &mut [u8]) -> Option<usize> {
46
+ let dot = signed.iter().rposition(|&b| b == b'.')?;
47
+ let (value, sig) = signed.split_at(dot);
48
+ let sig = &sig[1..];
49
+ if sig.len() != 64 {
50
+ return None;
51
+ }
52
+ let sig_bytes = hex_decode_32(sig)?;
53
+ if hmac::verify(key, value, &sig_bytes).is_err() {
54
+ return None;
55
+ }
56
+ if out.len() < value.len() {
57
+ return None;
58
+ }
59
+ out[..value.len()].copy_from_slice(value);
60
+ Some(value.len())
61
+ }
62
+
63
+ /// Constant-time verify; returns the signed value without its signature.
64
+ pub fn verify_cookie_bytes(signed: &[u8], key: &hmac::Key) -> Option<Vec<u8>> {
65
+ let dot = signed.iter().rposition(|&b| b == b'.')?;
66
+ let (value, sig) = signed.split_at(dot);
67
+ let sig = &sig[1..];
68
+ if sig.len() != 64 {
69
+ return None;
70
+ }
71
+ let sig_bytes = hex_decode_32(sig)?;
72
+ hmac::verify(key, value, &sig_bytes)
73
+ .ok()
74
+ .map(|()| value.to_vec())
75
+ }
76
+
77
+ /// Constant-time verify; returns whether the signature matches, WITHOUT
78
+ /// materializing the signed value. Same checks as [`verify_cookie_bytes`] but
79
+ /// skips the `value.to_vec()` — used by the packed batch to avoid a discarded
80
+ /// allocation per item.
81
+ pub fn verify_cookie_bytes_bool(signed: &[u8], key: &hmac::Key) -> bool {
82
+ let Some(dot) = signed.iter().rposition(|&b| b == b'.') else {
83
+ return false;
84
+ };
85
+ let (value, sig) = signed.split_at(dot);
86
+ let sig = &sig[1..];
87
+ if sig.len() != 64 {
88
+ return false;
89
+ }
90
+ let Some(sig_bytes) = hex_decode_32(sig) else {
91
+ return false;
92
+ };
93
+ hmac::verify(key, value, &sig_bytes).is_ok()
94
+ }
95
+
96
+ #[napi]
97
+ pub fn sign_cookie(value: Uint8Array, secret: Uint8Array) -> Buffer {
98
+ let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_ref());
99
+ Buffer::from(sign_cookie_bytes(value.as_ref(), &key))
100
+ }
101
+
102
+ #[napi]
103
+ pub fn verify_cookie(signed: Uint8Array, secret: Uint8Array) -> Option<Buffer> {
104
+ let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_ref());
105
+ verify_cookie_bytes(signed.as_ref(), &key).map(Buffer::from)
106
+ }
107
+
108
+ /// Higher-order instance: HMAC key compiled once, reused across calls.
109
+ #[napi]
110
+ pub struct CookieSigner {
111
+ key: hmac::Key,
112
+ }
113
+
114
+ #[napi]
115
+ impl CookieSigner {
116
+ #[napi(constructor)]
117
+ pub fn new(secret: Uint8Array) -> Self {
118
+ Self {
119
+ key: hmac::Key::new(hmac::HMAC_SHA256, secret.as_ref()),
120
+ }
121
+ }
122
+
123
+ #[napi]
124
+ pub fn sign(&self, value: Uint8Array) -> Buffer {
125
+ Buffer::from(sign_cookie_bytes(value.as_ref(), &self.key))
126
+ }
127
+
128
+ #[napi]
129
+ pub fn verify(&self, signed: Uint8Array) -> Option<Buffer> {
130
+ verify_cookie_bytes(signed.as_ref(), &self.key).map(Buffer::from)
131
+ }
132
+ }
133
+
134
+ #[cfg(test)]
135
+ mod tests {
136
+ use super::*;
137
+
138
+ fn key(secret: &[u8]) -> hmac::Key {
139
+ hmac::Key::new(hmac::HMAC_SHA256, secret)
140
+ }
141
+
142
+ #[test]
143
+ fn sign_then_verify_roundtrip() {
144
+ let k = key(b"super-secret");
145
+ let value = b"session=abc123";
146
+ let signed = sign_cookie_bytes(value, &k);
147
+ assert!(signed.starts_with(value));
148
+ assert_eq!(signed[value.len()], b'.');
149
+ assert_eq!(verify_cookie_bytes(&signed, &k).unwrap(), value);
150
+ }
151
+
152
+ #[test]
153
+ fn verify_rejects_tampered() {
154
+ let k = key(b"super-secret");
155
+ let value = b"session=abc123";
156
+ let mut signed = sign_cookie_bytes(value, &k);
157
+ let last = signed.len() - 1;
158
+ signed[last] = if signed[last] == b'0' { b'1' } else { b'0' };
159
+ assert!(verify_cookie_bytes(&signed, &k).is_none());
160
+ }
161
+
162
+ #[test]
163
+ fn verify_rejects_wrong_secret_and_malformed() {
164
+ let signed = sign_cookie_bytes(b"x", &key(b"secret-a"));
165
+ assert!(verify_cookie_bytes(&signed, &key(b"secret-b")).is_none());
166
+ assert!(verify_cookie_bytes(b"no-dot", &key(b"s")).is_none());
167
+ assert!(verify_cookie_bytes(b"x.bad", &key(b"s")).is_none());
168
+ }
169
+ }