@zudojs/rpc 1.2.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.
Files changed (55) hide show
  1. package/README.md +137 -20
  2. package/dist/index.d.ts +12 -8
  3. package/dist/index.js +14 -5
  4. package/dist/rpc/client/index.d.ts +5 -0
  5. package/dist/rpc/client/index.js +5 -0
  6. package/dist/rpc/client/rpcClient.core.d.ts +6 -7
  7. package/dist/rpc/client/rpcClient.core.js +32 -46
  8. package/dist/rpc/client/rpcWireError.helper.d.ts +13 -0
  9. package/dist/rpc/client/rpcWireError.helper.js +71 -0
  10. package/dist/rpc/constants/index.d.ts +6 -1
  11. package/dist/rpc/constants/index.js +6 -1
  12. package/dist/rpc/constants/rpcConstants.core.d.ts +20 -0
  13. package/dist/rpc/constants/rpcConstants.core.js +20 -0
  14. package/dist/rpc/context/rpcContext.type.d.ts +7 -0
  15. package/dist/rpc/dispatcher/rpcDispatcher.core.js +14 -1
  16. package/dist/rpc/reliability/cancellation/rpcAbort.helper.d.ts +17 -0
  17. package/dist/rpc/reliability/cancellation/rpcAbort.helper.js +40 -0
  18. package/dist/rpc/reliability/retry/rpcRetry.helper.js +3 -1
  19. package/dist/rpc/reliability/timeout/rpcTimeout.helper.d.ts +5 -3
  20. package/dist/rpc/reliability/timeout/rpcTimeout.helper.js +5 -4
  21. package/dist/rpc/server/index.d.ts +6 -0
  22. package/dist/rpc/server/index.js +5 -0
  23. package/dist/rpc/server/rpcBaseErrorMapping.helper.d.ts +16 -0
  24. package/dist/rpc/server/rpcBaseErrorMapping.helper.js +57 -0
  25. package/dist/rpc/server/rpcErrorMapping.helper.d.ts +35 -0
  26. package/dist/rpc/server/rpcErrorMapping.helper.js +101 -0
  27. package/dist/rpc/server/rpcServer.core.d.ts +0 -7
  28. package/dist/rpc/server/rpcServer.core.js +15 -93
  29. package/dist/rpc/transport/codec/index.d.ts +10 -0
  30. package/dist/rpc/transport/codec/index.js +8 -0
  31. package/dist/rpc/transport/codec/rpcBody.helper.d.ts +36 -0
  32. package/dist/rpc/transport/codec/rpcBody.helper.js +64 -0
  33. package/dist/rpc/transport/codec/rpcCodec.helper.d.ts +41 -0
  34. package/dist/rpc/transport/codec/rpcCodec.helper.js +40 -0
  35. package/dist/rpc/transport/http/index.d.ts +12 -0
  36. package/dist/rpc/transport/http/index.js +10 -0
  37. package/dist/rpc/transport/http/rpcFetchHandler.core.d.ts +36 -0
  38. package/dist/rpc/transport/http/rpcFetchHandler.core.js +97 -0
  39. package/dist/rpc/transport/http/rpcHttpExchange.helper.d.ts +27 -0
  40. package/dist/rpc/transport/http/rpcHttpExchange.helper.js +78 -0
  41. package/dist/rpc/transport/http/rpcHttpStatus.helper.d.ts +16 -0
  42. package/dist/rpc/transport/http/rpcHttpStatus.helper.js +37 -0
  43. package/dist/rpc/transport/http/rpcHttpTransport.core.d.ts +36 -0
  44. package/dist/rpc/transport/http/rpcHttpTransport.core.js +45 -0
  45. package/dist/rpc/transport/index.d.ts +12 -0
  46. package/dist/rpc/transport/index.js +9 -1
  47. package/dist/rpc/transport/memory/index.d.ts +7 -0
  48. package/dist/rpc/transport/memory/index.js +6 -0
  49. package/dist/rpc/transport/memory/rpcMemoryTransport.core.d.ts +42 -0
  50. package/dist/rpc/transport/memory/rpcMemoryTransport.core.js +73 -0
  51. package/dist/rpc/validation/rpcUnsafeKey.helper.d.ts +18 -0
  52. package/dist/rpc/validation/rpcUnsafeKey.helper.js +60 -0
  53. package/dist/rpc/validation/rpcValidation.core.d.ts +26 -3
  54. package/dist/rpc/validation/rpcValidation.core.js +27 -5
  55. package/package.json +9 -7
