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,176 @@
1
+ // rust/ingress/options.rs — Ingress option types + request limits.
2
+ //
3
+ // JS-facing configuration structs (`#[napi(object)]`) plus the internal
4
+ // `Limits` defaults/validation. Kept separate from the pipeline so the
5
+ // constructor's marshaling stays close to the shapes it consumes.
6
+
7
+ use napi::bindgen_prelude::*;
8
+ use napi_derive::napi;
9
+
10
+ use crate::ingress::cors::CorsOptions;
11
+
12
+ #[napi(object)]
13
+ pub struct RateLimitOptions {
14
+ pub limit: Option<u32>,
15
+ pub window_ms: Option<u32>,
16
+ pub max_entries: Option<u32>,
17
+ }
18
+
19
+ #[napi(object)]
20
+ pub struct TrustedProxyOptions {
21
+ pub enabled: Option<bool>,
22
+ pub networks: Option<Vec<String>>,
23
+ }
24
+
25
+ #[napi(object)]
26
+ pub struct IngressLimitsOptions {
27
+ pub max_url_bytes: Option<u32>,
28
+ pub max_query_bytes: Option<u32>,
29
+ pub max_cookie_bytes: Option<u32>,
30
+ pub max_headers_bytes: Option<u32>,
31
+ pub max_headers: Option<u32>,
32
+ pub max_pairs: Option<u32>,
33
+ }
34
+
35
+ #[napi(object)]
36
+ pub struct IngressOptions {
37
+ pub trust_proxy: Option<bool>,
38
+ pub trusted_proxies: Option<TrustedProxyOptions>,
39
+ pub parse_cookies: Option<bool>,
40
+ pub parse_query: Option<bool>,
41
+ pub require_json_body: Option<bool>,
42
+ pub schema: Option<Uint8Array>,
43
+ pub cors: Option<CorsOptions>,
44
+ pub rate_limit: Option<RateLimitOptions>,
45
+ pub https: Option<bool>,
46
+ pub max_body_bytes: Option<u32>,
47
+ pub enable_body_size_guard: Option<bool>,
48
+ pub emit_metadata_json: Option<bool>,
49
+ pub limits: Option<IngressLimitsOptions>,
50
+ }
51
+
52
+ // ── Limits ────────────────────────────────────────────────────────
53
+ // Default per-request ingress limits (bytes / counts). These bound how much
54
+ // of a request the pipeline will buffer/parse, protecting against oversized
55
+ // URL/query/cookie/header blocks. Tunable via `limits` in the ingress options.
56
+ pub(crate) const DEFAULT_MAX_URL_BYTES: usize = 65536;
57
+ pub(crate) const DEFAULT_MAX_QUERY_BYTES: usize = 16384;
58
+ pub(crate) const DEFAULT_MAX_COOKIE_BYTES: usize = 8192;
59
+ pub(crate) const DEFAULT_MAX_HEADERS_BYTES: usize = 65536;
60
+ pub(crate) const DEFAULT_MAX_HEADERS: usize = 100;
61
+ pub(crate) const DEFAULT_MAX_PAIRS: usize = 1024;
62
+ pub(crate) const DEFAULT_MAX_BODY_BYTES: usize = 1_048_576;
63
+
64
+ #[derive(Clone)]
65
+ pub(crate) struct Limits {
66
+ pub max_url_bytes: usize,
67
+ pub max_query_bytes: usize,
68
+ pub max_cookie_bytes: usize,
69
+ pub max_headers_bytes: usize,
70
+ pub max_headers: usize,
71
+ pub max_pairs: usize,
72
+ }
73
+
74
+ impl Default for Limits {
75
+ fn default() -> Self {
76
+ Self {
77
+ max_url_bytes: DEFAULT_MAX_URL_BYTES,
78
+ max_query_bytes: DEFAULT_MAX_QUERY_BYTES,
79
+ max_cookie_bytes: DEFAULT_MAX_COOKIE_BYTES,
80
+ max_headers_bytes: DEFAULT_MAX_HEADERS_BYTES,
81
+ max_headers: DEFAULT_MAX_HEADERS,
82
+ max_pairs: DEFAULT_MAX_PAIRS,
83
+ }
84
+ }
85
+ }
86
+
87
+ impl Limits {
88
+ pub(crate) fn from_options(opts: Option<IngressLimitsOptions>) -> Self {
89
+ let d = Self::default();
90
+ let Some(o) = opts else { return d };
91
+
92
+ Self {
93
+ max_url_bytes: o
94
+ .max_url_bytes
95
+ .map(|v| v as usize)
96
+ .unwrap_or(d.max_url_bytes),
97
+ max_query_bytes: o
98
+ .max_query_bytes
99
+ .map(|v| v as usize)
100
+ .unwrap_or(d.max_query_bytes),
101
+ max_cookie_bytes: o
102
+ .max_cookie_bytes
103
+ .map(|v| v as usize)
104
+ .unwrap_or(d.max_cookie_bytes),
105
+ max_headers_bytes: o
106
+ .max_headers_bytes
107
+ .map(|v| v as usize)
108
+ .unwrap_or(d.max_headers_bytes),
109
+ max_headers: o.max_headers.map(|v| v as usize).unwrap_or(d.max_headers),
110
+ max_pairs: o.max_pairs.map(|v| v as usize).unwrap_or(d.max_pairs),
111
+ }
112
+ }
113
+ }
114
+
115
+ #[cfg(test)]
116
+ mod tests {
117
+ use super::*;
118
+
119
+ #[test]
120
+ fn limits_defaults() {
121
+ let d = Limits::default();
122
+ assert_eq!(d.max_url_bytes, DEFAULT_MAX_URL_BYTES);
123
+ assert_eq!(d.max_query_bytes, DEFAULT_MAX_QUERY_BYTES);
124
+ assert_eq!(d.max_cookie_bytes, DEFAULT_MAX_COOKIE_BYTES);
125
+ assert_eq!(d.max_headers_bytes, DEFAULT_MAX_HEADERS_BYTES);
126
+ assert_eq!(d.max_headers, DEFAULT_MAX_HEADERS);
127
+ assert_eq!(d.max_pairs, DEFAULT_MAX_PAIRS);
128
+ }
129
+
130
+ #[test]
131
+ fn limits_none_keeps_defaults() {
132
+ let l = Limits::from_options(None);
133
+ assert_eq!(l.max_url_bytes, DEFAULT_MAX_URL_BYTES);
134
+ assert_eq!(l.max_pairs, DEFAULT_MAX_PAIRS);
135
+ assert_eq!(l.max_headers, DEFAULT_MAX_HEADERS);
136
+ }
137
+
138
+ #[test]
139
+ fn limits_partial_override_merges() {
140
+ let opts = IngressLimitsOptions {
141
+ max_url_bytes: Some(4096),
142
+ max_query_bytes: None,
143
+ max_cookie_bytes: None,
144
+ max_headers_bytes: None,
145
+ max_headers: None,
146
+ max_pairs: None,
147
+ };
148
+ let l = Limits::from_options(Some(opts));
149
+ // Overridden field wins; untouched fields keep their defaults.
150
+ assert_eq!(l.max_url_bytes, 4096);
151
+ assert_eq!(l.max_query_bytes, DEFAULT_MAX_QUERY_BYTES);
152
+ assert_eq!(l.max_cookie_bytes, DEFAULT_MAX_COOKIE_BYTES);
153
+ assert_eq!(l.max_headers_bytes, DEFAULT_MAX_HEADERS_BYTES);
154
+ assert_eq!(l.max_headers, DEFAULT_MAX_HEADERS);
155
+ assert_eq!(l.max_pairs, DEFAULT_MAX_PAIRS);
156
+ }
157
+
158
+ #[test]
159
+ fn limits_full_override() {
160
+ let opts = IngressLimitsOptions {
161
+ max_url_bytes: Some(1),
162
+ max_query_bytes: Some(2),
163
+ max_cookie_bytes: Some(3),
164
+ max_headers_bytes: Some(4),
165
+ max_headers: Some(5),
166
+ max_pairs: Some(6),
167
+ };
168
+ let l = Limits::from_options(Some(opts));
169
+ assert_eq!(l.max_url_bytes, 1);
170
+ assert_eq!(l.max_query_bytes, 2);
171
+ assert_eq!(l.max_cookie_bytes, 3);
172
+ assert_eq!(l.max_headers_bytes, 4);
173
+ assert_eq!(l.max_headers, 5);
174
+ assert_eq!(l.max_pairs, 6);
175
+ }
176
+ }
@@ -0,0 +1,406 @@
1
+ // rust/ingress/output.rs — Ingress output buffer layout and low-level write helpers
2
+ // The canonical numeric source for the ingress binary output layout.
3
+ // `ingress_constants.rs` re-exports these values to JS via NAPI.
4
+
5
+ // ── Output buffer binary layout ───────────────────────────────────
6
+ pub const OUT_VERDICT: usize = 0;
7
+ pub const OUT_ERROR_CODE: usize = 1;
8
+ pub const OUT_STATUS: usize = 2;
9
+ pub const OUT_FLAGS: usize = 4;
10
+ pub const OUT_RATE_LIMIT: usize = 8;
11
+ pub const OUT_RATE_REMAINING: usize = 12;
12
+ pub const OUT_RATE_RESET: usize = 16;
13
+ pub const OUT_RETRY_AFTER: usize = 24;
14
+ pub const OUT_COOKIES_JSON_LEN: usize = 32;
15
+ pub const OUT_QUERY_JSON_LEN: usize = 36;
16
+ pub const OUT_HEADER_VARIANT: usize = 40;
17
+ pub const OUT_BODY_JSON_LEN: usize = 44;
18
+ pub const OUT_DATA_START: usize = 48;
19
+
20
+ /// Upper bound for the allocating `handle_request_full_sync` output buffer
21
+ /// (64 MiB) — a misconfigured huge `outputBufferSize` would otherwise allocate
22
+ /// gigabytes and OOM the process.
23
+ pub const MAX_OUTPUT_BUFFER_SIZE: u32 = 64 * 1024 * 1024;
24
+
25
+ // ── Flags ─────────────────────────────────────────────────────────
26
+ pub const FLAG_HAS_COOKIES: u32 = 1 << 0;
27
+ pub const FLAG_HAS_QUERY: u32 = 1 << 1;
28
+ pub const FLAG_BODY_VALID_JSON: u32 = 1 << 2;
29
+ pub const FLAG_SCHEMA_VALID: u32 = 1 << 3;
30
+ pub const FLAG_CORS_ALLOWED: u32 = 1 << 4;
31
+ pub const FLAG_IS_PREFLIGHT: u32 = 1 << 5;
32
+ pub const FLAG_RATE_LIMITED: u32 = 1 << 6;
33
+ pub const FLAG_HTTPS: u32 = 1 << 7;
34
+ pub const FLAG_TRUSTED_PROXY: u32 = 1 << 8;
35
+ pub const FLAG_BODY_TRUNCATED: u32 = 1 << 9;
36
+
37
+ // ── Header variant bits ───────────────────────────────────────────
38
+ pub const HV_JSON: u8 = 1 << 0;
39
+ pub const HV_CORS_SIMPLE: u8 = 1 << 1;
40
+ pub const HV_CORS_PREFLIGHT: u8 = 1 << 2;
41
+ pub const HV_RATE_ACTIVE: u8 = 1 << 3;
42
+ pub const HV_RATE_LIMITED: u8 = 1 << 4;
43
+ pub const HV_COUNT: u8 = 32;
44
+
45
+ // ── Error codes ───────────────────────────────────────────────────
46
+ pub const ERR_CODE_NONE: u8 = 0;
47
+ pub const ERR_CODE_CORS_PREFLIGHT: u8 = 1;
48
+ pub const ERR_CODE_RATE_LIMITED: u8 = 2;
49
+ pub const ERR_CODE_BODY_TOO_LARGE: u8 = 3;
50
+ pub const ERR_CODE_INVALID_JSON: u8 = 4;
51
+ pub const ERR_CODE_SCHEMA_VALIDATION: u8 = 5;
52
+ pub const ERR_CODE_BAD_REQUEST: u8 = 6;
53
+ pub const ERR_CODE_REQUEST_TOO_LARGE: u8 = 7;
54
+ pub const ERR_CODE_INTERNAL: u8 = 8;
55
+
56
+ // ── Output header writer ──────────────────────────────────────────
57
+
58
+ /// Compute the header variant byte from booleans.
59
+ #[inline(always)]
60
+ pub fn compute_header_variant(
61
+ cors_ok: bool,
62
+ is_preflight: bool,
63
+ rate_active: bool,
64
+ rate_limited: bool,
65
+ wants_json: bool,
66
+ ) -> u8 {
67
+ let mut hv: u8 = 0;
68
+ if wants_json {
69
+ hv |= HV_JSON;
70
+ }
71
+ if cors_ok && !is_preflight {
72
+ hv |= HV_CORS_SIMPLE;
73
+ }
74
+ if is_preflight {
75
+ hv |= HV_CORS_PREFLIGHT;
76
+ }
77
+ if rate_active {
78
+ hv |= HV_RATE_ACTIVE;
79
+ }
80
+ if rate_limited {
81
+ hv |= HV_RATE_LIMITED;
82
+ }
83
+ hv
84
+ }
85
+
86
+ // ── Aggregated header fields ──────────────────────────────────────
87
+
88
+ /// Rate-limit metadata threaded through the pipeline and written into the
89
+ /// output header. Bundled into one struct so call sites stay readable.
90
+ #[derive(Default, Clone, Copy)]
91
+ pub(crate) struct RateInfo {
92
+ pub limit: u32,
93
+ pub remaining: u32,
94
+ pub reset_ms: u64,
95
+ pub retry_after_ms: u64,
96
+ }
97
+
98
+ /// The full set of fields written into the 48-byte ingress output header.
99
+ ///
100
+ /// Using a struct (instead of 13 positional arguments) keeps call sites
101
+ /// readable and prevents argument-order mistakes.
102
+ #[derive(Clone, Copy)]
103
+ pub(crate) struct HeaderFields {
104
+ pub verdict: u8,
105
+ pub error_code: u8,
106
+ pub status: u16,
107
+ pub flags: u32,
108
+ pub rate: RateInfo,
109
+ pub cookies_json_len: u32,
110
+ pub query_json_len: u32,
111
+ pub header_variant: u8,
112
+ pub body_json_len: u32,
113
+ }
114
+
115
+ impl HeaderFields {
116
+ /// Build a successful response (verdict 0, status 200).
117
+ #[inline(always)]
118
+ pub fn ok(
119
+ flags: u32,
120
+ rate: RateInfo,
121
+ cookies_json_len: u32,
122
+ query_json_len: u32,
123
+ header_variant: u8,
124
+ body_json_len: u32,
125
+ ) -> Self {
126
+ Self {
127
+ verdict: 0,
128
+ error_code: ERR_CODE_NONE,
129
+ status: 200,
130
+ flags,
131
+ rate,
132
+ cookies_json_len,
133
+ query_json_len,
134
+ header_variant,
135
+ body_json_len,
136
+ }
137
+ }
138
+
139
+ /// Build a terminal error response.
140
+ #[inline(always)]
141
+ pub fn terminal(
142
+ verdict: u8,
143
+ error_code: u8,
144
+ status: u16,
145
+ flags: u32,
146
+ rate: RateInfo,
147
+ header_variant: u8,
148
+ ) -> Self {
149
+ Self {
150
+ verdict,
151
+ error_code,
152
+ status,
153
+ flags,
154
+ rate,
155
+ cookies_json_len: 0,
156
+ query_json_len: 0,
157
+ header_variant,
158
+ body_json_len: 0,
159
+ }
160
+ }
161
+
162
+ /// Serialize into the output buffer. Returns the total bytes written.
163
+ #[inline(always)]
164
+ pub fn write(self, out: &mut [u8]) -> usize {
165
+ write_output_header(
166
+ out,
167
+ self.verdict,
168
+ self.error_code,
169
+ self.status,
170
+ self.flags,
171
+ self.rate.limit,
172
+ self.rate.remaining,
173
+ self.rate.reset_ms,
174
+ self.rate.retry_after_ms,
175
+ self.cookies_json_len,
176
+ self.query_json_len,
177
+ self.header_variant,
178
+ self.body_json_len,
179
+ )
180
+ }
181
+ }
182
+
183
+ /// Write the output header into the buffer. Returns the total written bytes
184
+ /// (header + data payload sizes).
185
+ #[inline(always)]
186
+ /// Write the binary output header for an ingress decision.
187
+ ///
188
+ /// The napi-driven ingress layout legitimately carries many fields; this is
189
+ /// the single writer for the packed decision header.
190
+ #[allow(clippy::too_many_arguments)]
191
+ pub fn write_output_header(
192
+ out: &mut [u8],
193
+ verdict: u8,
194
+ error_code: u8,
195
+ status: u16,
196
+ flags: u32,
197
+ rate_limit: u32,
198
+ rate_remaining: u32,
199
+ rate_reset_ms: u64,
200
+ retry_after_ms: u64,
201
+ cookies_json_len: u32,
202
+ query_json_len: u32,
203
+ header_variant: u8,
204
+ body_json_len: u32,
205
+ ) -> usize {
206
+ // The pipeline only writes 200 (`HeaderFields::ok`) or terminal 4xx/5xx,
207
+ // so 101 (and any other out-of-range status) is clamped to 500.
208
+ let status = if (200u16..=599u16).contains(&status) {
209
+ status
210
+ } else {
211
+ 500
212
+ };
213
+
214
+ // Defense-in-depth: the writer stores directly into the header slots; a
215
+ // SINGLE upfront check (instead of per-field `write_u16/32/64` capacity
216
+ // asserts) requires the full header, so a too-small buffer still fails
217
+ // loudly (→ JS 500 via catch_unwind) but the hot path pays one bounds
218
+ // check for the whole 48-byte header instead of seven. The header layout
219
+ // is fixed and power-of-two aligned, so all stores below are in-bounds
220
+ // after the check.
221
+ assert!(
222
+ out.len() >= OUT_DATA_START,
223
+ "output buffer too small for the ingress decision header"
224
+ );
225
+
226
+ unsafe {
227
+ let p = out.as_mut_ptr();
228
+ // # Safety: `p` points into `out`, which has been verified to hold at
229
+ // least `OUT_DATA_START` bytes above; every offset written below is a
230
+ // compile-time constant within `[0, OUT_DATA_START)`, so all these
231
+ // stores are in-bounds. No other reference to `out` is live here.
232
+ *p.add(OUT_VERDICT) = verdict;
233
+ *p.add(OUT_ERROR_CODE) = error_code;
234
+ (p.add(OUT_STATUS) as *mut u16).write_unaligned(status);
235
+ (p.add(OUT_FLAGS) as *mut u32).write_unaligned(flags);
236
+ (p.add(OUT_RATE_LIMIT) as *mut u32).write_unaligned(rate_limit);
237
+ (p.add(OUT_RATE_REMAINING) as *mut u32).write_unaligned(rate_remaining);
238
+ (p.add(OUT_RATE_RESET) as *mut u64).write_unaligned(rate_reset_ms);
239
+ (p.add(OUT_RETRY_AFTER) as *mut u64).write_unaligned(retry_after_ms);
240
+ (p.add(OUT_COOKIES_JSON_LEN) as *mut u32).write_unaligned(cookies_json_len);
241
+ (p.add(OUT_QUERY_JSON_LEN) as *mut u32).write_unaligned(query_json_len);
242
+ *p.add(OUT_HEADER_VARIANT) = header_variant;
243
+ *p.add(OUT_HEADER_VARIANT + 1) = 0;
244
+ *p.add(OUT_HEADER_VARIANT + 2) = 0;
245
+ *p.add(OUT_HEADER_VARIANT + 3) = 0;
246
+ (p.add(OUT_BODY_JSON_LEN) as *mut u32).write_unaligned(body_json_len);
247
+ }
248
+
249
+ OUT_DATA_START + cookies_json_len as usize + query_json_len as usize + body_json_len as usize
250
+ }
251
+
252
+ #[cfg(test)]
253
+ mod tests {
254
+ use super::{
255
+ compute_header_variant, write_output_header, HV_CORS_PREFLIGHT, HV_CORS_SIMPLE, HV_JSON,
256
+ HV_RATE_ACTIVE, HV_RATE_LIMITED, OUT_BODY_JSON_LEN, OUT_COOKIES_JSON_LEN, OUT_DATA_START,
257
+ OUT_ERROR_CODE, OUT_FLAGS, OUT_HEADER_VARIANT, OUT_QUERY_JSON_LEN, OUT_RATE_LIMIT,
258
+ OUT_RATE_REMAINING, OUT_RATE_RESET, OUT_RETRY_AFTER, OUT_STATUS, OUT_VERDICT,
259
+ };
260
+
261
+ #[test]
262
+ fn output_compute_header_variant_bits() {
263
+ assert_eq!(
264
+ compute_header_variant(false, false, false, false, true),
265
+ HV_JSON
266
+ );
267
+ assert_eq!(
268
+ compute_header_variant(true, false, false, false, true),
269
+ HV_JSON | HV_CORS_SIMPLE
270
+ );
271
+ assert_eq!(
272
+ compute_header_variant(false, true, false, false, true),
273
+ HV_JSON | HV_CORS_PREFLIGHT
274
+ );
275
+ assert_eq!(
276
+ compute_header_variant(true, false, true, false, true),
277
+ HV_JSON | HV_CORS_SIMPLE | HV_RATE_ACTIVE
278
+ );
279
+ assert_eq!(
280
+ compute_header_variant(false, false, true, true, true),
281
+ HV_JSON | HV_RATE_ACTIVE | HV_RATE_LIMITED
282
+ );
283
+ }
284
+
285
+ #[test]
286
+ fn output_write_header_layout() {
287
+ let mut out = vec![0u8; 1024];
288
+ let written = write_output_header(
289
+ &mut out,
290
+ 0, // verdict
291
+ 3, // error_code
292
+ 200, // status
293
+ 0x1F, // flags
294
+ 100, // rate_limit
295
+ 50, // rate_remaining
296
+ 123_456, // rate_reset_ms
297
+ 5, // retry_after_ms
298
+ 10, // cookies_json_len
299
+ 20, // query_json_len
300
+ HV_JSON | HV_RATE_ACTIVE,
301
+ 30, // body_json_len
302
+ );
303
+
304
+ assert_eq!(out[OUT_VERDICT], 0);
305
+ assert_eq!(out[OUT_ERROR_CODE], 3);
306
+ assert_eq!(
307
+ u16::from_le_bytes([out[OUT_STATUS], out[OUT_STATUS + 1]]),
308
+ 200
309
+ );
310
+ assert_eq!(
311
+ u32::from_le_bytes([
312
+ out[OUT_FLAGS],
313
+ out[OUT_FLAGS + 1],
314
+ out[OUT_FLAGS + 2],
315
+ out[OUT_FLAGS + 3]
316
+ ]),
317
+ 0x1F
318
+ );
319
+ assert_eq!(
320
+ u32::from_le_bytes([
321
+ out[OUT_RATE_LIMIT],
322
+ out[OUT_RATE_LIMIT + 1],
323
+ out[OUT_RATE_LIMIT + 2],
324
+ out[OUT_RATE_LIMIT + 3]
325
+ ]),
326
+ 100
327
+ );
328
+ assert_eq!(
329
+ u32::from_le_bytes([
330
+ out[OUT_RATE_REMAINING],
331
+ out[OUT_RATE_REMAINING + 1],
332
+ out[OUT_RATE_REMAINING + 2],
333
+ out[OUT_RATE_REMAINING + 3]
334
+ ]),
335
+ 50
336
+ );
337
+ let reset = u64::from_le_bytes(out[OUT_RATE_RESET..OUT_RATE_RESET + 8].try_into().unwrap());
338
+ assert_eq!(reset, 123_456);
339
+ let retry = u64::from_le_bytes(
340
+ out[OUT_RETRY_AFTER..OUT_RETRY_AFTER + 8]
341
+ .try_into()
342
+ .unwrap(),
343
+ );
344
+ assert_eq!(retry, 5);
345
+ assert_eq!(
346
+ u32::from_le_bytes([
347
+ out[OUT_COOKIES_JSON_LEN],
348
+ out[OUT_COOKIES_JSON_LEN + 1],
349
+ out[OUT_COOKIES_JSON_LEN + 2],
350
+ out[OUT_COOKIES_JSON_LEN + 3]
351
+ ]),
352
+ 10
353
+ );
354
+ assert_eq!(
355
+ u32::from_le_bytes([
356
+ out[OUT_QUERY_JSON_LEN],
357
+ out[OUT_QUERY_JSON_LEN + 1],
358
+ out[OUT_QUERY_JSON_LEN + 2],
359
+ out[OUT_QUERY_JSON_LEN + 3]
360
+ ]),
361
+ 20
362
+ );
363
+ assert_eq!(out[OUT_HEADER_VARIANT], HV_JSON | HV_RATE_ACTIVE);
364
+ assert_eq!(
365
+ u32::from_le_bytes([
366
+ out[OUT_BODY_JSON_LEN],
367
+ out[OUT_BODY_JSON_LEN + 1],
368
+ out[OUT_BODY_JSON_LEN + 2],
369
+ out[OUT_BODY_JSON_LEN + 3]
370
+ ]),
371
+ 30
372
+ );
373
+
374
+ assert_eq!(written, OUT_DATA_START + 10 + 20 + 30);
375
+ }
376
+
377
+ #[test]
378
+ fn output_write_header_normalizes_invalid_status() {
379
+ let mut out = vec![0u8; 128];
380
+ let written = write_output_header(&mut out, 1, 6, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0);
381
+ let status = u16::from_le_bytes([out[OUT_STATUS], out[OUT_STATUS + 1]]);
382
+ assert_eq!(status, 500, "invalid status must be coerced to 500");
383
+ assert_eq!(written, OUT_DATA_START);
384
+ }
385
+
386
+ #[test]
387
+ fn output_write_header_clamps_out_of_range_status() {
388
+ // The pipeline only emits 200 (`HeaderFields::ok`) or terminal 4xx/5xx;
389
+ // out-of-range statuses (e.g. 101, 600) are intentionally clamped to 500
390
+ // (see output.rs write_output_header).
391
+ let mut out = vec![0u8; 128];
392
+ write_output_header(&mut out, 1, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0);
393
+ let status = u16::from_le_bytes([out[OUT_STATUS], out[OUT_STATUS + 1]]);
394
+ assert_eq!(status, 500);
395
+ }
396
+
397
+ #[test]
398
+ #[should_panic(expected = "output buffer too small")]
399
+ fn output_header_panics_on_undersized() {
400
+ // Enterprise guard: `write_output_header` self-checks the full 48-byte
401
+ // header up front so a miscalculated buffer becomes a clean panic (→ napi
402
+ // catch_unwind → JS 500) instead of a silent OOB write.
403
+ let mut out = vec![0u8; crate::ingress::output::OUT_DATA_START - 1];
404
+ crate::ingress::output::write_output_header(&mut out, 0, 0, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0);
405
+ }
406
+ }