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,336 @@
1
+ // rust/json/fast_schema/capture.rs — One-pass derive: capture scalar values at
2
+ // JSON-pointer paths DURING the same validation walk (no second pass, no DOM).
3
+ //
4
+ // The `SchemaValidator.derive` napi entry (json_schema.rs) uses this to power
5
+ // "validate + extract" routes: the response is derived from a handful of body
6
+ // fields (e.g. `/api/orders` needs `lineItems.length` + `totalCents`), so
7
+ // Rust validates the schema AND captures those values in a single zero-DOM
8
+ // pass — replacing `JSON.parse` (DOM build + GC) + Ajv entirely on the happy
9
+ // path, and rejecting invalid bodies in microseconds.
10
+ //
11
+ // Capture is OFF by default (`Ctx::capture == None`): the bool/detailed hot
12
+ // paths are byte-for-byte unchanged. When active, `Capture` records, per
13
+ // target path, either the raw byte range of a scalar value (`Value`) or the
14
+ // element count of an array (`Length`, path terminal `/-`).
15
+ //
16
+ // Safety: capture fires only on the ROOT walk. Combinator / pattern / enum
17
+ // re-scans create fresh Cursors over sub-slices, and `Ctx` compares the
18
+ // cursor's data pointer against the root input pointer, so sub-scan offsets
19
+ // (which are relative to a sub-slice, not the input) can never corrupt
20
+ // captured ranges. `with_suppressed` branch checks are also excluded.
21
+
22
+ use std::collections::HashMap;
23
+
24
+ /// What to capture at a target path.
25
+ #[derive(Clone, Copy, PartialEq, Eq, Debug)]
26
+ pub enum CaptureKind {
27
+ /// The raw scalar value at the path (string / number / bool / null).
28
+ Value,
29
+ /// The element count of the ARRAY at the path (path terminal `/-`).
30
+ Length,
31
+ }
32
+
33
+ /// A single target: an object-key path (no array-index steps) + a capture kind.
34
+ #[derive(Clone, Debug)]
35
+ pub struct TargetPath {
36
+ pub steps: Vec<Vec<u8>>,
37
+ pub kind: CaptureKind,
38
+ }
39
+
40
+ /// Parse an RFC 6901 JSON pointer into a `TargetPath`.
41
+ ///
42
+ /// `"/totalCents"` → `Value` at steps `["totalCents"]`.
43
+ /// `"/lineItems/-"` → `Length` at steps `["lineItems"]`.
44
+ /// `"/"` (root) or a pointer with array-index steps → `None`.
45
+ pub fn parse_target(pointer: &str) -> Option<TargetPath> {
46
+ if pointer.is_empty() || !pointer.starts_with('/') {
47
+ return None;
48
+ }
49
+ let raw_tokens: Vec<&str> = pointer[1..].split('/').collect();
50
+ if raw_tokens.is_empty() || raw_tokens[0].is_empty() {
51
+ // "/" root or "//" empty first key — unsupported.
52
+ return None;
53
+ }
54
+ let last = raw_tokens[raw_tokens.len() - 1];
55
+ let (kind, body) = if last == "-" {
56
+ (CaptureKind::Length, &raw_tokens[..raw_tokens.len() - 1])
57
+ } else {
58
+ (CaptureKind::Value, &raw_tokens[..])
59
+ };
60
+ if body.is_empty() {
61
+ // A bare "-" ("/-") or a root-length target — unsupported.
62
+ return None;
63
+ }
64
+ let mut steps = Vec::with_capacity(body.len());
65
+ for tok in body {
66
+ // Array-index steps are unsupported: a token that is `-` or all ASCII
67
+ // digits is treated as an array index (JSON-pointer convention) and
68
+ // rejected. (A rare all-digit OBJECT key is thereby excluded too.)
69
+ let is_index = !tok.is_empty() && (*tok == "-" || tok.bytes().all(|b| b.is_ascii_digit()));
70
+ if is_index {
71
+ return None;
72
+ }
73
+ // Decode JSON-pointer escapes: ~1 -> '/', ~0 -> '~'.
74
+ let mut step = Vec::with_capacity(tok.len());
75
+ let bytes = tok.as_bytes();
76
+ let mut i = 0;
77
+ while i < bytes.len() {
78
+ if bytes[i] == b'~' && i + 1 < bytes.len() {
79
+ match bytes[i + 1] {
80
+ b'1' => step.push(b'/'),
81
+ b'0' => step.push(b'~'),
82
+ _ => step.push(bytes[i]),
83
+ }
84
+ i += 2;
85
+ } else {
86
+ step.push(bytes[i]);
87
+ i += 1;
88
+ }
89
+ }
90
+ steps.push(step);
91
+ }
92
+ Some(TargetPath { steps, kind })
93
+ }
94
+
95
+ /// One trie node. `children` maps an object key to the next node; `value_target`
96
+ /// / `length_target` are set when this node is the terminal of a `Value` /
97
+ /// `Length` target.
98
+ struct TrieNode {
99
+ children: HashMap<Box<[u8]>, usize>,
100
+ value_target: Option<usize>,
101
+ length_target: Option<usize>,
102
+ }
103
+
104
+ impl TrieNode {
105
+ fn new() -> Self {
106
+ Self {
107
+ children: HashMap::new(),
108
+ value_target: None,
109
+ length_target: None,
110
+ }
111
+ }
112
+ }
113
+
114
+ /// In-progress capture state, threaded through `Ctx` during one validation walk.
115
+ ///
116
+ /// Targets are compiled into a trie once per call; during the walk the active
117
+ /// node is tracked by `(node, alive)` pairs pushed/popped per OBJECT nesting
118
+ /// level — NO per-member key cloning (the detailed-error path stack is
119
+ /// untouched), so capture overhead on a valid 5000-item document is a few hash
120
+ /// lookups per member, not allocations. Dead subtrees (no remaining target
121
+ /// key) cost nothing.
122
+ pub struct Capture {
123
+ arena: Vec<TrieNode>,
124
+ /// Active trie-node stack: `(node_idx, alive)` per object nesting level.
125
+ stack: Vec<(usize, bool)>,
126
+ matched: Vec<bool>,
127
+ /// per-target captured raw value byte ranges (Value targets only)
128
+ ranges: Vec<(usize, usize)>,
129
+ /// per-target array lengths (Length targets only)
130
+ lengths: Vec<usize>,
131
+ }
132
+
133
+ impl Capture {
134
+ pub fn new(targets: Vec<TargetPath>) -> Self {
135
+ let n = targets.len();
136
+ let mut arena = vec![TrieNode::new()]; // node 0 = root
137
+ for (ti, t) in targets.iter().enumerate() {
138
+ let mut node = 0usize;
139
+ for step in &t.steps {
140
+ let key: Box<[u8]> = step.clone().into_boxed_slice();
141
+ let next = match arena[node].children.get(&key) {
142
+ Some(&idx) => idx,
143
+ None => {
144
+ let idx = arena.len();
145
+ arena.push(TrieNode::new());
146
+ arena[node].children.insert(key, idx);
147
+ idx
148
+ }
149
+ };
150
+ node = next;
151
+ }
152
+ match t.kind {
153
+ CaptureKind::Value => arena[node].value_target = Some(ti),
154
+ CaptureKind::Length => arena[node].length_target = Some(ti),
155
+ }
156
+ }
157
+ Self {
158
+ arena,
159
+ stack: vec![(0, true)],
160
+ matched: vec![false; n],
161
+ ranges: vec![(0, 0); n],
162
+ lengths: vec![0; n],
163
+ }
164
+ }
165
+
166
+ /// Descend the trie for an object member key. Returns false when the
167
+ /// subtree is dead (no target can match below).
168
+ #[inline]
169
+ pub fn enter_key(&mut self, key: &[u8]) -> bool {
170
+ let (node, alive) = *self.stack.last().unwrap_or(&(0, true));
171
+ if !alive {
172
+ self.stack.push((node, false));
173
+ return false;
174
+ }
175
+ match self.arena[node].children.get(key) {
176
+ Some(&next) => {
177
+ self.stack.push((next, true));
178
+ true
179
+ }
180
+ None => {
181
+ self.stack.push((node, false));
182
+ false
183
+ }
184
+ }
185
+ }
186
+
187
+ /// Leave the current object nesting level.
188
+ #[inline]
189
+ pub fn exit_key(&mut self) {
190
+ if self.stack.len() > 1 {
191
+ self.stack.pop();
192
+ }
193
+ }
194
+
195
+ /// Record a captured scalar value at the current node (Value leaf).
196
+ #[inline]
197
+ pub fn record_value(&mut self, start: usize, end: usize) {
198
+ let (node, alive) = *self.stack.last().unwrap_or(&(0, false));
199
+ if !alive {
200
+ return;
201
+ }
202
+ if let Some(ti) = self.arena[node].value_target {
203
+ if !self.matched[ti] {
204
+ self.matched[ti] = true;
205
+ self.ranges[ti] = (start, end);
206
+ }
207
+ }
208
+ }
209
+
210
+ /// Record a captured array length at the current node (Length leaf).
211
+ #[inline]
212
+ pub fn record_length(&mut self, count: usize) {
213
+ let (node, alive) = *self.stack.last().unwrap_or(&(0, false));
214
+ if !alive {
215
+ return;
216
+ }
217
+ if let Some(ti) = self.arena[node].length_target {
218
+ if !self.matched[ti] {
219
+ self.matched[ti] = true;
220
+ self.lengths[ti] = count;
221
+ }
222
+ }
223
+ }
224
+
225
+ pub fn target_count(&self) -> usize {
226
+ self.matched.len()
227
+ }
228
+
229
+ /// Captured raw value byte range for target `i` (valid body, path present).
230
+ pub fn value_range(&self, i: usize) -> Option<(usize, usize)> {
231
+ if self.matched.get(i) == Some(&true) {
232
+ Some(self.ranges[i])
233
+ } else {
234
+ None
235
+ }
236
+ }
237
+
238
+ /// Captured array length for target `i` (valid body, path present).
239
+ pub fn length(&self, i: usize) -> Option<usize> {
240
+ if self.matched.get(i) == Some(&true) {
241
+ Some(self.lengths[i])
242
+ } else {
243
+ None
244
+ }
245
+ }
246
+ }
247
+
248
+ #[cfg(test)]
249
+ mod tests {
250
+ use super::*;
251
+
252
+ #[test]
253
+ fn parse_value_pointer() {
254
+ let t = parse_target("/totalCents").unwrap();
255
+ assert_eq!(t.kind, CaptureKind::Value);
256
+ assert_eq!(t.steps, vec![b"totalCents".to_vec()]);
257
+ }
258
+
259
+ #[test]
260
+ fn parse_length_pointer() {
261
+ let t = parse_target("/lineItems/-").unwrap();
262
+ assert_eq!(t.kind, CaptureKind::Length);
263
+ assert_eq!(t.steps, vec![b"lineItems".to_vec()]);
264
+ }
265
+
266
+ #[test]
267
+ fn parse_nested() {
268
+ let t = parse_target("/customer/email").unwrap();
269
+ assert_eq!(t.kind, CaptureKind::Value);
270
+ assert_eq!(t.steps, vec![b"customer".to_vec(), b"email".to_vec()]);
271
+ }
272
+
273
+ #[test]
274
+ fn parse_escapes() {
275
+ let t = parse_target("/a~1b/c~0d").unwrap();
276
+ assert_eq!(t.steps, vec![b"a/b".to_vec(), b"c~d".to_vec()]);
277
+ }
278
+
279
+ #[test]
280
+ fn parse_rejects_root_and_indexes() {
281
+ assert!(parse_target("").is_none());
282
+ assert!(parse_target("/").is_none());
283
+ assert!(parse_target("/lineItems/0").is_none());
284
+ assert!(parse_target("lineItems").is_none());
285
+ assert!(parse_target("/-").is_none());
286
+ }
287
+
288
+ #[test]
289
+ fn trie_matches_object_and_length() {
290
+ let mut cap = Capture::new(vec![
291
+ parse_target("/lineItems/-").unwrap(),
292
+ parse_target("/totalCents").unwrap(),
293
+ ]);
294
+ // root object member "lineItems" (array): descend, then array close.
295
+ cap.enter_key(b"lineItems");
296
+ cap.record_length(5000);
297
+ cap.exit_key();
298
+ // root object member "totalCents": descend, then record scalar value.
299
+ cap.enter_key(b"totalCents");
300
+ cap.record_value(100, 110);
301
+ cap.exit_key();
302
+ assert_eq!(cap.length(0), Some(5000));
303
+ assert_eq!(cap.value_range(1), Some((100, 110)));
304
+ // a non-target key goes dead and records nothing
305
+ cap.enter_key(b"other");
306
+ cap.record_length(9);
307
+ cap.record_value(1, 2);
308
+ cap.exit_key();
309
+ assert_eq!(cap.length(0), Some(5000));
310
+ assert_eq!(cap.value_range(1), Some((100, 110)));
311
+ }
312
+
313
+ #[test]
314
+ fn trie_dead_subtree_is_free() {
315
+ let mut cap = Capture::new(vec![parse_target("/totalCents").unwrap()]);
316
+ // entering a non-target object key kills the subtree
317
+ cap.enter_key(b"lineItems");
318
+ let alive = cap.enter_key(b"sku"); // inside a dead subtree
319
+ assert!(!alive);
320
+ cap.record_value(1, 2); // no-op
321
+ cap.exit_key();
322
+ cap.exit_key();
323
+ assert!(cap.value_range(0).is_none());
324
+ }
325
+
326
+ #[test]
327
+ fn trie_nested_value() {
328
+ let mut cap = Capture::new(vec![parse_target("/customer/email").unwrap()]);
329
+ cap.enter_key(b"customer");
330
+ cap.enter_key(b"email");
331
+ cap.record_value(50, 60);
332
+ cap.exit_key();
333
+ cap.exit_key();
334
+ assert_eq!(cap.value_range(0), Some((50, 60)));
335
+ }
336
+ }