@abide/abide 0.42.0 → 0.43.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 (146) hide show
  1. package/AGENTS.md +234 -300
  2. package/CHANGELOG.md +34 -0
  3. package/README.md +50 -93
  4. package/package.json +3 -2
  5. package/src/abideResolverPlugin.ts +104 -14
  6. package/src/buildCli.ts +1 -1
  7. package/src/discoveryEntry.ts +3 -3
  8. package/src/lib/bundle/BundleMenuItem.ts +1 -1
  9. package/src/lib/cli/createClient.ts +12 -12
  10. package/src/lib/cli/dispatchCommand.ts +1 -1
  11. package/src/lib/cli/printSessionStatus.ts +1 -1
  12. package/src/lib/cli/resolveCliTarget.ts +1 -1
  13. package/src/lib/cli/runCli.ts +1 -1
  14. package/src/lib/cli/types/CliManifest.ts +1 -1
  15. package/src/lib/cli/types/CliManifestEntry.ts +2 -2
  16. package/src/lib/mcp/annotationsForMethod.ts +5 -5
  17. package/src/lib/mcp/createMcpServer.ts +3 -3
  18. package/src/lib/mcp/mcpSurface.ts +14 -14
  19. package/src/lib/mcp/types/McpServerOptions.ts +1 -1
  20. package/src/lib/server/DELETE.ts +4 -4
  21. package/src/lib/server/GET.ts +4 -4
  22. package/src/lib/server/HEAD.ts +4 -4
  23. package/src/lib/server/PATCH.ts +4 -4
  24. package/src/lib/server/POST.ts +4 -4
  25. package/src/lib/server/PUT.ts +4 -4
  26. package/src/lib/server/agent.ts +4 -4
  27. package/src/lib/server/env.ts +1 -1
  28. package/src/lib/server/error.ts +49 -7
  29. package/src/lib/server/json.ts +1 -1
  30. package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
  31. package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
  32. package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
  33. package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
  34. package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
  35. package/src/lib/server/rpc/parseArgs.ts +6 -6
  36. package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
  37. package/src/lib/server/rpc/registerRpc.ts +6 -0
  38. package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
  39. package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
  40. package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
  41. package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
  42. package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
  43. package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
  44. package/src/lib/server/rpc/unprocessed.ts +6 -7
  45. package/src/lib/server/rpc/validationError.ts +17 -0
  46. package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
  47. package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
  48. package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
  49. package/src/lib/server/runtime/createServer.ts +4 -4
  50. package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
  51. package/src/lib/server/runtime/crossOriginGate.ts +4 -4
  52. package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
  53. package/src/lib/server/runtime/registryManifests.ts +2 -2
  54. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  55. package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
  56. package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
  57. package/src/lib/server/runtime/types/RequestStore.ts +1 -1
  58. package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
  59. package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
  60. package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
  61. package/src/lib/shared/HttpError.ts +15 -1
  62. package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
  63. package/src/lib/shared/buildRpcRequest.ts +5 -5
  64. package/src/lib/shared/cache.ts +50 -168
  65. package/src/lib/shared/carriesBodyArgs.ts +4 -4
  66. package/src/lib/shared/createCacheStore.ts +12 -0
  67. package/src/lib/shared/createRemoteFunction.ts +11 -6
  68. package/src/lib/shared/decodeResponse.ts +2 -2
  69. package/src/lib/shared/detectRpcMethod.ts +17 -0
  70. package/src/lib/shared/emitLogRecord.ts +3 -3
  71. package/src/lib/shared/extraForwardHeaders.ts +1 -1
  72. package/src/lib/shared/findExportCallSite.ts +30 -273
  73. package/src/lib/shared/forwardHeaders.ts +2 -2
  74. package/src/lib/shared/httpErrorFor.ts +26 -0
  75. package/src/lib/shared/isAsciiWhitespace.ts +5 -0
  76. package/src/lib/shared/isIdentPart.ts +9 -0
  77. package/src/lib/shared/isIdentStart.ts +8 -0
  78. package/src/lib/shared/isReadOnlyMethod.ts +4 -4
  79. package/src/lib/shared/keyForRemoteCall.ts +4 -4
  80. package/src/lib/shared/log.ts +1 -1
  81. package/src/lib/shared/outboxProbeSlot.ts +20 -0
  82. package/src/lib/shared/pending.ts +4 -1
  83. package/src/lib/shared/prepareRpcModule.ts +96 -14
  84. package/src/lib/shared/probeRegistries.ts +17 -4
  85. package/src/lib/shared/queryStringFromArgs.ts +1 -1
  86. package/src/lib/shared/remoteMetaStore.ts +2 -2
  87. package/src/lib/shared/resolveClientFlags.ts +1 -1
  88. package/src/lib/shared/skipNonCode.ts +230 -0
  89. package/src/lib/shared/streamResponse.ts +9 -6
  90. package/src/lib/shared/stripImport.ts +3 -3
  91. package/src/lib/shared/types/CacheEntry.ts +2 -4
  92. package/src/lib/shared/types/CacheOnContext.ts +1 -11
  93. package/src/lib/shared/types/CacheStore.ts +13 -7
  94. package/src/lib/shared/types/ClientFlags.ts +1 -1
  95. package/src/lib/shared/types/ErrorConstructors.ts +17 -0
  96. package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
  97. package/src/lib/shared/types/ErrorSpec.ts +11 -0
  98. package/src/lib/shared/types/HttpMethod.ts +1 -0
  99. package/src/lib/shared/types/Outbox.ts +9 -0
  100. package/src/lib/shared/types/OutboxEntry.ts +27 -0
  101. package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
  102. package/src/lib/shared/types/RemoteCallable.ts +2 -2
  103. package/src/lib/shared/types/RemoteFunction.ts +21 -6
  104. package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
  105. package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
  106. package/src/lib/shared/types/RpcInvoker.ts +1 -1
  107. package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
  108. package/src/lib/shared/types/ValidationErrorData.ts +20 -0
  109. package/src/lib/shared/url.ts +3 -3
  110. package/src/lib/shared/writeRpcDts.ts +7 -7
  111. package/src/lib/shared/writeTestRpcDts.ts +3 -3
  112. package/src/lib/test/createTestApp.ts +10 -10
  113. package/src/lib/ui/compile/awaitPlan.ts +48 -0
  114. package/src/lib/ui/compile/compileShadow.ts +75 -21
  115. package/src/lib/ui/compile/desugarSignals.ts +42 -6
  116. package/src/lib/ui/compile/generateBuild.ts +43 -60
  117. package/src/lib/ui/compile/generateSSR.ts +84 -54
  118. package/src/lib/ui/compile/ifPlan.ts +30 -0
  119. package/src/lib/ui/compile/parseTemplate.ts +41 -11
  120. package/src/lib/ui/compile/stripEffects.ts +17 -9
  121. package/src/lib/ui/compile/switchPlan.ts +22 -0
  122. package/src/lib/ui/compile/tryPlan.ts +29 -0
  123. package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
  124. package/src/lib/ui/compile/types/TemplateNode.ts +6 -2
  125. package/src/lib/ui/createScope.ts +20 -2
  126. package/src/lib/ui/dom/disposeRange.ts +6 -10
  127. package/src/lib/ui/dom/hydrate.ts +2 -5
  128. package/src/lib/ui/dom/mount.ts +1 -2
  129. package/src/lib/ui/dom/withScope.ts +6 -9
  130. package/src/lib/ui/effect.ts +4 -2
  131. package/src/lib/ui/navigate.ts +40 -13
  132. package/src/lib/ui/outbox.ts +30 -110
  133. package/src/lib/ui/remoteProxy.ts +160 -10
  134. package/src/lib/ui/router.ts +3 -3
  135. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
  136. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
  137. package/src/lib/ui/socketChannel.ts +7 -1
  138. package/src/lib/ui/types/Scope.ts +20 -5
  139. package/template/CLAUDE.md +1 -1
  140. package/template/src/server/rpc/getHello.ts +3 -3
  141. package/template/test/app.test.ts +2 -2
  142. package/src/lib/server/rpc/registerVerb.ts +0 -6
  143. package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
  144. package/src/lib/shared/detectVerbMethod.ts +0 -17
  145. package/src/lib/shared/types/HttpVerb.ts +0 -1
  146. package/src/lib/ui/types/Outbox.ts +0 -14
