@abide/abide 0.41.1 → 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.
- package/AGENTS.md +234 -294
- package/CHANGELOG.md +82 -0
- package/README.md +51 -95
- package/package.json +3 -2
- package/src/abideLsp.ts +46 -8
- package/src/abideResolverPlugin.ts +107 -19
- package/src/buildCli.ts +1 -1
- package/src/discoveryEntry.ts +3 -3
- package/src/lib/bundle/BundleMenuItem.ts +1 -1
- package/src/lib/cli/createClient.ts +12 -12
- package/src/lib/cli/dispatchCommand.ts +1 -1
- package/src/lib/cli/printSessionStatus.ts +1 -1
- package/src/lib/cli/resolveCliTarget.ts +1 -1
- package/src/lib/cli/runCli.ts +1 -1
- package/src/lib/cli/types/CliManifest.ts +1 -1
- package/src/lib/cli/types/CliManifestEntry.ts +2 -2
- package/src/lib/mcp/annotationsForMethod.ts +5 -5
- package/src/lib/mcp/createMcpServer.ts +3 -3
- package/src/lib/mcp/mcpSurface.ts +14 -14
- package/src/lib/mcp/types/McpServerOptions.ts +1 -1
- package/src/lib/server/DELETE.ts +4 -4
- package/src/lib/server/GET.ts +4 -4
- package/src/lib/server/HEAD.ts +4 -4
- package/src/lib/server/PATCH.ts +4 -4
- package/src/lib/server/POST.ts +4 -4
- package/src/lib/server/PUT.ts +4 -4
- package/src/lib/server/agent.ts +4 -4
- package/src/lib/server/env.ts +1 -1
- package/src/lib/server/error.ts +49 -7
- package/src/lib/server/json.ts +1 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
- package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
- package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
- package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
- package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
- package/src/lib/server/rpc/parseArgs.ts +6 -6
- package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
- package/src/lib/server/rpc/registerRpc.ts +6 -0
- package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
- package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
- package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
- package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
- package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
- package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
- package/src/lib/server/rpc/unprocessed.ts +6 -7
- package/src/lib/server/rpc/validationError.ts +17 -0
- package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
- package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
- package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
- package/src/lib/server/runtime/createServer.ts +4 -4
- package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
- package/src/lib/server/runtime/crossOriginGate.ts +4 -4
- package/src/lib/server/runtime/devClientFingerprint.ts +2 -1
- package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
- package/src/lib/server/runtime/registryManifests.ts +2 -2
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
- package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
- package/src/lib/server/runtime/types/RequestStore.ts +1 -1
- package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
- package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
- package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
- package/src/lib/shared/HttpError.ts +15 -1
- package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
- package/src/lib/shared/buildRpcRequest.ts +5 -5
- package/src/lib/shared/cache.ts +50 -168
- package/src/lib/shared/carriesBodyArgs.ts +4 -4
- package/src/lib/shared/createCacheStore.ts +12 -0
- package/src/lib/shared/createRemoteFunction.ts +11 -6
- package/src/lib/shared/decodeResponse.ts +2 -2
- package/src/lib/shared/detectRpcMethod.ts +17 -0
- package/src/lib/shared/emitLogRecord.ts +3 -3
- package/src/lib/shared/escapeRegex.ts +9 -0
- package/src/lib/shared/extraForwardHeaders.ts +1 -1
- package/src/lib/shared/fileName.ts +9 -0
- package/src/lib/shared/fileStem.ts +3 -1
- package/src/lib/shared/findExportCallSite.ts +30 -273
- package/src/lib/shared/forwardHeaders.ts +2 -2
- package/src/lib/shared/httpErrorFor.ts +26 -0
- package/src/lib/shared/isAsciiWhitespace.ts +5 -0
- package/src/lib/shared/isIdentPart.ts +9 -0
- package/src/lib/shared/isIdentStart.ts +8 -0
- package/src/lib/shared/isReadOnlyMethod.ts +4 -4
- package/src/lib/shared/keyForRemoteCall.ts +4 -4
- package/src/lib/shared/log.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +20 -0
- package/src/lib/shared/pending.ts +4 -1
- package/src/lib/shared/prepareRpcModule.ts +96 -14
- package/src/lib/shared/probeRegistries.ts +17 -4
- package/src/lib/shared/programNameForPackage.ts +3 -1
- package/src/lib/shared/queryStringFromArgs.ts +1 -1
- package/src/lib/shared/remoteMetaStore.ts +2 -2
- package/src/lib/shared/resolveClientFlags.ts +1 -1
- package/src/lib/shared/skipNonCode.ts +230 -0
- package/src/lib/shared/streamResponse.ts +9 -6
- package/src/lib/shared/stripImport.ts +6 -4
- package/src/lib/shared/types/CacheEntry.ts +2 -4
- package/src/lib/shared/types/CacheOnContext.ts +1 -11
- package/src/lib/shared/types/CacheStore.ts +13 -7
- package/src/lib/shared/types/ClientFlags.ts +1 -1
- package/src/lib/shared/types/ErrorConstructors.ts +17 -0
- package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +11 -0
- package/src/lib/shared/types/HttpMethod.ts +1 -0
- package/src/lib/shared/types/Outbox.ts +9 -0
- package/src/lib/shared/types/OutboxEntry.ts +27 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
- package/src/lib/shared/types/RemoteCallable.ts +2 -2
- package/src/lib/shared/types/RemoteFunction.ts +21 -6
- package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
- package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
- package/src/lib/shared/types/RpcInvoker.ts +1 -1
- package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
- package/src/lib/shared/types/ValidationErrorData.ts +20 -0
- package/src/lib/shared/url.ts +3 -3
- package/src/lib/shared/writeRpcDts.ts +7 -7
- package/src/lib/shared/writeTestRpcDts.ts +3 -3
- package/src/lib/test/createTestApp.ts +10 -10
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +45 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/awaitPlan.ts +48 -0
- package/src/lib/ui/compile/compileShadow.ts +117 -31
- package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
- package/src/lib/ui/compile/desugarSignals.ts +42 -6
- package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
- package/src/lib/ui/compile/generateBuild.ts +45 -62
- package/src/lib/ui/compile/generateSSR.ts +90 -60
- package/src/lib/ui/compile/ifPlan.ts +30 -0
- package/src/lib/ui/compile/makeVarNamer.ts +10 -0
- package/src/lib/ui/compile/offsetToPosition.ts +9 -0
- package/src/lib/ui/compile/parseTemplate.ts +565 -115
- package/src/lib/ui/compile/stripEffects.ts +17 -9
- package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
- package/src/lib/ui/compile/switchPlan.ts +22 -0
- package/src/lib/ui/compile/tryPlan.ts +29 -0
- package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +15 -2
- package/src/lib/ui/createScope.ts +20 -2
- package/src/lib/ui/dom/appendText.ts +1 -5
- package/src/lib/ui/dom/applyResolved.ts +1 -5
- package/src/lib/ui/dom/disposeRange.ts +6 -10
- package/src/lib/ui/dom/hydrate.ts +2 -5
- package/src/lib/ui/dom/isComment.ts +6 -0
- package/src/lib/ui/dom/mount.ts +1 -2
- package/src/lib/ui/dom/withScope.ts +6 -9
- package/src/lib/ui/effect.ts +4 -2
- package/src/lib/ui/navigate.ts +40 -13
- package/src/lib/ui/outbox.ts +30 -110
- package/src/lib/ui/remoteProxy.ts +160 -10
- package/src/lib/ui/router.ts +3 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
- package/src/lib/ui/runtime/createDoc.ts +3 -3
- package/src/lib/ui/runtime/pathSegments.ts +10 -0
- package/src/lib/ui/socketChannel.ts +7 -1
- package/src/lib/ui/types/Scope.ts +20 -5
- package/template/.zed/settings.json +4 -0
- package/template/CLAUDE.md +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +4 -4
- package/template/test/app.test.ts +2 -2
- package/src/lib/server/rpc/registerVerb.ts +0 -6
- package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
- package/src/lib/shared/detectVerbMethod.ts +0 -17
- package/src/lib/shared/types/HttpVerb.ts +0 -1
- package/src/lib/ui/types/Outbox.ts +0 -14
package/src/lib/server/agent.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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))`.
|
package/src/lib/server/env.ts
CHANGED
|
@@ -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
|
|
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
|
package/src/lib/server/error.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
package/src/lib/server/json.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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 {
|
|
15
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
27
|
+
const RPC_TIMEOUT_ABORT = Symbol('abideRpcTimeoutAbort')
|
|
26
28
|
|
|
27
|
-
/*
|
|
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
|
|
33
|
-
rewrites every `export const
|
|
34
|
-
so the
|
|
35
|
-
`src/server/rpc/`, with `/rpc/` prefix) are threaded into
|
|
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
|
|
49
|
-
method:
|
|
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-
|
|
61
|
+
/* Per-rpc cap on actual received body bytes (413 past it); omitted = Bun's server-wide maxRequestBodySize. */
|
|
59
62
|
maxBodySize?: number
|
|
60
|
-
/* Per-
|
|
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
|
|
71
|
-
deliberately). MCP only auto-exposes read-only
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
159
|
+
function abortRpcTimeout(): void {
|
|
142
160
|
const req = requestContext.getStore()?.req as
|
|
143
|
-
| (Request & { [
|
|
161
|
+
| (Request & { [RPC_TIMEOUT_ABORT]?: AbortController })
|
|
144
162
|
| undefined
|
|
145
|
-
req?.[
|
|
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
|
-
|
|
174
|
+
rpc's outbound fetch is never cancelled) — then 504.
|
|
157
175
|
*/
|
|
158
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
25
|
+
export function dispatchRpcInProcess({
|
|
26
26
|
remote,
|
|
27
27
|
args,
|
|
28
28
|
baseUrl,
|
|
@@ -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
|
|
3
|
-
import {
|
|
2
|
+
import { rpcRegistry } from './rpcRegistry.ts'
|
|
3
|
+
import type { RpcRegistryEntry } from './types/RpcRegistryEntry.ts'
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
Finds the registered
|
|
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
|
|
12
|
-
for (const entry of
|
|
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 {
|
|
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
|
|
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-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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> {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RpcRegistryEntry } from './types/RpcRegistryEntry.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
Process-wide registry of every
|
|
5
|
-
|
|
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
|
|
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
|
|
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
|
|
9
|
-
request().signal; see
|
|
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
|
|
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
|
|
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
|
-
|
|
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>>
|