@@ -5,6 +5,7 @@ import { DEFAULT_RPC_TIMEOUT } from "../constants/rpcConstants.core.js";
5
5
  import { parseInput, parseOutput } from "../validation/rpcValidation.core.js";
6
6
  import { readDeadline, throwIfDeadlineExceeded, } from "../reliability/deadline/rpcDeadline.helper.js";
7
7
  import { createTimeout } from "../reliability/timeout/rpcTimeout.helper.js";
8
+ import { abortReasonToRPCError, raceAbort, } from "../reliability/cancellation/rpcAbort.helper.js";
8
9
  /**
9
10
  * Dispatches RPC requests to registered procedures.
10
11
  *
@@ -37,6 +38,10 @@ export class RPCDispatcher {
37
38
  // — deadline reading, the context's `metadata` — reads it as an object.
38
39
  const request = input.metadata === undefined ? { ...input, metadata: {} } : input;
39
40
  const procedure = this.registry.require(request.procedure);
41
+ const caller = trusted.signal;
42
+ if (caller?.aborted) {
43
+ throw abortReasonToRPCError(caller, request.procedure);
44
+ }
40
45
  const controller = new AbortController();
41
46
  const context = createRPCContext(request, controller.signal, trusted);
42
47
  const timeoutMs = this.resolveTimeout(request, procedure);
@@ -48,6 +53,13 @@ export class RPCDispatcher {
48
53
  }
49
54
  }
50
55
  const timeout = timeoutMs > 0 ? createTimeout(timeoutMs, request.procedure) : undefined;
56
+ // The caller going away cancels the work, not just the wait for it.
57
+ const onCallerAbort = () => {
58
+ if (!controller.signal.aborted && caller !== undefined) {
59
+ controller.abort(abortReasonToRPCError(caller, request.procedure));
60
+ }
61
+ };
62
+ caller?.addEventListener("abort", onCallerAbort, { once: true });
51
63
  try {
52
64
  const run = async () => {
53
65
  const input = procedure.options?.input
@@ -61,7 +73,7 @@ export class RPCDispatcher {
61
73
  ? parseOutput(procedure.options.output, result, request.procedure)
62
74
  : result;
63
75
  };
64
- const invoke = () => this.applyInterceptors(context, run);
76
+ const invoke = () => raceAbort(this.applyInterceptors(context, run), caller, request.procedure);
65
77
  const result = timeout === undefined
66
78
  ? await invoke()
67
79
  : await Promise.race([
@@ -82,6 +94,7 @@ export class RPCDispatcher {
82
94
  // an auth failure or a rate limit into a generic internal fault;
83
95
  // the server owns the mapping from error type to response code.
84
96
  timeout?.cancel();
97
+ caller?.removeEventListener("abort", onCallerAbort);
85
98
  if (!controller.signal.aborted) {
86
99
  // Release anything still listening on the request signal.
87
100
  controller.abort(new RPCCancelledError("Request completed.", request.procedure));
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @zudojs/rpc/reliability/cancellation
3
+ *
4
+ * Abort helpers used by transports to turn a signal into a typed error.
5
+ */
6
+ import type { RPCError } from "../../errors/rpc.errors.js";
7
+ /**
8
+ * Converts an aborted signal's reason into a typed RPC error: an RPC
9
+ * error reason (such as the client's `RPCTimeoutError`) passes through,
10
+ * anything else becomes an `RPCCancelledError`.
11
+ */
12
+ export declare function abortReasonToRPCError(signal: AbortSignal, procedureName?: string): RPCError;
13
+ /**
14
+ * Settles with `promise`, or rejects as soon as `signal` aborts.
15
+ */
16
+ export declare function raceAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined, procedureName?: string): Promise<T>;
17
+ //# sourceMappingURL=rpcAbort.helper.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @zudojs/rpc/reliability/cancellation
3
+ *
4
+ * Abort helpers used by transports to turn a signal into a typed error.
5
+ */
6
+ import { isRPCError, RPCCancelledError } from "../../errors/rpc.errors.js";
7
+ /**
8
+ * Converts an aborted signal's reason into a typed RPC error: an RPC
9
+ * error reason (such as the client's `RPCTimeoutError`) passes through,
10
+ * anything else becomes an `RPCCancelledError`.
11
+ */
12
+ export function abortReasonToRPCError(signal, procedureName) {
13
+ const reason = signal.reason;
14
+ return isRPCError(reason)
15
+ ? reason
16
+ : new RPCCancelledError("Call cancelled by caller.", procedureName);
17
+ }
18
+ /**
19
+ * Settles with `promise`, or rejects as soon as `signal` aborts.
20
+ */
21
+ export function raceAbort(promise, signal, procedureName) {
22
+ if (signal === undefined) {
23
+ return promise;
24
+ }
25
+ if (signal.aborted) {
26
+ return Promise.reject(abortReasonToRPCError(signal, procedureName));
27
+ }
28
+ return new Promise((resolve, reject) => {
29
+ const onAbort = () => reject(abortReasonToRPCError(signal, procedureName));
30
+ signal.addEventListener("abort", onAbort, { once: true });
31
+ promise.then((value) => {
32
+ signal.removeEventListener("abort", onAbort);
33
+ resolve(value);
34
+ }, (error) => {
35
+ signal.removeEventListener("abort", onAbort);
36
+ reject(error);
37
+ });
38
+ });
39
+ }
40
+ //# sourceMappingURL=rpcAbort.helper.js.map
@@ -54,6 +54,9 @@ export function calculateRetryDelay(attempt, options) {
54
54
  }
