castrum 0.9.6 → 0.9.8
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 +4 -3
- package/Cargo.toml +1 -1
- 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 +501 -5
- package/dist/index.js +1658 -1314
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/BENCHMARKS.md +13 -0
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +1 -1
- package/docs/FFI_BUN_GUIDE.md +13 -1
- package/docs/HOW_TO_ADD_AN_OP.md +24 -0
- package/docs/INGRESS.md +4 -4
- package/docs/REPO_MAP.md +8 -6
- package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
- package/index.ts +1 -0
- package/package.json +3 -2
- package/rust/crypto/aead.rs +44 -0
- package/rust/crypto/argon2.rs +32 -2
- package/rust/crypto/jwt/tests.rs +19 -0
- package/rust/crypto/jwt/token.rs +43 -28
- package/rust/crypto/session.rs +170 -7
- package/rust/ffi/crypto.rs +182 -61
- package/rust/ffi/hashing.rs +48 -33
- package/rust/ffi/http.rs +62 -36
- package/rust/ffi/metrics.rs +6 -2
- package/rust/ffi/payload.rs +47 -27
- package/rust/ffi/task.rs +51 -32
- package/rust/ffi/tests.rs +197 -0
- package/rust/ffi/util.rs +40 -0
- package/rust/http/headers.rs +13 -0
- package/rust/http/multipart.rs +139 -0
- package/rust/ingress/native_route.rs +21 -3
- package/rust/task/completion.rs +7 -0
- package/rust/task/mod.rs +1 -1
- package/rust/task/tests.rs +30 -0
- package/src/ingress/decode/baked-result.ts +13 -5
- package/src/ingress/decode/fast-result.ts +1 -1
- package/src/ingress/decode/result-base.ts +16 -2
- package/src/ingress/handlers.ts +103 -31
- package/src/ingress/headers/memoized-headers.ts +40 -0
- package/src/ingress/native-route.ts +25 -6
- package/src/ingress/options.ts +26 -2
- package/src/ingress/packing/gather-raw-headers.ts +24 -8
- package/src/ingress/response/baked-response.ts +118 -28
- package/src/ingress/router.ts +14 -2
- package/src/ingress/routes/common.ts +26 -10
- package/src/ingress/routes/echo.ts +50 -7
- package/src/ingress/routes/json-write.ts +21 -8
- package/src/ingress/routes/responder.ts +4 -4
- package/src/ingress/server-node.ts +33 -14
- package/src/ingress/server.ts +3 -4
- package/src/ingress/shared.ts +1 -1
- package/src/ingress/types.ts +15 -0
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs-selftest.ts +305 -0
- package/src/native/ffi/build/codecs.ts +0 -296
- package/src/native/ffi/build/instances-selftest.ts +264 -0
- package/src/native/ffi/build/instances.ts +39 -241
- package/src/native/ffi/build/metrics.ts +0 -7
- package/src/native/ffi/build/parse-selftest.ts +238 -0
- package/src/native/ffi/build/parse.ts +11 -234
- package/src/native/ffi/build.ts +5 -1
- package/src/native/ffi/selftest.ts +3 -3
- package/src/native/ffi/symbols.ts +460 -0
- package/src/native/ffi/types.ts +17 -0
- package/src/native/ffi.ts +2 -422
- package/src/native/ingress-binding.ts +39 -0
- package/src/runtime/codec.ts +24 -0
- package/src/runtime/types.ts +11 -0
- package/src/rust-ffi/batch/build.ts +18 -8
- package/src/rust-ffi/batch/types.ts +15 -0
- package/src/rust-ffi/options.ts +19 -0
- package/src/rust-ffi/scalar/crypto.ts +8 -0
- package/src/rust-ffi/scalar/factories.ts +47 -20
- package/src/rust-ffi/scalar/http.ts +60 -10
- package/src/rust-ffi/scalar/interface.ts +4 -1
- package/src/rust-ffi/scalar/payload.ts +27 -1
- package/src/shared/buffer-pool.ts +44 -9
- package/src/shared/codec.ts +26 -0
- package/src/shared/packed/parsers.ts +64 -7
- package/src/task/runtime.ts +29 -28
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -31,7 +31,7 @@ The bridge between TypeScript and Rust has **two transports**, both loading the
|
|
|
31
31
|
cdylib (built with napi-rs):
|
|
32
32
|
|
|
33
33
|
- **`bun:ffi` (PRIMARY on Bun)** — `src/native/ffi.ts` `dlopen`s the addon's
|
|
34
|
-
`extern "C"` exports (`rust/ffi/`,
|
|
34
|
+
`extern "C"` exports (`rust/ffi/`, 121 `castrum_*` symbols — 109 direct + 4
|
|
35
35
|
`validator_c_abi!` + 4 `validator_bytes_c_abi!` + 4 `compress_to_out!`,
|
|
36
36
|
parity guarded by
|
|
37
37
|
`test/unit/native/ffi-symbol-parity.test.ts`) and Bun JIT-calls them
|
package/docs/BENCHMARKS.md
CHANGED
|
@@ -162,6 +162,19 @@ Current medians (static path, 2000 connections, pipelining 1, single instance):
|
|
|
162
162
|
per-request `ReadableStream` construction is the cost, not the native body
|
|
163
163
|
write. Both servers default to copy mode; `INGRESS_ZERO_COPY=1` opts back in
|
|
164
164
|
for large-payload deployments.
|
|
165
|
+
- **Response construction dominates the JS-side cost — and is now memoized.**
|
|
166
|
+
A Bun CPU profile of the ingress route under load attributed ~80% of sampled
|
|
167
|
+
JS time to `new Response(...)` (the whole `run()` ~7%, the FFI pipeline call
|
|
168
|
+
~4%): passing an array-of-pairs makes Bun re-parse every header on every
|
|
169
|
+
response, measured **1832 ns vs 398 ns** for a persistent `Headers` instance
|
|
170
|
+
with the full security + CORS set. `buildSuccessInit` and the terminal
|
|
171
|
+
builders now pass a memoized `Headers`
|
|
172
|
+
(`src/ingress/headers/memoized-headers.ts`, keyed by the header array's
|
|
173
|
+
identity; the baked templates and per-origin cache return the same array
|
|
174
|
+
across requests, fresh per-request arrays are never retained). Interleaved
|
|
175
|
+
autocannon A/B (static `GET /api/users`, 8 workers, 2000 connections, three
|
|
176
|
+
independent runs): **+14.3% / +15.6% / +23.1% median RPS**, p50 −15%,
|
|
177
|
+
p99 −9…−24%, 0 errors, wire output byte-identical.
|
|
165
178
|
- Per-request cost breakdown (`bun bench/cost/router-cost.ts` +
|
|
166
179
|
`router-minimal-breakdown.ts`, min-of-5): the router's minimal route `run()`
|
|
167
180
|
is ~310ns (native pruned pipeline ~115ns + response/run machinery ~150ns +
|
package/docs/CASE_STUDY.md
CHANGED
|
@@ -44,7 +44,7 @@ as a pre-baked pipeline, and still be usable from both runtimes.
|
|
|
44
44
|
### 2.1 One cdylib, two transports
|
|
45
45
|
|
|
46
46
|
The addon is built with napi-rs (so Node can load it), but it *also* exports
|
|
47
|
-
`extern "C"` symbols (`rust/ffi/`, **
|
|
47
|
+
`extern "C"` symbols (`rust/ffi/`, **121 `castrum_*` symbols** — 109 direct + 4
|
|
48
48
|
`validator_c_abi!` + 4 `validator_bytes_c_abi!` + 4 `compress_to_out!`) so Bun can
|
|
49
49
|
`dlopen` it directly:
|
|
50
50
|
|
|
@@ -58,7 +58,7 @@ The addon is built with napi-rs (so Node can load it), but it *also* exports
|
|
|
58
58
|
|
|
59
59
|
```mermaid
|
|
60
60
|
flowchart LR
|
|
61
|
-
TS["TypeScript (index.ts)"] -->|"bun:ffi ·
|
|
61
|
+
TS["TypeScript (index.ts)"] -->|"bun:ffi · 121 C-ABI symbols · ~10–20 ns"| RUST["Rust cdylib (rust/ffi/)"]
|
|
62
62
|
TS -.->|"NAPI fallback (Node / self-test failure)"| RUST
|
|
63
63
|
RUST --> PIPELINE["8-stage ingress pipeline"]
|
|
64
64
|
TS --> PACKED["Packed, length-prefixed buffers (zero-copy)"]
|
package/docs/ENVIRONMENT.md
CHANGED
|
@@ -10,7 +10,7 @@ warned about and replaced with the documented default.
|
|
|
10
10
|
|----------|------|---------|-------------|
|
|
11
11
|
| `INGRESS_TRUST_PROXY` | bool | `false` | **Opt-in.** When `true`, honors `X-Forwarded-For` / `X-Real-IP` for client-IP resolution. Keep **off** unless the server sits behind a trusted edge that strips these headers, otherwise clients can spoof arbitrary IPs for IP-based rate limiting. |
|
|
12
12
|
| `INGRESS_REQUEST_ID_HEADER` | bool | `false` | Emit an `x-request-id` response header per request. The request ID is always generated internally; this only controls whether it is echoed to clients. |
|
|
13
|
-
| `INGRESS_ZERO_COPY` | bool | `false` | Enable zero-copy response bodies (legacy alias `INGRESS_UNSAFE_ZERO_COPY` still honored).
|
|
13
|
+
| `INGRESS_ZERO_COPY` | bool | `false` | Enable zero-copy response bodies (legacy alias `INGRESS_UNSAFE_ZERO_COPY` still honored). Off by default; when on, the handler serves a pooled output buffer via `pooledBodyResponse` (per-response output buffers are implemented — `src/shared/buffer-pool.ts`, `baked-response.ts::zeroCopyResponse`). Bound the borrowed set with `maxInFlight`. |
|
|
14
14
|
| `INGRESS_ZERO_COPY_MAX_IN_FLIGHT` | int | `128` | Max pooled output buffers kept in flight for zero-copy responses (min `1`). |
|
|
15
15
|
| `INGRESS_ZERO_COPY_TIMEOUT_MS` | int | `1000` | Timeout before a zero-copy pooled buffer is force-released (min `0`). |
|
|
16
16
|
| `INGRESS_OUTPUT_BUF_BYTES` | int | `131072` | Size of the Rust output buffer (min `65536`). If cookies + query + metadata JSON exceed this, `FLAG_BODY_TRUNCATED` is set and the request fails closed. |
|
package/docs/FFI_BUN_GUIDE.md
CHANGED
|
@@ -240,6 +240,18 @@ measuring the bare binding suggested the opposite. **Measure through the functio
|
|
|
240
240
|
users call, not the binding** — the guard itself costs ~4 ns, so guarding is
|
|
241
241
|
cheap and routing everything through bytes is not always a win.
|
|
242
242
|
|
|
243
|
+
**A second measured instance: the session seal/open siblings (0.9.7).** They
|
|
244
|
+
land in the same place, and the hinge is the caller's own encode. Through the
|
|
245
|
+
real call path, 200k calls, ns/call: one short-string `TextEncoder.encode`
|
|
246
|
+
costs 45.4; `session_seal` is 613.5 via `cstring` versus 557.8 via
|
|
247
|
+
`castrum_session_seal_bytes` when every argument is ALREADY bytes (−9.1%), but
|
|
248
|
+
620.6 when the caller re-encodes its JS strings per call (**+1.2%, slower**).
|
|
249
|
+
`session_open` mirrors it: 568.5 / 501.1 (−11.9%) / 579.5 (+1.9%). So ~45 ns of
|
|
250
|
+
encode against ~45–67 ns of transcode: the byte form pays only for a caller that
|
|
251
|
+
already holds bytes. Byte-identical tokens between the two forms was asserted at
|
|
252
|
+
runtime (`parity: true`), so the siblings are a correctness escape hatch — the
|
|
253
|
+
NUL case, or a caller that already owns buffers — never a blanket migration.
|
|
254
|
+
|
|
243
255
|
**Rule**: a `cstring` ARG is only correct when truncation cannot change the
|
|
244
256
|
answer — i.e. the string is developer-supplied config or NUL-free by
|
|
245
257
|
construction (a MIME extension, a base64 key, a header name). Anything
|
|
@@ -480,7 +492,7 @@ a type error, not a behavior change.
|
|
|
480
492
|
**Verify after any FFI change**:
|
|
481
493
|
|
|
482
494
|
```bash
|
|
483
|
-
nm -D --defined-only <addon> | grep -c castrum_ # = 109
|
|
495
|
+
nm -D --defined-only <addon> | grep -c castrum_ # = 121 (109 direct extern fns)
|
|
484
496
|
bun run bench:http:smoke # after touching decoders/handlers
|
|
485
497
|
bun test test/unit/native/ffi.test.ts # FFI↔napi parity + self-test
|
|
486
498
|
bun test test/unit/native/ffi-symbol-parity.test.ts
|
package/docs/HOW_TO_ADD_AN_OP.md
CHANGED
|
@@ -13,6 +13,30 @@ guards fail.
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
### ⚠️ Rebuild BOTH binaries before binding anything
|
|
17
|
+
|
|
18
|
+
`bun run build` writes the **baseline** addon only. The loader CPU-detects and
|
|
19
|
+
PREFERS the x86-64-v3 SIMD variant (`castrum.linux-x64-v3-gnu.node`, built by
|
|
20
|
+
`bun run build:v3`) on any AVX2 host — i.e. essentially every modern x86-64
|
|
21
|
+
machine, including CI runners.
|
|
22
|
+
|
|
23
|
+
A stale v3 binary therefore ships the OLD symbol table, and the failure mode is
|
|
24
|
+
**not** a clean "missing symbol" error: `bun:ffi` binds against it, the first call
|
|
25
|
+
jumps through a missing symbol, and the process **SIGSEGVs** — often at teardown,
|
|
26
|
+
with every test passing. Observed while adding
|
|
27
|
+
`castrum_session_{seal,open}_bytes`: `test/compat` printed `9 pass / 0 fail` and
|
|
28
|
+
then died, `gdb` showing the fault inside `castrum.linux-x64-v3-gnu.node`.
|
|
29
|
+
|
|
30
|
+
So: after adding or changing ANY `castrum_*` export, rebuild with
|
|
31
|
+
`bun run build:all` (baseline + v3). If you hit a segfault with no failing test,
|
|
32
|
+
check this FIRST — it is much cheaper than bisecting the ABI:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
nm -D --defined-only castrum.linux-x64-v3-gnu.node | grep <your_symbol>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
16
40
|
## 0. The six wiring points at a glance
|
|
17
41
|
|
|
18
42
|
| # | File | What you add |
|
package/docs/INGRESS.md
CHANGED
|
@@ -415,14 +415,14 @@ interface SyncIngressHandler {
|
|
|
415
415
|
|
|
416
416
|
| Option | Type | Default | Description |
|
|
417
417
|
|--------|------|---------|-------------|
|
|
418
|
-
| `trustProxy` | `boolean` | `false` | Trust `X-Forwarded-For` and `X-Real-IP` headers |
|
|
418
|
+
| `trustProxy` | `boolean` | `false` | Trust `X-Forwarded-For` and `X-Real-IP` headers. Duplicate `X-Forwarded-For` lines: only the **last** is read (warning in `rust/http/headers.rs`) |
|
|
419
419
|
| `trustedProxies` | `{ enabled, networks? }` | — | Fine-grained proxy trust with optional network whitelist |
|
|
420
420
|
| `parseCookies` | `boolean` | `false` | Parse `Cookie` header into structured JSON |
|
|
421
421
|
| `parseQuery` | `boolean` | `false` | Parse URL query string into structured JSON |
|
|
422
422
|
| `requireJsonBody` | `boolean` | `false` | Require body to be valid JSON |
|
|
423
423
|
| `schema` | `Uint8Array` | — | JSON Schema (as UTF-8 bytes) for body validation |
|
|
424
|
-
| `cors` | `CorsOptions` | — | CORS configuration |
|
|
425
|
-
| `rateLimit` | `{ limit, windowMs, maxEntries }` | — | Rate limiting configuration |
|
|
424
|
+
| `cors` | `CorsOptions` | — | CORS configuration. A present-but-empty `{}` (no `allowOrigin`) resolves to wildcard allow-all |
|
|
425
|
+
| `rateLimit` | `{ limit, windowMs, maxEntries }` | — | Rate limiting configuration. Without a `getIp` resolver every client shares ONE bucket (warns once at router construction) |
|
|
426
426
|
| `security` | `SecurityHeadersOptions` | — | Security headers configuration |
|
|
427
427
|
| `https` | `boolean` | — | Force HTTPS detection (overrides auto-detection) |
|
|
428
428
|
| `maxBodyBytes` | `number` | 1,048,576 | Maximum body size (1 MB) |
|
|
@@ -438,7 +438,7 @@ interface SyncIngressHandler {
|
|
|
438
438
|
|
|
439
439
|
| Option | Type | Default | Description |
|
|
440
440
|
|--------|------|---------|-------------|
|
|
441
|
-
| `allowOrigin` | `string[]` | — | Allowed origins
|
|
441
|
+
| `allowOrigin` | `string[]` | — | Allowed origins. Absent, empty, or `["*"]` = wildcard allow-all; list exact origins (or patterns) to narrow |
|
|
442
442
|
| `allowMethods` | `string[]` | `["GET", "HEAD", "POST"]` | Allowed HTTP methods |
|
|
443
443
|
| `allowHeaders` | `string[]` | — | Allowed request headers |
|
|
444
444
|
| `exposeHeaders` | `string[]` | — | Exposed response headers |
|
package/docs/REPO_MAP.md
CHANGED
|
@@ -37,11 +37,11 @@ Bun is the primary runtime; Node.js ≥20.3 is supported via a compiled ESM entr
|
|
|
37
37
|
| Build Rust addon (LOCAL max perf) | `bun run build:perf` | x86-64-v3 + AVX2 — **never for publish** |
|
|
38
38
|
| Build Rust addon (debug) | `bun run build:debug` | |
|
|
39
39
|
| Build compiled JS entry (Node) | `bun run build:js` | bundle + types → `dist/` (gitignored) |
|
|
40
|
-
| TS unit tests | `bun test` |
|
|
41
|
-
| Rust unit tests | `bun run test:rust` | `cargo test`,
|
|
40
|
+
| TS unit tests | `bun test test/unit test/property test/compat` | 838 tests (paths are explicit — a bare `bun test` pulls in the Node `.mjs` suites that need `dist/`) |
|
|
41
|
+
| Rust unit tests | `bun run test:rust` | `cargo test`, 628 tests |
|
|
42
42
|
| Node smoke tests | `bun run test:node` | explicit `.mjs` paths (Node 24 rejects dir arg) |
|
|
43
43
|
| Installed-tarball e2e | `bun run verify:install` | pack → install → import from `node_modules` |
|
|
44
|
-
| Typecheck | `bun run typecheck` | `bunx tsc --noEmit` (
|
|
44
|
+
| Typecheck | `bun run typecheck` | `bunx tsc --noEmit` (`index.ts`, `bench.ts`, `src/`, `bench/`) |
|
|
45
45
|
| CPU benchmark + correctness | `bun run check` | == `bun bench.ts`; writes `bench/results/cpu/` |
|
|
46
46
|
| HTTP bench (all servers) | `bun run bench:http` | |
|
|
47
47
|
| HTTP smoke (fast sanity) | `bun run bench:http:smoke` | **the wire-format guard** (CI-gated) |
|
|
@@ -131,7 +131,7 @@ src/
|
|
|
131
131
|
shared.ts JS constants + buildHeaderPlan + assertSyncCallback (shared by both paths).
|
|
132
132
|
constants.ts Binary-layout constants read from Rust at runtime (SINGLE SOURCE).
|
|
133
133
|
status.ts / errors.ts / options.ts / types.ts / body.ts / context.ts
|
|
134
|
-
packing/ header-packing.ts, input-packer.ts, gather-raw-headers.ts
|
|
134
|
+
packing/ header-packing.ts, input-packer.ts, gather-raw-headers.ts, select-headers.ts, scratch.ts
|
|
135
135
|
headers/ cors.ts, hsts.ts, security.ts (baked security merge), fast-templates.ts, baked-templates.ts
|
|
136
136
|
decode/ fast-result.ts, baked-result.ts (TWO decoders — see §4.2)
|
|
137
137
|
response/ terminal.ts, error-bodies.ts, baked-response.ts (pre-baked response builders)
|
|
@@ -178,7 +178,7 @@ rust/ ONE cdylib crate (Cargo [lib] → lib.rs).
|
|
|
178
178
|
CASTRUM_TASK_THREADS, batch dequeue + spin-then-park + optional
|
|
179
179
|
CASTRUM_TASK_PIN_CORES), result ring + batched thread-safe doorbell,
|
|
180
180
|
op dispatch + cancel + catch_unwind containment + `_into` ops.
|
|
181
|
-
ffi/ #[no_mangle] extern "C" exports (
|
|
181
|
+
ffi/ #[no_mangle] extern "C" exports (121 castrum_* symbols — 109 direct + 4
|
|
182
182
|
validator_c_abi! + 4 validator_bytes_c_abi! + 4 compress_to_out!; parity guarded by
|
|
183
183
|
test/unit/native/ffi-symbol-parity.test.ts) for Bun's bun:ffi
|
|
184
184
|
primary transport, incl. castrum_ingress_layout (the layout blob).
|
|
@@ -294,7 +294,9 @@ consumed.
|
|
|
294
294
|
- **Multi-platform (recommended)**: push a `v*` tag → `.github/workflows/ci.yml`
|
|
295
295
|
builds each platform addon, the `publish` job downloads them into
|
|
296
296
|
`./artifacts`, stages them, and runs `npm publish` (npm Trusted Publishing /
|
|
297
|
-
OIDC — no token
|
|
297
|
+
OIDC — preferred, no long-lived token; the job also targets the `NPM_TOKEN`
|
|
298
|
+
environment as a fallback secret. Needs the Trusted Publisher configured on
|
|
299
|
+
npmjs.com).
|
|
298
300
|
- **Manual single-platform**: `bun run release:manual`
|
|
299
301
|
(syncs package.json ↔ Cargo.toml ↔ CHANGELOG, tags, builds, publishes with
|
|
300
302
|
`CASTRUM_PUBLISH_ALLOW_PARTIAL=1`). `--dry-run` plans only.
|