@nifrajs/core 4.0.2 → 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/README.md +6 -2
- package/dist/assurance.d.ts +86 -6
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +186 -95
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +8 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +53 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/durable-effect.js +1 -1
- package/dist/durable-effect.js.map +1 -1
- package/dist/durable-saga.d.ts +6 -0
- package/dist/durable-saga.d.ts.map +1 -1
- package/dist/durable-saga.js +83 -10
- package/dist/durable-saga.js.map +1 -1
- package/dist/edge-kit.d.ts +8 -3
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +7 -2
- package/dist/edge-kit.js.map +1 -1
- package/dist/effect-scope.d.ts.map +1 -1
- package/dist/effect-scope.js +16 -5
- package/dist/effect-scope.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +9 -1
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-shared.d.ts +1 -2
- package/dist/internal/durable-shared.d.ts.map +1 -1
- package/dist/internal/durable-shared.js +25 -44
- package/dist/internal/durable-shared.js.map +1 -1
- package/dist/internal/durable-types.d.ts +3 -0
- package/dist/internal/durable-types.d.ts.map +1 -1
- package/dist/internal/hook-origins.d.ts +67 -0
- package/dist/internal/hook-origins.d.ts.map +1 -0
- package/dist/internal/hook-origins.js +86 -0
- package/dist/internal/hook-origins.js.map +1 -0
- package/dist/internal/route-assurance.d.ts +6 -3
- package/dist/internal/route-assurance.d.ts.map +1 -1
- package/dist/internal/route-assurance.js +29 -14
- package/dist/internal/route-assurance.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/route-program.d.ts +1 -1
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +59 -16
- package/dist/internal/route-program.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/reconciliation-worker.d.ts.map +1 -1
- package/dist/reconciliation-worker.js +12 -2
- package/dist/reconciliation-worker.js.map +1 -1
- package/dist/reflection.d.ts +53 -2
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +352 -5
- package/dist/reflection.js.map +1 -1
- package/dist/router/pattern.d.ts +1 -1
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +6 -3
- package/dist/router/pattern.js.map +1 -1
- package/dist/security/baseline.d.ts +3 -0
- package/dist/security/baseline.d.ts.map +1 -1
- package/dist/security/baseline.js +9 -5
- package/dist/security/baseline.js.map +1 -1
- package/dist/server/auth.d.ts +32 -3
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +23 -0
- package/dist/server/auth.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/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +34 -1
- package/dist/server/body.js.map +1 -1
- package/dist/server/client-ip.d.ts +3 -0
- package/dist/server/client-ip.d.ts.map +1 -1
- package/dist/server/client-ip.js +6 -5
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +2 -1
- package/dist/server/context.d.ts.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/idempotency-lane.d.ts +6 -3
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +38 -17
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +3 -1
- package/dist/server/logger.js.map +1 -1
- package/dist/server/methods.d.ts +1 -0
- package/dist/server/methods.d.ts.map +1 -1
- package/dist/server/methods.js +1 -0
- package/dist/server/methods.js.map +1 -1
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +29 -11
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/platform-adapters.d.ts.map +1 -1
- package/dist/server/platform-adapters.js +3 -2
- package/dist/server/platform-adapters.js.map +1 -1
- package/dist/server/request-context.d.ts +2 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +4 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +44 -11
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +192 -79
- package/dist/server/respond.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/response-observer-runtime.d.ts.map +1 -1
- package/dist/server/response-observer-runtime.js +10 -41
- package/dist/server/response-observer-runtime.js.map +1 -1
- package/dist/server/runtime-core.d.ts +2 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +2 -0
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +45 -10
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +67 -32
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +481 -334
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +11 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +18 -3
- package/dist/server/sse.js.map +1 -1
- package/dist/server/transport-lane.d.ts +7 -1
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/webhook.d.ts +5 -2
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +17 -1
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +33 -14
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +47 -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 +15 -3
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +4 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +6 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +55 -6
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +71 -40
- package/dist/tool-contract.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 +4 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/assurance.ts +299 -111
- package/src/capabilities.ts +62 -1
- package/src/durable-effect.ts +1 -1
- package/src/durable-saga.ts +94 -10
- package/src/edge-kit.ts +8 -3
- package/src/effect-scope.ts +15 -5
- package/src/errors.ts +1 -0
- package/src/evidence.ts +12 -3
- package/src/index.ts +7 -2
- package/src/internal/durable-shared.ts +25 -37
- package/src/internal/durable-types.ts +3 -0
- package/src/internal/hook-origins.ts +153 -0
- package/src/internal/route-assurance.ts +32 -18
- package/src/internal/route-compiler.ts +107 -16
- package/src/internal/route-program.ts +98 -44
- package/src/internal/same-origin.ts +14 -1
- package/src/reconciliation-worker.ts +10 -2
- package/src/reflection.ts +455 -4
- package/src/router/pattern.ts +6 -3
- package/src/security/baseline.ts +10 -5
- package/src/server/auth.ts +45 -3
- package/src/server/body-lane.ts +36 -1
- package/src/server/body.ts +34 -1
- package/src/server/client-ip.ts +8 -5
- package/src/server/context.ts +2 -1
- package/src/server/cookies.ts +12 -1
- package/src/server/edge.ts +2 -1
- package/src/server/http.ts +7 -1
- package/src/server/idempotency-lane.ts +64 -22
- package/src/server/install.ts +3 -0
- package/src/server/logger.ts +3 -1
- package/src/server/methods.ts +1 -0
- package/src/server/node-outcome.ts +34 -12
- package/src/server/platform-adapters.ts +3 -2
- package/src/server/request-context.ts +5 -0
- package/src/server/respond.ts +222 -69
- package/src/server/response-contract-lane.ts +162 -10
- package/src/server/response-observer-runtime.ts +16 -38
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +45 -10
- package/src/server/server.ts +707 -539
- package/src/server/sse.ts +84 -55
- package/src/server/transport-lane.ts +7 -1
- package/src/server/webhook.ts +22 -3
- package/src/server/websocket.ts +83 -18
- package/src/server/ws-bun.ts +48 -1
- package/src/server/ws-hook.ts +14 -2
- package/src/server.ts +6 -1
- package/src/single-copy.ts +55 -5
- package/src/tool-contract.ts +67 -39
- package/src/wire.ts +14 -0
- package/src/ws.ts +6 -0
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
|
|
|
@@ -77,6 +77,14 @@ export const IDENTITY_SENSITIVE_PACKAGES: readonly string[] = [
|
|
|
77
77
|
|
|
78
78
|
/** Files a redirect may rewrite. Everything else in a package (json, css, wasm) loads unchanged. */
|
|
79
79
|
const REDIRECTABLE = /\.(?:m|c)?[jt]sx?$/
|
|
80
|
+
|
|
81
|
+
/** The loader a {@link REDIRECTABLE} file is parsed with. `.ts` is never `tsx`: under JSX rules a
|
|
82
|
+
* generic arrow (`<T>(x: T) => x`) reads as an unclosed element and swallows the file. */
|
|
83
|
+
function sourceLoader(path: string): "js" | "jsx" | "ts" | "tsx" {
|
|
84
|
+
if (path.endsWith(".tsx")) return "tsx"
|
|
85
|
+
if (path.endsWith(".jsx")) return "jsx"
|
|
86
|
+
return /\.[mc]?ts$/.test(path) ? "ts" : "js"
|
|
87
|
+
}
|
|
80
88
|
/** Runaway guards. A real `node_modules` top level is hundreds of entries, a repo walk is a few. */
|
|
81
89
|
const MAX_SCANNED_ENTRIES = 4_096
|
|
82
90
|
const MAX_ANCESTOR_DIRS = 16
|
|
@@ -331,6 +339,26 @@ const installedCopy = (
|
|
|
331
339
|
}
|
|
332
340
|
}
|
|
333
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
|
+
|
|
334
362
|
/** Every `node_modules` from the app root up to (and including) its repository root. */
|
|
335
363
|
const nodeModulesDirs = (root: string): readonly string[] => {
|
|
336
364
|
const boundary = repoBoundary(root)
|
|
@@ -347,6 +375,17 @@ const nodeModulesDirs = (root: string): readonly string[] => {
|
|
|
347
375
|
return dirs
|
|
348
376
|
}
|
|
349
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
|
+
|
|
350
389
|
/**
|
|
351
390
|
* Package directories that are symlinked OUT of this repository - the `link:` / `file:` / `npm link`
|
|
352
391
|
* consumers whose own imports resolve somewhere this install does not own. Those are the only
|
|
@@ -380,9 +419,8 @@ const linkedPackageDirs = (root: string): readonly string[] => {
|
|
|
380
419
|
} catch {
|
|
381
420
|
continue
|
|
382
421
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (inside(repo, target)) continue
|
|
422
|
+
const target = link.isSymbolicLink() ? realpathOrSelf(candidate) : fileLinkedDir(candidate)
|
|
423
|
+
if (target === undefined || inside(repo, target)) continue
|
|
386
424
|
found.add(target)
|
|
387
425
|
}
|
|
388
426
|
}
|
|
@@ -411,6 +449,17 @@ const declaredNames = (root: string, declared: readonly string[]): readonly stri
|
|
|
411
449
|
return [...names].sort()
|
|
412
450
|
}
|
|
413
451
|
|
|
452
|
+
/**
|
|
453
|
+
* The installed package names the app's declaration covers: each exact name, and each package
|
|
454
|
+
* installed under a declared `@scope/*`. Empty without a declaration. A bundler that pins packages by
|
|
455
|
+
* name, such as Vite's `resolve.dedupe`, takes this list.
|
|
456
|
+
*/
|
|
457
|
+
export function singleCopyPackageNames(cwd: string): readonly string[] {
|
|
458
|
+
const root = realpathOrSelf(resolve(cwd))
|
|
459
|
+
const declared = readSingleCopyDeclaration(root)
|
|
460
|
+
return declared === undefined ? [] : declaredNames(root, declared)
|
|
461
|
+
}
|
|
462
|
+
|
|
414
463
|
/**
|
|
415
464
|
* Work out which foreign copies exist and which of them may be redirected.
|
|
416
465
|
*
|
|
@@ -531,6 +580,7 @@ export function singleCopyPlugin(options: SingleCopyOptions = {}): SingleCopyPlu
|
|
|
531
580
|
if (alternatives.length > 0) {
|
|
532
581
|
const filter = new RegExp(`^(?:${alternatives.join("|")})$`)
|
|
533
582
|
build.onResolve({ filter }, (args) => {
|
|
583
|
+
if (reachesAppCopy(args.path, args.importer, plan.root)) return undefined
|
|
534
584
|
try {
|
|
535
585
|
return { path: Bun.resolveSync(args.path, plan.root) }
|
|
536
586
|
} catch {
|
|
@@ -569,7 +619,7 @@ export function singleCopyPlugin(options: SingleCopyOptions = {}): SingleCopyPlu
|
|
|
569
619
|
// A runtime `onLoad` must always return an object - returning nothing fails the load rather
|
|
570
620
|
// than falling through to the default. Hand back the file as it was, at its own path, so its
|
|
571
621
|
// relative imports keep resolving exactly as they would have.
|
|
572
|
-
return { contents: readFileSync(args.path, "utf8"), loader:
|
|
622
|
+
return { contents: readFileSync(args.path, "utf8"), loader: sourceLoader(args.path) }
|
|
573
623
|
})
|
|
574
624
|
},
|
|
575
625
|
}
|
package/src/tool-contract.ts
CHANGED
|
@@ -461,6 +461,25 @@ export async function executeTool<Input, Output>(
|
|
|
461
461
|
...(tool.cost === undefined ? {} : { cost: tool.cost }),
|
|
462
462
|
})
|
|
463
463
|
}
|
|
464
|
+
// A failed abandon leaves the key reserved until its lease lapses, as a failed renewal does; the
|
|
465
|
+
// call's own outcome still stands.
|
|
466
|
+
const abandon = async (): Promise<void> => {
|
|
467
|
+
if (reservation === undefined) return
|
|
468
|
+
try {
|
|
469
|
+
await options.idempotency?.abandon(reservationInput(reservation))
|
|
470
|
+
} catch {}
|
|
471
|
+
}
|
|
472
|
+
// Once the executor has run, the reservation is never released: a retry would repeat a side effect
|
|
473
|
+
// that may have committed. A store that throws on completion counts as one that refused it.
|
|
474
|
+
let executed = false
|
|
475
|
+
const complete = async (): Promise<boolean> => {
|
|
476
|
+
if (reservation === undefined) return true
|
|
477
|
+
try {
|
|
478
|
+
return (await options.idempotency?.complete(reservationInput(reservation))) === true
|
|
479
|
+
} catch {
|
|
480
|
+
return false
|
|
481
|
+
}
|
|
482
|
+
}
|
|
464
483
|
type ToolCallOutcome =
|
|
465
484
|
| { readonly ok: true; readonly dryRun: boolean; readonly output?: Output }
|
|
466
485
|
| { readonly ok: false; readonly dryRun: boolean; readonly error: ToolError }
|
|
@@ -625,22 +644,19 @@ export async function executeTool<Input, Output>(
|
|
|
625
644
|
record("idempotency", tool.idempotency === undefined ? "skipped" : "passed")
|
|
626
645
|
|
|
627
646
|
if (options.budget !== undefined && !options.budget.consume(tool.cost)) {
|
|
628
|
-
|
|
629
|
-
await options.idempotency?.abandon(reservationInput(reservation))
|
|
647
|
+
await abandon()
|
|
630
648
|
record("budget", "denied", "budget_exceeded")
|
|
631
649
|
return finish({ ok: false, dryRun, error: { code: "budget_exceeded", stage: "budget" } })
|
|
632
650
|
}
|
|
633
651
|
record("budget", tool.cost === undefined ? "skipped" : "passed")
|
|
634
652
|
|
|
635
653
|
if (signal.aborted) {
|
|
636
|
-
|
|
637
|
-
await options.idempotency?.abandon(reservationInput(reservation))
|
|
654
|
+
await abandon()
|
|
638
655
|
record("execution", "denied", "cancelled")
|
|
639
656
|
return finish({ ok: false, dryRun, error: { code: "cancelled", stage: "execution" } })
|
|
640
657
|
}
|
|
641
658
|
if (dryRun) {
|
|
642
|
-
|
|
643
|
-
await options.idempotency?.abandon(reservationInput(reservation))
|
|
659
|
+
await abandon()
|
|
644
660
|
record("execution", "dry-run")
|
|
645
661
|
return finish({ ok: true, dryRun })
|
|
646
662
|
}
|
|
@@ -651,21 +667,19 @@ export async function executeTool<Input, Output>(
|
|
|
651
667
|
dryRun,
|
|
652
668
|
...(tool.policy === undefined ? {} : { policy: tool.policy }),
|
|
653
669
|
})
|
|
670
|
+
executed = true
|
|
654
671
|
record("execution", "committed")
|
|
655
672
|
const validatedOutput = await validateStandard(tool.output, output)
|
|
656
673
|
if (!validatedOutput.ok) {
|
|
657
674
|
// The executor has already run. Keep the reservation completed even when its return shape is
|
|
658
675
|
// invalid, so a caller cannot retry and duplicate a side effect that may have committed.
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
error: { code: "idempotency_capacity", stage: "idempotency" },
|
|
667
|
-
})
|
|
668
|
-
}
|
|
676
|
+
if (!(await complete())) {
|
|
677
|
+
record("idempotency", "failed", "idempotency_capacity")
|
|
678
|
+
return finish({
|
|
679
|
+
ok: false,
|
|
680
|
+
dryRun,
|
|
681
|
+
error: { code: "idempotency_capacity", stage: "idempotency" },
|
|
682
|
+
})
|
|
669
683
|
}
|
|
670
684
|
record("output", "failed", "output_invalid")
|
|
671
685
|
return finish({
|
|
@@ -675,20 +689,17 @@ export async function executeTool<Input, Output>(
|
|
|
675
689
|
})
|
|
676
690
|
}
|
|
677
691
|
record("output", "passed")
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
error: { code: "idempotency_capacity", stage: "idempotency" },
|
|
686
|
-
})
|
|
687
|
-
}
|
|
692
|
+
if (!(await complete())) {
|
|
693
|
+
record("idempotency", "failed", "idempotency_capacity")
|
|
694
|
+
return finish({
|
|
695
|
+
ok: false,
|
|
696
|
+
dryRun,
|
|
697
|
+
error: { code: "idempotency_capacity", stage: "idempotency" },
|
|
698
|
+
})
|
|
688
699
|
}
|
|
689
700
|
return finish({ ok: true, dryRun, output: validatedOutput.value })
|
|
690
701
|
} catch (error) {
|
|
691
|
-
if (
|
|
702
|
+
if (!executed) await abandon()
|
|
692
703
|
const code = signal.aborted || isAbortError(error) ? "cancelled" : "execution_failed"
|
|
693
704
|
record("execution", "failed", code)
|
|
694
705
|
return finish({ ok: false, dryRun, error: { code, stage: "execution" } })
|
|
@@ -788,18 +799,35 @@ export function toolHttpResult<Output>(result: ToolCallResult<Output>): Response
|
|
|
788
799
|
? { ok: true, output: result.output, dryRun: result.dryRun, evidence: result.evidence }
|
|
789
800
|
: { ok: false, error: result.error, dryRun: result.dryRun, evidence: result.evidence },
|
|
790
801
|
)
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
802
|
+
const headers: Record<string, string> = { "content-type": "application/json" }
|
|
803
|
+
const status = result.ok ? 200 : toolErrorStatus(result.error.code)
|
|
804
|
+
// The idempotency refusals answer as the HTTP idempotency lane does, a retry included.
|
|
805
|
+
if (!result.ok && retryableIdempotency(result.error.code)) headers["retry-after"] = "1"
|
|
806
|
+
return new Response(body, { status, headers })
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function retryableIdempotency(code: ToolError["code"]): boolean {
|
|
810
|
+
return code === "idempotency_in_flight" || code === "idempotency_capacity"
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function toolErrorStatus(code: ToolError["code"]): number {
|
|
814
|
+
switch (code) {
|
|
815
|
+
case "input_invalid":
|
|
816
|
+
return 422
|
|
817
|
+
case "approval_required":
|
|
818
|
+
case "approval_denied":
|
|
819
|
+
case "capability_denied":
|
|
820
|
+
return 403
|
|
821
|
+
case "idempotency_duplicate":
|
|
822
|
+
case "idempotency_in_flight":
|
|
823
|
+
return 409
|
|
824
|
+
case "budget_exceeded":
|
|
825
|
+
return 429
|
|
826
|
+
case "idempotency_capacity":
|
|
827
|
+
return 503
|
|
828
|
+
default:
|
|
829
|
+
return 500
|
|
830
|
+
}
|
|
803
831
|
}
|
|
804
832
|
|
|
805
833
|
export function toolInputJsonSchema<Input, Output>(
|
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
|
@@ -21,8 +21,11 @@ import { INSTALL_WS } from "./server/install.ts"
|
|
|
21
21
|
import type { IdentityPlugin } from "./server/plugin.ts"
|
|
22
22
|
import type { AnyServer } from "./server/server.ts"
|
|
23
23
|
import {
|
|
24
|
+
acceptedHandshake,
|
|
24
25
|
attachWebSocket,
|
|
26
|
+
handshakeUpgradeStep,
|
|
25
27
|
TopicRegistry,
|
|
28
|
+
webSocketSender,
|
|
26
29
|
wrapWebSocketMessageValidation,
|
|
27
30
|
} from "./server/websocket.ts"
|
|
28
31
|
import { createBunWsHandlers } from "./server/ws-bun.ts"
|
|
@@ -30,9 +33,12 @@ import type { WsRuntime } from "./server/ws-hook.ts"
|
|
|
30
33
|
|
|
31
34
|
const WS_RUNTIME: WsRuntime = {
|
|
32
35
|
wrapHandler: wrapWebSocketMessageValidation,
|
|
36
|
+
upgradeStep: handshakeUpgradeStep,
|
|
37
|
+
accepted: acceptedHandshake,
|
|
33
38
|
createTopics: () => new TopicRegistry(),
|
|
34
39
|
bunHandlers: createBunWsHandlers,
|
|
35
40
|
attach: attachWebSocket,
|
|
41
|
+
sender: webSocketSender,
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/** The install seam a server exposes so the `websocket()` plugin can hand it the runtime. */
|