castrum 0.9.3 → 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,152 @@
1
+ // rust/ffi/util.rs — shared helpers for the C-ABI export surface.
2
+ //
3
+ // The per-thread state + panic containment every `castrum_*` export relies on:
4
+ // compiled-HMAC-key LRU, AEAD algorithm map, `catch_unwind` guard, and the
5
+ // per-thread cstring buffer. Kept together so the domain wrapper files
6
+ // (`crypto.rs`, `jwt.rs`, …) stay focused on their symbols.
7
+
8
+ use aws_lc_rs::aead::{AES_256_GCM, CHACHA20_POLY1305};
9
+ use aws_lc_rs::hmac;
10
+ use lru::LruCache;
11
+ use std::cell::RefCell;
12
+ use std::num::NonZeroUsize;
13
+ use std::slice;
14
+
15
+ /// Build the HMAC-SHA256 key used by the scalar HMAC/cookie/CSRF cores (the
16
+ /// same per-call construction the napi scalar fns use). Test-only reference
17
+ /// helper: the hot C-ABI fns go through `hmac_key_cached` below.
18
+ #[cfg(test)]
19
+ #[inline]
20
+ pub(crate) fn hmac_key(secret: &[u8]) -> hmac::Key {
21
+ hmac::Key::new(hmac::HMAC_SHA256, secret)
22
+ }
23
+
24
+ /// Capacity of the per-thread compiled-key LRU.
25
+ pub(crate) const HMAC_KEY_CACHE_CAP: usize = 16;
26
+
27
+ // Test-only hit counter proving the cache actually reuses compiled keys.
28
+ #[cfg(test)]
29
+ thread_local! {
30
+ pub(crate) static HMAC_CACHE_HITS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
31
+ }
32
+
33
+ // The cache itself is also test-visible (tests clear it to measure hit counts).
34
+ thread_local! {
35
+ pub(crate) static HMAC_KEY_CACHE: RefCell<LruCache<Vec<u8>, hmac::Key>> = RefCell::new(
36
+ LruCache::new(NonZeroUsize::new(HMAC_KEY_CACHE_CAP).expect("cap is nonzero")),
37
+ );
38
+ }
39
+
40
+ /// Return a compiled HMAC-SHA256 key for `secret`, reusing a cached copy when
41
+ /// the same secret was used recently (per-thread LRU, cap 16).
42
+ #[inline]
43
+ pub(crate) fn hmac_key_cached(secret: &[u8]) -> hmac::Key {
44
+ HMAC_KEY_CACHE.with(|cache| {
45
+ let mut cache = cache.borrow_mut();
46
+ if let Some(key) = cache.get(secret) {
47
+ #[cfg(test)]
48
+ HMAC_CACHE_HITS.with(|h| h.set(h.get() + 1));
49
+ return key.clone();
50
+ }
51
+ let key = hmac::Key::new(hmac::HMAC_SHA256, secret);
52
+ cache.put(secret.to_vec(), key.clone());
53
+ key
54
+ })
55
+ }
56
+
57
+ /// Map an algorithm selector byte to the AEAD `Algorithm` (0 = AES-256-GCM,
58
+ /// 1 = ChaCha20-Poly1305) — mirrors `crate::crypto::aead::resolve_algorithm`.
59
+ #[inline]
60
+ pub(crate) fn aead_alg(alg: u8) -> Option<&'static aws_lc_rs::aead::Algorithm> {
61
+ match alg {
62
+ 0 => Some(&AES_256_GCM),
63
+ 1 => Some(&CHACHA20_POLY1305),
64
+ _ => None,
65
+ }
66
+ }
67
+
68
+ /// Run a fallible native core under `catch_unwind`, returning `fallback` on a
69
+ /// panic.
70
+ ///
71
+ /// The napi path relies on napi-rs wrapping every call in `catch_unwind` (a
72
+ /// panic becomes a JS exception/500). The raw C ABI (`bun:ffi`) has no such
73
+ /// guard, so a panic unwinding through an `extern "C"` frame is UB and kills
74
+ /// the whole Bun process (this is what happened under burst load in
75
+ /// `11-concurrent-burst`). Every `castrum_*` export that runs a fallible /
76
+ /// allocating core routes through this helper for parity: a panic becomes the
77
+ /// caller's error sentinel (`0` / `None`), never a process crash.
78
+ /// `AssertUnwindSafe` is required because the closures capture `&mut [u8]`
79
+ /// output slices.
80
+ #[inline]
81
+ pub(crate) fn panic_guard<T>(f: impl FnOnce() -> T, fallback: T) -> T {
82
+ std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(fallback)
83
+ }
84
+
85
+ /// Write the packed `[u8 allowed][u32 remaining LE][i64 reset_ms LE]` verdict;
86
+ /// returns bytes written or the exact required size (13) when `out_cap` is too
87
+ /// small. Never returns 0 on a valid call.
88
+ ///
89
+ /// # Safety
90
+ /// `out` must be valid for writes up to `out_cap`.
91
+ pub(crate) unsafe fn write_rate_check(
92
+ allowed: bool,
93
+ remaining: u32,
94
+ reset_ms: i64,
95
+ out: *mut u8,
96
+ out_cap: usize,
97
+ ) -> usize {
98
+ if out_cap < 13 {
99
+ return 13;
100
+ }
101
+ let o = slice::from_raw_parts_mut(out, 13);
102
+ o[0] = u8::from(allowed);
103
+ o[1..5].copy_from_slice(&remaining.to_le_bytes());
104
+ o[5..13].copy_from_slice(&reset_ms.to_le_bytes());
105
+ 13
106
+ }
107
+
108
+ // ── cstring-returning writers (per-thread reused buffer) ─────────
109
+ // Bun's `cstring` FFI return type has the ENGINE clone the result string out
110
+ // of the returned pointer at the moment of the call (bun:ffi docs), so the JS
111
+ // side pays ZERO decode + ZERO allocation for these single-string outputs.
112
+ // The buffer is thread-local and reused across calls (same pattern as
113
+ // HMAC_KEY_CACHE above) — valid only until the next call on the same thread,
114
+ // which is all the engine needs (it clones synchronously at return).
115
+ thread_local! {
116
+ static CSTR_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
117
+ }
118
+
119
+ /// Write `write` into the per-thread buffer sized to `needed` (reused across
120
+ /// calls), null-terminate, and return the pointer. `null` = write error
121
+ /// (malformed input / crypto failure — the caller maps to a throw or sentinel).
122
+ pub(crate) fn cstring_return(
123
+ needed: usize,
124
+ write: impl FnOnce(&mut [u8]) -> Option<usize>,
125
+ ) -> *const std::os::raw::c_char {
126
+ CSTR_BUF.with(|cell| {
127
+ let mut buf = cell.borrow_mut();
128
+ if buf.len() < needed + 1 {
129
+ buf.resize(needed + 1, 0);
130
+ }
131
+ let cap = buf.len() - 1;
132
+ match write(&mut buf[..cap]) {
133
+ Some(w) => {
134
+ buf[w] = 0;
135
+ buf.as_ptr() as *const std::os::raw::c_char
136
+ }
137
+ None => std::ptr::null(),
138
+ }
139
+ })
140
+ }
141
+
142
+ /// Read the bytes of a `cstring`-returning FFI symbol's result. The engine
143
+ /// cloned the string at the call, so the per-thread buffer reuse is safe here.
144
+ /// Test-only: the sole production consumer was the removed task-group dispatch
145
+ /// (`run_one_task`); remaining callers are in the C-ABI unit tests below.
146
+ #[cfg(test)]
147
+ pub(crate) unsafe fn cstr_bytes(p: *const std::os::raw::c_char) -> Option<Vec<u8>> {
148
+ if p.is_null() {
149
+ return None;
150
+ }
151
+ Some(std::ffi::CStr::from_ptr(p).to_bytes().to_vec())
152
+ }
@@ -0,0 +1,123 @@
1
+ // rust/ffi/validators.rs — value-returning validators (email / UUID / IPv4 / IPv6).
2
+ //
3
+ // Two ABI shapes per validator (docs/FFI_BUN_GUIDE.md §3):
4
+ // - `castrum_validate_*` — `cstring` ARG: the engine transcodes the JS
5
+ // STRING to a call-scoped NUL-terminated UTF-8 buffer in-engine, so the
6
+ // JS side does zero encode work and the callee borrows via `CStr::from_ptr`
7
+ // (no copy). The fast path when the caller holds a JS string.
8
+ // - `castrum_validate_*_bytes` — `(ptr,len)` pair: zero transcode when the
9
+ // caller already holds BYTES (a cstring ARG would force a CString decode
10
+ // in JS and an engine re-encode — measured ~100 ns/call of pure waste;
11
+ // guide rule "cstring args are a LOSS for BYTE inputs").
12
+
13
+ /// Email / UUID / IPv4 / IPv6 validators → 1/0 (`cstring` ARG shape).
14
+ macro_rules! validator_c_abi {
15
+ ($name:ident, $core:path) => {
16
+ #[doc = concat!("Validate input as a ", stringify!($name), " → 1/0.")]
17
+ ///
18
+ /// # Safety
19
+ /// `data` must be a valid NUL-terminated C string.
20
+ #[no_mangle]
21
+ pub unsafe extern "C" fn $name(data: *const std::os::raw::c_char) -> u8 {
22
+ if data.is_null() {
23
+ return 0;
24
+ }
25
+ u8::from($core(std::ffi::CStr::from_ptr(data).to_bytes()))
26
+ }
27
+ };
28
+ }
29
+
30
+ /// Email / UUID / IPv4 / IPv6 validators over raw bytes → 1/0 (`(ptr,len)` shape).
31
+ macro_rules! validator_bytes_c_abi {
32
+ ($name:ident, $core:path) => {
33
+ #[doc = concat!("Validate raw bytes as a ", stringify!($name), " → 1/0.")]
34
+ ///
35
+ /// Byte-input sibling of the `cstring`-ARG validator: no NUL
36
+ /// termination required (the length bounds the read), so JS callers
37
+ /// with existing byte buffers skip the decode + engine re-encode.
38
+ ///
39
+ /// # Safety
40
+ /// `data` must be valid for reads of `len` bytes.
41
+ #[no_mangle]
42
+ pub unsafe extern "C" fn $name(data: *const u8, len: usize) -> u8 {
43
+ if data.is_null() && len != 0 {
44
+ return 0;
45
+ }
46
+ let bytes = if len == 0 {
47
+ &[]
48
+ } else {
49
+ std::slice::from_raw_parts(data, len)
50
+ };
51
+ u8::from($core(bytes))
52
+ }
53
+ };
54
+ }
55
+
56
+ validator_c_abi!(
57
+ castrum_validate_email,
58
+ crate::util::validation::validate_email_bytes
59
+ );
60
+ validator_c_abi!(
61
+ castrum_validate_uuid,
62
+ crate::util::validation::validate_uuid_bytes
63
+ );
64
+ validator_c_abi!(
65
+ castrum_validate_ipv4,
66
+ crate::util::validation::validate_ipv4_bytes
67
+ );
68
+ validator_c_abi!(
69
+ castrum_validate_ipv6,
70
+ crate::util::validation::validate_ipv6_bytes
71
+ );
72
+
73
+ validator_bytes_c_abi!(
74
+ castrum_validate_email_bytes,
75
+ crate::util::validation::validate_email_bytes
76
+ );
77
+ validator_bytes_c_abi!(
78
+ castrum_validate_uuid_bytes,
79
+ crate::util::validation::validate_uuid_bytes
80
+ );
81
+ validator_bytes_c_abi!(
82
+ castrum_validate_ipv4_bytes,
83
+ crate::util::validation::validate_ipv4_bytes
84
+ );
85
+ validator_bytes_c_abi!(
86
+ castrum_validate_ipv6_bytes,
87
+ crate::util::validation::validate_ipv6_bytes
88
+ );
89
+
90
+ /// Batch fixed-width hex validation: NEWLINE-separated lines in `data`, one
91
+ /// verdict byte (1 = exactly `width` hex chars, 0 = not) per line into `out`.
92
+ /// Needed-size convention: `0` = bad width / null pointers (real error);
93
+ /// `w > out_cap` = exact required size; else bytes written.
94
+ ///
95
+ /// # Safety
96
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to
97
+ /// `out_cap`.
98
+ #[no_mangle]
99
+ pub unsafe extern "C" fn castrum_hex_validate_batch(
100
+ data: *const u8,
101
+ len: usize,
102
+ width: u32,
103
+ out: *mut u8,
104
+ out_cap: usize,
105
+ ) -> usize {
106
+ if data.is_null() || out.is_null() {
107
+ return 0;
108
+ }
109
+ let input = std::slice::from_raw_parts(data, len);
110
+ let needed = crate::util::validation::hex_batch_count(input);
111
+ if needed > out_cap {
112
+ return needed;
113
+ }
114
+ // `needed` verdict bytes fit — one bounded scratch Vec, one copy out.
115
+ let mut buf = Vec::with_capacity(needed);
116
+ match crate::util::validation::hex_batch_valid_into(input, width as usize, &mut buf) {
117
+ Ok(()) if buf.len() <= out_cap => {
118
+ std::slice::from_raw_parts_mut(out, buf.len()).copy_from_slice(&buf);
119
+ buf.len()
120
+ }
121
+ _ => 0,
122
+ }
123
+ }