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,419 @@
1
+ // rust/ffi/jwt.rs — JWT + Ed25519 C-ABI exports.
2
+ //
3
+ // HS256/EdDSA signing + verification and Ed25519 keypair/sign/verify for the
4
+ // RBAC auth path. Opaque-handle variants use the precompiled-key instances;
5
+ // the scalar variants derive the key per call.
6
+
7
+ use std::slice;
8
+
9
+ use super::util::{cstring_return, panic_guard};
10
+
11
+ /// JwtSigner: sign pre-serialized claim JSON with the PRECOMPILED key + ttl via
12
+ /// its opaque inner handle. Needed-size convention; 0 = invalid claims JSON /
13
+ /// null handle (real error).
14
+ ///
15
+ /// # Safety
16
+ /// `inner` must be a valid `JwtSigner` pointer from `inner_ptr()`, alive for
17
+ /// the call; `out` for writes up to `out_cap`.
18
+ #[no_mangle]
19
+ pub unsafe extern "C" fn castrum_jwt_signer_sign(
20
+ inner: usize,
21
+ claims: *const u8,
22
+ claims_len: usize,
23
+ now_seconds: i64,
24
+ out: *mut u8,
25
+ out_cap: usize,
26
+ ) -> usize {
27
+ if inner == 0 || claims.is_null() || out.is_null() {
28
+ return 0;
29
+ }
30
+ let c = slice::from_raw_parts(claims, claims_len);
31
+ let Some(token) = panic_guard(
32
+ || unsafe {
33
+ crate::crypto::jwt::jwt_signer_sign_bytes_core(
34
+ inner as *const crate::crypto::jwt::JwtSigner,
35
+ c,
36
+ now_seconds,
37
+ )
38
+ .ok()
39
+ },
40
+ None,
41
+ ) else {
42
+ return 0;
43
+ };
44
+ if token.len() > out_cap {
45
+ return token.len();
46
+ }
47
+ slice::from_raw_parts_mut(out, token.len()).copy_from_slice(&token);
48
+ token.len()
49
+ }
50
+
51
+ /// JwtSigner: verify a JWT with the PRECOMPILED key → claims JSON bytes.
52
+ /// Returns bytes written (0 = invalid signature / expired / malformed → null).
53
+ ///
54
+ /// # Safety
55
+ /// `inner` must be a valid `JwtSigner` pointer from `inner_ptr()`, alive for
56
+ /// the call; `out` for writes up to `out_cap`.
57
+ #[no_mangle]
58
+ pub unsafe extern "C" fn castrum_jwt_signer_verify(
59
+ inner: usize,
60
+ token: *const u8,
61
+ token_len: usize,
62
+ now_seconds: i64,
63
+ out: *mut u8,
64
+ out_cap: usize,
65
+ ) -> usize {
66
+ if inner == 0 || token.is_null() || out.is_null() {
67
+ return 0;
68
+ }
69
+ let t = slice::from_raw_parts(token, token_len);
70
+ let Some(claims) = panic_guard(
71
+ || unsafe {
72
+ crate::crypto::jwt::jwt_signer_verify_core(
73
+ inner as *const crate::crypto::jwt::JwtSigner,
74
+ t,
75
+ now_seconds,
76
+ )
77
+ },
78
+ None,
79
+ ) else {
80
+ return 0;
81
+ };
82
+ if claims.len() > out_cap {
83
+ return claims.len();
84
+ }
85
+ slice::from_raw_parts_mut(out, claims.len()).copy_from_slice(&claims);
86
+ claims.len()
87
+ }
88
+
89
+ /// Sign a JWT (HS256) from pre-serialized claim JSON bytes — the C-ABI sibling
90
+ /// of `jwt_sign_bytes`. `ttl <= 0` means no `iat`/`exp` injection (the napi
91
+ /// `Option<i64>` sentinel). Returns bytes written (0 on invalid claims JSON); a
92
+ /// result larger than `out_cap` reports the exact needed size (see
93
+ /// `compress_to_out!` for the convention).
94
+ ///
95
+ /// # Safety
96
+ /// `claims`/`secret` must be valid for reads of their lengths; `out` for
97
+ /// writes up to `out_cap`.
98
+ #[no_mangle]
99
+ pub unsafe extern "C" fn castrum_jwt_sign_bytes(
100
+ claims: *const u8,
101
+ clen: usize,
102
+ secret: *const u8,
103
+ slen: usize,
104
+ ttl: i64,
105
+ now: i64,
106
+ ) -> *const std::os::raw::c_char {
107
+ if claims.is_null() || secret.is_null() {
108
+ return std::ptr::null();
109
+ }
110
+ // Wrap in panic_guard: sonic parse + token build allocate — a panic must
111
+ // not unwind through the C ABI (process crash); it becomes null instead.
112
+ let token = panic_guard(
113
+ || {
114
+ let Ok(mut value) =
115
+ sonic_rs::from_slice::<sonic_rs::Value>(slice::from_raw_parts(claims, clen))
116
+ else {
117
+ return None;
118
+ };
119
+ let Ok(payload_b64) = crate::crypto::jwt::inject_and_payload_b64_sonic(
120
+ &mut value,
121
+ if ttl > 0 { Some(ttl) } else { None },
122
+ now,
123
+ ) else {
124
+ return None;
125
+ };
126
+ Some(crate::crypto::jwt::build_token(
127
+ crate::crypto::jwt::jwt_header_b64(),
128
+ &payload_b64,
129
+ slice::from_raw_parts(secret, slen),
130
+ ))
131
+ },
132
+ None,
133
+ );
134
+ let Some(token) = token else {
135
+ return std::ptr::null();
136
+ };
137
+ cstring_return(token.len(), |out| {
138
+ out[..token.len()].copy_from_slice(&token);
139
+ Some(token.len())
140
+ })
141
+ }
142
+
143
+ /// Sign a JWT (HS256) written directly into a caller buffer — the pooled
144
+ /// sibling of `castrum_jwt_sign_bytes` (no cstring round-trip; the caller
145
+ /// sizes from the claim JSON — same bound the cstring path uses). Returns
146
+ /// bytes written, the exact required size when `out_cap` is too small, or 0 on
147
+ /// invalid claims JSON.
148
+ ///
149
+ /// # Safety
150
+ /// `claims`/`secret` must be valid for reads of their lengths; `out` for
151
+ /// writes up to `out_cap`.
152
+ #[no_mangle]
153
+ pub unsafe extern "C" fn castrum_jwt_sign_bytes_into(
154
+ claims: *const u8,
155
+ clen: usize,
156
+ secret: *const u8,
157
+ slen: usize,
158
+ ttl: i64,
159
+ now: i64,
160
+ out: *mut u8,
161
+ out_cap: usize,
162
+ ) -> usize {
163
+ if claims.is_null() || secret.is_null() || out.is_null() {
164
+ return 0;
165
+ }
166
+ // Wrap in panic_guard: sonic parse + token build allocate — a panic must
167
+ // not unwind through the C ABI (process crash); it becomes 0 instead.
168
+ let token = panic_guard(
169
+ || {
170
+ let Ok(mut value) =
171
+ sonic_rs::from_slice::<sonic_rs::Value>(slice::from_raw_parts(claims, clen))
172
+ else {
173
+ return None;
174
+ };
175
+ let Ok(payload_b64) = crate::crypto::jwt::inject_and_payload_b64_sonic(
176
+ &mut value,
177
+ if ttl > 0 { Some(ttl) } else { None },
178
+ now,
179
+ ) else {
180
+ return None;
181
+ };
182
+ Some(crate::crypto::jwt::build_token(
183
+ crate::crypto::jwt::jwt_header_b64(),
184
+ &payload_b64,
185
+ slice::from_raw_parts(secret, slen),
186
+ ))
187
+ },
188
+ None,
189
+ );
190
+ let Some(token) = token else {
191
+ return 0;
192
+ };
193
+ if out_cap < token.len() {
194
+ // Needed-size convention: exact retry, no re-run.
195
+ return token.len();
196
+ }
197
+ let o = slice::from_raw_parts_mut(out, token.len());
198
+ o.copy_from_slice(&token);
199
+ token.len()
200
+ }
201
+
202
+ /// Verify an HS256 compact JWT → its claims as a null-terminated JSON C string
203
+ /// into the per-thread reused buffer (`cstring` return). `null` on invalid
204
+ /// signature / expired / malformed. This replaces the NAPI `jwtVerify` object
205
+ /// marshal (which builds a `serde_json::Value` + napi value on the verify
206
+ /// path) with a plain string crossing for the claims.
207
+ ///
208
+ /// # Safety
209
+ /// `token`/`secret` must be valid for reads of their lengths.
210
+ #[no_mangle]
211
+ pub unsafe extern "C" fn castrum_jwt_verify(
212
+ token: *const u8,
213
+ tlen: usize,
214
+ secret: *const u8,
215
+ slen: usize,
216
+ now: i64,
217
+ ) -> *const std::os::raw::c_char {
218
+ if token.is_null() || secret.is_null() {
219
+ return std::ptr::null();
220
+ }
221
+ let claims = panic_guard(
222
+ || {
223
+ crate::crypto::jwt::verify_token(
224
+ slice::from_raw_parts(token, tlen),
225
+ slice::from_raw_parts(secret, slen),
226
+ now,
227
+ )
228
+ },
229
+ None,
230
+ );
231
+ let Some(json) = claims else {
232
+ return std::ptr::null();
233
+ };
234
+ cstring_return(json.len(), |out| {
235
+ out[..json.len()].copy_from_slice(&json);
236
+ Some(json.len())
237
+ })
238
+ }
239
+
240
+ // ── Ed25519 / EdDSA JWT (RBAC auth) ────────────────────────────
241
+ // Ed25519 keypair generation + sign/verify for the EdDSA JWT path used by the
242
+ // auth module. Key formats: PKCS#8 v1 DER (private, 48 B) + SPKI DER (public,
243
+ // 44 B) — byte-identical to Node `crypto` exports, so the pure-TS fallback and
244
+ // the Rust addon produce the same `.env` keys.
245
+
246
+ /// Generate an Ed25519 keypair → packed `[u32 privLen][priv PKCS#8 v1 DER]
247
+ /// [u32 pubLen][pub SPKI DER]` (100 bytes total) written into `out`. Returns
248
+ /// bytes written, the exact required size when `out_cap` is too small, or 0
249
+ /// on CSPRNG failure.
250
+ ///
251
+ /// # Safety
252
+ /// `out` must be valid for writes up to `out_cap`.
253
+ #[no_mangle]
254
+ pub unsafe extern "C" fn castrum_ed25519_generate_keypair(out: *mut u8, out_cap: usize) -> usize {
255
+ if out.is_null() {
256
+ return 0;
257
+ }
258
+ let priv_len = crate::crypto::ed25519::ED25519_PKCS8_V1_LEN;
259
+ let pub_len = crate::crypto::ed25519::ED25519_SPKI_LEN;
260
+ let packed_len = 4 + priv_len + 4 + pub_len; // 100
261
+ if out_cap < packed_len {
262
+ // Needed-size convention: exact required size, no re-run.
263
+ return packed_len;
264
+ }
265
+ let result = panic_guard(crate::crypto::ed25519::generate_keypair, None);
266
+ let Some((private_der, public_der)) = result else {
267
+ return 0;
268
+ };
269
+ let o = slice::from_raw_parts_mut(out, packed_len);
270
+ o[0..4].copy_from_slice(&(private_der.len() as u32).to_le_bytes());
271
+ o[4..4 + private_der.len()].copy_from_slice(&private_der);
272
+ let pub_start = 4 + private_der.len();
273
+ o[pub_start..pub_start + 4].copy_from_slice(&(public_der.len() as u32).to_le_bytes());
274
+ o[pub_start + 4..packed_len].copy_from_slice(&public_der);
275
+ packed_len
276
+ }
277
+
278
+ /// Sign a message with an Ed25519 private key (PKCS#8 DER) → the 64-byte
279
+ /// signature written into `out`. Returns bytes written (64), the exact
280
+ /// required size when `out_cap` is too small, or 0 on invalid private key.
281
+ ///
282
+ /// # Safety
283
+ /// `key`/`msg` must be valid for reads of their lengths; `out` for writes up
284
+ /// to `out_cap`.
285
+ #[no_mangle]
286
+ pub unsafe extern "C" fn castrum_ed25519_sign(
287
+ key: *const u8,
288
+ klen: usize,
289
+ msg: *const u8,
290
+ mlen: usize,
291
+ out: *mut u8,
292
+ out_cap: usize,
293
+ ) -> usize {
294
+ if key.is_null() || msg.is_null() || out.is_null() {
295
+ return 0;
296
+ }
297
+ let sig_len = crate::crypto::ed25519::ED25519_SIGNATURE_LEN; // 64
298
+ if out_cap < sig_len {
299
+ return sig_len; // exact needed size
300
+ }
301
+ let sig = panic_guard(
302
+ || {
303
+ crate::crypto::ed25519::sign(
304
+ slice::from_raw_parts(key, klen),
305
+ slice::from_raw_parts(msg, mlen),
306
+ )
307
+ },
308
+ None,
309
+ );
310
+ let Some(sig) = sig else {
311
+ return 0; // invalid private key — real error
312
+ };
313
+ slice::from_raw_parts_mut(out, sig_len).copy_from_slice(&sig);
314
+ sig_len
315
+ }
316
+
317
+ /// Verify a 64-byte Ed25519 signature over `msg` with an SPKI DER (or raw
318
+ /// 32-byte) public key. Returns 1 (valid) or 0 (invalid).
319
+ ///
320
+ /// # Safety
321
+ /// `key`/`msg`/`sig` must be valid for reads of their lengths.
322
+ #[no_mangle]
323
+ pub unsafe extern "C" fn castrum_ed25519_verify(
324
+ key: *const u8,
325
+ klen: usize,
326
+ msg: *const u8,
327
+ mlen: usize,
328
+ sig: *const u8,
329
+ slen: usize,
330
+ ) -> u32 {
331
+ if key.is_null() || msg.is_null() || sig.is_null() {
332
+ return 0;
333
+ }
334
+ let valid = panic_guard(
335
+ || {
336
+ crate::crypto::ed25519::verify(
337
+ slice::from_raw_parts(key, klen),
338
+ slice::from_raw_parts(msg, mlen),
339
+ slice::from_raw_parts(sig, slen),
340
+ )
341
+ },
342
+ false,
343
+ );
344
+ valid as u32
345
+ }
346
+
347
+ /// Sign an EdDSA (Ed25519) JWT from pre-serialized claim JSON → compact token
348
+ /// as a null-terminated C string (cstring return — the engine clones it). `ttl
349
+ /// <= 0` means no `iat`/`exp` injection. `null` on invalid claims JSON /
350
+ /// invalid private key.
351
+ ///
352
+ /// # Safety
353
+ /// `claims`/`key` must be valid for reads of their lengths.
354
+ #[no_mangle]
355
+ pub unsafe extern "C" fn castrum_jwt_eddsa_sign(
356
+ claims: *const u8,
357
+ clen: usize,
358
+ key: *const u8,
359
+ klen: usize,
360
+ ttl: i64,
361
+ now: i64,
362
+ ) -> *const std::os::raw::c_char {
363
+ if claims.is_null() || key.is_null() {
364
+ return std::ptr::null();
365
+ }
366
+ let token = panic_guard(
367
+ || {
368
+ crate::crypto::jwt::sign_eddsa(
369
+ slice::from_raw_parts(claims, clen),
370
+ slice::from_raw_parts(key, klen),
371
+ if ttl > 0 { Some(ttl) } else { None },
372
+ now,
373
+ )
374
+ },
375
+ None,
376
+ );
377
+ let Some(token) = token else {
378
+ return std::ptr::null();
379
+ };
380
+ cstring_return(token.len(), |out| {
381
+ out[..token.len()].copy_from_slice(&token);
382
+ Some(token.len())
383
+ })
384
+ }
385
+
386
+ /// Verify an EdDSA (Ed25519) JWT → its claims as a null-terminated JSON C
387
+ /// string (cstring return). `null` on invalid signature / expired / malformed.
388
+ ///
389
+ /// # Safety
390
+ /// `token`/`key` must be valid for reads of their lengths.
391
+ #[no_mangle]
392
+ pub unsafe extern "C" fn castrum_jwt_eddsa_verify(
393
+ token: *const u8,
394
+ tlen: usize,
395
+ key: *const u8,
396
+ klen: usize,
397
+ now: i64,
398
+ ) -> *const std::os::raw::c_char {
399
+ if token.is_null() || key.is_null() {
400
+ return std::ptr::null();
401
+ }
402
+ let claims = panic_guard(
403
+ || {
404
+ crate::crypto::jwt::verify_token_eddsa(
405
+ slice::from_raw_parts(token, tlen),
406
+ slice::from_raw_parts(key, klen),
407
+ now,
408
+ )
409
+ },
410
+ None,
411
+ );
412
+ let Some(json) = claims else {
413
+ return std::ptr::null();
414
+ };
415
+ cstring_return(json.len(), |out| {
416
+ out[..json.len()].copy_from_slice(&json);
417
+ Some(json.len())
418
+ })
419
+ }