castrum 0.7.0 → 0.9.1
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 +154 -368
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +2763 -0
- package/dist/index.js +8707 -0
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +507 -0
- package/docs/BENCHMARKS.md +343 -0
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +81 -0
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +192 -0
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +746 -0
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +279 -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 +43 -0
- package/docs/bun-builtins-decision-matrix.md +115 -0
- package/index.ts +44 -8
- package/package.json +98 -33
- package/src/ingress/body.ts +289 -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 +639 -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 +68 -0
- package/src/ingress/index.ts +142 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +139 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +164 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +102 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +79 -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 +27 -0
- package/src/ingress/routes/index.ts +11 -0
- package/src/ingress/routes/json-write.ts +129 -0
- package/src/ingress/routes/options.ts +26 -0
- package/src/ingress/routes/read.ts +35 -0
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +475 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +191 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +220 -0
- package/src/integration/streaming.ts +78 -0
- package/src/integration/websocket.ts +93 -0
- package/src/loader/batch.ts +137 -0
- package/src/loader/cost.ts +105 -0
- package/src/loader/index.ts +745 -0
- package/src/loader/ops.ts +657 -0
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1775 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +240 -0
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +401 -0
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +87 -0
- package/src/rust-ffi/context.ts +193 -0
- package/src/rust-ffi/index.ts +51 -541
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +28 -0
- package/src/rust-ffi/scalar/crypto.ts +346 -0
- package/src/rust-ffi/scalar/factories.ts +459 -0
- package/src/rust-ffi/scalar/hashing.ts +74 -0
- package/src/rust-ffi/scalar/http.ts +337 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +325 -0
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +61 -0
- package/src/rust-ffi/scalar/payload.ts +241 -0
- package/src/rust-ffi/text.ts +121 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +73 -0
- package/src/shared/adaptive.ts +67 -0
- package/src/shared/buffer-pool.ts +264 -0
- package/src/shared/bytes.ts +108 -9
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +75 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/packed/wire.ts +504 -0
- package/src/shared/proven.ts +289 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +29 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +15 -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/data/json-rows.ts +0 -33
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
- package/src/shared/packed.ts +0 -316
|
@@ -0,0 +1,1775 @@
|
|
|
1
|
+
// src/native/ffi.ts — Bun-only C-ABI fast path via `bun:ffi` (transport core).
|
|
2
|
+
//
|
|
3
|
+
// Bun JIT-compiles `bun:ffi` calls down to direct native calls (~10-20ns
|
|
4
|
+
// crossing), versus ~100-350ns for a Node-API call. The same cdylib that Node
|
|
5
|
+
// loads through napi-rs ALSO exports `extern "C"` symbols (`rust/ffi.rs`), so
|
|
6
|
+
// under Bun we can `dlopen` it and call the hot scalar functions directly.
|
|
7
|
+
//
|
|
8
|
+
// Structure: this file is the transport CORE (dlopen + symbol binding +
|
|
9
|
+
// per-call wrappers). The pure pieces live beside it: `ffi/types.ts` (BunFFI /
|
|
10
|
+
// FfiMode / Raw signatures), `ffi/constants.ts` (caps + sizing + self-test
|
|
11
|
+
// vectors), `ffi/selftest.ts` (the bind-time self-test). This file re-exports
|
|
12
|
+
// `getBunFFI` + `BunFFI` so existing importers keep working unchanged.
|
|
13
|
+
//
|
|
14
|
+
// Safety / correctness strategy:
|
|
15
|
+
// - Lazily bound (no addon/ffi work until first use).
|
|
16
|
+
// - A one-time SELF-TEST runs at bind time (ffi/selftest.ts); any failure
|
|
17
|
+
// disables ffi and falls back to the napi addon.
|
|
18
|
+
// - Decoders that can fail ARE exposed with parity error semantics: a `0`
|
|
19
|
+
// write on non-empty input throws (the napi decoders throw rather than
|
|
20
|
+
// return a short buffer).
|
|
21
|
+
// - Never called under Node (falls back to napi immediately).
|
|
22
|
+
//
|
|
23
|
+
// Buffer ABI: input/out pointers are `(ptr, len)` pairs; the JS wrapper passes
|
|
24
|
+
// `(view, view.length)` and bun:ffi converts the TypedArray to its pointer.
|
|
25
|
+
|
|
26
|
+
import type { FFITypeOrString } from 'bun:ffi'
|
|
27
|
+
import { decodeUtf8, encodeUtf8 } from '../shared/codec'
|
|
28
|
+
import { resolveEnvVar } from '../shared/env'
|
|
29
|
+
import { isBun } from '../shared/runtime'
|
|
30
|
+
import {
|
|
31
|
+
COMPRESS_HEADROOM,
|
|
32
|
+
COMPRESS_INITIAL_CAP,
|
|
33
|
+
COMPRESS_MAX_CAP,
|
|
34
|
+
DECOMPRESS_FALLBACK_CAP,
|
|
35
|
+
DECOMPRESS_GUESS_MULTIPLIER_BROTLI,
|
|
36
|
+
DECOMPRESS_GUESS_MULTIPLIER_GZIP,
|
|
37
|
+
DECOMPRESS_MIN_INITIAL,
|
|
38
|
+
EMPTY_VIEW,
|
|
39
|
+
MAX_DECOMPRESSED,
|
|
40
|
+
MAX_JSON_PATCH_OUTPUT,
|
|
41
|
+
} from './ffi/constants'
|
|
42
|
+
import { selfTest } from './ffi/selftest'
|
|
43
|
+
import type {
|
|
44
|
+
BunFFI,
|
|
45
|
+
FfiMode,
|
|
46
|
+
Raw2,
|
|
47
|
+
Raw3,
|
|
48
|
+
Raw4,
|
|
49
|
+
Raw5,
|
|
50
|
+
Raw6,
|
|
51
|
+
Raw7,
|
|
52
|
+
Raw8,
|
|
53
|
+
Raw9,
|
|
54
|
+
Raw10,
|
|
55
|
+
Raw12,
|
|
56
|
+
RawCStr,
|
|
57
|
+
} from './ffi/types'
|
|
58
|
+
// Static import: loader.ts has no side effects (path resolution only — no
|
|
59
|
+
// dlopen), so this adds zero import-time cost while letting `bind()` reuse the
|
|
60
|
+
// exact same resolved `.node` path the napi fallback uses (the shared seam).
|
|
61
|
+
import { getAddonPath } from './loader'
|
|
62
|
+
|
|
63
|
+
// Re-export the type surface so `import type { BunFFI } from '../native/ffi'`
|
|
64
|
+
// keeps working (existing call sites).
|
|
65
|
+
export type { BunFFI, FfiMode } from './ffi/types'
|
|
66
|
+
|
|
67
|
+
let cached: BunFFI | null | undefined
|
|
68
|
+
/**
|
|
69
|
+
* Resolved buffer ABI mode, set once by `bind()`. `null` = ffi unavailable
|
|
70
|
+
* (Node / `CASTRUM_FFI_MODE=napi` / failed self-test), `'buffer-pair'` = the
|
|
71
|
+
* engine-native `buffer`/`buffer_length` pair is live, `'ptr-len'` = the
|
|
72
|
+
* explicit `(ptr, usize)` fallback.
|
|
73
|
+
*/
|
|
74
|
+
let bufferAbiMode: 'buffer-pair' | 'ptr-len' | null = null
|
|
75
|
+
|
|
76
|
+
// ── Transport selection (CASTRUM_FFI_MODE) ───────────────────────
|
|
77
|
+
|
|
78
|
+
function resolveFfiMode(): FfiMode {
|
|
79
|
+
const raw = resolveEnvVar('CASTRUM_FFI_MODE')
|
|
80
|
+
switch (raw) {
|
|
81
|
+
case 'ffi':
|
|
82
|
+
return 'ffi'
|
|
83
|
+
case 'napi':
|
|
84
|
+
return 'napi'
|
|
85
|
+
default:
|
|
86
|
+
return 'auto'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Lazily bind the Bun ffi fast path, or return `null` when unavailable
|
|
92
|
+
* (Node, `CASTRUM_FFI_MODE=napi`, missing symbols, or a failed self-test).
|
|
93
|
+
* `undefined` caches "not yet attempted".
|
|
94
|
+
*
|
|
95
|
+
* Under `CASTRUM_FFI_MODE=ffi` a bind/self-test failure THROWS instead of
|
|
96
|
+
* returning null — the caller asked for the primary transport explicitly.
|
|
97
|
+
*/
|
|
98
|
+
export function getBunFFI(): BunFFI | null {
|
|
99
|
+
if (cached !== undefined) {
|
|
100
|
+
return cached
|
|
101
|
+
}
|
|
102
|
+
cached = bind()
|
|
103
|
+
return cached
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Which buffer ABI the live `bun:ffi` binding uses, or `null` when the
|
|
108
|
+
* transport is unavailable (Node, `CASTRUM_FFI_MODE=napi`, or a failed
|
|
109
|
+
* self-test). `'buffer-pair'` = the engine-native `buffer`/`buffer_length`
|
|
110
|
+
* pair (atomic ptr+byteLength snapshot) is in use; `'ptr-len'` = the explicit
|
|
111
|
+
* `(ptr, usize)` fallback (older Bun / probe failure). Lazy — triggers the
|
|
112
|
+
* same one-time bind as {@link getBunFFI}. Under `CASTRUM_FFI_MODE=ffi` on Bun
|
|
113
|
+
* this is guaranteed non-null; a bind/self-test failure THROWS, matching
|
|
114
|
+
* `getBunFFI`.
|
|
115
|
+
*/
|
|
116
|
+
export function ffiBufferMode(): 'buffer-pair' | 'ptr-len' | null {
|
|
117
|
+
if (cached === undefined) {
|
|
118
|
+
getBunFFI()
|
|
119
|
+
}
|
|
120
|
+
return bufferAbiMode
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Probe whether this Bun build accepts the `buffer`/`buffer_length` ABI pair in
|
|
125
|
+
* `dlopen`. Bun's docs list `buffer_length` as engine-native (dlopen-supported),
|
|
126
|
+
* but an earlier canary threw "invalid ABI type" for it. If that regressed (or
|
|
127
|
+
* a future Bun removes it), we silently keep explicit `(ptr, len)` pairs. The
|
|
128
|
+
* full bind-time self-test is the safety net either way.
|
|
129
|
+
*/
|
|
130
|
+
function probeBufferLength(dlopen: typeof import('bun:ffi')['dlopen'], path: string): boolean {
|
|
131
|
+
try {
|
|
132
|
+
const { symbols, close } = dlopen(path, {
|
|
133
|
+
// bun-types lacks the `buffer_length` literal — cast it; runtime support
|
|
134
|
+
// is exactly what this probe is verifying.
|
|
135
|
+
castrum_crc32: {
|
|
136
|
+
args: ['buffer', 'buffer_length'] as unknown as readonly FFITypeOrString[],
|
|
137
|
+
returns: 'u32',
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
// `buffer_length` is `buffer`'s length twin — pass the SAME view twice
|
|
141
|
+
// (the engine reads ptr + byteLength off that object at call time).
|
|
142
|
+
const view = new Uint8Array([1, 2, 3])
|
|
143
|
+
const out = (symbols as Record<string, (a: unknown, b: unknown) => unknown>).castrum_crc32?.(
|
|
144
|
+
view,
|
|
145
|
+
view,
|
|
146
|
+
)
|
|
147
|
+
close()
|
|
148
|
+
return typeof out === 'number' && out >= 0
|
|
149
|
+
} catch {
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function bind(): BunFFI | null {
|
|
155
|
+
bufferAbiMode = null // reset — the mode below is only valid for a live bind
|
|
156
|
+
const mode = resolveFfiMode()
|
|
157
|
+
if (!isBun()) {
|
|
158
|
+
if (mode === 'ffi') {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'CASTRUM_FFI_MODE=ffi is invalid here: bun:ffi is a Bun-only transport ' +
|
|
161
|
+
'(this process is not Bun). Use CASTRUM_FFI_MODE=auto (default) so Node ' +
|
|
162
|
+
'uses the napi fallback.',
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
if (mode === 'napi') {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
172
|
+
const { dlopen } = require('bun:ffi') as typeof import('bun:ffi')
|
|
173
|
+
// Resolve the SAME addon file napi uses (./loader.ts — statically imported
|
|
174
|
+
// above; it only resolves the path, it never dlopens the addon).
|
|
175
|
+
const path = getAddonPath()
|
|
176
|
+
|
|
177
|
+
// Probe `buffer`/`buffer_length` (the engine reads ptr + len off the SAME
|
|
178
|
+
// TypedArray at call time — an atomic snapshot) once; when supported we use
|
|
179
|
+
// it for the input-only hot fns (one JS arg instead of two), else `(ptr,len)`.
|
|
180
|
+
const useBufferLength = probeBufferLength(dlopen, path)
|
|
181
|
+
bufferAbiMode = useBufferLength ? 'buffer-pair' : 'ptr-len'
|
|
182
|
+
|
|
183
|
+
// String ABI specs ("ptr"/"usize"/"u32"...): accepted by bun:ffi and typed
|
|
184
|
+
// via `FFITypeOrString` in bun-types. usize == uint64 on the host ABI.
|
|
185
|
+
// Symbol keys must be the exact `#[no_mangle] extern "C"` names in
|
|
186
|
+
// rust/ffi.rs. Every `(ptr,len)` pair is passed as `(view, view.length)`.
|
|
187
|
+
// The 8 input-only single-buffer fns use `inputAbi`; everything else keeps
|
|
188
|
+
// `(ptr,len)` because the output length is the caller's write cap.
|
|
189
|
+
const inputAbi: readonly FFITypeOrString[] = useBufferLength
|
|
190
|
+
? (['buffer', 'buffer_length'] as unknown as readonly FFITypeOrString[])
|
|
191
|
+
: ['ptr', 'usize']
|
|
192
|
+
// Phase 1.1: extend `buffer`/`buffer_length` to EVERY `(ptr,len)` pair, not
|
|
193
|
+
// just the input-only fns. The engine reads ptr + byteLength off the SAME
|
|
194
|
+
// view at call time (atomic snapshot), so call sites pass the view twice
|
|
195
|
+
// (see `lenOrView` below). Conversion is POSITIONAL and pair-aware: each
|
|
196
|
+
// `ptr` consumes its following `usize` as a length. Scalar args that happen
|
|
197
|
+
// to be `usize` (the opaque `inner` handle of `castrum_ingress_handle_packed`
|
|
198
|
+
// and the `max` param of gzip/brotli decompress) are passed through
|
|
199
|
+
// unchanged — a blind `usize`→`buffer_length` map would corrupt them.
|
|
200
|
+
const abi = (shape: readonly string[]): readonly FFITypeOrString[] => {
|
|
201
|
+
if (!useBufferLength) return shape as readonly FFITypeOrString[]
|
|
202
|
+
const out: FFITypeOrString[] = []
|
|
203
|
+
for (let i = 0; i < shape.length; i++) {
|
|
204
|
+
const t = shape[i]
|
|
205
|
+
if (t === 'ptr') {
|
|
206
|
+
out.push('buffer' as unknown as FFITypeOrString)
|
|
207
|
+
out.push('buffer_length' as unknown as FFITypeOrString)
|
|
208
|
+
i++ // consume the paired `usize`
|
|
209
|
+
} else {
|
|
210
|
+
out.push(t as FFITypeOrString)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out
|
|
214
|
+
}
|
|
215
|
+
// `u64_fast` returns a plain `number` when the value fits (< 2^53) instead
|
|
216
|
+
// of a BigInt — bun-types lacks the literal, so bind it via a typed const
|
|
217
|
+
// (same cast pattern as the `buffer_length` probe above). Every bytes-written
|
|
218
|
+
// return fits in a double, so this removes the per-call BigInt boxing on the
|
|
219
|
+
// hot scalar / `*_into` / packed-parser / ingress calls.
|
|
220
|
+
const U64_FAST = 'u64_fast' as unknown as FFITypeOrString
|
|
221
|
+
const { symbols } = dlopen(path, {
|
|
222
|
+
castrum_crc32: { args: inputAbi, returns: 'u32' },
|
|
223
|
+
castrum_fnv1a64: { args: inputAbi, returns: 'u64' },
|
|
224
|
+
castrum_xxh3: { args: inputAbi, returns: 'u64' },
|
|
225
|
+
castrum_json_valid: { args: inputAbi, returns: 'u8' },
|
|
226
|
+
castrum_utf8_valid: { args: inputAbi, returns: 'u8' },
|
|
227
|
+
castrum_hex_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
228
|
+
castrum_hex_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
229
|
+
castrum_url_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
230
|
+
castrum_url_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
231
|
+
// String-input validators: `cstring` ARG (the engine transcodes the JS
|
|
232
|
+
// string in-engine — no JS-side encode; Rust borrows via CStr::from_ptr).
|
|
233
|
+
castrum_validate_email: { args: ['cstring'], returns: 'u8' },
|
|
234
|
+
castrum_validate_uuid: { args: ['cstring'], returns: 'u8' },
|
|
235
|
+
castrum_validate_ipv4: { args: ['cstring'], returns: 'u8' },
|
|
236
|
+
castrum_validate_ipv6: { args: ['cstring'], returns: 'u8' },
|
|
237
|
+
castrum_json_sum_ids: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
238
|
+
castrum_hmac_sha256_verify: {
|
|
239
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
240
|
+
returns: 'u8',
|
|
241
|
+
},
|
|
242
|
+
castrum_csrf_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
|
|
243
|
+
castrum_password_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
|
|
244
|
+
castrum_password_verify_bcrypt: {
|
|
245
|
+
// `phc` crosses as a `cstring` ARG; `password` stays a `(ptr,len)` pair.
|
|
246
|
+
args: abi(['ptr', 'usize', 'cstring']),
|
|
247
|
+
returns: 'u8',
|
|
248
|
+
},
|
|
249
|
+
castrum_ws_accept_key: { args: ['cstring'], returns: 'cstring' },
|
|
250
|
+
castrum_ws_accept_key_into: {
|
|
251
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
252
|
+
returns: U64_FAST,
|
|
253
|
+
},
|
|
254
|
+
castrum_etag: { args: abi(['ptr', 'usize', 'u8']), returns: 'cstring' },
|
|
255
|
+
castrum_etag_into: { args: abi(['ptr', 'usize', 'u8', 'ptr', 'usize']), returns: U64_FAST },
|
|
256
|
+
castrum_conditional_is_not_modified: {
|
|
257
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'u8']),
|
|
258
|
+
returns: 'u8',
|
|
259
|
+
},
|
|
260
|
+
castrum_media_type_matcher_matches: {
|
|
261
|
+
args: abi(['usize', 'ptr', 'usize']),
|
|
262
|
+
returns: 'u8',
|
|
263
|
+
},
|
|
264
|
+
castrum_accept_negotiator_negotiate: {
|
|
265
|
+
args: abi(['usize', 'ptr', 'usize']),
|
|
266
|
+
returns: 'cstring',
|
|
267
|
+
},
|
|
268
|
+
castrum_jwt_signer_sign: {
|
|
269
|
+
args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
|
|
270
|
+
returns: U64_FAST,
|
|
271
|
+
},
|
|
272
|
+
castrum_jwt_signer_verify: {
|
|
273
|
+
args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
|
|
274
|
+
returns: U64_FAST,
|
|
275
|
+
},
|
|
276
|
+
castrum_template_render: {
|
|
277
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
278
|
+
returns: U64_FAST,
|
|
279
|
+
},
|
|
280
|
+
castrum_schema_validator_validate: {
|
|
281
|
+
args: abi(['usize', 'ptr', 'usize']),
|
|
282
|
+
returns: 'u8',
|
|
283
|
+
},
|
|
284
|
+
castrum_rate_limiter_check: {
|
|
285
|
+
// `key` crosses as a `cstring` ARG (text, no NUL in the intended use).
|
|
286
|
+
args: abi(['usize', 'cstring', 'i64', 'ptr', 'usize']),
|
|
287
|
+
returns: U64_FAST,
|
|
288
|
+
},
|
|
289
|
+
castrum_rate_limiter_check_key: {
|
|
290
|
+
args: abi(['usize', 'i64', 'i64', 'ptr', 'usize']),
|
|
291
|
+
returns: U64_FAST,
|
|
292
|
+
},
|
|
293
|
+
castrum_random_token: { args: ['u32'], returns: 'cstring' },
|
|
294
|
+
castrum_random_token_into: { args: ['u32', ...abi(['ptr', 'usize'])], returns: U64_FAST },
|
|
295
|
+
castrum_base64_encode: {
|
|
296
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
|
|
297
|
+
returns: U64_FAST,
|
|
298
|
+
},
|
|
299
|
+
castrum_base64_decode: {
|
|
300
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
|
|
301
|
+
returns: U64_FAST,
|
|
302
|
+
},
|
|
303
|
+
castrum_hmac_sha256: {
|
|
304
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
305
|
+
returns: U64_FAST,
|
|
306
|
+
},
|
|
307
|
+
castrum_sign_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
|
|
308
|
+
castrum_sign_cookie_into: {
|
|
309
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
310
|
+
returns: U64_FAST,
|
|
311
|
+
},
|
|
312
|
+
castrum_verify_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
|
|
313
|
+
castrum_verify_cookie_into: {
|
|
314
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
315
|
+
returns: U64_FAST,
|
|
316
|
+
},
|
|
317
|
+
castrum_csrf_token: { args: abi(['ptr', 'usize']), returns: 'cstring' },
|
|
318
|
+
castrum_csrf_token_into: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
319
|
+
castrum_password_hash: {
|
|
320
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'u32', 'u32', 'ptr', 'usize']),
|
|
321
|
+
returns: U64_FAST,
|
|
322
|
+
},
|
|
323
|
+
castrum_password_hash_bcrypt: {
|
|
324
|
+
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
325
|
+
returns: U64_FAST,
|
|
326
|
+
},
|
|
327
|
+
castrum_pbkdf2_sha256: {
|
|
328
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'ptr', 'usize']),
|
|
329
|
+
returns: U64_FAST,
|
|
330
|
+
},
|
|
331
|
+
castrum_aead_encrypt: {
|
|
332
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
|
|
333
|
+
returns: U64_FAST,
|
|
334
|
+
},
|
|
335
|
+
castrum_aead_decrypt: {
|
|
336
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
|
|
337
|
+
returns: U64_FAST,
|
|
338
|
+
},
|
|
339
|
+
castrum_ws_frame_encode: {
|
|
340
|
+
args: abi(['u8', 'ptr', 'usize', 'u8', 'u8', 'ptr', 'usize']),
|
|
341
|
+
returns: U64_FAST,
|
|
342
|
+
},
|
|
343
|
+
castrum_json_patch: {
|
|
344
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
345
|
+
returns: U64_FAST,
|
|
346
|
+
},
|
|
347
|
+
castrum_gzip_compress: {
|
|
348
|
+
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
349
|
+
returns: U64_FAST,
|
|
350
|
+
},
|
|
351
|
+
castrum_gzip_decompress: {
|
|
352
|
+
args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
|
|
353
|
+
returns: U64_FAST,
|
|
354
|
+
},
|
|
355
|
+
castrum_brotli_compress: {
|
|
356
|
+
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
357
|
+
returns: U64_FAST,
|
|
358
|
+
},
|
|
359
|
+
castrum_brotli_decompress: {
|
|
360
|
+
args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
|
|
361
|
+
returns: U64_FAST,
|
|
362
|
+
},
|
|
363
|
+
castrum_gzip_isize: { args: abi(['ptr', 'usize']), returns: 'u32' },
|
|
364
|
+
castrum_http_parse_request_packed: {
|
|
365
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
366
|
+
returns: U64_FAST,
|
|
367
|
+
},
|
|
368
|
+
castrum_query_parse_packed: {
|
|
369
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
370
|
+
returns: U64_FAST,
|
|
371
|
+
},
|
|
372
|
+
castrum_cookie_parse_packed: {
|
|
373
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
374
|
+
returns: U64_FAST,
|
|
375
|
+
},
|
|
376
|
+
castrum_http_date_into: { args: ['f64', ...abi(['ptr', 'usize'])], returns: U64_FAST },
|
|
377
|
+
castrum_sse_encode_into: {
|
|
378
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'u32', 'ptr', 'usize']),
|
|
379
|
+
returns: U64_FAST,
|
|
380
|
+
},
|
|
381
|
+
// ── Excluded-surface additions ──
|
|
382
|
+
castrum_jwt_sign_bytes: {
|
|
383
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64']),
|
|
384
|
+
returns: 'cstring',
|
|
385
|
+
},
|
|
386
|
+
castrum_jwt_sign_bytes_into: {
|
|
387
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64', 'ptr', 'usize']),
|
|
388
|
+
returns: U64_FAST,
|
|
389
|
+
},
|
|
390
|
+
castrum_ws_frame_decode_packed: {
|
|
391
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
392
|
+
returns: U64_FAST,
|
|
393
|
+
},
|
|
394
|
+
castrum_multipart_parse_packed: {
|
|
395
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
396
|
+
returns: U64_FAST,
|
|
397
|
+
},
|
|
398
|
+
castrum_form_parse_packed: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
399
|
+
// ── Phase 3 — stateless napi-only scalars (now FFI) ──────────
|
|
400
|
+
// Structural parse: parses ONCE, emits a packed token stream with a
|
|
401
|
+
// deduped string table so JS assembles the value with NO second parse.
|
|
402
|
+
castrum_json_parse_packed: {
|
|
403
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
404
|
+
returns: U64_FAST,
|
|
405
|
+
},
|
|
406
|
+
castrum_parse_media_type: {
|
|
407
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
408
|
+
returns: U64_FAST,
|
|
409
|
+
},
|
|
410
|
+
castrum_parse_http_date: {
|
|
411
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
412
|
+
returns: U64_FAST,
|
|
413
|
+
},
|
|
414
|
+
castrum_parse_accept_encoding: {
|
|
415
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
416
|
+
returns: U64_FAST,
|
|
417
|
+
},
|
|
418
|
+
castrum_url_encode_query: { args: abi(['ptr', 'usize']), returns: 'cstring' },
|
|
419
|
+
castrum_url_resolve: {
|
|
420
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
421
|
+
returns: 'cstring',
|
|
422
|
+
},
|
|
423
|
+
castrum_url_builder_resolve: {
|
|
424
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
425
|
+
returns: U64_FAST,
|
|
426
|
+
},
|
|
427
|
+
castrum_mime_from_extension: { args: ['cstring'], returns: 'cstring' },
|
|
428
|
+
castrum_jwt_verify: {
|
|
429
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64']),
|
|
430
|
+
returns: 'cstring',
|
|
431
|
+
},
|
|
432
|
+
castrum_ingress_handle_packed: {
|
|
433
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
434
|
+
returns: U64_FAST,
|
|
435
|
+
},
|
|
436
|
+
// Raw-components sibling of `castrum_ingress_handle_packed`: `url`/`ip`
|
|
437
|
+
// are `cstring` ARGs (Bun transcodes the JS strings to call-scoped
|
|
438
|
+
// NUL-terminated buffers in-engine — no JS-side Buffer.write encode);
|
|
439
|
+
// rid/headers/body are `(ptr,len)` byte slices; out is `(ptr,len)`.
|
|
440
|
+
castrum_ingress_handle_components: {
|
|
441
|
+
args: abi([
|
|
442
|
+
'usize',
|
|
443
|
+
'u8',
|
|
444
|
+
'cstring',
|
|
445
|
+
'cstring',
|
|
446
|
+
'ptr',
|
|
447
|
+
'usize',
|
|
448
|
+
'ptr',
|
|
449
|
+
'usize',
|
|
450
|
+
'ptr',
|
|
451
|
+
'usize',
|
|
452
|
+
'ptr',
|
|
453
|
+
'usize',
|
|
454
|
+
]),
|
|
455
|
+
returns: U64_FAST,
|
|
456
|
+
},
|
|
457
|
+
castrum_ingress_layout: { args: abi(['ptr', 'usize']), returns: U64_FAST },
|
|
458
|
+
// Per-route native stack (`@ignex/native` also dlopens these exact names
|
|
459
|
+
// itself; castrum binds them for the bind-time self-test + parity guard).
|
|
460
|
+
castrum_route_compile: { args: abi(['ptr', 'usize']), returns: U64_FAST },
|
|
461
|
+
castrum_route_run: {
|
|
462
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
463
|
+
returns: U64_FAST,
|
|
464
|
+
},
|
|
465
|
+
castrum_route_destroy: { args: ['usize'], returns: 'void' },
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
const bindings = build(symbols as Record<string, (...a: unknown[]) => unknown>, useBufferLength)
|
|
469
|
+
if (!selfTest(bindings)) {
|
|
470
|
+
if (mode === 'ffi') {
|
|
471
|
+
throw new Error(
|
|
472
|
+
'CASTRUM_FFI_MODE=ffi: the bun:ffi bind-time self-test failed, so the ' +
|
|
473
|
+
'primary transport cannot be trusted on this Bun/addon combination. ' +
|
|
474
|
+
'Unset CASTRUM_FFI_MODE (or use CASTRUM_FFI_MODE=auto) to fall back to napi.',
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
return null
|
|
478
|
+
}
|
|
479
|
+
return bindings
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (mode === 'ffi') {
|
|
482
|
+
const cause = err instanceof Error ? `: ${err.message}` : ''
|
|
483
|
+
throw new Error(`CASTRUM_FFI_MODE=ffi: failed to bind bun:ffi${cause}`)
|
|
484
|
+
}
|
|
485
|
+
return null
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Types for the raw dlopen'd functions (bun:ffi converts Uint8Array -> ptr for
|
|
490
|
+
// `ptr` args; `usize`/`u64`/`i64` returns surface as BigInt).
|
|
491
|
+
|
|
492
|
+
const flag = (v?: boolean): number => (v ? 1 : 0)
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Write with the C ABI's "needed" convention. The C side returns the EXACT
|
|
496
|
+
* total bytes the result requires: `0` = real error (throw immediately — no
|
|
497
|
+
* re-run, no grow-to-max allocation storm on invalid input), `w > out.length`
|
|
498
|
+
* = buffer too small with `w` the exact required size (allocate once, retry),
|
|
499
|
+
* else `w` is the written count. A miss therefore costs at most ONE retry with
|
|
500
|
+
* an exact buffer — never a doubling loop that re-runs the whole native op
|
|
501
|
+
* (which is what made FFI gzip/brotli/jsonPatch slower than napi).
|
|
502
|
+
*/
|
|
503
|
+
function growExact(
|
|
504
|
+
write: (out: Uint8Array) => number,
|
|
505
|
+
initial: number,
|
|
506
|
+
max: number,
|
|
507
|
+
error: string,
|
|
508
|
+
): Uint8Array {
|
|
509
|
+
let cap = Math.min(Math.max(initial, 16), max)
|
|
510
|
+
for (;;) {
|
|
511
|
+
const out = new Uint8Array(cap)
|
|
512
|
+
const w = Number(write(out))
|
|
513
|
+
if (w === 0) throw new Error(error)
|
|
514
|
+
if (w <= out.length) return out.subarray(0, w)
|
|
515
|
+
if (w > max) throw new Error(error)
|
|
516
|
+
cap = Math.min(w, max)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Decoder parity: a `0` write on non-empty input throws (napi `Result`). */
|
|
521
|
+
function writeOrThrow(w: number, inputLen: number, label: string): number {
|
|
522
|
+
if (w === 0 && inputLen !== 0) {
|
|
523
|
+
throw new Error(`${label}: invalid input or output buffer too small`)
|
|
524
|
+
}
|
|
525
|
+
return w
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Unpack the packed rate-limit verdict `[u8 allowed][u32 remaining LE][i64 reset_ms LE]`. */
|
|
529
|
+
function unpackRateCheck(
|
|
530
|
+
out: Uint8Array,
|
|
531
|
+
view: DataView,
|
|
532
|
+
): {
|
|
533
|
+
allowed: boolean
|
|
534
|
+
remaining: number
|
|
535
|
+
resetMs: number
|
|
536
|
+
} {
|
|
537
|
+
return {
|
|
538
|
+
allowed: out[0] === 1,
|
|
539
|
+
remaining: view.getUint32(1, true),
|
|
540
|
+
resetMs: Number(view.getBigInt64(5, true)),
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** base64 (no-pad) length of `n` bytes. */
|
|
545
|
+
const b64Len = (n: number): number => (n === 0 ? 0 : Math.ceil(n / 3) * 4 - ((3 - (n % 3)) % 3))
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Exact length of an argon2id PHC string for the given params
|
|
549
|
+
* (m/t/p/salt_len/out_len).
|
|
550
|
+
* Format: `$argon2id$v=19$m=<m>,t=<t>,p=<p>$<salt b64>$<hash b64>`.
|
|
551
|
+
* The salt b64 length is base64-no-pad of the CALLER's salt byte length
|
|
552
|
+
* (`SaltString::encode_b64`) — NOT a fixed 16 bytes. Hardcoding 22
|
|
553
|
+
* under-counts salts >16 bytes (e.g. the 18-byte "salty-salt-16bytes" →
|
|
554
|
+
* 24 chars) and growExact would re-run the whole ~tens-of-ms hash on the
|
|
555
|
+
* retry (measured 2× slowdown). Pre-sizing exactly keeps `passwordHash` a
|
|
556
|
+
* single native pass.
|
|
557
|
+
*/
|
|
558
|
+
function argon2PhcLength(m: number, t: number, p: number, saltLen: number, outLen: number): number {
|
|
559
|
+
return (
|
|
560
|
+
15 + // "$argon2id$v=19$"
|
|
561
|
+
2 + // "m="
|
|
562
|
+
String(m).length +
|
|
563
|
+
3 + // ",t="
|
|
564
|
+
String(t).length +
|
|
565
|
+
3 + // ",p="
|
|
566
|
+
String(p).length +
|
|
567
|
+
1 + // "$" before the salt
|
|
568
|
+
b64Len(saltLen) + // salt b64 (no-pad — depends on the caller's salt length)
|
|
569
|
+
1 + // "$" before the hash
|
|
570
|
+
b64Len(outLen)
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function build(
|
|
575
|
+
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
576
|
+
useBufferLength: boolean,
|
|
577
|
+
): BunFFI {
|
|
578
|
+
const crc32 = sym.castrum_crc32 as (...a: unknown[]) => number | bigint
|
|
579
|
+
const fnv = sym.castrum_fnv1a64 as (...a: unknown[]) => number | bigint
|
|
580
|
+
const xxh = sym.castrum_xxh3 as (...a: unknown[]) => number | bigint
|
|
581
|
+
const jsonValid = sym.castrum_json_valid as (...a: unknown[]) => number | bigint
|
|
582
|
+
const utf8Valid = sym.castrum_utf8_valid as (...a: unknown[]) => number | bigint
|
|
583
|
+
const hexEncode = sym.castrum_hex_encode as Raw4
|
|
584
|
+
const hexDecode = sym.castrum_hex_decode as Raw4
|
|
585
|
+
const urlEncode = sym.castrum_url_encode as Raw4
|
|
586
|
+
const urlDecode = sym.castrum_url_decode as Raw4
|
|
587
|
+
const validateEmail = sym.castrum_validate_email as (...a: unknown[]) => number | bigint
|
|
588
|
+
const validateUuid = sym.castrum_validate_uuid as (...a: unknown[]) => number | bigint
|
|
589
|
+
const validateIpv4 = sym.castrum_validate_ipv4 as (...a: unknown[]) => number | bigint
|
|
590
|
+
const validateIpv6 = sym.castrum_validate_ipv6 as (...a: unknown[]) => number | bigint
|
|
591
|
+
const jsonSumRaw = sym.castrum_json_sum_ids as Raw4
|
|
592
|
+
|
|
593
|
+
// Single-buffer input fns: with the probe-gated `buffer/buffer_length` ABI the
|
|
594
|
+
// engine reads ptr + byteLength off the SAME TypedArray at call time (an
|
|
595
|
+
// atomic snapshot — pass the view twice, per Bun's docs); with `(ptr,len)` we
|
|
596
|
+
// pass the length explicitly. The branch is on a bind-time constant, so the
|
|
597
|
+
// JIT folds it away at the call site.
|
|
598
|
+
const oneArg = (raw: (...a: unknown[]) => number | bigint, v: Uint8Array): number | bigint =>
|
|
599
|
+
useBufferLength ? raw(v, v) : raw(v, v.length)
|
|
600
|
+
// Phase 1.1: under `buffer`/`buffer_length` the engine reads ptr + byteLength
|
|
601
|
+
// off the SAME view, so every `(ptr,len)` pair passes the view itself for the
|
|
602
|
+
// length slot (atomic snapshot, one JS arg fewer); under `(ptr,usize)` it's
|
|
603
|
+
// the explicit length. The branch is a bind-time constant → JIT folds it.
|
|
604
|
+
const lenOrView = (v: Uint8Array): Uint8Array | number => (useBufferLength ? v : v.length)
|
|
605
|
+
const hmacVerify = sym.castrum_hmac_sha256_verify as Raw6
|
|
606
|
+
const csrfVerify = sym.castrum_csrf_verify as Raw4
|
|
607
|
+
const passwordVerify = sym.castrum_password_verify as Raw4
|
|
608
|
+
const passwordVerifyBcrypt = sym.castrum_password_verify_bcrypt as Raw3
|
|
609
|
+
const wsAcceptKey = sym.castrum_ws_accept_key as RawCStr
|
|
610
|
+
const wsAcceptKeyInto = sym.castrum_ws_accept_key_into as Raw4
|
|
611
|
+
const etagCStr = sym.castrum_etag as RawCStr
|
|
612
|
+
const etagIntoRaw = sym.castrum_etag_into as Raw5
|
|
613
|
+
const conditionalIsNotModifiedRaw = sym.castrum_conditional_is_not_modified as Raw6
|
|
614
|
+
const mediaTypeMatcherMatchesRaw = sym.castrum_media_type_matcher_matches as Raw3
|
|
615
|
+
const acceptNegotiatorNegotiateRaw = sym.castrum_accept_negotiator_negotiate as RawCStr
|
|
616
|
+
const jwtSignerSignRaw = sym.castrum_jwt_signer_sign as Raw6
|
|
617
|
+
const jwtSignerVerifyRaw = sym.castrum_jwt_signer_verify as Raw6
|
|
618
|
+
const templateRenderRaw = sym.castrum_template_render as Raw5
|
|
619
|
+
const schemaValidatorValidateRaw = sym.castrum_schema_validator_validate as Raw3
|
|
620
|
+
const rateLimiterCheckRaw = sym.castrum_rate_limiter_check as Raw5
|
|
621
|
+
const rateLimiterCheckKeyRaw = sym.castrum_rate_limiter_check_key as Raw5
|
|
622
|
+
const randomToken = sym.castrum_random_token as RawCStr
|
|
623
|
+
const randomTokenInto = sym.castrum_random_token_into as Raw3
|
|
624
|
+
const base64Encode = sym.castrum_base64_encode as Raw6
|
|
625
|
+
const base64Decode = sym.castrum_base64_decode as Raw6
|
|
626
|
+
const hmacSha256 = sym.castrum_hmac_sha256 as Raw6
|
|
627
|
+
const signCookie = sym.castrum_sign_cookie as RawCStr
|
|
628
|
+
const signCookieInto = sym.castrum_sign_cookie_into as Raw6
|
|
629
|
+
const verifyCookie = sym.castrum_verify_cookie as RawCStr
|
|
630
|
+
const verifyCookieInto = sym.castrum_verify_cookie_into as Raw6
|
|
631
|
+
const csrfToken = sym.castrum_csrf_token as RawCStr
|
|
632
|
+
const csrfTokenInto = sym.castrum_csrf_token_into as Raw4
|
|
633
|
+
const passwordHash = sym.castrum_password_hash as Raw10
|
|
634
|
+
const passwordHashBcrypt = sym.castrum_password_hash_bcrypt as Raw5
|
|
635
|
+
const pbkdf2 = sym.castrum_pbkdf2_sha256 as Raw8
|
|
636
|
+
const aeadEncrypt = sym.castrum_aead_encrypt as Raw9
|
|
637
|
+
const aeadDecrypt = sym.castrum_aead_decrypt as Raw9
|
|
638
|
+
const wsFrameEncode = sym.castrum_ws_frame_encode as Raw7
|
|
639
|
+
const jsonPatch = sym.castrum_json_patch as Raw6
|
|
640
|
+
const gzipCompress = sym.castrum_gzip_compress as Raw5
|
|
641
|
+
const gzipDecompress = sym.castrum_gzip_decompress as Raw5
|
|
642
|
+
const brotliCompress = sym.castrum_brotli_compress as Raw5
|
|
643
|
+
const brotliDecompress = sym.castrum_brotli_decompress as Raw5
|
|
644
|
+
const gzipIsize = sym.castrum_gzip_isize as Raw2
|
|
645
|
+
const httpParsePacked = sym.castrum_http_parse_request_packed as Raw4
|
|
646
|
+
const queryParsePacked = sym.castrum_query_parse_packed as Raw4
|
|
647
|
+
const cookieParsePacked = sym.castrum_cookie_parse_packed as Raw4
|
|
648
|
+
const httpDateIntoRaw = sym.castrum_http_date_into as Raw3
|
|
649
|
+
const sseEncodeIntoRaw = sym.castrum_sse_encode_into as Raw10
|
|
650
|
+
const jwtSignBytes = sym.castrum_jwt_sign_bytes as RawCStr
|
|
651
|
+
const jsonParsePackedSym = sym.castrum_json_parse_packed as Raw4
|
|
652
|
+
const jwtSignBytesInto = sym.castrum_jwt_sign_bytes_into as Raw8
|
|
653
|
+
const wsFrameDecodePacked = sym.castrum_ws_frame_decode_packed as Raw4
|
|
654
|
+
const multipartParsePacked = sym.castrum_multipart_parse_packed as Raw6
|
|
655
|
+
const formParsePacked = sym.castrum_form_parse_packed as Raw4
|
|
656
|
+
// Phase 3 — stateless napi-only scalars.
|
|
657
|
+
const parseMediaTypeSym = sym.castrum_parse_media_type as Raw4
|
|
658
|
+
const parseHttpDateSym = sym.castrum_parse_http_date as Raw4
|
|
659
|
+
const parseAcceptEncodingSym = sym.castrum_parse_accept_encoding as Raw4
|
|
660
|
+
const urlEncodeQuerySym = sym.castrum_url_encode_query as (...a: unknown[]) => string | null
|
|
661
|
+
const urlResolveSym = sym.castrum_url_resolve as (...a: unknown[]) => string | null
|
|
662
|
+
const urlBuilderResolveRaw = sym.castrum_url_builder_resolve as Raw5
|
|
663
|
+
const mimeFromExtensionSym = sym.castrum_mime_from_extension as (...a: unknown[]) => string | null
|
|
664
|
+
const jwtVerifySym = sym.castrum_jwt_verify as (...a: unknown[]) => string | null
|
|
665
|
+
const ingressHandlePacked = sym.castrum_ingress_handle_packed as Raw7
|
|
666
|
+
const ingressHandleComponentsSym = sym.castrum_ingress_handle_components as Raw12
|
|
667
|
+
const ingressLayoutSym = sym.castrum_ingress_layout as Raw2
|
|
668
|
+
const routeCompileSym = sym.castrum_route_compile as Raw2
|
|
669
|
+
const routeRunSym = sym.castrum_route_run as Raw5
|
|
670
|
+
const routeDestroySym = sym.castrum_route_destroy as (a: unknown) => void
|
|
671
|
+
|
|
672
|
+
// ── Encoder / decoder `Into` helpers ──────────────────────────────
|
|
673
|
+
const hexEncodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
674
|
+
// Mirror napi error semantics: a too-small buffer throws, not returns 0.
|
|
675
|
+
// Hex encode always writes exactly `input.length * 2` bytes on success.
|
|
676
|
+
if (output.length < input.length * 2) {
|
|
677
|
+
throw new Error('hex encode: output buffer too small')
|
|
678
|
+
}
|
|
679
|
+
return Number(hexEncode(input, lenOrView(input), output, lenOrView(output)))
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const urlEncodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
683
|
+
const w = Number(urlEncode(input, lenOrView(input), output, lenOrView(output)))
|
|
684
|
+
// Every input byte encodes to >= 1 output byte, so a 0 write on non-empty
|
|
685
|
+
// input means the buffer was too small (napi throws there too). Empty
|
|
686
|
+
// input legitimately writes 0.
|
|
687
|
+
if (w === 0 && input.length !== 0) {
|
|
688
|
+
throw new Error('url encode: output buffer too small')
|
|
689
|
+
}
|
|
690
|
+
return w
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const hexDecodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
694
|
+
const w = Number(hexDecode(input, lenOrView(input), output, lenOrView(output)))
|
|
695
|
+
return writeOrThrow(w, input.length, 'hex decode')
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const urlDecodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
699
|
+
const w = Number(urlDecode(input, lenOrView(input), output, lenOrView(output)))
|
|
700
|
+
return writeOrThrow(w, input.length, 'url decode')
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const base64DecodeInto = (
|
|
704
|
+
input: Uint8Array,
|
|
705
|
+
output: Uint8Array,
|
|
706
|
+
urlSafe?: boolean,
|
|
707
|
+
padding?: boolean,
|
|
708
|
+
): number => {
|
|
709
|
+
// napi defaults: urlSafe=false, padding=true (rust/crypto/base64.rs).
|
|
710
|
+
const w = Number(
|
|
711
|
+
base64Decode(
|
|
712
|
+
input,
|
|
713
|
+
lenOrView(input),
|
|
714
|
+
output,
|
|
715
|
+
lenOrView(output),
|
|
716
|
+
flag(urlSafe),
|
|
717
|
+
flag(padding ?? true),
|
|
718
|
+
),
|
|
719
|
+
)
|
|
720
|
+
return writeOrThrow(w, input.length, 'base64 decode')
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const base64EncodeInto = (
|
|
724
|
+
input: Uint8Array,
|
|
725
|
+
output: Uint8Array,
|
|
726
|
+
urlSafe?: boolean,
|
|
727
|
+
padding?: boolean,
|
|
728
|
+
): number => {
|
|
729
|
+
// napi defaults: urlSafe=false, padding=true.
|
|
730
|
+
const w = Number(
|
|
731
|
+
base64Encode(
|
|
732
|
+
input,
|
|
733
|
+
lenOrView(input),
|
|
734
|
+
output,
|
|
735
|
+
lenOrView(output),
|
|
736
|
+
flag(urlSafe),
|
|
737
|
+
flag(padding ?? true),
|
|
738
|
+
),
|
|
739
|
+
)
|
|
740
|
+
// Empty input legitimately writes 0 bytes — only a 0 write on NON-empty
|
|
741
|
+
// input is a real error (same convention as the decode/etag paths).
|
|
742
|
+
return writeOrThrow(w, input.length, 'base64 encode')
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// cstring-return (the engine clones the NUL-terminated string at call time)
|
|
746
|
+
// → the JS string directly (native transfer — zero encode); a `null` return
|
|
747
|
+
// is the C failure sentinel → throw.
|
|
748
|
+
const cstr = (s: string | null, label: string): string => {
|
|
749
|
+
if (s === null) throw new Error(label)
|
|
750
|
+
return s
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const etagInto = (data: Uint8Array, output: Uint8Array, weak?: boolean): number => {
|
|
754
|
+
// Native pooled `_into`: writes 10/12 bytes directly into the caller
|
|
755
|
+
// buffer (no cstring round-trip). Needed-size convention: a write larger
|
|
756
|
+
// than `output.length` reports the exact required size → throw.
|
|
757
|
+
const w = Number(etagIntoRaw(data, lenOrView(data), flag(weak), output, lenOrView(output)))
|
|
758
|
+
if (w === 0) {
|
|
759
|
+
throw new Error('etag: invalid input')
|
|
760
|
+
}
|
|
761
|
+
if (w > output.length) {
|
|
762
|
+
throw new Error('etag: output buffer too small')
|
|
763
|
+
}
|
|
764
|
+
return w
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Pooled 9-byte scratch for jsonSumIds (the packed `[u8 ok][i64 sum LE]`
|
|
768
|
+
// output). The wrapper returns the BigInt VALUE (no byte aliasing escapes),
|
|
769
|
+
// so a single reused buffer + cached DataView is safe — removes the per-call
|
|
770
|
+
// Uint8Array + DataView allocs (measured ~400ns of the FFI wrapper cost).
|
|
771
|
+
const jsonSumOut = new Uint8Array(9)
|
|
772
|
+
const jsonSumView = new DataView(jsonSumOut.buffer)
|
|
773
|
+
|
|
774
|
+
// ── Reusable scratch for the STRING-returning encode wrappers ────────
|
|
775
|
+
// Each wrapper writes its result and decodes it into an immutable JS string
|
|
776
|
+
// (or primitive) SYNCHRONOUSLY before the next call reuses the buffer — the
|
|
777
|
+
// same safety rationale as `jsonSumOut` / `generateRequestId`. Per-Worker
|
|
778
|
+
// module state (single-threaded event loop); never shared across Workers.
|
|
779
|
+
// NOT used for byte-returning ops (hexDecode/urlDecode/base64Decode/aead/
|
|
780
|
+
// wsFrame/packed parsers): their returned `Uint8Array` aliases the buffer
|
|
781
|
+
// and escapes to the caller — those stay on `*Into` (caller-owned).
|
|
782
|
+
let encodeScratch = new Uint8Array(0)
|
|
783
|
+
const scratchFor = (size: number): Uint8Array => {
|
|
784
|
+
if (encodeScratch.byteLength < size) encodeScratch = new Uint8Array(size)
|
|
785
|
+
return encodeScratch
|
|
786
|
+
}
|
|
787
|
+
// Fixed-size packed-verdict scratch + cached DataViews (parseHttpDate 9 B,
|
|
788
|
+
// rate-limit check 13 B) — same pooled pattern as `jsonSumView`. Kept
|
|
789
|
+
// separate from the growable `encodeScratch` so the cached DataViews are
|
|
790
|
+
// never invalidated by a buffer replacement.
|
|
791
|
+
const dateScratch = new Uint8Array(9)
|
|
792
|
+
const dateScratchView = new DataView(dateScratch.buffer)
|
|
793
|
+
const rateScratch = new Uint8Array(13)
|
|
794
|
+
const rateScratchView = new DataView(rateScratch.buffer)
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
crc32: (input) => Number(oneArg(crc32, input)) >>> 0,
|
|
798
|
+
fnv1a64: (input) => BigInt(oneArg(fnv, input)),
|
|
799
|
+
xxh3: (input) => BigInt(oneArg(xxh, input)),
|
|
800
|
+
jsonValid: (input) => Number(oneArg(jsonValid, input)) === 1,
|
|
801
|
+
utf8Valid: (input) => Number(oneArg(utf8Valid, input)) === 1,
|
|
802
|
+
hexEncode(input) {
|
|
803
|
+
// Pooled scratch — decoded synchronously to an immutable string (safe;
|
|
804
|
+
// removes the per-call `new Uint8Array(len*2)`). ALWAYS decode the
|
|
805
|
+
// written subarray: the shared scratch may be larger than `w` (grown by
|
|
806
|
+
// an earlier op), so decoding the whole buffer would read stale bytes.
|
|
807
|
+
const out = scratchFor(input.length * 2)
|
|
808
|
+
const w = hexEncodeInto(input, out)
|
|
809
|
+
return decodeUtf8(out.subarray(0, w))
|
|
810
|
+
},
|
|
811
|
+
hexEncodeInto,
|
|
812
|
+
urlEncode(input) {
|
|
813
|
+
// RFC 3986 worst case is 3 bytes per input byte (`%XX`). Pooled scratch
|
|
814
|
+
// (decoded synchronously — safe).
|
|
815
|
+
const out = scratchFor(input.length * 3)
|
|
816
|
+
const w = urlEncodeInto(input, out)
|
|
817
|
+
return decodeUtf8(out.subarray(0, w))
|
|
818
|
+
},
|
|
819
|
+
urlEncodeInto,
|
|
820
|
+
|
|
821
|
+
validateEmail: (input) => Number(validateEmail(input)) === 1,
|
|
822
|
+
validateUuid: (input) => Number(validateUuid(input)) === 1,
|
|
823
|
+
validateIpv4: (input) => Number(validateIpv4(input)) === 1,
|
|
824
|
+
validateIpv6: (input) => Number(validateIpv6(input)) === 1,
|
|
825
|
+
jsonSumIds: (input) => {
|
|
826
|
+
// Packed [u8 ok][i64 sum LE] output (9 B): ok=1 → valid array (the sum
|
|
827
|
+
// may be 0); ok=0 → invalid input. Bytes written: 9/1/0 (0 = real error).
|
|
828
|
+
const w = Number(jsonSumRaw(input, lenOrView(input), jsonSumOut, lenOrView(jsonSumOut)))
|
|
829
|
+
if (w === 0) {
|
|
830
|
+
throw new Error('json sum ids: output buffer too small')
|
|
831
|
+
}
|
|
832
|
+
if (jsonSumOut[0] === 0) {
|
|
833
|
+
// Mirrors the napi error phrasing (serde: "expected an array of objects
|
|
834
|
+
// with numeric ids") so both transports throw the same message.
|
|
835
|
+
throw new Error('json sum ids: expected an array of objects with numeric ids')
|
|
836
|
+
}
|
|
837
|
+
return jsonSumView.getBigInt64(1, true)
|
|
838
|
+
},
|
|
839
|
+
hmacSha256Verify: (key, data, signature) =>
|
|
840
|
+
Number(
|
|
841
|
+
hmacVerify(key, lenOrView(key), data, lenOrView(data), signature, lenOrView(signature)),
|
|
842
|
+
) === 1,
|
|
843
|
+
csrfVerify: (token, secret) =>
|
|
844
|
+
Number(csrfVerify(token, lenOrView(token), secret, lenOrView(secret))) === 1,
|
|
845
|
+
passwordVerify: (password, phc) =>
|
|
846
|
+
Number(passwordVerify(password, lenOrView(password), phc, lenOrView(phc))) === 1,
|
|
847
|
+
passwordVerifyBcrypt: (password, phc) =>
|
|
848
|
+
Number(passwordVerifyBcrypt(password, lenOrView(password), phc)) === 1,
|
|
849
|
+
|
|
850
|
+
hexDecode(input) {
|
|
851
|
+
const out = new Uint8Array(Math.floor(input.length / 2))
|
|
852
|
+
const w = hexDecodeInto(input, out)
|
|
853
|
+
return out.subarray(0, w)
|
|
854
|
+
},
|
|
855
|
+
hexDecodeInto,
|
|
856
|
+
urlDecode(input) {
|
|
857
|
+
const out = new Uint8Array(input.length)
|
|
858
|
+
const w = urlDecodeInto(input, out)
|
|
859
|
+
return out.subarray(0, w)
|
|
860
|
+
},
|
|
861
|
+
urlDecodeInto,
|
|
862
|
+
base64Decode(input, urlSafe, padding) {
|
|
863
|
+
const out = new Uint8Array(Math.ceil((input.length * 3) / 4))
|
|
864
|
+
const w = base64DecodeInto(input, out, urlSafe, padding)
|
|
865
|
+
return out.subarray(0, w)
|
|
866
|
+
},
|
|
867
|
+
base64DecodeInto,
|
|
868
|
+
|
|
869
|
+
wsAcceptKey(key) {
|
|
870
|
+
return cstr(wsAcceptKey(key), 'ws accept key: bad key')
|
|
871
|
+
},
|
|
872
|
+
wsAcceptKeyInto(key, output) {
|
|
873
|
+
// Native pooled `_into`: writes the 28-byte accept key directly into the
|
|
874
|
+
// caller buffer (no cstring round-trip). Needed-size convention.
|
|
875
|
+
const w = Number(wsAcceptKeyInto(key, lenOrView(key), output, lenOrView(output)))
|
|
876
|
+
if (w === 0) {
|
|
877
|
+
throw new Error('ws accept key: bad key')
|
|
878
|
+
}
|
|
879
|
+
if (w > output.length) {
|
|
880
|
+
throw new Error('ws accept key: output buffer too small')
|
|
881
|
+
}
|
|
882
|
+
return w
|
|
883
|
+
},
|
|
884
|
+
etag(data, weak) {
|
|
885
|
+
// cstring return (10 strong / 12 weak chars) — zero encode.
|
|
886
|
+
return cstr(etagCStr(data, lenOrView(data), flag(weak)), 'etag: invalid input')
|
|
887
|
+
},
|
|
888
|
+
etagInto,
|
|
889
|
+
conditionalIsNotModified(inner, ifNoneMatch, ifModifiedSince) {
|
|
890
|
+
// Opaque-handle eval of the precompiled `ConditionalRequest` state.
|
|
891
|
+
// flags bit0 = If-None-Match present, bit1 = If-Modified-Since present
|
|
892
|
+
// (present-but-empty is distinct from absent — napi Option parity). A
|
|
893
|
+
// null handle (0) → 0 (never dereferences freed state).
|
|
894
|
+
const flags = (ifNoneMatch === null ? 0 : 1) | (ifModifiedSince === null ? 0 : 2)
|
|
895
|
+
const inm = ifNoneMatch ?? EMPTY_VIEW
|
|
896
|
+
const ims = ifModifiedSince ?? EMPTY_VIEW
|
|
897
|
+
return (
|
|
898
|
+
Number(
|
|
899
|
+
conditionalIsNotModifiedRaw(inner, inm, lenOrView(inm), ims, lenOrView(ims), flags),
|
|
900
|
+
) === 1
|
|
901
|
+
)
|
|
902
|
+
},
|
|
903
|
+
mediaTypeMatcherMatches(inner, actual) {
|
|
904
|
+
// Precompiled expected-type match → u8.
|
|
905
|
+
return Number(mediaTypeMatcherMatchesRaw(inner, actual, lenOrView(actual))) === 1
|
|
906
|
+
},
|
|
907
|
+
acceptNegotiatorNegotiate(inner, header) {
|
|
908
|
+
// cstring best-supported encoding; `null` = identity (napi Option parity).
|
|
909
|
+
return acceptNegotiatorNegotiateRaw(inner, header, lenOrView(header))
|
|
910
|
+
},
|
|
911
|
+
jwtSignerSign(inner, claimsJson, nowSeconds) {
|
|
912
|
+
// Precompiled key + ttl → compact token. 0 = invalid claims JSON (real
|
|
913
|
+
// error → growExact throws); w > output.length = exact needed size.
|
|
914
|
+
return growExact(
|
|
915
|
+
(out) =>
|
|
916
|
+
Number(
|
|
917
|
+
jwtSignerSignRaw(
|
|
918
|
+
inner,
|
|
919
|
+
claimsJson,
|
|
920
|
+
lenOrView(claimsJson),
|
|
921
|
+
BigInt(nowSeconds),
|
|
922
|
+
out,
|
|
923
|
+
lenOrView(out),
|
|
924
|
+
),
|
|
925
|
+
),
|
|
926
|
+
Math.min(claimsJson.length + 128, 64 * 1024),
|
|
927
|
+
1024 * 1024,
|
|
928
|
+
'jwt signer: invalid claims JSON or output buffer too small',
|
|
929
|
+
)
|
|
930
|
+
},
|
|
931
|
+
jwtSignerVerify(inner, token, nowSeconds) {
|
|
932
|
+
// Precompiled key → claims JSON bytes; 0 = invalid / expired → null.
|
|
933
|
+
const out = new Uint8Array(Math.min(token.length + 256, 64 * 1024))
|
|
934
|
+
const w = Number(
|
|
935
|
+
jwtSignerVerifyRaw(inner, token, lenOrView(token), BigInt(nowSeconds), out, lenOrView(out)),
|
|
936
|
+
)
|
|
937
|
+
if (w === 0) return null
|
|
938
|
+
if (w > out.length) {
|
|
939
|
+
const out2 = new Uint8Array(w)
|
|
940
|
+
const w2 = Number(
|
|
941
|
+
jwtSignerVerifyRaw(
|
|
942
|
+
inner,
|
|
943
|
+
token,
|
|
944
|
+
lenOrView(token),
|
|
945
|
+
BigInt(nowSeconds),
|
|
946
|
+
out2,
|
|
947
|
+
lenOrView(out2),
|
|
948
|
+
),
|
|
949
|
+
)
|
|
950
|
+
return w2 === 0 ? null : out2.subarray(0, w2)
|
|
951
|
+
}
|
|
952
|
+
return out.subarray(0, w)
|
|
953
|
+
},
|
|
954
|
+
templateRender(inner, contextJson) {
|
|
955
|
+
// Compiled template + pre-serialized JSON context → UTF-8 bytes. 0 =
|
|
956
|
+
// invalid context / render error (real error → growExact throws).
|
|
957
|
+
return growExact(
|
|
958
|
+
(out) =>
|
|
959
|
+
Number(
|
|
960
|
+
templateRenderRaw(inner, contextJson, lenOrView(contextJson), out, lenOrView(out)),
|
|
961
|
+
),
|
|
962
|
+
Math.min(contextJson.length + 128, 64 * 1024),
|
|
963
|
+
1024 * 1024,
|
|
964
|
+
'template render: invalid context JSON or render failed',
|
|
965
|
+
)
|
|
966
|
+
},
|
|
967
|
+
schemaValidatorValidate(inner, doc) {
|
|
968
|
+
return Number(schemaValidatorValidateRaw(inner, doc, lenOrView(doc))) === 1
|
|
969
|
+
},
|
|
970
|
+
rateLimiterCheck(inner, key, nowMs) {
|
|
971
|
+
// Packed [u8 allowed][u32 remaining LE][i64 reset_ms LE] (13 bytes).
|
|
972
|
+
// Reused scratch + cached DataView (no per-call allocs). `key` is a
|
|
973
|
+
// `cstring` ARG (the engine transcodes the JS string in-engine).
|
|
974
|
+
const out = rateScratch
|
|
975
|
+
const w = Number(
|
|
976
|
+
rateLimiterCheckRaw(inner, key, BigInt(Math.trunc(nowMs)), out, lenOrView(out)),
|
|
977
|
+
)
|
|
978
|
+
if (w === 0) throw new Error('rate limiter check: null handle')
|
|
979
|
+
return unpackRateCheck(out, rateScratchView)
|
|
980
|
+
},
|
|
981
|
+
rateLimiterCheckKey(inner, key, nowMs) {
|
|
982
|
+
// Packed [u8 allowed][u32 remaining LE][i64 reset_ms LE] (13 bytes).
|
|
983
|
+
// Reused scratch + cached DataView (no per-call allocs).
|
|
984
|
+
const out = rateScratch
|
|
985
|
+
const w = Number(
|
|
986
|
+
rateLimiterCheckKeyRaw(inner, BigInt(key), BigInt(Math.trunc(nowMs)), out, lenOrView(out)),
|
|
987
|
+
)
|
|
988
|
+
if (w === 0) throw new Error('rate limiter check: null handle')
|
|
989
|
+
return unpackRateCheck(out, rateScratchView)
|
|
990
|
+
},
|
|
991
|
+
randomToken(byteLen) {
|
|
992
|
+
// cstring return of `byteLen*2` hex chars; byteLen 0 → empty string → empty
|
|
993
|
+
// Uint8Array (napi returns empty too). null = random source failed / >16MiB.
|
|
994
|
+
return cstr(
|
|
995
|
+
randomToken(byteLen),
|
|
996
|
+
'random token: output buffer too small or random source failed',
|
|
997
|
+
)
|
|
998
|
+
},
|
|
999
|
+
randomTokenInto(byteLen, output) {
|
|
1000
|
+
// Pooled sibling: native writes `byteLen*2` hex chars directly into the
|
|
1001
|
+
// caller buffer (no cstring round-trip). Needed-size convention: a write
|
|
1002
|
+
// larger than `output.length` reports the exact required size → throw
|
|
1003
|
+
// (the caller owns the buffer); 0 = real error (cap / RNG).
|
|
1004
|
+
const w = Number(randomTokenInto(byteLen, output, lenOrView(output)))
|
|
1005
|
+
if (w === 0) {
|
|
1006
|
+
throw new Error('random token: random source failed or byteLen exceeds 16 MiB')
|
|
1007
|
+
}
|
|
1008
|
+
if (w > output.length) {
|
|
1009
|
+
throw new Error('random token: output buffer too small')
|
|
1010
|
+
}
|
|
1011
|
+
return w
|
|
1012
|
+
},
|
|
1013
|
+
base64Encode(input, urlSafe, padding) {
|
|
1014
|
+
// Pooled scratch — decoded synchronously to an immutable string (safe).
|
|
1015
|
+
const out = scratchFor(Math.ceil(input.length / 3) * 4)
|
|
1016
|
+
const w = base64EncodeInto(input, out, urlSafe, padding)
|
|
1017
|
+
return decodeUtf8(out.subarray(0, w))
|
|
1018
|
+
},
|
|
1019
|
+
base64EncodeInto,
|
|
1020
|
+
hmacSha256(key, data) {
|
|
1021
|
+
// Pooled 64-byte scratch — decoded synchronously (safe).
|
|
1022
|
+
const out = scratchFor(64)
|
|
1023
|
+
const w = Number(hmacSha256(key, lenOrView(key), data, lenOrView(data), out, lenOrView(out)))
|
|
1024
|
+
if (w === 0) {
|
|
1025
|
+
throw new Error('hmac sha256: output buffer too small')
|
|
1026
|
+
}
|
|
1027
|
+
return decodeUtf8(out.subarray(0, w))
|
|
1028
|
+
},
|
|
1029
|
+
hmacSha256Into(key, data, output) {
|
|
1030
|
+
if (output.length < 64) {
|
|
1031
|
+
throw new Error('hmac sha256: output buffer too small')
|
|
1032
|
+
}
|
|
1033
|
+
const w = Number(
|
|
1034
|
+
hmacSha256(key, lenOrView(key), data, lenOrView(data), output, lenOrView(output)),
|
|
1035
|
+
)
|
|
1036
|
+
if (w === 0) {
|
|
1037
|
+
throw new Error('hmac sha256: output buffer too small')
|
|
1038
|
+
}
|
|
1039
|
+
return w
|
|
1040
|
+
},
|
|
1041
|
+
signCookie(value, secret) {
|
|
1042
|
+
// `value.<64-hex>` returned as a cstring (value.length + 1 + 64 chars).
|
|
1043
|
+
return cstr(
|
|
1044
|
+
signCookie(value, lenOrView(value), secret, lenOrView(secret)),
|
|
1045
|
+
'sign cookie: invalid input',
|
|
1046
|
+
)
|
|
1047
|
+
},
|
|
1048
|
+
signCookieInto(value, secret, output) {
|
|
1049
|
+
// Native pooled `_into`: writes `value.<64-hex>` directly into the caller
|
|
1050
|
+
// buffer (no cstring round-trip — this is why pooled sign_cookie was
|
|
1051
|
+
// previously a REGRESSION vs allocating). Needed-size convention.
|
|
1052
|
+
const w = Number(
|
|
1053
|
+
signCookieInto(
|
|
1054
|
+
value,
|
|
1055
|
+
lenOrView(value),
|
|
1056
|
+
secret,
|
|
1057
|
+
lenOrView(secret),
|
|
1058
|
+
output,
|
|
1059
|
+
lenOrView(output),
|
|
1060
|
+
),
|
|
1061
|
+
)
|
|
1062
|
+
if (w === 0) {
|
|
1063
|
+
throw new Error('sign cookie: invalid input')
|
|
1064
|
+
}
|
|
1065
|
+
if (w > output.length) {
|
|
1066
|
+
throw new Error('sign cookie: output buffer too small')
|
|
1067
|
+
}
|
|
1068
|
+
return w
|
|
1069
|
+
},
|
|
1070
|
+
verifyCookie(signed, secret) {
|
|
1071
|
+
// cstring return; `null` = invalid signature / malformed → null (napi parity).
|
|
1072
|
+
// CAVEAT (refactor): the engine clones a UTF-8 string, so a cookie VALUE
|
|
1073
|
+
// containing non-UTF-8 bytes or NUL cannot round-trip byte-faithfully on
|
|
1074
|
+
// the FFI path (napi still does). Signed values are ASCII in practice.
|
|
1075
|
+
const s = verifyCookie(signed, lenOrView(signed), secret, lenOrView(secret))
|
|
1076
|
+
return s === null ? null : s
|
|
1077
|
+
},
|
|
1078
|
+
verifyCookieInto(signed, secret, output) {
|
|
1079
|
+
// Native pooled `_into`: writes the verified value directly into the
|
|
1080
|
+
// caller buffer. 0 = invalid signature / malformed → null (napi parity,
|
|
1081
|
+
// like the allocating `verifyCookie`); a write larger than `output.length`
|
|
1082
|
+
// reports the exact required size → throw.
|
|
1083
|
+
const w = Number(
|
|
1084
|
+
verifyCookieInto(
|
|
1085
|
+
signed,
|
|
1086
|
+
lenOrView(signed),
|
|
1087
|
+
secret,
|
|
1088
|
+
lenOrView(secret),
|
|
1089
|
+
output,
|
|
1090
|
+
lenOrView(output),
|
|
1091
|
+
),
|
|
1092
|
+
)
|
|
1093
|
+
if (w === 0) {
|
|
1094
|
+
return null
|
|
1095
|
+
}
|
|
1096
|
+
if (w > output.length) {
|
|
1097
|
+
throw new Error('verify cookie: output buffer too small')
|
|
1098
|
+
}
|
|
1099
|
+
return w
|
|
1100
|
+
},
|
|
1101
|
+
csrfToken(secret) {
|
|
1102
|
+
// 129-char `hex.hex` cstring return.
|
|
1103
|
+
return cstr(
|
|
1104
|
+
csrfToken(secret, lenOrView(secret)),
|
|
1105
|
+
'csrf token: output buffer too small or random source failed',
|
|
1106
|
+
)
|
|
1107
|
+
},
|
|
1108
|
+
csrfTokenInto(secret, output) {
|
|
1109
|
+
// Native pooled `_into`: writes the 129-char `hex.hex` token directly
|
|
1110
|
+
// into the caller buffer (no cstring round-trip). Needed-size convention.
|
|
1111
|
+
const w = Number(csrfTokenInto(secret, lenOrView(secret), output, lenOrView(output)))
|
|
1112
|
+
if (w === 0) {
|
|
1113
|
+
throw new Error('csrf token: random source failed')
|
|
1114
|
+
}
|
|
1115
|
+
if (w > output.length) {
|
|
1116
|
+
throw new Error('csrf token: output buffer too small')
|
|
1117
|
+
}
|
|
1118
|
+
return w
|
|
1119
|
+
},
|
|
1120
|
+
passwordHash(password, salt, mCost, tCost, pCost, outLen) {
|
|
1121
|
+
// Pre-size with the EXACT PHC string length (computable from the params),
|
|
1122
|
+
// so the hash runs once — a grow-retry would re-run the whole argon2
|
|
1123
|
+
// hash on a miss. growExact remains the safety net.
|
|
1124
|
+
return decodeUtf8(
|
|
1125
|
+
growExact(
|
|
1126
|
+
(out) =>
|
|
1127
|
+
Number(
|
|
1128
|
+
passwordHash(
|
|
1129
|
+
password,
|
|
1130
|
+
lenOrView(password),
|
|
1131
|
+
salt,
|
|
1132
|
+
lenOrView(salt),
|
|
1133
|
+
mCost,
|
|
1134
|
+
tCost,
|
|
1135
|
+
pCost,
|
|
1136
|
+
outLen,
|
|
1137
|
+
out,
|
|
1138
|
+
lenOrView(out),
|
|
1139
|
+
),
|
|
1140
|
+
),
|
|
1141
|
+
argon2PhcLength(mCost, tCost, pCost, salt.length, outLen),
|
|
1142
|
+
2 * 1024 * 1024,
|
|
1143
|
+
'password hash: output buffer too small',
|
|
1144
|
+
),
|
|
1145
|
+
)
|
|
1146
|
+
},
|
|
1147
|
+
passwordHashBcrypt(password, cost) {
|
|
1148
|
+
// `$2b$CC$` + 22 salt chars + 31 hash chars = 60 chars.
|
|
1149
|
+
const out = new Uint8Array(64)
|
|
1150
|
+
const w = Number(passwordHashBcrypt(password, lenOrView(password), cost, out, lenOrView(out)))
|
|
1151
|
+
if (w === 0) {
|
|
1152
|
+
throw new Error('password hash bcrypt: output buffer too small')
|
|
1153
|
+
}
|
|
1154
|
+
return decodeUtf8(out.subarray(0, w))
|
|
1155
|
+
},
|
|
1156
|
+
pbkdf2Sha256(password, salt, rounds, dkLen) {
|
|
1157
|
+
// Rust clamps dkLen to [1, 1MiB] (PBKDF2_MIN_LEN/MAX_LEN) AFTER sizing its
|
|
1158
|
+
// own buffer — so pre-clamp here so dkLen 0 still yields a 1-byte result.
|
|
1159
|
+
const dk = Math.min(Math.max(dkLen, 1), 1024 * 1024)
|
|
1160
|
+
const out = new Uint8Array(dk)
|
|
1161
|
+
const w = Number(
|
|
1162
|
+
pbkdf2(
|
|
1163
|
+
password,
|
|
1164
|
+
lenOrView(password),
|
|
1165
|
+
salt,
|
|
1166
|
+
lenOrView(salt),
|
|
1167
|
+
rounds,
|
|
1168
|
+
dkLen,
|
|
1169
|
+
out,
|
|
1170
|
+
lenOrView(out),
|
|
1171
|
+
),
|
|
1172
|
+
)
|
|
1173
|
+
if (w === 0) {
|
|
1174
|
+
throw new Error('pbkdf2: output buffer too small')
|
|
1175
|
+
}
|
|
1176
|
+
return out.subarray(0, w)
|
|
1177
|
+
},
|
|
1178
|
+
aeadEncrypt(key, nonce, plaintext, algorithm = 0) {
|
|
1179
|
+
// ciphertext + 16-byte auth tag.
|
|
1180
|
+
const out = new Uint8Array(plaintext.length + 16)
|
|
1181
|
+
const w = Number(
|
|
1182
|
+
aeadEncrypt(
|
|
1183
|
+
key,
|
|
1184
|
+
lenOrView(key),
|
|
1185
|
+
nonce,
|
|
1186
|
+
lenOrView(nonce),
|
|
1187
|
+
plaintext,
|
|
1188
|
+
lenOrView(plaintext),
|
|
1189
|
+
algorithm,
|
|
1190
|
+
out,
|
|
1191
|
+
lenOrView(out),
|
|
1192
|
+
),
|
|
1193
|
+
)
|
|
1194
|
+
if (w === 0) {
|
|
1195
|
+
throw new Error('aead encrypt: output buffer too small or bad parameters')
|
|
1196
|
+
}
|
|
1197
|
+
return out.subarray(0, w)
|
|
1198
|
+
},
|
|
1199
|
+
aeadEncryptInto(key, nonce, plaintext, output, algorithm = 0) {
|
|
1200
|
+
const need = plaintext.length + 16
|
|
1201
|
+
if (output.length < need) {
|
|
1202
|
+
throw new Error('aead encrypt: output buffer too small')
|
|
1203
|
+
}
|
|
1204
|
+
const w = Number(
|
|
1205
|
+
aeadEncrypt(
|
|
1206
|
+
key,
|
|
1207
|
+
lenOrView(key),
|
|
1208
|
+
nonce,
|
|
1209
|
+
lenOrView(nonce),
|
|
1210
|
+
plaintext,
|
|
1211
|
+
lenOrView(plaintext),
|
|
1212
|
+
algorithm,
|
|
1213
|
+
output,
|
|
1214
|
+
lenOrView(output),
|
|
1215
|
+
),
|
|
1216
|
+
)
|
|
1217
|
+
if (w === 0) {
|
|
1218
|
+
throw new Error('aead encrypt: output buffer too small or bad parameters')
|
|
1219
|
+
}
|
|
1220
|
+
return w
|
|
1221
|
+
},
|
|
1222
|
+
aeadDecrypt(key, nonce, ciphertext, algorithm = 0) {
|
|
1223
|
+
const out = new Uint8Array(ciphertext.length)
|
|
1224
|
+
const w = Number(
|
|
1225
|
+
aeadDecrypt(
|
|
1226
|
+
key,
|
|
1227
|
+
lenOrView(key),
|
|
1228
|
+
nonce,
|
|
1229
|
+
lenOrView(nonce),
|
|
1230
|
+
ciphertext,
|
|
1231
|
+
lenOrView(ciphertext),
|
|
1232
|
+
algorithm,
|
|
1233
|
+
out,
|
|
1234
|
+
lenOrView(out),
|
|
1235
|
+
),
|
|
1236
|
+
)
|
|
1237
|
+
return w === 0 ? null : out.subarray(0, w)
|
|
1238
|
+
},
|
|
1239
|
+
|
|
1240
|
+
wsFrameEncode(opcode, payload, mask, fin) {
|
|
1241
|
+
// Header (max 10) + payload + mask key (4).
|
|
1242
|
+
const out = new Uint8Array(payload.length + 14)
|
|
1243
|
+
const w = Number(
|
|
1244
|
+
wsFrameEncode(
|
|
1245
|
+
opcode,
|
|
1246
|
+
payload,
|
|
1247
|
+
lenOrView(payload),
|
|
1248
|
+
flag(mask),
|
|
1249
|
+
flag(fin),
|
|
1250
|
+
out,
|
|
1251
|
+
lenOrView(out),
|
|
1252
|
+
),
|
|
1253
|
+
)
|
|
1254
|
+
if (w === 0) {
|
|
1255
|
+
throw new Error('ws frame encode: output buffer too small')
|
|
1256
|
+
}
|
|
1257
|
+
return out.subarray(0, w)
|
|
1258
|
+
},
|
|
1259
|
+
wsFrameEncodeInto(opcode, payload, mask, fin, output) {
|
|
1260
|
+
// Header (max 10) + payload + mask key (4).
|
|
1261
|
+
const need = payload.length + 14
|
|
1262
|
+
if (output.length < need) {
|
|
1263
|
+
throw new Error('ws frame encode: output buffer too small')
|
|
1264
|
+
}
|
|
1265
|
+
const w = Number(
|
|
1266
|
+
wsFrameEncode(
|
|
1267
|
+
opcode,
|
|
1268
|
+
payload,
|
|
1269
|
+
lenOrView(payload),
|
|
1270
|
+
flag(mask),
|
|
1271
|
+
flag(fin),
|
|
1272
|
+
output,
|
|
1273
|
+
lenOrView(output),
|
|
1274
|
+
),
|
|
1275
|
+
)
|
|
1276
|
+
if (w === 0) {
|
|
1277
|
+
throw new Error('ws frame encode: output buffer too small')
|
|
1278
|
+
}
|
|
1279
|
+
return w
|
|
1280
|
+
},
|
|
1281
|
+
jsonPatch(doc, patch) {
|
|
1282
|
+
return decodeUtf8(
|
|
1283
|
+
growExact(
|
|
1284
|
+
(out) =>
|
|
1285
|
+
Number(jsonPatch(doc, lenOrView(doc), patch, lenOrView(patch), out, lenOrView(out))),
|
|
1286
|
+
Math.min(Math.max(doc.length, patch.length) + 16, 64 * 1024),
|
|
1287
|
+
MAX_JSON_PATCH_OUTPUT,
|
|
1288
|
+
'json patch: output buffer too small or patch inapplicable',
|
|
1289
|
+
),
|
|
1290
|
+
)
|
|
1291
|
+
},
|
|
1292
|
+
gzipCompress(data, level = 6) {
|
|
1293
|
+
// The C ABI streams the compressed output directly into `out` (no internal
|
|
1294
|
+
// Vec — see rust/payload/compress.rs gzip_compress_into). Cap the initial
|
|
1295
|
+
// so large compressible inputs don't pay a full-size allocation (measured:
|
|
1296
|
+
// a 75 KiB input compressed to <1 KiB — COMPRESS_INITIAL_CAP is plenty and
|
|
1297
|
+
// single-pass). growExact handles incompressible data with at most one
|
|
1298
|
+
// exact retry (no re-run loop).
|
|
1299
|
+
return growExact(
|
|
1300
|
+
(out) =>
|
|
1301
|
+
Number(gzipCompress(data, lenOrView(data), Math.min(level, 9), out, lenOrView(out))),
|
|
1302
|
+
Math.min(data.length + COMPRESS_HEADROOM, COMPRESS_INITIAL_CAP),
|
|
1303
|
+
Math.max(data.length * 2 + COMPRESS_HEADROOM, COMPRESS_MAX_CAP),
|
|
1304
|
+
'gzip compress: output buffer too small',
|
|
1305
|
+
)
|
|
1306
|
+
},
|
|
1307
|
+
gzipCompressInto(data, output, level = 6) {
|
|
1308
|
+
const w = Number(
|
|
1309
|
+
gzipCompress(data, lenOrView(data), Math.min(level, 9), output, lenOrView(output)),
|
|
1310
|
+
)
|
|
1311
|
+
// Needed-size convention: w > output.length = too small; w === 0 = real error.
|
|
1312
|
+
if (w === 0) {
|
|
1313
|
+
throw new Error('gzip compress: invalid input')
|
|
1314
|
+
}
|
|
1315
|
+
if (w > output.length) {
|
|
1316
|
+
throw new Error('gzip compress: output buffer too small')
|
|
1317
|
+
}
|
|
1318
|
+
return w
|
|
1319
|
+
},
|
|
1320
|
+
gzipDecompress(data, maxDecompressed) {
|
|
1321
|
+
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
1322
|
+
// Pre-size from the gzip ISIZE trailer via the C probe (exact for
|
|
1323
|
+
// single-member streams) so the happy path is a single native pass; fall
|
|
1324
|
+
// back to a multiplier guess. growExact handles any residual miss with
|
|
1325
|
+
// one exact retry, and invalid input throws immediately (no 64 MiB
|
|
1326
|
+
// grow-to-max allocation storm).
|
|
1327
|
+
const isize = Number(gzipIsize(data, lenOrView(data)))
|
|
1328
|
+
const initial =
|
|
1329
|
+
isize !== 0
|
|
1330
|
+
? Math.min(isize, max)
|
|
1331
|
+
: Math.min(data.length * DECOMPRESS_GUESS_MULTIPLIER_GZIP, DECOMPRESS_FALLBACK_CAP)
|
|
1332
|
+
return growExact(
|
|
1333
|
+
(out) => Number(gzipDecompress(data, lenOrView(data), max, out, lenOrView(out))),
|
|
1334
|
+
initial,
|
|
1335
|
+
max,
|
|
1336
|
+
'gzip decompress: invalid stream or exceeded max decompressed size',
|
|
1337
|
+
)
|
|
1338
|
+
},
|
|
1339
|
+
gzipDecompressInto(data, output, maxDecompressed) {
|
|
1340
|
+
// Pooled sibling — the C ABI streams the decompressed output directly
|
|
1341
|
+
// into the caller buffer via the `_into` core (no internal Vec), keeping
|
|
1342
|
+
// the 64 MiB decompression-bomb cap. Needed-size convention: w === 0 =
|
|
1343
|
+
// real error (invalid stream / cap exceeded); w > output.length = exact
|
|
1344
|
+
// required size → throw (nothing to grow — the caller owns the buffer).
|
|
1345
|
+
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
1346
|
+
const w = Number(gzipDecompress(data, lenOrView(data), max, output, lenOrView(output)))
|
|
1347
|
+
if (w === 0) {
|
|
1348
|
+
throw new Error('gzip decompress: invalid stream or exceeded max decompressed size')
|
|
1349
|
+
}
|
|
1350
|
+
if (w > output.length) {
|
|
1351
|
+
throw new Error('gzip decompress: output buffer too small')
|
|
1352
|
+
}
|
|
1353
|
+
return w
|
|
1354
|
+
},
|
|
1355
|
+
brotliCompress(data, quality = 5) {
|
|
1356
|
+
// Same cap rationale as gzipCompress (streaming core); growExact for
|
|
1357
|
+
// incompressible data.
|
|
1358
|
+
return growExact(
|
|
1359
|
+
(out) =>
|
|
1360
|
+
Number(brotliCompress(data, lenOrView(data), Math.min(quality, 11), out, lenOrView(out))),
|
|
1361
|
+
Math.min(data.length + COMPRESS_HEADROOM, COMPRESS_INITIAL_CAP),
|
|
1362
|
+
Math.max(data.length * 2 + COMPRESS_HEADROOM, COMPRESS_MAX_CAP),
|
|
1363
|
+
'brotli compress: output buffer too small',
|
|
1364
|
+
)
|
|
1365
|
+
},
|
|
1366
|
+
brotliCompressInto(data, output, quality = 5) {
|
|
1367
|
+
const w = Number(
|
|
1368
|
+
brotliCompress(data, lenOrView(data), Math.min(quality, 11), output, lenOrView(output)),
|
|
1369
|
+
)
|
|
1370
|
+
// Needed-size convention: w > output.length = too small; w === 0 = real error.
|
|
1371
|
+
if (w === 0) {
|
|
1372
|
+
throw new Error('brotli compress: invalid input')
|
|
1373
|
+
}
|
|
1374
|
+
if (w > output.length) {
|
|
1375
|
+
throw new Error('brotli compress: output buffer too small')
|
|
1376
|
+
}
|
|
1377
|
+
return w
|
|
1378
|
+
},
|
|
1379
|
+
brotliDecompress(data, maxDecompressed) {
|
|
1380
|
+
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
1381
|
+
// Brotli has no cheap trailer size: DECOMPRESS_GUESS_MULTIPLIER_BROTLI×
|
|
1382
|
+
// initial covers typical JSON/text ratios (~10-30x) while the fallback
|
|
1383
|
+
// cap bounds over-allocation on large streams; growExact handles
|
|
1384
|
+
// higher-ratio streams with one exact retry.
|
|
1385
|
+
return growExact(
|
|
1386
|
+
(out) => Number(brotliDecompress(data, lenOrView(data), max, out, lenOrView(out))),
|
|
1387
|
+
Math.min(
|
|
1388
|
+
Math.max(data.length * DECOMPRESS_GUESS_MULTIPLIER_BROTLI, DECOMPRESS_MIN_INITIAL),
|
|
1389
|
+
DECOMPRESS_FALLBACK_CAP,
|
|
1390
|
+
),
|
|
1391
|
+
max,
|
|
1392
|
+
'brotli decompress: invalid stream or exceeded max decompressed size',
|
|
1393
|
+
)
|
|
1394
|
+
},
|
|
1395
|
+
brotliDecompressInto(data, output, maxDecompressed) {
|
|
1396
|
+
// Pooled sibling — streams into the caller buffer, keeps the 64 MiB cap
|
|
1397
|
+
// (same convention as gzipDecompressInto above).
|
|
1398
|
+
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
1399
|
+
const w = Number(brotliDecompress(data, lenOrView(data), max, output, lenOrView(output)))
|
|
1400
|
+
if (w === 0) {
|
|
1401
|
+
throw new Error('brotli decompress: invalid stream or exceeded max decompressed size')
|
|
1402
|
+
}
|
|
1403
|
+
if (w > output.length) {
|
|
1404
|
+
throw new Error('brotli decompress: output buffer too small')
|
|
1405
|
+
}
|
|
1406
|
+
return w
|
|
1407
|
+
},
|
|
1408
|
+
|
|
1409
|
+
httpParseRequestPackedInto(input, output) {
|
|
1410
|
+
const w = Number(httpParsePacked(input, lenOrView(input), output, lenOrView(output)))
|
|
1411
|
+
if (w === 0 && input.length !== 0) {
|
|
1412
|
+
throw new Error('http parse: output buffer too small or malformed request')
|
|
1413
|
+
}
|
|
1414
|
+
return w
|
|
1415
|
+
},
|
|
1416
|
+
queryParsePackedInto(input, output) {
|
|
1417
|
+
const w = Number(queryParsePacked(input, lenOrView(input), output, lenOrView(output)))
|
|
1418
|
+
// Needed-size convention: w > output.length = exact required size →
|
|
1419
|
+
// too-small (throw — the caller owns the buffer, nothing to grow);
|
|
1420
|
+
// w === 0 = real error (malformed %XX).
|
|
1421
|
+
if (w === 0 || w > output.length) {
|
|
1422
|
+
throw new Error('query parse: output buffer too small')
|
|
1423
|
+
}
|
|
1424
|
+
return w
|
|
1425
|
+
},
|
|
1426
|
+
cookieParsePackedInto(input, output) {
|
|
1427
|
+
const w = Number(cookieParsePacked(input, lenOrView(input), output, lenOrView(output)))
|
|
1428
|
+
// Needed-size convention (same as queryParsePackedInto).
|
|
1429
|
+
if (w === 0 || w > output.length) {
|
|
1430
|
+
throw new Error('cookie parse: output buffer too small')
|
|
1431
|
+
}
|
|
1432
|
+
return w
|
|
1433
|
+
},
|
|
1434
|
+
|
|
1435
|
+
// ── Excluded-surface additions ──────────────────────────────────
|
|
1436
|
+
jwtSignBytes(claimsJson, secret, ttl, now) {
|
|
1437
|
+
// Compact HS256 token returned as a cstring (engine-cloned) — the Rust
|
|
1438
|
+
// side builds the whole token and the JS pays zero decode. ttl<=0 = no
|
|
1439
|
+
// iat/exp (napi Option<i64> sentinel). i64 args must be BigInt.
|
|
1440
|
+
return cstr(
|
|
1441
|
+
jwtSignBytes(
|
|
1442
|
+
claimsJson,
|
|
1443
|
+
lenOrView(claimsJson),
|
|
1444
|
+
secret,
|
|
1445
|
+
lenOrView(secret),
|
|
1446
|
+
BigInt(ttl),
|
|
1447
|
+
BigInt(now),
|
|
1448
|
+
),
|
|
1449
|
+
'jwt sign: invalid claims JSON',
|
|
1450
|
+
)
|
|
1451
|
+
},
|
|
1452
|
+
jwtSignBytesInto(claimsJson, secret, ttl, now, output) {
|
|
1453
|
+
// Native pooled `_into`: writes the compact token directly into the
|
|
1454
|
+
// caller buffer (no cstring round-trip). Needed-size convention: a write
|
|
1455
|
+
// larger than `output.length` reports the exact required size → throw;
|
|
1456
|
+
// 0 = invalid claims JSON.
|
|
1457
|
+
const w = Number(
|
|
1458
|
+
jwtSignBytesInto(
|
|
1459
|
+
claimsJson,
|
|
1460
|
+
lenOrView(claimsJson),
|
|
1461
|
+
secret,
|
|
1462
|
+
lenOrView(secret),
|
|
1463
|
+
BigInt(ttl),
|
|
1464
|
+
BigInt(now),
|
|
1465
|
+
output,
|
|
1466
|
+
lenOrView(output),
|
|
1467
|
+
),
|
|
1468
|
+
)
|
|
1469
|
+
if (w === 0) {
|
|
1470
|
+
throw new Error('jwt sign: invalid claims JSON')
|
|
1471
|
+
}
|
|
1472
|
+
if (w > output.length) {
|
|
1473
|
+
throw new Error('jwt sign: output buffer too small')
|
|
1474
|
+
}
|
|
1475
|
+
return w
|
|
1476
|
+
},
|
|
1477
|
+
wsFrameDecodePacked(data) {
|
|
1478
|
+
// Max packed output = 6-byte header + payload.
|
|
1479
|
+
const out = new Uint8Array(data.length + 6)
|
|
1480
|
+
const w = Number(wsFrameDecodePacked(data, lenOrView(data), out, lenOrView(out)))
|
|
1481
|
+
return w === 0 ? null : out.subarray(0, w)
|
|
1482
|
+
},
|
|
1483
|
+
wsFrameDecodePackedInto(data, output) {
|
|
1484
|
+
// Pooled sibling: the caller provides the output buffer, sized to at
|
|
1485
|
+
// least `data.length + 6` (6-byte header + payload — the Rust core
|
|
1486
|
+
// returns 0 for BOTH too-small and malformed, so the caller must size
|
|
1487
|
+
// to the max; the allocating path always does). 0 = malformed → null,
|
|
1488
|
+
// mirroring the allocating path's return contract.
|
|
1489
|
+
const w = Number(wsFrameDecodePacked(data, lenOrView(data), output, lenOrView(output)))
|
|
1490
|
+
return w === 0 ? null : w
|
|
1491
|
+
},
|
|
1492
|
+
multipartParsePacked(body, boundary) {
|
|
1493
|
+
return growExact(
|
|
1494
|
+
(out) =>
|
|
1495
|
+
Number(
|
|
1496
|
+
multipartParsePacked(
|
|
1497
|
+
body,
|
|
1498
|
+
lenOrView(body),
|
|
1499
|
+
boundary,
|
|
1500
|
+
lenOrView(boundary),
|
|
1501
|
+
out,
|
|
1502
|
+
lenOrView(out),
|
|
1503
|
+
),
|
|
1504
|
+
),
|
|
1505
|
+
Math.min(body.length + boundary.length + 64, 64 * 1024),
|
|
1506
|
+
128 * 1024 * 1024,
|
|
1507
|
+
'multipart parse: output buffer too small',
|
|
1508
|
+
)
|
|
1509
|
+
},
|
|
1510
|
+
multipartParsePackedInto(body, boundary, output) {
|
|
1511
|
+
// Pooled sibling: the caller provides the output buffer. Needed-size
|
|
1512
|
+
// convention (same as queryParsePackedInto): w > output.length = exact
|
|
1513
|
+
// required size → throw; w === 0 = real error (malformed).
|
|
1514
|
+
const w = Number(
|
|
1515
|
+
multipartParsePacked(
|
|
1516
|
+
body,
|
|
1517
|
+
lenOrView(body),
|
|
1518
|
+
boundary,
|
|
1519
|
+
lenOrView(boundary),
|
|
1520
|
+
output,
|
|
1521
|
+
lenOrView(output),
|
|
1522
|
+
),
|
|
1523
|
+
)
|
|
1524
|
+
if (w === 0 || w > output.length) {
|
|
1525
|
+
throw new Error('multipart parse: output buffer too small or malformed body')
|
|
1526
|
+
}
|
|
1527
|
+
return w
|
|
1528
|
+
},
|
|
1529
|
+
formParsePackedInto(input, output) {
|
|
1530
|
+
const w = Number(formParsePacked(input, lenOrView(input), output, lenOrView(output)))
|
|
1531
|
+
// Needed-size convention (form aliases the query core — same convention).
|
|
1532
|
+
if (w === 0 || w > output.length) {
|
|
1533
|
+
throw new Error('form parse: output buffer too small')
|
|
1534
|
+
}
|
|
1535
|
+
return w
|
|
1536
|
+
},
|
|
1537
|
+
|
|
1538
|
+
// ── Wired WIP surface (JWT verify, task group) ──
|
|
1539
|
+
jsonParsePacked(input) {
|
|
1540
|
+
// Packed token stream (deduped string table + typed value tree) — the
|
|
1541
|
+
// scalar wrapper assembles the JS value from the tokens with NO second
|
|
1542
|
+
// JSON text parse. `0` = invalid JSON (real error → growExact throws);
|
|
1543
|
+
// `w > out.length` = exact required size (one exact retry).
|
|
1544
|
+
return growExact(
|
|
1545
|
+
(out) => Number(jsonParsePackedSym(input, lenOrView(input), out, lenOrView(out))),
|
|
1546
|
+
Math.min(input.length + (input.length >> 1), 16 * 1024 * 1024),
|
|
1547
|
+
Math.max(1024 * 1024, input.length * 16),
|
|
1548
|
+
'json parse packed: invalid JSON or output buffer too small',
|
|
1549
|
+
)
|
|
1550
|
+
},
|
|
1551
|
+
parseMediaType(input) {
|
|
1552
|
+
// Packed verdict: [u32 mediaTypeLen][mediaType][u32 charsetLen
|
|
1553
|
+
// (0xFFFFFFFF = none)][charset][u32 boundaryLen][boundary]
|
|
1554
|
+
// [u32 paramCount]{[u32 keyLen][key][u32 valLen][val]}. 0 = invalid media
|
|
1555
|
+
// type (real error → growExact throws); w > output.length = exact needed
|
|
1556
|
+
// size (one exact retry).
|
|
1557
|
+
return growExact(
|
|
1558
|
+
(out) => Number(parseMediaTypeSym(input, lenOrView(input), out, lenOrView(out))),
|
|
1559
|
+
Math.min(input.length + 64, 64 * 1024),
|
|
1560
|
+
1024 * 1024,
|
|
1561
|
+
'media type parse: invalid media type or output buffer too small',
|
|
1562
|
+
)
|
|
1563
|
+
},
|
|
1564
|
+
parseHttpDate(input) {
|
|
1565
|
+
// Packed [u8 ok][i64 secs LE] (9 B) / 1 B (ok=0). A too-small buffer is
|
|
1566
|
+
// never a 0 here — the Rust side reports the exact size (9/1) — so a 9-byte
|
|
1567
|
+
// buffer always succeeds. Reused scratch + cached DataView (no per-call
|
|
1568
|
+
// allocs).
|
|
1569
|
+
const out = dateScratch
|
|
1570
|
+
const w = Number(parseHttpDateSym(input, lenOrView(input), out, lenOrView(out)))
|
|
1571
|
+
if (w === 0) {
|
|
1572
|
+
throw new Error('http date parse: output buffer too small')
|
|
1573
|
+
}
|
|
1574
|
+
if (out[0] === 0) return null
|
|
1575
|
+
return dateScratchView.getBigInt64(1, true)
|
|
1576
|
+
},
|
|
1577
|
+
parseAcceptEncoding(input) {
|
|
1578
|
+
// Packed: [u32 count]{[u32 encLen][enc][f32 q][u32 order]} (empty header →
|
|
1579
|
+
// count 0, 4 bytes). 0 = too small (real error → growExact throws).
|
|
1580
|
+
return growExact(
|
|
1581
|
+
(out) => Number(parseAcceptEncodingSym(input, lenOrView(input), out, lenOrView(out))),
|
|
1582
|
+
Math.min(input.length + 64, 64 * 1024),
|
|
1583
|
+
1024 * 1024,
|
|
1584
|
+
'accept encoding parse: output buffer too small',
|
|
1585
|
+
)
|
|
1586
|
+
},
|
|
1587
|
+
urlEncodeQuery(input) {
|
|
1588
|
+
// Packed pairs `[u32 count]{[u32 keyLen][key][u32 valLen][val]}` (the JS
|
|
1589
|
+
// `packPairs` layout) → percent-encoded query TEXT as a cstring, keys
|
|
1590
|
+
// SORTED (matches the napi BTreeMap ordering). `null` = malformed packed
|
|
1591
|
+
// input / non-UTF-8 (napi parity: throws).
|
|
1592
|
+
return urlEncodeQuerySym(input, lenOrView(input))
|
|
1593
|
+
},
|
|
1594
|
+
urlResolve(base, reference) {
|
|
1595
|
+
// RFC 3986 resolution → cstring; `null` = non-UTF-8 input (napi parity).
|
|
1596
|
+
return urlResolveSym(base, lenOrView(base), reference, lenOrView(reference))
|
|
1597
|
+
},
|
|
1598
|
+
urlBuilderResolve(inner, reference) {
|
|
1599
|
+
// Opaque-handle resolve against a `UrlBuilder`'s PRECOMPILED base. 0 =
|
|
1600
|
+
// null handle / non-UTF-8 reference (real error → growExact throws);
|
|
1601
|
+
// w > output.length = exact needed size (one exact retry).
|
|
1602
|
+
return growExact(
|
|
1603
|
+
(out) =>
|
|
1604
|
+
Number(urlBuilderResolveRaw(inner, reference, lenOrView(reference), out, lenOrView(out))),
|
|
1605
|
+
Math.min(reference.length * 2 + 128, 64 * 1024),
|
|
1606
|
+
1024 * 1024,
|
|
1607
|
+
'url builder resolve: invalid reference or output buffer too small',
|
|
1608
|
+
)
|
|
1609
|
+
},
|
|
1610
|
+
mimeFromExtension(ext) {
|
|
1611
|
+
// Extension → MIME → cstring (cstring ARG: the engine transcodes the JS
|
|
1612
|
+
// extension in-engine); unknown → `application/octet-stream` (never null).
|
|
1613
|
+
return mimeFromExtensionSym(ext)
|
|
1614
|
+
},
|
|
1615
|
+
jwtVerify(token, secret, nowSeconds) {
|
|
1616
|
+
// Verify an HS256 JWT → claims as a JSON cstring; `null` = invalid
|
|
1617
|
+
// signature / expired / malformed (napi Option parity). `now` is an i64
|
|
1618
|
+
// C arg → must be passed as BigInt.
|
|
1619
|
+
return jwtVerifySym(token, lenOrView(token), secret, lenOrView(secret), BigInt(nowSeconds))
|
|
1620
|
+
},
|
|
1621
|
+
|
|
1622
|
+
// ── Napi-only hot ops, now FFI (kills the napi crossing) ──────
|
|
1623
|
+
httpDateInto(secs, output) {
|
|
1624
|
+
// 29-byte HTTP-date into the caller buffer. 0 = too-small buffer or
|
|
1625
|
+
// out-of-range year (napi httpDateInto throws on both).
|
|
1626
|
+
const w = Number(httpDateIntoRaw(secs, output, lenOrView(output)))
|
|
1627
|
+
if (w === 0) {
|
|
1628
|
+
throw new Error('http date: output buffer too small or year out of range')
|
|
1629
|
+
}
|
|
1630
|
+
return w
|
|
1631
|
+
},
|
|
1632
|
+
httpDate(secs) {
|
|
1633
|
+
// Allocating sibling — 32-byte buffer always fits the 29-byte date, so
|
|
1634
|
+
// the only 0 case is an out-of-range year (napi falls back to the
|
|
1635
|
+
// allocating format! there — mirror with Date.toUTCString). Pooled
|
|
1636
|
+
// scratch (decoded synchronously — safe).
|
|
1637
|
+
const out = scratchFor(32)
|
|
1638
|
+
const w = Number(httpDateIntoRaw(secs ?? 0, out, lenOrView(out)))
|
|
1639
|
+
if (w !== 0) return decodeUtf8(out.subarray(0, w))
|
|
1640
|
+
return new Date((secs ?? 0) * 1000).toUTCString()
|
|
1641
|
+
},
|
|
1642
|
+
sseEncodeEvent(event, data, id, retry) {
|
|
1643
|
+
// One SSE event → bytes. event/id are encoded to UTF-8 only when
|
|
1644
|
+
// non-null (flag bits 1/2/4 = present) so a present-but-empty string is
|
|
1645
|
+
// distinct from absent (napi Option parity). growExact with the needed-size
|
|
1646
|
+
// convention; `data.length + 64` covers the common single-line case.
|
|
1647
|
+
const ev = event === null ? EMPTY_VIEW : encodeUtf8(event)
|
|
1648
|
+
const idv = id === null ? EMPTY_VIEW : encodeUtf8(id)
|
|
1649
|
+
const flags = (event === null ? 0 : 1) | (id === null ? 0 : 2) | (retry === null ? 0 : 4)
|
|
1650
|
+
return growExact(
|
|
1651
|
+
(out) =>
|
|
1652
|
+
Number(
|
|
1653
|
+
sseEncodeIntoRaw(
|
|
1654
|
+
ev,
|
|
1655
|
+
lenOrView(ev),
|
|
1656
|
+
data,
|
|
1657
|
+
lenOrView(data),
|
|
1658
|
+
idv,
|
|
1659
|
+
lenOrView(idv),
|
|
1660
|
+
flags,
|
|
1661
|
+
retry ?? 0,
|
|
1662
|
+
out,
|
|
1663
|
+
lenOrView(out),
|
|
1664
|
+
),
|
|
1665
|
+
),
|
|
1666
|
+
data.length + 64,
|
|
1667
|
+
1 << 20,
|
|
1668
|
+
'sse encode: output buffer too small',
|
|
1669
|
+
)
|
|
1670
|
+
},
|
|
1671
|
+
sseEncodeEventInto(event, data, id, retry, output) {
|
|
1672
|
+
// Pooled sibling — caller-owned output buffer (sized ≥ `data.length + 64`
|
|
1673
|
+
// for the common single-line case). Needed-size convention: w > output.length
|
|
1674
|
+
// = exact required size → throw; w === 0 = real error (invalid UTF-8).
|
|
1675
|
+
const ev = event === null ? EMPTY_VIEW : encodeUtf8(event)
|
|
1676
|
+
const idv = id === null ? EMPTY_VIEW : encodeUtf8(id)
|
|
1677
|
+
const flags = (event === null ? 0 : 1) | (id === null ? 0 : 2) | (retry === null ? 0 : 4)
|
|
1678
|
+
const w = Number(
|
|
1679
|
+
sseEncodeIntoRaw(
|
|
1680
|
+
ev,
|
|
1681
|
+
lenOrView(ev),
|
|
1682
|
+
data,
|
|
1683
|
+
lenOrView(data),
|
|
1684
|
+
idv,
|
|
1685
|
+
lenOrView(idv),
|
|
1686
|
+
flags,
|
|
1687
|
+
retry ?? 0,
|
|
1688
|
+
output,
|
|
1689
|
+
lenOrView(output),
|
|
1690
|
+
),
|
|
1691
|
+
)
|
|
1692
|
+
if (w === 0) {
|
|
1693
|
+
throw new Error('sse encode: invalid UTF-8 in event/id')
|
|
1694
|
+
}
|
|
1695
|
+
if (w > output.length) {
|
|
1696
|
+
throw new Error('sse encode: output buffer too small')
|
|
1697
|
+
}
|
|
1698
|
+
return w
|
|
1699
|
+
},
|
|
1700
|
+
|
|
1701
|
+
ingressHandlePacked(inner, input, body, output) {
|
|
1702
|
+
const w = Number(
|
|
1703
|
+
ingressHandlePacked(
|
|
1704
|
+
inner,
|
|
1705
|
+
input,
|
|
1706
|
+
lenOrView(input),
|
|
1707
|
+
body ?? EMPTY_VIEW,
|
|
1708
|
+
// Under `buffer_length` the length slot must be a view (the engine
|
|
1709
|
+
// reads byteLength off it) — `EMPTY_VIEW` has byteLength 0, matching
|
|
1710
|
+
// a null body; under `(ptr,usize)` it's the explicit length 0.
|
|
1711
|
+
body ? lenOrView(body) : lenOrView(EMPTY_VIEW),
|
|
1712
|
+
output,
|
|
1713
|
+
lenOrView(output),
|
|
1714
|
+
),
|
|
1715
|
+
)
|
|
1716
|
+
if (w === 0) {
|
|
1717
|
+
throw new Error('ingress handle: output buffer too small or pipeline error')
|
|
1718
|
+
}
|
|
1719
|
+
return w
|
|
1720
|
+
},
|
|
1721
|
+
ingressHandleComponents(inner, methodKind, url, ip, rid, headers, body, output) {
|
|
1722
|
+
// `url`/`ip` are passed as JS strings to `cstring` args — the engine
|
|
1723
|
+
// transcodes them to call-scoped NUL-terminated UTF-8 buffers in-engine
|
|
1724
|
+
// (no JS-side `Buffer.write` encode, no frame assembly for URL/IP).
|
|
1725
|
+
const w = Number(
|
|
1726
|
+
ingressHandleComponentsSym(
|
|
1727
|
+
inner,
|
|
1728
|
+
methodKind,
|
|
1729
|
+
url,
|
|
1730
|
+
ip,
|
|
1731
|
+
rid,
|
|
1732
|
+
lenOrView(rid),
|
|
1733
|
+
headers,
|
|
1734
|
+
lenOrView(headers),
|
|
1735
|
+
body ?? EMPTY_VIEW,
|
|
1736
|
+
body ? lenOrView(body) : lenOrView(EMPTY_VIEW),
|
|
1737
|
+
output,
|
|
1738
|
+
lenOrView(output),
|
|
1739
|
+
),
|
|
1740
|
+
)
|
|
1741
|
+
if (w === 0) {
|
|
1742
|
+
throw new Error('ingress components: output buffer too small or pipeline error')
|
|
1743
|
+
}
|
|
1744
|
+
return w
|
|
1745
|
+
},
|
|
1746
|
+
ingressLayout(out) {
|
|
1747
|
+
const w = Number(ingressLayoutSym(out, lenOrView(out)))
|
|
1748
|
+
if (w !== out.length) {
|
|
1749
|
+
throw new Error('ingress layout: output buffer too small')
|
|
1750
|
+
}
|
|
1751
|
+
return w
|
|
1752
|
+
},
|
|
1753
|
+
|
|
1754
|
+
routeCompile(descriptor) {
|
|
1755
|
+
const handle = Number(routeCompileSym(descriptor, lenOrView(descriptor)))
|
|
1756
|
+
if (handle === 0) {
|
|
1757
|
+
throw new Error('route compile: invalid route descriptor')
|
|
1758
|
+
}
|
|
1759
|
+
return handle
|
|
1760
|
+
},
|
|
1761
|
+
routeRun(handle, frame, output) {
|
|
1762
|
+
// Needed-size convention: `0` = real error (malformed frame / panic); a
|
|
1763
|
+
// write larger than `output.length` is the EXACT required size (caller
|
|
1764
|
+
// allocates once and retries) — only a `0` write throws here.
|
|
1765
|
+
const w = Number(routeRunSym(handle, frame, lenOrView(frame), output, lenOrView(output)))
|
|
1766
|
+
if (w === 0) {
|
|
1767
|
+
throw new Error('route run: malformed frame or pipeline error')
|
|
1768
|
+
}
|
|
1769
|
+
return w
|
|
1770
|
+
},
|
|
1771
|
+
routeDestroy(handle) {
|
|
1772
|
+
routeDestroySym(handle)
|
|
1773
|
+
},
|
|
1774
|
+
}
|
|
1775
|
+
}
|