@nifrajs/core 5.0.0 → 5.1.0
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/dist/assurance.d.ts +15 -1
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +97 -71
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/edge-kit.d.ts +1 -1
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/route-compiler.d.ts +9 -0
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +95 -13
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/same-origin.d.ts +11 -1
- package/dist/internal/same-origin.d.ts.map +1 -1
- package/dist/internal/same-origin.js +8 -0
- package/dist/internal/same-origin.js.map +1 -1
- package/dist/server/body-lane.d.ts +8 -2
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +27 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/client-ip.js +6 -5
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +13 -1
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/edge.d.ts.map +1 -1
- package/dist/server/edge.js +2 -1
- package/dist/server/edge.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +5 -1
- package/dist/server/http.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +21 -6
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +139 -14
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/server-types.d.ts +16 -5
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +23 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +37 -13
- package/dist/server/server.js.map +1 -1
- package/dist/server/websocket.d.ts +11 -0
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +26 -12
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +7 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +42 -2
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +9 -3
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +35 -5
- package/dist/single-copy.js.map +1 -1
- package/dist/wire.d.ts +5 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +5 -0
- package/dist/wire.js.map +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +2 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/assurance.ts +120 -81
- package/src/capabilities.ts +3 -1
- package/src/edge-kit.ts +1 -1
- package/src/index.ts +2 -2
- package/src/internal/route-compiler.ts +107 -16
- package/src/internal/same-origin.ts +14 -1
- package/src/server/body-lane.ts +36 -1
- package/src/server/client-ip.ts +5 -5
- package/src/server/cookies.ts +12 -1
- package/src/server/edge.ts +2 -1
- package/src/server/http.ts +7 -1
- package/src/server/response-contract-lane.ts +162 -10
- package/src/server/server-types.ts +16 -5
- package/src/server/server.ts +48 -9
- package/src/server/websocket.ts +39 -13
- package/src/server/ws-bun.ts +48 -1
- package/src/server/ws-hook.ts +9 -2
- package/src/server.ts +1 -1
- package/src/single-copy.ts +35 -4
- package/src/wire.ts +14 -0
- package/src/ws.ts +2 -0
package/src/server/websocket.ts
CHANGED
|
@@ -233,8 +233,19 @@ export type WebSocketUpgradeOutcome =
|
|
|
233
233
|
* `.use(websocket())` tree-shaking). `@nifrajs/workers` may import `attachWebSocket` directly instead. */
|
|
234
234
|
readonly attach: WsAttach
|
|
235
235
|
readonly maxPayloadBytes?: number
|
|
236
|
+
/** Builds `ws.send` over an adapter's native send, enforcing the route's {@link
|
|
237
|
+
* WebSocketHandler.validateSend}. Set on every outcome a nifra app produces; an adapter that
|
|
238
|
+
* wires its own sockets instead of calling `attach` (Node, Deno) builds `send` with it. */
|
|
239
|
+
readonly sender?: WsSender
|
|
236
240
|
}
|
|
237
241
|
|
|
242
|
+
/** The send-builder the upgrade outcome carries (the installed runtime's `sender`). */
|
|
243
|
+
export type WsSender = (
|
|
244
|
+
handler: WebSocketHandler,
|
|
245
|
+
send: (payload: string | ArrayBufferView | ArrayBuffer) => void,
|
|
246
|
+
onError: (error: unknown) => void,
|
|
247
|
+
) => (payload: string | ArrayBufferView | ArrayBuffer) => void
|
|
248
|
+
|
|
238
249
|
/** The socket-wiring signature the upgrade outcome carries (the installed runtime's `attach`). */
|
|
239
250
|
export type WsAttach = (
|
|
240
251
|
socket: StandardWebSocket,
|
|
@@ -325,18 +336,11 @@ export function attachWebSocket<
|
|
|
325
336
|
socket.addEventListener("message", (event: { readonly data: unknown }) => {
|
|
326
337
|
const raw = event.data
|
|
327
338
|
const payload: WebSocketData = typeof raw === "string" ? raw : toBinary(raw)
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
typeof raw === "string"
|
|
334
|
-
? WS_PAYLOAD_ENCODER.encode(raw).byteLength
|
|
335
|
-
: (payload as Uint8Array).byteLength
|
|
336
|
-
if (payloadBytes > maxPayloadBytes) {
|
|
337
|
-
socket.close(1009, "message too large")
|
|
338
|
-
return
|
|
339
|
-
}
|
|
339
|
+
// The runtime hands over each message whole, so this bounds what reaches the handler, not what
|
|
340
|
+
// the runtime buffered to deliver it.
|
|
341
|
+
if (maxPayloadBytes !== undefined && exceedsPayloadCap(payload, maxPayloadBytes)) {
|
|
342
|
+
socket.close(1009, "message too large")
|
|
343
|
+
return
|
|
340
344
|
}
|
|
341
345
|
safe(() =>
|
|
342
346
|
(
|
|
@@ -363,7 +367,25 @@ export function attachWebSocket<
|
|
|
363
367
|
}
|
|
364
368
|
|
|
365
369
|
const WS_MESSAGE_DECODER = new TextDecoder()
|
|
366
|
-
|
|
370
|
+
|
|
371
|
+
/** Whether a message is over `cap` UTF-8 bytes, without encoding a text one: each UTF-16 unit takes
|
|
372
|
+
* 1-3 bytes (a surrogate pair 4 for its 2), so only a length between those bounds is counted. */
|
|
373
|
+
export function exceedsPayloadCap(
|
|
374
|
+
message: string | ArrayBuffer | Uint8Array,
|
|
375
|
+
cap: number,
|
|
376
|
+
): boolean {
|
|
377
|
+
if (typeof message !== "string") return message.byteLength > cap
|
|
378
|
+
const units = message.length
|
|
379
|
+
if (units > cap) return true
|
|
380
|
+
if (units * 3 <= cap) return false
|
|
381
|
+
let bytes = 0
|
|
382
|
+
for (let i = 0; i < units; i++) {
|
|
383
|
+
const unit = message.charCodeAt(i)
|
|
384
|
+
bytes += unit < 0x80 ? 1 : unit < 0x800 || (unit >= 0xd800 && unit <= 0xdfff) ? 2 : 3
|
|
385
|
+
if (bytes > cap) return true
|
|
386
|
+
}
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
367
389
|
|
|
368
390
|
type WebSocketSendPayload = string | ArrayBufferView | ArrayBuffer
|
|
369
391
|
|
|
@@ -448,6 +470,10 @@ export function createWebSocketSender(
|
|
|
448
470
|
}
|
|
449
471
|
}
|
|
450
472
|
|
|
473
|
+
/** {@link createWebSocketSender} configured from a route's handler: the outcome's `sender`. */
|
|
474
|
+
export const webSocketSender: WsSender = (handler, send, onError) =>
|
|
475
|
+
createWebSocketSender(send, handler.sendSchema, handler.validateSend, handler.transport, onError)
|
|
476
|
+
|
|
451
477
|
/**
|
|
452
478
|
* If the handler declares a `messageSchema`, return a copy whose `message` validates each frame -
|
|
453
479
|
* parse as JSON, run the Standard Schema, then call the user's `message` with the typed value, or
|
package/src/server/ws-bun.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
createWebSocketSender,
|
|
10
|
+
exceedsPayloadCap,
|
|
10
11
|
type NifraWebSocket,
|
|
11
12
|
type TopicRegistry,
|
|
12
13
|
type WebSocketData,
|
|
@@ -20,6 +21,9 @@ export interface BunWsData {
|
|
|
20
21
|
readonly handler: WebSocketHandler
|
|
21
22
|
/** Mounted/modern upgrades carry this registry; old/manual fixtures may omit it. */
|
|
22
23
|
readonly pubsub?: TopicRegistry
|
|
24
|
+
/** The inbound cap of the app this socket belongs to (the upgrade outcome's). Absent, the
|
|
25
|
+
* listening app's applies. */
|
|
26
|
+
readonly maxPayloadBytes?: number | undefined
|
|
23
27
|
data: unknown
|
|
24
28
|
nifra?: NifraWebSocket
|
|
25
29
|
}
|
|
@@ -38,6 +42,31 @@ export interface BunSocket {
|
|
|
38
42
|
readonly data: BunWsData
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The largest `wsMaxPayloadBytes` among the nifra apps mounted under `app` that serve sockets, or
|
|
47
|
+
* `floor` when none declares more. Bun takes one frame limit per listener, so it has to admit every
|
|
48
|
+
* mounted app's cap. Reads each server's own mount table, as reflection does, so an app that never
|
|
49
|
+
* listens pays nothing for it; a child that is not a nifra server names no cap here.
|
|
50
|
+
*/
|
|
51
|
+
function mountedPayloadCeiling(app: object, floor: number, seen: Set<object>): number {
|
|
52
|
+
if (seen.has(app)) return floor
|
|
53
|
+
seen.add(app)
|
|
54
|
+
const mounts: unknown = Reflect.get(app, "fetchMounts")
|
|
55
|
+
if (!Array.isArray(mounts)) return floor
|
|
56
|
+
let ceiling = floor
|
|
57
|
+
for (const mount of mounts) {
|
|
58
|
+
const child: unknown = typeof mount === "object" && mount !== null ? mount.app : undefined
|
|
59
|
+
if (typeof child !== "object" || child === null) continue
|
|
60
|
+
const routes: unknown = Reflect.get(child, "wsRoutes")
|
|
61
|
+
const cap: unknown = Reflect.get(child, "wsMaxPayloadBytes")
|
|
62
|
+
if (Array.isArray(routes) && routes.length > 0 && typeof cap === "number" && cap > ceiling) {
|
|
63
|
+
ceiling = cap
|
|
64
|
+
}
|
|
65
|
+
ceiling = mountedPayloadCeiling(child, ceiling, seen)
|
|
66
|
+
}
|
|
67
|
+
return ceiling
|
|
68
|
+
}
|
|
69
|
+
|
|
41
70
|
/** Normalize a received binary frame to `Uint8Array` (Bun hands a `Buffer`, already one; an
|
|
42
71
|
* `ArrayBuffer` is wrapped). Text frames stay `string`. */
|
|
43
72
|
function toBinaryData(message: ArrayBuffer | Uint8Array): Uint8Array {
|
|
@@ -127,9 +156,21 @@ function dispatchWsCallback(
|
|
|
127
156
|
* `nativePubsub` (set by `listen()` when the app has no validated-send route) makes subscriptions use
|
|
128
157
|
* Bun's own pub/sub; there the registry holds nothing and Bun auto-drops native subscriptions on
|
|
129
158
|
* close, so the `unsubscribeAll` sweep is skipped.
|
|
159
|
+
*
|
|
160
|
+
* `app` is the listening server. Its `wsMaxPayloadBytes` and its mounted apps' set the listener's
|
|
161
|
+
* `maxPayloadLength`; a socket whose app declares less is closed with `1009` here.
|
|
130
162
|
*/
|
|
131
|
-
export function createBunWsHandlers(
|
|
163
|
+
export function createBunWsHandlers(
|
|
164
|
+
topics: TopicRegistry,
|
|
165
|
+
nativePubsub = false,
|
|
166
|
+
app?: object,
|
|
167
|
+
): BunWsHandlers {
|
|
168
|
+
const own: unknown = app === undefined ? undefined : Reflect.get(app, "wsMaxPayloadBytes")
|
|
169
|
+
const appCap = typeof own === "number" ? own : Number.POSITIVE_INFINITY
|
|
170
|
+
const listenerCap =
|
|
171
|
+
app === undefined ? appCap : mountedPayloadCeiling(app, appCap, new Set<object>())
|
|
132
172
|
return {
|
|
173
|
+
maxPayloadLength: listenerCap,
|
|
133
174
|
open: (raw) => {
|
|
134
175
|
const ws = raw as BunSocket
|
|
135
176
|
const nifra = wrapBunSocket(ws, topics, ws.data.handler, nativePubsub)
|
|
@@ -138,6 +179,12 @@ export function createBunWsHandlers(topics: TopicRegistry, nativePubsub = false)
|
|
|
138
179
|
},
|
|
139
180
|
message: (raw, message) => {
|
|
140
181
|
const ws = raw as BunSocket
|
|
182
|
+
// Only a socket whose app declares less than the listener admits pays for a size check.
|
|
183
|
+
const cap = ws.data.maxPayloadBytes ?? appCap
|
|
184
|
+
if (cap < listenerCap && exceedsPayloadCap(message, cap)) {
|
|
185
|
+
ws.close(1009, "message too large")
|
|
186
|
+
return
|
|
187
|
+
}
|
|
141
188
|
const handler = ws.data.handler
|
|
142
189
|
const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics, handler, nativePubsub)
|
|
143
190
|
const data: WebSocketData = typeof message === "string" ? message : toBinaryData(message)
|
package/src/server/ws-hook.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
TopicRegistry,
|
|
16
16
|
WebSocketContext,
|
|
17
17
|
WebSocketHandler,
|
|
18
|
+
WsSender,
|
|
18
19
|
} from "./websocket.ts"
|
|
19
20
|
|
|
20
21
|
/** The Bun serve `websocket` callback trio, closed over one app's `TopicRegistry`. The `ws`
|
|
@@ -24,6 +25,9 @@ export interface BunWsHandlers {
|
|
|
24
25
|
open(ws: unknown): void
|
|
25
26
|
message(ws: unknown, message: string | ArrayBuffer | Uint8Array): void
|
|
26
27
|
close(ws: unknown, code: number, reason: string): void
|
|
28
|
+
/** The listener's `maxPayloadLength`: the largest `wsMaxPayloadBytes` among the app and the apps
|
|
29
|
+
* mounted on it. A socket whose app declares less checks its own cap per message. */
|
|
30
|
+
readonly maxPayloadLength: number
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/** What `@nifrajs/core/ws` registers: everything `server.ts` and `toFetchHandler` need at runtime. */
|
|
@@ -37,8 +41,9 @@ export interface WsRuntime {
|
|
|
37
41
|
/** One in-process pub/sub registry per app (backs `ws.subscribe` + `app.publish`). */
|
|
38
42
|
createTopics(): TopicRegistry
|
|
39
43
|
/** The Bun `websocket` config for `listen()` when the app has WS routes. `nativePubsub` routes
|
|
40
|
-
* `ws.subscribe` through Bun's own pub/sub (set when no route validates outbound frames).
|
|
41
|
-
|
|
44
|
+
* `ws.subscribe` through Bun's own pub/sub (set when no route validates outbound frames). `app` is
|
|
45
|
+
* the listening server, whose mount table names the payload caps the listener has to admit. */
|
|
46
|
+
bunHandlers(topics: TopicRegistry, nativePubsub?: boolean, app?: object): BunWsHandlers
|
|
42
47
|
/** `attachWebSocket` - wires a standard server socket (Workers `WebSocketPair`) to a handler. */
|
|
43
48
|
attach(
|
|
44
49
|
socket: StandardWebSocket,
|
|
@@ -46,4 +51,6 @@ export interface WsRuntime {
|
|
|
46
51
|
data: unknown,
|
|
47
52
|
options: { openNow: boolean; pubsub: TopicRegistry },
|
|
48
53
|
): NifraWebSocket
|
|
54
|
+
/** `webSocketSender` - `ws.send` for an adapter that wires its own sockets (Node, Deno). */
|
|
55
|
+
sender: WsSender
|
|
49
56
|
}
|
package/src/server.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
export { FrameworkError, RouteConfigError, type RouteConfigErrorCode } from "./errors.ts"
|
|
12
12
|
export { FRAMEWORK_NAME, type FrameworkName } from "./internal/brand.ts"
|
|
13
|
-
export { isSameOriginRequest } from "./internal/same-origin.ts"
|
|
13
|
+
export { isSameOriginForm, isSameOriginRequest } from "./internal/same-origin.ts"
|
|
14
14
|
export {
|
|
15
15
|
ROUTE_PATTERN_OVERLAP_MAX_STATES,
|
|
16
16
|
RoutePatternOverlapLimitError,
|
package/src/single-copy.ts
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs"
|
|
58
|
-
import { dirname, join, relative, resolve, sep } from "node:path"
|
|
58
|
+
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path"
|
|
59
59
|
import type { BunPlugin } from "bun"
|
|
60
60
|
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
61
61
|
|
|
@@ -339,6 +339,26 @@ const installedCopy = (
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
/** The package a bare specifier names, without its subpath: `@scope/name` or `name`. */
|
|
343
|
+
const packageNameOf = (specifier: string): string =>
|
|
344
|
+
specifier
|
|
345
|
+
.split("/")
|
|
346
|
+
.slice(0, specifier.startsWith("@") ? 2 : 1)
|
|
347
|
+
.join("/")
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Whether Bun's own node_modules walk from `importer` already reaches the app's copy. Bun then keeps
|
|
351
|
+
* the package's `sideEffects: false` pruning; a path handed back from `onResolve` loses it, so the
|
|
352
|
+
* bundle reads every module the entry re-exports, `node:` imports included.
|
|
353
|
+
*/
|
|
354
|
+
const reachesAppCopy = (specifier: string, importer: string, root: string): boolean => {
|
|
355
|
+
if (!isAbsolute(importer)) return false
|
|
356
|
+
const name = packageNameOf(specifier)
|
|
357
|
+
const ours = installedCopy(root, name, parse(root).root)
|
|
358
|
+
if (ours === undefined) return false
|
|
359
|
+
return installedCopy(dirname(importer), name, parse(importer).root)?.root === ours.root
|
|
360
|
+
}
|
|
361
|
+
|
|
342
362
|
/** Every `node_modules` from the app root up to (and including) its repository root. */
|
|
343
363
|
const nodeModulesDirs = (root: string): readonly string[] => {
|
|
344
364
|
const boundary = repoBoundary(root)
|
|
@@ -355,6 +375,17 @@ const nodeModulesDirs = (root: string): readonly string[] => {
|
|
|
355
375
|
return dirs
|
|
356
376
|
}
|
|
357
377
|
|
|
378
|
+
/** Where a real package folder lives when only its files are links: Bun installs a `file:` folder
|
|
379
|
+
* dependency as a directory of per-file symlinks, so its imports resolve beside the linked files. */
|
|
380
|
+
const fileLinkedDir = (dir: string): string | undefined => {
|
|
381
|
+
const manifest = join(dir, "package.json")
|
|
382
|
+
try {
|
|
383
|
+
return lstatSync(manifest).isSymbolicLink() ? dirname(realpathOrSelf(manifest)) : undefined
|
|
384
|
+
} catch {
|
|
385
|
+
return undefined
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
358
389
|
/**
|
|
359
390
|
* Package directories that are symlinked OUT of this repository - the `link:` / `file:` / `npm link`
|
|
360
391
|
* consumers whose own imports resolve somewhere this install does not own. Those are the only
|
|
@@ -388,9 +419,8 @@ const linkedPackageDirs = (root: string): readonly string[] => {
|
|
|
388
419
|
} catch {
|
|
389
420
|
continue
|
|
390
421
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (inside(repo, target)) continue
|
|
422
|
+
const target = link.isSymbolicLink() ? realpathOrSelf(candidate) : fileLinkedDir(candidate)
|
|
423
|
+
if (target === undefined || inside(repo, target)) continue
|
|
394
424
|
found.add(target)
|
|
395
425
|
}
|
|
396
426
|
}
|
|
@@ -550,6 +580,7 @@ export function singleCopyPlugin(options: SingleCopyOptions = {}): SingleCopyPlu
|
|
|
550
580
|
if (alternatives.length > 0) {
|
|
551
581
|
const filter = new RegExp(`^(?:${alternatives.join("|")})$`)
|
|
552
582
|
build.onResolve({ filter }, (args) => {
|
|
583
|
+
if (reachesAppCopy(args.path, args.importer, plan.root)) return undefined
|
|
553
584
|
try {
|
|
554
585
|
return { path: Bun.resolveSync(args.path, plan.root) }
|
|
555
586
|
} catch {
|
package/src/wire.ts
CHANGED
|
@@ -43,6 +43,11 @@ export interface WireDecodeLimits {
|
|
|
43
43
|
readonly maxCollectionEntries?: number
|
|
44
44
|
/** Maximum UTF-8 string plus decoded binary bytes materialized. Default 16 MiB. */
|
|
45
45
|
readonly maxDecodedBytes?: number
|
|
46
|
+
/**
|
|
47
|
+
* Maximum decimal digits in one `bigint`. Default 4,096 (past 8192-bit integers). Parsing grows
|
|
48
|
+
* faster than the digit count, so one long token would cost seconds of CPU under the byte limit.
|
|
49
|
+
*/
|
|
50
|
+
readonly maxBigIntDigits?: number
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
export const DEFAULT_WIRE_DECODE_LIMITS: Readonly<Required<WireDecodeLimits>> = Object.freeze({
|
|
@@ -50,6 +55,7 @@ export const DEFAULT_WIRE_DECODE_LIMITS: Readonly<Required<WireDecodeLimits>> =
|
|
|
50
55
|
maxDepth: 256,
|
|
51
56
|
maxCollectionEntries: 100_000,
|
|
52
57
|
maxDecodedBytes: 16 * 1024 * 1024,
|
|
58
|
+
maxBigIntDigits: 4096,
|
|
53
59
|
})
|
|
54
60
|
|
|
55
61
|
/** Thrown by {@link encode} for a value it will not encode (a function or a symbol). */
|
|
@@ -223,6 +229,11 @@ export function decode(wire: Wire, limits: WireDecodeLimits = {}): unknown {
|
|
|
223
229
|
DEFAULT_WIRE_DECODE_LIMITS.maxDecodedBytes,
|
|
224
230
|
"maxDecodedBytes",
|
|
225
231
|
)
|
|
232
|
+
const maxBigIntDigits = decodeLimit(
|
|
233
|
+
limits.maxBigIntDigits,
|
|
234
|
+
DEFAULT_WIRE_DECODE_LIMITS.maxBigIntDigits,
|
|
235
|
+
"maxBigIntDigits",
|
|
236
|
+
)
|
|
226
237
|
if (nodes.length > maxNodes) throw new WireDecodeError("wire node limit exceeded")
|
|
227
238
|
const built = new Map<number, unknown>()
|
|
228
239
|
let collectionEntries = 0
|
|
@@ -279,6 +290,9 @@ export function decode(wire: Wire, limits: WireDecodeLimits = {}): unknown {
|
|
|
279
290
|
if (typeof record.v !== "string" || !/^-?(?:0|[1-9][0-9]*)$/u.test(record.v)) {
|
|
280
291
|
throw new WireDecodeError("malformed bigint token")
|
|
281
292
|
}
|
|
293
|
+
if (record.v.length - (record.v.charCodeAt(0) === 45 ? 1 : 0) > maxBigIntDigits) {
|
|
294
|
+
throw new WireDecodeError("wire bigint digit limit exceeded")
|
|
295
|
+
}
|
|
282
296
|
chargeString(record.v)
|
|
283
297
|
return BigInt(record.v)
|
|
284
298
|
}
|
package/src/ws.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
attachWebSocket,
|
|
26
26
|
handshakeUpgradeStep,
|
|
27
27
|
TopicRegistry,
|
|
28
|
+
webSocketSender,
|
|
28
29
|
wrapWebSocketMessageValidation,
|
|
29
30
|
} from "./server/websocket.ts"
|
|
30
31
|
import { createBunWsHandlers } from "./server/ws-bun.ts"
|
|
@@ -37,6 +38,7 @@ const WS_RUNTIME: WsRuntime = {
|
|
|
37
38
|
createTopics: () => new TopicRegistry(),
|
|
38
39
|
bunHandlers: createBunWsHandlers,
|
|
39
40
|
attach: attachWebSocket,
|
|
41
|
+
sender: webSocketSender,
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/** The install seam a server exposes so the `websocket()` plugin can hand it the runtime. */
|