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/rust-ffi/packed.ts
CHANGED
|
@@ -3,22 +3,32 @@
|
|
|
3
3
|
// `rust.packed.*` is the zero-copy API: callers pass already-packed buffers and
|
|
4
4
|
// receive the raw packed output, plus the batch metadata/count helpers.
|
|
5
5
|
|
|
6
|
-
import { asNumber } from './options'
|
|
7
|
-
import { resolvePoolNative, type RustClientContext } from './context'
|
|
8
6
|
import type { PasswordHashOptions } from '../native'
|
|
7
|
+
import { type RustClientContext, resolvePoolNative } from './context'
|
|
8
|
+
import { asNumber } from './options'
|
|
9
9
|
|
|
10
10
|
/** Raw packed-wire FFI namespace. */
|
|
11
11
|
export interface RustPacked {
|
|
12
12
|
// ── Sync batch (packed in → packed out) ──
|
|
13
|
+
/** Packed CRC32 batch: packed `[u8[]]` items → packed u32 results. */
|
|
13
14
|
crc32BatchPacked(input: Uint8Array): Uint8Array
|
|
15
|
+
/** Packed JSON-validity batch: packed `[u8[]]` items → packed 0/1 bitset. */
|
|
14
16
|
jsonValidBatchPacked(input: Uint8Array): Uint8Array
|
|
17
|
+
/** Packed email-validator batch: packed `[u8[]]` items → packed 0/1 bitset. */
|
|
15
18
|
validateEmailBatchPacked(input: Uint8Array): Uint8Array
|
|
19
|
+
/** Packed UUID-validator batch: packed `[u8[]]` items → packed 0/1 bitset. */
|
|
16
20
|
validateUuidBatchPacked(input: Uint8Array): Uint8Array
|
|
21
|
+
/** Packed IPv4-validator batch: packed `[u8[]]` items → packed 0/1 bitset. */
|
|
17
22
|
validateIpv4BatchPacked(input: Uint8Array): Uint8Array
|
|
23
|
+
/** Packed IPv6-validator batch: packed `[u8[]]` items → packed 0/1 bitset. */
|
|
18
24
|
validateIpv6BatchPacked(input: Uint8Array): Uint8Array
|
|
25
|
+
/** Packed JSON-sum batch: packed `[{id}]` docs → packed `[ok u8][sum i64 LE]`. */
|
|
19
26
|
jsonSumBatchPacked(input: Uint8Array): Uint8Array
|
|
27
|
+
/** Packed query-string parser batch: packed `[u8[]]` queries → packed pair lists. */
|
|
20
28
|
queryParseBatchPacked(input: Uint8Array): Uint8Array
|
|
29
|
+
/** Packed cookie-header parser batch: packed `[u8[]]` headers → packed pair lists. */
|
|
21
30
|
cookieParseBatchPacked(input: Uint8Array): Uint8Array
|
|
31
|
+
/** Packed HTTP-request parser batch: packed `[u8[]]` requests → packed request sections. */
|
|
22
32
|
httpParseRequestBatchPacked(input: Uint8Array): Uint8Array
|
|
23
33
|
/** Batch RFC 6902 JSON Patch (two packed lists, zipped) → packed results. */
|
|
24
34
|
jsonPatchBatchPacked(docs: Uint8Array, patches: Uint8Array): Uint8Array
|
|
@@ -26,24 +36,39 @@ export interface RustPacked {
|
|
|
26
36
|
fnv1A64BatchPacked(input: Uint8Array): Uint8Array
|
|
27
37
|
/** Packed ETag batch (10 strong / 12 weak bytes per item). */
|
|
28
38
|
etagBatchPacked(input: Uint8Array, weak?: boolean | null): Uint8Array
|
|
39
|
+
/** Packed percent-encoding batch: packed `[u8[]]` items → packed hex-percent bytes. */
|
|
29
40
|
urlEncodeBatchPacked(input: Uint8Array): Uint8Array
|
|
41
|
+
/** Packed percent-decoding batch: packed `[u8[]]` items → packed decoded bytes (string-safe). */
|
|
30
42
|
urlDecodeBatchPacked(input: Uint8Array): Uint8Array
|
|
43
|
+
/** Packed percent-decoding batch returning raw bytes (`%FF` preserved). */
|
|
31
44
|
urlDecodeBytesBatchPacked(input: Uint8Array): Uint8Array
|
|
45
|
+
/** Packed MIME-lookup batch: packed `[u8[]]` extensions → packed media-type strings. */
|
|
32
46
|
mimeFromExtensionBatchPacked(input: Uint8Array): Uint8Array
|
|
47
|
+
/** Packed WebSocket accept-key batch: packed `[u8[]]` keys → packed base64 SHA-1 results. */
|
|
33
48
|
wsAcceptKeyBatchPacked(input: Uint8Array): Uint8Array
|
|
34
49
|
|
|
35
50
|
// ── Batch metadata / counts ──
|
|
51
|
+
/** Valid JSON count in a packed JSON batch (0..n; invalid items not counted). */
|
|
36
52
|
jsonValidBatchCountPacked(input: Uint8Array): number
|
|
53
|
+
/** Valid-email count in a packed email batch. */
|
|
37
54
|
validateEmailBatchCountPacked(input: Uint8Array): number
|
|
55
|
+
/** Valid-UUID count in a packed UUID batch. */
|
|
38
56
|
validateUuidBatchCountPacked(input: Uint8Array): number
|
|
57
|
+
/** Valid-IPv4 count in a packed IPv4 batch. */
|
|
39
58
|
validateIpv4BatchCountPacked(input: Uint8Array): number
|
|
59
|
+
/** Valid-IPv6 count in a packed IPv6 batch. */
|
|
40
60
|
validateIpv6BatchCountPacked(input: Uint8Array): number
|
|
61
|
+
/** Total `id` sum across a packed JSON batch (valid items only). */
|
|
41
62
|
jsonSumBatchTotalPacked(input: Uint8Array): number
|
|
63
|
+
/** Total packed-output length (bytes) a query batch will produce. */
|
|
42
64
|
queryParseBatchTotalLenPacked(input: Uint8Array): number
|
|
65
|
+
/** Total packed-output length (bytes) a cookie batch will produce. */
|
|
43
66
|
cookieParseBatchTotalLenPacked(input: Uint8Array): number
|
|
67
|
+
/** Total packed-output length (bytes) an HTTP-request batch will produce. */
|
|
44
68
|
httpParseRequestBatchTotalLenPacked(input: Uint8Array): number
|
|
45
69
|
|
|
46
70
|
// ── Backend-framework features ──
|
|
71
|
+
/** Packed Argon2id-hash batch: packed `[u8[]]` passwords + shared salt → packed PHC strings. */
|
|
47
72
|
passwordHashBatchPacked(
|
|
48
73
|
input: Uint8Array,
|
|
49
74
|
salt: Uint8Array,
|
|
@@ -53,47 +78,60 @@ export interface RustPacked {
|
|
|
53
78
|
passwordVerifyBatchPacked(passwords: Uint8Array, phcs: Uint8Array): Uint8Array
|
|
54
79
|
/** Packed URL-resolve batch (two packed lists, zipped) → packed results. */
|
|
55
80
|
urlResolveBatchPacked(bases: Uint8Array, references: Uint8Array): Uint8Array
|
|
81
|
+
/** Packed AEAD-encrypt batch (packed inputs + shared key/nonce) → packed ciphertexts. */
|
|
56
82
|
aeadEncryptBatchPacked(
|
|
57
83
|
input: Uint8Array,
|
|
58
84
|
key: Uint8Array,
|
|
59
85
|
nonce: Uint8Array,
|
|
60
86
|
algorithm?: string | null,
|
|
61
87
|
): Uint8Array
|
|
88
|
+
/** Packed AEAD-decrypt batch (packed inputs + shared key/nonce) → packed plaintexts. */
|
|
62
89
|
aeadDecryptBatchPacked(
|
|
63
90
|
input: Uint8Array,
|
|
64
91
|
key: Uint8Array,
|
|
65
92
|
nonce: Uint8Array,
|
|
66
93
|
algorithm?: string | null,
|
|
67
94
|
): Uint8Array
|
|
95
|
+
/** Packed gzip-compress batch: packed `[u8[]]` items → packed gzip streams. */
|
|
68
96
|
gzipCompressBatchPacked(input: Uint8Array, level?: number | null): Uint8Array
|
|
97
|
+
/** Packed gzip-decompress batch (64 MiB cap per item) → packed plaintexts. */
|
|
69
98
|
gzipDecompressBatchPacked(input: Uint8Array): Uint8Array
|
|
99
|
+
/** Packed brotli-compress batch: packed `[u8[]]` items → packed brotli streams. */
|
|
70
100
|
brotliCompressBatchPacked(input: Uint8Array, quality?: number | null): Uint8Array
|
|
101
|
+
/** Packed brotli-decompress batch (64 MiB cap per item) → packed plaintexts. */
|
|
71
102
|
brotliDecompressBatchPacked(input: Uint8Array): Uint8Array
|
|
103
|
+
/** Packed HS256 JWT-sign batch (packed claims + shared secret) → packed tokens. */
|
|
72
104
|
jwtSignBatchPacked(
|
|
73
105
|
input: Uint8Array,
|
|
74
106
|
secret: Uint8Array,
|
|
75
107
|
ttlSeconds: number | null,
|
|
76
108
|
nowSeconds: number,
|
|
77
109
|
): Uint8Array
|
|
110
|
+
/** Packed HS256 JWT-verify batch (packed tokens + shared secret) → packed claims/validity. */
|
|
78
111
|
jwtVerifyBatchPacked(input: Uint8Array, secret: Uint8Array, nowSeconds: number): Uint8Array
|
|
79
112
|
/** Batch HMAC-SHA256 sign (packed in → hex-signature byte results). */
|
|
80
113
|
hmacSha256BatchPacked(input: Uint8Array, key: Uint8Array): Uint8Array
|
|
81
114
|
/** Batch HMAC-SHA256 verify (two packed lists: data + hex sigs, zipped) → bitset. */
|
|
82
115
|
hmacSha256VerifyBatchPacked(input: Uint8Array, sigs: Uint8Array, key: Uint8Array): Uint8Array
|
|
116
|
+
/** Packed multipart/form-data parser batch (packed bodies + shared boundary) → packed parts. */
|
|
83
117
|
multipartParseBatchPacked(input: Uint8Array, boundary: Uint8Array): Uint8Array
|
|
118
|
+
/** Packed minijinja template batch: packed contexts + shared source → packed renders. */
|
|
84
119
|
templateRenderBatchPacked(input: Uint8Array, source: string): Uint8Array
|
|
120
|
+
/** Packed SSE-frame batch: packed data + shared event/id/retry → packed frames. */
|
|
85
121
|
sseEncodeBatchPacked(
|
|
86
122
|
input: Uint8Array,
|
|
87
123
|
event: string | null,
|
|
88
124
|
id: string | null,
|
|
89
125
|
retry: number | null,
|
|
90
126
|
): Uint8Array
|
|
127
|
+
/** Packed WebSocket-frame encode batch: packed payloads + shared opcode/mask/fin → packed frames. */
|
|
91
128
|
wsFrameEncodeBatchPacked(
|
|
92
129
|
input: Uint8Array,
|
|
93
130
|
opcode: number,
|
|
94
131
|
mask: boolean,
|
|
95
132
|
fin: boolean,
|
|
96
133
|
): Uint8Array
|
|
134
|
+
/** Packed WebSocket-frame decode batch: packed frames → packed (fin, opcode, payload) results. */
|
|
97
135
|
wsFrameDecodeBatchPacked(input: Uint8Array): Uint8Array
|
|
98
136
|
}
|
|
99
137
|
|
package/src/rust-ffi/proven.ts
CHANGED
|
@@ -1,42 +1,28 @@
|
|
|
1
|
-
// src/rust-ffi/proven.ts —
|
|
1
|
+
// src/rust-ffi/proven.ts — the baked "proven selection" surface.
|
|
2
2
|
//
|
|
3
|
-
// `proven` exposes the FULL `rust.*` surface
|
|
4
|
-
//
|
|
5
|
-
// the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// The registry (`src/shared/proven.ts`) is the single source of truth for the
|
|
11
|
-
// annotations; `bun run check:proven` audits it against the latest benchmark
|
|
12
|
-
// report (with `--fail` to gate CI).
|
|
3
|
+
// `proven` re-exposes the FULL `rust.*` surface (identical object) alongside
|
|
4
|
+
// the pure-data registry (`src/shared/proven.ts`) that states which
|
|
5
|
+
// implementation is the benchmark-proven winner for every op: 'native',
|
|
6
|
+
// 'js', or 'bun' (Bun built-in delegation). The winners are BAKED (committed,
|
|
7
|
+
// benchmark-derived) and verified by `test/unit/shared/proven.test.ts` — no
|
|
8
|
+
// live benchmark audit and no `@deprecated` JSDoc machinery (the old
|
|
9
|
+
// `check:proven` / `check:annotate` friction, intentionally not re-added).
|
|
13
10
|
|
|
14
11
|
import { rust, type RustClient } from './client'
|
|
15
12
|
|
|
16
|
-
/**
|
|
17
|
-
* The performance-annotated client: the full {@link RustClient} surface.
|
|
18
|
-
* Each exported function's JSDoc notes its measured performance vs the JS
|
|
19
|
-
* baseline; functions slower than the baseline carry `@deprecated`.
|
|
20
|
-
*/
|
|
13
|
+
/** The full `rust.*` surface — identical to `rust`; the proven wiring is baked (see {@link ../shared/proven}). */
|
|
21
14
|
export type ProvenClient = RustClient
|
|
22
15
|
|
|
23
|
-
/**
|
|
24
|
-
* The full `rust.*` surface with benchmark-annotated JSDoc. Identical to
|
|
25
|
-
* `rust` — the annotations and `@deprecated` markers are what communicate
|
|
26
|
-
* performance now, not a curated subset.
|
|
27
|
-
*/
|
|
16
|
+
/** The full `rust.*` surface, exposed for audit. Same object as `rust`. */
|
|
28
17
|
export const proven: ProvenClient = rust
|
|
29
18
|
|
|
30
|
-
// Re-export the registry + helpers so consumers can audit the surface.
|
|
31
19
|
export {
|
|
32
|
-
|
|
33
|
-
provenStatus,
|
|
20
|
+
PROVEN_SELECTION,
|
|
34
21
|
isProven,
|
|
35
|
-
|
|
22
|
+
provenEntry,
|
|
23
|
+
provenImpl,
|
|
24
|
+
provenStatus,
|
|
36
25
|
provenSummary,
|
|
26
|
+
provenSurface,
|
|
37
27
|
} from '../shared/proven'
|
|
38
|
-
export type {
|
|
39
|
-
PerformanceStatus,
|
|
40
|
-
ProvenEntry,
|
|
41
|
-
ProvenKey,
|
|
42
|
-
} from '../shared/proven'
|
|
28
|
+
export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
|
|
@@ -2,48 +2,71 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Mirrors rust/crypto/*: base64/hex codecs, signed cookies, CSRF, random
|
|
4
4
|
// tokens, JWT, password hashing and AEAD.
|
|
5
|
+
//
|
|
6
|
+
// Runtime dispatch is centralized in the adapter (`ctx.runtime`): Bun built-in
|
|
7
|
+
// delegations (randomToken/base64*/hexEncode) come from `builtins.has(op)` and
|
|
8
|
+
// the native call from `transport.resolve(op)` / `transport.ffi` (bun:ffi
|
|
9
|
+
// first, napi fallback) — no inline `isBun()` / `getBunFFI()`.
|
|
5
10
|
|
|
6
11
|
import type { PasswordHashOptions } from '../../native'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import type
|
|
10
|
-
|
|
11
|
-
const encoder = new TextEncoder()
|
|
12
|
-
const decoder = new TextDecoder()
|
|
12
|
+
import { encoder } from '../../shared/bytes'
|
|
13
|
+
import { writeInto } from '../into'
|
|
14
|
+
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
13
15
|
|
|
14
16
|
/** Auth / crypto scalar methods (`Pick<RustScalar, ...>`). */
|
|
15
17
|
export function buildCrypto(ctx: RustClientContext) {
|
|
16
18
|
const { addon } = ctx
|
|
19
|
+
const { builtins, transport } = ctx.runtime
|
|
20
|
+
// Hoist the immutable per-runtime builtin-delegation decisions into a
|
|
21
|
+
// bind-time map (BUILTIN_OPS is a module constant — never mutated after
|
|
22
|
+
// load) so the hot path reads object properties instead of `Set.has`.
|
|
23
|
+
const HAS = {
|
|
24
|
+
randomToken: builtins.has('randomToken'),
|
|
25
|
+
base64Encode: builtins.has('base64Encode'),
|
|
26
|
+
base64UrlEncode: builtins.has('base64UrlEncode'),
|
|
27
|
+
hexEncode: builtins.has('hexEncode'),
|
|
28
|
+
}
|
|
29
|
+
// Lazy-memoized ffi surface: binds on first call, single local read after.
|
|
30
|
+
const ffi = memoizeFfi(transport)
|
|
17
31
|
|
|
18
32
|
return {
|
|
19
|
-
randomToken(byteLen: number): Uint8Array {
|
|
33
|
+
randomToken(byteLen: number): Uint8Array | string {
|
|
20
34
|
// Optimal by default under Bun: `crypto.getRandomValues` + native hex
|
|
21
|
-
// beats the rust+FFI crossing for token-sized draws (decision matrix)
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
// beats the rust+FFI crossing for token-sized draws (decision matrix);
|
|
36
|
+
// the builtin carries the 16 MiB guard and returns the hex STRING. Node
|
|
37
|
+
// keeps the addon.
|
|
38
|
+
if (HAS.randomToken) return builtins.randomToken(byteLen)
|
|
39
|
+
return resolveNative(ctx, 'randomToken')(byteLen) as Uint8Array | string
|
|
40
|
+
},
|
|
41
|
+
randomTokenInto(byteLen: number, output: Uint8Array): number {
|
|
42
|
+
// Pooled sibling — writes `byteLen*2` hex chars into `output`. FFI path
|
|
43
|
+
// uses the native `_into` (no cstring round-trip); without ffi the Bun
|
|
44
|
+
// builtin (or addon) hex is written into the caller buffer.
|
|
45
|
+
const f = ffi()
|
|
46
|
+
if (f) return f.randomTokenInto(byteLen, output)
|
|
47
|
+
if (HAS.randomToken) {
|
|
48
|
+
const need = byteLen * 2
|
|
49
|
+
if (output.length < need) throw new Error('random token: output buffer too small')
|
|
50
|
+
output.set(encoder.encode(builtins.randomToken(byteLen) as string))
|
|
51
|
+
return need
|
|
31
52
|
}
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
return addon.randomToken(byteLen)
|
|
53
|
+
const bytes = addon.randomToken(byteLen)
|
|
54
|
+
return writeInto('random token', output, bytes)
|
|
35
55
|
},
|
|
36
|
-
base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array {
|
|
56
|
+
base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array | string {
|
|
37
57
|
// Bun's Buffer base64 (SIMD) beats the rust+FFI crossing (~2x, measured)
|
|
38
|
-
// for the standard (url-safe=false, padded) case;
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
if (
|
|
42
|
-
|
|
58
|
+
// for the standard (url-safe=false, padded) case; the builtin returns
|
|
59
|
+
// `undefined` for url-safe/unpadded so those fall through to native.
|
|
60
|
+
// Parity pinned by test/unit/features/encoding.test.ts.
|
|
61
|
+
if (HAS.base64Encode) {
|
|
62
|
+
const encoded = builtins.base64Encode(input, urlSafe, padding)
|
|
63
|
+
if (encoded !== undefined) return encoded
|
|
43
64
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
return resolveNative(ctx, 'base64Encode')(
|
|
66
|
+
input,
|
|
67
|
+
urlSafe ?? undefined,
|
|
68
|
+
padding ?? undefined,
|
|
69
|
+
) as Uint8Array | string
|
|
47
70
|
},
|
|
48
71
|
base64EncodeInto(
|
|
49
72
|
input: Uint8Array,
|
|
@@ -51,14 +74,19 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
51
74
|
urlSafe?: boolean,
|
|
52
75
|
padding?: boolean,
|
|
53
76
|
): number {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
return resolveNative(ctx, 'base64EncodeInto')(
|
|
78
|
+
input,
|
|
79
|
+
output,
|
|
80
|
+
urlSafe ?? undefined,
|
|
81
|
+
padding ?? undefined,
|
|
82
|
+
) as number
|
|
57
83
|
},
|
|
58
84
|
base64Decode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
return resolveNative(ctx, 'base64Decode')(
|
|
86
|
+
input,
|
|
87
|
+
urlSafe ?? undefined,
|
|
88
|
+
padding ?? undefined,
|
|
89
|
+
) as Uint8Array
|
|
62
90
|
},
|
|
63
91
|
base64DecodeInto(
|
|
64
92
|
input: Uint8Array,
|
|
@@ -66,69 +94,73 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
66
94
|
urlSafe?: boolean,
|
|
67
95
|
padding?: boolean,
|
|
68
96
|
): number {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
return resolveNative(ctx, 'base64DecodeInto')(
|
|
98
|
+
input,
|
|
99
|
+
output,
|
|
100
|
+
urlSafe ?? undefined,
|
|
101
|
+
padding ?? undefined,
|
|
102
|
+
) as number
|
|
72
103
|
},
|
|
73
|
-
base64UrlEncode(input: Uint8Array): Uint8Array {
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
base64UrlEncode(input: Uint8Array): Uint8Array | string {
|
|
105
|
+
// Bun's native `Buffer.toString('base64url')` (SIMD) beats the rust+FFI
|
|
106
|
+
// crossing — same delegation rationale as `base64Encode`; verified
|
|
107
|
+
// byte-parity with the Rust `URL_SAFE_NO_PAD` engine
|
|
108
|
+
// (test/unit/features/encoding.test.ts). The ffi path reuses
|
|
109
|
+
// `base64Encode(input, true, false)` (no dedicated base64UrlEncode symbol).
|
|
110
|
+
if (HAS.base64UrlEncode) return builtins.base64UrlEncode(input)
|
|
111
|
+
const f = ffi()
|
|
112
|
+
if (f) return f.base64Encode(input, true, false)
|
|
76
113
|
return addon.base64UrlEncode(input)
|
|
77
114
|
},
|
|
78
115
|
base64UrlDecode(input: Uint8Array): Uint8Array {
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
116
|
+
const f = ffi()
|
|
117
|
+
if (f) return f.base64Decode(input, true, false)
|
|
81
118
|
return addon.base64UrlDecode(input)
|
|
82
119
|
},
|
|
83
|
-
hexEncode(input: Uint8Array): Uint8Array {
|
|
84
|
-
// Bun
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
return
|
|
120
|
+
hexEncode(input: Uint8Array): Uint8Array | string {
|
|
121
|
+
// Bun's native `Buffer.toString('hex')` (SIMD) beats the rust+FFI
|
|
122
|
+
// crossing, especially for large inputs. Verified byte-parity with the
|
|
123
|
+
// Rust hex encoder. Bun returns the hex STRING; Node keeps the addon bytes.
|
|
124
|
+
if (HAS.hexEncode) return builtins.hexEncode(input)
|
|
125
|
+
return resolveNative(ctx, 'hexEncode')(input) as Uint8Array | string
|
|
89
126
|
},
|
|
90
127
|
hexEncodeInto(input: Uint8Array, output: Uint8Array): number {
|
|
91
|
-
|
|
92
|
-
if (ffi) return ffi.hexEncodeInto(input, output)
|
|
93
|
-
return addon.hexEncodeInto(input, output)
|
|
128
|
+
return resolveNative(ctx, 'hexEncodeInto')(input, output) as number
|
|
94
129
|
},
|
|
95
130
|
hexDecode(input: Uint8Array): Uint8Array {
|
|
96
|
-
|
|
97
|
-
if (ffi) return ffi.hexDecode(input)
|
|
98
|
-
return addon.hexDecode(input)
|
|
131
|
+
return resolveNative(ctx, 'hexDecode')(input) as Uint8Array
|
|
99
132
|
},
|
|
100
133
|
hexDecodeInto(input: Uint8Array, output: Uint8Array): number {
|
|
101
|
-
|
|
102
|
-
if (ffi) return ffi.hexDecodeInto(input, output)
|
|
103
|
-
return addon.hexDecodeInto(input, output)
|
|
134
|
+
return resolveNative(ctx, 'hexDecodeInto')(input, output) as number
|
|
104
135
|
},
|
|
105
|
-
signCookie(value: Uint8Array, secret: Uint8Array): Uint8Array {
|
|
106
|
-
|
|
107
|
-
if (ffi) return ffi.signCookie(value, secret)
|
|
108
|
-
return addon.signCookie(value, secret)
|
|
136
|
+
signCookie(value: Uint8Array, secret: Uint8Array): Uint8Array | string {
|
|
137
|
+
return resolveNative(ctx, 'signCookie')(value, secret) as Uint8Array | string
|
|
109
138
|
},
|
|
110
139
|
signCookieInto(value: Uint8Array, secret: Uint8Array, output: Uint8Array): number {
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
140
|
+
const f = ffi()
|
|
141
|
+
if (f) return f.signCookieInto(value, secret, output)
|
|
142
|
+
return writeInto('sign cookie', output, addon.signCookie(value, secret))
|
|
143
|
+
},
|
|
144
|
+
verifyCookie(signed: Uint8Array, secret: Uint8Array): Uint8Array | string | null {
|
|
145
|
+
return resolveNative(ctx, 'verifyCookie')(signed, secret) as Uint8Array | string | null
|
|
146
|
+
},
|
|
147
|
+
verifyCookieInto(signed: Uint8Array, secret: Uint8Array, output: Uint8Array): number | null {
|
|
148
|
+
const f = ffi()
|
|
149
|
+
if (f) return f.verifyCookieInto(signed, secret, output)
|
|
150
|
+
const value = addon.verifyCookie(signed, secret)
|
|
151
|
+
if (value === null) return null
|
|
152
|
+
return writeInto('verify cookie', output, value)
|
|
117
153
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (ffi) return ffi.verifyCookie(signed, secret)
|
|
121
|
-
return addon.verifyCookie(signed, secret)
|
|
154
|
+
csrfToken(secret: Uint8Array): Uint8Array | string {
|
|
155
|
+
return resolveNative(ctx, 'csrfToken')(secret) as Uint8Array | string
|
|
122
156
|
},
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
return addon.csrfToken(secret)
|
|
157
|
+
csrfTokenInto(secret: Uint8Array, output: Uint8Array): number {
|
|
158
|
+
const f = ffi()
|
|
159
|
+
if (f) return f.csrfTokenInto(secret, output)
|
|
160
|
+
return writeInto('csrf token', output, addon.csrfToken(secret))
|
|
127
161
|
},
|
|
128
162
|
csrfVerify(token: Uint8Array, secret: Uint8Array): boolean {
|
|
129
|
-
|
|
130
|
-
if (ffi) return ffi.csrfVerify(token, secret)
|
|
131
|
-
return addon.csrfVerify(token, secret)
|
|
163
|
+
return resolveNative(ctx, 'csrfVerify')(token, secret) as boolean
|
|
132
164
|
},
|
|
133
165
|
jwtSign(
|
|
134
166
|
claims: Record<string, unknown>,
|
|
@@ -136,6 +168,23 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
136
168
|
ttlSeconds?: number | null,
|
|
137
169
|
nowSeconds?: number,
|
|
138
170
|
): Uint8Array {
|
|
171
|
+
// FFI-first: route the object claims through the C-ABI `jwtSignBytes`
|
|
172
|
+
// (pre-serialized JSON). The engine returns the token as a string
|
|
173
|
+
// (native transfer); re-encode to bytes to keep the public BYTE contract
|
|
174
|
+
// (batch/benchmark consumers compare bytes). NOTE: JSON.stringify
|
|
175
|
+
// preserves JS insertion order while napi's serde_json sorts keys — for
|
|
176
|
+
// multi-key claims the two transports emit different-but-equally-valid
|
|
177
|
+
// tokens (both verify; parity tests cross-verify semantically).
|
|
178
|
+
const f = ffi()
|
|
179
|
+
if (f) {
|
|
180
|
+
const token = f.jwtSignBytes(
|
|
181
|
+
encoder.encode(JSON.stringify(claims)),
|
|
182
|
+
secret,
|
|
183
|
+
ttlSeconds ?? 0,
|
|
184
|
+
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
185
|
+
)
|
|
186
|
+
return encoder.encode(token)
|
|
187
|
+
}
|
|
139
188
|
return addon.jwtSign(
|
|
140
189
|
claims,
|
|
141
190
|
secret,
|
|
@@ -148,12 +197,12 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
148
197
|
secret: Uint8Array,
|
|
149
198
|
ttlSeconds?: number | null,
|
|
150
199
|
nowSeconds?: number,
|
|
151
|
-
): Uint8Array {
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
200
|
+
): Uint8Array | string {
|
|
201
|
+
const f = ffi()
|
|
202
|
+
if (f) {
|
|
154
203
|
// The C-ABI `ttl <= 0` sentinel matches napi's Option<i64> (inject only
|
|
155
204
|
// when positive), and nowSeconds defaults identically.
|
|
156
|
-
return
|
|
205
|
+
return f.jwtSignBytes(
|
|
157
206
|
claimsJson,
|
|
158
207
|
secret,
|
|
159
208
|
ttlSeconds ?? 0,
|
|
@@ -167,19 +216,55 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
167
216
|
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
168
217
|
)
|
|
169
218
|
},
|
|
219
|
+
jwtSignBytesInto(
|
|
220
|
+
claimsJson: Uint8Array,
|
|
221
|
+
secret: Uint8Array,
|
|
222
|
+
output: Uint8Array,
|
|
223
|
+
ttlSeconds?: number | null,
|
|
224
|
+
nowSeconds?: number,
|
|
225
|
+
): number {
|
|
226
|
+
const f = ffi()
|
|
227
|
+
if (f) {
|
|
228
|
+
return f.jwtSignBytesInto(
|
|
229
|
+
claimsJson,
|
|
230
|
+
secret,
|
|
231
|
+
ttlSeconds ?? 0,
|
|
232
|
+
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
233
|
+
output,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
return writeInto(
|
|
237
|
+
'jwt sign',
|
|
238
|
+
output,
|
|
239
|
+
addon.jwtSignBytes(
|
|
240
|
+
claimsJson,
|
|
241
|
+
secret,
|
|
242
|
+
ttlSeconds ?? null,
|
|
243
|
+
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
},
|
|
170
247
|
jwtVerify(token: Uint8Array, secret: Uint8Array, nowSeconds?: number): unknown {
|
|
248
|
+
const f = ffi()
|
|
249
|
+
if (f) {
|
|
250
|
+
// Verify via the FFI cstring path (castrum_jwt_verify): the engine
|
|
251
|
+
// clones the claims JSON string at return (zero decode), and `null` =
|
|
252
|
+
// invalid signature / expired / malformed → null (napi Option parity).
|
|
253
|
+
const claims = f.jwtVerify(token, secret, nowSeconds ?? Math.floor(Date.now() / 1000))
|
|
254
|
+
return claims === null ? null : (JSON.parse(claims) as unknown)
|
|
255
|
+
}
|
|
171
256
|
return addon.jwtVerify(token, secret, nowSeconds ?? Math.floor(Date.now() / 1000))
|
|
172
257
|
},
|
|
173
258
|
passwordHash(
|
|
174
259
|
password: Uint8Array,
|
|
175
260
|
salt: Uint8Array,
|
|
176
261
|
options?: PasswordHashOptions | null,
|
|
177
|
-
): Uint8Array {
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
262
|
+
): Uint8Array | string {
|
|
263
|
+
const f = ffi()
|
|
264
|
+
if (f) {
|
|
180
265
|
// Resolve the napi defaults (rust/crypto/argon2.rs resolve_opts).
|
|
181
266
|
const o = options ?? {}
|
|
182
|
-
return
|
|
267
|
+
return f.passwordHash(
|
|
183
268
|
password,
|
|
184
269
|
salt,
|
|
185
270
|
o.mCost ?? 19456,
|
|
@@ -191,18 +276,16 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
191
276
|
return addon.passwordHash(password, salt, options ?? null)
|
|
192
277
|
},
|
|
193
278
|
passwordVerify(password: Uint8Array, phc: Uint8Array): boolean {
|
|
194
|
-
|
|
195
|
-
if (ffi) return ffi.passwordVerify(password, phc)
|
|
196
|
-
return addon.passwordVerify(password, phc)
|
|
279
|
+
return resolveNative(ctx, 'passwordVerify')(password, phc) as boolean
|
|
197
280
|
},
|
|
198
281
|
passwordHashBcrypt(password: Uint8Array, cost: number): string {
|
|
199
|
-
|
|
200
|
-
if (ffi) return decoder.decode(ffi.passwordHashBcrypt(password, cost))
|
|
201
|
-
return addon.passwordHashBcrypt(password, cost)
|
|
282
|
+
return resolveNative(ctx, 'passwordHashBcrypt')(password, cost) as string
|
|
202
283
|
},
|
|
203
284
|
passwordVerifyBcrypt(password: Uint8Array, hash: string): boolean {
|
|
204
|
-
|
|
205
|
-
|
|
285
|
+
// FFI-first: `hash` is a `cstring` ARG (the engine transcodes the PHC
|
|
286
|
+
// string in-engine — no JS encode); `password` stays `(ptr,len)` bytes.
|
|
287
|
+
const f = ffi()
|
|
288
|
+
if (f) return f.passwordVerifyBcrypt(password, hash)
|
|
206
289
|
return addon.passwordVerifyBcrypt(password, hash)
|
|
207
290
|
},
|
|
208
291
|
pbkdf2Sha256(
|
|
@@ -211,9 +294,7 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
211
294
|
rounds: number,
|
|
212
295
|
dkLen: number,
|
|
213
296
|
): Uint8Array {
|
|
214
|
-
|
|
215
|
-
if (ffi) return ffi.pbkdf2Sha256(password, salt, rounds, dkLen)
|
|
216
|
-
return addon.pbkdf2Sha256(password, salt, rounds, dkLen)
|
|
297
|
+
return resolveNative(ctx, 'pbkdf2Sha256')(password, salt, rounds, dkLen) as Uint8Array
|
|
217
298
|
},
|
|
218
299
|
aeadEncrypt(
|
|
219
300
|
key: Uint8Array,
|
|
@@ -221,9 +302,9 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
221
302
|
plaintext: Uint8Array,
|
|
222
303
|
algorithm?: string | null,
|
|
223
304
|
): Uint8Array {
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
return
|
|
305
|
+
const f = ffi()
|
|
306
|
+
if (f) {
|
|
307
|
+
return f.aeadEncrypt(key, nonce, plaintext, algorithm === 'chacha20-poly1305' ? 1 : 0)
|
|
227
308
|
}
|
|
228
309
|
return addon.aeadEncrypt(key, nonce, plaintext, algorithm ?? null)
|
|
229
310
|
},
|
|
@@ -234,9 +315,9 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
234
315
|
output: Uint8Array,
|
|
235
316
|
algorithm?: string | null,
|
|
236
317
|
): number {
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
return
|
|
318
|
+
const f = ffi()
|
|
319
|
+
if (f) {
|
|
320
|
+
return f.aeadEncryptInto(
|
|
240
321
|
key,
|
|
241
322
|
nonce,
|
|
242
323
|
plaintext,
|
|
@@ -255,9 +336,9 @@ export function buildCrypto(ctx: RustClientContext) {
|
|
|
255
336
|
ciphertext: Uint8Array,
|
|
256
337
|
algorithm?: string | null,
|
|
257
338
|
): Uint8Array | null {
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
return
|
|
339
|
+
const f = ffi()
|
|
340
|
+
if (f) {
|
|
341
|
+
return f.aeadDecrypt(key, nonce, ciphertext, algorithm === 'chacha20-poly1305' ? 1 : 0)
|
|
261
342
|
}
|
|
262
343
|
return addon.aeadDecrypt(key, nonce, ciphertext, algorithm ?? null)
|
|
263
344
|
},
|