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
|
@@ -3,28 +3,30 @@
|
|
|
3
3
|
// Every `create*` here constructs a compiled-once native instance (key / schema
|
|
4
4
|
// / template precompiled at construction) plus the rayon thread-pool controls.
|
|
5
5
|
|
|
6
|
-
import { asNumber } from '../options'
|
|
7
|
-
import { getBunFFI, type BunFFI } from '../../native/ffi'
|
|
8
|
-
import type { RustClientContext } from '../context'
|
|
9
6
|
import type {
|
|
10
|
-
HmacSignerInstance,
|
|
11
|
-
SchemaValidatorInstance,
|
|
12
|
-
TemplateRendererInstance,
|
|
13
|
-
FormParserInstance,
|
|
14
|
-
MediaTypeParserInstance,
|
|
15
|
-
MediaTypeMatcherInstance,
|
|
16
|
-
ConditionalRequestInstance,
|
|
17
7
|
AcceptNegotiatorInstance,
|
|
8
|
+
AeadCipherInstance,
|
|
9
|
+
Argon2HasherInstance,
|
|
18
10
|
Base64CodecInstance,
|
|
11
|
+
ConditionalRequestInstance,
|
|
19
12
|
CookieSignerInstance,
|
|
20
13
|
CsrfProtectorInstance,
|
|
21
|
-
|
|
14
|
+
FormParserInstance,
|
|
15
|
+
HmacSignerInstance,
|
|
22
16
|
JwtSignerInstance,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
RateLimiterInstance,
|
|
17
|
+
MediaTypeMatcherInstance,
|
|
18
|
+
MediaTypeParserInstance,
|
|
26
19
|
PasswordHashOptions,
|
|
20
|
+
RateLimiterInstance,
|
|
21
|
+
SchemaValidatorInstance,
|
|
22
|
+
TemplateRendererInstance,
|
|
23
|
+
UrlBuilderInstance,
|
|
27
24
|
} from '../../native'
|
|
25
|
+
import type { BunFFI } from '../../native/ffi'
|
|
26
|
+
import { encoder } from '../../shared/bytes'
|
|
27
|
+
import { decodeUtf8, encodeUtf8 } from '../../shared/codec'
|
|
28
|
+
import type { RustClientContext } from '../context'
|
|
29
|
+
import { asNumber } from '../options'
|
|
28
30
|
|
|
29
31
|
// ── FFI-backed instance wrappers ───────────────────────────────────────────
|
|
30
32
|
// On Bun, instances whose methods have a STATELESS C-ABI sibling are backed by
|
|
@@ -41,7 +43,12 @@ import type {
|
|
|
41
43
|
function ffiHmacSigner(key: Uint8Array, ffi: BunFFI): HmacSignerInstance {
|
|
42
44
|
return {
|
|
43
45
|
sign(data) {
|
|
44
|
-
|
|
46
|
+
// Pooled `hmacSha256Into`: writes the 64 hex chars directly into a fresh
|
|
47
|
+
// buffer — no cstring return → no string clone → no encodeUtf8 round-trip
|
|
48
|
+
// (the old path cloned the hex string then re-encoded it to bytes).
|
|
49
|
+
const out = new Uint8Array(64)
|
|
50
|
+
const w = ffi.hmacSha256Into(key, data, out)
|
|
51
|
+
return out.subarray(0, w)
|
|
45
52
|
},
|
|
46
53
|
verify(data, sig) {
|
|
47
54
|
return ffi.hmacSha256Verify(key, data, sig)
|
|
@@ -56,7 +63,11 @@ function ffiBase64Codec(
|
|
|
56
63
|
): Base64CodecInstance {
|
|
57
64
|
return {
|
|
58
65
|
encode(input) {
|
|
59
|
-
|
|
66
|
+
// Pooled `base64EncodeInto`: writes the base64 bytes directly into a
|
|
67
|
+
// fresh buffer sized for the input — no cstring return / re-encode.
|
|
68
|
+
const out = new Uint8Array(Math.ceil(input.length / 3) * 4 + 3)
|
|
69
|
+
const w = ffi.base64EncodeInto(input, out, urlSafe, padding)
|
|
70
|
+
return out.subarray(0, w)
|
|
60
71
|
},
|
|
61
72
|
decode(input) {
|
|
62
73
|
return ffi.base64Decode(input, urlSafe, padding)
|
|
@@ -67,10 +78,178 @@ function ffiBase64Codec(
|
|
|
67
78
|
function ffiCookieSigner(secret: Uint8Array, ffi: BunFFI): CookieSignerInstance {
|
|
68
79
|
return {
|
|
69
80
|
sign(value) {
|
|
70
|
-
|
|
81
|
+
// Pooled `signCookieInto`: writes `value.<64-hex>` directly into a fresh
|
|
82
|
+
// buffer — no cstring return / re-encode round-trip.
|
|
83
|
+
const out = new Uint8Array(value.length + 65)
|
|
84
|
+
const w = ffi.signCookieInto(value, secret, out)
|
|
85
|
+
return out.subarray(0, w)
|
|
71
86
|
},
|
|
72
87
|
verify(signed) {
|
|
73
|
-
|
|
88
|
+
// Pooled `verifyCookieInto`: writes the verified value directly (the
|
|
89
|
+
// value is a substring of `signed`, so `signed.length + 1` always fits).
|
|
90
|
+
const out = new Uint8Array(signed.length + 1)
|
|
91
|
+
const w = ffi.verifyCookieInto(signed, secret, out)
|
|
92
|
+
return w === null ? null : out.subarray(0, w)
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Opaque-handle `ConditionalRequest`: the napi instance compiles the resource
|
|
99
|
+
* state once (construction); per-call `isNotModified` evaluates it through the
|
|
100
|
+
* C-ABI (`castrum_conditional_is_not_modified`) via the inner handle — no napi
|
|
101
|
+
* crossing. The napi instance MUST stay alive for the handle's lifetime (same
|
|
102
|
+
* contract as the ingress handler / `Ingress::inner_ptr`) — `keepAlive` is
|
|
103
|
+
* referenced by the returned method so the native state is never GC'd while
|
|
104
|
+
* the handle is in use.
|
|
105
|
+
*/
|
|
106
|
+
function ffiConditionalRequest(
|
|
107
|
+
napi: ConditionalRequestInstance,
|
|
108
|
+
inner: number,
|
|
109
|
+
ffi: BunFFI,
|
|
110
|
+
): ConditionalRequestInstance {
|
|
111
|
+
// Referenced by the method below → captured → prevents GC of the native
|
|
112
|
+
// state the opaque handle points into.
|
|
113
|
+
const keepAlive = napi
|
|
114
|
+
return {
|
|
115
|
+
isNotModified(ifNoneMatch, ifModifiedSince) {
|
|
116
|
+
void keepAlive
|
|
117
|
+
return ffi.conditionalIsNotModified(inner, ifNoneMatch ?? null, ifModifiedSince ?? null)
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Opaque-handle `UrlBuilder`: the napi instance parses the base ONCE; per-call
|
|
124
|
+
* `resolve` evaluates it through the C-ABI (`castrum_url_builder_resolve`) via
|
|
125
|
+
* the inner handle — no napi crossing. `keepAlive` references the napi instance
|
|
126
|
+
* so the precompiled base is never GC'd while the handle is in use.
|
|
127
|
+
*/
|
|
128
|
+
function ffiUrlBuilder(napi: UrlBuilderInstance, inner: number, ffi: BunFFI): UrlBuilderInstance {
|
|
129
|
+
const keepAlive = napi
|
|
130
|
+
return {
|
|
131
|
+
resolve(reference) {
|
|
132
|
+
void keepAlive
|
|
133
|
+
return ffi.urlBuilderResolve(inner, reference)
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Opaque-handle `MediaTypeMatcher` (expected precompiled once). */
|
|
139
|
+
function ffiMediaTypeMatcher(
|
|
140
|
+
napi: MediaTypeMatcherInstance,
|
|
141
|
+
inner: number,
|
|
142
|
+
ffi: BunFFI,
|
|
143
|
+
): MediaTypeMatcherInstance {
|
|
144
|
+
const keepAlive = napi
|
|
145
|
+
return {
|
|
146
|
+
matches(actual) {
|
|
147
|
+
void keepAlive
|
|
148
|
+
return ffi.mediaTypeMatcherMatches(inner, actual)
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Opaque-handle `AcceptNegotiator` (supported list precompiled once). */
|
|
154
|
+
function ffiAcceptNegotiator(
|
|
155
|
+
napi: AcceptNegotiatorInstance,
|
|
156
|
+
inner: number,
|
|
157
|
+
ffi: BunFFI,
|
|
158
|
+
): AcceptNegotiatorInstance {
|
|
159
|
+
const keepAlive = napi
|
|
160
|
+
return {
|
|
161
|
+
negotiate(header) {
|
|
162
|
+
void keepAlive
|
|
163
|
+
return ffi.acceptNegotiatorNegotiate(inner, header)
|
|
164
|
+
},
|
|
165
|
+
negotiateServerPreference(header) {
|
|
166
|
+
void keepAlive
|
|
167
|
+
return ffi.acceptNegotiatorNegotiateServer(inner, header)
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Opaque-handle `JwtSigner` (HS256 key + ttl precompiled once). */
|
|
173
|
+
function ffiJwtSigner(napi: JwtSignerInstance, inner: number, ffi: BunFFI): JwtSignerInstance {
|
|
174
|
+
const keepAlive = napi
|
|
175
|
+
return {
|
|
176
|
+
sign(claims, nowSeconds) {
|
|
177
|
+
void keepAlive
|
|
178
|
+
return ffi.jwtSignerSign(inner, encoder.encode(JSON.stringify(claims)), nowSeconds)
|
|
179
|
+
},
|
|
180
|
+
signBytes(claimsJson, nowSeconds) {
|
|
181
|
+
void keepAlive
|
|
182
|
+
return ffi.jwtSignerSign(inner, claimsJson, nowSeconds)
|
|
183
|
+
},
|
|
184
|
+
verify(token, nowSeconds) {
|
|
185
|
+
void keepAlive
|
|
186
|
+
const claims = ffi.jwtSignerVerify(inner, token, nowSeconds)
|
|
187
|
+
return claims === null ? null : (JSON.parse(decodeUtf8(claims)) as unknown)
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Opaque-handle `TemplateRenderer` (template compiled once). */
|
|
193
|
+
function ffiTemplateRenderer(
|
|
194
|
+
napi: TemplateRendererInstance,
|
|
195
|
+
inner: number,
|
|
196
|
+
ffi: BunFFI,
|
|
197
|
+
): TemplateRendererInstance {
|
|
198
|
+
const keepAlive = napi
|
|
199
|
+
return {
|
|
200
|
+
render(context) {
|
|
201
|
+
void keepAlive
|
|
202
|
+
return ffi.templateRender(inner, encoder.encode(JSON.stringify(context)))
|
|
203
|
+
},
|
|
204
|
+
renderBytes(contextJson) {
|
|
205
|
+
void keepAlive
|
|
206
|
+
return ffi.templateRender(inner, contextJson)
|
|
207
|
+
},
|
|
208
|
+
renderBatchPacked(data) {
|
|
209
|
+
void keepAlive
|
|
210
|
+
return napi.renderBatchPacked(data)
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Opaque-handle `SchemaValidator` (schema compiled once); the hot `validate`
|
|
216
|
+
* is a C-ABI eval; the detailed/batch/derive ops stay on the napi instance. */
|
|
217
|
+
function ffiSchemaValidator(
|
|
218
|
+
napi: SchemaValidatorInstance,
|
|
219
|
+
inner: number,
|
|
220
|
+
ffi: BunFFI,
|
|
221
|
+
): SchemaValidatorInstance {
|
|
222
|
+
const keepAlive = napi
|
|
223
|
+
return {
|
|
224
|
+
validate(input) {
|
|
225
|
+
void keepAlive
|
|
226
|
+
return ffi.schemaValidatorValidate(inner, input)
|
|
227
|
+
},
|
|
228
|
+
validateDetailed: napi.validateDetailed.bind(napi),
|
|
229
|
+
validateFirstError: napi.validateFirstError.bind(napi),
|
|
230
|
+
validateBatchPackedCount: napi.validateBatchPackedCount.bind(napi),
|
|
231
|
+
validateBatchPackedBitset: napi.validateBatchPackedBitset.bind(napi),
|
|
232
|
+
derive: napi.derive.bind(napi),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Opaque-handle `RateLimiter` (sharded state compiled once). */
|
|
237
|
+
function ffiRateLimiter(
|
|
238
|
+
napi: RateLimiterInstance,
|
|
239
|
+
inner: number,
|
|
240
|
+
ffi: BunFFI,
|
|
241
|
+
): RateLimiterInstance {
|
|
242
|
+
const keepAlive = napi
|
|
243
|
+
return {
|
|
244
|
+
check(key, nowMs) {
|
|
245
|
+
void keepAlive
|
|
246
|
+
// `key` is a `cstring` ARG — the engine transcodes the rate-limit key
|
|
247
|
+
// in-engine (no JS-side encode).
|
|
248
|
+
return ffi.rateLimiterCheck(inner, key, nowMs)
|
|
249
|
+
},
|
|
250
|
+
checkKey(key, nowMs) {
|
|
251
|
+
void keepAlive
|
|
252
|
+
return ffi.rateLimiterCheckKey(inner, key, nowMs)
|
|
74
253
|
},
|
|
75
254
|
}
|
|
76
255
|
}
|
|
@@ -78,7 +257,11 @@ function ffiCookieSigner(secret: Uint8Array, ffi: BunFFI): CookieSignerInstance
|
|
|
78
257
|
function ffiCsrfProtector(secret: Uint8Array, ffi: BunFFI): CsrfProtectorInstance {
|
|
79
258
|
return {
|
|
80
259
|
create() {
|
|
81
|
-
|
|
260
|
+
// Pooled `csrfTokenInto`: writes the 129-char `hex.hex` directly into a
|
|
261
|
+
// fresh buffer — no cstring return / re-encode round-trip.
|
|
262
|
+
const out = new Uint8Array(129)
|
|
263
|
+
const w = ffi.csrfTokenInto(secret, out)
|
|
264
|
+
return out.subarray(0, w)
|
|
82
265
|
},
|
|
83
266
|
verify(token) {
|
|
84
267
|
return ffi.csrfVerify(token, secret)
|
|
@@ -99,7 +282,7 @@ function ffiArgon2Hasher(
|
|
|
99
282
|
const outLen = o.outLen ?? 32
|
|
100
283
|
return {
|
|
101
284
|
hash(password, salt) {
|
|
102
|
-
return ffi.passwordHash(password, salt, mCost, tCost, pCost, outLen)
|
|
285
|
+
return encodeUtf8(ffi.passwordHash(password, salt, mCost, tCost, pCost, outLen))
|
|
103
286
|
},
|
|
104
287
|
verify(password, phc) {
|
|
105
288
|
return ffi.passwordVerify(password, phc)
|
|
@@ -148,22 +331,33 @@ function ffiFormParser(ffi: BunFFI): FormParserInstance {
|
|
|
148
331
|
|
|
149
332
|
/** Compiled-once factory + runtime-control methods (`Pick<RustScalar, ...>`). */
|
|
150
333
|
export function buildFactories(ctx: RustClientContext) {
|
|
334
|
+
const { transport } = ctx.runtime
|
|
151
335
|
const { addon } = ctx
|
|
152
336
|
|
|
153
337
|
return {
|
|
154
338
|
createSchemaValidator(schema: Uint8Array): SchemaValidatorInstance {
|
|
339
|
+
const ffi = transport.ffi
|
|
340
|
+
if (ffi) {
|
|
341
|
+
const napi = new addon.SchemaValidator(schema)
|
|
342
|
+
return ffiSchemaValidator(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
|
|
343
|
+
}
|
|
155
344
|
return new addon.SchemaValidator(schema)
|
|
156
345
|
},
|
|
157
346
|
createHmacSigner(key: Uint8Array): HmacSignerInstance {
|
|
158
|
-
const ffi =
|
|
347
|
+
const ffi = transport.ffi
|
|
159
348
|
if (ffi) return ffiHmacSigner(key, ffi)
|
|
160
349
|
return new addon.HmacSigner(key)
|
|
161
350
|
},
|
|
162
351
|
createTemplateRenderer(source: string): TemplateRendererInstance {
|
|
352
|
+
const ffi = transport.ffi
|
|
353
|
+
if (ffi) {
|
|
354
|
+
const napi = new addon.TemplateRenderer(source)
|
|
355
|
+
return ffiTemplateRenderer(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
|
|
356
|
+
}
|
|
163
357
|
return new addon.TemplateRenderer(source)
|
|
164
358
|
},
|
|
165
359
|
createFormParser(capacity?: number): FormParserInstance {
|
|
166
|
-
const ffi =
|
|
360
|
+
const ffi = transport.ffi
|
|
167
361
|
if (ffi) return ffiFormParser(ffi)
|
|
168
362
|
return new addon.FormParser(capacity)
|
|
169
363
|
},
|
|
@@ -174,43 +368,74 @@ export function buildFactories(ctx: RustClientContext) {
|
|
|
174
368
|
etagValue: Uint8Array,
|
|
175
369
|
lastModifiedSecs?: number,
|
|
176
370
|
): ConditionalRequestInstance {
|
|
371
|
+
// FFI-first via the opaque inner handle (the napi instance compiles the
|
|
372
|
+
// state once; per-call `isNotModified` is a C-ABI eval). The wrapper
|
|
373
|
+
// holds the napi instance for the handle's lifetime.
|
|
374
|
+
const ffi = transport.ffi
|
|
375
|
+
if (ffi) {
|
|
376
|
+
const napi = new addon.ConditionalRequest(etagValue, lastModifiedSecs ?? undefined)
|
|
377
|
+
return ffiConditionalRequest(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
|
|
378
|
+
}
|
|
177
379
|
return new addon.ConditionalRequest(etagValue, lastModifiedSecs ?? undefined)
|
|
178
380
|
},
|
|
179
381
|
createAcceptNegotiator(supported: string[]): AcceptNegotiatorInstance {
|
|
382
|
+
const ffi = transport.ffi
|
|
383
|
+
if (ffi) {
|
|
384
|
+
const napi = new addon.AcceptNegotiator(supported)
|
|
385
|
+
return ffiAcceptNegotiator(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
|
|
386
|
+
}
|
|
180
387
|
return new addon.AcceptNegotiator(supported)
|
|
181
388
|
},
|
|
182
389
|
createBase64Codec(urlSafe?: boolean, padding?: boolean): Base64CodecInstance {
|
|
183
|
-
const ffi =
|
|
390
|
+
const ffi = transport.ffi
|
|
184
391
|
if (ffi) return ffiBase64Codec(urlSafe, padding, ffi)
|
|
185
392
|
return new addon.Base64Codec(urlSafe ?? undefined, padding ?? undefined)
|
|
186
393
|
},
|
|
187
394
|
createCookieSigner(secret: Uint8Array): CookieSignerInstance {
|
|
188
|
-
const ffi =
|
|
395
|
+
const ffi = transport.ffi
|
|
189
396
|
if (ffi) return ffiCookieSigner(secret, ffi)
|
|
190
397
|
return new addon.CookieSigner(secret)
|
|
191
398
|
},
|
|
192
399
|
createCsrfProtector(secret: Uint8Array): CsrfProtectorInstance {
|
|
193
|
-
const ffi =
|
|
400
|
+
const ffi = transport.ffi
|
|
194
401
|
if (ffi) return ffiCsrfProtector(secret, ffi)
|
|
195
402
|
return new addon.CsrfProtector(secret)
|
|
196
403
|
},
|
|
197
404
|
createUrlBuilder(base: Uint8Array): UrlBuilderInstance {
|
|
405
|
+
// FFI-first via the opaque inner handle (the napi instance parses the
|
|
406
|
+
// base ONCE; per-call `resolve` is a C-ABI eval). The wrapper holds the
|
|
407
|
+
// napi instance for the handle's lifetime.
|
|
408
|
+
const ffi = transport.ffi
|
|
409
|
+
if (ffi) {
|
|
410
|
+
const napi = new addon.UrlBuilder(base)
|
|
411
|
+
return ffiUrlBuilder(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
|
|
412
|
+
}
|
|
198
413
|
return new addon.UrlBuilder(base)
|
|
199
414
|
},
|
|
200
415
|
createJwtSigner(secret: Uint8Array, ttlSeconds?: number): JwtSignerInstance {
|
|
416
|
+
const ffi = transport.ffi
|
|
417
|
+
if (ffi) {
|
|
418
|
+
const napi = new addon.JwtSigner(secret, ttlSeconds ?? undefined)
|
|
419
|
+
return ffiJwtSigner(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
|
|
420
|
+
}
|
|
201
421
|
return new addon.JwtSigner(secret, ttlSeconds ?? undefined)
|
|
202
422
|
},
|
|
203
423
|
createAeadCipher(key: Uint8Array, algorithm?: string): AeadCipherInstance {
|
|
204
|
-
const ffi =
|
|
424
|
+
const ffi = transport.ffi
|
|
205
425
|
if (ffi) return ffiAeadCipher(key, algorithm, ffi)
|
|
206
426
|
return new addon.AeadCipher(key, algorithm ?? undefined)
|
|
207
427
|
},
|
|
208
428
|
createArgon2Hasher(options?: PasswordHashOptions | null): Argon2HasherInstance {
|
|
209
|
-
const ffi =
|
|
429
|
+
const ffi = transport.ffi
|
|
210
430
|
if (ffi) return ffiArgon2Hasher(options, ffi)
|
|
211
431
|
return new addon.Argon2Hasher(options ?? undefined)
|
|
212
432
|
},
|
|
213
433
|
createMediaTypeMatcher(expected: Uint8Array): MediaTypeMatcherInstance {
|
|
434
|
+
const ffi = transport.ffi
|
|
435
|
+
if (ffi) {
|
|
436
|
+
const napi = new addon.MediaTypeMatcher(expected)
|
|
437
|
+
return ffiMediaTypeMatcher(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
|
|
438
|
+
}
|
|
214
439
|
return new addon.MediaTypeMatcher(expected)
|
|
215
440
|
},
|
|
216
441
|
createRateLimiter(
|
|
@@ -218,6 +443,11 @@ export function buildFactories(ctx: RustClientContext) {
|
|
|
218
443
|
windowMs: number,
|
|
219
444
|
maxEntries?: number | null,
|
|
220
445
|
): RateLimiterInstance {
|
|
446
|
+
const ffi = transport.ffi
|
|
447
|
+
if (ffi) {
|
|
448
|
+
const napi = new addon.RateLimiter(limit, windowMs, maxEntries ?? undefined)
|
|
449
|
+
return ffiRateLimiter(napi, Number(napi.innerPtr?.() ?? 0n), ffi)
|
|
450
|
+
}
|
|
221
451
|
return new addon.RateLimiter(limit, windowMs, maxEntries ?? undefined)
|
|
222
452
|
},
|
|
223
453
|
initThreadPool(threads?: number): void {
|
|
@@ -2,71 +2,73 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Mirrors rust/crypto/hashing.rs + hmac_sha256.rs: crc32 / fnv1a64 / HMAC.
|
|
4
4
|
// (HMAC reuses the context-cached signer to avoid re-constructing the key.)
|
|
5
|
+
//
|
|
6
|
+
// Runtime dispatch is centralized in the adapter (`ctx.runtime`): the Bun
|
|
7
|
+
// built-in delegations (crc32/xxh3/hmacSha256) come from `builtins.has(op)`
|
|
8
|
+
// and the native call from `transport.resolve(op)` / `transport.ffi`
|
|
9
|
+
// (bun:ffi first, napi fallback) — no inline `isBun()` / `getBunFFI()`.
|
|
5
10
|
|
|
11
|
+
import { toBytes } from '../../shared/bytes'
|
|
12
|
+
import { writeInto } from '../into'
|
|
13
|
+
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
6
14
|
import { asBigInt, asNumber } from '../options'
|
|
7
|
-
import { resolveNative, type RustClientContext } from '../context'
|
|
8
|
-
import { getBunFFI } from '../../native/ffi'
|
|
9
|
-
import { isBun } from '../../shared/runtime'
|
|
10
|
-
|
|
11
|
-
const hexEncoder = new TextEncoder()
|
|
12
15
|
|
|
13
16
|
/** Hashing scalar methods (`Pick<RustScalar, ...>`). */
|
|
14
17
|
export function buildHashing(ctx: RustClientContext) {
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
+
const { builtins, transport } = ctx.runtime
|
|
19
|
+
// Hoist the immutable per-runtime builtin-delegation decisions into a
|
|
20
|
+
// bind-time map (BUILTIN_OPS is a module constant — never mutated after
|
|
21
|
+
// load) so the hot path reads object properties instead of `Set.has`.
|
|
22
|
+
const HAS = {
|
|
23
|
+
crc32: builtins.has('crc32'),
|
|
24
|
+
xxh3: builtins.has('xxh3'),
|
|
25
|
+
hmacSha256: builtins.has('hmacSha256'),
|
|
26
|
+
}
|
|
27
|
+
// Lazy-memoized ffi surface: binds on first call, single local read after.
|
|
28
|
+
const ffi = memoizeFfi(transport)
|
|
29
|
+
// bun:ffi JIT-calls the C ABI exports with ~1/4 the crossing cost of napi
|
|
30
|
+
// for these sub-µs hashes; the transport resolves ffi-first and caches.
|
|
18
31
|
return {
|
|
19
32
|
crc32(input: Uint8Array): number {
|
|
20
33
|
// Optimal by default under Bun: `Bun.hash.crc32` (native SIMD) beats the
|
|
21
34
|
// rust+FFI crossing (~2.8-8.4x, decision matrix) and matches the
|
|
22
|
-
// `
|
|
23
|
-
|
|
24
|
-
if (isBun()) return Bun.hash.crc32(input) >>> 0
|
|
25
|
-
const ffi = getBunFFI()
|
|
26
|
-
if (ffi) return ffi.crc32(input)
|
|
35
|
+
// `opImpl` recommendation. Node keeps the addon.
|
|
36
|
+
if (HAS.crc32) return builtins.crc32(input)
|
|
27
37
|
return asNumber(resolveNative(ctx, 'crc32')(input)) >>> 0
|
|
28
38
|
},
|
|
29
39
|
fnv1a64(input: Uint8Array): bigint {
|
|
30
|
-
const ffi = getBunFFI()
|
|
31
|
-
if (ffi) return ffi.fnv1a64(input)
|
|
32
40
|
return asBigInt(resolveNative(ctx, 'fnv1a64')(input))
|
|
33
41
|
},
|
|
34
42
|
xxh3(input: Uint8Array): bigint {
|
|
35
43
|
// Optimal by default under Bun: `Bun.hash.xxHash3` beats the rust+FFI
|
|
36
44
|
// crossing (~4x, decision matrix). Node keeps the addon.
|
|
37
|
-
if (
|
|
38
|
-
const ffi = getBunFFI()
|
|
39
|
-
if (ffi) return ffi.xxh3(input)
|
|
45
|
+
if (HAS.xxh3) return builtins.xxh3(input)
|
|
40
46
|
return asBigInt(resolveNative(ctx, 'xxh3')(input))
|
|
41
47
|
},
|
|
42
|
-
hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array {
|
|
48
|
+
hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array | string {
|
|
43
49
|
// Optimal by default under Bun: `Bun.CryptoHasher` (native) beats the
|
|
44
|
-
// rust+FFI crossing (~1.2x, decision matrix).
|
|
45
|
-
//
|
|
46
|
-
// addon
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
const hasher = new Bun.CryptoHasher('sha256', key)
|
|
51
|
-
hasher.update(data)
|
|
52
|
-
return hexEncoder.encode(Buffer.from(hasher.digest()).toString('hex'))
|
|
53
|
-
}
|
|
54
|
-
const ffi = getBunFFI()
|
|
55
|
-
if (ffi) return ffi.hmacSha256(key, data)
|
|
50
|
+
// rust+FFI crossing (~1.2x, decision matrix). Bun returns the 64-char
|
|
51
|
+
// lowercase-hex STRING (native transfer — no TextEncoder round-trip); the
|
|
52
|
+
// addon returns the same hex as bytes. Node keeps the FFI path.
|
|
53
|
+
if (HAS.hmacSha256) return builtins.hmacSha256(key, data)
|
|
54
|
+
const f = ffi()
|
|
55
|
+
if (f) return f.hmacSha256(key, data)
|
|
56
56
|
return ctx.hmacSigner(key).sign(data)
|
|
57
57
|
},
|
|
58
58
|
hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number {
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
if (output.length < bytes.length) throw new Error('hmac sha256: output buffer too small')
|
|
63
|
-
output.set(bytes)
|
|
64
|
-
return bytes.length
|
|
59
|
+
const f = ffi()
|
|
60
|
+
if (f) return f.hmacSha256Into(key, data, output)
|
|
61
|
+
return writeInto('hmac sha256', output, ctx.hmacSigner(key).sign(data))
|
|
65
62
|
},
|
|
66
|
-
hmacSha256Verify(key: Uint8Array, data: Uint8Array, sig: Uint8Array): boolean {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
hmacSha256Verify(key: Uint8Array, data: Uint8Array, sig: Uint8Array | string): boolean {
|
|
64
|
+
// `hmacSha256` returns the signature as a hex STRING under Bun (built-in
|
|
65
|
+
// delegation / cstring) and as hex bytes under napi. The verifier accepts
|
|
66
|
+
// both — normalize the string to hex bytes so the ffi/napi cores see the
|
|
67
|
+
// same input (sign → verify is self-consistent on every transport).
|
|
68
|
+
const sigBytes = toBytes(sig)
|
|
69
|
+
const f = ffi()
|
|
70
|
+
if (f) return f.hmacSha256Verify(key, data, sigBytes)
|
|
71
|
+
return ctx.hmacSigner(key).verify(data, sigBytes)
|
|
70
72
|
},
|
|
71
73
|
}
|
|
72
74
|
}
|