@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.
Files changed (239) hide show
  1. package/README.md +6 -2
  2. package/dist/assurance.d.ts +86 -6
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +186 -95
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/capabilities.d.ts +8 -0
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +53 -1
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/durable-effect.js +1 -1
  11. package/dist/durable-effect.js.map +1 -1
  12. package/dist/durable-saga.d.ts +6 -0
  13. package/dist/durable-saga.d.ts.map +1 -1
  14. package/dist/durable-saga.js +83 -10
  15. package/dist/durable-saga.js.map +1 -1
  16. package/dist/edge-kit.d.ts +8 -3
  17. package/dist/edge-kit.d.ts.map +1 -1
  18. package/dist/edge-kit.js +7 -2
  19. package/dist/edge-kit.js.map +1 -1
  20. package/dist/effect-scope.d.ts.map +1 -1
  21. package/dist/effect-scope.js +16 -5
  22. package/dist/effect-scope.js.map +1 -1
  23. package/dist/errors.d.ts +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js.map +1 -1
  26. package/dist/evidence.d.ts +2 -0
  27. package/dist/evidence.d.ts.map +1 -1
  28. package/dist/evidence.js +9 -1
  29. package/dist/evidence.js.map +1 -1
  30. package/dist/index.d.ts +5 -4
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -3
  33. package/dist/index.js.map +1 -1
  34. package/dist/internal/durable-shared.d.ts +1 -2
  35. package/dist/internal/durable-shared.d.ts.map +1 -1
  36. package/dist/internal/durable-shared.js +25 -44
  37. package/dist/internal/durable-shared.js.map +1 -1
  38. package/dist/internal/durable-types.d.ts +3 -0
  39. package/dist/internal/durable-types.d.ts.map +1 -1
  40. package/dist/internal/hook-origins.d.ts +67 -0
  41. package/dist/internal/hook-origins.d.ts.map +1 -0
  42. package/dist/internal/hook-origins.js +86 -0
  43. package/dist/internal/hook-origins.js.map +1 -0
  44. package/dist/internal/route-assurance.d.ts +6 -3
  45. package/dist/internal/route-assurance.d.ts.map +1 -1
  46. package/dist/internal/route-assurance.js +29 -14
  47. package/dist/internal/route-assurance.js.map +1 -1
  48. package/dist/internal/route-compiler.d.ts +9 -0
  49. package/dist/internal/route-compiler.d.ts.map +1 -1
  50. package/dist/internal/route-compiler.js +95 -13
  51. package/dist/internal/route-compiler.js.map +1 -1
  52. package/dist/internal/route-program.d.ts +1 -1
  53. package/dist/internal/route-program.d.ts.map +1 -1
  54. package/dist/internal/route-program.js +59 -16
  55. package/dist/internal/route-program.js.map +1 -1
  56. package/dist/internal/same-origin.d.ts +11 -1
  57. package/dist/internal/same-origin.d.ts.map +1 -1
  58. package/dist/internal/same-origin.js +8 -0
  59. package/dist/internal/same-origin.js.map +1 -1
  60. package/dist/reconciliation-worker.d.ts.map +1 -1
  61. package/dist/reconciliation-worker.js +12 -2
  62. package/dist/reconciliation-worker.js.map +1 -1
  63. package/dist/reflection.d.ts +53 -2
  64. package/dist/reflection.d.ts.map +1 -1
  65. package/dist/reflection.js +352 -5
  66. package/dist/reflection.js.map +1 -1
  67. package/dist/router/pattern.d.ts +1 -1
  68. package/dist/router/pattern.d.ts.map +1 -1
  69. package/dist/router/pattern.js +6 -3
  70. package/dist/router/pattern.js.map +1 -1
  71. package/dist/security/baseline.d.ts +3 -0
  72. package/dist/security/baseline.d.ts.map +1 -1
  73. package/dist/security/baseline.js +9 -5
  74. package/dist/security/baseline.js.map +1 -1
  75. package/dist/server/auth.d.ts +32 -3
  76. package/dist/server/auth.d.ts.map +1 -1
  77. package/dist/server/auth.js +23 -0
  78. package/dist/server/auth.js.map +1 -1
  79. package/dist/server/body-lane.d.ts +8 -2
  80. package/dist/server/body-lane.d.ts.map +1 -1
  81. package/dist/server/body-lane.js +27 -2
  82. package/dist/server/body-lane.js.map +1 -1
  83. package/dist/server/body.d.ts +7 -0
  84. package/dist/server/body.d.ts.map +1 -1
  85. package/dist/server/body.js +34 -1
  86. package/dist/server/body.js.map +1 -1
  87. package/dist/server/client-ip.d.ts +3 -0
  88. package/dist/server/client-ip.d.ts.map +1 -1
  89. package/dist/server/client-ip.js +6 -5
  90. package/dist/server/client-ip.js.map +1 -1
  91. package/dist/server/context.d.ts +2 -1
  92. package/dist/server/context.d.ts.map +1 -1
  93. package/dist/server/cookies.d.ts.map +1 -1
  94. package/dist/server/cookies.js +13 -1
  95. package/dist/server/cookies.js.map +1 -1
  96. package/dist/server/edge.d.ts.map +1 -1
  97. package/dist/server/edge.js +2 -1
  98. package/dist/server/edge.js.map +1 -1
  99. package/dist/server/http.d.ts.map +1 -1
  100. package/dist/server/http.js +5 -1
  101. package/dist/server/http.js.map +1 -1
  102. package/dist/server/idempotency-lane.d.ts +6 -3
  103. package/dist/server/idempotency-lane.d.ts.map +1 -1
  104. package/dist/server/idempotency-lane.js +38 -17
  105. package/dist/server/idempotency-lane.js.map +1 -1
  106. package/dist/server/install.d.ts +2 -0
  107. package/dist/server/install.d.ts.map +1 -1
  108. package/dist/server/install.js +2 -0
  109. package/dist/server/install.js.map +1 -1
  110. package/dist/server/logger.d.ts.map +1 -1
  111. package/dist/server/logger.js +3 -1
  112. package/dist/server/logger.js.map +1 -1
  113. package/dist/server/methods.d.ts +1 -0
  114. package/dist/server/methods.d.ts.map +1 -1
  115. package/dist/server/methods.js +1 -0
  116. package/dist/server/methods.js.map +1 -1
  117. package/dist/server/node-outcome.d.ts.map +1 -1
  118. package/dist/server/node-outcome.js +29 -11
  119. package/dist/server/node-outcome.js.map +1 -1
  120. package/dist/server/platform-adapters.d.ts.map +1 -1
  121. package/dist/server/platform-adapters.js +3 -2
  122. package/dist/server/platform-adapters.js.map +1 -1
  123. package/dist/server/request-context.d.ts +2 -1
  124. package/dist/server/request-context.d.ts.map +1 -1
  125. package/dist/server/request-context.js +4 -1
  126. package/dist/server/request-context.js.map +1 -1
  127. package/dist/server/respond.d.ts +44 -11
  128. package/dist/server/respond.d.ts.map +1 -1
  129. package/dist/server/respond.js +192 -79
  130. package/dist/server/respond.js.map +1 -1
  131. package/dist/server/response-contract-lane.d.ts +21 -6
  132. package/dist/server/response-contract-lane.d.ts.map +1 -1
  133. package/dist/server/response-contract-lane.js +139 -14
  134. package/dist/server/response-contract-lane.js.map +1 -1
  135. package/dist/server/response-observer-runtime.d.ts.map +1 -1
  136. package/dist/server/response-observer-runtime.js +10 -41
  137. package/dist/server/response-observer-runtime.js.map +1 -1
  138. package/dist/server/runtime-core.d.ts +2 -0
  139. package/dist/server/runtime-core.d.ts.map +1 -1
  140. package/dist/server/runtime-core.js +2 -0
  141. package/dist/server/runtime-core.js.map +1 -1
  142. package/dist/server/server-types.d.ts +45 -10
  143. package/dist/server/server-types.d.ts.map +1 -1
  144. package/dist/server/server.d.ts +67 -32
  145. package/dist/server/server.d.ts.map +1 -1
  146. package/dist/server/server.js +481 -334
  147. package/dist/server/server.js.map +1 -1
  148. package/dist/server/sse.d.ts +11 -1
  149. package/dist/server/sse.d.ts.map +1 -1
  150. package/dist/server/sse.js +18 -3
  151. package/dist/server/sse.js.map +1 -1
  152. package/dist/server/transport-lane.d.ts +7 -1
  153. package/dist/server/transport-lane.d.ts.map +1 -1
  154. package/dist/server/transport-lane.js.map +1 -1
  155. package/dist/server/webhook.d.ts +5 -2
  156. package/dist/server/webhook.d.ts.map +1 -1
  157. package/dist/server/webhook.js +17 -1
  158. package/dist/server/webhook.js.map +1 -1
  159. package/dist/server/websocket.d.ts +33 -14
  160. package/dist/server/websocket.d.ts.map +1 -1
  161. package/dist/server/websocket.js +47 -12
  162. package/dist/server/websocket.js.map +1 -1
  163. package/dist/server/ws-bun.d.ts +7 -1
  164. package/dist/server/ws-bun.d.ts.map +1 -1
  165. package/dist/server/ws-bun.js +42 -2
  166. package/dist/server/ws-bun.js.map +1 -1
  167. package/dist/server/ws-hook.d.ts +15 -3
  168. package/dist/server/ws-hook.d.ts.map +1 -1
  169. package/dist/server.d.ts +4 -3
  170. package/dist/server.d.ts.map +1 -1
  171. package/dist/server.js +3 -2
  172. package/dist/server.js.map +1 -1
  173. package/dist/single-copy.d.ts +6 -0
  174. package/dist/single-copy.d.ts.map +1 -1
  175. package/dist/single-copy.js +55 -6
  176. package/dist/single-copy.js.map +1 -1
  177. package/dist/tool-contract.d.ts.map +1 -1
  178. package/dist/tool-contract.js +71 -40
  179. package/dist/tool-contract.js.map +1 -1
  180. package/dist/wire.d.ts +5 -0
  181. package/dist/wire.d.ts.map +1 -1
  182. package/dist/wire.js +5 -0
  183. package/dist/wire.js.map +1 -1
  184. package/dist/ws.d.ts.map +1 -1
  185. package/dist/ws.js +4 -1
  186. package/dist/ws.js.map +1 -1
  187. package/package.json +1 -1
  188. package/src/assurance.ts +299 -111
  189. package/src/capabilities.ts +62 -1
  190. package/src/durable-effect.ts +1 -1
  191. package/src/durable-saga.ts +94 -10
  192. package/src/edge-kit.ts +8 -3
  193. package/src/effect-scope.ts +15 -5
  194. package/src/errors.ts +1 -0
  195. package/src/evidence.ts +12 -3
  196. package/src/index.ts +7 -2
  197. package/src/internal/durable-shared.ts +25 -37
  198. package/src/internal/durable-types.ts +3 -0
  199. package/src/internal/hook-origins.ts +153 -0
  200. package/src/internal/route-assurance.ts +32 -18
  201. package/src/internal/route-compiler.ts +107 -16
  202. package/src/internal/route-program.ts +98 -44
  203. package/src/internal/same-origin.ts +14 -1
  204. package/src/reconciliation-worker.ts +10 -2
  205. package/src/reflection.ts +455 -4
  206. package/src/router/pattern.ts +6 -3
  207. package/src/security/baseline.ts +10 -5
  208. package/src/server/auth.ts +45 -3
  209. package/src/server/body-lane.ts +36 -1
  210. package/src/server/body.ts +34 -1
  211. package/src/server/client-ip.ts +8 -5
  212. package/src/server/context.ts +2 -1
  213. package/src/server/cookies.ts +12 -1
  214. package/src/server/edge.ts +2 -1
  215. package/src/server/http.ts +7 -1
  216. package/src/server/idempotency-lane.ts +64 -22
  217. package/src/server/install.ts +3 -0
  218. package/src/server/logger.ts +3 -1
  219. package/src/server/methods.ts +1 -0
  220. package/src/server/node-outcome.ts +34 -12
  221. package/src/server/platform-adapters.ts +3 -2
  222. package/src/server/request-context.ts +5 -0
  223. package/src/server/respond.ts +222 -69
  224. package/src/server/response-contract-lane.ts +162 -10
  225. package/src/server/response-observer-runtime.ts +16 -38
  226. package/src/server/runtime-core.ts +2 -0
  227. package/src/server/server-types.ts +45 -10
  228. package/src/server/server.ts +707 -539
  229. package/src/server/sse.ts +84 -55
  230. package/src/server/transport-lane.ts +7 -1
  231. package/src/server/webhook.ts +22 -3
  232. package/src/server/websocket.ts +83 -18
  233. package/src/server/ws-bun.ts +48 -1
  234. package/src/server/ws-hook.ts +14 -2
  235. package/src/server.ts +6 -1
  236. package/src/single-copy.ts +55 -5
  237. package/src/tool-contract.ts +67 -39
  238. package/src/wire.ts +14 -0
  239. package/src/ws.ts +6 -0
