@zudojs/rpc 1.2.0 → 1.3.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 +17 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/rpc/constants/index.d.ts +1 -1
- package/dist/rpc/constants/index.js +1 -1
- package/dist/rpc/constants/rpcConstants.core.d.ts +9 -0
- package/dist/rpc/constants/rpcConstants.core.js +9 -0
- package/dist/rpc/server/rpcServer.core.js +9 -3
- package/dist/rpc/validation/rpcValidation.core.d.ts +17 -3
- package/dist/rpc/validation/rpcValidation.core.js +18 -5
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -74,7 +74,11 @@ const server = new RPCServer();
|
|
|
74
74
|
server.register(sum);
|
|
75
75
|
|
|
76
76
|
const response = await server.handle(
|
|
77
|
-
createRPCRequest({
|
|
77
|
+
createRPCRequest({
|
|
78
|
+
id: "req-1",
|
|
79
|
+
procedure: "math.sum",
|
|
80
|
+
payload: { a: 1, b: 2 },
|
|
81
|
+
}),
|
|
78
82
|
);
|
|
79
83
|
// { id: "req-1", success: true, result: 3 }
|
|
80
84
|
```
|
|
@@ -98,7 +102,10 @@ import { RPCClient, type RPCTransport } from "@zudojs/rpc";
|
|
|
98
102
|
const transport: RPCTransport = { send: (request) => server.handle(request) };
|
|
99
103
|
const client = new RPCClient(transport, { timeout: 5_000 });
|
|
100
104
|
|
|
101
|
-
const total = await client.call<{ a: number; b: number }, number>("math.sum", {
|
|
105
|
+
const total = await client.call<{ a: number; b: number }, number>("math.sum", {
|
|
106
|
+
a: 1,
|
|
107
|
+
b: 2,
|
|
108
|
+
});
|
|
102
109
|
// 3
|
|
103
110
|
```
|
|
104
111
|
|
|
@@ -158,12 +165,19 @@ caller's.
|
|
|
158
165
|
|
|
159
166
|
```typescript
|
|
160
167
|
const server = new RPCServer(undefined, undefined, {
|
|
161
|
-
limits: { maxPayloadBytes: 256 * 1024 },
|
|
168
|
+
limits: { maxPayloadBytes: 256 * 1024, maxRequestIdLength: 128 },
|
|
162
169
|
dispatch: { defaultTimeout: 10_000 },
|
|
163
170
|
onInternalError: (error, requestId) => logger.error({ requestId, error }),
|
|
164
171
|
});
|
|
165
172
|
```
|
|
166
173
|
|
|
174
|
+
`maxPayloadBytes` bounds `payload` and `metadata` together, because both are
|
|
175
|
+
caller-controlled and both reach the handler. `maxRequestIdLength` bounds
|
|
176
|
+
`request.id`, which every response echoes back; an id over the limit is
|
|
177
|
+
refused before a response is built, so it is never reflected. Both default to
|
|
178
|
+
`MAX_RPC_PAYLOAD_SIZE` (1 MiB) and `MAX_RPC_REQUEST_ID_LENGTH` (128), and
|
|
179
|
+
either can be set to `0` when the transport already enforces the limit.
|
|
180
|
+
|
|
167
181
|
## License
|
|
168
182
|
|
|
169
183
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export type { RPCProcedureName, RPCMetadata, RPCMetadataOptions, RPCRequest, RPCRequestOptions, RPCErrorPayload, RPCResponse, } from "./rpc/types/index.js";
|
|
23
23
|
export { createRPCMetadata, createRPCRequest, createRPCResponse, createRPCErrorResponse, } from "./rpc/types/index.js";
|
|
24
|
-
export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpc/constants/index.js";
|
|
24
|
+
export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpc/constants/index.js";
|
|
25
25
|
export type { RPCRequestLimits, RPCSchema } from "./rpc/validation/index.js";
|
|
26
26
|
export { assertValidProcedureName, assertValidRequest, measurePayloadBytes, toValidationIssues, parseInput, parseOutput, } from "./rpc/validation/index.js";
|
|
27
27
|
export type { RPCErrorOptions } from "./rpc/errors/index.js";
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export { createRPCMetadata, createRPCRequest, createRPCResponse, createRPCErrorResponse, } from "./rpc/types/index.js";
|
|
23
23
|
// Constants
|
|
24
|
-
export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpc/constants/index.js";
|
|
24
|
+
export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpc/constants/index.js";
|
|
25
25
|
export { assertValidProcedureName, assertValidRequest, measurePayloadBytes, toValidationIssues, parseInput, parseOutput, } from "./rpc/validation/index.js";
|
|
26
26
|
export { RPCError, RPCProcedureNotFoundError, RPCInvalidRequestError, RPCValidationError, RPCAuthenticationError, RPCForbiddenError, RPCTimeoutError, RPCCancelledError, RPCInternalError, RPCTransportError, RPCSerializationError, RPCDeserializationError, RPCUnavailableError, RPCRateLimitedError, RPCDeadlineExceededError, RPCDuplicateProcedureError, createRPCError, isRPCError, } from "./rpc/errors/index.js";
|
|
27
27
|
export { createRPCProcedure } from "./rpc/procedure/index.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpcConstants.core.js";
|
|
1
|
+
export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpcConstants.core.js";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpcConstants.core.js";
|
|
1
|
+
export { DEFAULT_RPC_TIMEOUT, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, MAX_PENDING_REQUESTS, MAX_MIDDLEWARE, MAX_PROCEDURES, MAX_PROCEDURE_NAME_LENGTH, MAX_TIMER_DELAY, PROCEDURE_NAME_PATTERN, INTERNAL_ERROR_MESSAGE, } from "./rpcConstants.core.js";
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -11,6 +11,15 @@ export declare const DEFAULT_RPC_TIMEOUT = 30000;
|
|
|
11
11
|
* Maximum payload size for RPC messages (1MB).
|
|
12
12
|
*/
|
|
13
13
|
export declare const MAX_RPC_PAYLOAD_SIZE: number;
|
|
14
|
+
/**
|
|
15
|
+
* Maximum length of a request id accepted from a peer.
|
|
16
|
+
*
|
|
17
|
+
* The id is echoed verbatim into every success and error response, so an
|
|
18
|
+
* unbounded id is a reflection amplifier: the server writes back whatever
|
|
19
|
+
* the caller sent, on both paths. 128 characters holds a UUID, a ULID or a
|
|
20
|
+
* W3C trace id with room to spare.
|
|
21
|
+
*/
|
|
22
|
+
export declare const MAX_RPC_REQUEST_ID_LENGTH = 128;
|
|
14
23
|
/**
|
|
15
24
|
* Maximum number of pending requests allowed in the client.
|
|
16
25
|
*/
|
|
@@ -11,6 +11,15 @@ export const DEFAULT_RPC_TIMEOUT = 30_000;
|
|
|
11
11
|
* Maximum payload size for RPC messages (1MB).
|
|
12
12
|
*/
|
|
13
13
|
export const MAX_RPC_PAYLOAD_SIZE = 1024 * 1024;
|
|
14
|
+
/**
|
|
15
|
+
* Maximum length of a request id accepted from a peer.
|
|
16
|
+
*
|
|
17
|
+
* The id is echoed verbatim into every success and error response, so an
|
|
18
|
+
* unbounded id is a reflection amplifier: the server writes back whatever
|
|
19
|
+
* the caller sent, on both paths. 128 characters holds a UUID, a ULID or a
|
|
20
|
+
* W3C trace id with room to spare.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_RPC_REQUEST_ID_LENGTH = 128;
|
|
14
23
|
/**
|
|
15
24
|
* Maximum number of pending requests allowed in the client.
|
|
16
25
|
*/
|
|
@@ -3,7 +3,7 @@ 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
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";
|
|
6
|
+
import { INTERNAL_ERROR_MESSAGE, MAX_RPC_REQUEST_ID_LENGTH, } from "../constants/rpcConstants.core.js";
|
|
7
7
|
import { assertValidRequest } from "../validation/rpcValidation.core.js";
|
|
8
8
|
/**
|
|
9
9
|
* Wire codes for the error types the server maps.
|
|
@@ -72,8 +72,14 @@ export class RPCServer {
|
|
|
72
72
|
* `context.auth`; authorise on that, never on frame metadata.
|
|
73
73
|
*/
|
|
74
74
|
async handle(request, trusted = {}) {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// An id the validator would refuse is never reflected: the error
|
|
76
|
+
// response below echoes this value, so accepting an over-long id here
|
|
77
|
+
// would amplify it straight back to the peer that sent it.
|
|
78
|
+
const rawId = request?.id;
|
|
79
|
+
const maxIdLength = this.options.limits?.maxRequestIdLength ?? MAX_RPC_REQUEST_ID_LENGTH;
|
|
80
|
+
const requestId = typeof rawId === "string" &&
|
|
81
|
+
(maxIdLength <= 0 || rawId.length <= maxIdLength)
|
|
82
|
+
? rawId
|
|
77
83
|
: "";
|
|
78
84
|
try {
|
|
79
85
|
assertValidRequest(request, this.options.limits);
|
|
@@ -13,11 +13,21 @@ import type { RPCRequest } from "../types/rpcRequest.type.js";
|
|
|
13
13
|
*/
|
|
14
14
|
export interface RPCRequestLimits {
|
|
15
15
|
/**
|
|
16
|
-
* Maximum encoded
|
|
16
|
+
* Maximum combined encoded size, in bytes, of the caller-controlled
|
|
17
|
+
* parts of the frame — `payload` and `metadata`. Defaults to
|
|
17
18
|
* {@link MAX_RPC_PAYLOAD_SIZE}. Set to `0` to skip the check when the
|
|
18
19
|
* transport already enforces a frame limit.
|
|
20
|
+
*
|
|
21
|
+
* `metadata` counts because it is caller-controlled and is handed to
|
|
22
|
+
* middleware and handlers as `context.metadata`; measuring `payload`
|
|
23
|
+
* alone left an unbounded second channel into the same handler.
|
|
19
24
|
*/
|
|
20
25
|
readonly maxPayloadBytes?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum length of `request.id`. Defaults to
|
|
28
|
+
* {@link MAX_RPC_REQUEST_ID_LENGTH}. Set to `0` to skip the check.
|
|
29
|
+
*/
|
|
30
|
+
readonly maxRequestIdLength?: number;
|
|
21
31
|
/**
|
|
22
32
|
* Whether procedure names must match {@link PROCEDURE_NAME_PATTERN}.
|
|
23
33
|
* Defaults to `true`.
|
|
@@ -41,8 +51,12 @@ export declare function measurePayloadBytes(payload: unknown): number | undefine
|
|
|
41
51
|
/**
|
|
42
52
|
* Validates the shape and size of an incoming request.
|
|
43
53
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
54
|
+
* Every caller-controlled part of the frame is bounded: the id by
|
|
55
|
+
* length, the procedure name by length and pattern, and `payload` plus
|
|
56
|
+
* `metadata` by their combined encoded size.
|
|
57
|
+
*
|
|
58
|
+
* @throws {RPCInvalidRequestError} when the frame is malformed, the id is
|
|
59
|
+
* over-long, or payload and metadata together exceed the configured limit.
|
|
46
60
|
*/
|
|
47
61
|
export declare function assertValidRequest(request: unknown, limits?: RPCRequestLimits): asserts request is RPCRequest;
|
|
48
62
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* the first checks the server runs, not the last.
|
|
8
8
|
*/
|
|
9
9
|
import { RPCInternalError, RPCInvalidRequestError, RPCValidationError, } from "../errors/rpc.errors.js";
|
|
10
|
-
import { MAX_PROCEDURE_NAME_LENGTH, MAX_RPC_PAYLOAD_SIZE, PROCEDURE_NAME_PATTERN, } from "../constants/rpcConstants.core.js";
|
|
10
|
+
import { MAX_PROCEDURE_NAME_LENGTH, MAX_RPC_PAYLOAD_SIZE, MAX_RPC_REQUEST_ID_LENGTH, PROCEDURE_NAME_PATTERN, } from "../constants/rpcConstants.core.js";
|
|
11
11
|
/**
|
|
12
12
|
* Validates a procedure name.
|
|
13
13
|
*
|
|
@@ -49,8 +49,12 @@ export function measurePayloadBytes(payload) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Validates the shape and size of an incoming request.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Every caller-controlled part of the frame is bounded: the id by
|
|
53
|
+
* length, the procedure name by length and pattern, and `payload` plus
|
|
54
|
+
* `metadata` by their combined encoded size.
|
|
55
|
+
*
|
|
56
|
+
* @throws {RPCInvalidRequestError} when the frame is malformed, the id is
|
|
57
|
+
* over-long, or payload and metadata together exceed the configured limit.
|
|
54
58
|
*/
|
|
55
59
|
export function assertValidRequest(request, limits = {}) {
|
|
56
60
|
if (typeof request !== "object" || request === null) {
|
|
@@ -60,6 +64,13 @@ export function assertValidRequest(request, limits = {}) {
|
|
|
60
64
|
if (typeof candidate.id !== "string" || candidate.id.length === 0) {
|
|
61
65
|
throw new RPCInvalidRequestError("Request id must be a non-empty string.");
|
|
62
66
|
}
|
|
67
|
+
// Checked before anything else touches the frame: the id is reflected
|
|
68
|
+
// into every response the server builds, so an oversized one must be
|
|
69
|
+
// refused before a response exists to carry it.
|
|
70
|
+
const maxIdLength = limits.maxRequestIdLength ?? MAX_RPC_REQUEST_ID_LENGTH;
|
|
71
|
+
if (maxIdLength > 0 && candidate.id.length > maxIdLength) {
|
|
72
|
+
throw new RPCInvalidRequestError(`Request id exceeds ${maxIdLength} characters.`);
|
|
73
|
+
}
|
|
63
74
|
if (limits.enforceProcedureNamePattern ?? true) {
|
|
64
75
|
assertValidProcedureName(candidate.procedure);
|
|
65
76
|
}
|
|
@@ -73,10 +84,12 @@ export function assertValidRequest(request, limits = {}) {
|
|
|
73
84
|
}
|
|
74
85
|
const maxBytes = limits.maxPayloadBytes ?? MAX_RPC_PAYLOAD_SIZE;
|
|
75
86
|
if (maxBytes > 0) {
|
|
76
|
-
const
|
|
77
|
-
|
|
87
|
+
const payloadSize = measurePayloadBytes(candidate.payload);
|
|
88
|
+
const metadataSize = measurePayloadBytes(candidate.metadata);
|
|
89
|
+
if (payloadSize === undefined || metadataSize === undefined) {
|
|
78
90
|
throw new RPCInvalidRequestError("Request payload could not be encoded.", candidate.procedure);
|
|
79
91
|
}
|
|
92
|
+
const size = payloadSize + metadataSize;
|
|
80
93
|
if (size > maxBytes) {
|
|
81
94
|
throw new RPCInvalidRequestError(`Request payload of ${size} bytes exceeds the ${maxBytes} byte limit.`, candidate.procedure);
|
|
82
95
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/rpc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Remote procedure call infrastructure for Zudojs applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"node": ">=24.0.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@zudojs/errors": "1.
|
|
31
|
-
"@zudojs/constants": "1.1.
|
|
32
|
-
"@zudojs/types": "1.1.
|
|
33
|
-
"@zudojs/schema": "1.1.
|
|
30
|
+
"@zudojs/errors": "1.2.0",
|
|
31
|
+
"@zudojs/constants": "1.1.1",
|
|
32
|
+
"@zudojs/types": "1.1.1",
|
|
33
|
+
"@zudojs/schema": "1.1.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "7.0.2",
|