@zudojs/rpc 1.3.0 → 1.4.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/README.md +120 -17
- package/dist/index.d.ts +12 -8
- package/dist/index.js +14 -5
- package/dist/rpc/client/index.d.ts +5 -0
- package/dist/rpc/client/index.js +5 -0
- package/dist/rpc/client/rpcClient.core.d.ts +6 -7
- package/dist/rpc/client/rpcClient.core.js +32 -46
- package/dist/rpc/client/rpcWireError.helper.d.ts +13 -0
- package/dist/rpc/client/rpcWireError.helper.js +71 -0
- package/dist/rpc/constants/index.d.ts +6 -1
- package/dist/rpc/constants/index.js +6 -1
- package/dist/rpc/constants/rpcConstants.core.d.ts +11 -0
- package/dist/rpc/constants/rpcConstants.core.js +11 -0
- package/dist/rpc/context/rpcContext.type.d.ts +7 -0
- package/dist/rpc/dispatcher/rpcDispatcher.core.js +14 -1
- package/dist/rpc/reliability/cancellation/rpcAbort.helper.d.ts +17 -0
- package/dist/rpc/reliability/cancellation/rpcAbort.helper.js +40 -0
- package/dist/rpc/reliability/retry/rpcRetry.helper.js +3 -1
- package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +5 -3
- package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +5 -4
- package/dist/rpc/server/index.d.ts +6 -0
- package/dist/rpc/server/index.js +5 -0
- package/dist/rpc/server/rpcBaseErrorMapping.helper.d.ts +16 -0
- package/dist/rpc/server/rpcBaseErrorMapping.helper.js +57 -0
- package/dist/rpc/server/rpcErrorMapping.helper.d.ts +35 -0
- package/dist/rpc/server/rpcErrorMapping.helper.js +101 -0
- package/dist/rpc/server/rpcServer.core.d.ts +0 -7
- package/dist/rpc/server/rpcServer.core.js +7 -91
- package/dist/rpc/transport/codec/index.d.ts +10 -0
- package/dist/rpc/transport/codec/index.js +8 -0
- package/dist/rpc/transport/codec/rpcBody.helper.d.ts +36 -0
- package/dist/rpc/transport/codec/rpcBody.helper.js +64 -0
- package/dist/rpc/transport/codec/rpcCodec.helper.d.ts +41 -0
- package/dist/rpc/transport/codec/rpcCodec.helper.js +40 -0
- package/dist/rpc/transport/http/index.d.ts +12 -0
- package/dist/rpc/transport/http/index.js +10 -0
- package/dist/rpc/transport/http/rpcFetchHandler.core.d.ts +36 -0
- package/dist/rpc/transport/http/rpcFetchHandler.core.js +97 -0
- package/dist/rpc/transport/http/rpcHttpExchange.helper.d.ts +27 -0
- package/dist/rpc/transport/http/rpcHttpExchange.helper.js +78 -0
- package/dist/rpc/transport/http/rpcHttpStatus.helper.d.ts +16 -0
- package/dist/rpc/transport/http/rpcHttpStatus.helper.js +37 -0
- package/dist/rpc/transport/http/rpcHttpTransport.core.d.ts +36 -0
- package/dist/rpc/transport/http/rpcHttpTransport.core.js +45 -0
- package/dist/rpc/transport/index.d.ts +12 -0
- package/dist/rpc/transport/index.js +9 -1
- package/dist/rpc/transport/memory/index.d.ts +7 -0
- package/dist/rpc/transport/memory/index.js +6 -0
- package/dist/rpc/transport/memory/rpcMemoryTransport.core.d.ts +42 -0
- package/dist/rpc/transport/memory/rpcMemoryTransport.core.js +73 -0
- package/dist/rpc/validation/rpcUnsafeKey.helper.d.ts +18 -0
- package/dist/rpc/validation/rpcUnsafeKey.helper.js +60 -0
- package/dist/rpc/validation/rpcValidation.core.d.ts +11 -2
- package/dist/rpc/validation/rpcValidation.core.js +11 -2
- package/package.json +9 -7
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createSerializer } from "@zudojs/serialization";
|
|
2
|
+
import { DEFAULT_RPC_HTTP_MAX_BODY_BYTES, MAX_RPC_FRAME_DEPTH, } from "../../constants/rpcConstants.core.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates the default frame serializer: `@zudojs/serialization` JSON with
|
|
5
|
+
* size and depth limits, so decoding an untrusted frame is bounded.
|
|
6
|
+
*/
|
|
7
|
+
export function createRPCJsonSerializer(options = {}) {
|
|
8
|
+
return createSerializer("json", {
|
|
9
|
+
maxSize: options.maxSize ?? DEFAULT_RPC_HTTP_MAX_BODY_BYTES,
|
|
10
|
+
maxDepth: options.maxDepth ?? MAX_RPC_FRAME_DEPTH,
|
|
11
|
+
preserveTypes: options.preserveTypes ?? false,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Determines whether a decoded value has the shape of an
|
|
16
|
+
* {@link RPCResponse}: a string `id`, a boolean `success`, and — when
|
|
17
|
+
* present — an `error` with string `code` and `message`.
|
|
18
|
+
*
|
|
19
|
+
* Transports check this before handing a peer's reply to the client, so
|
|
20
|
+
* a proxy's HTML error page or a truncated body is reported as a
|
|
21
|
+
* transport failure instead of being read as a result.
|
|
22
|
+
*/
|
|
23
|
+
export function isRPCResponseFrame(value) {
|
|
24
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const frame = value;
|
|
28
|
+
if (typeof frame.id !== "string" || typeof frame.success !== "boolean") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (frame.success) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const error = frame.error;
|
|
35
|
+
return (typeof error === "object" &&
|
|
36
|
+
error !== null &&
|
|
37
|
+
typeof error.code === "string" &&
|
|
38
|
+
typeof error.message === "string");
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=rpcCodec.helper.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport over the web-standard Fetch API: a client transport that
|
|
3
|
+
* POSTs frames with `fetch`, a `(request: Request) => Promise<Response>`
|
|
4
|
+
* server handler any HTTP server can mount, and the wire-code to HTTP
|
|
5
|
+
* status map they share.
|
|
6
|
+
*/
|
|
7
|
+
export type { RPCHttpHeaders, RPCHttpTransportOptions, } from "./rpcHttpTransport.core.js";
|
|
8
|
+
export { createRPCHttpTransport } from "./rpcHttpTransport.core.js";
|
|
9
|
+
export type { RPCFetchHandlerOptions } from "./rpcFetchHandler.core.js";
|
|
10
|
+
export { createRPCFetchHandler } from "./rpcFetchHandler.core.js";
|
|
11
|
+
export { RPC_HTTP_STATUS, rpcHttpStatus } from "./rpcHttpStatus.helper.js";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport over the web-standard Fetch API: a client transport that
|
|
3
|
+
* POSTs frames with `fetch`, a `(request: Request) => Promise<Response>`
|
|
4
|
+
* server handler any HTTP server can mount, and the wire-code to HTTP
|
|
5
|
+
* status map they share.
|
|
6
|
+
*/
|
|
7
|
+
export { createRPCHttpTransport } from "./rpcHttpTransport.core.js";
|
|
8
|
+
export { createRPCFetchHandler } from "./rpcFetchHandler.core.js";
|
|
9
|
+
export { RPC_HTTP_STATUS, rpcHttpStatus } from "./rpcHttpStatus.helper.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RPCAuthContext } from "../../context/rpcContext.type.js";
|
|
2
|
+
import type { RPCFrameHandler } from "../memory/rpcMemoryTransport.core.js";
|
|
3
|
+
import type { RPCFrameSerializer } from "../codec/rpcCodec.helper.js";
|
|
4
|
+
/** Options for {@link createRPCFetchHandler}. */
|
|
5
|
+
export interface RPCFetchHandlerOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Derives trusted identity from the HTTP request (verify a bearer token,
|
|
8
|
+
* read an mTLS peer, look up a session). The result reaches middleware
|
|
9
|
+
* and handlers as `context.auth`. Throw an `RPCAuthenticationError` to
|
|
10
|
+
* refuse the call; any other error is answered as an internal error.
|
|
11
|
+
*/
|
|
12
|
+
readonly auth?: (request: Request) => RPCAuthContext | undefined | Promise<RPCAuthContext | undefined>;
|
|
13
|
+
/** Frame serializer. Must match the clients'. Defaults to plain JSON. */
|
|
14
|
+
readonly serializer?: RPCFrameSerializer;
|
|
15
|
+
/** Largest request body accepted, in bytes. */
|
|
16
|
+
readonly maxBodyBytes?: number;
|
|
17
|
+
/** Receives failures answered with the generic internal error. */
|
|
18
|
+
readonly onInternalError?: (error: unknown, requestId: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates a web-standard handler, `(request: Request) => Promise<Response>`,
|
|
22
|
+
* that serves an `RPCServer` over HTTP. Mount it on any server that speaks
|
|
23
|
+
* the Fetch API (`@zudojs/http`, Node's `http` via an adapter, Bun, Deno,
|
|
24
|
+
* edge runtimes) at one POST endpoint.
|
|
25
|
+
*
|
|
26
|
+
* Every reply is an RPC frame: a request that is not POST, not JSON,
|
|
27
|
+
* oversized, or unparseable is answered with an `RPC_INVALID_REQUEST` or
|
|
28
|
+
* `RPC_DESERIALIZATION_ERROR` frame, never an HTML page. Stack traces and
|
|
29
|
+
* internal messages never leave the process — the server replaces them
|
|
30
|
+
* with `INTERNAL_ERROR_MESSAGE`, and this handler does the same for
|
|
31
|
+
* failures of its own (a result that cannot be serialized, a throwing
|
|
32
|
+
* `auth` hook). The call runs under `request.signal`: a client that
|
|
33
|
+
* disconnects cancels the procedure (its `context.signal` aborts).
|
|
34
|
+
*/
|
|
35
|
+
export declare function createRPCFetchHandler(server: RPCFrameHandler, options?: RPCFetchHandlerOptions): (request: Request) => Promise<Response>;
|
|
36
|
+
//# sourceMappingURL=rpcFetchHandler.core.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createRPCJsonSerializer } from "../codec/rpcCodec.helper.js";
|
|
2
|
+
import { readBoundedBody } from "../codec/rpcBody.helper.js";
|
|
3
|
+
import { createRPCErrorResponse } from "../../types/rpcResponse.type.js";
|
|
4
|
+
import { mapRPCError } from "../../server/rpcErrorMapping.helper.js";
|
|
5
|
+
import { DEFAULT_RPC_HTTP_MAX_BODY_BYTES, INTERNAL_ERROR_MESSAGE, MAX_RPC_REQUEST_ID_LENGTH, } from "../../constants/rpcConstants.core.js";
|
|
6
|
+
import { rpcHttpStatus } from "./rpcHttpStatus.helper.js";
|
|
7
|
+
const JSON_TYPE = /^application\/(?:[\w.+-]+\+)?json(?:\s*;|$)/i;
|
|
8
|
+
/**
|
|
9
|
+
* Creates a web-standard handler, `(request: Request) => Promise<Response>`,
|
|
10
|
+
* that serves an `RPCServer` over HTTP. Mount it on any server that speaks
|
|
11
|
+
* the Fetch API (`@zudojs/http`, Node's `http` via an adapter, Bun, Deno,
|
|
12
|
+
* edge runtimes) at one POST endpoint.
|
|
13
|
+
*
|
|
14
|
+
* Every reply is an RPC frame: a request that is not POST, not JSON,
|
|
15
|
+
* oversized, or unparseable is answered with an `RPC_INVALID_REQUEST` or
|
|
16
|
+
* `RPC_DESERIALIZATION_ERROR` frame, never an HTML page. Stack traces and
|
|
17
|
+
* internal messages never leave the process — the server replaces them
|
|
18
|
+
* with `INTERNAL_ERROR_MESSAGE`, and this handler does the same for
|
|
19
|
+
* failures of its own (a result that cannot be serialized, a throwing
|
|
20
|
+
* `auth` hook). The call runs under `request.signal`: a client that
|
|
21
|
+
* disconnects cancels the procedure (its `context.signal` aborts).
|
|
22
|
+
*/
|
|
23
|
+
export function createRPCFetchHandler(server, options = {}) {
|
|
24
|
+
const serializer = options.serializer ?? createRPCJsonSerializer();
|
|
25
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_RPC_HTTP_MAX_BODY_BYTES;
|
|
26
|
+
const reply = (frame, extra) => {
|
|
27
|
+
let text;
|
|
28
|
+
let status = rpcHttpStatus(frame);
|
|
29
|
+
try {
|
|
30
|
+
text = serializer.serialize(frame);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
options.onInternalError?.(error, frame.id);
|
|
34
|
+
const fallback = createRPCErrorResponse(frame.id, {
|
|
35
|
+
code: "RPC_SERIALIZATION_ERROR",
|
|
36
|
+
message: INTERNAL_ERROR_MESSAGE,
|
|
37
|
+
});
|
|
38
|
+
text = serializer.serialize(fallback);
|
|
39
|
+
status = rpcHttpStatus(fallback);
|
|
40
|
+
}
|
|
41
|
+
return new Response(text, {
|
|
42
|
+
status,
|
|
43
|
+
headers: {
|
|
44
|
+
"content-type": "application/json; charset=utf-8",
|
|
45
|
+
"cache-control": "no-store",
|
|
46
|
+
"x-content-type-options": "nosniff",
|
|
47
|
+
...extra,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const refuse = (code, message, extra) => reply(createRPCErrorResponse("", { code, message }), extra);
|
|
52
|
+
return async (request) => {
|
|
53
|
+
if (request.method !== "POST") {
|
|
54
|
+
return refuse("RPC_INVALID_REQUEST", "RPC requests must use POST.", { allow: "POST" });
|
|
55
|
+
}
|
|
56
|
+
if (!JSON_TYPE.test(request.headers.get("content-type") ?? "")) {
|
|
57
|
+
return refuse("RPC_INVALID_REQUEST", "RPC requests must be sent as application/json.");
|
|
58
|
+
}
|
|
59
|
+
const body = await readBoundedBody(request, maxBodyBytes);
|
|
60
|
+
if (!body.ok) {
|
|
61
|
+
return refuse("RPC_INVALID_REQUEST", body.reason === "too-large"
|
|
62
|
+
? `RPC request body exceeds ${maxBodyBytes} bytes.`
|
|
63
|
+
: "RPC request body could not be read.");
|
|
64
|
+
}
|
|
65
|
+
let frame;
|
|
66
|
+
try {
|
|
67
|
+
frame = serializer.deserialize(body.text);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return refuse("RPC_DESERIALIZATION_ERROR", "RPC request body is not valid JSON.");
|
|
71
|
+
}
|
|
72
|
+
const frameId = readFrameId(frame);
|
|
73
|
+
let auth;
|
|
74
|
+
try {
|
|
75
|
+
auth = await options.auth?.(request);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const mapped = mapRPCError(error);
|
|
79
|
+
if (mapped.internal) {
|
|
80
|
+
options.onInternalError?.(error, frameId);
|
|
81
|
+
}
|
|
82
|
+
return reply(createRPCErrorResponse(frameId, mapped.payload));
|
|
83
|
+
}
|
|
84
|
+
const response = await server.handle(frame, auth === undefined ? { signal: request.signal } : { auth, signal: request.signal });
|
|
85
|
+
return reply(response);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reads a frame's id for error replies built before the server has
|
|
90
|
+
* validated the frame. Only a short, safe id is echoed, so the handler
|
|
91
|
+
* never reflects arbitrary caller input.
|
|
92
|
+
*/
|
|
93
|
+
function readFrameId(frame) {
|
|
94
|
+
const id = frame?.id;
|
|
95
|
+
return typeof id === "string" && id.length <= MAX_RPC_REQUEST_ID_LENGTH && /^[\w.:-]+$/.test(id) ? id : "";
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=rpcFetchHandler.core.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request encoding, fetch error typing and response decoding for
|
|
3
|
+
* `createRPCHttpTransport`.
|
|
4
|
+
*/
|
|
5
|
+
import type { RPCRequest } from "../../types/rpcRequest.type.js";
|
|
6
|
+
import type { RPCResponse } from "../../types/rpcResponse.type.js";
|
|
7
|
+
import type { RPCTransportRequestOptions } from "../rpcTransport.type.js";
|
|
8
|
+
import type { RPCFrameSerializer } from "../codec/rpcCodec.helper.js";
|
|
9
|
+
/**
|
|
10
|
+
* Links the caller signal and the deadline into one abort signal.
|
|
11
|
+
*
|
|
12
|
+
* The deadline is clamped to the timer range, as the client's own timer
|
|
13
|
+
* is: `AbortSignal.timeout` fires after 1 ms for anything past 2^31-1 ms
|
|
14
|
+
* (about 24.8 days) and throws for a non-integer beyond 2^32-1, so a long
|
|
15
|
+
* or `Infinity` timeout used to fail every call at once.
|
|
16
|
+
*/
|
|
17
|
+
export declare function linkSignals(options: RPCTransportRequestOptions): {
|
|
18
|
+
readonly signal: AbortSignal;
|
|
19
|
+
dispose(): void;
|
|
20
|
+
};
|
|
21
|
+
/** Runs the fetch, typing a failure as transport, timeout or cancellation. */
|
|
22
|
+
export declare function fetchOrThrow(run: () => Promise<Response>, signal: AbortSignal, options: RPCTransportRequestOptions, procedure: string): Promise<Response>;
|
|
23
|
+
/** Serializes a request frame. */
|
|
24
|
+
export declare function encode(serializer: RPCFrameSerializer, request: RPCRequest): string;
|
|
25
|
+
/** Reads, parses and shape-checks the reply to `request`. */
|
|
26
|
+
export declare function decode(serializer: RPCFrameSerializer, response: Response, maxBytes: number, request: RPCRequest): Promise<RPCResponse>;
|
|
27
|
+
//# sourceMappingURL=rpcHttpExchange.helper.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request encoding, fetch error typing and response decoding for
|
|
3
|
+
* `createRPCHttpTransport`.
|
|
4
|
+
*/
|
|
5
|
+
import { isRPCResponseFrame } from "../codec/rpcCodec.helper.js";
|
|
6
|
+
import { readBoundedBody } from "../codec/rpcBody.helper.js";
|
|
7
|
+
import { RPCSerializationError, RPCTimeoutError, RPCTransportError, } from "../../errors/rpc.errors.js";
|
|
8
|
+
import { combineSignals } from "../../reliability/cancellation/rpcCancellation.helper.js";
|
|
9
|
+
import { abortReasonToRPCError } from "../../reliability/cancellation/rpcAbort.helper.js";
|
|
10
|
+
import { MAX_TIMER_DELAY } from "../../constants/rpcConstants.core.js";
|
|
11
|
+
/**
|
|
12
|
+
* Links the caller signal and the deadline into one abort signal.
|
|
13
|
+
*
|
|
14
|
+
* The deadline is clamped to the timer range, as the client's own timer
|
|
15
|
+
* is: `AbortSignal.timeout` fires after 1 ms for anything past 2^31-1 ms
|
|
16
|
+
* (about 24.8 days) and throws for a non-integer beyond 2^32-1, so a long
|
|
17
|
+
* or `Infinity` timeout used to fail every call at once.
|
|
18
|
+
*/
|
|
19
|
+
export function linkSignals(options) {
|
|
20
|
+
const timeout = options.timeout;
|
|
21
|
+
const deadline = timeout !== undefined && timeout > 0
|
|
22
|
+
? AbortSignal.timeout(Math.min(Math.ceil(timeout), MAX_TIMER_DELAY))
|
|
23
|
+
: undefined;
|
|
24
|
+
return combineSignals(options.signal, deadline);
|
|
25
|
+
}
|
|
26
|
+
/** Runs the fetch, typing a failure as transport, timeout or cancellation. */
|
|
27
|
+
export async function fetchOrThrow(run, signal, options, procedure) {
|
|
28
|
+
try {
|
|
29
|
+
return await run();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!signal.aborted) {
|
|
33
|
+
throw withCause(new RPCTransportError("RPC HTTP request failed.", procedure), error);
|
|
34
|
+
}
|
|
35
|
+
if (options.signal?.aborted) {
|
|
36
|
+
throw abortReasonToRPCError(options.signal, procedure);
|
|
37
|
+
}
|
|
38
|
+
throw new RPCTimeoutError(options.timeout ?? 0, procedure);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Serializes a request frame. */
|
|
42
|
+
export function encode(serializer, request) {
|
|
43
|
+
try {
|
|
44
|
+
return serializer.serialize(request);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw withCause(new RPCSerializationError("RPC request could not be serialized.", request.procedure), error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Reads, parses and shape-checks the reply to `request`. */
|
|
51
|
+
export async function decode(serializer, response, maxBytes, request) {
|
|
52
|
+
const read = await readBoundedBody(response, maxBytes);
|
|
53
|
+
const failure = (detail) => new RPCTransportError(`RPC server replied with HTTP ${response.status} and ${detail}.`, request.procedure);
|
|
54
|
+
if (!read.ok) {
|
|
55
|
+
throw failure(read.reason === "too-large" ? "an oversized body" : "an unreadable body");
|
|
56
|
+
}
|
|
57
|
+
let frame;
|
|
58
|
+
try {
|
|
59
|
+
frame = serializer.deserialize(read.text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw failure("a body that is not valid JSON");
|
|
63
|
+
}
|
|
64
|
+
if (!isRPCResponseFrame(frame) || (frame.id !== request.id && frame.id !== "")) {
|
|
65
|
+
throw failure("a body that is not a response to this request");
|
|
66
|
+
}
|
|
67
|
+
return frame;
|
|
68
|
+
}
|
|
69
|
+
function withCause(error, cause) {
|
|
70
|
+
Object.defineProperty(error, "cause", {
|
|
71
|
+
value: cause,
|
|
72
|
+
enumerable: false,
|
|
73
|
+
configurable: true,
|
|
74
|
+
writable: true,
|
|
75
|
+
});
|
|
76
|
+
return error;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=rpcHttpExchange.helper.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RPCResponse } from "../../types/rpcResponse.type.js";
|
|
2
|
+
/**
|
|
3
|
+
* HTTP status used by {@link createRPCFetchHandler} for each wire code.
|
|
4
|
+
*
|
|
5
|
+
* The status is advisory — for proxies, logs and dashboards. The frame's
|
|
6
|
+
* `error.code` is authoritative, and {@link createRPCHttpTransport} reads
|
|
7
|
+
* any well-formed frame whatever status it arrives with. Codes not listed
|
|
8
|
+
* here (custom `RPCError` codes) are sent as 500.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RPC_HTTP_STATUS: Readonly<Record<string, number>>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the HTTP status for a response frame: 200 for success, the
|
|
13
|
+
* mapped status for a known error code, and 500 otherwise.
|
|
14
|
+
*/
|
|
15
|
+
export declare function rpcHttpStatus(response: RPCResponse): number;
|
|
16
|
+
//# sourceMappingURL=rpcHttpStatus.helper.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP status used by {@link createRPCFetchHandler} for each wire code.
|
|
3
|
+
*
|
|
4
|
+
* The status is advisory — for proxies, logs and dashboards. The frame's
|
|
5
|
+
* `error.code` is authoritative, and {@link createRPCHttpTransport} reads
|
|
6
|
+
* any well-formed frame whatever status it arrives with. Codes not listed
|
|
7
|
+
* here (custom `RPCError` codes) are sent as 500.
|
|
8
|
+
*/
|
|
9
|
+
export const RPC_HTTP_STATUS = Object.freeze({
|
|
10
|
+
RPC_INVALID_REQUEST: 400,
|
|
11
|
+
RPC_DESERIALIZATION_ERROR: 400,
|
|
12
|
+
RPC_UNAUTHENTICATED: 401,
|
|
13
|
+
RPC_FORBIDDEN: 403,
|
|
14
|
+
RPC_PROCEDURE_NOT_FOUND: 404,
|
|
15
|
+
RPC_NOT_FOUND: 404,
|
|
16
|
+
RPC_CONFLICT: 409,
|
|
17
|
+
RPC_VALIDATION_ERROR: 422,
|
|
18
|
+
RPC_RATE_LIMITED: 429,
|
|
19
|
+
RPC_CANCELLED: 499,
|
|
20
|
+
RPC_INTERNAL_ERROR: 500,
|
|
21
|
+
RPC_SERIALIZATION_ERROR: 500,
|
|
22
|
+
RPC_UNAVAILABLE: 503,
|
|
23
|
+
RPC_TIMEOUT: 504,
|
|
24
|
+
RPC_DEADLINE_EXCEEDED: 504,
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Returns the HTTP status for a response frame: 200 for success, the
|
|
28
|
+
* mapped status for a known error code, and 500 otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export function rpcHttpStatus(response) {
|
|
31
|
+
if (response.success) {
|
|
32
|
+
return 200;
|
|
33
|
+
}
|
|
34
|
+
const code = response.error?.code;
|
|
35
|
+
return (code !== undefined ? RPC_HTTP_STATUS[code] : undefined) ?? 500;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=rpcHttpStatus.helper.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RPCRequest } from "../../types/rpcRequest.type.js";
|
|
2
|
+
import type { RPCTransport } from "../rpcTransport.type.js";
|
|
3
|
+
import type { RPCFrameSerializer } from "../codec/rpcCodec.helper.js";
|
|
4
|
+
/** Headers accepted by the `Headers` constructor. */
|
|
5
|
+
export type RPCHttpHeaders = ConstructorParameters<typeof Headers>[0];
|
|
6
|
+
/**
|
|
7
|
+
* Options for {@link createRPCHttpTransport}.
|
|
8
|
+
*/
|
|
9
|
+
export interface RPCHttpTransportOptions {
|
|
10
|
+
/** Endpoint the server's fetch handler is mounted on. */
|
|
11
|
+
readonly url: string | URL;
|
|
12
|
+
/**
|
|
13
|
+
* Extra request headers — a fixed set, or a function called per request
|
|
14
|
+
* (for a short-lived bearer token). They cannot replace `content-type`.
|
|
15
|
+
*/
|
|
16
|
+
readonly headers?: RPCHttpHeaders | ((request: RPCRequest) => RPCHttpHeaders | Promise<RPCHttpHeaders>);
|
|
17
|
+
/** Fetch implementation. Defaults to the global `fetch`. */
|
|
18
|
+
readonly fetch?: (input: string | URL, init: RequestInit) => Promise<Response>;
|
|
19
|
+
/** Frame serializer. Must match the server's. Defaults to plain JSON. */
|
|
20
|
+
readonly serializer?: RPCFrameSerializer;
|
|
21
|
+
/** Largest response body accepted, in bytes. */
|
|
22
|
+
readonly maxResponseBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates a transport that POSTs each frame to a remote
|
|
26
|
+
* {@link createRPCFetchHandler} with the global `fetch`.
|
|
27
|
+
*
|
|
28
|
+
* The client's signal and deadline (`options.timeout`) both abort the
|
|
29
|
+
* underlying request, so a timed-out call releases its connection.
|
|
30
|
+
* Failures are typed: a network error or a reply that is not an RPC frame
|
|
31
|
+
* is an `RPCTransportError`, an expired deadline an `RPCTimeoutError`, a
|
|
32
|
+
* caller abort an `RPCCancelledError`. An error frame from the server is
|
|
33
|
+
* returned as a response for the client to rebuild into a typed error.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createRPCHttpTransport(options: RPCHttpTransportOptions): RPCTransport;
|
|
36
|
+
//# sourceMappingURL=rpcHttpTransport.core.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createRPCJsonSerializer } from "../codec/rpcCodec.helper.js";
|
|
2
|
+
import { RPCUnavailableError } from "../../errors/rpc.errors.js";
|
|
3
|
+
import { DEFAULT_RPC_HTTP_MAX_BODY_BYTES } from "../../constants/rpcConstants.core.js";
|
|
4
|
+
import { decode, encode, fetchOrThrow, linkSignals } from "./rpcHttpExchange.helper.js";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a transport that POSTs each frame to a remote
|
|
7
|
+
* {@link createRPCFetchHandler} with the global `fetch`.
|
|
8
|
+
*
|
|
9
|
+
* The client's signal and deadline (`options.timeout`) both abort the
|
|
10
|
+
* underlying request, so a timed-out call releases its connection.
|
|
11
|
+
* Failures are typed: a network error or a reply that is not an RPC frame
|
|
12
|
+
* is an `RPCTransportError`, an expired deadline an `RPCTimeoutError`, a
|
|
13
|
+
* caller abort an `RPCCancelledError`. An error frame from the server is
|
|
14
|
+
* returned as a response for the client to rebuild into a typed error.
|
|
15
|
+
*/
|
|
16
|
+
export function createRPCHttpTransport(options) {
|
|
17
|
+
const serializer = options.serializer ?? createRPCJsonSerializer();
|
|
18
|
+
const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
|
|
19
|
+
const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_RPC_HTTP_MAX_BODY_BYTES;
|
|
20
|
+
const url = new URL(options.url);
|
|
21
|
+
let closed = false;
|
|
22
|
+
return {
|
|
23
|
+
async send(request, sendOptions = {}) {
|
|
24
|
+
if (closed) {
|
|
25
|
+
throw new RPCUnavailableError("RPC HTTP transport has been closed.", request.procedure);
|
|
26
|
+
}
|
|
27
|
+
const body = encode(serializer, request);
|
|
28
|
+
const signals = linkSignals(sendOptions);
|
|
29
|
+
try {
|
|
30
|
+
const headers = new Headers(typeof options.headers === "function" ? await options.headers(request) : options.headers);
|
|
31
|
+
headers.set("content-type", "application/json");
|
|
32
|
+
headers.set("accept", "application/json");
|
|
33
|
+
const response = await fetchOrThrow(() => doFetch(url, { method: "POST", headers, body, signal: signals.signal }), signals.signal, sendOptions, request.procedure);
|
|
34
|
+
return await decode(serializer, response, maxResponseBytes, request);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
signals.dispose();
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async close() {
|
|
41
|
+
closed = true;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=rpcHttpTransport.core.js.map
|
|
@@ -1,2 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC transports: the {@link RPCTransport} contract an `RPCClient` sends
|
|
3
|
+
* through, plus the built-in implementations — in-memory for tests and
|
|
4
|
+
* modular monoliths, and HTTP (client transport and server fetch handler)
|
|
5
|
+
* built on the web-standard Fetch API.
|
|
6
|
+
*/
|
|
1
7
|
export type { RPCTransport, RPCTransportRequestOptions, } from "./rpcTransport.type.js";
|
|
8
|
+
export type { RPCFrameSerializer, RPCJsonSerializerOptions, RPCBodyReadResult, RPCBodySource, } from "./codec/index.js";
|
|
9
|
+
export { createRPCJsonSerializer, isRPCResponseFrame, readBoundedBody, } from "./codec/index.js";
|
|
10
|
+
export type { RPCFrameHandler, RPCMemoryTransportOptions, } from "./memory/index.js";
|
|
11
|
+
export { createRPCMemoryTransport } from "./memory/index.js";
|
|
12
|
+
export type { RPCHttpHeaders, RPCHttpTransportOptions, RPCFetchHandlerOptions, } from "./http/index.js";
|
|
13
|
+
export { createRPCHttpTransport, createRPCFetchHandler, RPC_HTTP_STATUS, rpcHttpStatus, } from "./http/index.js";
|
|
2
14
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* RPC transports: the {@link RPCTransport} contract an `RPCClient` sends
|
|
3
|
+
* through, plus the built-in implementations — in-memory for tests and
|
|
4
|
+
* modular monoliths, and HTTP (client transport and server fetch handler)
|
|
5
|
+
* built on the web-standard Fetch API.
|
|
6
|
+
*/
|
|
7
|
+
export { createRPCJsonSerializer, isRPCResponseFrame, readBoundedBody, } from "./codec/index.js";
|
|
8
|
+
export { createRPCMemoryTransport } from "./memory/index.js";
|
|
9
|
+
export { createRPCHttpTransport, createRPCFetchHandler, RPC_HTTP_STATUS, rpcHttpStatus, } from "./http/index.js";
|
|
2
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process transport: connects an `RPCClient` to an `RPCServer` in the
|
|
3
|
+
* same process, serializing frames across the boundary by default.
|
|
4
|
+
*/
|
|
5
|
+
export type { RPCFrameHandler, RPCMemoryTransportOptions, } from "./rpcMemoryTransport.core.js";
|
|
6
|
+
export { createRPCMemoryTransport } from "./rpcMemoryTransport.core.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { RPCRequest } from "../../types/rpcRequest.type.js";
|
|
2
|
+
import type { RPCResponse } from "../../types/rpcResponse.type.js";
|
|
3
|
+
import type { RPCAuthContext, RPCContextOptions } from "../../context/rpcContext.type.js";
|
|
4
|
+
import type { RPCTransport } from "../rpcTransport.type.js";
|
|
5
|
+
import type { RPCFrameSerializer } from "../codec/rpcCodec.helper.js";
|
|
6
|
+
/**
|
|
7
|
+
* The part of an `RPCServer` a transport delivers frames to.
|
|
8
|
+
*/
|
|
9
|
+
export interface RPCFrameHandler {
|
|
10
|
+
handle(request: RPCRequest, trusted?: RPCContextOptions): Promise<RPCResponse>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Options for {@link createRPCMemoryTransport}.
|
|
14
|
+
*/
|
|
15
|
+
export interface RPCMemoryTransportOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Trusted identity handed to the server as `context.auth` — a fixed
|
|
18
|
+
* value, or a function of the request. This is what an HTTP transport
|
|
19
|
+
* would derive from a verified credential; in memory the caller's own
|
|
20
|
+
* code supplies it.
|
|
21
|
+
*/
|
|
22
|
+
readonly auth?: RPCAuthContext | ((request: RPCRequest) => RPCAuthContext | undefined | Promise<RPCAuthContext | undefined>);
|
|
23
|
+
/**
|
|
24
|
+
* How frames cross the boundary. By default each request and response is
|
|
25
|
+
* round-tripped through a JSON serializer, so the server can never see or
|
|
26
|
+
* mutate the caller's objects and a payload that could not travel over a
|
|
27
|
+
* network fails here too. Pass another serializer to match a remote
|
|
28
|
+
* transport, or `false` to pass frames by reference.
|
|
29
|
+
*/
|
|
30
|
+
readonly serializer?: RPCFrameSerializer | false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Creates a transport that delivers frames to a server in the same
|
|
34
|
+
* process — for tests, and for modular monoliths that may later split a
|
|
35
|
+
* module into its own service without touching callers.
|
|
36
|
+
*
|
|
37
|
+
* Honours `options.signal`: an aborted call stops waiting at once and the
|
|
38
|
+
* server's dispatch is cancelled with it. After `close()` every send fails
|
|
39
|
+
* with `RPCUnavailableError`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createRPCMemoryTransport(server: RPCFrameHandler, options?: RPCMemoryTransportOptions): RPCTransport;
|
|
42
|
+
//# sourceMappingURL=rpcMemoryTransport.core.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createRPCJsonSerializer } from "../codec/rpcCodec.helper.js";
|
|
2
|
+
import { createRPCErrorResponse } from "../../types/rpcResponse.type.js";
|
|
3
|
+
import { RPCSerializationError, RPCUnavailableError, } from "../../errors/rpc.errors.js";
|
|
4
|
+
import { INTERNAL_ERROR_MESSAGE } from "../../constants/rpcConstants.core.js";
|
|
5
|
+
import { raceAbort } from "../../reliability/cancellation/rpcAbort.helper.js";
|
|
6
|
+
/**
|
|
7
|
+
* Creates a transport that delivers frames to a server in the same
|
|
8
|
+
* process — for tests, and for modular monoliths that may later split a
|
|
9
|
+
* module into its own service without touching callers.
|
|
10
|
+
*
|
|
11
|
+
* Honours `options.signal`: an aborted call stops waiting at once and the
|
|
12
|
+
* server's dispatch is cancelled with it. After `close()` every send fails
|
|
13
|
+
* with `RPCUnavailableError`.
|
|
14
|
+
*/
|
|
15
|
+
export function createRPCMemoryTransport(server, options = {}) {
|
|
16
|
+
const serializer = options.serializer === false
|
|
17
|
+
? undefined
|
|
18
|
+
: (options.serializer ?? createRPCJsonSerializer());
|
|
19
|
+
let closed = false;
|
|
20
|
+
const resolveAuth = async (request) => typeof options.auth === "function" ? options.auth(request) : options.auth;
|
|
21
|
+
return {
|
|
22
|
+
async send(request, sendOptions = {}) {
|
|
23
|
+
if (closed) {
|
|
24
|
+
throw new RPCUnavailableError("RPC memory transport has been closed.", request.procedure);
|
|
25
|
+
}
|
|
26
|
+
const frame = copy(serializer, request, request.procedure);
|
|
27
|
+
const exchange = async () => {
|
|
28
|
+
const auth = await resolveAuth(request);
|
|
29
|
+
const signal = sendOptions.signal;
|
|
30
|
+
const response = await server.handle(frame, {
|
|
31
|
+
...(auth === undefined ? {} : { auth }),
|
|
32
|
+
...(signal === undefined ? {} : { signal }),
|
|
33
|
+
});
|
|
34
|
+
return copyResponse(serializer, response);
|
|
35
|
+
};
|
|
36
|
+
return raceAbort(exchange(), sendOptions.signal, request.procedure);
|
|
37
|
+
},
|
|
38
|
+
async close() {
|
|
39
|
+
closed = true;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function copy(serializer, value, procedure) {
|
|
44
|
+
if (serializer === undefined) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return serializer.deserialize(serializer.serialize(value));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new RPCSerializationError("RPC request could not be serialized.", procedure);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A response the server produced but that cannot cross the boundary (a
|
|
56
|
+
* `BigInt` result under plain JSON, a cycle) is answered the way a
|
|
57
|
+
* network transport would: a generic internal error frame.
|
|
58
|
+
*/
|
|
59
|
+
function copyResponse(serializer, response) {
|
|
60
|
+
if (serializer === undefined) {
|
|
61
|
+
return response;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return serializer.deserialize(serializer.serialize(response));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return createRPCErrorResponse(response.id, {
|
|
68
|
+
code: "RPC_SERIALIZATION_ERROR",
|
|
69
|
+
message: INTERNAL_ERROR_MESSAGE,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=rpcMemoryTransport.core.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection of prototype-polluting keys in decoded, untrusted data.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Returns the first `__proto__`, `constructor` or `prototype` key found
|
|
6
|
+
* anywhere in `value`, or `undefined` when there is none.
|
|
7
|
+
*
|
|
8
|
+
* `JSON.parse` keeps such a key as an ordinary own property, so it reaches
|
|
9
|
+
* handlers intact; the moment one is copied with `Object.assign`, a
|
|
10
|
+
* `for…in` merge or a bracket assignment, it replaces the target's
|
|
11
|
+
* prototype. Transports refuse a frame that carries one rather than
|
|
12
|
+
* silently dropping it.
|
|
13
|
+
*
|
|
14
|
+
* Walks plain objects and arrays only, iteratively (no recursion limit)
|
|
15
|
+
* and cycle-safe, so it is safe on any decoded or in-process value.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findUnsafeKey(value: unknown): string | undefined;
|
|
18
|
+
//# sourceMappingURL=rpcUnsafeKey.helper.d.ts.map
|