@@ -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
- if (!link.isSymbolicLink()) continue
384
- const target = realpathOrSelf(candidate)
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: "js" }
622
+ return { contents: readFileSync(args.path, "utf8"), loader: sourceLoader(args.path) }
573
623
  })
574
624
  },
575
625
  }
@@ -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
- if (reservation !== undefined)
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
- if (reservation !== undefined)
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
- if (reservation !== undefined)
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 (reservation !== undefined) {
660
- const completed = await options.idempotency?.complete(reservationInput(reservation))
661
- if (completed !== true) {
662
- record("idempotency", "failed", "idempotency_capacity")
663
- return finish({
664
- ok: false,
665
- dryRun,
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 (reservation !== undefined) {
679
- const completed = await options.idempotency?.complete(reservationInput(reservation))
680
- if (completed !== true) {
681
- record("idempotency", "failed", "idempotency_capacity")
682
- return finish({
683
- ok: false,
684
- dryRun,
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 (reservation !== undefined) await options.idempotency?.abandon(reservationInput(reservation))
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 status = result.ok
792
- ? 200
793
- : result.error.code === "input_invalid"
794
- ? 422
795
- : result.error.code === "approval_required" ||
796
- result.error.code === "approval_denied" ||
797
- result.error.code === "capability_denied"
798
- ? 403
799
- : result.error.code === "budget_exceeded"
800
- ? 429
801
- : 500
802
- return new Response(body, { status, headers: { "content-type": "application/json" } })
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. */