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,222 @@
1
+ // rust/crypto/session.rs — fused session-envelope seal/open.
2
+ //
3
+ // The framework's session plugin per-request path was:
4
+ // seal: JSON.stringify(envelope) → signCookie(json, secret)
5
+ // open: verifyCookie(token, secret) → JSON.parse(unsigned)
6
+ // i.e. two FFI crossings + a full envelope stringify/parse. Both steps here
7
+ // build/extract the envelope NATIVELY (id/data/exp), so the caller crosses
8
+ // ONCE and only `data` (the arbitrary user object) is stringified/parsed in
9
+ // JS — the id and exp never round-trip through JSC.
10
+
11
+ use crate::crypto::cookie_sign::{sign_cookie_bytes, verify_cookie_bytes};
12
+ use aws_lc_rs::hmac;
13
+
14
+ /// Build the envelope JSON for `id` / `dataJson` / `exp` and sign it with
15
+ /// HMAC-SHA256 (same wire format as signCookie: `payload.<64-hex>`).
16
+ /// `data_json` must be a valid JSON object/value — embedded verbatim.
17
+ pub fn seal_core(id: &[u8], data_json: &[u8], exp_secs: i64, secret: &[u8]) -> Option<Vec<u8>> {
18
+ if id.is_empty() || secret.is_empty() {
19
+ return None;
20
+ }
21
+ // Envelope size: {"id":"","data":<data>,"exp":0} + escapes on id only
22
+ // (data is already valid JSON; exp is an integer literal).
23
+ let id_esc_len = crate::json::json_ser::json_escaped_len(id);
24
+ let mut json = Vec::with_capacity(id_esc_len + data_json.len() + 32);
25
+ json.push(b'{');
26
+ json.extend_from_slice(b"\"id\":\"");
27
+ let mut wpos = json.len();
28
+ json.resize(json.len() + id_esc_len, 0);
29
+ crate::json::json_ser::write_json_escaped(&mut json, &mut wpos, id);
30
+ json.truncate(wpos);
31
+ json.extend_from_slice(b"\",\"data\":");
32
+ json.extend_from_slice(data_json);
33
+ json.extend_from_slice(b",\"exp\":");
34
+ json.extend_from_slice(exp_secs.to_string().as_bytes());
35
+ json.push(b'}');
36
+
37
+ let key = hmac::Key::new(hmac::HMAC_SHA256, secret);
38
+ Some(sign_cookie_bytes(&json, &key))
39
+ }
40
+
41
+ /// Verify a sealed token and extract the envelope fields into the packed
42
+ /// layout: `[u8 ok=1][i64 exp][u32 idLen][id][u32 dataLen][dataJson]`.
43
+ /// Returns `None` on bad signature / malformed envelope.
44
+ pub fn open_core(token: &[u8], secret: &[u8]) -> Option<(i64, Vec<u8>, Vec<u8>)> {
45
+ if secret.is_empty() {
46
+ return None;
47
+ }
48
+ let key = hmac::Key::new(hmac::HMAC_SHA256, secret);
49
+ let json = verify_cookie_bytes(token, &key)?;
50
+ // Minimal extraction over the known shape {"id":"…","data":…,"exp":N}.
51
+ // The id/data values were written by `seal` (or an equivalent signer), so
52
+ // a targeted scan of the top-level keys is sufficient — no DOM needed.
53
+ let mut id: Option<Vec<u8>> = None;
54
+ let mut data: Option<Vec<u8>> = None;
55
+ let mut exp: Option<i64> = None;
56
+
57
+ let mut pos = 1usize; // skip '{'
58
+ let bytes = &json[..];
59
+ while pos < bytes.len() {
60
+ match bytes[pos] {
61
+ b',' | b'{' | b'}' => pos += 1,
62
+ b'"' => {
63
+ // key string
64
+ let kstart = pos + 1;
65
+ let mut kend = kstart;
66
+ while kend < bytes.len() && bytes[kend] != b'"' {
67
+ kend += 1;
68
+ }
69
+ if kend >= bytes.len() {
70
+ return None;
71
+ }
72
+ let key = &bytes[kstart..kend];
73
+ pos = kend + 1;
74
+ if bytes.get(pos) != Some(&b':') {
75
+ return None;
76
+ }
77
+ pos += 1;
78
+ while matches!(bytes.get(pos), Some(b' ')) {
79
+ pos += 1;
80
+ }
81
+ match key {
82
+ b"id" => {
83
+ if bytes.get(pos) != Some(&b'"') {
84
+ return None;
85
+ }
86
+ let vs = pos + 1;
87
+ let mut ve = vs;
88
+ while ve < bytes.len() && bytes[ve] != b'"' {
89
+ ve += 1;
90
+ }
91
+ id = Some(bytes[vs..ve].to_vec());
92
+ pos = ve + 1;
93
+ }
94
+ b"exp" => {
95
+ let vs = pos;
96
+ while matches!(bytes.get(pos), Some(c) if c.is_ascii_digit() || *c == b'-')
97
+ {
98
+ pos += 1;
99
+ }
100
+ exp = std::str::from_utf8(&bytes[vs..pos])
101
+ .ok()
102
+ .and_then(|s| s.parse::<i64>().ok());
103
+ }
104
+ b"data" => {
105
+ // Capture the raw JSON value: object/array/string/num.
106
+ let vs = pos;
107
+ let ve = skip_json_value(bytes, vs)?;
108
+ data = Some(bytes[vs..ve].to_vec());
109
+ pos = ve;
110
+ }
111
+ _ => {
112
+ // Unknown key: skip its value generically.
113
+ pos = skip_json_value(bytes, pos)?;
114
+ }
115
+ }
116
+ }
117
+ _ => pos += 1,
118
+ }
119
+ }
120
+
121
+ let id = id?;
122
+ if id.is_empty() {
123
+ return None;
124
+ }
125
+ Some((exp.unwrap_or(0), id, data.unwrap_or_else(|| b"{}".to_vec())))
126
+ }
127
+
128
+ /// Skip one JSON value starting at `pos`; returns the end offset.
129
+ fn skip_json_value(bytes: &[u8], pos: usize) -> Option<usize> {
130
+ match bytes.get(pos)? {
131
+ b'"' => {
132
+ let mut e = pos + 1;
133
+ while e < bytes.len() {
134
+ if bytes[e] == b'\\' {
135
+ e += 2;
136
+ continue;
137
+ }
138
+ if bytes[e] == b'"' {
139
+ return Some(e + 1);
140
+ }
141
+ e += 1;
142
+ }
143
+ None
144
+ }
145
+ b'{' | b'[' => {
146
+ let open = bytes[pos];
147
+ let close = if open == b'{' { b'}' } else { b']' };
148
+ let mut depth = 0usize;
149
+ let mut e = pos;
150
+ while e < bytes.len() {
151
+ match bytes[e] {
152
+ b'"' => e = skip_json_value(bytes, e)? - 1, // string inside
153
+ c if c == open => depth += 1,
154
+ c if c == close => {
155
+ depth -= 1;
156
+ if depth == 0 {
157
+ return Some(e + 1);
158
+ }
159
+ }
160
+ _ => {}
161
+ }
162
+ e += 1;
163
+ }
164
+ None
165
+ }
166
+ _ => {
167
+ // number / true / false / null → until delimiter
168
+ let mut e = pos;
169
+ while e < bytes.len() && !matches!(bytes[e], b',' | b'}' | b']') {
170
+ e += 1;
171
+ }
172
+ Some(e)
173
+ }
174
+ }
175
+ }
176
+
177
+ #[cfg(test)]
178
+ mod tests {
179
+ use super::*;
180
+
181
+ const SECRET: &[u8] = b"test-secret";
182
+
183
+ #[test]
184
+ fn seal_open_round_trip() {
185
+ let token =
186
+ seal_core(b"sess-123", br#"{"cart":["a","b"]}"#, 1_700_000_000, SECRET).expect("seal");
187
+ let (exp, id, data) = open_core(&token, SECRET).expect("open");
188
+ assert_eq!(id, b"sess-123");
189
+ assert_eq!(exp, 1_700_000_000);
190
+ assert_eq!(data, br#"{"cart":["a","b"]}"#);
191
+ }
192
+
193
+ #[test]
194
+ fn tampered_token_rejected() {
195
+ let mut token = seal_core(b"id1", b"{}", 0, SECRET).expect("seal");
196
+ let last = token.len() - 1;
197
+ token[last] ^= 0xff;
198
+ assert!(open_core(&token, SECRET).is_none());
199
+ }
200
+
201
+ #[test]
202
+ fn wrong_secret_rejected() {
203
+ let token = seal_core(b"id1", b"{}", 0, SECRET).expect("seal");
204
+ assert!(open_core(&token, b"other").is_none());
205
+ }
206
+
207
+ #[test]
208
+ fn empty_id_rejected() {
209
+ assert!(seal_core(b"", b"{}", 0, SECRET).is_none());
210
+ }
211
+
212
+ #[test]
213
+ fn wire_format_matches_sign_cookie() {
214
+ // The sealed token must be verifiable by the EXISTING cookie verify
215
+ // core (same "payload.<hex>" wire format) so old readers stay valid.
216
+ let token = seal_core(b"sid", br#"{"n":1}"#, 5, SECRET).expect("seal");
217
+ let key = hmac::Key::new(hmac::HMAC_SHA256, SECRET);
218
+ let unsigned = verify_cookie_bytes(&token, &key).expect("verify via cookie core");
219
+ let s = String::from_utf8(unsigned).expect("utf8");
220
+ assert!(s.starts_with("{\"id\":\"sid\",\"data\":{\"n\":1},\"exp\":5}"));
221
+ }
222
+ }