@@ -67,7 +67,7 @@ a bare word runs a command:
67
67
  /disconnect → forget the saved connection, exit
68
68
  <cmd> [--flags] → one-shot RPC against the resumed target
69
69
 
70
- The connection verbs are `/`-prefixed only — no bare aliases — so a bare word is
70
+ The connection rpcs are `/`-prefixed only — no bare aliases — so a bare word is
71
71
  always an RPC command and never collides. Env layers ABIDE_APP_URL/ABIDE_APP_TOKEN (shell >
72
72
  data-dir > binary-dir) supply the baked default a fresh download resumes against.
73
73
  */
@@ -2,7 +2,7 @@ import type { CliManifestEntry } from './CliManifestEntry.ts'
2
2
 
3
3
  /*
4
4
  Map from rpc export-name (e.g. "getReport") to its manifest entry. Built
5
- by the bundler from the same verbRegistry MCP consumes; entries are
5
+ by the bundler from the same rpcRegistry MCP consumes; entries are
6
6
  emitted only for rpcs with `clients.cli: true`. The CLI binary and any
7
7
  programmatic createClient caller read this to dispatch calls.
8
8
  */
@@ -1,4 +1,4 @@
1
- import type { HttpVerb } from '../../shared/types/HttpVerb.ts'
1
+ import type { HttpMethod } from '../../shared/types/HttpMethod.ts'
2
2
 
3
3
  /*
4
4
  Per-command manifest entry baked into the standalone CLI binary by the
@@ -9,7 +9,7 @@ both rpcs and socket commands — a socket `tail` is a GET against
9
9
  streams it live; a socket `publish` is a POST to the same path.
10
10
  */
