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.
- package/Cargo.lock +2242 -0
- package/Cargo.toml +155 -0
- package/README.md +14 -6
- package/build.rs +5 -0
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +1508 -1291
- package/dist/index.js +3391 -2069
- package/docs/API.md +2 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +5 -3
- package/docs/ENVIRONMENT.md +25 -2
- package/docs/FFI_BUN_GUIDE.md +242 -14
- package/docs/HOW_TO_ADD_AN_OP.md +10 -0
- package/docs/REPO_MAP.md +17 -6
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +24 -25
- package/package.json +173 -156
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +105 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +69 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/task.rs +172 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +136 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +211 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +770 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +567 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +883 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +73 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +594 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/health.ts +1 -2
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +130 -12
- package/src/native/ffi/build/compress.ts +22 -10
- package/src/native/ffi/build/instances.ts +46 -4
- package/src/native/ffi/build/metrics.ts +247 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +32 -1
- package/src/native/ffi/build.ts +4 -0
- package/src/native/ffi/selftest.ts +12 -3
- package/src/native/ffi/types.ts +151 -9
- package/src/native/ffi.ts +160 -15
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +55 -5
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +71 -27
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/rust-ffi/text.ts +25 -3
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +24 -4
- package/src/shared/codec.ts +18 -3
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- 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
|
+
}
|