castrum 0.9.0 → 0.9.2
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/README.md +153 -630
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1653 -1817
- package/dist/index.js +6172 -4294
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +111 -17
- package/docs/BENCHMARKS.md +117 -49
- package/docs/CASE_STUDY.md +243 -0
- package/docs/ENVIRONMENT.md +24 -7
- package/docs/FFI_BUN_GUIDE.md +335 -0
- package/docs/GETTING_STARTED.md +18 -34
- package/docs/HOW_TO_ADD_AN_OP.md +180 -0
- package/docs/INGRESS-ROUTER.md +82 -0
- package/docs/INGRESS.md +11 -7
- package/docs/NATIVE-ROUTE.md +77 -0
- package/docs/REPO_MAP.md +103 -53
- package/docs/adr/0001-two-wire-formats.md +1 -1
- package/docs/adr/0003-bun-builtins-delegation.md +6 -5
- package/docs/ai/LOCAL_DEV.md +81 -0
- package/docs/ai/TREE.md +1070 -0
- package/docs/bun-builtins-decision-matrix.md +25 -18
- package/index.ts +9 -16
- package/package.json +41 -28
- package/src/ingress/body.ts +167 -27
- package/src/ingress/decode/result-base.ts +10 -2
- package/src/ingress/handlers.ts +116 -285
- package/src/ingress/headers/security.ts +41 -0
- package/src/ingress/health.ts +1 -0
- package/src/ingress/index.ts +45 -169
- package/src/ingress/native-route.ts +182 -0
- package/src/ingress/packing/gather-raw-headers.ts +110 -22
- package/src/ingress/packing/header-packing.ts +5 -5
- package/src/ingress/packing/input-packer.ts +17 -3
- package/src/ingress/packing/route-wire.ts +197 -0
- package/src/ingress/packing/select-headers.ts +20 -7
- package/src/ingress/path-matcher.ts +115 -0
- package/src/ingress/response/baked-response.ts +302 -0
- package/src/ingress/router.ts +277 -0
- package/src/ingress/routes/common.ts +50 -1
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +3 -13
- package/src/ingress/routes/index.ts +5 -4
- package/src/ingress/routes/json-write.ts +26 -47
- package/src/ingress/routes/native.ts +130 -0
- package/src/ingress/routes/options.ts +2 -12
- package/src/ingress/routes/read.ts +3 -13
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +7 -7
- package/src/ingress/server.ts +83 -112
- package/src/ingress/sync.ts +151 -0
- package/src/ingress/types.ts +44 -1
- package/src/integration/pipeline.ts +3 -0
- package/src/integration/streaming.ts +2 -3
- package/src/integration/websocket.ts +4 -3
- package/src/loader/batch.ts +4 -0
- package/src/loader/cost.ts +1 -0
- package/src/loader/index.ts +43 -228
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/build/codecs.ts +736 -0
- package/src/native/ffi/build/compress.ts +248 -0
- package/src/native/ffi/build/instances.ts +598 -0
- package/src/native/ffi/build/parse.ts +539 -0
- package/src/native/ffi/build/util.ts +140 -0
- package/src/native/ffi/build.ts +94 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +22 -0
- package/src/native/ffi/types.ts +499 -0
- package/src/native/ffi.ts +307 -1357
- package/src/native/loader.ts +52 -6
- package/src/native/types/instances.ts +279 -0
- package/src/native/types.ts +41 -238
- 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 +1 -1
- 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 +5 -6
- package/src/rust-ffi/context.ts +39 -10
- package/src/rust-ffi/index.ts +28 -17
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +1 -1
- package/src/rust-ffi/packed.ts +40 -2
- package/src/rust-ffi/proven.ts +16 -30
- package/src/rust-ffi/scalar/crypto.ts +188 -107
- package/src/rust-ffi/scalar/factories.ts +257 -27
- package/src/rust-ffi/scalar/hashing.ts +43 -41
- package/src/rust-ffi/scalar/http.ts +208 -77
- package/src/rust-ffi/scalar/index.ts +3 -3
- package/src/rust-ffi/scalar/interface.ts +88 -213
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +28 -12
- package/src/rust-ffi/scalar/payload.ts +159 -42
- package/src/rust-ffi/text.ts +31 -5
- package/src/selection.ts +27 -31
- package/src/shared/adaptive.ts +1 -2
- package/src/shared/buffer-pool.ts +72 -17
- package/src/shared/bytes.ts +75 -6
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +4 -4
- package/src/shared/log.ts +1 -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.ts → packed/wire.ts} +31 -39
- package/src/shared/proven.ts +189 -496
- package/src/shared/runtime.ts +20 -25
- package/src/shared/uuid.ts +12 -28
- package/src/data/json-rows.ts +0 -33
- package/src/rust-ffi/batch.ts +0 -431
- package/src/shared/bench-classify.ts +0 -209
package/src/native/ffi.ts
CHANGED
|
@@ -1,271 +1,58 @@
|
|
|
1
|
-
// src/native/ffi.ts — Bun-only C-ABI fast path via `bun:ffi
|
|
1
|
+
// src/native/ffi.ts — Bun-only C-ABI fast path via `bun:ffi` (transport core).
|
|
2
2
|
//
|
|
3
3
|
// Bun JIT-compiles `bun:ffi` calls down to direct native calls (~10-20ns
|
|
4
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
|
|
6
|
-
// under Bun we can `dlopen` it and call the hot scalar functions directly
|
|
7
|
-
//
|
|
5
|
+
// loads through napi-rs ALSO exports `extern "C"` symbols (`rust/ffi/`), so
|
|
6
|
+
// under Bun we can `dlopen` it and call the hot scalar functions directly.
|
|
7
|
+
//
|
|
8
|
+
// Structure: this file is the transport CORE (transport selection + probe +
|
|
9
|
+
// dlopen + symbol binding + bind-time self-test gating). The per-call wrapper
|
|
10
|
+
// surface lives in `build.ts` (delegating to `build/{codecs,compress,parse,
|
|
11
|
+
// instances}.ts`), and the pure pieces beside it: `ffi/types.ts` (BunFFI /
|
|
12
|
+
// FfiMode / Raw signatures), `ffi/constants.ts` (caps + sizing + self-test
|
|
13
|
+
// vectors), `ffi/selftest.ts` (the bind-time self-test). This file re-exports
|
|
14
|
+
// `getBunFFI` + `BunFFI` so existing importers keep working unchanged.
|
|
8
15
|
//
|
|
9
16
|
// Safety / correctness strategy:
|
|
10
17
|
// - Lazily bound (no addon/ffi work until first use).
|
|
11
|
-
// - A one-time SELF-TEST runs at bind time
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// - Decoders that can fail (hexDecode / urlDecode / base64Decode) ARE exposed
|
|
17
|
-
// here with parity error semantics: a `0` write on non-empty input throws
|
|
18
|
-
// (the napi decoders throw rather than return a short buffer). The only
|
|
19
|
-
// unavoidable divergence is decompressing a stream that yields exactly 0
|
|
20
|
-
// bytes, which the C ABI can't distinguish from "too small" (napi returns
|
|
21
|
-
// an empty buffer; ffi throws) — degenerate and not exercised by tests.
|
|
18
|
+
// - A one-time SELF-TEST runs at bind time (ffi/selftest.ts); any failure
|
|
19
|
+
// disables ffi and falls back to the napi addon.
|
|
20
|
+
// - Decoders that can fail ARE exposed with parity error semantics: a `0`
|
|
21
|
+
// write on non-empty input throws (the napi decoders throw rather than
|
|
22
|
+
// return a short buffer).
|
|
22
23
|
// - Never called under Node (falls back to napi immediately).
|
|
23
24
|
//
|
|
24
25
|
// Buffer ABI: input/out pointers are `(ptr, len)` pairs; the JS wrapper passes
|
|
25
26
|
// `(view, view.length)` and bun:ffi converts the TypedArray to its pointer.
|
|
26
27
|
|
|
27
|
-
// Type-only: erased at compile time (no runtime import of `bun:ffi` — that
|
|
28
|
-
// stays lazy inside `bind()`). `FFITypeOrString` is the dlopen ABI-string union.
|
|
29
28
|
import type { FFITypeOrString } from 'bun:ffi'
|
|
30
29
|
import { resolveEnvVar } from '../shared/env'
|
|
31
30
|
import { isBun } from '../shared/runtime'
|
|
31
|
+
import { build } from './ffi/build'
|
|
32
|
+
import { selfTest } from './ffi/selftest'
|
|
33
|
+
import type { BunFFI, FfiMode } from './ffi/types'
|
|
32
34
|
// Static import: loader.ts has no side effects (path resolution only — no
|
|
33
35
|
// dlopen), so this adds zero import-time cost while letting `bind()` reuse the
|
|
34
36
|
// exact same resolved `.node` path the napi fallback uses (the shared seam).
|
|
35
37
|
import { getAddonPath } from './loader'
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
export interface BunFFI {
|
|
41
|
-
crc32(input: Uint8Array): number
|
|
42
|
-
fnv1a64(input: Uint8Array): bigint
|
|
43
|
-
xxh3(input: Uint8Array): bigint
|
|
44
|
-
jsonValid(input: Uint8Array): boolean
|
|
45
|
-
/** Lowercase-hex encode into a fresh `Uint8Array` (size `input.length * 2`). */
|
|
46
|
-
hexEncode(input: Uint8Array): Uint8Array
|
|
47
|
-
/** Lowercase-hex encode into `output`; returns bytes written. */
|
|
48
|
-
hexEncodeInto(input: Uint8Array, output: Uint8Array): number
|
|
49
|
-
/** RFC 3986 percent-encode into a fresh buffer (size `input.length * 3`). */
|
|
50
|
-
urlEncode(input: Uint8Array): Uint8Array
|
|
51
|
-
/** RFC 3986 percent-encode into `output`; returns bytes written. */
|
|
52
|
-
urlEncodeInto(input: Uint8Array, output: Uint8Array): number
|
|
53
|
-
|
|
54
|
-
// ── Validators (u8 → boolean) ─────────────────────────────────────
|
|
55
|
-
validateEmail(input: Uint8Array): boolean
|
|
56
|
-
validateUuid(input: Uint8Array): boolean
|
|
57
|
-
validateIpv4(input: Uint8Array): boolean
|
|
58
|
-
validateIpv6(input: Uint8Array): boolean
|
|
59
|
-
/** Sum of `id` fields across a JSON array → bigint (throws on non-array input). */
|
|
60
|
-
jsonSumIds(input: Uint8Array): bigint
|
|
61
|
-
|
|
62
|
-
// ── Constant-time verify (u8 → boolean) ───────────────────────────
|
|
63
|
-
hmacSha256Verify(key: Uint8Array, data: Uint8Array, signature: Uint8Array): boolean
|
|
64
|
-
csrfVerify(token: Uint8Array, secret: Uint8Array): boolean
|
|
65
|
-
passwordVerify(password: Uint8Array, phc: Uint8Array): boolean
|
|
66
|
-
passwordVerifyBcrypt(password: Uint8Array, phc: Uint8Array): boolean
|
|
67
|
-
|
|
68
|
-
// ── Decoders (throw on malformed input — napi `Result` parity) ──
|
|
69
|
-
/** Hex-decode into a fresh buffer (size `input.length / 2`). */
|
|
70
|
-
hexDecode(input: Uint8Array): Uint8Array
|
|
71
|
-
/** Hex-decode into `output`; returns bytes written (throws if too small). */
|
|
72
|
-
hexDecodeInto(input: Uint8Array, output: Uint8Array): number
|
|
73
|
-
/** Percent-decode into a fresh buffer (size `input.length`). */
|
|
74
|
-
urlDecode(input: Uint8Array): Uint8Array
|
|
75
|
-
/** Percent-decode into `output`; returns bytes written (throws if too small). */
|
|
76
|
-
urlDecodeInto(input: Uint8Array, output: Uint8Array): number
|
|
77
|
-
/** base64-decode into a fresh buffer (size `ceil(len/4)*3`). */
|
|
78
|
-
base64Decode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array
|
|
79
|
-
/** base64-decode into `output`; returns bytes written (throws if too small). */
|
|
80
|
-
base64DecodeInto(
|
|
81
|
-
input: Uint8Array,
|
|
82
|
-
output: Uint8Array,
|
|
83
|
-
urlSafe?: boolean,
|
|
84
|
-
padding?: boolean,
|
|
85
|
-
): number
|
|
86
|
-
|
|
87
|
-
// ── Fixed-size output writers ─────────────────────────────────────
|
|
88
|
-
/** RFC 6455 Sec-WebSocket-Accept (28 bytes) into a fresh buffer. */
|
|
89
|
-
wsAcceptKey(key: Uint8Array): Uint8Array
|
|
90
|
-
/** crc32 ETag (10 strong / 12 weak bytes) into a fresh buffer. */
|
|
91
|
-
etag(data: Uint8Array, weak?: boolean): Uint8Array
|
|
92
|
-
/** crc32 ETag into `output`; returns bytes written. */
|
|
93
|
-
etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number
|
|
94
|
-
/** `byteLen` random bytes → `byteLen * 2` hex chars. */
|
|
95
|
-
randomToken(byteLen: number): Uint8Array
|
|
96
|
-
/** base64-encode into a fresh buffer (size `ceil(len/3)*4`). */
|
|
97
|
-
base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array
|
|
98
|
-
/** base64-encode into `output`; returns bytes written. */
|
|
99
|
-
base64EncodeInto(
|
|
100
|
-
input: Uint8Array,
|
|
101
|
-
output: Uint8Array,
|
|
102
|
-
urlSafe?: boolean,
|
|
103
|
-
padding?: boolean,
|
|
104
|
-
): number
|
|
105
|
-
/** HMAC-SHA256 hex (64 chars) into a fresh buffer. */
|
|
106
|
-
hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array
|
|
107
|
-
/** HMAC-SHA256 hex into `output`; returns bytes written (throws if too small). */
|
|
108
|
-
hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number
|
|
109
|
-
/** Sign cookie `value` as `value.<64-hex>` into a fresh buffer. */
|
|
110
|
-
signCookie(value: Uint8Array, secret: Uint8Array): Uint8Array
|
|
111
|
-
/** Sign cookie into `output`; returns bytes written (throws if too small). */
|
|
112
|
-
signCookieInto(value: Uint8Array, secret: Uint8Array, output: Uint8Array): number
|
|
113
|
-
/** Verify a signed cookie → value bytes, or `null` on bad signature. */
|
|
114
|
-
verifyCookie(signed: Uint8Array, secret: Uint8Array): Uint8Array | null
|
|
115
|
-
/** CSRF token (129 bytes: 64 rnd-hex + '.' + 64 sig-hex). */
|
|
116
|
-
csrfToken(secret: Uint8Array): Uint8Array
|
|
117
|
-
/** Argon2id PHC hash bytes (m/t/p/out_len params). */
|
|
118
|
-
passwordHash(
|
|
119
|
-
password: Uint8Array,
|
|
120
|
-
salt: Uint8Array,
|
|
121
|
-
mCost: number,
|
|
122
|
-
tCost: number,
|
|
123
|
-
pCost: number,
|
|
124
|
-
outLen: number,
|
|
125
|
-
): Uint8Array
|
|
126
|
-
/** bcrypt `$2b$` PHC string bytes (cost clamped 4..=31). */
|
|
127
|
-
passwordHashBcrypt(password: Uint8Array, cost: number): Uint8Array
|
|
128
|
-
/** PBKDF2-HMAC-SHA256 → `dkLen` bytes (rounds, dkLen clamped). */
|
|
129
|
-
pbkdf2Sha256(password: Uint8Array, salt: Uint8Array, rounds: number, dkLen: number): Uint8Array
|
|
130
|
-
/** AEAD encrypt (0 = AES-256-GCM, 1 = ChaCha20-Poly1305) → ct+tag. */
|
|
131
|
-
aeadEncrypt(
|
|
132
|
-
key: Uint8Array,
|
|
133
|
-
nonce: Uint8Array,
|
|
134
|
-
plaintext: Uint8Array,
|
|
135
|
-
algorithm?: number,
|
|
136
|
-
): Uint8Array
|
|
137
|
-
/** AEAD encrypt into `output` (ct + 16-byte tag); returns bytes written. */
|
|
138
|
-
aeadEncryptInto(
|
|
139
|
-
key: Uint8Array,
|
|
140
|
-
nonce: Uint8Array,
|
|
141
|
-
plaintext: Uint8Array,
|
|
142
|
-
output: Uint8Array,
|
|
143
|
-
algorithm?: number,
|
|
144
|
-
): number
|
|
145
|
-
/** AEAD decrypt → plaintext, or `null` on auth failure. */
|
|
146
|
-
aeadDecrypt(
|
|
147
|
-
key: Uint8Array,
|
|
148
|
-
nonce: Uint8Array,
|
|
149
|
-
ciphertext: Uint8Array,
|
|
150
|
-
algorithm?: number,
|
|
151
|
-
): Uint8Array | null
|
|
152
|
-
|
|
153
|
-
// ── Frame / patch / compression (variable-size → grow-retry) ─────
|
|
154
|
-
/** RFC 6455 frame encode into a fresh buffer. */
|
|
155
|
-
wsFrameEncode(opcode: number, payload: Uint8Array, mask: boolean, fin: boolean): Uint8Array
|
|
156
|
-
/** RFC 6455 frame encode into `output`; returns bytes written (throws if too small). */
|
|
157
|
-
wsFrameEncodeInto(
|
|
158
|
-
opcode: number,
|
|
159
|
-
payload: Uint8Array,
|
|
160
|
-
mask: boolean,
|
|
161
|
-
fin: boolean,
|
|
162
|
-
output: Uint8Array,
|
|
163
|
-
): number
|
|
164
|
-
/** RFC 6902 JSON patch into a fresh buffer. */
|
|
165
|
-
jsonPatch(doc: Uint8Array, patch: Uint8Array): Uint8Array
|
|
166
|
-
/** gzip-compress into a fresh buffer (level clamped 0..=9, default 6). */
|
|
167
|
-
gzipCompress(data: Uint8Array, level?: number): Uint8Array
|
|
168
|
-
/** gzip-compress into `output`; returns bytes written (throws if too small). */
|
|
169
|
-
gzipCompressInto(data: Uint8Array, output: Uint8Array, level?: number): number
|
|
170
|
-
/** gzip-decompress into a fresh buffer (capped by `maxDecompressed`). */
|
|
171
|
-
gzipDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array
|
|
172
|
-
/** brotli-compress into a fresh buffer (quality clamped 0..=11, default 5). */
|
|
173
|
-
brotliCompress(data: Uint8Array, quality?: number): Uint8Array
|
|
174
|
-
/** brotli-compress into `output`; returns bytes written (throws if too small). */
|
|
175
|
-
brotliCompressInto(data: Uint8Array, output: Uint8Array, quality?: number): number
|
|
176
|
-
/** brotli-decompress into a fresh buffer (capped by `maxDecompressed`). */
|
|
177
|
-
brotliDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array
|
|
178
|
-
|
|
179
|
-
// ── Packed parsers (into caller buffers) ─────────────────────────
|
|
180
|
-
/** HTTP request parse → packed output into `output`; returns bytes written. */
|
|
181
|
-
httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number
|
|
182
|
-
/** Query string parse → packed output into `output`; returns bytes written. */
|
|
183
|
-
queryParsePackedInto(input: Uint8Array, output: Uint8Array): number
|
|
184
|
-
/** Cookie header parse → packed output into `output`; returns bytes written. */
|
|
185
|
-
cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number
|
|
186
|
-
|
|
187
|
-
// ── Excluded-surface additions (packed / opaque-handle) ─────────
|
|
188
|
-
/** Sign JWT (HS256) from pre-serialized claim JSON (ttl<=0 → no iat/exp). */
|
|
189
|
-
jwtSignBytes(claimsJson: Uint8Array, secret: Uint8Array, ttl: number, now: number): Uint8Array
|
|
190
|
-
/** Decode a WS frame into packed `[flags][opcode][u32 len][payload]`; null on malformed. */
|
|
191
|
-
wsFrameDecodePacked(data: Uint8Array): Uint8Array | null
|
|
192
|
-
/** Parse multipart/form-data into the packed parts layout. */
|
|
193
|
-
multipartParsePacked(body: Uint8Array, boundary: Uint8Array): Uint8Array
|
|
194
|
-
/** Parse x-www-form-urlencoded into packed pairs into `output`. */
|
|
195
|
-
formParsePackedInto(input: Uint8Array, output: Uint8Array): number
|
|
196
|
-
/**
|
|
197
|
-
* Run the ingress pipeline on a packed frame via the opaque inner handle from
|
|
198
|
-
* `Ingress.ingressInnerPtr()` (valid only while the instance is alive — the
|
|
199
|
-
* caller must hold it). Returns bytes written; throws on error / too-small.
|
|
200
|
-
*/
|
|
201
|
-
ingressHandlePacked(
|
|
202
|
-
inner: number,
|
|
203
|
-
input: Uint8Array,
|
|
204
|
-
body: Uint8Array | null,
|
|
205
|
-
output: Uint8Array,
|
|
206
|
-
): number
|
|
207
|
-
/**
|
|
208
|
-
* Write the ingress binary-layout constants (38 × u32 LE — `rust/ffi.rs`
|
|
209
|
-
* `IngressLayout`, numeric source `rust/ingress/output.rs`) into `output`;
|
|
210
|
-
* returns bytes written. Lets `src/ingress/constants.ts` read the layout via
|
|
211
|
-
* bun:ffi on Bun so importing the package does NOT dlopen the napi addon.
|
|
212
|
-
*/
|
|
213
|
-
ingressLayout(out: Uint8Array): number
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ── Caps mirrored from the Rust napi layer ────────────────────────
|
|
217
|
-
// rust/payload/compress.rs DEFAULT_MAX_DECOMPRESSED (64 MiB decompression bomb
|
|
218
|
-
// guard — must stay in sync; do not raise without benchmarking).
|
|
219
|
-
const MAX_DECOMPRESSED = 64 * 1024 * 1024
|
|
220
|
-
// rust/json/json_patch_ops.rs MAX_JSON_PATCH_OUTPUT (128 MiB).
|
|
221
|
-
const MAX_JSON_PATCH_OUTPUT = 128 * 1024 * 1024
|
|
222
|
-
|
|
223
|
-
// ── Output-buffer sizing heuristics (measured) ─────────────────────
|
|
224
|
-
// These are ALLOCATION caps only, not correctness bounds: `growExact` covers
|
|
225
|
-
// any residual miss with at most one exact-size retry (no re-run loop), so a
|
|
226
|
-
// guess that is too small costs one extra native pass, never a wrong result.
|
|
227
|
-
//
|
|
228
|
-
// Compress: a 75 KiB input compresses to <1 KiB, so a 16 KiB initial is
|
|
229
|
-
// plenty and single-pass; the 1 MiB ceiling bounds incompressible data.
|
|
230
|
-
const COMPRESS_INITIAL_CAP = 16 * 1024
|
|
231
|
-
const COMPRESS_MAX_CAP = 1024 * 1024
|
|
232
|
-
const COMPRESS_HEADROOM = 64 // small-input slack above `data.length`
|
|
233
|
-
// Decompress (no trailer / expensive trailer): typical JSON/text ratios.
|
|
234
|
-
const DECOMPRESS_GUESS_MULTIPLIER_GZIP = 16
|
|
235
|
-
const DECOMPRESS_GUESS_MULTIPLIER_BROTLI = 32
|
|
236
|
-
const DECOMPRESS_FALLBACK_CAP = 4 * 1024 * 1024 // 4 MiB over-alloc bound
|
|
237
|
-
const DECOMPRESS_MIN_INITIAL = 1024
|
|
238
|
-
// JWT: token ≈ header(~36) + payload(≈4/3× claims) + sig(~43) + 2 dots. The
|
|
239
|
-
// old `claims.length + 128` under-sized a typical token (measured 168 B for a
|
|
240
|
-
// 30 B claim → grow-retry double-run, making ffi slower than napi). 2×+128
|
|
241
|
-
// with a 256-byte floor covers typical claims in one pass.
|
|
242
|
-
const JWT_INITIAL_MULTIPLIER = 2
|
|
243
|
-
const JWT_INITIAL_EXTRA = 128
|
|
244
|
-
const JWT_INITIAL_FLOOR = 256
|
|
245
|
-
|
|
246
|
-
// ── Known-good vectors for the bind-time self-test ───────────────
|
|
247
|
-
// These mirror the Rust `#[cfg(test)]` vectors in rust/ffi.rs.
|
|
248
|
-
const SELFTEST_HEX = encoder.encode('hello') // -> 68656c6c6f
|
|
249
|
-
const SELFTEST_JSON = encoder.encode('{"a":1}')
|
|
250
|
-
|
|
251
|
-
// Empty view for `null` body slots in (ptr, len) pairs — the C side treats
|
|
252
|
-
// len 0 as "no body" regardless of the pointer value.
|
|
253
|
-
const EMPTY_VIEW = new Uint8Array(0)
|
|
39
|
+
// Re-export the type surface so `import type { BunFFI } from '../native/ffi'`
|
|
40
|
+
// keeps working (existing call sites).
|
|
41
|
+
export type { BunFFI, FfiMode } from './ffi/types'
|
|
254
42
|
|
|
255
43
|
let cached: BunFFI | null | undefined
|
|
44
|
+
/**
|
|
45
|
+
* Resolved buffer ABI mode, set once by `bind()`. `null` = ffi unavailable
|
|
46
|
+
* (Node / `CASTRUM_FFI_MODE=napi` / failed self-test), `'buffer-pair'` = the
|
|
47
|
+
* engine-native `buffer`/`buffer_length` pair is live, `'ptr-len'` = the
|
|
48
|
+
* explicit `(ptr, usize)` fallback.
|
|
49
|
+
*/
|
|
50
|
+
let bufferAbiMode: 'buffer-pair' | 'ptr-len' | null = null
|
|
256
51
|
|
|
257
52
|
// ── Transport selection (CASTRUM_FFI_MODE) ───────────────────────
|
|
258
|
-
// auto (default): use bun:ffi on Bun, silently fall back to napi when the
|
|
259
|
-
// bind or self-test fails (and always on Node).
|
|
260
|
-
// ffi : force bun:ffi on Bun — throw a clear error if it can't bind or the
|
|
261
|
-
// self-test fails (use in benches/CI that MUST run ffi).
|
|
262
|
-
// napi : never bind — every call goes through the napi addon (the fallback;
|
|
263
|
-
// useful for exercising the fallback path on Bun).
|
|
264
|
-
// The legacy alias `RUST_FFI_MODE` is accepted too (see src/shared/env.ts).
|
|
265
|
-
export type FfiMode = 'auto' | 'ffi' | 'napi'
|
|
266
53
|
|
|
267
54
|
function resolveFfiMode(): FfiMode {
|
|
268
|
-
const raw = resolveEnvVar('CASTRUM_FFI_MODE'
|
|
55
|
+
const raw = resolveEnvVar('CASTRUM_FFI_MODE')
|
|
269
56
|
switch (raw) {
|
|
270
57
|
case 'ffi':
|
|
271
58
|
return 'ffi'
|
|
@@ -292,6 +79,23 @@ export function getBunFFI(): BunFFI | null {
|
|
|
292
79
|
return cached
|
|
293
80
|
}
|
|
294
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Which buffer ABI the live `bun:ffi` binding uses, or `null` when the
|
|
84
|
+
* transport is unavailable (Node, `CASTRUM_FFI_MODE=napi`, or a failed
|
|
85
|
+
* self-test). `'buffer-pair'` = the engine-native `buffer`/`buffer_length`
|
|
86
|
+
* pair (atomic ptr+byteLength snapshot) is in use; `'ptr-len'` = the explicit
|
|
87
|
+
* `(ptr, usize)` fallback (older Bun / probe failure). Lazy — triggers the
|
|
88
|
+
* same one-time bind as {@link getBunFFI}. Under `CASTRUM_FFI_MODE=ffi` on Bun
|
|
89
|
+
* this is guaranteed non-null; a bind/self-test failure THROWS, matching
|
|
90
|
+
* `getBunFFI`.
|
|
91
|
+
*/
|
|
92
|
+
export function ffiBufferMode(): 'buffer-pair' | 'ptr-len' | null {
|
|
93
|
+
if (cached === undefined) {
|
|
94
|
+
getBunFFI()
|
|
95
|
+
}
|
|
96
|
+
return bufferAbiMode
|
|
97
|
+
}
|
|
98
|
+
|
|
295
99
|
/**
|
|
296
100
|
* Probe whether this Bun build accepts the `buffer`/`buffer_length` ABI pair in
|
|
297
101
|
* `dlopen`. Bun's docs list `buffer_length` as engine-native (dlopen-supported),
|
|
@@ -324,6 +128,7 @@ function probeBufferLength(dlopen: typeof import('bun:ffi')['dlopen'], path: str
|
|
|
324
128
|
}
|
|
325
129
|
|
|
326
130
|
function bind(): BunFFI | null {
|
|
131
|
+
bufferAbiMode = null // reset — the mode below is only valid for a live bind
|
|
327
132
|
const mode = resolveFfiMode()
|
|
328
133
|
if (!isBun()) {
|
|
329
134
|
if (mode === 'ffi') {
|
|
@@ -349,125 +154,319 @@ function bind(): BunFFI | null {
|
|
|
349
154
|
// TypedArray at call time — an atomic snapshot) once; when supported we use
|
|
350
155
|
// it for the input-only hot fns (one JS arg instead of two), else `(ptr,len)`.
|
|
351
156
|
const useBufferLength = probeBufferLength(dlopen, path)
|
|
157
|
+
bufferAbiMode = useBufferLength ? 'buffer-pair' : 'ptr-len'
|
|
352
158
|
|
|
353
159
|
// String ABI specs ("ptr"/"usize"/"u32"...): accepted by bun:ffi and typed
|
|
354
160
|
// via `FFITypeOrString` in bun-types. usize == uint64 on the host ABI.
|
|
355
161
|
// Symbol keys must be the exact `#[no_mangle] extern "C"` names in
|
|
356
|
-
// rust/ffi
|
|
162
|
+
// rust/ffi/. Every `(ptr,len)` pair is passed as `(view, view.length)`.
|
|
357
163
|
// The 8 input-only single-buffer fns use `inputAbi`; everything else keeps
|
|
358
164
|
// `(ptr,len)` because the output length is the caller's write cap.
|
|
359
165
|
const inputAbi: readonly FFITypeOrString[] = useBufferLength
|
|
360
166
|
? (['buffer', 'buffer_length'] as unknown as readonly FFITypeOrString[])
|
|
361
167
|
: ['ptr', 'usize']
|
|
168
|
+
// Phase 1.1: extend `buffer`/`buffer_length` to EVERY `(ptr,len)` pair, not
|
|
169
|
+
// just the input-only fns. The engine reads ptr + byteLength off the SAME
|
|
170
|
+
// view at call time (atomic snapshot), so call sites pass the view twice
|
|
171
|
+
// (see `lenOrView` below). Conversion is POSITIONAL and pair-aware: each
|
|
172
|
+
// `ptr` consumes its following `usize` as a length. Scalar args that happen
|
|
173
|
+
// to be `usize` (the opaque `inner` handle of `castrum_ingress_handle_packed`
|
|
174
|
+
// and the `max` param of gzip/brotli decompress) are passed through
|
|
175
|
+
// unchanged — a blind `usize`→`buffer_length` map would corrupt them.
|
|
176
|
+
const abi = (shape: readonly string[]): readonly FFITypeOrString[] => {
|
|
177
|
+
if (!useBufferLength) return shape as readonly FFITypeOrString[]
|
|
178
|
+
const out: FFITypeOrString[] = []
|
|
179
|
+
for (let i = 0; i < shape.length; i++) {
|
|
180
|
+
const t = shape[i]
|
|
181
|
+
if (t === 'ptr') {
|
|
182
|
+
out.push('buffer' as unknown as FFITypeOrString)
|
|
183
|
+
out.push('buffer_length' as unknown as FFITypeOrString)
|
|
184
|
+
i++ // consume the paired `usize`
|
|
185
|
+
} else {
|
|
186
|
+
out.push(t as FFITypeOrString)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return out
|
|
190
|
+
}
|
|
191
|
+
// `u64_fast` returns a plain `number` when the value fits (< 2^53) instead
|
|
192
|
+
// of a BigInt — bun-types lacks the literal, so bind it via a typed const
|
|
193
|
+
// (same cast pattern as the `buffer_length` probe above). Every bytes-written
|
|
194
|
+
// return fits in a double, so this removes the per-call BigInt boxing on the
|
|
195
|
+
// hot scalar / `*_into` / packed-parser / ingress calls.
|
|
196
|
+
const U64_FAST = 'u64_fast' as unknown as FFITypeOrString
|
|
362
197
|
const { symbols } = dlopen(path, {
|
|
363
198
|
castrum_crc32: { args: inputAbi, returns: 'u32' },
|
|
364
199
|
castrum_fnv1a64: { args: inputAbi, returns: 'u64' },
|
|
365
200
|
castrum_xxh3: { args: inputAbi, returns: 'u64' },
|
|
366
201
|
castrum_json_valid: { args: inputAbi, returns: 'u8' },
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
202
|
+
castrum_utf8_valid: { args: inputAbi, returns: 'u8' },
|
|
203
|
+
castrum_hex_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
204
|
+
castrum_hex_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
205
|
+
castrum_url_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
206
|
+
castrum_url_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
207
|
+
// String-input validators: `cstring` ARG (the engine transcodes the JS
|
|
208
|
+
// string in-engine — no JS-side encode; Rust borrows via CStr::from_ptr).
|
|
209
|
+
castrum_validate_email: { args: ['cstring'], returns: 'u8' },
|
|
210
|
+
castrum_validate_uuid: { args: ['cstring'], returns: 'u8' },
|
|
211
|
+
castrum_validate_ipv4: { args: ['cstring'], returns: 'u8' },
|
|
212
|
+
castrum_validate_ipv6: { args: ['cstring'], returns: 'u8' },
|
|
213
|
+
castrum_json_sum_ids: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
376
214
|
castrum_hmac_sha256_verify: {
|
|
377
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
|
|
215
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
216
|
+
returns: 'u8',
|
|
217
|
+
},
|
|
218
|
+
castrum_csrf_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
|
|
219
|
+
castrum_password_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
|
|
220
|
+
castrum_password_verify_bcrypt: {
|
|
221
|
+
// `phc` crosses as a `cstring` ARG; `password` stays a `(ptr,len)` pair.
|
|
222
|
+
args: abi(['ptr', 'usize', 'cstring']),
|
|
378
223
|
returns: 'u8',
|
|
379
224
|
},
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
225
|
+
castrum_ws_accept_key: { args: ['cstring'], returns: 'cstring' },
|
|
226
|
+
castrum_ws_accept_key_into: {
|
|
227
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
228
|
+
returns: U64_FAST,
|
|
229
|
+
},
|
|
230
|
+
castrum_etag: { args: abi(['ptr', 'usize', 'u8']), returns: 'cstring' },
|
|
231
|
+
castrum_etag_into: { args: abi(['ptr', 'usize', 'u8', 'ptr', 'usize']), returns: U64_FAST },
|
|
232
|
+
castrum_conditional_is_not_modified: {
|
|
233
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'u8']),
|
|
234
|
+
returns: 'u8',
|
|
235
|
+
},
|
|
236
|
+
castrum_media_type_matcher_matches: {
|
|
237
|
+
args: abi(['usize', 'ptr', 'usize']),
|
|
238
|
+
returns: 'u8',
|
|
239
|
+
},
|
|
240
|
+
castrum_accept_negotiator_negotiate: {
|
|
241
|
+
args: abi(['usize', 'ptr', 'usize']),
|
|
242
|
+
returns: 'cstring',
|
|
243
|
+
},
|
|
244
|
+
// Server-preference sibling (RFC 7231 server semantics): `header` is a
|
|
245
|
+
// `cstring` ARG — the engine transcodes the JS header string in-engine.
|
|
246
|
+
castrum_accept_negotiator_negotiate_server: {
|
|
247
|
+
args: ['usize', 'cstring'],
|
|
248
|
+
returns: 'cstring',
|
|
249
|
+
},
|
|
250
|
+
castrum_jwt_signer_sign: {
|
|
251
|
+
args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
|
|
252
|
+
returns: U64_FAST,
|
|
253
|
+
},
|
|
254
|
+
castrum_jwt_signer_verify: {
|
|
255
|
+
args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
|
|
256
|
+
returns: U64_FAST,
|
|
257
|
+
},
|
|
258
|
+
castrum_template_render: {
|
|
259
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
260
|
+
returns: U64_FAST,
|
|
261
|
+
},
|
|
262
|
+
castrum_schema_validator_validate: {
|
|
263
|
+
args: abi(['usize', 'ptr', 'usize']),
|
|
264
|
+
returns: 'u8',
|
|
265
|
+
},
|
|
266
|
+
castrum_rate_limiter_check: {
|
|
267
|
+
// `key` crosses as a `cstring` ARG (text, no NUL in the intended use).
|
|
268
|
+
args: abi(['usize', 'cstring', 'i64', 'ptr', 'usize']),
|
|
269
|
+
returns: U64_FAST,
|
|
270
|
+
},
|
|
271
|
+
castrum_rate_limiter_check_key: {
|
|
272
|
+
args: abi(['usize', 'i64', 'i64', 'ptr', 'usize']),
|
|
273
|
+
returns: U64_FAST,
|
|
274
|
+
},
|
|
275
|
+
castrum_random_token: { args: ['u32'], returns: 'cstring' },
|
|
276
|
+
castrum_random_token_into: { args: ['u32', ...abi(['ptr', 'usize'])], returns: U64_FAST },
|
|
386
277
|
castrum_base64_encode: {
|
|
387
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8'],
|
|
388
|
-
returns:
|
|
278
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
|
|
279
|
+
returns: U64_FAST,
|
|
389
280
|
},
|
|
390
281
|
castrum_base64_decode: {
|
|
391
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8'],
|
|
392
|
-
returns:
|
|
282
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
|
|
283
|
+
returns: U64_FAST,
|
|
393
284
|
},
|
|
394
285
|
castrum_hmac_sha256: {
|
|
395
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
|
|
396
|
-
returns:
|
|
286
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
287
|
+
returns: U64_FAST,
|
|
397
288
|
},
|
|
398
|
-
castrum_sign_cookie: {
|
|
399
|
-
|
|
400
|
-
|
|
289
|
+
castrum_sign_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
|
|
290
|
+
castrum_sign_cookie_into: {
|
|
291
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
292
|
+
returns: U64_FAST,
|
|
401
293
|
},
|
|
402
|
-
castrum_verify_cookie: {
|
|
403
|
-
|
|
404
|
-
|
|
294
|
+
castrum_verify_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
|
|
295
|
+
castrum_verify_cookie_into: {
|
|
296
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
297
|
+
returns: U64_FAST,
|
|
405
298
|
},
|
|
406
|
-
castrum_csrf_token: { args: ['ptr', 'usize'
|
|
299
|
+
castrum_csrf_token: { args: abi(['ptr', 'usize']), returns: 'cstring' },
|
|
300
|
+
castrum_csrf_token_into: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
407
301
|
castrum_password_hash: {
|
|
408
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'u32', 'u32', 'ptr', 'usize'],
|
|
409
|
-
returns:
|
|
302
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'u32', 'u32', 'ptr', 'usize']),
|
|
303
|
+
returns: U64_FAST,
|
|
410
304
|
},
|
|
411
305
|
castrum_password_hash_bcrypt: {
|
|
412
|
-
args: ['ptr', 'usize', 'u32', 'ptr', 'usize'],
|
|
413
|
-
returns:
|
|
306
|
+
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
307
|
+
returns: U64_FAST,
|
|
414
308
|
},
|
|
415
309
|
castrum_pbkdf2_sha256: {
|
|
416
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'ptr', 'usize'],
|
|
417
|
-
returns:
|
|
310
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'ptr', 'usize']),
|
|
311
|
+
returns: U64_FAST,
|
|
418
312
|
},
|
|
419
313
|
castrum_aead_encrypt: {
|
|
420
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize'],
|
|
421
|
-
returns:
|
|
314
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
|
|
315
|
+
returns: U64_FAST,
|
|
422
316
|
},
|
|
423
317
|
castrum_aead_decrypt: {
|
|
424
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize'],
|
|
425
|
-
returns:
|
|
318
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
|
|
319
|
+
returns: U64_FAST,
|
|
426
320
|
},
|
|
427
321
|
castrum_ws_frame_encode: {
|
|
428
|
-
args: ['u8', 'ptr', 'usize', 'u8', 'u8', 'ptr', 'usize'],
|
|
429
|
-
returns:
|
|
322
|
+
args: abi(['u8', 'ptr', 'usize', 'u8', 'u8', 'ptr', 'usize']),
|
|
323
|
+
returns: U64_FAST,
|
|
430
324
|
},
|
|
431
325
|
castrum_json_patch: {
|
|
432
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
|
|
433
|
-
returns:
|
|
326
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
327
|
+
returns: U64_FAST,
|
|
328
|
+
},
|
|
329
|
+
castrum_gzip_compress: {
|
|
330
|
+
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
331
|
+
returns: U64_FAST,
|
|
434
332
|
},
|
|
435
|
-
castrum_gzip_compress: { args: ['ptr', 'usize', 'u32', 'ptr', 'usize'], returns: 'usize' },
|
|
436
333
|
castrum_gzip_decompress: {
|
|
437
|
-
args: ['ptr', 'usize', 'usize', 'ptr', 'usize'],
|
|
438
|
-
returns:
|
|
334
|
+
args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
|
|
335
|
+
returns: U64_FAST,
|
|
336
|
+
},
|
|
337
|
+
castrum_brotli_compress: {
|
|
338
|
+
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
339
|
+
returns: U64_FAST,
|
|
439
340
|
},
|
|
440
|
-
castrum_brotli_compress: { args: ['ptr', 'usize', 'u32', 'ptr', 'usize'], returns: 'usize' },
|
|
441
341
|
castrum_brotli_decompress: {
|
|
442
|
-
args: ['ptr', 'usize', 'usize', 'ptr', 'usize'],
|
|
443
|
-
returns:
|
|
342
|
+
args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
|
|
343
|
+
returns: U64_FAST,
|
|
444
344
|
},
|
|
445
|
-
castrum_gzip_isize: { args: ['ptr', 'usize'], returns: 'u32' },
|
|
345
|
+
castrum_gzip_isize: { args: abi(['ptr', 'usize']), returns: 'u32' },
|
|
446
346
|
castrum_http_parse_request_packed: {
|
|
447
|
-
args: ['ptr', 'usize', 'ptr', 'usize'],
|
|
448
|
-
returns:
|
|
347
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
348
|
+
returns: U64_FAST,
|
|
349
|
+
},
|
|
350
|
+
castrum_query_parse_packed: {
|
|
351
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
352
|
+
returns: U64_FAST,
|
|
353
|
+
},
|
|
354
|
+
castrum_cookie_parse_packed: {
|
|
355
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
356
|
+
returns: U64_FAST,
|
|
357
|
+
},
|
|
358
|
+
castrum_http_date_into: { args: ['f64', ...abi(['ptr', 'usize'])], returns: U64_FAST },
|
|
359
|
+
castrum_sse_encode_into: {
|
|
360
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'u32', 'ptr', 'usize']),
|
|
361
|
+
returns: U64_FAST,
|
|
449
362
|
},
|
|
450
|
-
castrum_query_parse_packed: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
|
|
451
|
-
castrum_cookie_parse_packed: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
|
|
452
363
|
// ── Excluded-surface additions ──
|
|
453
364
|
castrum_jwt_sign_bytes: {
|
|
454
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64'
|
|
455
|
-
returns: '
|
|
365
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64']),
|
|
366
|
+
returns: 'cstring',
|
|
367
|
+
},
|
|
368
|
+
castrum_jwt_sign_bytes_into: {
|
|
369
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64', 'ptr', 'usize']),
|
|
370
|
+
returns: U64_FAST,
|
|
371
|
+
},
|
|
372
|
+
// ── Ed25519 / EdDSA JWT (RBAC auth) — bound + self-tested; the public
|
|
373
|
+
// surface is deferred to the auth-module consumer ────────────────
|
|
374
|
+
castrum_ed25519_generate_keypair: {
|
|
375
|
+
args: abi(['ptr', 'usize']),
|
|
376
|
+
returns: U64_FAST,
|
|
377
|
+
},
|
|
378
|
+
castrum_ed25519_sign: {
|
|
379
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
380
|
+
returns: U64_FAST,
|
|
381
|
+
},
|
|
382
|
+
castrum_ed25519_verify: {
|
|
383
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
384
|
+
returns: 'u32',
|
|
385
|
+
},
|
|
386
|
+
castrum_jwt_eddsa_sign: {
|
|
387
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64']),
|
|
388
|
+
returns: 'cstring',
|
|
389
|
+
},
|
|
390
|
+
castrum_jwt_eddsa_verify: {
|
|
391
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64']),
|
|
392
|
+
returns: 'cstring',
|
|
456
393
|
},
|
|
457
394
|
castrum_ws_frame_decode_packed: {
|
|
458
|
-
args: ['ptr', 'usize', 'ptr', 'usize'],
|
|
459
|
-
returns:
|
|
395
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
396
|
+
returns: U64_FAST,
|
|
460
397
|
},
|
|
461
398
|
castrum_multipart_parse_packed: {
|
|
462
|
-
args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
|
|
463
|
-
returns:
|
|
399
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
400
|
+
returns: U64_FAST,
|
|
401
|
+
},
|
|
402
|
+
castrum_form_parse_packed: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
403
|
+
// ── Phase 3 — stateless napi-only scalars (now FFI) ──────────
|
|
404
|
+
// Structural parse: parses ONCE, emits a packed token stream with a
|
|
405
|
+
// deduped string table so JS assembles the value with NO second parse.
|
|
406
|
+
castrum_json_parse_packed: {
|
|
407
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
408
|
+
returns: U64_FAST,
|
|
409
|
+
},
|
|
410
|
+
castrum_parse_media_type: {
|
|
411
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
412
|
+
returns: U64_FAST,
|
|
413
|
+
},
|
|
414
|
+
castrum_parse_http_date: {
|
|
415
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
416
|
+
returns: U64_FAST,
|
|
417
|
+
},
|
|
418
|
+
castrum_parse_accept_encoding: {
|
|
419
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
420
|
+
returns: U64_FAST,
|
|
421
|
+
},
|
|
422
|
+
castrum_url_encode_query: { args: abi(['ptr', 'usize']), returns: 'cstring' },
|
|
423
|
+
castrum_url_resolve: {
|
|
424
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
425
|
+
returns: 'cstring',
|
|
426
|
+
},
|
|
427
|
+
castrum_url_builder_resolve: {
|
|
428
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
429
|
+
returns: U64_FAST,
|
|
430
|
+
},
|
|
431
|
+
castrum_mime_from_extension: { args: ['cstring'], returns: 'cstring' },
|
|
432
|
+
castrum_jwt_verify: {
|
|
433
|
+
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64']),
|
|
434
|
+
returns: 'cstring',
|
|
464
435
|
},
|
|
465
|
-
castrum_form_parse_packed: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
|
|
466
436
|
castrum_ingress_handle_packed: {
|
|
467
|
-
args: ['usize', 'ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
|
|
468
|
-
returns:
|
|
437
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
438
|
+
returns: U64_FAST,
|
|
439
|
+
},
|
|
440
|
+
// Raw-components sibling of `castrum_ingress_handle_packed`: `url`/`ip`
|
|
441
|
+
// are `cstring` ARGs (Bun transcodes the JS strings to call-scoped
|
|
442
|
+
// NUL-terminated buffers in-engine — no JS-side Buffer.write encode);
|
|
443
|
+
// rid/headers/body are `(ptr,len)` byte slices; out is `(ptr,len)`.
|
|
444
|
+
castrum_ingress_handle_components: {
|
|
445
|
+
args: abi([
|
|
446
|
+
'usize',
|
|
447
|
+
'u8',
|
|
448
|
+
'cstring',
|
|
449
|
+
'cstring',
|
|
450
|
+
'ptr',
|
|
451
|
+
'usize',
|
|
452
|
+
'ptr',
|
|
453
|
+
'usize',
|
|
454
|
+
'ptr',
|
|
455
|
+
'usize',
|
|
456
|
+
'ptr',
|
|
457
|
+
'usize',
|
|
458
|
+
]),
|
|
459
|
+
returns: U64_FAST,
|
|
460
|
+
},
|
|
461
|
+
castrum_ingress_layout: { args: abi(['ptr', 'usize']), returns: U64_FAST },
|
|
462
|
+
// Per-route native stack (`@ignex/native` also dlopens these exact names
|
|
463
|
+
// itself; castrum binds them for the bind-time self-test + parity guard).
|
|
464
|
+
castrum_route_compile: { args: abi(['ptr', 'usize']), returns: U64_FAST },
|
|
465
|
+
castrum_route_run: {
|
|
466
|
+
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
467
|
+
returns: U64_FAST,
|
|
469
468
|
},
|
|
470
|
-
|
|
469
|
+
castrum_route_destroy: { args: ['usize'], returns: 'void' },
|
|
471
470
|
})
|
|
472
471
|
|
|
473
472
|
const bindings = build(symbols as Record<string, (...a: unknown[]) => unknown>, useBufferLength)
|
|
@@ -490,1052 +489,3 @@ function bind(): BunFFI | null {
|
|
|
490
489
|
return null
|
|
491
490
|
}
|
|
492
491
|
}
|
|
493
|
-
|
|
494
|
-
// Types for the raw dlopen'd functions (bun:ffi converts Uint8Array -> ptr for
|
|
495
|
-
// `ptr` args; `usize`/`u64`/`i64` returns surface as BigInt).
|
|
496
|
-
type Raw2 = (a: unknown, b: unknown) => number | bigint
|
|
497
|
-
type Raw3 = (a: unknown, b: unknown, c: unknown) => number | bigint
|
|
498
|
-
type Raw4 = (a: unknown, b: unknown, c: unknown, d: unknown) => number | bigint
|
|
499
|
-
type Raw5 = (a: unknown, b: unknown, c: unknown, d: unknown, e: unknown) => number | bigint
|
|
500
|
-
type Raw6 = (
|
|
501
|
-
a: unknown,
|
|
502
|
-
b: unknown,
|
|
503
|
-
c: unknown,
|
|
504
|
-
d: unknown,
|
|
505
|
-
e: unknown,
|
|
506
|
-
f: unknown,
|
|
507
|
-
) => number | bigint
|
|
508
|
-
type Raw7 = (
|
|
509
|
-
a: unknown,
|
|
510
|
-
b: unknown,
|
|
511
|
-
c: unknown,
|
|
512
|
-
d: unknown,
|
|
513
|
-
e: unknown,
|
|
514
|
-
f: unknown,
|
|
515
|
-
g: unknown,
|
|
516
|
-
) => number | bigint
|
|
517
|
-
type Raw8 = (
|
|
518
|
-
a: unknown,
|
|
519
|
-
b: unknown,
|
|
520
|
-
c: unknown,
|
|
521
|
-
d: unknown,
|
|
522
|
-
e: unknown,
|
|
523
|
-
f: unknown,
|
|
524
|
-
g: unknown,
|
|
525
|
-
h: unknown,
|
|
526
|
-
) => number | bigint
|
|
527
|
-
type Raw9 = (
|
|
528
|
-
a: unknown,
|
|
529
|
-
b: unknown,
|
|
530
|
-
c: unknown,
|
|
531
|
-
d: unknown,
|
|
532
|
-
e: unknown,
|
|
533
|
-
f: unknown,
|
|
534
|
-
g: unknown,
|
|
535
|
-
h: unknown,
|
|
536
|
-
i: unknown,
|
|
537
|
-
) => number | bigint
|
|
538
|
-
type Raw10 = (
|
|
539
|
-
a: unknown,
|
|
540
|
-
b: unknown,
|
|
541
|
-
c: unknown,
|
|
542
|
-
d: unknown,
|
|
543
|
-
e: unknown,
|
|
544
|
-
f: unknown,
|
|
545
|
-
g: unknown,
|
|
546
|
-
h: unknown,
|
|
547
|
-
i: unknown,
|
|
548
|
-
j: unknown,
|
|
549
|
-
) => number | bigint
|
|
550
|
-
|
|
551
|
-
const flag = (v?: boolean): number => (v ? 1 : 0)
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Write with the C ABI's "needed" convention. The C side returns the EXACT
|
|
555
|
-
* total bytes the result requires: `0` = real error (throw immediately — no
|
|
556
|
-
* re-run, no grow-to-max allocation storm on invalid input), `w > out.length`
|
|
557
|
-
* = buffer too small with `w` the exact required size (allocate once, retry),
|
|
558
|
-
* else `w` is the written count. A miss therefore costs at most ONE retry with
|
|
559
|
-
* an exact buffer — never a doubling loop that re-runs the whole native op
|
|
560
|
-
* (which is what made FFI gzip/brotli/jsonPatch slower than napi).
|
|
561
|
-
*/
|
|
562
|
-
function growExact(
|
|
563
|
-
write: (out: Uint8Array) => number,
|
|
564
|
-
initial: number,
|
|
565
|
-
max: number,
|
|
566
|
-
error: string,
|
|
567
|
-
): Uint8Array {
|
|
568
|
-
let cap = Math.min(Math.max(initial, 16), max)
|
|
569
|
-
for (;;) {
|
|
570
|
-
const out = new Uint8Array(cap)
|
|
571
|
-
const w = Number(write(out))
|
|
572
|
-
if (w === 0) throw new Error(error)
|
|
573
|
-
if (w <= out.length) return out.subarray(0, w)
|
|
574
|
-
if (w > max) throw new Error(error)
|
|
575
|
-
cap = Math.min(w, max)
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/** Decoder parity: a `0` write on non-empty input throws (napi `Result`). */
|
|
580
|
-
function writeOrThrow(w: number, inputLen: number, label: string): number {
|
|
581
|
-
if (w === 0 && inputLen !== 0) {
|
|
582
|
-
throw new Error(`${label}: invalid input or output buffer too small`)
|
|
583
|
-
}
|
|
584
|
-
return w
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/** base64 (no-pad) length of `n` bytes. */
|
|
588
|
-
const b64Len = (n: number): number => (n === 0 ? 0 : Math.ceil(n / 3) * 4 - ((3 - (n % 3)) % 3))
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Exact length of an argon2id PHC string for the given params (m/t/p/out_len).
|
|
592
|
-
* Format: `$argon2id$v=19$m=<m>,t=<t>,p=<p>$<22-char salt b64>$<hash b64>`.
|
|
593
|
-
* The salt is always 16 bytes → 22 base64 chars (SaltString::encode_b64).
|
|
594
|
-
* Pre-sizing exactly makes `passwordHash` a single native pass instead of a
|
|
595
|
-
* grow-retry (which would re-run the whole ~tens-of-ms hash on a miss).
|
|
596
|
-
*/
|
|
597
|
-
function argon2PhcLength(m: number, t: number, p: number, outLen: number): number {
|
|
598
|
-
return (
|
|
599
|
-
15 + // "$argon2id$v=19$"
|
|
600
|
-
2 + // "m="
|
|
601
|
-
String(m).length +
|
|
602
|
-
3 + // ",t="
|
|
603
|
-
String(t).length +
|
|
604
|
-
3 + // ",p="
|
|
605
|
-
String(p).length +
|
|
606
|
-
1 + // "$" before the salt
|
|
607
|
-
22 + // salt b64 (16 bytes)
|
|
608
|
-
1 + // "$" before the hash
|
|
609
|
-
b64Len(outLen)
|
|
610
|
-
)
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function build(
|
|
614
|
-
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
615
|
-
useBufferLength: boolean,
|
|
616
|
-
): BunFFI {
|
|
617
|
-
const crc32 = sym.castrum_crc32 as (...a: unknown[]) => number | bigint
|
|
618
|
-
const fnv = sym.castrum_fnv1a64 as (...a: unknown[]) => number | bigint
|
|
619
|
-
const xxh = sym.castrum_xxh3 as (...a: unknown[]) => number | bigint
|
|
620
|
-
const jsonValid = sym.castrum_json_valid as (...a: unknown[]) => number | bigint
|
|
621
|
-
const hexEncode = sym.castrum_hex_encode as Raw4
|
|
622
|
-
const hexDecode = sym.castrum_hex_decode as Raw4
|
|
623
|
-
const urlEncode = sym.castrum_url_encode as Raw4
|
|
624
|
-
const urlDecode = sym.castrum_url_decode as Raw4
|
|
625
|
-
const validateEmail = sym.castrum_validate_email as (...a: unknown[]) => number | bigint
|
|
626
|
-
const validateUuid = sym.castrum_validate_uuid as (...a: unknown[]) => number | bigint
|
|
627
|
-
const validateIpv4 = sym.castrum_validate_ipv4 as (...a: unknown[]) => number | bigint
|
|
628
|
-
const validateIpv6 = sym.castrum_validate_ipv6 as (...a: unknown[]) => number | bigint
|
|
629
|
-
const jsonSumRaw = sym.castrum_json_sum_ids as Raw4
|
|
630
|
-
|
|
631
|
-
// Single-buffer input fns: with the probe-gated `buffer/buffer_length` ABI the
|
|
632
|
-
// engine reads ptr + byteLength off the SAME TypedArray at call time (an
|
|
633
|
-
// atomic snapshot — pass the view twice, per Bun's docs); with `(ptr,len)` we
|
|
634
|
-
// pass the length explicitly. The branch is on a bind-time constant, so the
|
|
635
|
-
// JIT folds it away at the call site.
|
|
636
|
-
const oneArg = (raw: (...a: unknown[]) => number | bigint, v: Uint8Array): number | bigint =>
|
|
637
|
-
useBufferLength ? raw(v, v) : raw(v, v.length)
|
|
638
|
-
const hmacVerify = sym.castrum_hmac_sha256_verify as Raw6
|
|
639
|
-
const csrfVerify = sym.castrum_csrf_verify as Raw4
|
|
640
|
-
const passwordVerify = sym.castrum_password_verify as Raw4
|
|
641
|
-
const passwordVerifyBcrypt = sym.castrum_password_verify_bcrypt as Raw4
|
|
642
|
-
const wsAcceptKey = sym.castrum_ws_accept_key as Raw4
|
|
643
|
-
const etag = sym.castrum_etag as Raw5
|
|
644
|
-
const randomToken = sym.castrum_random_token as Raw3
|
|
645
|
-
const base64Encode = sym.castrum_base64_encode as Raw6
|
|
646
|
-
const base64Decode = sym.castrum_base64_decode as Raw6
|
|
647
|
-
const hmacSha256 = sym.castrum_hmac_sha256 as Raw6
|
|
648
|
-
const signCookie = sym.castrum_sign_cookie as Raw6
|
|
649
|
-
const verifyCookie = sym.castrum_verify_cookie as Raw6
|
|
650
|
-
const csrfToken = sym.castrum_csrf_token as Raw4
|
|
651
|
-
const passwordHash = sym.castrum_password_hash as Raw10
|
|
652
|
-
const passwordHashBcrypt = sym.castrum_password_hash_bcrypt as Raw5
|
|
653
|
-
const pbkdf2 = sym.castrum_pbkdf2_sha256 as Raw8
|
|
654
|
-
const aeadEncrypt = sym.castrum_aead_encrypt as Raw9
|
|
655
|
-
const aeadDecrypt = sym.castrum_aead_decrypt as Raw9
|
|
656
|
-
const wsFrameEncode = sym.castrum_ws_frame_encode as Raw7
|
|
657
|
-
const jsonPatch = sym.castrum_json_patch as Raw6
|
|
658
|
-
const gzipCompress = sym.castrum_gzip_compress as Raw5
|
|
659
|
-
const gzipDecompress = sym.castrum_gzip_decompress as Raw5
|
|
660
|
-
const brotliCompress = sym.castrum_brotli_compress as Raw5
|
|
661
|
-
const brotliDecompress = sym.castrum_brotli_decompress as Raw5
|
|
662
|
-
const gzipIsize = sym.castrum_gzip_isize as Raw2
|
|
663
|
-
const httpParsePacked = sym.castrum_http_parse_request_packed as Raw4
|
|
664
|
-
const queryParsePacked = sym.castrum_query_parse_packed as Raw4
|
|
665
|
-
const cookieParsePacked = sym.castrum_cookie_parse_packed as Raw4
|
|
666
|
-
const jwtSignBytes = sym.castrum_jwt_sign_bytes as Raw8
|
|
667
|
-
const wsFrameDecodePacked = sym.castrum_ws_frame_decode_packed as Raw4
|
|
668
|
-
const multipartParsePacked = sym.castrum_multipart_parse_packed as Raw6
|
|
669
|
-
const formParsePacked = sym.castrum_form_parse_packed as Raw4
|
|
670
|
-
const ingressHandlePacked = sym.castrum_ingress_handle_packed as Raw7
|
|
671
|
-
const ingressLayoutSym = sym.castrum_ingress_layout as Raw2
|
|
672
|
-
|
|
673
|
-
// ── Encoder / decoder `Into` helpers ──────────────────────────────
|
|
674
|
-
const hexEncodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
675
|
-
// Mirror napi error semantics: a too-small buffer throws, not returns 0.
|
|
676
|
-
// Hex encode always writes exactly `input.length * 2` bytes on success.
|
|
677
|
-
if (output.length < input.length * 2) {
|
|
678
|
-
throw new Error('hex encode: output buffer too small')
|
|
679
|
-
}
|
|
680
|
-
return Number(hexEncode(input, input.length, output, output.length))
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const urlEncodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
684
|
-
const w = Number(urlEncode(input, input.length, output, output.length))
|
|
685
|
-
// Every input byte encodes to >= 1 output byte, so a 0 write on non-empty
|
|
686
|
-
// input means the buffer was too small (napi throws there too). Empty
|
|
687
|
-
// input legitimately writes 0.
|
|
688
|
-
if (w === 0 && input.length !== 0) {
|
|
689
|
-
throw new Error('url encode: output buffer too small')
|
|
690
|
-
}
|
|
691
|
-
return w
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const hexDecodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
695
|
-
const w = Number(hexDecode(input, input.length, output, output.length))
|
|
696
|
-
return writeOrThrow(w, input.length, 'hex decode')
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const urlDecodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
700
|
-
const w = Number(urlDecode(input, input.length, output, output.length))
|
|
701
|
-
return writeOrThrow(w, input.length, 'url decode')
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
const base64DecodeInto = (
|
|
705
|
-
input: Uint8Array,
|
|
706
|
-
output: Uint8Array,
|
|
707
|
-
urlSafe?: boolean,
|
|
708
|
-
padding?: boolean,
|
|
709
|
-
): number => {
|
|
710
|
-
// napi defaults: urlSafe=false, padding=true (rust/crypto/base64.rs).
|
|
711
|
-
const w = Number(
|
|
712
|
-
base64Decode(
|
|
713
|
-
input,
|
|
714
|
-
input.length,
|
|
715
|
-
output,
|
|
716
|
-
output.length,
|
|
717
|
-
flag(urlSafe),
|
|
718
|
-
flag(padding ?? true),
|
|
719
|
-
),
|
|
720
|
-
)
|
|
721
|
-
return writeOrThrow(w, input.length, 'base64 decode')
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const base64EncodeInto = (
|
|
725
|
-
input: Uint8Array,
|
|
726
|
-
output: Uint8Array,
|
|
727
|
-
urlSafe?: boolean,
|
|
728
|
-
padding?: boolean,
|
|
729
|
-
): number => {
|
|
730
|
-
// napi defaults: urlSafe=false, padding=true.
|
|
731
|
-
const w = Number(
|
|
732
|
-
base64Encode(
|
|
733
|
-
input,
|
|
734
|
-
input.length,
|
|
735
|
-
output,
|
|
736
|
-
output.length,
|
|
737
|
-
flag(urlSafe),
|
|
738
|
-
flag(padding ?? true),
|
|
739
|
-
),
|
|
740
|
-
)
|
|
741
|
-
// Empty input legitimately writes 0 bytes — only a 0 write on NON-empty
|
|
742
|
-
// input is a real error (same convention as the decode/etag paths).
|
|
743
|
-
return writeOrThrow(w, input.length, 'base64 encode')
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
const etagInto = (data: Uint8Array, output: Uint8Array, weak?: boolean): number => {
|
|
747
|
-
const w = Number(etag(data, data.length, output, output.length, flag(weak)))
|
|
748
|
-
if (w === 0 && data.length !== 0) {
|
|
749
|
-
throw new Error('etag: output buffer too small')
|
|
750
|
-
}
|
|
751
|
-
return w
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
return {
|
|
755
|
-
crc32: (input) => Number(oneArg(crc32, input)) >>> 0,
|
|
756
|
-
fnv1a64: (input) => BigInt(oneArg(fnv, input)),
|
|
757
|
-
xxh3: (input) => BigInt(oneArg(xxh, input)),
|
|
758
|
-
jsonValid: (input) => Number(oneArg(jsonValid, input)) === 1,
|
|
759
|
-
hexEncode(input) {
|
|
760
|
-
const out = new Uint8Array(input.length * 2)
|
|
761
|
-
const w = hexEncodeInto(input, out)
|
|
762
|
-
return w === input.length * 2 ? out : out.subarray(0, w)
|
|
763
|
-
},
|
|
764
|
-
hexEncodeInto,
|
|
765
|
-
urlEncode(input) {
|
|
766
|
-
// RFC 3986 worst case is 3 bytes per input byte (`%XX`).
|
|
767
|
-
const out = new Uint8Array(input.length * 3)
|
|
768
|
-
const w = urlEncodeInto(input, out)
|
|
769
|
-
return out.subarray(0, w)
|
|
770
|
-
},
|
|
771
|
-
urlEncodeInto,
|
|
772
|
-
|
|
773
|
-
validateEmail: (input) => Number(oneArg(validateEmail, input)) === 1,
|
|
774
|
-
validateUuid: (input) => Number(oneArg(validateUuid, input)) === 1,
|
|
775
|
-
validateIpv4: (input) => Number(oneArg(validateIpv4, input)) === 1,
|
|
776
|
-
validateIpv6: (input) => Number(oneArg(validateIpv6, input)) === 1,
|
|
777
|
-
jsonSumIds: (input) => {
|
|
778
|
-
// Packed [u8 ok][i64 sum LE] output (9 B): ok=1 → valid array (the sum
|
|
779
|
-
// may be 0); ok=0 → invalid input. Bytes written: 9/1/0 (0 = real error).
|
|
780
|
-
const out = new Uint8Array(9)
|
|
781
|
-
const w = Number(jsonSumRaw(input, input.length, out, out.length))
|
|
782
|
-
if (w === 0) {
|
|
783
|
-
throw new Error('json sum ids: output buffer too small')
|
|
784
|
-
}
|
|
785
|
-
if (out[0] === 0) {
|
|
786
|
-
// Mirrors the napi error phrasing (serde: "expected an array of objects
|
|
787
|
-
// with numeric ids") so both transports throw the same message.
|
|
788
|
-
throw new Error('json sum ids: expected an array of objects with numeric ids')
|
|
789
|
-
}
|
|
790
|
-
return new DataView(out.buffer, out.byteOffset, out.byteLength).getBigInt64(1, true)
|
|
791
|
-
},
|
|
792
|
-
hmacSha256Verify: (key, data, signature) =>
|
|
793
|
-
Number(hmacVerify(key, key.length, data, data.length, signature, signature.length)) === 1,
|
|
794
|
-
csrfVerify: (token, secret) =>
|
|
795
|
-
Number(csrfVerify(token, token.length, secret, secret.length)) === 1,
|
|
796
|
-
passwordVerify: (password, phc) =>
|
|
797
|
-
Number(passwordVerify(password, password.length, phc, phc.length)) === 1,
|
|
798
|
-
passwordVerifyBcrypt: (password, phc) =>
|
|
799
|
-
Number(passwordVerifyBcrypt(password, password.length, phc, phc.length)) === 1,
|
|
800
|
-
|
|
801
|
-
hexDecode(input) {
|
|
802
|
-
const out = new Uint8Array(Math.floor(input.length / 2))
|
|
803
|
-
const w = hexDecodeInto(input, out)
|
|
804
|
-
return out.subarray(0, w)
|
|
805
|
-
},
|
|
806
|
-
hexDecodeInto,
|
|
807
|
-
urlDecode(input) {
|
|
808
|
-
const out = new Uint8Array(input.length)
|
|
809
|
-
const w = urlDecodeInto(input, out)
|
|
810
|
-
return out.subarray(0, w)
|
|
811
|
-
},
|
|
812
|
-
urlDecodeInto,
|
|
813
|
-
base64Decode(input, urlSafe, padding) {
|
|
814
|
-
const out = new Uint8Array(Math.ceil((input.length * 3) / 4))
|
|
815
|
-
const w = base64DecodeInto(input, out, urlSafe, padding)
|
|
816
|
-
return out.subarray(0, w)
|
|
817
|
-
},
|
|
818
|
-
base64DecodeInto,
|
|
819
|
-
|
|
820
|
-
wsAcceptKey(key) {
|
|
821
|
-
const out = new Uint8Array(28)
|
|
822
|
-
const w = Number(wsAcceptKey(key, key.length, out, out.length))
|
|
823
|
-
if (w === 0) {
|
|
824
|
-
throw new Error('ws accept key: bad key or output buffer too small')
|
|
825
|
-
}
|
|
826
|
-
return out.subarray(0, w)
|
|
827
|
-
},
|
|
828
|
-
etag(data, weak) {
|
|
829
|
-
const out = new Uint8Array(12)
|
|
830
|
-
const w = etagInto(data, out, weak)
|
|
831
|
-
return out.subarray(0, w)
|
|
832
|
-
},
|
|
833
|
-
etagInto,
|
|
834
|
-
randomToken(byteLen) {
|
|
835
|
-
const out = new Uint8Array(byteLen * 2)
|
|
836
|
-
const w = Number(randomToken(byteLen, out, out.length))
|
|
837
|
-
// byteLen 0 legitimately yields an empty token (napi returns empty too);
|
|
838
|
-
// any other 0 is a real error (buffer too small / random source failed).
|
|
839
|
-
if (w === 0 && byteLen !== 0) {
|
|
840
|
-
throw new Error('random token: output buffer too small or random source failed')
|
|
841
|
-
}
|
|
842
|
-
return out.subarray(0, w)
|
|
843
|
-
},
|
|
844
|
-
base64Encode(input, urlSafe, padding) {
|
|
845
|
-
const out = new Uint8Array(Math.ceil(input.length / 3) * 4)
|
|
846
|
-
const w = base64EncodeInto(input, out, urlSafe, padding)
|
|
847
|
-
return out.subarray(0, w)
|
|
848
|
-
},
|
|
849
|
-
base64EncodeInto,
|
|
850
|
-
hmacSha256(key, data) {
|
|
851
|
-
const out = new Uint8Array(64)
|
|
852
|
-
const w = Number(hmacSha256(key, key.length, data, data.length, out, out.length))
|
|
853
|
-
if (w === 0) {
|
|
854
|
-
throw new Error('hmac sha256: output buffer too small')
|
|
855
|
-
}
|
|
856
|
-
return out.subarray(0, w)
|
|
857
|
-
},
|
|
858
|
-
hmacSha256Into(key, data, output) {
|
|
859
|
-
if (output.length < 64) {
|
|
860
|
-
throw new Error('hmac sha256: output buffer too small')
|
|
861
|
-
}
|
|
862
|
-
const w = Number(hmacSha256(key, key.length, data, data.length, output, output.length))
|
|
863
|
-
if (w === 0) {
|
|
864
|
-
throw new Error('hmac sha256: output buffer too small')
|
|
865
|
-
}
|
|
866
|
-
return w
|
|
867
|
-
},
|
|
868
|
-
signCookie(value, secret) {
|
|
869
|
-
// `value.<64-hex>` → value.length + 1 + 64.
|
|
870
|
-
const out = new Uint8Array(value.length + 65)
|
|
871
|
-
const w = Number(signCookie(value, value.length, secret, secret.length, out, out.length))
|
|
872
|
-
if (w === 0) {
|
|
873
|
-
throw new Error('sign cookie: output buffer too small')
|
|
874
|
-
}
|
|
875
|
-
return out.subarray(0, w)
|
|
876
|
-
},
|
|
877
|
-
signCookieInto(value, secret, output) {
|
|
878
|
-
const need = value.length + 65
|
|
879
|
-
if (output.length < need) {
|
|
880
|
-
throw new Error('sign cookie: output buffer too small')
|
|
881
|
-
}
|
|
882
|
-
const w = Number(
|
|
883
|
-
signCookie(value, value.length, secret, secret.length, output, output.length),
|
|
884
|
-
)
|
|
885
|
-
if (w === 0) {
|
|
886
|
-
throw new Error('sign cookie: output buffer too small')
|
|
887
|
-
}
|
|
888
|
-
return w
|
|
889
|
-
},
|
|
890
|
-
verifyCookie(signed, secret) {
|
|
891
|
-
const out = new Uint8Array(signed.length)
|
|
892
|
-
const w = Number(verifyCookie(signed, signed.length, secret, secret.length, out, out.length))
|
|
893
|
-
return w === 0 ? null : out.subarray(0, w)
|
|
894
|
-
},
|
|
895
|
-
csrfToken(secret) {
|
|
896
|
-
const out = new Uint8Array(129)
|
|
897
|
-
const w = Number(csrfToken(secret, secret.length, out, out.length))
|
|
898
|
-
if (w === 0) {
|
|
899
|
-
throw new Error('csrf token: output buffer too small or random source failed')
|
|
900
|
-
}
|
|
901
|
-
return out.subarray(0, w)
|
|
902
|
-
},
|
|
903
|
-
passwordHash(password, salt, mCost, tCost, pCost, outLen) {
|
|
904
|
-
// Pre-size with the EXACT PHC string length (computable from the params),
|
|
905
|
-
// so the hash runs once — a grow-retry would re-run the whole argon2
|
|
906
|
-
// hash on a miss. growExact remains the safety net.
|
|
907
|
-
return growExact(
|
|
908
|
-
(out) =>
|
|
909
|
-
Number(
|
|
910
|
-
passwordHash(
|
|
911
|
-
password,
|
|
912
|
-
password.length,
|
|
913
|
-
salt,
|
|
914
|
-
salt.length,
|
|
915
|
-
mCost,
|
|
916
|
-
tCost,
|
|
917
|
-
pCost,
|
|
918
|
-
outLen,
|
|
919
|
-
out,
|
|
920
|
-
out.length,
|
|
921
|
-
),
|
|
922
|
-
),
|
|
923
|
-
argon2PhcLength(mCost, tCost, pCost, outLen),
|
|
924
|
-
2 * 1024 * 1024,
|
|
925
|
-
'password hash: output buffer too small',
|
|
926
|
-
)
|
|
927
|
-
},
|
|
928
|
-
passwordHashBcrypt(password, cost) {
|
|
929
|
-
// `$2b$CC$` + 22 salt chars + 31 hash chars = 60 chars.
|
|
930
|
-
const out = new Uint8Array(64)
|
|
931
|
-
const w = Number(passwordHashBcrypt(password, password.length, cost, out, out.length))
|
|
932
|
-
if (w === 0) {
|
|
933
|
-
throw new Error('password hash bcrypt: output buffer too small')
|
|
934
|
-
}
|
|
935
|
-
return out.subarray(0, w)
|
|
936
|
-
},
|
|
937
|
-
pbkdf2Sha256(password, salt, rounds, dkLen) {
|
|
938
|
-
// Rust clamps dkLen to [1, 1MiB] (PBKDF2_MIN_LEN/MAX_LEN) AFTER sizing its
|
|
939
|
-
// own buffer — so pre-clamp here so dkLen 0 still yields a 1-byte result.
|
|
940
|
-
const dk = Math.min(Math.max(dkLen, 1), 1024 * 1024)
|
|
941
|
-
const out = new Uint8Array(dk)
|
|
942
|
-
const w = Number(
|
|
943
|
-
pbkdf2(password, password.length, salt, salt.length, rounds, dkLen, out, out.length),
|
|
944
|
-
)
|
|
945
|
-
if (w === 0) {
|
|
946
|
-
throw new Error('pbkdf2: output buffer too small')
|
|
947
|
-
}
|
|
948
|
-
return out.subarray(0, w)
|
|
949
|
-
},
|
|
950
|
-
aeadEncrypt(key, nonce, plaintext, algorithm = 0) {
|
|
951
|
-
// ciphertext + 16-byte auth tag.
|
|
952
|
-
const out = new Uint8Array(plaintext.length + 16)
|
|
953
|
-
const w = Number(
|
|
954
|
-
aeadEncrypt(
|
|
955
|
-
key,
|
|
956
|
-
key.length,
|
|
957
|
-
nonce,
|
|
958
|
-
nonce.length,
|
|
959
|
-
plaintext,
|
|
960
|
-
plaintext.length,
|
|
961
|
-
algorithm,
|
|
962
|
-
out,
|
|
963
|
-
out.length,
|
|
964
|
-
),
|
|
965
|
-
)
|
|
966
|
-
if (w === 0) {
|
|
967
|
-
throw new Error('aead encrypt: output buffer too small or bad parameters')
|
|
968
|
-
}
|
|
969
|
-
return out.subarray(0, w)
|
|
970
|
-
},
|
|
971
|
-
aeadEncryptInto(key, nonce, plaintext, output, algorithm = 0) {
|
|
972
|
-
const need = plaintext.length + 16
|
|
973
|
-
if (output.length < need) {
|
|
974
|
-
throw new Error('aead encrypt: output buffer too small')
|
|
975
|
-
}
|
|
976
|
-
const w = Number(
|
|
977
|
-
aeadEncrypt(
|
|
978
|
-
key,
|
|
979
|
-
key.length,
|
|
980
|
-
nonce,
|
|
981
|
-
nonce.length,
|
|
982
|
-
plaintext,
|
|
983
|
-
plaintext.length,
|
|
984
|
-
algorithm,
|
|
985
|
-
output,
|
|
986
|
-
output.length,
|
|
987
|
-
),
|
|
988
|
-
)
|
|
989
|
-
if (w === 0) {
|
|
990
|
-
throw new Error('aead encrypt: output buffer too small or bad parameters')
|
|
991
|
-
}
|
|
992
|
-
return w
|
|
993
|
-
},
|
|
994
|
-
aeadDecrypt(key, nonce, ciphertext, algorithm = 0) {
|
|
995
|
-
const out = new Uint8Array(ciphertext.length)
|
|
996
|
-
const w = Number(
|
|
997
|
-
aeadDecrypt(
|
|
998
|
-
key,
|
|
999
|
-
key.length,
|
|
1000
|
-
nonce,
|
|
1001
|
-
nonce.length,
|
|
1002
|
-
ciphertext,
|
|
1003
|
-
ciphertext.length,
|
|
1004
|
-
algorithm,
|
|
1005
|
-
out,
|
|
1006
|
-
out.length,
|
|
1007
|
-
),
|
|
1008
|
-
)
|
|
1009
|
-
return w === 0 ? null : out.subarray(0, w)
|
|
1010
|
-
},
|
|
1011
|
-
|
|
1012
|
-
wsFrameEncode(opcode, payload, mask, fin) {
|
|
1013
|
-
// Header (max 10) + payload + mask key (4).
|
|
1014
|
-
const out = new Uint8Array(payload.length + 14)
|
|
1015
|
-
const w = Number(
|
|
1016
|
-
wsFrameEncode(opcode, payload, payload.length, flag(mask), flag(fin), out, out.length),
|
|
1017
|
-
)
|
|
1018
|
-
if (w === 0) {
|
|
1019
|
-
throw new Error('ws frame encode: output buffer too small')
|
|
1020
|
-
}
|
|
1021
|
-
return out.subarray(0, w)
|
|
1022
|
-
},
|
|
1023
|
-
wsFrameEncodeInto(opcode, payload, mask, fin, output) {
|
|
1024
|
-
// Header (max 10) + payload + mask key (4).
|
|
1025
|
-
const need = payload.length + 14
|
|
1026
|
-
if (output.length < need) {
|
|
1027
|
-
throw new Error('ws frame encode: output buffer too small')
|
|
1028
|
-
}
|
|
1029
|
-
const w = Number(
|
|
1030
|
-
wsFrameEncode(
|
|
1031
|
-
opcode,
|
|
1032
|
-
payload,
|
|
1033
|
-
payload.length,
|
|
1034
|
-
flag(mask),
|
|
1035
|
-
flag(fin),
|
|
1036
|
-
output,
|
|
1037
|
-
output.length,
|
|
1038
|
-
),
|
|
1039
|
-
)
|
|
1040
|
-
if (w === 0) {
|
|
1041
|
-
throw new Error('ws frame encode: output buffer too small')
|
|
1042
|
-
}
|
|
1043
|
-
return w
|
|
1044
|
-
},
|
|
1045
|
-
jsonPatch(doc, patch) {
|
|
1046
|
-
return growExact(
|
|
1047
|
-
(out) => Number(jsonPatch(doc, doc.length, patch, patch.length, out, out.length)),
|
|
1048
|
-
Math.min(Math.max(doc.length, patch.length) + 16, 64 * 1024),
|
|
1049
|
-
MAX_JSON_PATCH_OUTPUT,
|
|
1050
|
-
'json patch: output buffer too small or patch inapplicable',
|
|
1051
|
-
)
|
|
1052
|
-
},
|
|
1053
|
-
gzipCompress(data, level = 6) {
|
|
1054
|
-
// The C ABI streams the compressed output directly into `out` (no internal
|
|
1055
|
-
// Vec — see rust/payload/compress.rs gzip_compress_into). Cap the initial
|
|
1056
|
-
// so large compressible inputs don't pay a full-size allocation (measured:
|
|
1057
|
-
// a 75 KiB input compressed to <1 KiB — COMPRESS_INITIAL_CAP is plenty and
|
|
1058
|
-
// single-pass). growExact handles incompressible data with at most one
|
|
1059
|
-
// exact retry (no re-run loop).
|
|
1060
|
-
return growExact(
|
|
1061
|
-
(out) => Number(gzipCompress(data, data.length, Math.min(level, 9), out, out.length)),
|
|
1062
|
-
Math.min(data.length + COMPRESS_HEADROOM, COMPRESS_INITIAL_CAP),
|
|
1063
|
-
Math.max(data.length * 2 + COMPRESS_HEADROOM, COMPRESS_MAX_CAP),
|
|
1064
|
-
'gzip compress: output buffer too small',
|
|
1065
|
-
)
|
|
1066
|
-
},
|
|
1067
|
-
gzipCompressInto(data, output, level = 6) {
|
|
1068
|
-
const w = Number(gzipCompress(data, data.length, Math.min(level, 9), output, output.length))
|
|
1069
|
-
// Needed-size convention: w > output.length = too small; w === 0 = real error.
|
|
1070
|
-
if (w === 0) {
|
|
1071
|
-
throw new Error('gzip compress: invalid input')
|
|
1072
|
-
}
|
|
1073
|
-
if (w > output.length) {
|
|
1074
|
-
throw new Error('gzip compress: output buffer too small')
|
|
1075
|
-
}
|
|
1076
|
-
return w
|
|
1077
|
-
},
|
|
1078
|
-
gzipDecompress(data, maxDecompressed) {
|
|
1079
|
-
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
1080
|
-
// Pre-size from the gzip ISIZE trailer via the C probe (exact for
|
|
1081
|
-
// single-member streams) so the happy path is a single native pass; fall
|
|
1082
|
-
// back to a multiplier guess. growExact handles any residual miss with
|
|
1083
|
-
// one exact retry, and invalid input throws immediately (no 64 MiB
|
|
1084
|
-
// grow-to-max allocation storm).
|
|
1085
|
-
const isize = Number(gzipIsize(data, data.length))
|
|
1086
|
-
const initial =
|
|
1087
|
-
isize !== 0
|
|
1088
|
-
? Math.min(isize, max)
|
|
1089
|
-
: Math.min(data.length * DECOMPRESS_GUESS_MULTIPLIER_GZIP, DECOMPRESS_FALLBACK_CAP)
|
|
1090
|
-
return growExact(
|
|
1091
|
-
(out) => Number(gzipDecompress(data, data.length, max, out, out.length)),
|
|
1092
|
-
initial,
|
|
1093
|
-
max,
|
|
1094
|
-
'gzip decompress: invalid stream or exceeded max decompressed size',
|
|
1095
|
-
)
|
|
1096
|
-
},
|
|
1097
|
-
brotliCompress(data, quality = 5) {
|
|
1098
|
-
// Same cap rationale as gzipCompress (streaming core); growExact for
|
|
1099
|
-
// incompressible data.
|
|
1100
|
-
return growExact(
|
|
1101
|
-
(out) => Number(brotliCompress(data, data.length, Math.min(quality, 11), out, out.length)),
|
|
1102
|
-
Math.min(data.length + COMPRESS_HEADROOM, COMPRESS_INITIAL_CAP),
|
|
1103
|
-
Math.max(data.length * 2 + COMPRESS_HEADROOM, COMPRESS_MAX_CAP),
|
|
1104
|
-
'brotli compress: output buffer too small',
|
|
1105
|
-
)
|
|
1106
|
-
},
|
|
1107
|
-
brotliCompressInto(data, output, quality = 5) {
|
|
1108
|
-
const w = Number(
|
|
1109
|
-
brotliCompress(data, data.length, Math.min(quality, 11), output, output.length),
|
|
1110
|
-
)
|
|
1111
|
-
// Needed-size convention: w > output.length = too small; w === 0 = real error.
|
|
1112
|
-
if (w === 0) {
|
|
1113
|
-
throw new Error('brotli compress: invalid input')
|
|
1114
|
-
}
|
|
1115
|
-
if (w > output.length) {
|
|
1116
|
-
throw new Error('brotli compress: output buffer too small')
|
|
1117
|
-
}
|
|
1118
|
-
return w
|
|
1119
|
-
},
|
|
1120
|
-
brotliDecompress(data, maxDecompressed) {
|
|
1121
|
-
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
1122
|
-
// Brotli has no cheap trailer size: DECOMPRESS_GUESS_MULTIPLIER_BROTLI×
|
|
1123
|
-
// initial covers typical JSON/text ratios (~10-30x) while the fallback
|
|
1124
|
-
// cap bounds over-allocation on large streams; growExact handles
|
|
1125
|
-
// higher-ratio streams with one exact retry.
|
|
1126
|
-
return growExact(
|
|
1127
|
-
(out) => Number(brotliDecompress(data, data.length, max, out, out.length)),
|
|
1128
|
-
Math.min(
|
|
1129
|
-
Math.max(data.length * DECOMPRESS_GUESS_MULTIPLIER_BROTLI, DECOMPRESS_MIN_INITIAL),
|
|
1130
|
-
DECOMPRESS_FALLBACK_CAP,
|
|
1131
|
-
),
|
|
1132
|
-
max,
|
|
1133
|
-
'brotli decompress: invalid stream or exceeded max decompressed size',
|
|
1134
|
-
)
|
|
1135
|
-
},
|
|
1136
|
-
|
|
1137
|
-
httpParseRequestPackedInto(input, output) {
|
|
1138
|
-
const w = Number(httpParsePacked(input, input.length, output, output.length))
|
|
1139
|
-
if (w === 0 && input.length !== 0) {
|
|
1140
|
-
throw new Error('http parse: output buffer too small or malformed request')
|
|
1141
|
-
}
|
|
1142
|
-
return w
|
|
1143
|
-
},
|
|
1144
|
-
queryParsePackedInto(input, output) {
|
|
1145
|
-
const w = Number(queryParsePacked(input, input.length, output, output.length))
|
|
1146
|
-
if (w === 0 && input.length !== 0) {
|
|
1147
|
-
throw new Error('query parse: output buffer too small')
|
|
1148
|
-
}
|
|
1149
|
-
return w
|
|
1150
|
-
},
|
|
1151
|
-
cookieParsePackedInto(input, output) {
|
|
1152
|
-
const w = Number(cookieParsePacked(input, input.length, output, output.length))
|
|
1153
|
-
if (w === 0 && input.length !== 0) {
|
|
1154
|
-
throw new Error('cookie parse: output buffer too small')
|
|
1155
|
-
}
|
|
1156
|
-
return w
|
|
1157
|
-
},
|
|
1158
|
-
|
|
1159
|
-
// ── Excluded-surface additions ──────────────────────────────────
|
|
1160
|
-
jwtSignBytes(claimsJson, secret, ttl, now) {
|
|
1161
|
-
// Token = header(~36) + payload (claims + iat/exp, base64url ≈ 4/3×) +
|
|
1162
|
-
// signature (~43) + 2 dots. The old `claims.length + 128` under-sized a
|
|
1163
|
-
// typical token (measured 168 B for a 30 B claim → grow-retry double-run,
|
|
1164
|
-
// which made ffi slower than napi). 2×+128 with a 256-byte floor covers
|
|
1165
|
-
// typical claims in one pass; growExact handles huge claims with one
|
|
1166
|
-
// exact retry (no re-run loop).
|
|
1167
|
-
return growExact(
|
|
1168
|
-
(out) =>
|
|
1169
|
-
Number(
|
|
1170
|
-
jwtSignBytes(
|
|
1171
|
-
claimsJson,
|
|
1172
|
-
claimsJson.length,
|
|
1173
|
-
secret,
|
|
1174
|
-
secret.length,
|
|
1175
|
-
ttl,
|
|
1176
|
-
now,
|
|
1177
|
-
out,
|
|
1178
|
-
out.length,
|
|
1179
|
-
),
|
|
1180
|
-
),
|
|
1181
|
-
Math.min(
|
|
1182
|
-
Math.max(
|
|
1183
|
-
claimsJson.length * JWT_INITIAL_MULTIPLIER + JWT_INITIAL_EXTRA,
|
|
1184
|
-
JWT_INITIAL_FLOOR,
|
|
1185
|
-
),
|
|
1186
|
-
1024 * 1024,
|
|
1187
|
-
),
|
|
1188
|
-
1024 * 1024,
|
|
1189
|
-
'jwt sign: output buffer too small',
|
|
1190
|
-
)
|
|
1191
|
-
},
|
|
1192
|
-
wsFrameDecodePacked(data) {
|
|
1193
|
-
// Max packed output = 6-byte header + payload.
|
|
1194
|
-
const out = new Uint8Array(data.length + 6)
|
|
1195
|
-
const w = Number(wsFrameDecodePacked(data, data.length, out, out.length))
|
|
1196
|
-
return w === 0 ? null : out.subarray(0, w)
|
|
1197
|
-
},
|
|
1198
|
-
multipartParsePacked(body, boundary) {
|
|
1199
|
-
return growExact(
|
|
1200
|
-
(out) =>
|
|
1201
|
-
Number(
|
|
1202
|
-
multipartParsePacked(body, body.length, boundary, boundary.length, out, out.length),
|
|
1203
|
-
),
|
|
1204
|
-
Math.min(body.length + boundary.length + 64, 64 * 1024),
|
|
1205
|
-
128 * 1024 * 1024,
|
|
1206
|
-
'multipart parse: output buffer too small',
|
|
1207
|
-
)
|
|
1208
|
-
},
|
|
1209
|
-
formParsePackedInto(input, output) {
|
|
1210
|
-
const w = Number(formParsePacked(input, input.length, output, output.length))
|
|
1211
|
-
if (w === 0 && input.length !== 0) {
|
|
1212
|
-
throw new Error('form parse: output buffer too small')
|
|
1213
|
-
}
|
|
1214
|
-
return w
|
|
1215
|
-
},
|
|
1216
|
-
ingressHandlePacked(inner, input, body, output) {
|
|
1217
|
-
const w = Number(
|
|
1218
|
-
ingressHandlePacked(
|
|
1219
|
-
inner,
|
|
1220
|
-
input,
|
|
1221
|
-
input.length,
|
|
1222
|
-
body ?? EMPTY_VIEW,
|
|
1223
|
-
body?.length ?? 0,
|
|
1224
|
-
output,
|
|
1225
|
-
output.length,
|
|
1226
|
-
),
|
|
1227
|
-
)
|
|
1228
|
-
if (w === 0) {
|
|
1229
|
-
throw new Error('ingress handle: output buffer too small or pipeline error')
|
|
1230
|
-
}
|
|
1231
|
-
return w
|
|
1232
|
-
},
|
|
1233
|
-
ingressLayout(out) {
|
|
1234
|
-
const w = Number(ingressLayoutSym(out, out.length))
|
|
1235
|
-
if (w !== out.length) {
|
|
1236
|
-
throw new Error('ingress layout: output buffer too small')
|
|
1237
|
-
}
|
|
1238
|
-
return w
|
|
1239
|
-
},
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
/** Verify every bound function against known-good results; false disables ffi. */
|
|
1244
|
-
function selfTest(b: BunFFI): boolean {
|
|
1245
|
-
const dec = new TextDecoder()
|
|
1246
|
-
const enc = encoder
|
|
1247
|
-
|
|
1248
|
-
if (b.crc32(enc.encode('123456789')) !== 0xcbf4_3926) {
|
|
1249
|
-
return false
|
|
1250
|
-
}
|
|
1251
|
-
if (b.fnv1a64(enc.encode('foobar')) !== 0x8594_4171_f739_67e8n) {
|
|
1252
|
-
return false
|
|
1253
|
-
}
|
|
1254
|
-
// XXH3-64 of empty input = 0x2d06800538d394c2 (standard reference vector).
|
|
1255
|
-
if (b.xxh3(new Uint8Array(0)) !== 0x2d06800538d394c2n) {
|
|
1256
|
-
return false
|
|
1257
|
-
}
|
|
1258
|
-
if (b.jsonValid(SELFTEST_JSON) !== true || b.jsonValid(enc.encode('{not json')) !== false) {
|
|
1259
|
-
return false
|
|
1260
|
-
}
|
|
1261
|
-
const hexOut = new Uint8Array(SELFTEST_HEX.length * 2)
|
|
1262
|
-
if (b.hexEncodeInto(SELFTEST_HEX, hexOut) !== 10 || dec.decode(hexOut) !== '68656c6c6f') {
|
|
1263
|
-
return false
|
|
1264
|
-
}
|
|
1265
|
-
const urlInput = enc.encode('a b/c')
|
|
1266
|
-
const urlOut = new Uint8Array(9)
|
|
1267
|
-
if (b.urlEncodeInto(urlInput, urlOut) !== 9 || dec.decode(urlOut) !== 'a%20b%2Fc') {
|
|
1268
|
-
return false
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
// Ingress layout blob (38 × u32 LE). The pinned values catch a reordered
|
|
1272
|
-
// `#[repr(C)] IngressLayout` (drift → self-test fails → napi fallback); the
|
|
1273
|
-
// Rust unit test `ingress_layout_c_abi_matches_output_source` pins every
|
|
1274
|
-
// field against output.rs. Slot order mirrors the struct field order.
|
|
1275
|
-
const layoutBuf = new Uint8Array(38 * 4)
|
|
1276
|
-
b.ingressLayout(layoutBuf)
|
|
1277
|
-
const layoutView = new DataView(layoutBuf.buffer, layoutBuf.byteOffset, layoutBuf.byteLength)
|
|
1278
|
-
if (
|
|
1279
|
-
layoutView.getUint32(0, true) !== 0 || // OUT_VERDICT
|
|
1280
|
-
layoutView.getUint32(2 * 4, true) !== 2 || // OUT_STATUS
|
|
1281
|
-
layoutView.getUint32(12 * 4, true) !== 48 || // OUT_DATA_START
|
|
1282
|
-
layoutView.getUint32(13 * 4, true) !== 1 || // FLAG_HAS_COOKIES
|
|
1283
|
-
layoutView.getUint32(28 * 4, true) !== 32 || // HV_COUNT
|
|
1284
|
-
layoutView.getUint32(37 * 4, true) !== 8 // ERR_INTERNAL
|
|
1285
|
-
) {
|
|
1286
|
-
return false
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// Ingress pipeline C-ABI: with a null (0) inner handle the Rust side returns
|
|
1290
|
-
// 0 immediately and the wrapper throws. This exercises the symbol's ABI (arg
|
|
1291
|
-
// count/types/return) at bind time — a signature drift would surface here
|
|
1292
|
-
// instead of crashing under load. Real frame→output parity is covered by
|
|
1293
|
-
// ffi.test.ts against a live napi instance.
|
|
1294
|
-
try {
|
|
1295
|
-
b.ingressHandlePacked(0, enc.encode('/'), null, new Uint8Array(64))
|
|
1296
|
-
return false // a null handle must throw, not return
|
|
1297
|
-
} catch {
|
|
1298
|
-
// expected: null inner handle → 0 → throw
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// ── New bindings ───────────────────────────────────────────────────
|
|
1302
|
-
const a = enc.encode('a@b.com')
|
|
1303
|
-
const uuid = enc.encode('550e8400-e29b-41d4-a716-446655440000')
|
|
1304
|
-
if (
|
|
1305
|
-
!b.validateEmail(a) ||
|
|
1306
|
-
!b.validateUuid(uuid) ||
|
|
1307
|
-
!b.validateIpv4(enc.encode('192.168.0.1')) ||
|
|
1308
|
-
!b.validateIpv6(enc.encode('2001:db8::1')) ||
|
|
1309
|
-
b.validateEmail(enc.encode('not-an-email')) ||
|
|
1310
|
-
b.validateUuid(enc.encode('not-a-uuid'))
|
|
1311
|
-
) {
|
|
1312
|
-
return false
|
|
1313
|
-
}
|
|
1314
|
-
if (b.jsonSumIds(enc.encode(`[{"id":1},{"id":2}]`)) !== 3n) {
|
|
1315
|
-
return false
|
|
1316
|
-
}
|
|
1317
|
-
// The packed [u8 ok][i64 sum LE] ABI: a legit zero-sum is ok, invalid input throws.
|
|
1318
|
-
if (b.jsonSumIds(enc.encode(`[{"id":0},{"id":0}]`)) !== 0n) {
|
|
1319
|
-
return false
|
|
1320
|
-
}
|
|
1321
|
-
let sumInvalidThrew = false
|
|
1322
|
-
try {
|
|
1323
|
-
b.jsonSumIds(enc.encode('nope'))
|
|
1324
|
-
} catch {
|
|
1325
|
-
sumInvalidThrew = true
|
|
1326
|
-
}
|
|
1327
|
-
if (!sumInvalidThrew) {
|
|
1328
|
-
return false
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// HMAC RFC 4231 test case 1 (0x0b × 20 key, "Hi There" data).
|
|
1332
|
-
const hmacKey = new Uint8Array(20).fill(0x0b)
|
|
1333
|
-
const hmacData = enc.encode('Hi There')
|
|
1334
|
-
const hmacSig = enc.encode('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7')
|
|
1335
|
-
const hmacHex = b.hmacSha256(hmacKey, hmacData)
|
|
1336
|
-
if (dec.decode(hmacHex) !== 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7') {
|
|
1337
|
-
return false
|
|
1338
|
-
}
|
|
1339
|
-
if (!b.hmacSha256Verify(hmacKey, hmacData, hmacSig)) {
|
|
1340
|
-
return false
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
// Decoders round-trip.
|
|
1344
|
-
const decoded = b.hexDecode(enc.encode('68656c6c6f'))
|
|
1345
|
-
if (
|
|
1346
|
-
dec.decode(decoded) !== 'hello' ||
|
|
1347
|
-
dec.decode(b.urlDecode(enc.encode('a%20b%2Fc'))) !== 'a b/c'
|
|
1348
|
-
) {
|
|
1349
|
-
return false
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
// WebSocket accept key (RFC 6455 sample).
|
|
1353
|
-
if (
|
|
1354
|
-
dec.decode(b.wsAcceptKey(enc.encode('dGhlIHNhbXBsZSBub25jZQ=='))) !==
|
|
1355
|
-
's3pPLMBiTxaQ9kYGzzhZRbK+xOo='
|
|
1356
|
-
) {
|
|
1357
|
-
return false
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
// ETag: strong = 10 bytes, weak = 12 bytes.
|
|
1361
|
-
if (b.etag(SELFTEST_HEX).length !== 10 || b.etag(SELFTEST_HEX, true).length !== 12) {
|
|
1362
|
-
return false
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
// base64.
|
|
1366
|
-
if (dec.decode(b.base64Encode(SELFTEST_HEX)) !== 'aGVsbG8=') {
|
|
1367
|
-
return false
|
|
1368
|
-
}
|
|
1369
|
-
if (dec.decode(b.base64Decode(enc.encode('aGVsbG8='))) !== 'hello') {
|
|
1370
|
-
return false
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
// Signed cookie round-trip.
|
|
1374
|
-
const secret = enc.encode('s3cr3t-secret')
|
|
1375
|
-
const signed = b.signCookie(SELFTEST_HEX, secret)
|
|
1376
|
-
const verified = b.verifyCookie(signed, secret)
|
|
1377
|
-
if (verified === null || dec.decode(verified) !== 'hello') {
|
|
1378
|
-
return false
|
|
1379
|
-
}
|
|
1380
|
-
if (b.verifyCookie(enc.encode('tampered.0000'), secret) !== null) {
|
|
1381
|
-
return false
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
// CSRF token round-trip (issued token verifies against the same secret).
|
|
1385
|
-
const csrfTokenBytes = b.csrfToken(secret)
|
|
1386
|
-
if (csrfTokenBytes.length !== 129 || !b.csrfVerify(csrfTokenBytes, secret)) {
|
|
1387
|
-
return false
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// Argon2id round-trip at minimum cost (fast) — full defaults would take ~50ms.
|
|
1391
|
-
const pw = enc.encode('correct horse battery staple')
|
|
1392
|
-
const salt = enc.encode('salty-salt-16b')
|
|
1393
|
-
const phc = b.passwordHash(pw, salt, 8, 1, 1, 16)
|
|
1394
|
-
if (phc.length === 0 || !b.passwordVerify(pw, phc)) {
|
|
1395
|
-
return false
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// bcrypt round-trip at minimum cost (fast).
|
|
1399
|
-
const bcryptPhc = b.passwordHashBcrypt(pw, 4)
|
|
1400
|
-
if (bcryptPhc.length === 0 || !b.passwordVerifyBcrypt(pw, bcryptPhc)) {
|
|
1401
|
-
return false
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// PBKDF2-HMAC-SHA256: password="password", salt="salt", c=1, dkLen=32.
|
|
1405
|
-
// The C ABI writes the RAW derived key; hex-encode before comparing.
|
|
1406
|
-
const dk = b.pbkdf2Sha256(enc.encode('password'), enc.encode('salt'), 1, 32)
|
|
1407
|
-
if (
|
|
1408
|
-
dec.decode(b.hexEncode(dk)) !==
|
|
1409
|
-
'120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b'
|
|
1410
|
-
) {
|
|
1411
|
-
return false
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// AEAD AES-256-GCM round-trip (key 32B, nonce 12B).
|
|
1415
|
-
const aeadKey = new Uint8Array(32).fill(0x42)
|
|
1416
|
-
const nonce = new Uint8Array(12).fill(0x07)
|
|
1417
|
-
const ct = b.aeadEncrypt(aeadKey, nonce, SELFTEST_HEX, 0)
|
|
1418
|
-
const pt = b.aeadDecrypt(aeadKey, nonce, ct, 0)
|
|
1419
|
-
if (pt === null || dec.decode(pt) !== 'hello') {
|
|
1420
|
-
return false
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// WebSocket frame: text frame, FIN, no mask → first byte 0x81.
|
|
1424
|
-
const frame = b.wsFrameEncode(1, SELFTEST_HEX, false, true)
|
|
1425
|
-
if (frame.length === 0 || frame[0] !== 0x81) {
|
|
1426
|
-
return false
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
// JSON patch: add a key.
|
|
1430
|
-
const patched = b.jsonPatch(
|
|
1431
|
-
enc.encode(`{"a":"b"}`),
|
|
1432
|
-
enc.encode(`[{"op":"add","path":"/c","value":"d"}]`),
|
|
1433
|
-
)
|
|
1434
|
-
if (!dec.decode(patched).includes(`"c":"d"`)) {
|
|
1435
|
-
return false
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
// gzip / brotli round-trips.
|
|
1439
|
-
const gz = b.gzipCompress(SELFTEST_HEX)
|
|
1440
|
-
if (dec.decode(b.gzipDecompress(gz)) !== 'hello') {
|
|
1441
|
-
return false
|
|
1442
|
-
}
|
|
1443
|
-
const br = b.brotliCompress(SELFTEST_HEX)
|
|
1444
|
-
if (dec.decode(b.brotliDecompress(br)) !== 'hello') {
|
|
1445
|
-
return false
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
// Needed-size convention: invalid compressed input throws IMMEDIATELY (the C
|
|
1449
|
-
// ABI returns 0 = real error, so the JS wrapper does NOT grow-retry re-runs
|
|
1450
|
-
// or allocate up to the 64 MiB decompression cap per bad input).
|
|
1451
|
-
let decompressThrew = false
|
|
1452
|
-
try {
|
|
1453
|
-
b.gzipDecompress(enc.encode('not-a-gzip-stream'))
|
|
1454
|
-
} catch {
|
|
1455
|
-
decompressThrew = true
|
|
1456
|
-
}
|
|
1457
|
-
if (!decompressThrew) {
|
|
1458
|
-
return false
|
|
1459
|
-
}
|
|
1460
|
-
decompressThrew = false
|
|
1461
|
-
try {
|
|
1462
|
-
b.brotliDecompress(enc.encode('not-brotli-stream'))
|
|
1463
|
-
} catch {
|
|
1464
|
-
decompressThrew = true
|
|
1465
|
-
}
|
|
1466
|
-
if (!decompressThrew) {
|
|
1467
|
-
return false
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// Packed parsers (non-empty output). Packed output is LARGER than input (each
|
|
1471
|
-
// component gets a u32 length prefix), so size with the Rust allocator's
|
|
1472
|
-
// conservative upper bound (`input.len() * 9 + 16` in query_parser.rs).
|
|
1473
|
-
const req = enc.encode('GET /a?b=1 HTTP/1.1\r\nHost: example.com\r\n\r\n')
|
|
1474
|
-
const reqOut = new Uint8Array(req.length * 9 + 16)
|
|
1475
|
-
if (b.httpParseRequestPackedInto(req, reqOut) === 0) {
|
|
1476
|
-
return false
|
|
1477
|
-
}
|
|
1478
|
-
const qIn = enc.encode('a=1&b=2')
|
|
1479
|
-
const qOut = new Uint8Array(qIn.length * 9 + 16)
|
|
1480
|
-
if (b.queryParsePackedInto(qIn, qOut) === 0) {
|
|
1481
|
-
return false
|
|
1482
|
-
}
|
|
1483
|
-
const cIn = enc.encode('a=1; b=2')
|
|
1484
|
-
const cOut = new Uint8Array(cIn.length * 9 + 16)
|
|
1485
|
-
if (b.cookieParsePackedInto(cIn, cOut) === 0) {
|
|
1486
|
-
return false
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
// ── Excluded-surface additions ────────────────────────────────────
|
|
1490
|
-
// form parse shares the query core → 2 pairs.
|
|
1491
|
-
const fIn = enc.encode('a=1&b=2')
|
|
1492
|
-
const fOut = new Uint8Array(fIn.length * 9 + 16)
|
|
1493
|
-
if (b.formParsePackedInto(fIn, fOut) === 0 || fOut[0] !== 2) {
|
|
1494
|
-
return false
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// multipart parse → 1 part named "field".
|
|
1498
|
-
const boundary = enc.encode('----boundary')
|
|
1499
|
-
// Wire format is `--{boundary}` — boundary is `----boundary`, so the body
|
|
1500
|
-
// must open with `------boundary`.
|
|
1501
|
-
const mBody = enc.encode(
|
|
1502
|
-
'------boundary\r\nContent-Disposition: form-data; name="field"\r\n\r\nvalue\r\n------boundary--',
|
|
1503
|
-
)
|
|
1504
|
-
const mOut = b.multipartParsePacked(mBody, boundary)
|
|
1505
|
-
if (mOut[0] !== 1) {
|
|
1506
|
-
return false
|
|
1507
|
-
}
|
|
1508
|
-
// Packed layout: [u32 count][u32 name_len][name]... → name_len at offset 4.
|
|
1509
|
-
const mNameLen = (mOut[4] ?? 0) | ((mOut[5] ?? 0) << 8) | ((mOut[6] ?? 0) << 16) | ((mOut[7] ?? 0) << 24)
|
|
1510
|
-
if (dec.decode(mOut.subarray(8, 8 + mNameLen)) !== 'field') {
|
|
1511
|
-
return false
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
// WS frame decode: encode("hello") → decode → fin=1, opcode=1, payload="hello".
|
|
1515
|
-
const wf = b.wsFrameEncode(1, SELFTEST_HEX, true, true)
|
|
1516
|
-
const wd = b.wsFrameDecodePacked(wf)
|
|
1517
|
-
if (wd === null || wd[0] !== 1 || wd[1] !== 1 || dec.decode(wd.subarray(6)) !== 'hello') {
|
|
1518
|
-
return false
|
|
1519
|
-
}
|
|
1520
|
-
if (b.wsFrameDecodePacked(enc.encode('\x80')) !== null) {
|
|
1521
|
-
return false
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
// JWT sign with ttl=0 (deterministic — no iat/exp), then verify the
|
|
1525
|
-
// signature with the FFI HMAC to prove the binding is real.
|
|
1526
|
-
const jwtSecret = enc.encode('my-secret')
|
|
1527
|
-
const jwt = b.jwtSignBytes(enc.encode('{"sub":"user-1"}'), jwtSecret, 0, 0)
|
|
1528
|
-
const jwtStr = dec.decode(jwt)
|
|
1529
|
-
const segs = jwtStr.split('.')
|
|
1530
|
-
if (segs.length !== 3 || segs[0] === '' || segs[1] === '' || segs[2] === '') {
|
|
1531
|
-
return false
|
|
1532
|
-
}
|
|
1533
|
-
const signingInput = enc.encode(`${segs[0]}.${segs[1]}`)
|
|
1534
|
-
const sigHex = dec.decode(b.hmacSha256(jwtSecret, signingInput))
|
|
1535
|
-
const sigBytes = b.base64Decode(enc.encode(segs[2]), true, false)
|
|
1536
|
-
if (sigBytes === null || dec.decode(b.hexEncode(sigBytes)) !== sigHex) {
|
|
1537
|
-
return false
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
return true
|
|
1541
|
-
}
|