55
55
  /**
56
56
  * Sleeps for a duration, rejecting early if the signal aborts.
57
+ *
58
+ * The timer stays ref'd: a pending retry is work the caller is awaiting,
59
+ * and an unref'd backoff let a script exit before the next attempt.
57
60
  */
58
61
  function sleep(ms, signal) {
59
62
  if (ms <= 0) {
@@ -64,7 +67,6 @@ function sleep(ms, signal) {
64
67
  signal?.removeEventListener("abort", onAbort);
65
68
  resolve();
66
69
  }, ms);
67
- timer.unref?.();
68
70
  function onAbort() {
69
71
  clearTimeout(timer);
70
72
  reject(signal?.reason instanceof Error
@@ -6,9 +6,11 @@
6
6
  /**
7
7
  * Creates a timeout promise that rejects after the given duration.
8
8
  *
9
- * The returned `cancel` clears the underlying timer. A timeout promise
10
- * whose timer is never cleared keeps the event loop alive for its full
11
- * duration even after the work it guarded has finished.
9
+ * The returned `cancel` clears the underlying timer, and every caller in
10
+ * this package cancels it as soon as the guarded work settles. The timer
11
+ * is deliberately *not* unref'd: it guards pending work, and in a script
12
+ * with no other handle an unref'd deadline let Node exit (code 13,
13
+ * "unsettled top-level await") before the timeout could reject.
12
14
  */
13
15
  export declare function createTimeout(duration: number, procedureName?: string): {
14
16
  readonly promise: Promise<never>;
@@ -8,9 +8,11 @@ import { MAX_TIMER_DELAY } from "../../constants/rpcConstants.core.js";
8
8
  /**
9
9
  * Creates a timeout promise that rejects after the given duration.
10
10
  *
11
- * The returned `cancel` clears the underlying timer. A timeout promise
12
- * whose timer is never cleared keeps the event loop alive for its full
13
- * duration even after the work it guarded has finished.
11
+ * The returned `cancel` clears the underlying timer, and every caller in
12
+ * this package cancels it as soon as the guarded work settles. The timer
13
+ * is deliberately *not* unref'd: it guards pending work, and in a script
14
+ * with no other handle an unref'd deadline let Node exit (code 13,
15
+ * "unsettled top-level await") before the timeout could reject.
14
16
  */
15
17
  export function createTimeout(duration, procedureName) {
16
18
  const delay = Math.min(Math.max(0, duration), MAX_TIMER_DELAY);
@@ -19,7 +21,6 @@ export function createTimeout(duration, procedureName) {
19
21
  timer = setTimeout(() => {
20
22
  reject(new RPCTimeoutError(duration, procedureName));
21
23
  }, delay);
22
- timer.unref?.();
23
24
  });
24
25
  return {
25
26
  promise,
@@ -1,3 +1,9 @@
1
+ /**
2
+ * RPC server: validates incoming frames, dispatches them to registered
3
+ * procedures, and maps every failure onto a wire-safe error payload.
4
+ */
1
5
  export type { RPCServerOptions } from "./rpcServer.core.js";
2
6
  export { RPCServer } from "./rpcServer.core.js";
7
+ export type { RPCMappedError } from "./rpcErrorMapping.helper.js";
8
+ export { mapRPCError } from "./rpcErrorMapping.helper.js";
3
9
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,7 @@
1
+ /**
2
+ * RPC server: validates incoming frames, dispatches them to registered
3
+ * procedures, and maps every failure onto a wire-safe error payload.
4
+ */
1
5
  export { RPCServer } from "./rpcServer.core.js";
6
+ export { mapRPCError } from "./rpcErrorMapping.helper.js";
2
7
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Wire mapping for exposable `@zudojs/errors` errors that are not RPC
3
+ * errors — a `NotFoundError` or `ConflictError` thrown by domain code a
4
+ * procedure calls.
5
+ */
6
+ import type { RPCErrorPayload } from "../types/rpcResponse.type.js";
7
+ /**
8
+ * Maps a `@zudojs/errors` error built with `expose: true` onto a wire
9
+ * payload: its status picks the RPC code (404 → `RPC_NOT_FOUND`, 409 →
10
+ * `RPC_CONFLICT`, 401, 403, 422, 429 …; a `ValidationError` is always
11
+ * `RPC_VALIDATION_ERROR`), and an unlisted status keeps the error's own
12
+ * code. The message is the error's own, which `expose: true` declares
13
+ * safe. Returns `undefined` for anything else, which stays internal.
14
+ */
15
+ export declare function mapExposedBaseError(error: unknown): RPCErrorPayload | undefined;
16
+ //# sourceMappingURL=rpcBaseErrorMapping.helper.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Wire mapping for exposable `@zudojs/errors` errors that are not RPC
3
+ * errors — a `NotFoundError` or `ConflictError` thrown by domain code a
4
+ * procedure calls.
5
+ */
6
+ import { isBaseError, RateLimitError, ValidationError } from "@zudojs/errors";
7
+ /** Wire code for each HTTP-style status an exposable error can carry. */
8
+ const STATUS_WIRE_CODES = new Map([
9
+ [400, "RPC_VALIDATION_ERROR"],
10
+ [401, "RPC_UNAUTHENTICATED"],
11
+ [403, "RPC_FORBIDDEN"],
12
+ [404, "RPC_NOT_FOUND"],
13
+ [408, "RPC_TIMEOUT"],
14
+ [409, "RPC_CONFLICT"],
15
+ [422, "RPC_VALIDATION_ERROR"],
16
+ [429, "RPC_RATE_LIMITED"],
17
+ [503, "RPC_UNAVAILABLE"],
18
+ [504, "RPC_TIMEOUT"],
19
+ ]);
20
+ function validationDetails(error) {
21
+ if (error.issues.length === 0) {
22
+ return undefined;
23
+ }
24
+ // The received `value` is dropped: a response never echoes caller input.
25
+ return error.issues.map((issue) => ({
26
+ path: (issue.path ?? (issue.field !== undefined ? [issue.field] : [])).map(String).join("."),
27
+ code: issue.code ?? "invalid",
28
+ message: issue.message,
29
+ }));
30
+ }
31
+ /**
32
+ * Maps a `@zudojs/errors` error built with `expose: true` onto a wire
33
+ * payload: its status picks the RPC code (404 → `RPC_NOT_FOUND`, 409 →
34
+ * `RPC_CONFLICT`, 401, 403, 422, 429 …; a `ValidationError` is always
35
+ * `RPC_VALIDATION_ERROR`), and an unlisted status keeps the error's own
36
+ * code. The message is the error's own, which `expose: true` declares
37
+ * safe. Returns `undefined` for anything else, which stays internal.
38
+ */
39
+ export function mapExposedBaseError(error) {
40
+ if (!isBaseError(error) || error.expose !== true) {
41
+ return undefined;
42
+ }
43
+ const code = error instanceof ValidationError
44
+ ? "RPC_VALIDATION_ERROR"
45
+ : (STATUS_WIRE_CODES.get(error.statusCode) ?? String(error.code));
46
+ const details = error instanceof ValidationError
47
+ ? validationDetails(error)
48
+ : error instanceof RateLimitError && error.retryAfterSeconds !== undefined
49
+ ? { retryAfter: error.retryAfterSeconds }
50
+ : undefined;
51
+ return {
52
+ code,
53
+ message: error.message,
54
+ ...(details !== undefined ? { details } : {}),
55
+ };
56
+ }
57
+ //# sourceMappingURL=rpcBaseErrorMapping.helper.js.map
@@ -0,0 +1,35 @@
1
+ import type { RPCErrorPayload } from "../types/rpcResponse.type.js";
2
+ /**
3
+ * An error mapped onto the wire.
4
+ *
5
+ * `internal` is `true` when the original error carried server detail that
6
+ * was withheld from `payload`; transports hand the original error to their
7
+ * `onInternalError` hook in that case so the failure stays diagnosable.
8
+ */
9
+ export interface RPCMappedError {
10
+ readonly payload: RPCErrorPayload;
11
+ readonly internal: boolean;
12
+ }
13
+ /**
14
+ * Maps any thrown value onto a wire-safe error payload.
15
+ *
16
+ * Typed RPC errors keep their wire code (and, for validation and rate
17
+ * limiting, their `details`). A `@zudojs/errors` error built with
18
+ * `expose: true` — `NotFoundError`, `ConflictError`, `ValidationError` … —
19
+ * travels with its own message under the matching code (`RPC_NOT_FOUND`,
20
+ * `RPC_CONFLICT`, `RPC_VALIDATION_ERROR` …). Anything not built to be
21
+ * exposed — an `RPCInternalError`, a non-exposed custom `RPCError` or
22
+ * `BaseError`, or any other error — is answered with
23
+ * {@link INTERNAL_ERROR_MESSAGE}, so exception text, stack traces and
24
+ * causes never reach the remote side.
25
+ */
26
+ export declare function mapRPCError(error: unknown): RPCMappedError;
27
+ /**
28
+ * The wire code of a typed RPC error — `RPC_TIMEOUT` for an
29
+ * `RPCTimeoutError`, and so on — or `undefined` for anything without one.
30
+ * The client stamps it on every error it rejects with, so a caller
31
+ * compares `error.code` against the same strings whether the failure was
32
+ * reported by the server or raised locally (its own deadline, a cancel).
33
+ */
34
+ export declare function rpcWireCodeOf(error: unknown): string | undefined;
35
+ //# sourceMappingURL=rpcErrorMapping.helper.d.ts.map
@@ -0,0 +1,101 @@
1
+ import { isRPCError, RPCAuthenticationError, RPCCancelledError, RPCDeadlineExceededError, RPCDeserializationError, RPCForbiddenError, RPCInternalError, RPCInvalidRequestError, RPCProcedureNotFoundError, RPCRateLimitedError, RPCSerializationError, RPCTimeoutError, RPCTransportError, RPCUnavailableError, RPCValidationError, } from "../errors/rpc.errors.js";
2
+ import { INTERNAL_ERROR_MESSAGE } from "../constants/rpcConstants.core.js";
3
+ import { mapExposedBaseError } from "./rpcBaseErrorMapping.helper.js";
4
+ /**
5
+ * Wire codes for the error types the server maps.
6
+ *
7
+ * Ordered most specific first; every entry is a type a caller can act
8
+ * on, which is why they must not be collapsed into a generic internal
9
+ * error. The third element marks a typed error whose message is server
10
+ * detail: its code travels, its message goes to `onInternalError`.
11
+ */
12
+ const ERROR_CODES = [
13
+ [RPCProcedureNotFoundError, "RPC_PROCEDURE_NOT_FOUND"],
14
+ [RPCValidationError, "RPC_VALIDATION_ERROR"],
15
+ [RPCInvalidRequestError, "RPC_INVALID_REQUEST"],
16
+ [RPCAuthenticationError, "RPC_UNAUTHENTICATED"],
17
+ [RPCForbiddenError, "RPC_FORBIDDEN"],
18
+ [RPCRateLimitedError, "RPC_RATE_LIMITED"],
19
+ [RPCDeadlineExceededError, "RPC_DEADLINE_EXCEEDED"],
20
+ [RPCTimeoutError, "RPC_TIMEOUT"],
21
+ [RPCCancelledError, "RPC_CANCELLED"],
22
+ [RPCUnavailableError, "RPC_UNAVAILABLE"],
23
+ [RPCSerializationError, "RPC_SERIALIZATION_ERROR", true],
24
+ [RPCDeserializationError, "RPC_DESERIALIZATION_ERROR"],
25
+ ];
26
+ /**
27
+ * Maps any thrown value onto a wire-safe error payload.
28
+ *
29
+ * Typed RPC errors keep their wire code (and, for validation and rate
30
+ * limiting, their `details`). A `@zudojs/errors` error built with
31
+ * `expose: true` — `NotFoundError`, `ConflictError`, `ValidationError` … —
32
+ * travels with its own message under the matching code (`RPC_NOT_FOUND`,
33
+ * `RPC_CONFLICT`, `RPC_VALIDATION_ERROR` …). Anything not built to be
34
+ * exposed — an `RPCInternalError`, a non-exposed custom `RPCError` or
35
+ * `BaseError`, or any other error — is answered with
36
+ * {@link INTERNAL_ERROR_MESSAGE}, so exception text, stack traces and
37
+ * causes never reach the remote side.
38
+ */
39
+ export function mapRPCError(error) {
40
+ if (error instanceof RPCInternalError || !(error instanceof Error)) {
41
+ return internalFailure("RPC_INTERNAL_ERROR");
42
+ }
43
+ for (const [type, code, internal] of ERROR_CODES) {
44
+ if (error instanceof type) {
45
+ return internal === true
46
+ ? internalFailure(code)
47
+ : exposed(code, error.message, detailsOf(error));
48
+ }
49
+ }
50
+ if (isRPCError(error)) {
51
+ return error.expose === false
52
+ ? internalFailure(error.code)
53
+ : exposed(error.code, error.message, undefined);
54
+ }
55
+ const base = mapExposedBaseError(error);
56
+ if (base !== undefined) {
57
+ return { payload: base, internal: false };
58
+ }
59
+ return internalFailure("RPC_INTERNAL_ERROR");
60
+ }
61
+ /**
62
+ * The wire code of a typed RPC error — `RPC_TIMEOUT` for an
63
+ * `RPCTimeoutError`, and so on — or `undefined` for anything without one.
64
+ * The client stamps it on every error it rejects with, so a caller
65
+ * compares `error.code` against the same strings whether the failure was
66
+ * reported by the server or raised locally (its own deadline, a cancel).
67
+ */
68
+ export function rpcWireCodeOf(error) {
69
+ if (error instanceof RPCInternalError) {
70
+ return "RPC_INTERNAL_ERROR";
71
+ }
72
+ if (error instanceof RPCTransportError) {
73
+ return "RPC_TRANSPORT_ERROR";
74
+ }
75
+ return ERROR_CODES.find(([type]) => error instanceof type)?.[1];
76
+ }
77
+ function detailsOf(error) {
78
+ if (error instanceof RPCValidationError &&
79
+ error.issues !== undefined &&
80
+ error.issues.length > 0) {
81
+ return error.issues;
82
+ }
83
+ if (error instanceof RPCRateLimitedError && error.retryAfter !== undefined) {
84
+ return { retryAfter: error.retryAfter };
85
+ }
86
+ return undefined;
87
+ }
88
+ function exposed(code, message, details) {
89
+ return {
90
+ payload: {
91
+ code,
92
+ message,
93
+ ...(details !== undefined ? { details } : {}),
94
+ },
95
+ internal: false,
96
+ };
97
+ }
98
+ function internalFailure(code) {
99
+ return { payload: { code, message: INTERNAL_ERROR_MESSAGE }, internal: true };
100
+ }
101
+ //# sourceMappingURL=rpcErrorMapping.helper.js.map
@@ -61,12 +61,5 @@ export declare class RPCServer {
61
61
  * Returns the procedure registry.
62
62
  */
63
63
  getRegistry(): RPCProcedureRegistry;
64
- /**
65
- * Maps a known error onto a wire payload.
66
- *
67
- * Returns `undefined` for anything unrecognised, which the caller
68
- * answers with a generic internal error.
69
- */
70
- private mapError;
71
64
  }
72
65
  //# sourceMappingURL=rpcServer.core.d.ts.map
@@ -2,33 +2,9 @@ import { RPCProcedureRegistry } from "../procedure/rpcProcedureRegistry.core.js"
2
2
  import { RPCMiddlewareStack } from "../middleware/rpcMiddleware.core.js";
3
3
  import { RPCDispatcher } from "../dispatcher/rpcDispatcher.core.js";
4
4
  import { createRPCErrorResponse } from "../types/rpcResponse.type.js";
5
- import { isRPCError, RPCAuthenticationError, RPCCancelledError, RPCDeadlineExceededError, RPCDeserializationError, RPCForbiddenError, RPCInternalError, RPCInvalidRequestError, RPCProcedureNotFoundError, RPCRateLimitedError, RPCSerializationError, RPCTimeoutError, RPCUnavailableError, RPCValidationError, } from "../errors/rpc.errors.js";
6
- import { INTERNAL_ERROR_MESSAGE } from "../constants/rpcConstants.core.js";
5
+ import { MAX_RPC_REQUEST_ID_LENGTH } from "../constants/rpcConstants.core.js";
7
6
  import { assertValidRequest } from "../validation/rpcValidation.core.js";
8
- /**
9
- * Wire codes for the error types the server maps.
10
- *
11
- * Ordered most specific first; every entry is a type a caller can act
12
- * on, which is why they must not be collapsed into a generic internal
13
- * error.
14
- */
15
- const ERROR_CODES = [
16
- [RPCProcedureNotFoundError, "RPC_PROCEDURE_NOT_FOUND"],
17
- [RPCValidationError, "RPC_VALIDATION_ERROR"],
18
- [RPCInvalidRequestError, "RPC_INVALID_REQUEST"],
19
- [RPCAuthenticationError, "RPC_UNAUTHENTICATED"],
20
- [RPCForbiddenError, "RPC_FORBIDDEN"],
21
- [RPCRateLimitedError, "RPC_RATE_LIMITED"],
22
- [RPCDeadlineExceededError, "RPC_DEADLINE_EXCEEDED"],
23
- [RPCTimeoutError, "RPC_TIMEOUT"],
24
- [RPCCancelledError, "RPC_CANCELLED"],
25
- [RPCUnavailableError, "RPC_UNAVAILABLE"],
26
- // A serialization failure is a server fault (500, `expose: false`) whose
27
- // message names what could not be encoded; the code travels, the
28
- // message goes to `onInternalError`.
29
- [RPCSerializationError, "RPC_SERIALIZATION_ERROR", true],
30
- [RPCDeserializationError, "RPC_DESERIALIZATION_ERROR"],
31
- ];
7
+ import { mapRPCError } from "./rpcErrorMapping.helper.js";
32
8
  /**
33
9
  * RPC server that receives and dispatches requests.
34
10
  */
@@ -72,8 +48,14 @@ export class RPCServer {
72
48
  * `context.auth`; authorise on that, never on frame metadata.
73
49
  */
74
50
  async handle(request, trusted = {}) {
75
- const requestId = typeof request?.id === "string"
76
- ? request.id
51
+ // An id the validator would refuse is never reflected: the error
52
+ // response below echoes this value, so accepting an over-long id here
53
+ // would amplify it straight back to the peer that sent it.
54
+ const rawId = request?.id;
55
+ const maxIdLength = this.options.limits?.maxRequestIdLength ?? MAX_RPC_REQUEST_ID_LENGTH;
56
+ const requestId = typeof rawId === "string" &&
57
+ (maxIdLength <= 0 || rawId.length <= maxIdLength)
58
+ ? rawId
77
59
  : "";
78
60
  try {
79
61
  assertValidRequest(request, this.options.limits);
@@ -85,29 +67,14 @@ export class RPCServer {
85
67
  return await this.dispatcher.dispatch(frame, trusted);
86
68
  }
87
69
  catch (error) {
88
- const mapped = this.mapError(error);
89
- if (mapped === undefined) {
90
- this.options.onInternalError?.(error, requestId);
91
- return createRPCErrorResponse(requestId, {
92
- code: "RPC_INTERNAL_ERROR",
93
- message: INTERNAL_ERROR_MESSAGE,
94
- });
95
- }
70
+ const mapped = mapRPCError(error);
96
71
  if (mapped.internal) {
97
- // The error is typed, so the caller keeps its code, but it was
98
- // constructed with `expose: false`: its message is server detail
99
- // and goes to the log, not the wire.
72
+ // Internal detail goes to the log, never the wire: the caller is
73
+ // an untrusted peer and exception text can name hosts, paths,
74
+ // credentials or queries.
100
75
  this.options.onInternalError?.(error, requestId);
101
- return createRPCErrorResponse(requestId, {
102
- code: mapped.code,
103
- message: INTERNAL_ERROR_MESSAGE,
104
- });
105
76
  }
106
- return createRPCErrorResponse(requestId, {
107
- code: mapped.code,
108
- message: mapped.message,
109
- ...(mapped.details !== undefined ? { details: mapped.details } : {}),
110
- });
77
+ return createRPCErrorResponse(requestId, mapped.payload);
111
78
  }
112
79
  }
113
80
  /**
@@ -116,50 +83,5 @@ export class RPCServer {
116
83
  getRegistry() {
117
84
  return this.registry;
118
85
  }
119
- /**
120
- * Maps a known error onto a wire payload.
121
- *
122
- * Returns `undefined` for anything unrecognised, which the caller
123
- * answers with a generic internal error.
124
- */
125
- mapError(error) {
126
- // An RPCInternalError *is* the generic internal failure: it carries
127
- // `expose: false` and a message written for the log. Mapping it like
128
- // any other RPCError put that message on the wire.
129
- if (error instanceof RPCInternalError) {
130
- return undefined;
131
- }
132
- for (const [type, code, internal] of ERROR_CODES) {
133
- if (error instanceof type) {
134
- const payload = {
135
- code,
136
- message: error.message,
137
- ...(internal ? { internal: true } : {}),
138
- };
139
- if (error instanceof RPCValidationError &&
140
- error.issues !== undefined &&
141
- error.issues.length > 0) {
142
- payload.details = error.issues;
143
- }
144
- if (error instanceof RPCRateLimitedError &&
145
- error.retryAfter !== undefined) {
146
- payload.details = { retryAfter: error.retryAfter };
147
- }
148
- return payload;
149
- }
150
- }
151
- // A custom RPCError subclass is still a deliberate, caller-facing
152
- // error; honour its own code rather than hiding it. Its message only
153
- // travels when the error was built to be exposed — `RPCError`
154
- // defaults to `expose: false`.
155
- if (isRPCError(error)) {
156
- return {
157
- code: error.code,
158
- message: error.message,
159
- internal: error.expose === false,
160
- };
161
- }
162
- return undefined;
163
- }
164
86
  }
165
87
  //# sourceMappingURL=rpcServer.core.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Wire encoding shared by the built-in transports: the frame serializer
3
+ * (`@zudojs/serialization` JSON with size and depth limits), a response
4
+ * frame shape guard, and a bounded Fetch API body reader.
5
+ */
6
+ export type { RPCFrameSerializer, RPCJsonSerializerOptions, } from "./rpcCodec.helper.js";
7
+ export { createRPCJsonSerializer, isRPCResponseFrame } from "./rpcCodec.helper.js";
8
+ export type { RPCBodyReadResult, RPCBodySource } from "./rpcBody.helper.js";
9
+ export { readBoundedBody } from "./rpcBody.helper.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Wire encoding shared by the built-in transports: the frame serializer
3
+ * (`@zudojs/serialization` JSON with size and depth limits), a response
4
+ * frame shape guard, and a bounded Fetch API body reader.
5
+ */
6
+ export { createRPCJsonSerializer, isRPCResponseFrame } from "./rpcCodec.helper.js";
7
+ export { readBoundedBody } from "./rpcBody.helper.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Outcome of {@link readBoundedBody}.
3
+ *
4
+ * `too-large` means the body exceeded the limit (by its declared
5
+ * `Content-Length` or while streaming); `unreadable` means the stream
6
+ * failed, was aborted, or was not valid UTF-8.
7
+ */
8
+ export type RPCBodyReadResult = {
9
+ readonly ok: true;
10
+ readonly text: string;
11
+ } | {
12
+ readonly ok: false;
13
+ readonly reason: "too-large" | "unreadable";
14
+ };
15
+ /**
16
+ * The part of a Fetch API `Request` or `Response` a body read needs.
17
+ */
18
+ export interface RPCBodySource {
19
+ readonly body: ReadableStream<Uint8Array> | null;
20
+ readonly headers: Headers;
21
+ }
22
+ /**
23
+ * Reads a Fetch API body as UTF-8 text without ever buffering more than
24
+ * `maxBytes`.
25
+ *
26
+ * `request.text()` buffers the whole body before anything can check its
27
+ * size, so a peer could make the server hold an arbitrarily large body in
28
+ * memory. This checks the declared `Content-Length` first, then counts
29
+ * bytes as they stream and cancels the stream the moment the limit is
30
+ * crossed. Transport-neutral: `@zudojs/api`'s fetch binding reads request
31
+ * bodies through it too.
32
+ *
33
+ * @param maxBytes Limit in bytes; `0` or less disables the limit.
34
+ */
35
+ export declare function readBoundedBody(source: RPCBodySource, maxBytes: number): Promise<RPCBodyReadResult>;
36
+ //# sourceMappingURL=rpcBody.helper.d.ts.map