castrum 0.7.0 → 0.9.0
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/LICENSE +21 -0
- package/README.md +322 -59
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +3161 -0
- package/dist/index.js +7306 -0
- package/docs/ARCHITECTURE.md +415 -0
- package/docs/BENCHMARKS.md +307 -0
- package/docs/ENVIRONMENT.md +79 -0
- package/docs/GETTING_STARTED.md +209 -0
- package/docs/INGRESS.md +745 -0
- package/docs/REPO_MAP.md +259 -0
- package/docs/adr/0001-two-wire-formats.md +39 -0
- package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
- package/docs/adr/0003-bun-builtins-delegation.md +42 -0
- package/docs/bun-builtins-decision-matrix.md +108 -0
- package/index.ts +51 -8
- package/package.json +73 -20
- package/src/data/json-rows.ts +13 -13
- package/src/ingress/body.ts +149 -0
- package/src/ingress/constants.ts +220 -48
- package/src/ingress/context.ts +136 -0
- package/src/ingress/decode/baked-result.ts +121 -0
- package/src/ingress/decode/fast-result.ts +160 -0
- package/src/ingress/decode/packed-sections.ts +56 -0
- package/src/ingress/decode/result-base.ts +172 -0
- package/src/ingress/errors.ts +72 -0
- package/src/ingress/fast.ts +152 -1021
- package/src/ingress/handlers.ts +563 -1001
- package/src/ingress/headers/baked-templates.ts +102 -0
- package/src/ingress/headers/cors.ts +44 -0
- package/src/ingress/headers/fast-templates.ts +238 -0
- package/src/ingress/headers/hsts.ts +83 -0
- package/src/ingress/health.ts +67 -0
- package/src/ingress/index.ts +130 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +75 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +150 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +96 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/routes/common.ts +38 -0
- package/src/ingress/routes/delete.ts +14 -0
- package/src/ingress/routes/echo.ts +107 -0
- package/src/ingress/routes/fallback.ts +23 -0
- package/src/ingress/routes/head.ts +37 -0
- package/src/ingress/routes/index.ts +10 -0
- package/src/ingress/routes/json-write.ts +153 -0
- package/src/ingress/routes/options.ts +36 -0
- package/src/ingress/routes/read.ts +45 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +443 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +157 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +217 -0
- package/src/integration/streaming.ts +79 -0
- package/src/integration/websocket.ts +92 -0
- package/src/loader/batch.ts +133 -0
- package/src/loader/cost.ts +104 -0
- package/src/loader/index.ts +930 -0
- package/src/loader/ops.ts +657 -0
- package/src/native/ffi.ts +1541 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +194 -0
- package/src/native/types.ts +598 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch.ts +431 -0
- package/src/rust-ffi/client.ts +88 -0
- package/src/rust-ffi/context.ts +164 -0
- package/src/rust-ffi/index.ts +40 -541
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +42 -0
- package/src/rust-ffi/scalar/crypto.ts +265 -0
- package/src/rust-ffi/scalar/factories.ts +233 -0
- package/src/rust-ffi/scalar/hashing.ts +72 -0
- package/src/rust-ffi/scalar/http.ts +206 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +450 -0
- package/src/rust-ffi/scalar/json.ts +45 -0
- package/src/rust-ffi/scalar/payload.ts +124 -0
- package/src/rust-ffi/text.ts +95 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +77 -0
- package/src/shared/adaptive.ts +68 -0
- package/src/shared/bench-classify.ts +209 -0
- package/src/shared/buffer-pool.ts +209 -0
- package/src/shared/bytes.ts +35 -5
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +74 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed.ts +350 -154
- package/src/shared/proven.ts +596 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +34 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +31 -0
- package/Cargo.toml +0 -73
- package/bench.ts +0 -3
- package/rust/batch.rs +0 -195
- package/rust/cookie_parser.rs +0 -70
- package/rust/cors.rs +0 -263
- package/rust/hashing.rs +0 -164
- package/rust/headers.rs +0 -234
- package/rust/hmac_sha256.rs +0 -113
- package/rust/http_parser.rs +0 -132
- package/rust/ingress.rs +0 -616
- package/rust/ingress_constants.rs +0 -89
- package/rust/ip_trust.rs +0 -185
- package/rust/json_ops.rs +0 -66
- package/rust/json_patch_ops.rs +0 -25
- package/rust/json_schema.rs +0 -151
- package/rust/json_ser.rs +0 -429
- package/rust/lib.rs +0 -48
- package/rust/method.rs +0 -141
- package/rust/mime_lookup.rs +0 -76
- package/rust/output.rs +0 -238
- package/rust/proxy.rs +0 -90
- package/rust/query_parser.rs +0 -141
- package/rust/random_token.rs +0 -52
- package/rust/rate_limit.rs +0 -203
- package/rust/terminal.rs +0 -95
- package/rust/unit_tests.rs +0 -843
- package/rust/url_codec.rs +0 -133
- package/rust/util.rs +0 -537
- package/rust/validation.rs +0 -107
- package/rust/websocket.rs +0 -23
- package/src/baseline/index.ts +0 -12
- package/src/baseline/tasks/cookie.ts +0 -22
- package/src/baseline/tasks/hashing.ts +0 -18
- package/src/baseline/tasks/hmac.ts +0 -38
- package/src/baseline/tasks/http.ts +0 -62
- package/src/baseline/tasks/json-patch.ts +0 -12
- package/src/baseline/tasks/json.ts +0 -50
- package/src/baseline/tasks/mime.ts +0 -5
- package/src/baseline/tasks/query.ts +0 -14
- package/src/baseline/tasks/token.ts +0 -15
- package/src/baseline/tasks/url.ts +0 -11
- package/src/baseline/tasks/validation.ts +0 -19
- package/src/baseline/tasks/websocket.ts +0 -14
- package/src/bench/assert.ts +0 -70
- package/src/bench/checks.ts +0 -227
- package/src/bench/checksum.ts +0 -27
- package/src/bench/comparisons.ts +0 -70
- package/src/bench/concurrent-worker.ts +0 -135
- package/src/bench/fixtures.ts +0 -233
- package/src/bench/index.ts +0 -2
- package/src/bench/measure.ts +0 -177
- package/src/bench/now.ts +0 -3
- package/src/bench/report.ts +0 -79
- package/src/bench/run.ts +0 -57
- package/src/bench/tasks/complex.ts +0 -232
- package/src/bench/tasks/concurrent.ts +0 -213
- package/src/bench/tasks/cookie.ts +0 -44
- package/src/bench/tasks/hashing.ts +0 -33
- package/src/bench/tasks/hmac.ts +0 -36
- package/src/bench/tasks/http.ts +0 -50
- package/src/bench/tasks/index.ts +0 -52
- package/src/bench/tasks/json-patch.ts +0 -21
- package/src/bench/tasks/json.ts +0 -33
- package/src/bench/tasks/mime.ts +0 -21
- package/src/bench/tasks/query.ts +0 -44
- package/src/bench/tasks/stress.ts +0 -142
- package/src/bench/tasks/token.ts +0 -20
- package/src/bench/tasks/url.ts +0 -34
- package/src/bench/tasks/validation.ts +0 -57
- package/src/bench/tasks/websocket.ts +0 -21
- package/src/bench/types.ts +0 -60
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "castrum",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Enterprise-grade Bun + Rust runtime: Rust-accelerated FFI primitives, a production-grade HTTP ingress pipeline, validation, hashing, and JSON processing with benchmark-grade performance.",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"main": "index.
|
|
8
|
-
"module": "index.
|
|
9
|
-
"types": "index.ts",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"keywords": [
|
|
12
12
|
"bun",
|
|
@@ -29,27 +29,58 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"exports": {
|
|
32
|
-
".":
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"bun": "./index.ts",
|
|
35
|
+
"node": "./dist/index.js",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
33
39
|
},
|
|
34
40
|
"files": [
|
|
35
41
|
"index.ts",
|
|
36
|
-
"
|
|
42
|
+
"dist",
|
|
37
43
|
"src",
|
|
38
|
-
"
|
|
39
|
-
"Cargo.toml",
|
|
44
|
+
"docs",
|
|
40
45
|
"README.md",
|
|
41
|
-
"*.node"
|
|
46
|
+
"*.node",
|
|
47
|
+
"!src/bench",
|
|
48
|
+
"!src/baseline"
|
|
42
49
|
],
|
|
43
50
|
"scripts": {
|
|
44
51
|
"build": "napi build --release --platform",
|
|
52
|
+
"build:perf": "bash scripts/build-perf.sh",
|
|
53
|
+
"build:js": "bun run build:js:bundle && bun run build:js:types",
|
|
54
|
+
"build:js:bundle": "bun build ./index.ts --target=node --format=esm --outdir=dist",
|
|
55
|
+
"build:js:types": "dts-bundle-generator -o dist/index.d.ts index.ts",
|
|
45
56
|
"bench": "bun run build && bun bench.ts",
|
|
46
57
|
"check": "bun bench.ts",
|
|
47
58
|
"test": "bun test",
|
|
59
|
+
"test:all": "bun test && bun run typecheck && bun run typecheck:test && bun run lint:ci && cargo test --lib",
|
|
60
|
+
"test:coverage": "node scripts/check-coverage.mjs",
|
|
61
|
+
"setup": "bun install && bun run build",
|
|
62
|
+
"test:node": "node --test test/integration/node-smoke.test.mjs test/integration/node-enterprise.test.mjs",
|
|
63
|
+
"verify:install": "node scripts/verify-install.mjs",
|
|
48
64
|
"build:debug": "napi build --platform",
|
|
49
|
-
"
|
|
65
|
+
"lint": "bunx biome check .",
|
|
66
|
+
"lint:fix": "bunx biome check --write .",
|
|
67
|
+
"lint:ci": "bunx biome check . --formatter-enabled=false --assist-enabled=false --error-on-warnings",
|
|
68
|
+
"format": "bunx biome format --write .",
|
|
69
|
+
"typecheck": "bunx tsc --noEmit",
|
|
70
|
+
"typecheck:test": "bunx tsc --noEmit -p tsconfig.test.json",
|
|
71
|
+
"test:rust": "cargo test",
|
|
72
|
+
"deny": "cargo deny check",
|
|
73
|
+
"check:version": "bun scripts/check-version.mjs",
|
|
74
|
+
"check:jsdoc": "bun scripts/check-jsdoc.ts",
|
|
75
|
+
"audit": "bun audit",
|
|
50
76
|
"publish:manual": "node scripts/publish-manual.mjs",
|
|
51
77
|
"publish:manual:dry": "node scripts/publish-manual.mjs --dry-run",
|
|
52
|
-
"prepublishOnly": "node scripts/prepublish.mjs",
|
|
78
|
+
"prepublishOnly": "bun run build:js && node scripts/prepublish.mjs",
|
|
79
|
+
"check:proven": "bun scripts/check-proven.ts",
|
|
80
|
+
"check:proven:fail": "bun scripts/check-proven.ts --fail",
|
|
81
|
+
"check:annotate": "bun scripts/annotate-performance.ts",
|
|
82
|
+
"check:annotate:dry": "bun scripts/annotate-performance.ts --dry-run",
|
|
83
|
+
"check:docs": "bun run check && bun run check:annotate && bun run check:proven",
|
|
53
84
|
"bench:http": "bun run bench/run-bench.ts",
|
|
54
85
|
"bench:http:smoke": "bun run bench/run-bench.ts smoke",
|
|
55
86
|
"bench:http:stress": "bun run bench/run-bench.ts stress",
|
|
@@ -62,13 +93,22 @@
|
|
|
62
93
|
"bench:http:elysia": "bun run bench/run-bench.ts '' elysia",
|
|
63
94
|
"bench:http:ingress": "bun run bench/run-bench.ts '' ingress",
|
|
64
95
|
"bench:http:bun-only": "bun run bench/run-bench.ts '' bun",
|
|
65
|
-
"bench:http:all-heavy": "bun run bench/run-bench.ts 1"
|
|
96
|
+
"bench:http:all-heavy": "bun run bench/run-bench.ts 1",
|
|
97
|
+
"bench:startup": "bun run bench/startup.ts",
|
|
98
|
+
"bench:ffi": "bun run bench/ffi-all.ts",
|
|
99
|
+
"bench:ffi:load": "bun run bench/ffi-load.ts",
|
|
100
|
+
"bench:ffi:public": "bun run bench/ffi-public.ts",
|
|
101
|
+
"bench:ffi:workers": "bun run bench/ffi-workers.ts",
|
|
102
|
+
"verify:native:batch": "bun run scripts/verify-native-batch.ts"
|
|
66
103
|
},
|
|
67
|
-
"dependencies": {
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
104
|
+
"dependencies": {},
|
|
105
|
+
"peerDependencies": {
|
|
106
|
+
"@types/node": ">=20"
|
|
107
|
+
},
|
|
108
|
+
"peerDependenciesMeta": {
|
|
109
|
+
"@types/node": {
|
|
110
|
+
"optional": true
|
|
111
|
+
}
|
|
72
112
|
},
|
|
73
113
|
"devDependencies": {
|
|
74
114
|
"@elysia/cors": "^1.4.2",
|
|
@@ -76,19 +116,32 @@
|
|
|
76
116
|
"@types/bun": "^1.3.14",
|
|
77
117
|
"@types/mime-types": "^3.0.1",
|
|
78
118
|
"@types/validator": "^13.15.10",
|
|
79
|
-
"
|
|
119
|
+
"ajv": "^8.20.0",
|
|
120
|
+
"crc-32": "^1.2.2",
|
|
121
|
+
"dts-bundle-generator": "^9.5.1",
|
|
122
|
+
"elysia": "^1.4.0",
|
|
123
|
+
"fast-json-patch": "^3.1.1",
|
|
124
|
+
"mime-types": "^3.0.2",
|
|
125
|
+
"typescript": "5.9.3",
|
|
126
|
+
"validator": "^13.15.35"
|
|
127
|
+
},
|
|
128
|
+
"overrides": {
|
|
129
|
+
"typescript": "5.9.3"
|
|
80
130
|
},
|
|
81
131
|
"engines": {
|
|
82
|
-
"bun": ">=1.1.0"
|
|
132
|
+
"bun": ">=1.1.0",
|
|
133
|
+
"node": ">=20.3"
|
|
83
134
|
},
|
|
84
135
|
"napi": {
|
|
85
136
|
"binaryName": "castrum",
|
|
86
137
|
"targets": [
|
|
87
138
|
"aarch64-apple-darwin",
|
|
88
139
|
"aarch64-unknown-linux-gnu",
|
|
140
|
+
"aarch64-unknown-linux-musl",
|
|
89
141
|
"x86_64-apple-darwin",
|
|
90
142
|
"x86_64-pc-windows-msvc",
|
|
91
|
-
"x86_64-unknown-linux-gnu"
|
|
143
|
+
"x86_64-unknown-linux-gnu",
|
|
144
|
+
"x86_64-unknown-linux-musl"
|
|
92
145
|
]
|
|
93
146
|
}
|
|
94
147
|
}
|
package/src/data/json-rows.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { encoder } from
|
|
1
|
+
import { encoder } from '../shared/bytes'
|
|
2
2
|
|
|
3
3
|
export interface JsonRowNested {
|
|
4
|
-
version: number
|
|
5
|
-
createdAt: string
|
|
4
|
+
version: number
|
|
5
|
+
createdAt: string
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface JsonRow {
|
|
9
|
-
id: number
|
|
10
|
-
name: string
|
|
11
|
-
active: boolean
|
|
12
|
-
score: number
|
|
13
|
-
tags: string[]
|
|
14
|
-
nested: JsonRowNested
|
|
9
|
+
id: number
|
|
10
|
+
name: string
|
|
11
|
+
active: boolean
|
|
12
|
+
score: number
|
|
13
|
+
tags: string[]
|
|
14
|
+
nested: JsonRowNested
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function createJsonRows(rows: number): JsonRow[] {
|
|
@@ -20,14 +20,14 @@ export function createJsonRows(rows: number): JsonRow[] {
|
|
|
20
20
|
name: `user_${i}`,
|
|
21
21
|
active: i % 2 === 0,
|
|
22
22
|
score: i * 1.25,
|
|
23
|
-
tags: [
|
|
23
|
+
tags: ['alpha', 'beta', 'gamma'],
|
|
24
24
|
nested: {
|
|
25
25
|
version: i % 10,
|
|
26
|
-
createdAt:
|
|
26
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
27
27
|
},
|
|
28
|
-
}))
|
|
28
|
+
}))
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function jsonRowsBytes(rows: number): Uint8Array {
|
|
32
|
-
return encoder.encode(JSON.stringify(createJsonRows(rows)))
|
|
32
|
+
return encoder.encode(JSON.stringify(createJsonRows(rows)))
|
|
33
33
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/ingress/body.ts — Streaming request-body reading for the async ingress
|
|
2
|
+
// API.
|
|
3
|
+
|
|
4
|
+
const EMPTY_BODY = new Uint8Array(0)
|
|
5
|
+
|
|
6
|
+
/** Read the request body once, enforcing the size guard when enabled. */
|
|
7
|
+
export function readRequestBodyOnce(
|
|
8
|
+
req: Request,
|
|
9
|
+
maxBytes: number,
|
|
10
|
+
guard: boolean,
|
|
11
|
+
timeoutMs?: number,
|
|
12
|
+
): Promise<Uint8Array> {
|
|
13
|
+
if (req.body === null) {
|
|
14
|
+
return Promise.resolve(EMPTY_BODY)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return readBodyWithLimit(req, maxBytes, guard, timeoutMs)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function bodyTooLargeError(): Error & { code?: string } {
|
|
21
|
+
const err = new Error('BODY_TOO_LARGE') as Error & { code?: string }
|
|
22
|
+
err.code = 'BODY_TOO_LARGE'
|
|
23
|
+
return err
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Stream-read the request body, enforcing the size guard when enabled and an
|
|
28
|
+
* optional overall deadline.
|
|
29
|
+
*
|
|
30
|
+
* Errors carry `err.code`:
|
|
31
|
+
* - `BODY_TOO_LARGE` when `guard` is on and the body exceeds `maxBytes`
|
|
32
|
+
* (thrown as soon as the limit is crossed — the body is never fully
|
|
33
|
+
* buffered first, which is the slowloris/large-body protection).
|
|
34
|
+
* - `REQUEST_TIMEOUT` when `timeoutMs` elapses before the body completes.
|
|
35
|
+
*/
|
|
36
|
+
export async function readBodyWithLimit(
|
|
37
|
+
req: Request,
|
|
38
|
+
maxBytes: number,
|
|
39
|
+
guard: boolean,
|
|
40
|
+
timeoutMs?: number,
|
|
41
|
+
): Promise<Uint8Array> {
|
|
42
|
+
const body = req.body
|
|
43
|
+
|
|
44
|
+
if (!body) {
|
|
45
|
+
return EMPTY_BODY
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const deadline = timeoutMs && timeoutMs > 0 ? timeoutMs : 0
|
|
49
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
50
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
51
|
+
if (deadline <= 0) return
|
|
52
|
+
timer = setTimeout(() => reject(timeoutError()), deadline)
|
|
53
|
+
// Don't let a pending body timeout keep the event loop alive once the
|
|
54
|
+
// request has otherwise completed (Node; a no-op under Bun).
|
|
55
|
+
timer.unref?.()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Fast path: a declared Content-Length lets us PROVE the body fits the
|
|
59
|
+
// guard before reading, so one native `arrayBuffer()` read replaces the
|
|
60
|
+
// reader + per-chunk `Promise.race` + `concatUint8Arrays` churn (the bench
|
|
61
|
+
// POSTs are single-shot bodies — this removes ~4-6 allocations per write
|
|
62
|
+
// request). The deadline (when set) is still enforced with a single race,
|
|
63
|
+
// not one per chunk. `req.arrayBuffer()` is bounded by the server's
|
|
64
|
+
// `maxRequestBodySize`, and the post-read length re-check catches a client
|
|
65
|
+
// that lies about Content-Length.
|
|
66
|
+
const declared = req.headers.get('content-length')
|
|
67
|
+
if (declared !== null) {
|
|
68
|
+
const declaredLen = Number(declared)
|
|
69
|
+
if (Number.isFinite(declaredLen)) {
|
|
70
|
+
if (guard && declaredLen > maxBytes) {
|
|
71
|
+
throw bodyTooLargeError()
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const buf =
|
|
75
|
+
deadline > 0 ? await Promise.race([req.arrayBuffer(), timeout]) : await req.arrayBuffer()
|
|
76
|
+
const bytes = new Uint8Array(buf)
|
|
77
|
+
if (guard && bytes.byteLength > maxBytes) {
|
|
78
|
+
throw bodyTooLargeError()
|
|
79
|
+
}
|
|
80
|
+
return bytes
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(timer)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const reader = body.getReader()
|
|
88
|
+
const chunks: Uint8Array[] = []
|
|
89
|
+
let total = 0
|
|
90
|
+
// True when the stream was fully read to `done` — on that path the reader is
|
|
91
|
+
// already closed, so the `cancel()` in `finally` is a wasted async round-trip
|
|
92
|
+
// per request. It is still required when the loop exits early (timeout/error).
|
|
93
|
+
let completed = false
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
for (;;) {
|
|
97
|
+
const { done, value } =
|
|
98
|
+
deadline > 0 ? await Promise.race([reader.read(), timeout]) : await reader.read()
|
|
99
|
+
|
|
100
|
+
if (done) {
|
|
101
|
+
completed = true
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
if (!value) continue
|
|
105
|
+
|
|
106
|
+
total += value.byteLength
|
|
107
|
+
|
|
108
|
+
if (guard && total > maxBytes) {
|
|
109
|
+
throw bodyTooLargeError()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
chunks.push(value)
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
clearTimeout(timer)
|
|
116
|
+
// Release the underlying stream. Cancelling while a read() is still
|
|
117
|
+
// pending (the timeout case) can resolve that pending read() as `done`
|
|
118
|
+
// and RACE the timeout rejection — so reject first, cancel only AFTER
|
|
119
|
+
// the loop has settled. The `.catch` keeps the cancel from masking a
|
|
120
|
+
// thrown REQUEST_TIMEOUT / BODY_TOO_LARGE. On the normal `done` path the
|
|
121
|
+
// stream is already closed, so skip the cancel entirely.
|
|
122
|
+
if (!completed) {
|
|
123
|
+
await reader.cancel().catch(() => {})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return concatUint8Arrays(chunks, total)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function timeoutError(): Error & { code?: string } {
|
|
131
|
+
const err = new Error('REQUEST_TIMEOUT') as Error & { code?: string }
|
|
132
|
+
err.code = 'REQUEST_TIMEOUT'
|
|
133
|
+
return err
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function concatUint8Arrays(chunks: Uint8Array[], total: number): Uint8Array {
|
|
137
|
+
if (chunks.length === 0) return EMPTY_BODY
|
|
138
|
+
if (chunks.length === 1) return chunks[0] ?? EMPTY_BODY
|
|
139
|
+
|
|
140
|
+
const out = new Uint8Array(total)
|
|
141
|
+
let offset = 0
|
|
142
|
+
|
|
143
|
+
for (const chunk of chunks) {
|
|
144
|
+
out.set(chunk, offset)
|
|
145
|
+
offset += chunk.byteLength
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return out
|
|
149
|
+
}
|
package/src/ingress/constants.ts
CHANGED
|
@@ -1,56 +1,228 @@
|
|
|
1
1
|
// src/ingress/constants.ts — SINGLE SOURCE OF TRUTH for ingress binary layout constants
|
|
2
|
-
// These are read from
|
|
2
|
+
// These are read from Rust to eliminate duplication across:
|
|
3
3
|
// - src/ingress/index.ts
|
|
4
4
|
// - bench/servers/ingress-server.ts
|
|
5
5
|
// - Any future consumers
|
|
6
6
|
//
|
|
7
|
-
// IMPORTANT: Do NOT hardcode these values. They come from Rust's
|
|
8
|
-
//
|
|
7
|
+
// IMPORTANT: Do NOT hardcode these values. They come from Rust's
|
|
8
|
+
// `ingress/output.rs` (the single numeric source), projected to JS via either
|
|
9
|
+
// `ingress_constants.rs` (NAPI) or the C-ABI layout blob `rust/ffi.rs`
|
|
10
|
+
// `IngressLayout` (`castrum_ingress_layout`). Both read the same numbers, so
|
|
11
|
+
// they cannot drift.
|
|
9
12
|
|
|
10
|
-
import
|
|
13
|
+
import { getAddon } from '../native'
|
|
14
|
+
import { getBunFFI, type BunFFI } from '../native/ffi'
|
|
15
|
+
|
|
16
|
+
// Transport: bun:ffi is PRIMARY on Bun, so the layout is read through the
|
|
17
|
+
// C-ABI blob WITHOUT dlopening the napi addon. Node — or forced
|
|
18
|
+
// `CASTRUM_FFI_MODE=napi`, a missing symbol, or a failed bind-time self-test —
|
|
19
|
+
// falls back to the napi addon. This is the ONE module that touches native at
|
|
20
|
+
// import time; everything else in the package stays lazy.
|
|
21
|
+
const ffi = getBunFFI()
|
|
22
|
+
|
|
23
|
+
// Slot order mirrors the `#[repr(C)] IngressLayout` struct in rust/ffi.rs
|
|
24
|
+
// (38 × u32 LE). Drift is guarded by the Rust unit test
|
|
25
|
+
// `ingress_layout_c_abi_matches_output_source` and by the pinned values in the
|
|
26
|
+
// bun:ffi bind-time self-test (a reorder → self-test fails → napi fallback).
|
|
27
|
+
const SLOT = {
|
|
28
|
+
OUT_VERDICT: 0,
|
|
29
|
+
OUT_ERROR_CODE: 1,
|
|
30
|
+
OUT_STATUS: 2,
|
|
31
|
+
OUT_FLAGS: 3,
|
|
32
|
+
OUT_RATE_LIMIT: 4,
|
|
33
|
+
OUT_RATE_REMAINING: 5,
|
|
34
|
+
OUT_RATE_RESET: 6,
|
|
35
|
+
OUT_RETRY_AFTER: 7,
|
|
36
|
+
OUT_COOKIES_JSON_LEN: 8,
|
|
37
|
+
OUT_QUERY_JSON_LEN: 9,
|
|
38
|
+
OUT_HEADER_VARIANT: 10,
|
|
39
|
+
OUT_BODY_JSON_LEN: 11,
|
|
40
|
+
OUT_DATA_START: 12,
|
|
41
|
+
FLAG_HAS_COOKIES: 13,
|
|
42
|
+
FLAG_HAS_QUERY: 14,
|
|
43
|
+
FLAG_BODY_VALID_JSON: 15,
|
|
44
|
+
FLAG_SCHEMA_VALID: 16,
|
|
45
|
+
FLAG_CORS_ALLOWED: 17,
|
|
46
|
+
FLAG_IS_PREFLIGHT: 18,
|
|
47
|
+
FLAG_RATE_LIMITED: 19,
|
|
48
|
+
FLAG_HTTPS: 20,
|
|
49
|
+
FLAG_TRUSTED_PROXY: 21,
|
|
50
|
+
FLAG_BODY_TRUNCATED: 22,
|
|
51
|
+
HV_JSON: 23,
|
|
52
|
+
HV_CORS_SIMPLE: 24,
|
|
53
|
+
HV_CORS_PREFLIGHT: 25,
|
|
54
|
+
HV_RATE_ACTIVE: 26,
|
|
55
|
+
HV_RATE_LIMITED: 27,
|
|
56
|
+
HV_COUNT: 28,
|
|
57
|
+
ERR_NONE: 29,
|
|
58
|
+
ERR_CORS_PREFLIGHT: 30,
|
|
59
|
+
ERR_RATE_LIMITED: 31,
|
|
60
|
+
ERR_BODY_TOO_LARGE: 32,
|
|
61
|
+
ERR_INVALID_JSON: 33,
|
|
62
|
+
ERR_SCHEMA_VALIDATION: 34,
|
|
63
|
+
ERR_BAD_REQUEST: 35,
|
|
64
|
+
ERR_REQUEST_TOO_LARGE: 36,
|
|
65
|
+
ERR_INTERNAL: 37,
|
|
66
|
+
} as const
|
|
67
|
+
|
|
68
|
+
type Layout = Record<keyof typeof SLOT, number>
|
|
69
|
+
|
|
70
|
+
/** Read the layout via the bun:ffi C-ABI blob (Bun primary path). */
|
|
71
|
+
function readLayoutViaFfi(ffiLive: BunFFI): Layout {
|
|
72
|
+
const buf = new Uint8Array(38 * 4)
|
|
73
|
+
ffiLive.ingressLayout(buf)
|
|
74
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
75
|
+
const out = {} as Layout
|
|
76
|
+
for (const key of Object.keys(SLOT) as (keyof typeof SLOT)[]) {
|
|
77
|
+
out[key] = view.getUint32(SLOT[key] * 4, true)
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read the layout via the napi addon (Node / fallback). */
|
|
83
|
+
function readLayoutViaNapi(): Layout {
|
|
84
|
+
const addon = getAddon()
|
|
85
|
+
return {
|
|
86
|
+
OUT_VERDICT: addon.INGRESS_OUT_VERDICT,
|
|
87
|
+
OUT_ERROR_CODE: addon.INGRESS_OUT_ERROR_CODE,
|
|
88
|
+
OUT_STATUS: addon.INGRESS_OUT_STATUS,
|
|
89
|
+
OUT_FLAGS: addon.INGRESS_OUT_FLAGS,
|
|
90
|
+
OUT_RATE_LIMIT: addon.INGRESS_OUT_RATE_LIMIT,
|
|
91
|
+
OUT_RATE_REMAINING: addon.INGRESS_OUT_RATE_REMAINING,
|
|
92
|
+
OUT_RATE_RESET: addon.INGRESS_OUT_RATE_RESET,
|
|
93
|
+
OUT_RETRY_AFTER: addon.INGRESS_OUT_RETRY_AFTER,
|
|
94
|
+
OUT_COOKIES_JSON_LEN: addon.INGRESS_OUT_COOKIES_JSON_LEN,
|
|
95
|
+
OUT_QUERY_JSON_LEN: addon.INGRESS_OUT_QUERY_JSON_LEN,
|
|
96
|
+
OUT_HEADER_VARIANT: addon.INGRESS_OUT_HEADER_VARIANT,
|
|
97
|
+
OUT_BODY_JSON_LEN: addon.INGRESS_OUT_BODY_JSON_LEN,
|
|
98
|
+
OUT_DATA_START: addon.INGRESS_OUT_DATA_START,
|
|
99
|
+
FLAG_HAS_COOKIES: addon.INGRESS_FLAG_HAS_COOKIES,
|
|
100
|
+
FLAG_HAS_QUERY: addon.INGRESS_FLAG_HAS_QUERY,
|
|
101
|
+
FLAG_BODY_VALID_JSON: addon.INGRESS_FLAG_BODY_VALID_JSON,
|
|
102
|
+
FLAG_SCHEMA_VALID: addon.INGRESS_FLAG_SCHEMA_VALID,
|
|
103
|
+
FLAG_CORS_ALLOWED: addon.INGRESS_FLAG_CORS_ALLOWED,
|
|
104
|
+
FLAG_IS_PREFLIGHT: addon.INGRESS_FLAG_IS_PREFLIGHT,
|
|
105
|
+
FLAG_RATE_LIMITED: addon.INGRESS_FLAG_RATE_LIMITED,
|
|
106
|
+
FLAG_HTTPS: addon.INGRESS_FLAG_HTTPS,
|
|
107
|
+
FLAG_TRUSTED_PROXY: addon.INGRESS_FLAG_TRUSTED_PROXY,
|
|
108
|
+
FLAG_BODY_TRUNCATED: addon.INGRESS_FLAG_BODY_TRUNCATED,
|
|
109
|
+
HV_JSON: addon.INGRESS_HV_JSON,
|
|
110
|
+
HV_CORS_SIMPLE: addon.INGRESS_HV_CORS_SIMPLE,
|
|
111
|
+
HV_CORS_PREFLIGHT: addon.INGRESS_HV_CORS_PREFLIGHT,
|
|
112
|
+
HV_RATE_ACTIVE: addon.INGRESS_HV_RATE_ACTIVE,
|
|
113
|
+
HV_RATE_LIMITED: addon.INGRESS_HV_RATE_LIMITED,
|
|
114
|
+
HV_COUNT: addon.INGRESS_HV_COUNT,
|
|
115
|
+
ERR_NONE: addon.INGRESS_ERR_NONE,
|
|
116
|
+
ERR_CORS_PREFLIGHT: addon.INGRESS_ERR_CORS_PREFLIGHT,
|
|
117
|
+
ERR_RATE_LIMITED: addon.INGRESS_ERR_RATE_LIMITED,
|
|
118
|
+
ERR_BODY_TOO_LARGE: addon.INGRESS_ERR_BODY_TOO_LARGE,
|
|
119
|
+
ERR_INVALID_JSON: addon.INGRESS_ERR_INVALID_JSON,
|
|
120
|
+
ERR_SCHEMA_VALIDATION: addon.INGRESS_ERR_SCHEMA_VALIDATION,
|
|
121
|
+
ERR_BAD_REQUEST: addon.INGRESS_ERR_BAD_REQUEST,
|
|
122
|
+
ERR_REQUEST_TOO_LARGE: addon.INGRESS_ERR_REQUEST_TOO_LARGE,
|
|
123
|
+
ERR_INTERNAL: addon.INGRESS_ERR_INTERNAL,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const L: Layout = ffi !== null ? readLayoutViaFfi(ffi) : readLayoutViaNapi()
|
|
11
128
|
|
|
12
129
|
// ── Output buffer layout ───────────────────────────────────
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
export const
|
|
22
|
-
|
|
23
|
-
export const
|
|
24
|
-
|
|
25
|
-
export const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export const
|
|
30
|
-
|
|
31
|
-
export const
|
|
32
|
-
|
|
33
|
-
export const
|
|
34
|
-
|
|
35
|
-
export const
|
|
36
|
-
|
|
37
|
-
export const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export const
|
|
42
|
-
|
|
43
|
-
export const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
export const
|
|
50
|
-
|
|
51
|
-
export const
|
|
52
|
-
|
|
53
|
-
export const
|
|
54
|
-
|
|
55
|
-
export const
|
|
56
|
-
|
|
130
|
+
// Byte offsets into the fixed header of the native ingress OUTPUT buffer.
|
|
131
|
+
// The header occupies [0, OUT_DATA_START); the cookie/query/body JSON sections
|
|
132
|
+
// follow at OUT_DATA_START (see decode/packed-sections.ts). Read through the
|
|
133
|
+
// C-ABI layout blob (Bun) or napi (Node) — never hardcoded.
|
|
134
|
+
|
|
135
|
+
/** Offset of the verdict byte (0 = accepted, nonzero = terminal error). */
|
|
136
|
+
export const OUT_VERDICT: number = L.OUT_VERDICT
|
|
137
|
+
/** Offset of the error-code byte (see ERR_CODE_* below). */
|
|
138
|
+
export const OUT_ERROR_CODE: number = L.OUT_ERROR_CODE
|
|
139
|
+
/** Offset of the raw u16 status. */
|
|
140
|
+
export const OUT_STATUS: number = L.OUT_STATUS
|
|
141
|
+
/** Offset of the u32 flags word (see FLAG_* below). */
|
|
142
|
+
export const OUT_FLAGS: number = L.OUT_FLAGS
|
|
143
|
+
/** Offset of the u32 rate-limit budget (U32_MAX = disabled). */
|
|
144
|
+
export const OUT_RATE_LIMIT: number = L.OUT_RATE_LIMIT
|
|
145
|
+
/** Offset of the u32 rate-limit remaining count. */
|
|
146
|
+
export const OUT_RATE_REMAINING: number = L.OUT_RATE_REMAINING
|
|
147
|
+
/** Offset of the u64 rate-limit window reset (ms epoch). */
|
|
148
|
+
export const OUT_RATE_RESET: number = L.OUT_RATE_RESET
|
|
149
|
+
/** Offset of the u64 Retry-After value (ms). */
|
|
150
|
+
export const OUT_RETRY_AFTER: number = L.OUT_RETRY_AFTER
|
|
151
|
+
/** Offset of the u32 cookies-JSON section length. */
|
|
152
|
+
export const OUT_COOKIES_JSON_LEN: number = L.OUT_COOKIES_JSON_LEN
|
|
153
|
+
/** Offset of the u32 query-JSON section length. */
|
|
154
|
+
export const OUT_QUERY_JSON_LEN: number = L.OUT_QUERY_JSON_LEN
|
|
155
|
+
/** Offset of the header-template variant byte (see HV_* below). */
|
|
156
|
+
export const OUT_HEADER_VARIANT: number = L.OUT_HEADER_VARIANT
|
|
157
|
+
/** Offset of the u32 body-JSON section length. */
|
|
158
|
+
export const OUT_BODY_JSON_LEN: number = L.OUT_BODY_JSON_LEN
|
|
159
|
+
/** Byte offset where the variable-length JSON sections begin. */
|
|
160
|
+
export const OUT_DATA_START: number = L.OUT_DATA_START
|
|
161
|
+
|
|
162
|
+
// ── Flags (bits within the OUT_FLAGS u32) ──────────────────
|
|
163
|
+
/** Request carried a cookie header (cookie-JSON section present). */
|
|
164
|
+
export const FLAG_HAS_COOKIES: number = L.FLAG_HAS_COOKIES
|
|
165
|
+
/** Request carried a query string (query-JSON section present). */
|
|
166
|
+
export const FLAG_HAS_QUERY: number = L.FLAG_HAS_QUERY
|
|
167
|
+
/** The request body parsed as valid JSON. */
|
|
168
|
+
export const FLAG_BODY_VALID_JSON: number = L.FLAG_BODY_VALID_JSON
|
|
169
|
+
/** The request body validated against the ingress schema. */
|
|
170
|
+
export const FLAG_SCHEMA_VALID: number = L.FLAG_SCHEMA_VALID
|
|
171
|
+
/** The CORS engine allowed the request origin. */
|
|
172
|
+
export const FLAG_CORS_ALLOWED: number = L.FLAG_CORS_ALLOWED
|
|
173
|
+
/** The request was a CORS preflight (OPTIONS + request-method). */
|
|
174
|
+
export const FLAG_IS_PREFLIGHT: number = L.FLAG_IS_PREFLIGHT
|
|
175
|
+
/** The request was rate-limited. */
|
|
176
|
+
export const FLAG_RATE_LIMITED: number = L.FLAG_RATE_LIMITED
|
|
177
|
+
/** The request arrived over HTTPS (trusted-proxy scheme). */
|
|
178
|
+
export const FLAG_HTTPS: number = L.FLAG_HTTPS
|
|
179
|
+
/** The request came from a trusted proxy (per the proxy config). */
|
|
180
|
+
export const FLAG_TRUSTED_PROXY: number = L.FLAG_TRUSTED_PROXY
|
|
181
|
+
/** A declared output section overran the buffer (truncated). */
|
|
182
|
+
export const FLAG_BODY_TRUNCATED: number = L.FLAG_BODY_TRUNCATED
|
|
183
|
+
|
|
184
|
+
// ── Header variant bits (bits within the OUT_HEADER_VARIANT byte) ──
|
|
185
|
+
/** Response body is JSON (`content-type: application/json`). */
|
|
186
|
+
export const HV_JSON: number = L.HV_JSON
|
|
187
|
+
/** CORS simple-request variant (Origin echoed + vary headers). */
|
|
188
|
+
export const HV_CORS_SIMPLE: number = L.HV_CORS_SIMPLE
|
|
189
|
+
/** CORS preflight variant (allow-methods/headers/max-age). */
|
|
190
|
+
export const HV_CORS_PREFLIGHT: number = L.HV_CORS_PREFLIGHT
|
|
191
|
+
/** Rate-limit active variant (`ratelimit-limit` header). */
|
|
192
|
+
export const HV_RATE_ACTIVE: number = L.HV_RATE_ACTIVE
|
|
193
|
+
/** Rate-limited variant (Retry-After emitted). */
|
|
194
|
+
export const HV_RATE_LIMITED: number = L.HV_RATE_LIMITED
|
|
195
|
+
/** Total number of header-variant combinations (32). */
|
|
196
|
+
export const HV_COUNT: number = L.HV_COUNT
|
|
197
|
+
|
|
198
|
+
// ── Error codes (bytes in OUT_ERROR_CODE) ──────────────────
|
|
199
|
+
/** No error (accepted). */
|
|
200
|
+
export const ERR_CODE_NONE: number = L.ERR_NONE
|
|
201
|
+
/** CORS preflight rejected. */
|
|
202
|
+
export const ERR_CODE_CORS_PREFLIGHT: number = L.ERR_CORS_PREFLIGHT
|
|
203
|
+
/** Rate limit exceeded. */
|
|
204
|
+
export const ERR_CODE_RATE_LIMITED: number = L.ERR_RATE_LIMITED
|
|
205
|
+
/** Request body exceeded the size guard. */
|
|
206
|
+
export const ERR_CODE_BODY_TOO_LARGE: number = L.ERR_BODY_TOO_LARGE
|
|
207
|
+
/** Request body is not valid JSON. */
|
|
208
|
+
export const ERR_CODE_INVALID_JSON: number = L.ERR_INVALID_JSON
|
|
209
|
+
/** Request body failed schema validation. */
|
|
210
|
+
export const ERR_CODE_SCHEMA_VALIDATION: number = L.ERR_SCHEMA_VALIDATION
|
|
211
|
+
/** Malformed request (bad request line / headers). */
|
|
212
|
+
export const ERR_CODE_BAD_REQUEST: number = L.ERR_BAD_REQUEST
|
|
213
|
+
/** Request headers/URL exceeded the configured limits. */
|
|
214
|
+
export const ERR_CODE_REQUEST_TOO_LARGE: number = L.ERR_REQUEST_TOO_LARGE
|
|
215
|
+
/** Internal pipeline failure (maps to 500). */
|
|
216
|
+
export const ERR_CODE_INTERNAL: number = L.ERR_INTERNAL
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* JS-only error-code sentinel for a request-body read timeout in the async
|
|
220
|
+
* `createIngress` path.
|
|
221
|
+
*
|
|
222
|
+
* The native pipeline never emits this value — it is a JS body-reader error
|
|
223
|
+
* (`REQUEST_TIMEOUT` from `readBodyWithLimit`) mapped onto the numeric
|
|
224
|
+
* error-code contract so the shared status/name/message helpers stay
|
|
225
|
+
* consistent. Deliberately not sourced from Rust (see the header comment:
|
|
226
|
+
* numeric layout values come from the addon; this one is a JS-level error).
|
|
227
|
+
*/
|
|
228
|
+
export const ERR_CODE_REQUEST_TIMEOUT: number = -1
|