@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.
- package/AGENTS.md +234 -300
- package/CHANGELOG.md +34 -0
- package/README.md +50 -93
- package/package.json +3 -2
- package/src/abideResolverPlugin.ts +104 -14
- 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/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/extraForwardHeaders.ts +1 -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/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 +3 -3
- 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/awaitPlan.ts +48 -0
- package/src/lib/ui/compile/compileShadow.ts +75 -21
- package/src/lib/ui/compile/desugarSignals.ts +42 -6
- package/src/lib/ui/compile/generateBuild.ts +43 -60
- package/src/lib/ui/compile/generateSSR.ts +84 -54
- package/src/lib/ui/compile/ifPlan.ts +30 -0
- package/src/lib/ui/compile/parseTemplate.ts +41 -11
- package/src/lib/ui/compile/stripEffects.ts +17 -9
- package/src/lib/ui/compile/switchPlan.ts +22 -0
- package/src/lib/ui/compile/tryPlan.ts +29 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +6 -2
- package/src/lib/ui/createScope.ts +20 -2
- package/src/lib/ui/dom/disposeRange.ts +6 -10
- package/src/lib/ui/dom/hydrate.ts +2 -5
- 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/socketChannel.ts +7 -1
- package/src/lib/ui/types/Scope.ts +20 -5
- package/template/CLAUDE.md +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- 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
|
@@ -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>>
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
|
33
|
-
`crossOrigin: true` exempts a mutating
|
|
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
|
|
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
|
-
- `
|
|
58
|
+
- `Rpc(fn, { clients })` — schemaless but with explicit client
|
|
44
59
|
targeting (e.g. server-internal RPC with `clients: { browser: false }`).
|
|
45
|
-
- `
|
|
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
|
-
|
|
64
|
+
type RpcHelperOf<Opts> = {
|
|
50
65
|
/*
|
|
51
|
-
`
|
|
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:
|
|
85
|
+
opts: Opts & {
|
|
69
86
|
inputSchema: InputSchema
|
|
70
87
|
filesSchema: FilesSchema
|
|
88
|
+
errors?: Errors
|
|
71
89
|
},
|
|
72
|
-
): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
fn: RemoteHandler<
|
|
79
|
-
opts:
|
|
80
|
-
): RemoteFunction<
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9
|
+
export function unprocessed(method: string): never {
|
|
11
10
|
throw new Error(
|
|
12
|
-
`[abide] \`${
|
|
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 {
|
|
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
|
|
10
|
-
at call time (not cached) so
|
|
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
|
|
14
|
-
undefined to mark a
|
|
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
|
|
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 {
|
|
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 {
|
|
4
|
+
import { rpcRegistry } from '../rpc/rpcRegistry.ts'
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
Turns a
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/*
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|