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,286 @@
1
+ // rust/crypto/jwt/api.rs — napi entry points for JWT signing/verification.
2
+ //
3
+ // Thin napi boundary over the pure token core in `token.rs`: scalar sign/
4
+ // verify, the precompiled-key `JwtSigner` instance, the C-ABI `_core` helpers
5
+ // consumed by `rust/ffi.rs`, and the packed sign/verify batches.
6
+
7
+ use napi::bindgen_prelude::*;
8
+ use napi_derive::napi;
9
+
10
+ use crate::crypto::base64::base64url_encode_bytes;
11
+
12
+ use super::{
13
+ build_token, build_token_with_key, inject_and_payload_b64, inject_and_payload_b64_sonic,
14
+ inject_ttl_claims, jwt_header_b64, sign_eddsa, verify_token, verify_token_eddsa,
15
+ verify_token_with_key,
16
+ };
17
+
18
+ /// Sign a JWT (HS256). `claims` is any JSON value; when it is an object and a
19
+ /// positive `ttlSeconds` is given, `iat`/`exp` are injected (unless present).
20
+ /// `nowSeconds` is the caller-supplied current epoch seconds (keeps the core
21
+ /// time-free and testable). Returns the compact token bytes.
22
+ #[napi]
23
+ pub fn jwt_sign(
24
+ claims: serde_json::Value,
25
+ secret: Uint8Array,
26
+ ttl_seconds: Option<i64>,
27
+ now_seconds: i64,
28
+ ) -> Result<Buffer> {
29
+ let mut claims = claims;
30
+ let payload_b64 = inject_and_payload_b64(&mut claims, ttl_seconds, now_seconds)?;
31
+ Ok(Buffer::from(build_token(
32
+ jwt_header_b64(),
33
+ &payload_b64,
34
+ secret.as_ref(),
35
+ )))
36
+ }
37
+
38
+ /// Sign a JWT (HS256) from pre-serialized claim JSON bytes. Avoids the napi
39
+ /// `serde_json::Value` DOM marshal of `jwt_sign` for callers that already hold
40
+ /// the claim bytes (e.g. JSON.stringify'd on the JS side). The claims are
41
+ /// parsed straight into a `sonic_rs::Value` (compact, no per-key heap
42
+ /// `String`). Semantics are identical to `jwt_sign` (incl. `iat`/`exp`
43
+ /// injection).
44
+ #[napi]
45
+ pub fn jwt_sign_bytes(
46
+ claims_json: Uint8Array,
47
+ secret: Uint8Array,
48
+ ttl_seconds: Option<i64>,
49
+ now_seconds: i64,
50
+ ) -> Result<Buffer> {
51
+ let mut claims: sonic_rs::Value =
52
+ sonic_rs::from_slice(claims_json.as_ref()).map_err(napi_err)?;
53
+ let payload_b64 = inject_and_payload_b64_sonic(&mut claims, ttl_seconds, now_seconds)?;
54
+ Ok(Buffer::from(build_token(
55
+ jwt_header_b64(),
56
+ &payload_b64,
57
+ secret.as_ref(),
58
+ )))
59
+ }
60
+
61
+ /// Verify a JWT (HS256 signature + time claims). Returns the decoded claims
62
+ /// object (marshaled from the compact sonic Value), or `null` on any failure
63
+ /// (malformed, bad signature, wrong `alg`, expired, not-yet-valid `nbf`/`iat`,
64
+ /// non-JSON payload).
65
+ #[napi]
66
+ pub fn jwt_verify(
67
+ env: Env,
68
+ token: Uint8Array,
69
+ secret: Uint8Array,
70
+ now_seconds: i64,
71
+ ) -> Result<Unknown<'static>> {
72
+ let Some(payload) = verify_token(token.as_ref(), secret.as_ref(), now_seconds) else {
73
+ return crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new());
74
+ };
75
+ match sonic_rs::from_slice::<sonic_rs::Value>(&payload) {
76
+ Ok(v) => crate::json::napi_marshal::sonic_value_to_js(&env, &v),
77
+ // The payload already parsed for the time-claim check — unreachable.
78
+ Err(_) => crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new()),
79
+ }
80
+ }
81
+
82
+ /// Sign an EdDSA (Ed25519) JWT from pre-serialized claim JSON bytes. Semantics
83
+ /// identical to `jwt_sign_bytes` (incl. `iat`/`exp` injection); the signature
84
+ /// is Ed25519 (RFC 8032) under the `EdDSA` alg.
85
+ #[napi]
86
+ pub fn jwt_sign_eddsa(
87
+ claims_json: Uint8Array,
88
+ private_key: Uint8Array,
89
+ ttl_seconds: Option<i64>,
90
+ now_seconds: i64,
91
+ ) -> Result<Buffer> {
92
+ match sign_eddsa(
93
+ claims_json.as_ref(),
94
+ private_key.as_ref(),
95
+ ttl_seconds,
96
+ now_seconds,
97
+ ) {
98
+ Some(token) => Ok(Buffer::from(token)),
99
+ None => Err(Error::from_reason(
100
+ "EdDSA sign failed (invalid claims or private key)",
101
+ )),
102
+ }
103
+ }
104
+
105
+ /// Verify an EdDSA (Ed25519) JWT (signature + time claims). Returns the decoded
106
+ /// claims object, or `null` on any failure.
107
+ #[napi]
108
+ pub fn jwt_verify_eddsa(
109
+ env: Env,
110
+ token: Uint8Array,
111
+ public_key: Uint8Array,
112
+ now_seconds: i64,
113
+ ) -> Result<Unknown<'static>> {
114
+ let Some(payload) = verify_token_eddsa(token.as_ref(), public_key.as_ref(), now_seconds) else {
115
+ return crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new());
116
+ };
117
+ match sonic_rs::from_slice::<sonic_rs::Value>(&payload) {
118
+ Ok(v) => crate::json::napi_marshal::sonic_value_to_js(&env, &v),
119
+ // The payload already parsed for the time-claim check — unreachable.
120
+ Err(_) => crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new()),
121
+ }
122
+ }
123
+
124
+ /// Higher-order instance: precompiles the HMAC-SHA256 key once from `secret`,
125
+ /// so `sign`/`verify` never re-derive the key on the per-call path. `ttl` is
126
+ /// fixed at construction (0/absent → no `iat`/`exp` injection).
127
+ #[napi]
128
+ pub struct JwtSigner {
129
+ pub(crate) key: aws_lc_rs::hmac::Key,
130
+ ttl_seconds: Option<i64>,
131
+ }
132
+
133
+ #[napi]
134
+ impl JwtSigner {
135
+ #[napi(constructor)]
136
+ pub fn new(secret: Uint8Array, ttl_seconds: Option<i64>) -> Self {
137
+ Self {
138
+ key: aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret.as_ref()),
139
+ ttl_seconds,
140
+ }
141
+ }
142
+
143
+ /// Opaque handle to the precompiled key + ttl, for the `bun:ffi` C-ABI
144
+ /// fast path (`castrum_jwt_signer_*` in rust/ffi.rs). Only valid while THIS
145
+ /// instance is alive; the JS wrapper holds the instance.
146
+ #[napi]
147
+ pub fn inner_ptr(&self) -> u64 {
148
+ self as *const JwtSigner as u64
149
+ }
150
+
151
+ /// Sign a JWT with the precompiled key. When `ttl` was set at construction,
152
+ /// `iat`/`exp` are injected into object claims (unless already present).
153
+ #[napi]
154
+ pub fn sign(&self, claims: serde_json::Value, now_seconds: i64) -> Result<Buffer> {
155
+ let mut claims = claims;
156
+ let payload_b64 = inject_and_payload_b64(&mut claims, self.ttl_seconds, now_seconds)?;
157
+ Ok(Buffer::from(build_token_with_key(
158
+ jwt_header_b64(),
159
+ &payload_b64,
160
+ &self.key,
161
+ )))
162
+ }
163
+
164
+ /// Sign a JWT from pre-serialized claim JSON bytes with the precompiled
165
+ /// key — no napi `serde_json::Value` marshal, no per-call key derivation.
166
+ #[napi]
167
+ pub fn sign_bytes(&self, claims_json: Uint8Array, now_seconds: i64) -> Result<Buffer> {
168
+ let mut claims: sonic_rs::Value =
169
+ sonic_rs::from_slice(claims_json.as_ref()).map_err(napi_err)?;
170
+ let payload_b64 = inject_and_payload_b64_sonic(&mut claims, self.ttl_seconds, now_seconds)?;
171
+ Ok(Buffer::from(build_token_with_key(
172
+ jwt_header_b64(),
173
+ &payload_b64,
174
+ &self.key,
175
+ )))
176
+ }
177
+
178
+ /// Verify a JWT with the precompiled key (signature + `alg` + time claims).
179
+ /// Returns the decoded claims object, or `null` on any failure.
180
+ #[napi]
181
+ pub fn verify(
182
+ &self,
183
+ env: Env,
184
+ token: Uint8Array,
185
+ now_seconds: i64,
186
+ ) -> Result<Unknown<'static>> {
187
+ let Some(payload) = verify_token_with_key(token.as_ref(), &self.key, now_seconds) else {
188
+ return crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new());
189
+ };
190
+ match sonic_rs::from_slice::<sonic_rs::Value>(&payload) {
191
+ Ok(v) => crate::json::napi_marshal::sonic_value_to_js(&env, &v),
192
+ // The payload already parsed for the time-claim check — unreachable.
193
+ Err(_) => crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new()),
194
+ }
195
+ }
196
+ }
197
+
198
+ /// C-ABI support: sign pre-serialized claim JSON with the precompiled key.
199
+ /// `Err(())` on invalid claims JSON (napi parity: throws).
200
+ ///
201
+ /// # Safety
202
+ /// `p` must be a valid `*const JwtSigner` from `inner_ptr`, alive for the call.
203
+ pub(crate) unsafe fn jwt_signer_sign_bytes_core(
204
+ p: *const JwtSigner,
205
+ claims_json: &[u8],
206
+ now_seconds: i64,
207
+ ) -> std::result::Result<Vec<u8>, ()> {
208
+ let this = &*p;
209
+ let mut claims: sonic_rs::Value = sonic_rs::from_slice(claims_json).map_err(|_| ())?;
210
+ let payload_b64 =
211
+ inject_and_payload_b64_sonic(&mut claims, this.ttl_seconds, now_seconds).map_err(|_| ())?;
212
+ Ok(build_token_with_key(
213
+ jwt_header_b64(),
214
+ &payload_b64,
215
+ &this.key,
216
+ ))
217
+ }
218
+
219
+ /// C-ABI support: verify a JWT with the precompiled key → the decoded payload
220
+ /// BYTES (valid JSON claims), or None on invalid signature / expired /
221
+ /// malformed. No `serde_json::Value` DOM, no re-serialize.
222
+ ///
223
+ /// # Safety
224
+ /// `p` must be a valid `*const JwtSigner` from `inner_ptr`, alive for the call.
225
+ pub(crate) unsafe fn jwt_signer_verify_core(
226
+ p: *const JwtSigner,
227
+ token: &[u8],
228
+ now_seconds: i64,
229
+ ) -> Option<Vec<u8>> {
230
+ let this = &*p;
231
+ verify_token_with_key(token, &this.key, now_seconds)
232
+ }
233
+
234
+ fn napi_err(e: impl std::fmt::Display) -> Error {
235
+ Error::from_reason(e.to_string())
236
+ }
237
+
238
+ /// Parallel sign batch: packed `[u32 count]{[u32 len][claims-json]}` in →
239
+ /// packed `[u32 count]{[u32 len][token]}` out (same secret/ttl/now for all).
240
+ #[napi]
241
+ pub fn jwt_sign_batch_packed(
242
+ data: Uint8Array,
243
+ secret: Uint8Array,
244
+ ttl_seconds: Option<i64>,
245
+ now_seconds: i64,
246
+ ) -> Result<Buffer> {
247
+ // Derive the HMAC key ONCE for the whole batch (build_token derives a
248
+ // fresh key per call), then route through the shared packed batch helper
249
+ // (zero-alloc serial PackedIter path; rayon direct-write when justified).
250
+ let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret.as_ref());
251
+
252
+ let sign_one = |claims_json: &[u8]| -> Vec<u8> {
253
+ let Ok(mut claims) = sonic_rs::from_slice::<sonic_rs::Value>(claims_json) else {
254
+ return Vec::new();
255
+ };
256
+ inject_ttl_claims(&mut claims, ttl_seconds, now_seconds);
257
+ let header_b64 = jwt_header_b64();
258
+ let Ok(payload_bytes) = sonic_rs::to_vec(&claims) else {
259
+ return Vec::new();
260
+ };
261
+ let payload_b64 = base64url_encode_bytes(&payload_bytes);
262
+ build_token_with_key(header_b64, &payload_b64, &key)
263
+ };
264
+
265
+ crate::util::run_packed_batch(data.as_ref(), sign_one).map(Buffer::from)
266
+ }
267
+
268
+ /// Parallel verify batch: packed `[u32 count]{[u32 len][token]}` in → bitset
269
+ /// (`[u32 count][ceil(n/8) bytes]`) of valid tokens.
270
+ #[napi]
271
+ pub fn jwt_verify_batch_packed(
272
+ data: Uint8Array,
273
+ secret: Uint8Array,
274
+ now_seconds: i64,
275
+ ) -> Result<Buffer> {
276
+ // Derive the HMAC key ONCE for the whole batch (verify_token derives a
277
+ // fresh key per call), then route through the shared bitset batch helper
278
+ // (zero-alloc serial PackedIter path; rayon direct-write when justified).
279
+ let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret.as_ref());
280
+ crate::util::run_bitset_batch(
281
+ data.as_ref(),
282
+ |token| verify_token_with_key(token, &key, now_seconds).is_some(),
283
+ 4096,
284
+ )
285
+ .map(Buffer::from)
286
+ }
@@ -0,0 +1,22 @@
1
+ //! HS256 / EdDSA JSON Web Tokens.
2
+ //!
3
+ //! Split into a small folder so the token machinery is navigable:
4
+ //! - `token.rs` — pure token core (headers, HMAC, split/build/verify, zero-DOM
5
+ //! time-claim checks, `iat`/`exp` injection). Reused by the napi boundary,
6
+ //! the C-ABI fast paths in `rust/ffi.rs`, and `crate::crypto::ed25519`.
7
+ //! - `api.rs` — napi entry points (scalar sign/verify, precompiled-key
8
+ //! `JwtSigner` instance, C-ABI `_core` helpers, packed sign/verify batches).
9
+ //! - `tests.rs` — unit tests.
10
+ //!
11
+ //! All items are re-exported at this module root (`pub use self::token::*` +
12
+ //! `pub use self::api::*`) so existing `crate::crypto::jwt::*` call sites keep
13
+ //! working unchanged.
14
+
15
+ mod api;
16
+ mod token;
17
+
18
+ #[cfg(test)]
19
+ mod tests;
20
+
21
+ pub use self::api::*;
22
+ pub use self::token::*;
@@ -0,0 +1,312 @@
1
+ // rust/crypto/jwt/tests.rs — unit tests for the JWT core + napi entry points.
2
+ //
3
+ // Exercises the token core in `token.rs` and the napi boundary in `napi.rs`.
4
+ // The old local base64url helpers were consolidated into
5
+ // `crate::crypto::base64`; the tests import them under their former `b64url_*`
6
+ // names so the assertions read the same.
7
+
8
+ use super::*;
9
+ use crate::crypto::base64::{
10
+ base64url_decode_bytes as b64url_decode, base64url_encode_bytes as b64url_encode,
11
+ };
12
+ use napi::bindgen_prelude::*;
13
+ use sonic_rs::JsonValueTrait;
14
+
15
+ const SECRET: &[u8] = b"test-secret-key";
16
+
17
+ fn token(claims: &serde_json::Value, secret: &[u8]) -> Vec<u8> {
18
+ token_with_header(
19
+ &serde_json::json!({ "alg": "HS256", "typ": "JWT" }),
20
+ claims,
21
+ secret,
22
+ )
23
+ }
24
+
25
+ fn token_with_header(
26
+ header: &serde_json::Value,
27
+ claims: &serde_json::Value,
28
+ secret: &[u8],
29
+ ) -> Vec<u8> {
30
+ let header_b64 = b64url_encode(&serde_json::to_vec(header).unwrap());
31
+ let payload_b64 = b64url_encode(&serde_json::to_vec(claims).unwrap());
32
+ build_token(&header_b64, &payload_b64, secret)
33
+ }
34
+
35
+ #[test]
36
+ fn verify_token_rejects_wrong_alg() {
37
+ // Regression: `alg` must be allowlisted. A token with a VALID HS256
38
+ // signature but header `alg: "none"` (or any non-HS256) must be
39
+ // rejected — otherwise alg-confusion attacks become possible later.
40
+ let claims = serde_json::json!({ "sub": "1" });
41
+ let none = token_with_header(
42
+ &serde_json::json!({ "alg": "none", "typ": "JWT" }),
43
+ &claims,
44
+ SECRET,
45
+ );
46
+ assert!(verify_token(&none, SECRET, 1_000_000).is_none());
47
+
48
+ let rs256 = token_with_header(
49
+ &serde_json::json!({ "alg": "RS256", "typ": "JWT" }),
50
+ &claims,
51
+ SECRET,
52
+ );
53
+ assert!(verify_token(&rs256, SECRET, 1_000_000).is_none());
54
+ }
55
+
56
+ #[test]
57
+ fn verify_token_enforces_nbf() {
58
+ let claims = serde_json::json!({ "sub": "1", "nbf": 2_000_000 });
59
+ let t = token(&claims, SECRET);
60
+ // Before nbf -> rejected; at/after nbf -> accepted.
61
+ assert!(verify_token(&t, SECRET, 1_999_999).is_none());
62
+ assert!(verify_token(&t, SECRET, 2_000_000).is_some());
63
+ }
64
+
65
+ #[test]
66
+ fn verify_token_enforces_iat_leeway() {
67
+ let now = 1_000_000;
68
+ // iat in the far future -> rejected (beyond 60s skew leeway).
69
+ let far = token(
70
+ &serde_json::json!({ "sub": "1", "iat": now + 10_000 }),
71
+ SECRET,
72
+ );
73
+ assert!(verify_token(&far, SECRET, now).is_none());
74
+ // iat within leeway -> accepted.
75
+ let near = token(&serde_json::json!({ "sub": "1", "iat": now + 30 }), SECRET);
76
+ assert!(verify_token(&near, SECRET, now).is_some());
77
+ }
78
+
79
+ #[test]
80
+ fn verify_token_accepts_valid_claims() {
81
+ let claims = serde_json::json!({ "sub": "123", "role": "admin" });
82
+ let t = token(&claims, SECRET);
83
+ let payload = verify_token(&t, SECRET, 1_000_000).expect("valid token verifies");
84
+ let v: sonic_rs::Value = sonic_rs::from_slice(&payload).unwrap();
85
+ assert_eq!(v["sub"], "123");
86
+ }
87
+
88
+ #[test]
89
+ fn sign_then_verify_roundtrip() {
90
+ let claims = serde_json::json!({ "sub": "123", "name": "John", "role": "admin" });
91
+ let t = token(&claims, SECRET);
92
+ assert!(verify_signature(&t, SECRET));
93
+ }
94
+
95
+ #[test]
96
+ fn verify_rejects_wrong_secret() {
97
+ let claims = serde_json::json!({ "sub": "1" });
98
+ let t = token(&claims, SECRET);
99
+ assert!(!verify_signature(&t, b"wrong-secret"));
100
+ }
101
+
102
+ #[test]
103
+ fn verify_rejects_tampered_payload() {
104
+ let claims = serde_json::json!({ "sub": "1", "role": "user" });
105
+ let t = token(&claims, SECRET);
106
+ let first_dot = memchr::memchr(b'.', &t).unwrap();
107
+ let second_rel = memchr::memchr(b'.', &t[first_dot + 1..]).unwrap();
108
+ let payload_start = first_dot + 1;
109
+ let payload_end = payload_start + second_rel;
110
+ let mid = (payload_start + payload_end) / 2;
111
+
112
+ let mut tampered = t.clone();
113
+ tampered[mid] ^= 0x01;
114
+ assert!(!verify_signature(&tampered, SECRET));
115
+ }
116
+
117
+ #[test]
118
+ fn verify_rejects_malformed() {
119
+ assert!(!verify_signature(b"no-dots-here", SECRET));
120
+ assert!(!verify_signature(b"a.b.c.d", SECRET));
121
+ assert!(!verify_signature(b".", SECRET));
122
+ assert!(!verify_signature(b"", SECRET));
123
+ }
124
+
125
+ #[test]
126
+ fn base64url_roundtrip() {
127
+ let data = b"hello world \x00\xff binary";
128
+ let enc = b64url_encode(data);
129
+ assert_eq!(b64url_decode(&enc).unwrap(), data);
130
+ assert!(!enc.contains(&b'='));
131
+ }
132
+
133
+ #[test]
134
+ fn base64url_uses_urlsafe_alphabet() {
135
+ // Bytes 0xfb 0xff encode to base64url "-_8" (no +/ or /).
136
+ assert_eq!(b64url_encode(b"\xfb\xff"), b"-_8");
137
+ }
138
+
139
+ #[test]
140
+ fn split_handles_three_segments() {
141
+ let t = b"aaa.bbb.ccc";
142
+ let parts = split_token(t).unwrap();
143
+ assert_eq!(parts.header_b64, b"aaa");
144
+ assert_eq!(parts.payload_b64, b"bbb");
145
+ assert_eq!(parts.sig_b64, b"ccc");
146
+ }
147
+
148
+ #[test]
149
+ fn jwt_signer_instance_roundtrips_and_injects_claims() {
150
+ // Precompiled-key instance: sign+verify with no per-call key derivation.
151
+ let signer = JwtSigner::new(Uint8Array::new(SECRET.to_vec()), Some(3600));
152
+ let now = 1_000_000i64;
153
+
154
+ let claims = serde_json::json!({ "sub": "123" });
155
+ let token = signer.sign(claims.clone(), now).unwrap();
156
+
157
+ // ttl injected iat/exp at construction time.
158
+ let verified_payload = verify_token_with_key(&token, &signer.key, now).expect("token verifies");
159
+ let verified: sonic_rs::Value = sonic_rs::from_slice(&verified_payload).unwrap();
160
+ assert_eq!(verified["sub"], "123");
161
+ assert_eq!(verified["iat"], now);
162
+ assert_eq!(verified["exp"], now + 3600);
163
+
164
+ // Same instance verifies, wrong key instance rejects.
165
+ let other = JwtSigner::new(Uint8Array::new(b"other-secret".to_vec()), None);
166
+ assert!(verify_token_with_key(&token, &other.key, now).is_none());
167
+
168
+ // No ttl -> no iat/exp injected.
169
+ let no_ttl = JwtSigner::new(Uint8Array::new(SECRET.to_vec()), None);
170
+ let t2 = no_ttl.sign(claims.clone(), now).unwrap();
171
+ let v2_payload = verify_token_with_key(&t2, &no_ttl.key, now).unwrap();
172
+ let v2: sonic_rs::Value = sonic_rs::from_slice(&v2_payload).unwrap();
173
+ assert!(v2.get("iat").is_none());
174
+ assert!(v2.get("exp").is_none());
175
+ }
176
+
177
+ #[test]
178
+ fn jwt_sign_bytes_matches_value_sign_and_verifies() {
179
+ let claims_json = br#"{"sub":"123","name":"John"}"#.to_vec();
180
+ let secret = Uint8Array::new(SECRET.to_vec());
181
+
182
+ // Byte-JSON sign (no napi Value marshal) matches the Value sign
183
+ // semantically. Key ORDER differs between the two serializers — sonic
184
+ // (`sort_keys` → BTreeMap) emits sorted keys, serde_json
185
+ // (`preserve_order` → IndexMap) emits insertion order — so compare the
186
+ // decoded claims, not the raw token bytes.
187
+ let from_bytes = jwt_sign_bytes(
188
+ Uint8Array::new(claims_json.clone()),
189
+ secret,
190
+ None,
191
+ 1_000_000,
192
+ )
193
+ .unwrap();
194
+ let claims: serde_json::Value = serde_json::from_slice(&claims_json).unwrap();
195
+ let from_value = jwt_sign(claims, Uint8Array::new(SECRET.to_vec()), None, 1_000_000).unwrap();
196
+ let claims_from_bytes: sonic_rs::Value = sonic_rs::from_slice(
197
+ &b64url_decode(split_token(&from_bytes).unwrap().payload_b64).unwrap(),
198
+ )
199
+ .unwrap();
200
+ let claims_from_value: sonic_rs::Value = sonic_rs::from_slice(
201
+ &b64url_decode(split_token(&from_value).unwrap().payload_b64).unwrap(),
202
+ )
203
+ .unwrap();
204
+ assert_eq!(claims_from_bytes, claims_from_value);
205
+
206
+ // The bytes-signed token verifies and carries the claims.
207
+ let verified_payload = verify_token(from_bytes.as_ref(), SECRET, 1_000_000).unwrap();
208
+ let verified: sonic_rs::Value = sonic_rs::from_slice(&verified_payload).unwrap();
209
+ assert_eq!(verified["sub"], "123");
210
+ assert_eq!(verified["name"], "John");
211
+ }
212
+
213
+ #[test]
214
+ fn jwt_sign_bytes_injects_ttl_and_instance_sign_bytes_works() {
215
+ let claims_json = br#"{"sub":"123"}"#.to_vec();
216
+ let token = jwt_sign_bytes(
217
+ Uint8Array::new(claims_json.clone()),
218
+ Uint8Array::new(SECRET.to_vec()),
219
+ Some(3600),
220
+ 1_000_000,
221
+ )
222
+ .unwrap();
223
+ let payload = verify_token(&token, SECRET, 1_000_000).unwrap();
224
+ let v: sonic_rs::Value = sonic_rs::from_slice(&payload).unwrap();
225
+ assert_eq!(v["iat"], 1_000_000);
226
+ assert_eq!(v["exp"], 1_000_000 + 3600);
227
+
228
+ // Precompiled-key instance: sign_bytes injects its construction ttl.
229
+ let signer = JwtSigner::new(Uint8Array::new(SECRET.to_vec()), Some(3600));
230
+ let t2 = signer
231
+ .sign_bytes(Uint8Array::new(claims_json), 2_000_000)
232
+ .unwrap();
233
+ let v2_payload = verify_token_with_key(&t2, &signer.key, 2_000_000).unwrap();
234
+ let v2: sonic_rs::Value = sonic_rs::from_slice(&v2_payload).unwrap();
235
+ assert_eq!(v2["iat"], 2_000_000);
236
+ assert_eq!(v2["exp"], 2_000_000 + 3600);
237
+ }
238
+
239
+ #[test]
240
+ fn eddsa_sign_verify_round_trip() {
241
+ let (private_der, public_der) = crate::crypto::ed25519::generate_keypair().unwrap();
242
+ let claims_json = br#"{"sub":"user-1","roles":["admin"]}"#.to_vec();
243
+
244
+ let token = sign_eddsa(&claims_json, &private_der, None, 1_000_000).unwrap();
245
+ // Compact token: exactly two dots, EdDSA header.
246
+ let parts = split_token(&token).unwrap();
247
+ assert_eq!(
248
+ b64url_decode(parts.header_b64).unwrap(),
249
+ br#"{"alg":"EdDSA","typ":"JWT"}"#
250
+ );
251
+
252
+ // Verifies with the matching public key.
253
+ let payload = verify_token_eddsa(&token, &public_der, 1_000_000).unwrap();
254
+ let v: sonic_rs::Value = sonic_rs::from_slice(&payload).unwrap();
255
+ assert_eq!(v["sub"], "user-1");
256
+ assert_eq!(v["roles"][0], "admin");
257
+
258
+ // Wrong public key (a different keypair) fails.
259
+ let (_, other_pub) = crate::crypto::ed25519::generate_keypair().unwrap();
260
+ assert!(verify_token_eddsa(&token, &other_pub, 1_000_000).is_none());
261
+
262
+ // Tampered payload fails (signature over header.payload is invalid).
263
+ let mut bad = token.clone();
264
+ let n = bad.len();
265
+ bad[n - 2] ^= 0x01;
266
+ assert!(verify_token_eddsa(&bad, &public_der, 1_000_000).is_none());
267
+ }
268
+
269
+ #[test]
270
+ fn eddsa_injects_ttl_and_enforces_time_claims() {
271
+ let (private_der, public_der) = crate::crypto::ed25519::generate_keypair().unwrap();
272
+ let claims_json = br#"{"sub":"user-1"}"#.to_vec();
273
+
274
+ let token = sign_eddsa(&claims_json, &private_der, Some(3600), 1_000_000).unwrap();
275
+ let v: sonic_rs::Value =
276
+ sonic_rs::from_slice(&verify_token_eddsa(&token, &public_der, 1_000_000).unwrap()).unwrap();
277
+ assert_eq!(v["iat"], 1_000_000);
278
+ assert_eq!(v["exp"], 1_000_000 + 3600);
279
+
280
+ // Expired token rejected.
281
+ assert!(verify_token_eddsa(&token, &public_der, 1_000_000 + 3600).is_none());
282
+ // Not-yet-valid (future iat beyond leeway) rejected.
283
+ let future = sign_eddsa(&claims_json, &private_der, Some(3600), 1_000_000).unwrap();
284
+ assert!(verify_token_eddsa(&future, &public_der, 1_000_000 - 61).is_none());
285
+ assert!(verify_token_eddsa(&future, &public_der, 1_000_000).is_some());
286
+ }
287
+
288
+ #[test]
289
+ fn eddsa_rejects_wrong_alg_and_malformed() {
290
+ let (private_der, public_der) = crate::crypto::ed25519::generate_keypair().unwrap();
291
+ let claims_json = br#"{"sub":"x"}"#.to_vec();
292
+ let token = sign_eddsa(&claims_json, &private_der, None, 1_000_000).unwrap();
293
+
294
+ // Swap the header alg to HS256 while keeping a valid Ed25519 signature
295
+ // over the new header.payload — the alg allowlist must reject it.
296
+ let header_b64 = b64url_encode(br#"{"alg":"HS256","typ":"JWT"}"#);
297
+ let parts = split_token(&token).unwrap();
298
+ let mut forged_input = Vec::new();
299
+ forged_input.extend_from_slice(&header_b64);
300
+ forged_input.push(b'.');
301
+ forged_input.extend_from_slice(parts.payload_b64);
302
+ let sig = crate::crypto::ed25519::sign(&private_der, &forged_input).unwrap();
303
+ let mut forged = forged_input;
304
+ forged.push(b'.');
305
+ forged.extend_from_slice(&b64url_encode(&sig));
306
+ assert!(verify_token_eddsa(&forged, &public_der, 1_000_000).is_none());
307
+
308
+ // Malformed tokens.
309
+ assert!(verify_token_eddsa(b"not-a-token", &public_der, 1_000_000).is_none());
310
+ assert!(verify_token_eddsa(b"a.b.c.d", &public_der, 1_000_000).is_none());
311
+ assert!(verify_token_eddsa(&token, &[], 1_000_000).is_none());
312
+ }