@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
@@ -0,0 +1,23 @@
1
+ import type { StandardSchemaV1 } from '../../shared/types/StandardSchemaV1.ts'
2
+
3
+ /*
4
+ Flattens Standard Schema issues into a top-level-field → first-message map, the
5
+ form-friendly companion to the raw `issues` on a 422. A path segment is either a
6
+ bare `PropertyKey` or a `{ key }` wrapper (the spec allows both), so normalize
7
+ before reading. First message wins per field (a form shows one per input); the
8
+ raw `issues` keep every message and the full path. Issues with no string field
9
+ (root-level refinements) are omitted — they live only in `issues`.
10
+ */
11
+ export function fieldErrorsFromIssues(
12
+ issues: readonly StandardSchemaV1.Issue[],
13
+ ): Record<string, string> {
14
+ const fields: Record<string, string> = {}
15
+ for (const issue of issues) {
16
+ const segment = issue.path?.[0]
17
+ const key = typeof segment === 'object' ? segment.key : segment
18
+ if (typeof key === 'string' && !(key in fields)) {
19
+ fields[key] = issue.message
20
+ }
21
+ }
22
+ return fields
23
+ }
@@ -1,15 +1,15 @@
1
1
  import { commandNameForUrl } from '../../shared/commandNameForUrl.ts'
2
- import type { VerbRegistryEntry } from './types/VerbRegistryEntry.ts'
3
- import { verbRegistry } from './verbRegistry.ts'
2
+ import { rpcRegistry } from './rpcRegistry.ts'
3
+ import type { RpcRegistryEntry } from './types/RpcRegistryEntry.ts'
4
4
 
5
5
  /*
6
- Finds the registered verb whose URL maps to a given command name (folder
6
+ Finds the registered rpc whose URL maps to a given command name (folder
7
7
  segments joined with `-`, per commandNameForUrl). The CLI client proxy and
8
8
  the MCP tool dispatcher both key off this name, so the scan lives here once
9
9
  rather than being re-implemented — and reused — at each call site.
10
10
  */
