castrum 0.9.2 → 0.9.4

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 (184) hide show
  1. package/Cargo.lock +2259 -0
  2. package/Cargo.toml +138 -0
  3. package/README.md +6 -0
  4. package/build.rs +5 -0
  5. package/castrum.linux-x64-gnu.node +0 -0
  6. package/dist/index.d.ts +1348 -1295
  7. package/dist/index.js +4307 -3776
  8. package/docs/API.md +1 -1
  9. package/docs/ARCHITECTURE.md +3 -2
  10. package/docs/CASE_STUDY.md +2 -2
  11. package/docs/ENVIRONMENT.md +16 -0
  12. package/docs/FFI_BUN_GUIDE.md +89 -6
  13. package/docs/REPO_MAP.md +2 -2
  14. package/index.ts +19 -25
  15. package/package.json +9 -1
  16. package/rust/crypto/aead.rs +378 -0
  17. package/rust/crypto/argon2.rs +265 -0
  18. package/rust/crypto/base64.rs +420 -0
  19. package/rust/crypto/bcrypt.rs +89 -0
  20. package/rust/crypto/cookie_sign.rs +169 -0
  21. package/rust/crypto/csrf.rs +106 -0
  22. package/rust/crypto/ed25519.rs +215 -0
  23. package/rust/crypto/hashing.rs +140 -0
  24. package/rust/crypto/hmac_sha256.rs +88 -0
  25. package/rust/crypto/jwt/api.rs +286 -0
  26. package/rust/crypto/jwt/mod.rs +22 -0
  27. package/rust/crypto/jwt/tests.rs +312 -0
  28. package/rust/crypto/jwt/token.rs +360 -0
  29. package/rust/crypto/mod.rs +27 -0
  30. package/rust/crypto/pbkdf2.rs +96 -0
  31. package/rust/crypto/random_token.rs +85 -0
  32. package/rust/crypto/session.rs +222 -0
  33. package/rust/ffi/crypto.rs +708 -0
  34. package/rust/ffi/hashing.rs +285 -0
  35. package/rust/ffi/http.rs +752 -0
  36. package/rust/ffi/ingress.rs +303 -0
  37. package/rust/ffi/json.rs +517 -0
  38. package/rust/ffi/jwt.rs +419 -0
  39. package/rust/ffi/metrics.rs +373 -0
  40. package/rust/ffi/mod.rs +68 -0
  41. package/rust/ffi/payload.rs +275 -0
  42. package/rust/ffi/probe.rs +60 -0
  43. package/rust/ffi/rate_limit.rs +77 -0
  44. package/rust/ffi/route.rs +85 -0
  45. package/rust/ffi/tests.rs +2333 -0
  46. package/rust/ffi/util.rs +152 -0
  47. package/rust/ffi/validators.rs +123 -0
  48. package/rust/http/accept.rs +606 -0
  49. package/rust/http/cookie_parser.rs +140 -0
  50. package/rust/http/etag.rs +289 -0
  51. package/rust/http/form.rs +132 -0
  52. package/rust/http/headers.rs +267 -0
  53. package/rust/http/http_date.rs +286 -0
  54. package/rust/http/http_parser.rs +252 -0
  55. package/rust/http/media_type.rs +355 -0
  56. package/rust/http/method.rs +200 -0
  57. package/rust/http/mime_lookup.rs +130 -0
  58. package/rust/http/mod.rs +32 -0
  59. package/rust/http/multipart.rs +685 -0
  60. package/rust/http/query_parser.rs +180 -0
  61. package/rust/http/url_codec.rs +296 -0
  62. package/rust/http/url_join.rs +324 -0
  63. package/rust/ingress/api.rs +355 -0
  64. package/rust/ingress/cors.rs +403 -0
  65. package/rust/ingress/ingress_constants.rs +209 -0
  66. package/rust/ingress/ip_trust.rs +312 -0
  67. package/rust/ingress/mod.rs +48 -0
  68. package/rust/ingress/native_route.rs +779 -0
  69. package/rust/ingress/options.rs +176 -0
  70. package/rust/ingress/output.rs +406 -0
  71. package/rust/ingress/packed.rs +191 -0
  72. package/rust/ingress/pipeline.rs +534 -0
  73. package/rust/ingress/proxy.rs +138 -0
  74. package/rust/ingress/rate_limit.rs +538 -0
  75. package/rust/ingress/terminal.rs +114 -0
  76. package/rust/ingress/tests.rs +566 -0
  77. package/rust/ingress/time.rs +70 -0
  78. package/rust/json/fast_schema/capture.rs +336 -0
  79. package/rust/json/fast_schema/compile.rs +556 -0
  80. package/rust/json/fast_schema/cursor.rs +368 -0
  81. package/rust/json/fast_schema/email.rs +282 -0
  82. package/rust/json/fast_schema/errors.rs +247 -0
  83. package/rust/json/fast_schema/mod.rs +57 -0
  84. package/rust/json/fast_schema/tests.rs +1143 -0
  85. package/rust/json/fast_schema/types.rs +140 -0
  86. package/rust/json/fast_schema/validate.rs +931 -0
  87. package/rust/json/json_ops.rs +121 -0
  88. package/rust/json/json_schema.rs +837 -0
  89. package/rust/json/json_ser.rs +881 -0
  90. package/rust/json/mod.rs +19 -0
  91. package/rust/json/napi_marshal.rs +318 -0
  92. package/rust/json/patch/api.rs +60 -0
  93. package/rust/json/patch/engine.rs +153 -0
  94. package/rust/json/patch/mod.rs +47 -0
  95. package/rust/json/patch/ops.rs +270 -0
  96. package/rust/json/patch/pointer.rs +49 -0
  97. package/rust/json/patch/tests.rs +499 -0
  98. package/rust/lib.rs +72 -0
  99. package/rust/metrics/api.rs +148 -0
  100. package/rust/metrics/mod.rs +21 -0
  101. package/rust/metrics/registry.rs +961 -0
  102. package/rust/panic_safety.rs +40 -0
  103. package/rust/payload/compress.rs +512 -0
  104. package/rust/payload/mod.rs +13 -0
  105. package/rust/payload/sse.rs +214 -0
  106. package/rust/payload/template.rs +253 -0
  107. package/rust/payload/websocket.rs +69 -0
  108. package/rust/payload/ws_frames.rs +354 -0
  109. package/rust/proptest_suite.rs +139 -0
  110. package/rust/selection.rs +95 -0
  111. package/rust/src/bin/native-bench.rs +495 -0
  112. package/rust/test_support.rs +71 -0
  113. package/rust/util/batch/api.rs +266 -0
  114. package/rust/util/batch/core.rs +473 -0
  115. package/rust/util/batch/mod.rs +23 -0
  116. package/rust/util/batch/tests.rs +445 -0
  117. package/rust/util/batch_core.rs +184 -0
  118. package/rust/util/bytes.rs +407 -0
  119. package/rust/util/mod.rs +34 -0
  120. package/rust/util/packed.rs +473 -0
  121. package/rust/util/text.rs +130 -0
  122. package/rust/util/threadpool.rs +128 -0
  123. package/rust/util/validation.rs +419 -0
  124. package/rust-toolchain.toml +17 -0
  125. package/scripts/postinstall.mjs +199 -0
  126. package/src/ingress/constants.ts +1 -1
  127. package/src/ingress/context.ts +2 -2
  128. package/src/ingress/decode/baked-result.ts +13 -13
  129. package/src/ingress/decode/fast-result.ts +17 -17
  130. package/src/ingress/errors.ts +7 -7
  131. package/src/ingress/headers/baked-templates.ts +1 -1
  132. package/src/ingress/headers/fast-templates.ts +3 -3
  133. package/src/ingress/index.ts +1 -1
  134. package/src/ingress/native-route.ts +9 -5
  135. package/src/ingress/options.ts +1 -1
  136. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  137. package/src/ingress/packing/route-wire.ts +1 -5
  138. package/src/ingress/response/error-bodies.ts +5 -5
  139. package/src/ingress/response/terminal.ts +1 -1
  140. package/src/ingress/routes/echo.ts +4 -4
  141. package/src/ingress/routes/fallback.ts +2 -2
  142. package/src/ingress/routes/head.ts +2 -2
  143. package/src/ingress/routes/native.ts +2 -1
  144. package/src/ingress/routes/options.ts +1 -1
  145. package/src/ingress/routes/read.ts +2 -2
  146. package/src/ingress/server.ts +10 -6
  147. package/src/ingress/status.ts +7 -7
  148. package/src/integration/batch.ts +1 -1
  149. package/src/integration/index.ts +9 -9
  150. package/src/integration/pipeline.ts +6 -6
  151. package/src/integration/websocket.ts +2 -2
  152. package/src/loader/index.ts +1 -5
  153. package/src/loader/ops.ts +1 -1
  154. package/src/native/ffi/build/codecs.ts +104 -8
  155. package/src/native/ffi/build/compress.ts +14 -10
  156. package/src/native/ffi/build/instances.ts +40 -3
  157. package/src/native/ffi/build/metrics.ts +224 -0
  158. package/src/native/ffi/build/parse.ts +59 -23
  159. package/src/native/ffi/build/util.ts +14 -1
  160. package/src/native/ffi/build.ts +2 -0
  161. package/src/native/ffi/selftest.ts +10 -3
  162. package/src/native/ffi/types.ts +111 -9
  163. package/src/native/ffi.ts +87 -1
  164. package/src/native/index.ts +1 -1
  165. package/src/native/loader.ts +2 -2
  166. package/src/native/types/instances.ts +34 -0
  167. package/src/native/types.ts +11 -0
  168. package/src/runtime/codec.ts +53 -9
  169. package/src/runtime/types.ts +7 -0
  170. package/src/rust-ffi/batch/build.ts +16 -9
  171. package/src/rust-ffi/index.ts +7 -6
  172. package/src/rust-ffi/options.ts +1 -3
  173. package/src/rust-ffi/proven.ts +3 -3
  174. package/src/rust-ffi/scalar/crypto.ts +1 -1
  175. package/src/rust-ffi/scalar/factories.ts +73 -0
  176. package/src/rust-ffi/scalar/hashing.ts +1 -1
  177. package/src/rust-ffi/scalar/http.ts +17 -11
  178. package/src/rust-ffi/scalar/index.ts +2 -0
  179. package/src/rust-ffi/scalar/interface.ts +20 -0
  180. package/src/rust-ffi/scalar/payload.ts +11 -3
  181. package/src/rust-ffi/scalar/validation.ts +121 -0
  182. package/src/selection.ts +1 -1
  183. package/src/shared/bytes.ts +5 -4
  184. package/src/shared/codec.ts +18 -3
