castrum 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +664 -8
- package/dist/index.js +2014 -1327
- package/docs/BENCHMARKS.md +13 -0
- package/docs/CASE_STUDY.md +1 -1
- package/docs/ENVIRONMENT.md +8 -1
- package/docs/FFI_BUN_GUIDE.md +1 -1
- package/docs/INGRESS.md +4 -4
- package/docs/REPO_MAP.md +7 -5
- package/docs/RND-CONCURRENCY.md +20 -4
- package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
- package/docs/superpowers/plans/2026-09-18-elysia-learnings-adoption.md +634 -0
- package/index.ts +6 -0
- package/package.json +4 -2
- package/rust/crypto/aead.rs +44 -0
- package/rust/crypto/argon2.rs +32 -2
- package/rust/crypto/jwt/tests.rs +19 -0
- package/rust/crypto/jwt/token.rs +43 -28
- package/rust/crypto/session.rs +6 -0
- package/rust/ffi/crypto.rs +63 -32
- package/rust/ffi/hashing.rs +48 -33
- package/rust/ffi/http.rs +62 -36
- package/rust/ffi/metrics.rs +6 -2
- package/rust/ffi/payload.rs +47 -27
- package/rust/ffi/task.rs +55 -35
- package/rust/ffi/tests.rs +24 -0
- package/rust/ffi/util.rs +40 -0
- package/rust/http/headers.rs +13 -0
- package/rust/http/multipart.rs +139 -0
- package/rust/ingress/api.rs +21 -5
- package/rust/ingress/mod.rs +7 -0
- package/rust/ingress/native_route.rs +25 -5
- package/rust/ingress/schema_cache.rs +266 -0
- package/rust/task/completion.rs +7 -0
- package/rust/task/mod.rs +2 -1
- package/rust/task/runtime.rs +55 -7
- package/rust/task/tests.rs +128 -1
- package/src/ingress/abort.ts +48 -0
- package/src/ingress/body.ts +104 -8
- package/src/ingress/decode/baked-result.ts +13 -5
- package/src/ingress/decode/fast-result.ts +1 -1
- package/src/ingress/decode/result-base.ts +16 -2
- package/src/ingress/handlers.ts +104 -31
- package/src/ingress/headers/memoized-headers.ts +40 -0
- package/src/ingress/health.ts +22 -0
- package/src/ingress/index.ts +3 -1
- package/src/ingress/native-route.ts +25 -6
- package/src/ingress/options.ts +26 -2
- package/src/ingress/packing/gather-raw-headers.ts +24 -8
- package/src/ingress/response/baked-response.ts +118 -28
- package/src/ingress/router.ts +50 -3
- package/src/ingress/routes/common.ts +35 -11
- package/src/ingress/routes/echo.ts +60 -7
- package/src/ingress/routes/json-write.ts +32 -8
- package/src/ingress/routes/native.ts +24 -5
- package/src/ingress/routes/options.ts +6 -2
- package/src/ingress/routes/responder.ts +29 -9
- package/src/ingress/routes/safe-record.ts +57 -0
- package/src/ingress/server-error.ts +111 -0
- package/src/ingress/server-node.ts +135 -37
- package/src/ingress/server.ts +143 -36
- package/src/ingress/shared.ts +1 -1
- package/src/ingress/types.ts +15 -0
- package/src/integration/pipeline.ts +6 -0
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs-selftest.ts +305 -0
- package/src/native/ffi/build/codecs.ts +0 -296
- package/src/native/ffi/build/instances-selftest.ts +264 -0
- package/src/native/ffi/build/instances.ts +10 -259
- package/src/native/ffi/build/parse-selftest.ts +238 -0
- package/src/native/ffi/build/parse.ts +11 -234
- package/src/native/ffi/build/task.ts +2 -1
- package/src/native/ffi/build.ts +5 -1
- package/src/native/ffi/selftest.ts +3 -3
- package/src/native/ffi/symbols.ts +460 -0
- package/src/native/ffi/types.ts +6 -1
- package/src/native/ffi.ts +2 -434
- package/src/native/ingress-binding.ts +39 -0
- package/src/native/types.ts +6 -0
- package/src/runtime/codec.ts +24 -0
- package/src/runtime/detect.ts +14 -0
- package/src/runtime/types.ts +11 -0
- package/src/rust-ffi/batch/build.ts +18 -8
- package/src/rust-ffi/batch/types.ts +15 -0
- package/src/rust-ffi/context.ts +46 -3
- package/src/rust-ffi/options.ts +19 -0
- package/src/rust-ffi/scalar/crypto.ts +8 -0
- package/src/rust-ffi/scalar/factories.ts +53 -20
- package/src/rust-ffi/scalar/http.ts +60 -10
- package/src/rust-ffi/scalar/interface.ts +18 -1
- package/src/rust-ffi/scalar/payload.ts +27 -1
- package/src/rust-ffi/text.ts +5 -4
- package/src/shared/buffer-pool.ts +54 -9
- package/src/shared/codec.ts +26 -0
- package/src/shared/memory.ts +68 -0
- package/src/shared/metrics.ts +86 -3
- package/src/shared/packed/parsers.ts +64 -7
- package/src/shared/runtime.ts +7 -0
- package/src/task/runtime.ts +81 -37
package/src/native/ffi.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
// `(view, view.length)` and bun:ffi converts the TypedArray to its pointer.
|
|
27
27
|
|
|
28
28
|
import type { FFITypeOrString } from 'bun:ffi'
|
|
29
|
+
import { buildFFISymbolMap } from './ffi/symbols'
|
|
29
30
|
import { resolveEnvVar } from '../shared/env'
|
|
30
31
|
import { isBun } from '../shared/runtime'
|
|
31
32
|
import { build } from './ffi/build'
|
|
@@ -177,440 +178,7 @@ function bind(forcedPath?: string, tried: string[] = []): BunFFI | null {
|
|
|
177
178
|
const useBufferLength = probeBufferLength(dlopen, path)
|
|
178
179
|
bufferAbiMode = useBufferLength ? 'buffer-pair' : 'ptr-len'
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
// via `FFITypeOrString` in bun-types. usize == uint64 on the host ABI.
|
|
182
|
-
// Symbol keys must be the exact `#[no_mangle] extern "C"` names in
|
|
183
|
-
// rust/ffi/. Every `(ptr,len)` pair is passed as `(view, view.length)`.
|
|
184
|
-
// The 8 input-only single-buffer fns use `inputAbi`; everything else keeps
|
|
185
|
-
// `(ptr,len)` because the output length is the caller's write cap.
|
|
186
|
-
const inputAbi: readonly FFITypeOrString[] = useBufferLength
|
|
187
|
-
? (['buffer', 'buffer_length'] as unknown as readonly FFITypeOrString[])
|
|
188
|
-
: ['ptr', 'usize']
|
|
189
|
-
// Phase 1.1: extend `buffer`/`buffer_length` to EVERY `(ptr,len)` pair, not
|
|
190
|
-
// just the input-only fns. The engine reads ptr + byteLength off the SAME
|
|
191
|
-
// view at call time (atomic snapshot), so call sites pass the view twice
|
|
192
|
-
// (see `lenOrView` below). Conversion is POSITIONAL and pair-aware: each
|
|
193
|
-
// `ptr` consumes its following `usize` as a length. Scalar args that happen
|
|
194
|
-
// to be `usize` (the opaque `inner` handle of `castrum_ingress_handle_packed`
|
|
195
|
-
// and the `max` param of gzip/brotli decompress) are passed through
|
|
196
|
-
// unchanged — a blind `usize`→`buffer_length` map would corrupt them.
|
|
197
|
-
const abi = (shape: readonly string[]): readonly FFITypeOrString[] => {
|
|
198
|
-
if (!useBufferLength) return shape as readonly FFITypeOrString[]
|
|
199
|
-
const out: FFITypeOrString[] = []
|
|
200
|
-
for (let i = 0; i < shape.length; i++) {
|
|
201
|
-
const t = shape[i]
|
|
202
|
-
if (t === 'ptr') {
|
|
203
|
-
out.push('buffer' as unknown as FFITypeOrString)
|
|
204
|
-
out.push('buffer_length' as unknown as FFITypeOrString)
|
|
205
|
-
i++ // consume the paired `usize`
|
|
206
|
-
} else {
|
|
207
|
-
out.push(t as FFITypeOrString)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return out
|
|
211
|
-
}
|
|
212
|
-
// `u64_fast` returns a plain `number` when the value fits (< 2^53) instead
|
|
213
|
-
// of a BigInt — bun-types lacks the literal, so bind it via a typed const
|
|
214
|
-
// (same cast pattern as the `buffer_length` probe above). Every bytes-written
|
|
215
|
-
// return fits in a double, so this removes the per-call BigInt boxing on the
|
|
216
|
-
// hot scalar / `*_into` / packed-parser / ingress calls.
|
|
217
|
-
const U64_FAST = 'u64_fast' as unknown as FFITypeOrString
|
|
218
|
-
const { symbols } = dlopen(path, {
|
|
219
|
-
castrum_crc32: { args: inputAbi, returns: 'u32' },
|
|
220
|
-
castrum_fnv1a64: { args: inputAbi, returns: 'u64' },
|
|
221
|
-
castrum_xxh3: { args: inputAbi, returns: 'u64' },
|
|
222
|
-
castrum_json_valid: { args: inputAbi, returns: 'u8' },
|
|
223
|
-
castrum_utf8_valid: { args: inputAbi, returns: 'u8' },
|
|
224
|
-
castrum_hex_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
225
|
-
castrum_hex_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
226
|
-
castrum_url_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
227
|
-
castrum_url_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
228
|
-
// String-input validators: `cstring` ARG (the engine transcodes the JS
|
|
229
|
-
// string in-engine — no JS-side encode; Rust borrows via CStr::from_ptr).
|
|
230
|
-
// The `_bytes` siblings take a `(ptr,len)` pair — zero transcode when
|
|
231
|
-
// the caller already holds bytes (a cstring arg would force a decode +
|
|
232
|
-
// engine re-encode; docs/FFI_BUN_GUIDE.md §3 shape 4).
|
|
233
|
-
//
|
|
234
|
-
// SAFETY RULE for every `cstring` ARG added here: the transcode is
|
|
235
|
-
// NUL-TERMINATED, so an embedded U+0000 truncates the value in-engine. A
|
|
236
|
-
// verdict op bound this way can be fooled (`validateEmail` on
|
|
237
|
-
// 'a@b.com\0<script>' returned TRUE before the byte re-route). Either
|
|
238
|
-
// give the op a `_bytes` sibling and route byte callers to it, or guard
|
|
239
|
-
// the string form with `hasNul` — see docs/FFI_BUN_GUIDE.md §6.1,
|
|
240
|
-
// pinned by test/unit/native/cstring-nul.test.ts.
|
|
241
|
-
castrum_validate_email: { args: ['cstring'], returns: 'u8' },
|
|
242
|
-
castrum_validate_uuid: { args: ['cstring'], returns: 'u8' },
|
|
243
|
-
castrum_validate_ipv4: { args: ['cstring'], returns: 'u8' },
|
|
244
|
-
castrum_validate_ipv6: { args: ['cstring'], returns: 'u8' },
|
|
245
|
-
castrum_validate_email_bytes: { args: inputAbi, returns: 'u8' },
|
|
246
|
-
castrum_validate_uuid_bytes: { args: inputAbi, returns: 'u8' },
|
|
247
|
-
castrum_validate_ipv4_bytes: { args: inputAbi, returns: 'u8' },
|
|
248
|
-
castrum_validate_ipv6_bytes: { args: inputAbi, returns: 'u8' },
|
|
249
|
-
castrum_json_sum_ids: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
250
|
-
castrum_hmac_sha256_verify: {
|
|
251
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
252
|
-
returns: 'u8',
|
|
253
|
-
},
|
|
254
|
-
castrum_csrf_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
|
|
255
|
-
castrum_password_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
|
|
256
|
-
castrum_password_verify_bcrypt: {
|
|
257
|
-
// `phc` crosses as a `cstring` ARG; `password` stays a `(ptr,len)` pair.
|
|
258
|
-
args: abi(['ptr', 'usize', 'cstring']),
|
|
259
|
-
returns: 'u8',
|
|
260
|
-
},
|
|
261
|
-
castrum_ws_accept_key: { args: ['cstring'], returns: 'cstring' },
|
|
262
|
-
castrum_ws_accept_key_into: {
|
|
263
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
264
|
-
returns: U64_FAST,
|
|
265
|
-
},
|
|
266
|
-
castrum_etag: { args: abi(['ptr', 'usize', 'u8']), returns: 'cstring' },
|
|
267
|
-
castrum_etag_into: { args: abi(['ptr', 'usize', 'u8', 'ptr', 'usize']), returns: U64_FAST },
|
|
268
|
-
castrum_conditional_is_not_modified: {
|
|
269
|
-
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'u8']),
|
|
270
|
-
returns: 'u8',
|
|
271
|
-
},
|
|
272
|
-
castrum_media_type_matcher_matches: {
|
|
273
|
-
args: abi(['usize', 'ptr', 'usize']),
|
|
274
|
-
returns: 'u8',
|
|
275
|
-
},
|
|
276
|
-
castrum_accept_negotiator_negotiate: {
|
|
277
|
-
args: abi(['usize', 'ptr', 'usize']),
|
|
278
|
-
returns: 'cstring',
|
|
279
|
-
},
|
|
280
|
-
// Server-preference sibling (RFC 7231 server semantics): `header` is a
|
|
281
|
-
// `cstring` ARG — the engine transcodes the JS header string in-engine.
|
|
282
|
-
castrum_accept_negotiator_negotiate_server: {
|
|
283
|
-
args: ['usize', 'cstring'],
|
|
284
|
-
returns: 'cstring',
|
|
285
|
-
},
|
|
286
|
-
castrum_jwt_signer_sign: {
|
|
287
|
-
args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
|
|
288
|
-
returns: U64_FAST,
|
|
289
|
-
},
|
|
290
|
-
castrum_jwt_signer_verify: {
|
|
291
|
-
args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
|
|
292
|
-
returns: U64_FAST,
|
|
293
|
-
},
|
|
294
|
-
castrum_template_render: {
|
|
295
|
-
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
296
|
-
returns: U64_FAST,
|
|
297
|
-
},
|
|
298
|
-
castrum_schema_validator_validate: {
|
|
299
|
-
args: abi(['usize', 'ptr', 'usize']),
|
|
300
|
-
returns: 'u8',
|
|
301
|
-
},
|
|
302
|
-
castrum_rate_limiter_check: {
|
|
303
|
-
// `key` crosses as a `cstring` ARG (text, no NUL in the intended use).
|
|
304
|
-
args: abi(['usize', 'cstring', 'i64', 'ptr', 'usize']),
|
|
305
|
-
returns: U64_FAST,
|
|
306
|
-
},
|
|
307
|
-
castrum_rate_limiter_check_key: {
|
|
308
|
-
args: abi(['usize', 'i64', 'i64', 'ptr', 'usize']),
|
|
309
|
-
returns: U64_FAST,
|
|
310
|
-
},
|
|
311
|
-
castrum_random_token: { args: ['u32'], returns: 'cstring' },
|
|
312
|
-
castrum_random_token_into: { args: ['u32', ...abi(['ptr', 'usize'])], returns: U64_FAST },
|
|
313
|
-
castrum_base64_encode: {
|
|
314
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
|
|
315
|
-
returns: U64_FAST,
|
|
316
|
-
},
|
|
317
|
-
castrum_base64_decode: {
|
|
318
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
|
|
319
|
-
returns: U64_FAST,
|
|
320
|
-
},
|
|
321
|
-
castrum_hmac_sha256: {
|
|
322
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
323
|
-
returns: U64_FAST,
|
|
324
|
-
},
|
|
325
|
-
castrum_sign_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
|
|
326
|
-
castrum_sign_cookie_into: {
|
|
327
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
328
|
-
returns: U64_FAST,
|
|
329
|
-
},
|
|
330
|
-
castrum_verify_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
|
|
331
|
-
castrum_verify_cookie_into: {
|
|
332
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
333
|
-
returns: U64_FAST,
|
|
334
|
-
},
|
|
335
|
-
castrum_csrf_token: { args: abi(['ptr', 'usize']), returns: 'cstring' },
|
|
336
|
-
castrum_csrf_token_into: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
337
|
-
castrum_password_hash: {
|
|
338
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'u32', 'u32', 'ptr', 'usize']),
|
|
339
|
-
returns: U64_FAST,
|
|
340
|
-
},
|
|
341
|
-
castrum_password_hash_bcrypt: {
|
|
342
|
-
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
343
|
-
returns: U64_FAST,
|
|
344
|
-
},
|
|
345
|
-
castrum_pbkdf2_sha256: {
|
|
346
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'ptr', 'usize']),
|
|
347
|
-
returns: U64_FAST,
|
|
348
|
-
},
|
|
349
|
-
castrum_aead_encrypt: {
|
|
350
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
|
|
351
|
-
returns: U64_FAST,
|
|
352
|
-
},
|
|
353
|
-
castrum_aead_decrypt: {
|
|
354
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
|
|
355
|
-
returns: U64_FAST,
|
|
356
|
-
},
|
|
357
|
-
castrum_ws_frame_encode: {
|
|
358
|
-
args: abi(['u8', 'ptr', 'usize', 'u8', 'u8', 'ptr', 'usize']),
|
|
359
|
-
returns: U64_FAST,
|
|
360
|
-
},
|
|
361
|
-
castrum_json_patch: {
|
|
362
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
363
|
-
returns: U64_FAST,
|
|
364
|
-
},
|
|
365
|
-
castrum_gzip_compress: {
|
|
366
|
-
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
367
|
-
returns: U64_FAST,
|
|
368
|
-
},
|
|
369
|
-
castrum_gzip_decompress: {
|
|
370
|
-
args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
|
|
371
|
-
returns: U64_FAST,
|
|
372
|
-
},
|
|
373
|
-
castrum_brotli_compress: {
|
|
374
|
-
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
375
|
-
returns: U64_FAST,
|
|
376
|
-
},
|
|
377
|
-
castrum_brotli_decompress: {
|
|
378
|
-
args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
|
|
379
|
-
returns: U64_FAST,
|
|
380
|
-
},
|
|
381
|
-
castrum_gzip_isize: { args: abi(['ptr', 'usize']), returns: 'u32' },
|
|
382
|
-
castrum_http_parse_request_packed: {
|
|
383
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
384
|
-
returns: U64_FAST,
|
|
385
|
-
},
|
|
386
|
-
castrum_query_parse_packed: {
|
|
387
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
388
|
-
returns: U64_FAST,
|
|
389
|
-
},
|
|
390
|
-
castrum_cookie_parse_packed: {
|
|
391
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
392
|
-
returns: U64_FAST,
|
|
393
|
-
},
|
|
394
|
-
castrum_http_date_into: { args: ['f64', ...abi(['ptr', 'usize'])], returns: U64_FAST },
|
|
395
|
-
castrum_sse_encode_into: {
|
|
396
|
-
// event/id stay `(ptr,len)` byte args. A cstring-ARG conversion was
|
|
397
|
-
// tried (2026-08-23) and reverted: the win was unproven once the
|
|
398
|
-
// codec encode path got faster, and ABI-desync segfaults during the
|
|
399
|
-
// experiment were misread as an engine bug — see docs/FFI_BUN_GUIDE.md
|
|
400
|
-
// §14 before touching this signature.
|
|
401
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'u32', 'ptr', 'usize']),
|
|
402
|
-
returns: U64_FAST,
|
|
403
|
-
},
|
|
404
|
-
// ── Excluded-surface additions ──
|
|
405
|
-
castrum_jwt_sign_bytes: {
|
|
406
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64']),
|
|
407
|
-
returns: 'cstring',
|
|
408
|
-
},
|
|
409
|
-
castrum_jwt_sign_bytes_into: {
|
|
410
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64', 'ptr', 'usize']),
|
|
411
|
-
returns: U64_FAST,
|
|
412
|
-
},
|
|
413
|
-
// ── Ed25519 / EdDSA JWT (RBAC auth) — bound + self-tested; the public
|
|
414
|
-
// surface is deferred to the auth-module consumer ────────────────
|
|
415
|
-
castrum_ed25519_generate_keypair: {
|
|
416
|
-
args: abi(['ptr', 'usize']),
|
|
417
|
-
returns: U64_FAST,
|
|
418
|
-
},
|
|
419
|
-
castrum_ed25519_sign: {
|
|
420
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
421
|
-
returns: U64_FAST,
|
|
422
|
-
},
|
|
423
|
-
castrum_ed25519_verify: {
|
|
424
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
425
|
-
returns: 'u32',
|
|
426
|
-
},
|
|
427
|
-
castrum_jwt_eddsa_sign: {
|
|
428
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64']),
|
|
429
|
-
returns: 'cstring',
|
|
430
|
-
},
|
|
431
|
-
castrum_jwt_eddsa_verify: {
|
|
432
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64']),
|
|
433
|
-
returns: 'cstring',
|
|
434
|
-
},
|
|
435
|
-
castrum_ws_frame_decode_packed: {
|
|
436
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
437
|
-
returns: U64_FAST,
|
|
438
|
-
},
|
|
439
|
-
castrum_multipart_parse_packed: {
|
|
440
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
441
|
-
returns: U64_FAST,
|
|
442
|
-
},
|
|
443
|
-
castrum_form_parse_packed: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
444
|
-
// ── Phase 3 — stateless napi-only scalars (now FFI) ──────────
|
|
445
|
-
// Structural parse: parses ONCE, emits a packed token stream with a
|
|
446
|
-
// deduped string table so JS assembles the value with NO second parse.
|
|
447
|
-
castrum_json_parse_packed: {
|
|
448
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
449
|
-
returns: U64_FAST,
|
|
450
|
-
},
|
|
451
|
-
castrum_parse_media_type: {
|
|
452
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
453
|
-
returns: U64_FAST,
|
|
454
|
-
},
|
|
455
|
-
castrum_parse_http_date: {
|
|
456
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
457
|
-
returns: U64_FAST,
|
|
458
|
-
},
|
|
459
|
-
castrum_parse_accept_encoding: {
|
|
460
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
461
|
-
returns: U64_FAST,
|
|
462
|
-
},
|
|
463
|
-
castrum_url_encode_query: { args: abi(['ptr', 'usize']), returns: 'cstring' },
|
|
464
|
-
castrum_url_resolve: {
|
|
465
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize']),
|
|
466
|
-
returns: 'cstring',
|
|
467
|
-
},
|
|
468
|
-
castrum_url_builder_resolve: {
|
|
469
|
-
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
470
|
-
returns: U64_FAST,
|
|
471
|
-
},
|
|
472
|
-
castrum_mime_from_extension: { args: ['cstring'], returns: 'cstring' },
|
|
473
|
-
castrum_jwt_verify: {
|
|
474
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64']),
|
|
475
|
-
returns: 'cstring',
|
|
476
|
-
},
|
|
477
|
-
castrum_ingress_handle_packed: {
|
|
478
|
-
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
479
|
-
returns: U64_FAST,
|
|
480
|
-
},
|
|
481
|
-
// Raw-components sibling of `castrum_ingress_handle_packed`: `url`/`ip`
|
|
482
|
-
// are `cstring` ARGs (Bun transcodes the JS strings to call-scoped
|
|
483
|
-
// NUL-terminated buffers in-engine — no JS-side Buffer.write encode);
|
|
484
|
-
// rid/headers/body are `(ptr,len)` byte slices; out is `(ptr,len)`.
|
|
485
|
-
castrum_ingress_handle_components: {
|
|
486
|
-
args: abi([
|
|
487
|
-
'usize',
|
|
488
|
-
'u8',
|
|
489
|
-
'cstring',
|
|
490
|
-
'cstring',
|
|
491
|
-
'ptr',
|
|
492
|
-
'usize',
|
|
493
|
-
'ptr',
|
|
494
|
-
'usize',
|
|
495
|
-
'ptr',
|
|
496
|
-
'usize',
|
|
497
|
-
'ptr',
|
|
498
|
-
'usize',
|
|
499
|
-
]),
|
|
500
|
-
returns: U64_FAST,
|
|
501
|
-
},
|
|
502
|
-
castrum_ingress_layout: { args: abi(['ptr', 'usize']), returns: U64_FAST },
|
|
503
|
-
// Per-route native stack (`@ignex/native` also dlopens these exact names
|
|
504
|
-
// itself; castrum binds them for the bind-time self-test + parity guard).
|
|
505
|
-
castrum_route_compile: { args: abi(['ptr', 'usize']), returns: U64_FAST },
|
|
506
|
-
castrum_route_run: {
|
|
507
|
-
args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
508
|
-
returns: U64_FAST,
|
|
509
|
-
},
|
|
510
|
-
castrum_route_destroy: { args: ['usize'], returns: 'void' },
|
|
511
|
-
// ── Batch fixed-width hex validation (ObjectId-shaped ids, etc.) ──
|
|
512
|
-
// NEWLINE-separated lines in; one verdict byte (1/0) per line out.
|
|
513
|
-
castrum_hex_validate_batch: {
|
|
514
|
-
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
515
|
-
returns: U64_FAST,
|
|
516
|
-
},
|
|
517
|
-
// ── JS-RegExp metacharacter escaping (needed-size convention) ──
|
|
518
|
-
castrum_regex_escape: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
519
|
-
// ── Metrics registry (`castrum_metrics_*`) ──────────────────────
|
|
520
|
-
// Caller-owned handle (route-stack ownership model): create returns a
|
|
521
|
-
// Box<Registry> as usize; declare fns take name/label-keys as cstring
|
|
522
|
-
// ARGS (label keys `\x1f`-separated) and return the u32 series id
|
|
523
|
-
// (0xFFFFFFFF = error); record/set cross the packed label VALUES as a
|
|
524
|
-
// `(ptr,len)` pair + the amount as f64.
|
|
525
|
-
castrum_metrics_create: { args: [], returns: U64_FAST },
|
|
526
|
-
castrum_metrics_counter: { args: ['usize', 'cstring', 'cstring'], returns: 'u32' },
|
|
527
|
-
castrum_metrics_gauge: { args: ['usize', 'cstring', 'cstring'], returns: 'u32' },
|
|
528
|
-
castrum_metrics_histogram: {
|
|
529
|
-
args: ['usize', 'cstring', 'cstring', 'cstring'],
|
|
530
|
-
returns: 'u32',
|
|
531
|
-
},
|
|
532
|
-
castrum_metrics_record: {
|
|
533
|
-
args: abi(['usize', 'u32', 'ptr', 'usize', 'f64']),
|
|
534
|
-
returns: 'u8',
|
|
535
|
-
},
|
|
536
|
-
castrum_metrics_gauge_set: {
|
|
537
|
-
args: abi(['usize', 'u32', 'ptr', 'usize', 'f64']),
|
|
538
|
-
returns: 'u8',
|
|
539
|
-
},
|
|
540
|
-
// Zero-encode siblings: the joined label values cross as ONE `cstring`
|
|
541
|
-
// ARG — the engine transcodes the JS string in-engine (no TextEncoder).
|
|
542
|
-
castrum_metrics_record_str: {
|
|
543
|
-
args: ['usize', 'u32', 'cstring', 'f64'],
|
|
544
|
-
returns: 'u8',
|
|
545
|
-
},
|
|
546
|
-
castrum_metrics_gauge_set_str: {
|
|
547
|
-
args: ['usize', 'u32', 'cstring', 'f64'],
|
|
548
|
-
returns: 'u8',
|
|
549
|
-
},
|
|
550
|
-
// Zero-copy text escape: cstring ARG in, cstring return out — the JS
|
|
551
|
-
// side does zero encode AND zero decode (engine clones at return).
|
|
552
|
-
castrum_regex_escape_str: { args: ['cstring'], returns: 'cstring' },
|
|
553
|
-
// String-input batch hex validation: `ids.join('\n')` crosses as a
|
|
554
|
-
// cstring ARG.
|
|
555
|
-
castrum_hex_validate_batch_str: {
|
|
556
|
-
args: abi(['cstring', 'u32', 'ptr', 'usize']),
|
|
557
|
-
returns: U64_FAST,
|
|
558
|
-
},
|
|
559
|
-
castrum_metrics_render: { args: abi(['usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
560
|
-
castrum_metrics_snapshot: { args: abi(['usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
561
|
-
castrum_metrics_record_batch: {
|
|
562
|
-
args: abi(['usize', 'ptr', 'usize']),
|
|
563
|
-
returns: 'u8',
|
|
564
|
-
},
|
|
565
|
-
// Fused wire-level validation: RAW query/cookie → JSON → draft-07 gate.
|
|
566
|
-
// `inner` is the SchemaValidator inner handle; strings cross as cstring
|
|
567
|
-
// ARGs (engine-transcoded).
|
|
568
|
-
castrum_query_validate: { args: ['usize', 'cstring'], returns: 'u8' },
|
|
569
|
-
castrum_cookie_validate: { args: ['usize', 'cstring'], returns: 'u8' },
|
|
570
|
-
// Fused session envelope: seal builds+signs; open verifies + extracts.
|
|
571
|
-
castrum_session_seal: {
|
|
572
|
-
args: ['cstring', 'cstring', 'i64', 'cstring'],
|
|
573
|
-
returns: 'cstring',
|
|
574
|
-
},
|
|
575
|
-
castrum_session_open: {
|
|
576
|
-
args: abi(['cstring', 'cstring', 'ptr', 'usize']),
|
|
577
|
-
returns: U64_FAST,
|
|
578
|
-
},
|
|
579
|
-
// Byte-arg siblings of the pair above: identical core and wire format,
|
|
580
|
-
// but every INPUT crosses as `(ptr,len)`. A `cstring` ARG costs the
|
|
581
|
-
// engine a transcode per arg AND silently truncates at an embedded
|
|
582
|
-
// U+0000 — reachable here because `id`/`data_json` are user-derived.
|
|
583
|
-
castrum_session_seal_bytes: {
|
|
584
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
|
|
585
|
-
returns: 'cstring',
|
|
586
|
-
},
|
|
587
|
-
castrum_session_open_bytes: {
|
|
588
|
-
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
589
|
-
returns: U64_FAST,
|
|
590
|
-
},
|
|
591
|
-
// Off-thread task runtime: `submit` takes a packed `(ptr,len)` arg blob
|
|
592
|
-
// plus a JS-assigned id; `drain` writes the packed completion batch into
|
|
593
|
-
// a caller buffer (needed-size convention). `set_doorbell` takes the raw
|
|
594
|
-
// `JSCallback({threadsafe:true}).ptr` as a bare pointer (`abi` leaves a
|
|
595
|
-
// lone `ptr` untouched), so the pool can wake JS once per batch.
|
|
596
|
-
castrum_task_init: { args: ['u32'], returns: 'u32' },
|
|
597
|
-
castrum_task_submit: { args: abi(['u32', 'ptr', 'usize', 'usize']), returns: 'u32' },
|
|
598
|
-
castrum_task_submit_slice: {
|
|
599
|
-
args: abi(['u32', 'ptr', 'usize', 'ptr', 'usize', 'usize']),
|
|
600
|
-
returns: 'u32',
|
|
601
|
-
},
|
|
602
|
-
castrum_task_submit_out: {
|
|
603
|
-
args: abi(['u32', 'ptr', 'usize', 'usize', 'ptr', 'usize']),
|
|
604
|
-
returns: 'u32',
|
|
605
|
-
},
|
|
606
|
-
castrum_task_drain: { args: abi(['ptr', 'usize']), returns: U64_FAST },
|
|
607
|
-
castrum_task_pending: { args: [], returns: 'u32' },
|
|
608
|
-
castrum_task_cancel: { args: ['usize'], returns: 'u32' },
|
|
609
|
-
castrum_task_set_doorbell: { args: ['ptr'], returns: 'u32' },
|
|
610
|
-
castrum_task_shutdown: { args: [], returns: 'u32' },
|
|
611
|
-
castrum_task_threads: { args: [], returns: 'u32' },
|
|
612
|
-
castrum_metrics_destroy: { args: ['usize'], returns: 'void' },
|
|
613
|
-
})
|
|
181
|
+
const { symbols } = dlopen(path, buildFFISymbolMap(useBufferLength))
|
|
614
182
|
|
|
615
183
|
// (`as unknown as`: the zero-arg `castrum_metrics_create` infers
|
|
616
184
|
// `(...args: never[])`, which can't overlap `(...a: unknown[])` directly.)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/native/ingress-binding.ts — shared self-tested ingress transport for framework adapters.
|
|
2
|
+
|
|
3
|
+
import { type BunFFI, ffiAddonPath, getBunFFI } from './ffi'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Synchronous ingress writers backed by castrum's process-lifetime FFI binding.
|
|
7
|
+
* Handles MUST originate from addonPath and remain owned/alive while in use.
|
|
8
|
+
* Writers throw on native failure; a return larger than output.byteLength is
|
|
9
|
+
* the exact required capacity, not a completed write. No buffer is retained.
|
|
10
|
+
*/
|
|
11
|
+
export type IngressBinding = Pick<
|
|
12
|
+
BunFFI,
|
|
13
|
+
'ingressHandleComponents' | 'ingressHandlePacked' | 'ingressLayout'
|
|
14
|
+
> & { readonly addonPath: string }
|
|
15
|
+
|
|
16
|
+
let cached: IngressBinding | null | undefined
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Share castrum's self-tested ingress writers without a second dlopen/map.
|
|
20
|
+
* @returns The binding and actual binary path, or null when FFI is unavailable.
|
|
21
|
+
* @example
|
|
22
|
+
* const binding = getIngressBinding()
|
|
23
|
+
* // Verify binding.addonPath matches the addon that owns your ingress handle.
|
|
24
|
+
*/
|
|
25
|
+
export function getIngressBinding(): IngressBinding | null {
|
|
26
|
+
if (cached !== undefined) return cached
|
|
27
|
+
const ffi = getBunFFI()
|
|
28
|
+
const addonPath = ffiAddonPath()
|
|
29
|
+
cached =
|
|
30
|
+
ffi && addonPath
|
|
31
|
+
? {
|
|
32
|
+
addonPath,
|
|
33
|
+
ingressHandleComponents: ffi.ingressHandleComponents,
|
|
34
|
+
ingressHandlePacked: ffi.ingressHandlePacked,
|
|
35
|
+
ingressLayout: ffi.ingressLayout,
|
|
36
|
+
}
|
|
37
|
+
: null
|
|
38
|
+
return cached
|
|
39
|
+
}
|
package/src/native/types.ts
CHANGED
|
@@ -344,6 +344,12 @@ export interface NativeAddon {
|
|
|
344
344
|
|
|
345
345
|
initThreadPool(rayonThreads?: number): void
|
|
346
346
|
rayonNumThreads(): number
|
|
347
|
+
/**
|
|
348
|
+
* Drop the process-wide compiled request-body schema cache (identical schema
|
|
349
|
+
* bytes are compiled once and shared across ingress instances/routes).
|
|
350
|
+
* Maintenance hook for the public `flushMemory()`; idempotent.
|
|
351
|
+
*/
|
|
352
|
+
clearSchemaCache(): void
|
|
347
353
|
|
|
348
354
|
jsonValidBatchPacked(input: Uint8Array): Uint8Array
|
|
349
355
|
validateEmailBatchPacked(input: Uint8Array): Uint8Array
|
package/src/runtime/codec.ts
CHANGED
|
@@ -79,6 +79,7 @@ function createBunCodec(): Utf8Codec {
|
|
|
79
79
|
// ffi surface). This fallback matches the historical behavior.
|
|
80
80
|
decodeUtf8Fatal: decodeUtf8Bun,
|
|
81
81
|
decodeUtf8Range: decodeUtf8RangeBun,
|
|
82
|
+
decodeUtf8RangeView: decodeUtf8RangeViewBun,
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -154,6 +155,21 @@ function decodeUtf8RangeBun(bytes: Uint8Array, start: number, end: number): stri
|
|
|
154
155
|
return view.toString('latin1', base + start, base + end)
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
/**
|
|
159
|
+
* View-based ranged decode (Bun): same ASCII-scan + latin1/utf8 split as
|
|
160
|
+
* `decodeUtf8RangeBun`, but the caller supplies the bounded Buffer view so the
|
|
161
|
+
* WeakMap lookup + `byteOffset` arithmetic happen once per packed RESULT, not
|
|
162
|
+
* once per field. Offsets are relative to the view (0 = view start).
|
|
163
|
+
*/
|
|
164
|
+
function decodeUtf8RangeViewBun(view: Buffer, start: number, end: number): string {
|
|
165
|
+
for (let i = start; i < end; i++) {
|
|
166
|
+
if ((view[i] ?? 0) >= 0x80) {
|
|
167
|
+
return view.toString('utf8', start, end)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return view.toString('latin1', start, end)
|
|
171
|
+
}
|
|
172
|
+
|
|
157
173
|
// ── Node codec ────────────────────────────────────────────────────
|
|
158
174
|
function createNodeCodec(): Utf8Codec {
|
|
159
175
|
return {
|
|
@@ -179,6 +195,14 @@ function createNodeCodec(): Utf8Codec {
|
|
|
179
195
|
}
|
|
180
196
|
return view.toString('latin1', start, end)
|
|
181
197
|
},
|
|
198
|
+
decodeUtf8RangeView(view: Buffer, start: number, end: number): string {
|
|
199
|
+
for (let i = start; i < end; i++) {
|
|
200
|
+
if ((view[i] ?? 0) >= 0x80) {
|
|
201
|
+
return view.toString('utf8', start, end)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return view.toString('latin1', start, end)
|
|
205
|
+
},
|
|
182
206
|
}
|
|
183
207
|
}
|
|
184
208
|
|
package/src/runtime/detect.ts
CHANGED
|
@@ -27,6 +27,20 @@ export function isNodeRuntime(): boolean {
|
|
|
27
27
|
return detectedRuntime === 'node'
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Request a synchronous full garbage collection when the host runtime exposes
|
|
32
|
+
* one. On Bun this is `Bun.gc(true)`; on Node it is `globalThis.gc?.()`, which
|
|
33
|
+
* is only defined when the process runs with `--expose-gc`. A no-op otherwise.
|
|
34
|
+
*/
|
|
35
|
+
export function forceGc(): void {
|
|
36
|
+
if (detectedRuntime === 'bun') {
|
|
37
|
+
Bun.gc(true)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
// Node only defines `globalThis.gc` under `--expose-gc`.
|
|
41
|
+
;(globalThis as { gc?: () => void }).gc?.()
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
/** The Node.js major version when running under Node, otherwise `null`. */
|
|
31
45
|
export function nodeMajorVersion(): number | null {
|
|
32
46
|
if (detectedRuntime !== 'node') return null
|
package/src/runtime/types.ts
CHANGED
|
@@ -38,6 +38,17 @@ export interface Utf8Codec {
|
|
|
38
38
|
* a latin1 fast path (~2x on Bun); multi-byte ranges fall back to UTF-8.
|
|
39
39
|
*/
|
|
40
40
|
decodeUtf8Range(bytes: Uint8Array, start: number, end: number): string
|
|
41
|
+
/**
|
|
42
|
+
* Decode the byte RANGE `[start, end)` of a PREPARED bounded `Buffer` view
|
|
43
|
+
* using ABSOLUTE offsets into the view (0 = the view's first byte).
|
|
44
|
+
* `unpackMediaType` resolves the WeakMap/Buffer view ONCE per packed result
|
|
45
|
+
* and calls this per field — removing the per-field cache lookup + view
|
|
46
|
+
* arithmetic. Identical semantics to `decodeUtf8Range` (replacement mode,
|
|
47
|
+
* ASCII latin1 fast path); offsets are relative to the view, not the view's
|
|
48
|
+
* backing buffer. Optional: runtimes/tests may leave it undefined — callers
|
|
49
|
+
* fall back to `decodeUtf8Range` on the original bytes.
|
|
50
|
+
*/
|
|
51
|
+
decodeUtf8RangeView?: (view: Buffer, start: number, end: number) => string
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
/** UUIDv7 generation (Bun built-in vs crypto.randomUUID on Node). */
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
withPackScratch2,
|
|
23
23
|
} from '../../shared/packed'
|
|
24
24
|
import { type RustClientContext, resolvePoolNative } from '../context'
|
|
25
|
+
import { assertNonEmptySecret } from '../options'
|
|
25
26
|
import type { RustBatch } from './types'
|
|
26
27
|
|
|
27
28
|
// Native batch fns are process-wide singletons (one lazy addon). `resolvePoolNative`
|
|
@@ -129,11 +130,18 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
129
130
|
via(ctx, 'brotliCompressBatchPacked', unpackByteResults, [quality ?? null])(items),
|
|
130
131
|
brotliDecompress: (items, maxDecompressed) =>
|
|
131
132
|
via(ctx, 'brotliDecompressBatchPacked', unpackByteResults, [maxDecompressed ?? null])(items),
|
|
132
|
-
jwtVerify: (tokens, secret, nowSeconds) =>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
jwtVerify: (tokens, secret, nowSeconds) => {
|
|
134
|
+
assertNonEmptySecret(secret, 'batch.jwtVerify')
|
|
135
|
+
return via(ctx, 'jwtVerifyBatchPacked', unpackBitset, [secret, nowSeconds])(tokens)
|
|
136
|
+
},
|
|
137
|
+
hmacSha256: (items, key) => {
|
|
138
|
+
assertNonEmptySecret(key, 'batch.hmacSha256')
|
|
139
|
+
return via(ctx, 'hmacSha256BatchPacked', unpackByteResults, [key])(items)
|
|
140
|
+
},
|
|
141
|
+
hmacSha256Verify: (items, sigs, key) => {
|
|
142
|
+
assertNonEmptySecret(key, 'batch.hmacSha256Verify')
|
|
143
|
+
return via2(ctx, 'hmacSha256VerifyBatchPacked', unpackBitset, [key])(items, sigs)
|
|
144
|
+
},
|
|
137
145
|
// ── Two-list ──
|
|
138
146
|
passwordVerify: (items, phcs) =>
|
|
139
147
|
via2(ctx, 'passwordVerifyBatchPacked', unpackBitset)(items, phcs),
|
|
@@ -141,12 +149,14 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
141
149
|
via2(ctx, 'urlResolveBatchPacked', unpackByteResults)(bases, references),
|
|
142
150
|
templateRender: (source, contexts) =>
|
|
143
151
|
via(ctx, 'templateRenderBatchPacked', unpackByteResults, [source])(contexts),
|
|
144
|
-
jwtSign: (items, secret, ttlSeconds, nowSeconds) =>
|
|
145
|
-
|
|
152
|
+
jwtSign: (items, secret, ttlSeconds, nowSeconds) => {
|
|
153
|
+
assertNonEmptySecret(secret, 'batch.jwtSign')
|
|
154
|
+
return via(ctx, 'jwtSignBatchPacked', unpackByteResults, [
|
|
146
155
|
secret,
|
|
147
156
|
ttlSeconds ?? null,
|
|
148
157
|
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
149
|
-
])(items)
|
|
158
|
+
])(items)
|
|
159
|
+
},
|
|
150
160
|
sseEncode: (items, event, id, retry) =>
|
|
151
161
|
via(ctx, 'sseEncodeBatchPacked', unpackByteResults, [
|
|
152
162
|
event ?? null,
|