11
- export function findVerbByCommandName(name: string): VerbRegistryEntry | undefined {
12
- for (const entry of verbRegistry.values()) {
11
+ export function findRpcByCommandName(name: string): RpcRegistryEntry | undefined {
12
+ for (const entry of rpcRegistry.values()) {
13
13
  if (commandNameForUrl(entry.remote.url) === name) {
14
14
  return entry
15
15
  }
@@ -3,7 +3,7 @@ import { contentTypeOf } from '../../shared/contentTypeOf.ts'
3
3
  import { decodeRefJson } from '../../shared/decodeRefJson.ts'
4
4
  import { HttpError } from '../../shared/HttpError.ts'
5
5
  import { REF_JSON_HEADER } from '../../shared/REF_JSON_HEADER.ts'
6
- import type { HttpVerb } from '../../shared/types/HttpVerb.ts'
6
+ import type { HttpMethod } from '../../shared/types/HttpMethod.ts'
7
7
  import { error } from '../error.ts'
8
8
  import { requestContext } from '../runtime/requestContext.ts'
9
9
  import { readBodyWithinLimit } from './readBodyWithinLimit.ts'
@@ -41,7 +41,7 @@ function splitFormData(form: FormData): Record<string, unknown> {
41
41
  }
42
42
 
43
43
  /*
44
- Parses + merges every source of args available for a verb-defined handler:
44
+ Parses + merges every source of args available for a rpc-defined handler:
45
45
  - body (json or form-encoded, ignored for GET/DELETE/HEAD)
46
46
  - url query string
47
47
 
@@ -54,12 +54,12 @@ to layer the query into, and the framework's args type is a single bag rather
54
54
  than a `{body, query}` envelope. Returns undefined when no source contributes
55
55
  any key.
56
56
 
57
- `maxBodySize` (per-verb, opt-in) bounds the body's actual received bytes
57
+ `maxBodySize` (per-rpc, opt-in) bounds the body's actual received bytes
58
58
  before any parse — see readBodyWithinLimit. Omitted = no abide-level check;
59
59
  Bun.serve's server-wide maxRequestBodySize is the ceiling.
60
60
  */
61
61
  export async function parseArgs(
62
- method: HttpVerb,
62
+ method: HttpMethod,
63
63
  request: Request,
64
64
  maxBodySize?: number,
65
65
  ): Promise<unknown> {
@@ -129,12 +129,12 @@ export async function parseArgs(
129
129
  /*
130
130
  TODO(query-coercion): query params arrive as strings, so a numeric/boolean
131
131
  field reaches schema validation as `'2'`/`'true'`. Deferred deliberately:
132
- parseArgs has no access to the verb's inputSchema (it lives in defineVerb),
132
+ parseArgs has no access to the rpc's inputSchema (it lives in defineRpc),
133
133
  and Standard Schema exposes no type structure to drive type-aware coercion.
134
134
  Blind value-shape coercion is unsafe — it would corrupt legitimately
135
135
  string-typed fields whose value looks numeric/boolean (ids, zip codes,
136
136
  version strings like '1.0'), silently breaking GET validation. A correct fix
137
- needs the schema threaded in here (or a coercing schema adapter at the verb).
137
+ needs the schema threaded in here (or a coercing schema adapter at the rpc).
138
138
  */
139
139
  const bodyObject = (body ?? {}) as Record<string, unknown>
140
140
  const merged = { ...Object.fromEntries(url.searchParams), ...bodyObject }
@@ -2,14 +2,14 @@ import { HttpError } from '../../shared/HttpError.ts'
2
2
  import { error } from '../error.ts'
3
3
 
4
4
  /*
5
- Enforces a verb's maxBodySize on the actual received bytes, not the
5
+ Enforces a rpc's maxBodySize on the actual received bytes, not the
6
6
  Content-Length header (absent on chunked bodies, trivially spoofed). A
7
7
  declared Content-Length over the limit rejects before reading anything;
8
8
  otherwise the body streams into a buffer that throws 413 the moment the
9
9
  limit is crossed — aborting the read, so the remaining bytes are never
10
10
  consumed. Returns a reassembled Request carrying the buffered body and the
11
11
  original headers, so the parse path's .text()/.formData() work unchanged.
12
- Only called when the verb declares maxBodySize; the default ceiling is
12
+ Only called when the rpc declares maxBodySize; the default ceiling is
13
13
  Bun.serve's server-wide maxRequestBodySize, which Bun enforces natively.
14
14
  */
15
15
  export async function readBodyWithinLimit(request: Request, maxBytes: number): Promise<Request> {
@@ -0,0 +1,6 @@
1
+ import { rpcRegistry } from './rpcRegistry.ts'
2
+ import type { RpcRegistryEntry } from './types/RpcRegistryEntry.ts'
3
+
4
+ export function registerRpc(entry: RpcRegistryEntry): void {
5
+ rpcRegistry.set(entry.remote.url, entry)
6
+ }
@@ -1,11 +1,11 @@
1
- import type { VerbRegistryEntry } from './types/VerbRegistryEntry.ts'
1
+ import type { RpcRegistryEntry } from './types/RpcRegistryEntry.ts'
2
2
 
3
3
  /*
4
- Process-wide registry of every verb-bound RPC declared in the app.
5
- defineVerb inserts on first construction (which happens at module-load
4
+ Process-wide registry of every rpc-bound RPC declared in the app.
5
+ defineRpc inserts on first construction (which happens at module-load
6
6
  time inside the rpc dispatcher cache or eagerly when MCP / CLI walks the
7
7
  rpc manifest). MCP server reads this to build its tools list; the CLI
8
8
  binary reads it to generate subcommands. The browser path never touches
9
9
  this — the client stub has no schema or clients metadata to register.
10
10
  */
11
- export const verbRegistry = new Map<string, VerbRegistryEntry>()
11
+ export const rpcRegistry = new Map<string, RpcRegistryEntry>()
@@ -1,12 +1,12 @@
1
1
  import { error } from '../error.ts'
2
2
 
3
3
  /*
4
- Bounds a verb handler's execution: races the in-flight `work` against `ms`.
4
+ Bounds a rpc handler's execution: races the in-flight `work` against `ms`.
5
5
  On the deadline it resolves a 504 — so the caller (an SSR cache read, an
6
6
  MCP/CLI invocation, or the network response) is unblocked in time with an
7
7
  honest status — and calls `onTimeout` to cancel any
8
- cooperating outbound work (the network path composes the verb's deadline into
9
- request().signal; see defineVerb).
8
+ cooperating outbound work (the network path composes the rpc's deadline into
9
+ request().signal; see defineRpc).
10
10
 
11
11
  `work` keeps running after the deadline — JS can't cancel a running async
12
12
  function, only stop awaiting it — so its eventual settlement is swallowed to
@@ -14,7 +14,7 @@ avoid an unhandled rejection, and a late-resolved streaming Response has its
14
14
  body cancelled to release the underlying source. A handler that wants its own
15
15
  outbound I/O torn down should pass request().signal to it.
16
16
  */
17
- export function runWithVerbTimeout(
17
+ export function runWithRpcTimeout(
18
18
  work: Promise<Response>,
19
19
  ms: number,
20
20
  onTimeout: () => void,
@@ -1,7 +1,9 @@
1
+ import type { ErrorConstructors } from '../../../shared/types/ErrorConstructors.ts'
2
+ import type { ErrorSpec } from '../../../shared/types/ErrorSpec.ts'
1
3
  import type { TypedResponse } from './TypedResponse.ts'
2
4
 
3
5
  /*
4
- Handler signature for verb-defined remote functions. Args is `undefined` for
6
+ Handler signature for rpc-defined remote functions. Args is `undefined` for
5
7
  GETs/DELETEs with no query, JSON-shaped objects for json bodies, and
6
8
  form-shaped objects for form-encoded bodies. For a multipart upload it's the
7
9
  text fields (`inputSchema`) intersected with the validated File parts
@@ -13,7 +15,7 @@ decoding (a parsed object for JSON, a string for text/*, a Blob otherwise,
13
15
  `undefined` for 204). The handler must return a Response at runtime; the
14
16
  `TypedResponse<Return>` brand on `json`/`error`/`redirect`/`jsonl`/`sse`
15
17
  carries the body shape through the function's inferred return type so the
16
- verb helper can infer `Return` automatically — no need to annotate
18
+ rpc helper can infer `Return` automatically — no need to annotate
17
19
  `GET<Args, Return>` when the handler returns one of the respond helpers.
18
20
  A bare `new Response(...)` is still acceptable: the brand is optional, so
19
21
  untagged Responses fall back to `Return = unknown`.
@@ -22,6 +24,10 @@ Handlers that need the inbound Request (headers, `request.signal`, …) read
22
24
  it via `request()` from `abide/server` rather than a handler parameter, so
23
25
  the signature stays a single parsed-`args` bag.
24
26
  */
25
- export type RemoteHandler<Args, Return> = (
27
+ export type RemoteHandler<Args, Return, Errors extends ErrorSpec = Record<never, never>> = (
26
28
  args: Args,
29
+ /* The rpc's declared error constructors (`error(errors.invalidCoupon({…}))`), typed
30
+ from its `errors` opt; an empty object when none declared. A handler that takes only
31
+ `args` is still assignable here (fewer params is assignable to more). */
32
+ ctx: { errors: ErrorConstructors<Errors> },
27
33
  ) => TypedResponse<Return> | Promise<TypedResponse<Return>>
@@ -1,16 +1,18 @@
1
1
  import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
2
+ import type { ErrorSpec } from '../../../shared/types/ErrorSpec.ts'
2
3
  import type { RemoteFunction } from '../../../shared/types/RemoteFunction.ts'
3
4
  import type { StandardSchemaV1 } from '../../../shared/types/StandardSchemaV1.ts'
4
5
  import type { RemoteHandler } from './RemoteHandler.ts'
5
6
 
6
7
  /*
7
- Options every verb overload accepts: the OpenAPI 200 `outputSchema`, the
8
+ Options every rpc overload accepts: the OpenAPI 200 `outputSchema`, the
8
9
  `clients` surface flags, the same-origin CSRF exemption (`crossOrigin`), the
9
10
  pre-parse body-byte ceiling (`maxBodySize`), and the per-surface handler
10
11
  `timeout` (ms). The schema-bearing overloads intersect this with their own
11
- `inputSchema`/`filesSchema` members.
12
+ `inputSchema`/`filesSchema` members. Mutating helpers widen it with `outbox`
13
+ (see MutatingRpcOpts) — a read RPC never accepts it.
12
14
  */
13
- type VerbBaseOpts = {
15
+ type RpcBaseOpts = {
14
16
  outputSchema?: StandardSchemaV1
15
17
  clients?: Partial<ClientFlags>
16
18
  crossOrigin?: boolean
@@ -19,9 +21,22 @@ type VerbBaseOpts = {
19
21
  }
20
22
 
21
23
  /*
22
- Shared signature for every verb helper (GET / POST / …). Three overloads:
24
+ Mutating-helper options: the shared base plus durable delivery. `outbox` lives
25
+ here, not on RpcBaseOpts, because a read RPC has nothing to durably deliver —
26
+ so `GET(fn, { outbox: true })` is a compile error, not the runtime throw it used
27
+ to be. Keeps the type surface honest with the defineRpc guard.
28
+ */
29
+ type MutatingRpcOpts = RpcBaseOpts & {
30
+ /* Durable delivery: on an unreachable server the call still throws, and the request is
31
+ parked for replay. Drains on `rpc.outbox.retry()` — no auto-drain. The call shape is
32
+ unchanged — `rpc.outbox` exposes the queue. */
33
+ outbox?: boolean
34
+ }
35
+
36
+ /*
37
+ Shared signature for every rpc helper (GET / POST / …). Three overloads:
23
38
 
24
- - `Verb(fn, { inputSchema, outputSchema?, clients? })` — `Args` infers
39
+ - `Rpc(fn, { inputSchema, outputSchema?, clients? })` — `Args` infers
25
40
  from `InferInput<InputSchema>`, the handler receives
26
41
  `InferOutput<InputSchema>`. Generic order is `<Return, InputSchema>` so
27
42
  users can override `Return` while letting `InputSchema` infer from
@@ -29,10 +44,10 @@ Shared signature for every verb helper (GET / POST / …). Three overloads:
29
44
  the success body — it feeds the OpenAPI 200 response and the MCP tool
30
45
  `outputSchema`. JSON Schema is projected from each schema's own
31
46
  `toJSONSchema()` (wrap with withJsonSchema if the library lacks one).
32
- `clients` controls which surfaces (browser / mcp / cli) expose this verb.
33
- `crossOrigin: true` exempts a mutating verb from the router's same-origin
47
+ `clients` controls which surfaces (browser / mcp / cli) expose this rpc.
48
+ `crossOrigin: true` exempts a mutating rpc from the router's same-origin
34
49
  CSRF gate — by default a browser request whose Origin doesn't match the
35
- app's own host is refused with 403 on every non-GET/HEAD verb.
50
+ app's own host is refused with 403 on every non-GET/HEAD rpc.
36
51
  `maxBodySize` caps the body's actual received bytes (413 past it),
37
52
  enforced before parsing; omitted, the only ceiling is Bun.serve's
38
53
  server-wide maxRequestBodySize. `timeout` (ms) bounds the handler's
@@ -40,15 +55,15 @@ Shared signature for every verb helper (GET / POST / …). Three overloads:
40
55
  exceeded; on the network path it also aborts request().signal so a
41
56
  handler's `fetch(ext, { signal: request().signal })` is cancelled, not
42
57
  just abandoned.
43
- - `Verb(fn, { clients })` — schemaless but with explicit client
58
+ - `Rpc(fn, { clients })` — schemaless but with explicit client
44
59
  targeting (e.g. server-internal RPC with `clients: { browser: false }`).
45
- - `Verb(fn)` — bare handler. `Args` and `Return` come from the handler
60
+ - `Rpc(fn)` — bare handler. `Args` and `Return` come from the handler
46
61
  type; `Return` is usually inferred via the `TypedResponse<T>` brand on
47
62
  `json`/`error`/`redirect`/`jsonl`/`sse`.
48
63
  */
49
- export type VerbHelper = {
64
+ type RpcHelperOf<Opts> = {
50
65
  /*
51
- `Verb(fn, { inputSchema, filesSchema, … })` — multipart upload. The
66
+ `Rpc(fn, { inputSchema, filesSchema, … })` — multipart upload. The
52
67
  handler receives the text fields (`InferOutput<InputSchema>`) intersected
53
68
  with the validated File parts (`InferOutput<FilesSchema>`); both are merged
54
69
  into one args bag. The call site sends a FormData (RemoteFunction's call
@@ -60,25 +75,44 @@ export type VerbHelper = {
60
75
  Return = unknown,
61
76
  InputSchema extends StandardSchemaV1 = StandardSchemaV1,
62
77
  FilesSchema extends StandardSchemaV1 = StandardSchemaV1,
78
+ Errors extends ErrorSpec = Record<string, never>,
63
79
  >(
64
80
  fn: RemoteHandler<
65
81
  StandardSchemaV1.InferOutput<InputSchema> & StandardSchemaV1.InferOutput<FilesSchema>,
66
- Return
82
+ Return,
83
+ Errors
67
84
  >,
68
- opts: VerbBaseOpts & {
85
+ opts: Opts & {
69
86
  inputSchema: InputSchema
70
87
  filesSchema: FilesSchema
88
+ errors?: Errors
71
89
  },
72
- ): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return>
73
- <Return = unknown, InputSchema extends StandardSchemaV1 = StandardSchemaV1>(
74
- fn: RemoteHandler<StandardSchemaV1.InferOutput<InputSchema>, Return>,
75
- opts: VerbBaseOpts & { inputSchema: InputSchema },
76
- ): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return>
77
- <Args = undefined, Return = unknown>(
78
- fn: RemoteHandler<Args, Return>,
79
- opts: VerbBaseOpts,
80
- ): RemoteFunction<Args, Return>
90
+ ): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return, Errors>
91
+ <
92
+ Return = unknown,
93
+ InputSchema extends StandardSchemaV1 = StandardSchemaV1,
94
+ Errors extends ErrorSpec = Record<string, never>,
95
+ >(
96
+ fn: RemoteHandler<StandardSchemaV1.InferOutput<InputSchema>, Return, Errors>,
97
+ opts: Opts & { inputSchema: InputSchema; errors?: Errors },
98
+ ): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return, Errors>
99
+ <Args = undefined, Return = unknown, Errors extends ErrorSpec = Record<never, never>>(
100
+ fn: RemoteHandler<Args, Return, Errors>,
101
+ opts: Opts & { errors?: Errors },
102
+ ): RemoteFunction<Args, Return, Errors>
81
103
  <Args = undefined, Return = unknown>(
82
104
  fn: RemoteHandler<Args, Return>,
83
105
  ): RemoteFunction<Args, Return>
84
106
  }
107
+
108
+ /* The read helpers (GET/HEAD): no `outbox` — a read has nothing to durably deliver. */
109
+ export type RpcHelper = RpcHelperOf<RpcBaseOpts>
110
+
111
+ /*
112
+ The mutating helpers (POST/PUT/PATCH/DELETE). A durable (`outbox`) call is a normal
113
+ RemoteFunction — it throws exactly like a non-durable one and only parks the request as a
114
+ side-effect on an unreachable server — so there is no separate return shape; `outbox` rides
115
+ MutatingRpcOpts and `rpc.outbox` exposes the queue. The distinct opts base is what makes
116
+ `outbox` legal here and a compile error on the read helpers.
117
+ */
118
+ export type MutatingRpcHelper = RpcHelperOf<MutatingRpcOpts>
@@ -3,7 +3,7 @@ import type { RemoteFunction } from '../../../shared/types/RemoteFunction.ts'
3
3
  import type { StandardSchemaV1 } from '../../../shared/types/StandardSchemaV1.ts'
4
4
 
5
5
  /*
6
- Per-verb registry record on the server side. MCP and CLI enumerate this
6
+ Per-rpc registry record on the server side. MCP and CLI enumerate this
7
7
  to discover which RPCs are advertised (clients flags) and what shapes
8
8
  they expect/return. The schemas and resolved clients stay off the public
9
9
  RemoteFunction shape so the browser-side proxy doesn't need to carry
@@ -20,13 +20,13 @@ from `inputSchema` because a File has no honest JSON-Schema conversion — it
20
20
  stays out of the MCP/CLI projection that `inputSchema` feeds, and the OpenAPI
21
21
  multipart body advertises the file parts generically as binary.
22
22
  */
23
- export type VerbRegistryEntry = {
23
+ export type RpcRegistryEntry = {
24
24
  remote: RemoteFunction<unknown, unknown>
25
25
  inputSchema: StandardSchemaV1 | undefined
26
26
  outputSchema: StandardSchemaV1 | undefined
27
27
  filesSchema: StandardSchemaV1 | undefined
28
28
  clients: ClientFlags
29
- /* The verb's declared opts, recorded so introspection (inspector) can report
29
+ /* The rpc's declared opts, recorded so introspection (inspector) can report
30
30
  the deadline/body-cap/CSRF-exemption a handler runs under. Undefined = the
31
31
  framework default (no deadline, Bun's server-wide body ceiling, gated). */
32
32
  timeout: number | undefined
@@ -1,14 +1,14 @@
1
1
  /*
2
2
  A `Response` tagged with the body type the framework will hand back to
3
3
  callers after Content-Type-driven decoding. The tag is phantom — it
4
- adds no runtime field, only a type-level slot so the verb helpers can
4
+ adds no runtime field, only a type-level slot so the rpc helpers can
5
5
  infer `Return` from the handler's return type instead of forcing every
6
6
  route to annotate it via `GET<Args, Return>`.
7
7
 
8
8
  The respond helpers (`json<T>`, `error`, `redirect`, `jsonl<F>`,
9
9
  `sse<F>`) all return a `TypedResponse<T>`, so a handler ending in
10
10
  `return json({ user })` exposes `{ user: ... }` as its body type; the
11
- verb overload picks it up via `RemoteHandler<Args, Return>`.
11
+ rpc overload picks it up via `RemoteHandler<Args, Return>`.
12
12
 
13
13
  `T` is optional on the brand so a plain `new Response(...)` (untagged)
14
14
  remains assignable to `TypedResponse<unknown>`; in that case `Return`
@@ -1,14 +1,13 @@
1
- import type { RemoteFunction } from '../../shared/types/RemoteFunction.ts'
2
-
3
1
  /*
4
- Verb helpers (GET / POST / …) are placeholders — the bundler rewrites every
2
+ Rpc helpers (GET / POST / …) are placeholders — the bundler rewrites every
5
3
  `export const x = GET(fn)` inside `src/server/rpc/<file>.ts` into a call to
6
- defineVerb (server target) or remoteProxy (client target). If a call slips
4
+ defineRpc (server target) or remoteProxy (client target). If a call slips
7
5
  through, the bundler plugin didn't process the file; throwing here surfaces
8
- that cleanly instead of silently returning undefined.
6
+ that cleanly instead of silently returning undefined. Returns `never` (it always
7
+ throws), so a helper's arrow satisfies every overload of RpcHelper/MutatingRpcHelper.
9
8
  */
10
- export function unprocessed<Args, Return>(verb: string): RemoteFunction<Args, Return> {
9
+ export function unprocessed(method: string): never {
11
10
  throw new Error(
12
- `[abide] \`${verb}\` was called outside an $rpc module — verb helpers are only valid as the value of \`export const <filename> = ...\` inside a file under src/server/rpc/`,
11
+ `[abide] \`${method}\` was called outside an $rpc module — rpc helpers are only valid as the value of \`export const <filename> = ...\` inside a file under src/server/rpc/`,
13
12
  )
14
13
  }
@@ -0,0 +1,17 @@
1
+ import type { StandardSchemaV1 } from '../../shared/types/StandardSchemaV1.ts'
2
+ import type { ValidationErrorData } from '../../shared/types/ValidationErrorData.ts'
3
+ import { error } from '../error.ts'
4
+ import { fieldErrorsFromIssues } from './fieldErrorsFromIssues.ts'
5
+
6
+ /*
7
+ The framework-reserved `validation` typed error a 422 carries: the raw Standard
8
+ Schema `issues` plus the form-friendly field → first-message map. Routed through
9
+ the single `error()` funnel (descriptor form) so it serializes as the same
10
+ `{ $abideError, data }` body every typed error uses, and so the 422's reason
11
+ phrase reaches `HttpError.statusText`. The client parses it back onto
12
+ `HttpError.kind = 'validation'` / `.data: ValidationErrorData`.
13
+ */
14
+ export function validationError(issues: readonly StandardSchemaV1.Issue[]): Response {
15
+ const data: ValidationErrorData = { issues, fields: fieldErrorsFromIssues(issues) }
16
+ return error({ $abideError: 'validation', status: 422, data })
17
+ }
@@ -1,20 +1,20 @@
1
1
  import { jsonSchemaForSchema } from '../../shared/jsonSchemaForSchema.ts'
2
2
  import { promptRegistry } from '../prompts/promptRegistry.ts'
3
- import { verbRegistry } from '../rpc/verbRegistry.ts'
3
+ import { rpcRegistry } from '../rpc/rpcRegistry.ts'
4
4
  import { socketOperations } from '../sockets/socketOperations.ts'
5
5
  import { socketRegistry } from '../sockets/socketRegistry.ts'
6
6
  import type { InspectorSurface } from './types/InspectorSurface.ts'
7
7
 
8
8
  /*
9
- Projects the live verb + socket registries into the inspector's catalog. Read
10
- at call time (not cached) so verbs constructed after boot — lazily on first
9
+ Projects the live rpc + socket registries into the inspector's catalog. Read
10
+ at call time (not cached) so rpcs constructed after boot — lazily on first
11
11
  hit, or eagerly once ensureRegistriesLoaded walked the manifest — show up.
12
12
  Schemas go through jsonSchemaForSchema, the same projection MCP and OpenAPI
13
- use, so the three can't disagree on a verb's shape; a missing schema stays
14
- undefined to mark a verb that carries no machine-advertisable contract.
13
+ use, so the three can't disagree on a rpc's shape; a missing schema stays
14
+ undefined to mark a rpc that carries no machine-advertisable contract.
15
15
  */
16
16
  export function buildInspectorSurface(): InspectorSurface {
17
- const verbs = Array.from(verbRegistry.values()).map((entry) => ({
17
+ const rpcs = Array.from(rpcRegistry.values()).map((entry) => ({
18
18
  url: entry.remote.url,
19
19
  method: entry.remote.method,
20
20
  clients: { ...entry.remote.clients },
@@ -41,5 +41,5 @@ export function buildInspectorSurface(): InspectorSurface {
41
41
  description: entry.prompt.description,
42
42
  inputSchema: entry.jsonSchema,
43
43
  }))
44
- return { verbs, sockets, prompts }
44
+ return { rpcs, sockets, prompts }
45
45
  }
@@ -1,10 +1,10 @@
1
1
  import { carriesBodyArgs } from '../../shared/carriesBodyArgs.ts'
2
2
  import { commandNameForUrl } from '../../shared/commandNameForUrl.ts'
3
3
  import { jsonSchemaForSchema } from '../../shared/jsonSchemaForSchema.ts'
4
- import { verbRegistry } from '../rpc/verbRegistry.ts'
4
+ import { rpcRegistry } from '../rpc/rpcRegistry.ts'
5
5
 
6
6
  /*
7
- Turns a verb's resolved JSON Schema into OpenAPI query parameters — one
7
+ Turns a rpc's resolved JSON Schema into OpenAPI query parameters — one
8
8
  per top-level property, marked required when the schema lists it. Used
9
9
  for GET/DELETE/HEAD operations, which carry their args on the query
10
10
  string (mirroring buildRpcRequest).
@@ -24,7 +24,7 @@ function queryParameters(jsonSchema: Record<string, unknown>): Array<Record<stri
24
24
  }
25
25
 
26
26
  /*
27
- Request body schema for a multipart upload verb: the text fields from
27
+ Request body schema for a multipart upload rpc: the text fields from
28
28
  inputSchema, plus the binary parts. A File has no honest
29
29
  Standard-Schema→JSON-Schema conversion, so the file parts are advertised
30
30
  generically as additional binary properties rather than named per field.
@@ -44,7 +44,7 @@ function multipartBodySchema(textSchema: Record<string, unknown>): Record<string
44
44
  }
45
45
 
46
46
  /*
47
- Builds an OpenAPI 3.1 document from the verb registry — the HTTP surface
47
+ Builds an OpenAPI 3.1 document from the rpc registry — the HTTP surface
48
48
  every rpc exposes regardless of which non-browser clients it advertises.
49
49
  GET/DELETE/HEAD args become query parameters; POST/PUT/PATCH args become
50
50
  a JSON request body. operationId is the folder-prefixed command name so
@@ -55,13 +55,13 @@ export function buildOpenApiSpec(info: {
55
55
  version: string
56
56
  }): Record<string, unknown> {
57
57
  const paths: Record<string, Record<string, unknown>> = {}
58
- for (const entry of verbRegistry.values()) {
58
+ for (const entry of rpcRegistry.values()) {
59
59
  const url = entry.remote.url
60
60
  const method = entry.remote.method
61
61
  const jsonSchema = jsonSchemaForSchema(entry.inputSchema)
62
62
  const description = jsonSchema.description as string | undefined
63
63
  /*
64
- When the verb declares an `outputSchema`, describe the 200 body
64
+ When the rpc declares an `outputSchema`, describe the 200 body
65
65
  with it so external tooling sees the real return shape; otherwise
66
66
  fall back to a bare OK.
67
67
  */
@@ -3,7 +3,7 @@ import { memoizeByKey } from '../../shared/memoizeByKey.ts'
3
3
  import { NAV_HEADER } from '../../shared/NAV_HEADER.ts'
4
4
  import { REMOTE_FUNCTION } from '../../shared/REMOTE_FUNCTION.ts'
5
5
  import { TEXT_PLAIN } from '../../shared/TEXT_PLAIN.ts'
6
- import type { HttpVerb } from '../../shared/types/HttpVerb.ts'
6
+ import type { HttpMethod } from '../../shared/types/HttpMethod.ts'
7
7
  import type { RemoteFunction } from '../../shared/types/RemoteFunction.ts'
8
8
  import type { Pages } from '../../ui/types/Pages.ts'
9
9
  import type { RemoteRoutes } from '../rpc/types/RemoteRoutes.ts'
@@ -26,7 +26,7 @@ type RenderPage = (
26
26
  store: RequestStore,
27
27
  ) => Promise<Response>
28
28
 
29
- /* The framework's 405 — `Allow` names the permitted verb(s), body and NO_STORE shared so the rpc and page branches can't drift. */
29
+ /* The framework's 405 — `Allow` names the permitted rpc(s), body and NO_STORE shared so the rpc and page branches can't drift. */
30
30
  function methodNotAllowed(allow: string): Response {
31
31
  return new Response('Method Not Allowed', {
32
32
  status: 405,
@@ -36,9 +36,9 @@ function methodNotAllowed(allow: string): Response {
36
36
 
37
37
  /*
38
38
  Owns route dispatch: deciding, per registered URL, whether a request hits an
39
- rpc verb, a page render, or nothing — and the method-matching that picks the
39
+ rpc rpc, a page render, or nothing — and the method-matching that picks the
40
40
  status. Page URLs (under src/ui/pages/) serve GET/HEAD by rendering; rpc
41
- URLs (under src/server/rpc/, `/rpc/...`) dispatch to the single declared verb,
41
+ URLs (under src/server/rpc/, `/rpc/...`) dispatch to the single declared rpc,
42
42
  405 on method mismatch; an unregistered URL is 404. Page and rpc URLs are
43
43
  disjoint by construction, so each route lands in exactly one branch.
44
44
 
@@ -66,7 +66,7 @@ export function createRouteDispatcher({
66
66
  Each $rpc module has exactly one named export, validated at build
67
67
  time. Pick the first REMOTE_FUNCTION-branded export — exact, so an
68
68
  incidental re-export carrying method/url props can't be mistaken
69
- for the verb.
69
+ for the rpc.
70
70
  */
71
71
  return loader().then((mod) => {
72
72
  for (const value of Object.values(mod)) {
@@ -82,14 +82,14 @@ export function createRouteDispatcher({
82
82
  const hasPage = pages[routeUrl] !== undefined
83
83
  const hasRpc = rpc[routeUrl] !== undefined
84
84
  return async function routeHandler(req, pathParams, store) {
85
- const method = req.method as HttpVerb
85
+ const method = req.method as HttpMethod
86
86
  if (hasRpc) {
87
87
  const fn = await loadRpc(routeUrl)
88
88
  if (fn && fn.method === method) {
89
89
  const forbidden = crossOriginGate(req, store.url, {
90
90
  allowReadOnly: true,
91
91
  optOut: fn.crossOrigin === true,
92
- hint: 'Declare `crossOrigin: true` on the verb to accept cross-site calls.',
92
+ hint: 'Declare `crossOrigin: true` on the rpc to accept cross-site calls.',
93
93
  })
94
94
  if (forbidden) {
95
95
  return forbidden
@@ -71,7 +71,7 @@ const DEV_REBUILD_PATH = '/__abide/reload'
71
71
  /*
72
72
  Unlike the framework's own plumbing routes above (the socket multiplex, MCP
73
73
  endpoint, CLI download), the OpenAPI document describes the app's public HTTP
74
- surface — the /rpc/* verbs — rather than abide internals, so it sits at the
74
+ surface — the /rpc/* rpcs — rather than abide internals, so it sits at the
75
75
  conventional root path where external tooling and scanners expect to find it
76
76
  (/openapi.json, alongside /swagger.json, /.well-known/*) rather than under the
77
77
  /__abide/ namespace.
@@ -81,7 +81,7 @@ const OPENAPI_PATH = '/openapi.json'
81
81
  /*
82
82
  Starts a Bun HTTP server that ties together the framework conventions:
83
83
  page.abide under src/ui/pages/ for views (layout.abide wraps the pages beneath
84
- it), one named export per file under src/server/rpc/ for verb-bound remote
84
+ it), one named export per file under src/server/rpc/ for rpc-bound remote
85
85
  functions, one named export
86
86
  per file under src/server/sockets/ for broadcast sockets, and an optional
87
87
  app.ts for boot-time setup, request middleware, and error fallback. Page
@@ -120,7 +120,7 @@ export async function createServer({
120
120
  /*
121
121
  Bun's server-wide request body ceiling, enforced natively by Bun.serve
122
122
  (its own default is ~128MB). Surfaced as an option + env so deployments
123
- can raise/lower it; per-verb tightening is the verbs' maxBodySize.
123
+ can raise/lower it; per-rpc tightening is the rpcs' maxBodySize.
124
124
  */
125
125
  maxRequestBodySize = parseBoundedEnvInt(
126
126
  process.env.ABIDE_MAX_REQUEST_BODY_SIZE,
@@ -242,7 +242,7 @@ export async function createServer({
242
242
  isn't installed. Resolved at boot so the fetch route below can branch on it.
243
243
  */
244
244
  const inspectorHandler = await maybeMountInspector({ name: appName, version: appVersion })
245
- /* Built on first request, then reused — the verb registry is frozen after load. */
245
+ /* Built on first request, then reused — the rpc registry is frozen after load. */
246
246
  let openApiSpec: ReturnType<typeof buildOpenApiSpec> | undefined
247
247
  const cliCwd = process.cwd()
248
248
 
@@ -4,7 +4,7 @@ import { TEXT_PLAIN } from '../../shared/TEXT_PLAIN.ts'
4
4
  /*
5
5
  The framework's CSRF refusal, shared by every same-origin gate — the socket
6
6
  upgrade, the socket REST publish face, the MCP endpoint, and mutating rpc
7
- verbs — so the 403 body can't drift between surfaces. `hint` appends a
7
+ rpcs — so the 403 body can't drift between surfaces. `hint` appends a
8
8
  surface-specific remedy; rpc names its `crossOrigin: true` opt-out so the
9
9
  first developer this 403s can self-serve.
10
10
  */
@@ -1,18 +1,18 @@
1
1
  import { isReadOnlyMethod } from '../../shared/isReadOnlyMethod.ts'
2
- import type { HttpVerb } from '../../shared/types/HttpVerb.ts'
2
+ import type { HttpMethod } from '../../shared/types/HttpMethod.ts'
3
3
  import { crossOriginForbidden } from './crossOriginForbidden.ts'
4
4
  import { isCrossOriginRequest } from './isCrossOriginRequest.ts'
5
5
 
6
6
  /*
7
7
  The framework's CSRF/CSWSH posture in one place: a cross-origin browser
8
8
  request to a mutating framework endpoint is refused. Every endpoint that
9
- parses bodies ignoring Content-Type (rpc verbs, socket publish, MCP JSON-RPC)
9
+ parses bodies ignoring Content-Type (rpc rpcs, socket publish, MCP JSON-RPC)
10
10
  must gate here — a hostile page's text/plain form trick could otherwise
11
11
  smuggle a payload in with the visitor's ambient cookies; non-browser clients
12
12
  send no Origin and pass. Returns the 403 to send, or undefined to proceed.
13
13
  Mount sites declare their variation: `allowReadOnly` lets GET/HEAD reads
14
14
  through (rpc reads and socket tails stay open cross-origin), `optOut` honours
15
- a verb's explicit `crossOrigin: true`, `hint` names the remedy in the 403.
15
+ a rpc's explicit `crossOrigin: true`, `hint` names the remedy in the 403.
16
16
  */
17
17
  export function crossOriginGate(
18
18
  req: Request,
@@ -22,7 +22,7 @@ export function crossOriginGate(
22
22
  if (options.optOut) {
23
23
  return undefined
24
24
  }
25
- if (options.allowReadOnly && isReadOnlyMethod(req.method as HttpVerb)) {
25
+ if (options.allowReadOnly && isReadOnlyMethod(req.method as HttpMethod)) {
26
26
  return undefined
27
27
  }
28
28
  return isCrossOriginRequest(req, url) ? crossOriginForbidden(options.hint) : undefined