11
11
  export type CliManifestEntry = {
12
- method: HttpVerb
12
+ method: HttpMethod
13
13
  url: string
14
14
  jsonSchema?: Record<string, unknown>
15
15
  // Request Accept header. Socket tail sets text/event-stream to stream live frames.
@@ -1,8 +1,8 @@
1
- import type { HttpVerb } from '../shared/types/HttpVerb.ts'
1
+ import type { HttpMethod } from '../shared/types/HttpMethod.ts'
2
2
 
3
3
  /*
4
- Maps an HTTP verb to MCP tool annotations so a model can tell a read from
5
- a write before calling. Abide derives these from the verb the RPC was
4
+ Maps an HTTP method to MCP tool annotations so a model can tell a read from
5
+ a write before calling. Abide derives these from the rpc the RPC was
6
6
  declared with rather than asking the author to repeat the intent:
7
7
  - GET / HEAD → read-only, non-destructive
8
8
  - POST → creates; not idempotent, not (necessarily) destructive
@@ -10,9 +10,9 @@ declared with rather than asking the author to repeat the intent:
10
10
  - PATCH → modifies; destructive, not idempotent
11
11
  - DELETE → removes; idempotent + destructive
12
12
  The shape matches MCP's ToolAnnotations (readOnlyHint / destructiveHint /
13
- idempotentHint); fields a verb doesn't imply are left off.
13
+ idempotentHint); fields a rpc doesn't imply are left off.
14
14
  */
15
- export function annotationsForMethod(method: HttpVerb): Record<string, boolean> {
15
+ export function annotationsForMethod(method: HttpMethod): Record<string, boolean> {
16
16
  switch (method) {
17
17
  case 'GET':
18
18
  case 'HEAD':
@@ -14,10 +14,10 @@ object whose `handle(request)` is the function the bun route at
14
14
  default-constructs it; there is no user-authored server module. Server
15
15
  name/version default from package.json.
16
16
 
17
- Tools are derived from every verb with `clients.mcp: true` (auto-on for
18
- read-only verbs that carry a schema; mutating verbs opt in explicitly)
17
+ Tools are derived from every rpc with `clients.mcp: true` (auto-on for
18
+ read-only rpcs that carry a schema; mutating rpcs opt in explicitly)
19
19
  and from every socket with `clients.mcp: true` (a `<base>-tail` read tool
20
- plus a `<base>-publish` tool when clientPublish is set). The HTTP verb
20
+ plus a `<base>-publish` tool when clientPublish is set). The HTTP method
21
21
  feeds each rpc tool's annotations. Auth inherits from the inbound request
22
22
  — bearer / cookie headers flow into the synthesized Request that hits
23
23
  each rpc handler. An optional `authorize` hook in opts can short-circuit
@@ -1,7 +1,7 @@
1
1
  import { promptRegistry } from '../server/prompts/promptRegistry.ts'
2
- import { dispatchVerbInProcess } from '../server/rpc/dispatchVerbInProcess.ts'
3
- import { findVerbByCommandName } from '../server/rpc/findVerbByCommandName.ts'
4
- import { verbRegistry } from '../server/rpc/verbRegistry.ts'
2
+ import { dispatchRpcInProcess } from '../server/rpc/dispatchRpcInProcess.ts'
3
+ import { findRpcByCommandName } from '../server/rpc/findRpcByCommandName.ts'
4
+ import { rpcRegistry } from '../server/rpc/rpcRegistry.ts'
5
5
  import { socketOperations } from '../server/sockets/socketOperations.ts'
6
6
  import { socketRegistry } from '../server/sockets/socketRegistry.ts'
7
7
  import { abideLog } from '../shared/abideLog.ts'
@@ -58,12 +58,12 @@ export type McpSurface = {
58
58
  /*
59
59
  Builds the array of MCP tool descriptors.
60
60
 
61
- RPCs: every verb with clients.mcp=true becomes one tool named after the
62
- export's URL (folder segments joined with `-`). The HTTP verb feeds the
61
+ RPCs: every rpc with clients.mcp=true becomes one tool named after the
62
+ export's URL (folder segments joined with `-`). The HTTP method feeds the
63
63
  tool's annotations (readOnlyHint / destructiveHint / idempotentHint) so
64
64
  a model can tell a read from a write; reads auto-expose while mutating
65
- verbs require an explicit clients.mcp (see resolveClientFlags). When the
66
- verb declares an `outputSchema` it's advertised as the tool outputSchema.
65
+ rpcs require an explicit clients.mcp (see resolveClientFlags). When the
66
+ rpc declares an `outputSchema` it's advertised as the tool outputSchema.
67
67
 
68
68
  Sockets: every socket with clients.mcp=true contributes a `<base>-tail`
69
69
  read tool (recent buffered messages) and, when clientPublish is set, a
@@ -71,7 +71,7 @@ read tool (recent buffered messages) and, when clientPublish is set, a
71
71
  */
72
72
  export function buildTools(): ToolDescriptor[] {
73
73
  const tools: ToolDescriptor[] = []
74
- for (const entry of verbRegistry.values()) {
74
+ for (const entry of rpcRegistry.values()) {
75
75
  if (!entry.clients.mcp) {
76
76
  continue
77
77
  }
@@ -154,7 +154,7 @@ export function buildPrompts(): PromptDescriptor[] {
154
154
  }
155
155
 
156
156
  /* MCP tool-dispatch spans, opt-in via DEBUG=abide:mcp — a model's tool call,
157
- wrapping the underlying verb dispatch in the same trace. */
157
+ wrapping the underlying rpc dispatch in the same trace. */
158
158
  const mcpLog = abideLog.channel('abide:mcp')
159
159
 
160
160
  function textResult(text: string, isError = false): ToolResult {
@@ -203,7 +203,7 @@ function callSocketTool(
203
203
 
204
204
  /*
205
205
  Tool dispatch. RPC tools synthesize a Request (with forwarded auth
206
- headers from `inbound`) and pipe it through verb.fetch inside the request
206
+ headers from `inbound`) and pipe it through rpc.fetch inside the request
207
207
  scope — the same seam the HTTP router crosses, so validation, the handler,
208
208
  and the request-scoped helpers (per-call cache(), cookies(), request())
209
209
  behave identically. A handler throw is caught by the scope and framed as
@@ -217,14 +217,14 @@ export async function callTool(
217
217
  args: Record<string, unknown> | undefined,
218
218
  inbound: Request,
219
219
  ): Promise<ToolResult> {
220
- const entry = findVerbByCommandName(toolName)
220
+ const entry = findRpcByCommandName(toolName)
221
221
  if (entry) {
222
- /* A verb owns this name. If it isn't mcp-exposed it's still unavailable —
222
+ /* A rpc owns this name. If it isn't mcp-exposed it's still unavailable —
223
223
  don't fall through to a socket op that happens to share the name. */
224
224
  if (!entry.clients.mcp) {
225
225
  throw new Error(`unknown tool: ${toolName}`)
226
226
  }
227
- const response = await dispatchVerbInProcess({
227
+ const response = await dispatchRpcInProcess({
228
228
  remote: entry.remote,
229
229
  args,
230
230
  baseUrl: `${new URL(inbound.url).origin}/`,
@@ -264,7 +264,7 @@ export function renderPrompt(
264
264
 
265
265
  /*
266
266
  Projects the app's MCP surface for an in-process consumer bound to `request`
267
- — tool calls forward that request's auth headers into the verb handler, so
267
+ — tool calls forward that request's auth headers into the rpc handler, so
268
268
  the model acts with the caller's identity. Used by `agent()`.
269
269
  */
270
270
  export function mcpSurface(request: Request): McpSurface {
@@ -7,7 +7,7 @@ package.json by the bundler when MCP is wired into createServer).
7
7
  response. Defaults come from the project's package.json.
8
8
  - `authorize`: optional boundary check. Runs once per MCP request before
9
9
  any tool/resource dispatch. Throw HttpError (or any Error) to reject.
10
- Per-tool authorization stays in the underlying verb handler.
10
+ Per-tool authorization stays in the underlying rpc handler.
11
11
  */
12
12
  export type McpServerOptions = {
13
13
  name?: string
@@ -1,10 +1,10 @@
1
- import type { VerbHelper } from './rpc/types/VerbHelper.ts'
1
+ import type { MutatingRpcHelper } from './rpc/types/RpcHelper.ts'
2
2
  import { unprocessed } from './rpc/unprocessed.ts'
3
3
 
4
4
  /*
5
- DELETE verb helper. The bundler rewrites every `export const x = DELETE(fn)` inside
6
- `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
5
+ DELETE rpc helper. The bundler rewrites every `export const x = DELETE(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineRpc call (server target) or a
7
7
  remoteProxy stub (client target). Calling this directly throws.
8
8
  */
9
9
  // @documentation rpc
10
- export const DELETE: VerbHelper = (_fn: any, _opts?: any) => unprocessed('DELETE')
10
+ export const DELETE: MutatingRpcHelper = (_fn: any, _opts?: any) => unprocessed('DELETE')
@@ -1,10 +1,10 @@
1
- import type { VerbHelper } from './rpc/types/VerbHelper.ts'
1
+ import type { RpcHelper } from './rpc/types/RpcHelper.ts'
2
2
  import { unprocessed } from './rpc/unprocessed.ts'
3
3
 
4
4
  /*
5
- GET verb helper. The bundler rewrites every `export const x = GET(fn)` inside
6
- `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
5
+ GET rpc helper. The bundler rewrites every `export const x = GET(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineRpc call (server target) or a
7
7
  remoteProxy stub (client target). Calling this directly throws.
8
8
  */
9
9
  // @documentation rpc
10
- export const GET: VerbHelper = (_fn: any, _opts?: any) => unprocessed('GET')
10
+ export const GET: RpcHelper = (_fn: any, _opts?: any) => unprocessed('GET')
@@ -1,10 +1,10 @@
1
- import type { VerbHelper } from './rpc/types/VerbHelper.ts'
1
+ import type { RpcHelper } from './rpc/types/RpcHelper.ts'
2
2
  import { unprocessed } from './rpc/unprocessed.ts'
3
3
 
4
4
  /*
5
- HEAD verb helper. The bundler rewrites every `export const x = HEAD(fn)` inside
6
- `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
5
+ HEAD rpc helper. The bundler rewrites every `export const x = HEAD(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineRpc call (server target) or a
7
7
  remoteProxy stub (client target). Calling this directly throws.
8
8
  */
9
9
  // @documentation rpc
10
- export const HEAD: VerbHelper = (_fn: any, _opts?: any) => unprocessed('HEAD')
10
+ export const HEAD: RpcHelper = (_fn: any, _opts?: any) => unprocessed('HEAD')
@@ -1,10 +1,10 @@
1
- import type { VerbHelper } from './rpc/types/VerbHelper.ts'
1
+ import type { MutatingRpcHelper } from './rpc/types/RpcHelper.ts'
2
2
  import { unprocessed } from './rpc/unprocessed.ts'
3
3
 
4
4
  /*
5
- PATCH verb helper. The bundler rewrites every `export const x = PATCH(fn)` inside
6
- `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
5
+ PATCH rpc helper. The bundler rewrites every `export const x = PATCH(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineRpc call (server target) or a
7
7
  remoteProxy stub (client target). Calling this directly throws.
8
8
  */
9
9
  // @documentation rpc
10
- export const PATCH: VerbHelper = (_fn: any, _opts?: any) => unprocessed('PATCH')
10
+ export const PATCH: MutatingRpcHelper = (_fn: any, _opts?: any) => unprocessed('PATCH')
@@ -1,10 +1,10 @@
1
- import type { VerbHelper } from './rpc/types/VerbHelper.ts'
1
+ import type { MutatingRpcHelper } from './rpc/types/RpcHelper.ts'
2
2
  import { unprocessed } from './rpc/unprocessed.ts'
3
3
 
4
4
  /*
5
- POST verb helper. The bundler rewrites every `export const x = POST(fn)` inside
6
- `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
5
+ POST rpc helper. The bundler rewrites every `export const x = POST(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineRpc call (server target) or a
7
7
  remoteProxy stub (client target). Calling this directly throws.
8
8
  */
9
9
  // @documentation rpc
10
- export const POST: VerbHelper = (_fn: any, _opts?: any) => unprocessed('POST')
10
+ export const POST: MutatingRpcHelper = (_fn: any, _opts?: any) => unprocessed('POST')
@@ -1,10 +1,10 @@
1
- import type { VerbHelper } from './rpc/types/VerbHelper.ts'
1
+ import type { MutatingRpcHelper } from './rpc/types/RpcHelper.ts'
2
2
  import { unprocessed } from './rpc/unprocessed.ts'
3
3
 
4
4
  /*
5
- PUT verb helper. The bundler rewrites every `export const x = PUT(fn)` inside
6
- `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
5
+ PUT rpc helper. The bundler rewrites every `export const x = PUT(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineRpc call (server target) or a
7
7
  remoteProxy stub (client target). Calling this directly throws.
8
8
  */
9
9
  // @documentation rpc
10
- export const PUT: VerbHelper = (_fn: any, _opts?: any) => unprocessed('PUT')
10
+ export const PUT: MutatingRpcHelper = (_fn: any, _opts?: any) => unprocessed('PUT')
@@ -6,7 +6,7 @@ import { request } from './request.ts'
6
6
  The in-app agent surface. `agent(engine, messages)` runs a model engine
7
7
  against the app's own MCP surface and returns the engine's frame stream —
8
8
  it does NOT pick a transport. The handler wraps it in `jsonl()` or `sse()`,
9
- so consumption is the app's choice, same as any other streaming verb:
9
+ so consumption is the app's choice, same as any other streaming rpc:
10
10
 
11
11
  // src/server/rpc/chat.ts
12
12
  import { agent } from '@abide/abide/server/agent'
@@ -18,10 +18,10 @@ so consumption is the app's choice, same as any other streaming verb:
18
18
 
19
19
  The engine — provider-specific, lives in a `@abide/<provider>` package —
20
20
  only sees the surface in and yields frames out, so swapping providers never
21
- touches the verb or the UI.
21
+ touches the rpc or the UI.
22
22
 
23
23
  Permission is decided server-side, not negotiated at runtime: the surface
24
- is already gated by each verb's `clients.mcp` declaration plus its own
24
+ is already gated by each rpc's `clients.mcp` declaration plus its own
25
25
  per-call handler auth, and any provider built-ins (e.g. Claude Code's bash
26
26
  tool) are fenced by static rules in the engine's config.
27
27
  */
@@ -71,7 +71,7 @@ export type AgentEngine = (input: {
71
71
 
72
72
  /*
73
73
  Runs an engine against the current request's MCP surface and returns its
74
- AgentFrame stream. Must be called inside a verb's request scope —
74
+ AgentFrame stream. Must be called inside a rpc's request scope —
75
75
  mcpSurface() forwards the caller's auth into every tool dispatch. The
76
76
  handler chooses the transport: `jsonl(agent(engine, messages))` or
77
77
  `sse(agent(engine, messages))`.
@@ -14,7 +14,7 @@ deep inside a handler:
14
14
 
15
15
  Reads `Bun.env` (the process environment plus any `.env` Bun loaded), so any
16
16
  Standard Schema library — zod, valibot, arktype — works without an adapter,
17
- same as the verb helpers. Coercion (e.g. a numeric PORT) lives in the schema.
17
+ same as the rpc helpers. Coercion (e.g. a numeric PORT) lives in the schema.
18
18
 
19
19
  The schema is registered (envSchemaStore) so the bundle launcher can project
20
20
  the first-run setup form from the same declaration. When the launcher imports
@@ -1,5 +1,6 @@
1
1
  import { NO_STORE } from '../shared/CACHE_CONTROL_VALUES.ts'
2
2
  import { TEXT_PLAIN } from '../shared/TEXT_PLAIN.ts'
3
+ import type { ErrorDescriptor } from '../shared/types/ErrorDescriptor.ts'
3
4
  import type { TypedResponse } from './rpc/types/TypedResponse.ts'
4
5
  import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
5
6
 
@@ -57,17 +58,58 @@ branch carries (`TypedResponse<{user}> | TypedResponse<never>` → Return
57
58
  = {user}).
58
59
  */
59
60
  // @documentation response
60
- export function error(status: number, message?: string, init?: ResponseInit): TypedResponse<never> {
61
+ export function error(descriptor: ErrorDescriptor): TypedResponse<never>
62
+ export function error(status: number, message?: string, init?: ResponseInit): TypedResponse<never>
63
+ export function error(
64
+ statusOrDescriptor: number | ErrorDescriptor,
65
+ message?: string,
66
+ init?: ResponseInit,
67
+ ): TypedResponse<never> {
68
+ /* A typed-error descriptor (`error(errors.invalidCoupon({…}))`) serializes as a
69
+ JSON `{ $abideError, data }` body at the descriptor's status — the client
70
+ parses it back onto the thrown HttpError's `.kind` / `.data`. */
71
+ if (typeof statusOrDescriptor === 'object') {
72
+ const descriptor = statusOrDescriptor
73
+ return new Response(
74
+ JSON.stringify({ $abideError: descriptor.$abideError, data: descriptor.data }),
75
+ withStatusText(
76
+ withResponseDefaults(
77
+ undefined,
78
+ { 'Content-Type': 'application/json', 'Cache-Control': NO_STORE },
79
+ descriptor.status,
80
+ ),
81
+ descriptor.status,
82
+ ),
83
+ ) as TypedResponse<never>
84
+ }
85
+ const status = statusOrDescriptor
61
86
  const body = message ?? STATUS_TEXT[status] ?? `HTTP ${status}`
62
87
  return new Response(
63
88
  body,
64
- withResponseDefaults(
65
- init,
66
- {
67
- 'Content-Type': TEXT_PLAIN,
68
- 'Cache-Control': NO_STORE,
69
- },
89
+ withStatusText(
90
+ withResponseDefaults(
91
+ init,
92
+ {
93
+ 'Content-Type': TEXT_PLAIN,
94
+ 'Cache-Control': NO_STORE,
95
+ },
96
+ status,
97
+ ),
70
98
  status,
71
99
  ),
72
100
  ) as TypedResponse<never>
73
101
  }
102
+
103
+ /*
104
+ Stamps the status's standard reason phrase onto `init.statusText` so the wire
105
+ status line reads `404 Not Found` and the client's `HttpError.statusText` is
106
+ populated — Bun's `Response` never derives statusText from the code. A caller's
107
+ explicit `init.statusText` (preserved through withResponseDefaults) still wins;
108
+ an unlisted code leaves it untouched.
109
+ */
110
+ function withStatusText(init: ResponseInit, status: number): ResponseInit {
111
+ if (init.statusText === undefined && STATUS_TEXT[status] !== undefined) {
112
+ return { ...init, statusText: STATUS_TEXT[status] }
113
+ }
114
+ return init
115
+ }
@@ -13,7 +13,7 @@ own per-request cache handles in-process dedupe.
13
13
  json(await db.getOrder(id)),
14
14
  )
15
15
 
16
- The return type carries `T` as a phantom brand so the verb helper can
16
+ The return type carries `T` as a phantom brand so the rpc helper can
17
17
  infer the caller-facing `Return` from the handler body — no need to
18
18
  annotate `GET<Args, Return>` just to type the response shape.
19
19
 
@@ -0,0 +1,19 @@
1
+ import type { ErrorConstructors } from '../../shared/types/ErrorConstructors.ts'
2
+ import type { ErrorSpec } from '../../shared/types/ErrorSpec.ts'
3
+
4
+ /*
5
+ Turns a rpc's declared `ErrorSpec` into the constructor object handed to the
6
+ handler (`(args, { errors })`). Each constructor stamps its name + status onto an
7
+ `ErrorDescriptor` carrying the call's `data`, which `error()` serializes as the
8
+ `{ $abideError, data }` body. Receiver-agnostic on data: a nullary error ignores
9
+ the (absent) argument.
10
+ */
11
+ export function buildErrorConstructors<Spec extends ErrorSpec>(
12
+ spec: Spec,
13
+ ): ErrorConstructors<Spec> {
14
+ const entries = Object.entries(spec).map(([name, { status }]) => [
15
+ name,
16
+ (data: unknown) => ({ $abideError: name, status, data }),
17
+ ])
18
+ return Object.fromEntries(entries) as ErrorConstructors<Spec>
19
+ }
@@ -5,34 +5,36 @@ import { forwardHeaders } from '../../shared/forwardHeaders.ts'
5
5
  import { isReadOnlyMethod } from '../../shared/isReadOnlyMethod.ts'
6
6
  import { resolveClientFlags } from '../../shared/resolveClientFlags.ts'
7
7
  import type { ClientFlags } from '../../shared/types/ClientFlags.ts'
8
- import type { HttpVerb } from '../../shared/types/HttpVerb.ts'
8
+ import type { ErrorSpec } from '../../shared/types/ErrorSpec.ts'
9
+ import type { HttpMethod } from '../../shared/types/HttpMethod.ts'
9
10
  import type { RemoteFunction } from '../../shared/types/RemoteFunction.ts'
10
11
  import type { StandardSchemaV1 } from '../../shared/types/StandardSchemaV1.ts'
11
- import { json } from '../json.ts'
12
12
  import { requestContext } from '../runtime/requestContext.ts'
13
+ import { buildErrorConstructors } from './buildErrorConstructors.ts'
13
14
  import { parseArgs } from './parseArgs.ts'
14
- import { registerVerb } from './registerVerb.ts'
15
- import { runWithVerbTimeout } from './runWithVerbTimeout.ts'
15
+ import { registerRpc } from './registerRpc.ts'
16
+ import { runWithRpcTimeout } from './runWithRpcTimeout.ts'
16
17
  import type { RemoteHandler } from './types/RemoteHandler.ts'
18
+ import { validationError } from './validationError.ts'
17
19
 
18
20
  /*
19
- Stash for the per-request AbortController a timed verb composes into the
21
+ Stash for the per-request AbortController a timed rpc composes into the
20
22
  inbound signal — read back in invoke's deadline callback to fire it. Lives on
21
- the scope's Request (one verb per .fetch — network or in-process dispatch) so
23
+ the scope's Request (one rpc per .fetch — network or in-process dispatch) so
22
24
  an SSR pass's many in-process cache reads, which call invoke() directly and
23
25
  never reach parseArgsForFetch, can't cross-cancel.
24
26
  */
25
- const VERB_TIMEOUT_ABORT = Symbol('abideVerbTimeoutAbort')
27
+ const RPC_TIMEOUT_ABORT = Symbol('abideRpcTimeoutAbort')
26
28
 
27
- /* Verb dispatch + validation spans, opt-in via DEBUG=abide:rpc. Reveals an
29
+ /* Rpc dispatch + validation spans, opt-in via DEBUG=abide:rpc. Reveals an
28
30
  in-process RPC→RPC call (same request scope, same trace) as a nested span. */
29
31
  const rpcLog = abideLog.channel('abide:rpc')
30
32
 
31
33
  /*
32
- Builds a RemoteFunction from an HTTP verb + RPC URL + handler. The bundler
33
- rewrites every `export const VERB = handler(fn)` inside an `$rpc/**` module
34
- so the verb (from the export name) and the URL (from the file path under
35
- `src/server/rpc/`, with `/rpc/` prefix) are threaded into defineVerb.
34
+ Builds a RemoteFunction from an HTTP method + RPC URL + handler. The bundler
35
+ rewrites every `export const METHOD = handler(fn)` inside an `$rpc/**` module
36
+ so the rpc (from the export name) and the URL (from the file path under
37
+ `src/server/rpc/`, with `/rpc/` prefix) are threaded into defineRpc.
36
38
 
37
39
  The plain call (`fn(args)`) resolves to the Content-Type-decoded body;
38
40
  non-2xx responses throw HttpError. `.raw(args)` returns the underlying
@@ -45,32 +47,48 @@ Every raw invocation records the synthesized Request against the returned
45
47
  promise so cache() can stash it on the entry without re-building.
46
48
  */
47
49
  // @documentation plumbing
48
- export function defineVerb<Args, Return>(
49
- method: HttpVerb,
50
+ export function defineRpc<Args, Return>(
51
+ method: HttpMethod,
50
52
  url: string,
51
53
  handler: RemoteHandler<Args, Return>,
52
54
  opts?: {
53
55
  inputSchema?: StandardSchemaV1
54
56
  outputSchema?: StandardSchemaV1
55
57
  filesSchema?: StandardSchemaV1
58
+ errors?: ErrorSpec
56
59
  clients?: Partial<ClientFlags>
57
60
  crossOrigin?: boolean
58
- /* Per-verb cap on actual received body bytes (413 past it); omitted = Bun's server-wide maxRequestBodySize. */
61
+ /* Per-rpc cap on actual received body bytes (413 past it); omitted = Bun's server-wide maxRequestBodySize. */
59
62
  maxBodySize?: number
60
- /* Per-verb handler deadline (ms): a 504 once exceeded, on every surface (SSR/MCP/CLI/network). */
63
+ /* Per-rpc handler deadline (ms): a 504 once exceeded, on every surface (SSR/MCP/CLI/network). */
61
64
  timeout?: number
65
+ /* Durable delivery: on an unreachable server (transport failure / 502/503/504/52x)
66
+ the client proxy parks the request for replay instead of just throwing. The
67
+ parked write drains on `rpc.outbox.retry()` (no auto-drain). Mutating methods
68
+ only. */
69
+ outbox?: boolean
62
70
  },
63
71
  ): RemoteFunction<Args, Return> {
72
+ /* `outbox: true` is a mutation contract — a read RPC has nothing to durably deliver,
73
+ so reject it at declaration rather than silently ignore. The server handler itself
74
+ is unaffected; durability is a client-proxy concern. */
75
+ if (opts?.outbox === true && isReadOnlyMethod(method)) {
76
+ throw new Error(
77
+ `[abide] outbox: true is only valid on mutating RPCs (POST/PUT/PATCH/DELETE), not ${method}`,
78
+ )
79
+ }
64
80
  const timeout = opts?.timeout
65
81
  const inputSchema = opts?.inputSchema
66
82
  const outputSchema = opts?.outputSchema
67
83
  const filesSchema = opts?.filesSchema
84
+ /* The declared error constructors handed to the handler as its `{ errors }` ctx. */
85
+ const errors = buildErrorConstructors(opts?.errors ?? {})
68
86
  /*
69
87
  An input schema makes the handler safe to advertise to non-browser
70
- surfaces. CLI flips on for any verb with one (a human/script invokes it
71
- deliberately). MCP only auto-exposes read-only verbs (GET/HEAD) — a
88
+ surfaces. CLI flips on for any rpc with one (a human/script invokes it
89
+ deliberately). MCP only auto-exposes read-only rpcs (GET/HEAD) — a
72
90
  model shouldn't be able to mutate/delete just because the handler
73
- carries a schema, so mutating verbs require an explicit clients.mcp.
91
+ carries a schema, so mutating rpcs require an explicit clients.mcp.
74
92
  Explicit `clients` always wins.
75
93
  */
76
94
  const hasSchema = inputSchema !== undefined
@@ -97,13 +115,13 @@ export function defineVerb<Args, Return>(
97
115
  async function runHandler(args: Args | undefined): Promise<Response> {
98
116
  return rpcLog.trace(
99
117
  `rpc ${method} ${url}`,
100
- () => handler(args as Args) as unknown as Response,
118
+ () => handler(args as Args, { errors }) as unknown as Response,
101
119
  )
102
120
  }
103
121
 
104
122
  /*
105
123
  Validates the parsed args against inputSchema (text fields), then — when the
106
- verb declares filesSchema — validates the File parts parseArgs split onto
124
+ rpc declares filesSchema — validates the File parts parseArgs split onto
107
125
  the request store and merges them into the args bag the handler receives.
108
126
  Either schema's issues become a 422. Files stay out of inputSchema so its
109
127
  JSON-Schema projection (OpenAPI/MCP/CLI) never has to model a binary.
@@ -115,7 +133,7 @@ export function defineVerb<Args, Return>(
115
133
  inputSchema['~standard'].validate(value),
116
134
  )
117
135
  if (result.issues) {
118
- return json({ issues: result.issues }, { status: 422 })
136
+ return validationError(result.issues)
119
137
  }
120
138
  value = result.value
121
139
  }
@@ -123,7 +141,7 @@ export function defineVerb<Args, Return>(
123
141
  const files = requestContext.getStore()?.files ?? {}
124
142
  const result = await filesSchema['~standard'].validate(files)
125
143
  if (result.issues) {
126
- return json({ issues: result.issues }, { status: 422 })
144
+ return validationError(result.issues)
127
145
  }
128
146
  value = { ...(value as object), ...(result.value as object) }
129
147
  }
@@ -138,11 +156,11 @@ export function defineVerb<Args, Return>(
138
156
  SSR call.
139
157
  */
140
158
  /* Abort the controller parseArgsForFetch stashed on store.req; a no-op when none was stashed (SSR cache reads). */
141
- function abortVerbTimeout(): void {
159
+ function abortRpcTimeout(): void {
142
160
  const req = requestContext.getStore()?.req as
143
- | (Request & { [VERB_TIMEOUT_ABORT]?: AbortController })
161
+ | (Request & { [RPC_TIMEOUT_ABORT]?: AbortController })
144
162
  | undefined
145
- req?.[VERB_TIMEOUT_ABORT]?.abort(new DOMException('handler timeout', 'TimeoutError'))
163
+ req?.[RPC_TIMEOUT_ABORT]?.abort(new DOMException('handler timeout', 'TimeoutError'))
146
164
  }
147
165
 
148
166
  function invoke(args: Args | undefined): Promise<Response> {
@@ -153,9 +171,9 @@ export function defineVerb<Args, Return>(
153
171
  /*
154
172
  On the deadline, fire the controller parseArgsForFetch composed into
155
173
  request().signal (absent on the SSR cache-read path, so a sibling
156
- verb's outbound fetch is never cancelled) — then 504.
174
+ rpc's outbound fetch is never cancelled) — then 504.
157
175
  */
158
- return runWithVerbTimeout(work, timeout, abortVerbTimeout)
176
+ return runWithRpcTimeout(work, timeout, abortRpcTimeout)
159
177
  }
160
178
 
161
179
  const remote = createRemoteFunction<Args, Return>({
@@ -168,17 +186,17 @@ export function defineVerb<Args, Return>(
168
186
  parseArgsForFetch: async (request) => {
169
187
  const args = await parseArgs(method, request, opts?.maxBodySize)
170
188
  /*
171
- Compose this verb's deadline into request().signal so a handler's
189
+ Compose this rpc's deadline into request().signal so a handler's
172
190
  fetch(ext, { signal: request().signal }) is cancelled when the
173
191
  timeout fires — not just abandoned. Applied after parseArgs onto the
174
- scope's *final* request: a maxBodySize verb swaps store.req for a
192
+ scope's *final* request: a maxBodySize rpc swaps store.req for a
175
193
  buffered copy (readBodyWithinLimit) and an app.handle hook may
176
194
  rewrite it, so composing onto the inbound `request` would leave
177
- request() — and abortVerbTimeout, which reads store.req — pointed at
195
+ request() — and abortRpcTimeout, which reads store.req — pointed at
178
196
  an un-cancellable signal. Only the signal is shadowed; the body
179
197
  stays readable. The store always exists here (network + in-process
180
198
  dispatch both run inside runWithRequestScope); SSR cache reads call
181
- invoke() directly, never this path, so a sibling verb is never
199
+ invoke() directly, never this path, so a sibling rpc is never
182
200
  cross-cancelled.
183
201
  */
184
202
  if (timeout !== undefined) {
@@ -187,7 +205,7 @@ export function defineVerb<Args, Return>(
187
205
  const controller = new AbortController()
188
206
  const composed = AbortSignal.any([req.signal, controller.signal])
189
207
  Object.defineProperty(req, 'signal', { value: composed, configurable: true })
190
- Object.defineProperty(req, VERB_TIMEOUT_ABORT, {
208
+ Object.defineProperty(req, RPC_TIMEOUT_ABORT, {
191
209
  value: controller,
192
210
  configurable: true,
193
211
  })
@@ -196,7 +214,7 @@ export function defineVerb<Args, Return>(
196
214
  return args as Args | undefined
197
215
  },
198
216
  })
199
- registerVerb({
217
+ registerRpc({
200
218
  remote: remote as RemoteFunction<unknown, unknown>,
201
219
  inputSchema,
202
220
  outputSchema,
@@ -4,11 +4,11 @@ import type { AppModule } from '../AppModule.ts'
4
4
  import { runWithRequestScope } from '../runtime/runWithRequestScope.ts'
5
5
 
6
6
  /*
7
- Runs a verb in-process: synthesizes the rpc Request from the remote's own
7
+ Runs a rpc in-process: synthesizes the rpc Request from the remote's own
8
8
  method + url and pipes it through remote.fetch — the same handler/validation/
9
9
  error path the HTTP router uses, no network hop. The single in-process
10
10
  dispatch every consumer surface (the CLI client, the MCP tool dispatcher, and
11
- the test client) routes through, so they can't drift on how a verb is invoked.
11
+ the test client) routes through, so they can't drift on how a rpc is invoked.
12
12
  Takes the RemoteFunction directly — invocation never reads the registry
13
13
  entry's schemas/clients (validation is closed over inside the remote), so the
14
14
  entry is not a dependency here. `baseUrl` gives the synthetic Request its
@@ -22,7 +22,7 @@ resolution, and the app's handleError (or the 500 fallback) on a throw. The
22
22
  synthesized Request is shared between the scope store and the handler fetch so
23
23
  request() returns the same Request parseArgs read from.
24
24
  */
25
- export function dispatchVerbInProcess({
25
+ export function dispatchRpcInProcess({
26
26
  remote,
27
27
  args,
28
28
  baseUrl,