@@ -0,0 +1,140 @@
1
+ // rust/crypto/hashing.rs — FNV-1a / XXH3 checksums.
2
+
3
+ use napi::bindgen_prelude::*;
4
+ use napi_derive::napi;
5
+
6
+ use xxhash_rust::xxh3::xxh3_64;
7
+
8
+ pub const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
9
+ pub const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
10
+
11
+ // ── FNV-1a (byte-by-byte, correct per spec) ──────────────────────
12
+
13
+ /// FNV-1a in the "continue" style. FNV-1a is inherently byte-serial,
14
+ /// so we process each byte individually (this is the correct algorithm).
15
+ #[inline]
16
+ pub fn fnv1a64_continue(mut hash: u64, input: &[u8]) -> u64 {
17
+ for &b in input {
18
+ hash ^= u64::from(b);
19
+ hash = hash.wrapping_mul(FNV_PRIME);
20
+ }
21
+ hash
22
+ }
23
+
24
+ /// FNV-1a on a full byte slice, with fast path for small inputs.
25
+ #[inline]
26
+ pub fn fnv1a64_bytes(input: &[u8]) -> u64 {
27
+ fnv1a64_continue(FNV_OFFSET_BASIS, input)
28
+ }
29
+
30
+ // ── XXHash wrappers ────────────────────────────────────────────────
31
+
32
+ #[inline]
33
+ pub fn fast_hash_bytes(input: &[u8]) -> u64 {
34
+ xxh3_64(input)
35
+ }
36
+
37
+ // ── Napi exports ───────────────────────────────────────────────────
38
+
39
+ /// CRC32 over a raw byte slice (pure core, shared by the napi boundary and
40
+ /// the pure-core unit tests).
41
+ #[inline]
42
+ pub fn crc32_bytes(input: &[u8]) -> u32 {
43
+ crc32fast::hash(input)
44
+ }
45
+
46
+ #[napi]
47
+ pub fn crc32(input: Uint8Array) -> u32 {
48
+ crc32_bytes(input.as_ref())
49
+ }
50
+
51
+ #[napi(js_name = "fnv1a64")]
52
+ pub fn fnv1a64(input: Uint8Array) -> u64 {
53
+ fnv1a64_bytes(input.as_ref())
54
+ }
55
+
56
+ /// XXH3-64 over raw bytes. High-throughput non-cryptographic hash; the same
57
+ /// core the ingress IP-trust hasher uses. Exposed publicly so callers can
58
+ /// race it against `Bun.hash.xxHash3` — see docs/bun-builtins-decision-matrix.md.
59
+ #[napi]
60
+ pub fn xxh3(input: Uint8Array) -> u64 {
61
+ fast_hash_bytes(input.as_ref())
62
+ }
63
+
64
+ /// CRC32 batch — delegates to the shared packed-u32 writer (same wire format
65
+ /// as the `_into` variant; validation + parallelism live in
66
+ /// `util::packed::write_u32_batch_into`, single source of truth).
67
+ #[napi]
68
+ pub fn crc32_batch_packed(input: Uint8Array) -> Result<Buffer> {
69
+ let data = input.as_ref();
70
+ let count = crate::util::packed::PackedIter::new(data)?.len();
71
+ let mut out = vec![0u8; 4 + count.saturating_mul(4)];
72
+ let written = crate::util::write_u32_batch_into(data, &mut out, crc32_bytes)?;
73
+ out.truncate(written);
74
+ Ok(Buffer::from(out))
75
+ }
76
+
77
+ /// Reusable-output CRC32 batch: writes `[u32 count][u32…]` into `output` and
78
+ /// returns bytes written. Wire format is byte-identical to
79
+ /// [`crc32_batch_packed`]; the JS loader uses this with a pooled buffer.
80
+ #[napi]
81
+ pub fn crc32_batch_packed_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
82
+ crate::util::run_packed_into(&input, &mut output, |data, out| {
83
+ crate::util::write_u32_batch_into(data, out, crc32_bytes)
84
+ })
85
+ }
86
+
87
+ #[cfg(test)]
88
+ mod tests {
89
+ use super::*;
90
+
91
+ #[test]
92
+ fn fnv1a_known_vectors() {
93
+ // Standard FNV-1a 64-bit test vectors.
94
+ assert_eq!(fnv1a64_bytes(b""), 0xcbf2_9ce4_8422_2325);
95
+ assert_eq!(fnv1a64_bytes(b"a"), 0xaf63_dc4c_8601_ec8c);
96
+ assert_eq!(fnv1a64_bytes(b"foobar"), 0x8594_4171_f739_67e8);
97
+ // Continue-style matches one-shot.
98
+ let h = fnv1a64_continue(fnv1a64_bytes(b"foo"), b"bar");
99
+ assert_eq!(h, fnv1a64_bytes(b"foobar"));
100
+ }
101
+
102
+ #[test]
103
+ fn crc32_known_vector() {
104
+ assert_eq!(crc32fast::hash(b""), 0);
105
+ // Standard CRC-32 check value.
106
+ assert_eq!(crc32fast::hash(b"123456789"), 0xcbf4_3926);
107
+ // The napi wrapper agrees.
108
+ assert_eq!(crc32(Uint8Array::new(b"123456789".to_vec())), 0xcbf4_3926);
109
+ }
110
+
111
+ #[test]
112
+ fn fnv1a_napi_agrees_with_core() {
113
+ assert_eq!(
114
+ fnv1a64(Uint8Array::new(b"foobar".to_vec())),
115
+ fnv1a64_bytes(b"foobar")
116
+ );
117
+ }
118
+
119
+ #[test]
120
+ fn xxh3_stable_and_distinct() {
121
+ let empty = fast_hash_bytes(b"");
122
+ assert_eq!(empty, fast_hash_bytes(b""));
123
+ assert_ne!(empty, fast_hash_bytes(b"a"));
124
+ assert_eq!(fast_hash_bytes(b"castrum"), fast_hash_bytes(b"castrum"));
125
+ }
126
+
127
+ #[test]
128
+ fn xxh3_napi_agrees_with_core() {
129
+ let input = b"castrum xxh3 known-vector check 0123456789";
130
+ assert_eq!(
131
+ xxh3(Uint8Array::new(input.to_vec())),
132
+ fast_hash_bytes(input)
133
+ );
134
+ // Deterministic across calls on the same bytes.
135
+ assert_eq!(
136
+ xxh3(Uint8Array::new(input.to_vec())),
137
+ xxh3(Uint8Array::new(input.to_vec()))
138
+ );
139
+ }
140
+ }
@@ -0,0 +1,88 @@
1
+ // rust/crypto/hmac_sha256.rs — HMAC-SHA256 sign/verify (scalar + HmacSigner instance).
2
+
3
+ use crate::util::bytes::{hex_decode_32, hex_encode_32, trim_ascii_whitespace};
4
+ use aws_lc_rs::hmac;
5
+ use napi::bindgen_prelude::*;
6
+ use napi_derive::napi;
7
+
8
+ #[napi]
9
+ pub fn hmac_sha256(key: Uint8Array, data: Buffer) -> Buffer {
10
+ let key = hmac::Key::new(hmac::HMAC_SHA256, key.as_ref());
11
+ let tag = hmac::sign(&key, data.as_ref());
12
+
13
+ let mut out = [0u8; 64];
14
+ hex_encode_32(tag.as_ref(), &mut out);
15
+
16
+ Buffer::from(out.to_vec())
17
+ }
18
+
19
+ #[napi]
20
+ pub fn hmac_sha256_verify(key: Uint8Array, data: Buffer, sig: Buffer) -> bool {
21
+ let sig = trim_ascii_whitespace(sig.as_ref());
22
+
23
+ let Some(sig_bytes) = hex_decode_32(sig) else {
24
+ return false;
25
+ };
26
+
27
+ let key = hmac::Key::new(hmac::HMAC_SHA256, key.as_ref());
28
+ hmac::verify(&key, data.as_ref(), &sig_bytes).is_ok()
29
+ }
30
+
31
+ #[napi]
32
+ pub struct HmacSigner {
33
+ key: hmac::Key,
34
+ }
35
+
36
+ #[napi]
37
+ impl HmacSigner {
38
+ #[napi(constructor)]
39
+ pub fn new(key: Uint8Array) -> Self {
40
+ Self {
41
+ key: hmac::Key::new(hmac::HMAC_SHA256, key.as_ref()),
42
+ }
43
+ }
44
+
45
+ #[napi]
46
+ pub fn sign(&self, data: Buffer) -> Buffer {
47
+ let tag = hmac::sign(&self.key, data.as_ref());
48
+
49
+ let mut out = [0u8; 64];
50
+ hex_encode_32(tag.as_ref(), &mut out);
51
+
52
+ Buffer::from(out.to_vec())
53
+ }
54
+
55
+ #[napi]
56
+ pub fn verify(&self, data: Buffer, sig: Buffer) -> bool {
57
+ let sig = trim_ascii_whitespace(sig.as_ref());
58
+
59
+ let Some(sig_bytes) = hex_decode_32(sig) else {
60
+ return false;
61
+ };
62
+
63
+ hmac::verify(&self.key, data.as_ref(), &sig_bytes).is_ok()
64
+ }
65
+ }
66
+
67
+ #[cfg(test)]
68
+ mod tests {
69
+ #[test]
70
+ fn hmac_sign_then_verify_roundtrip() {
71
+ let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, b"secret-key");
72
+ let data = b"hello world";
73
+ let tag = aws_lc_rs::hmac::sign(&key, data);
74
+
75
+ let mut hex = [0u8; 64];
76
+ crate::util::bytes::hex_encode_32(tag.as_ref(), &mut hex);
77
+ let decoded = crate::util::bytes::hex_decode_32(&hex).expect("valid hex decodes");
78
+
79
+ assert!(aws_lc_rs::hmac::verify(&key, data, &decoded).is_ok());
80
+ }
81
+
82
+ #[test]
83
+ fn hex_decode_32_rejects_bad_length_and_digits() {
84
+ assert!(crate::util::bytes::hex_decode_32(b"abc").is_none());
85
+ assert!(crate::util::bytes::hex_decode_32(&[b'0'; 64]).is_some());
86
+ assert!(crate::util::bytes::hex_decode_32(&[b'g'; 64]).is_none());
87
+ }
88
+ }
@@ -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::*;