@orpc/server 2.0.0-beta.47 → 2.0.0-beta.49

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 (178) hide show
  1. package/README.md +6 -7
  2. package/dist/adapters/aws-lambda/handler.d.ts +25 -0
  3. package/dist/adapters/aws-lambda/handler.js +24 -0
  4. package/dist/adapters/aws-lambda/index.d.ts +10 -45
  5. package/dist/adapters/aws-lambda/index.js +2 -0
  6. package/dist/adapters/aws-lambda/rpc-handler.d.ts +19 -0
  7. package/dist/adapters/aws-lambda/rpc-handler.js +18 -0
  8. package/dist/adapters/crossws/handler.d.ts +45 -0
  9. package/dist/adapters/crossws/handler.js +60 -0
  10. package/dist/adapters/crossws/index.d.ts +2 -63
  11. package/dist/adapters/crossws/index.js +2 -0
  12. package/dist/adapters/crossws/rpc-handler.d.ts +16 -0
  13. package/dist/adapters/crossws/rpc-handler.js +15 -0
  14. package/dist/adapters/fastify/handler.d.ts +21 -0
  15. package/dist/adapters/fastify/handler.js +20 -0
  16. package/dist/adapters/fastify/index.d.ts +2 -42
  17. package/dist/adapters/fastify/index.js +2 -0
  18. package/dist/adapters/fastify/rpc-handler.d.ts +15 -0
  19. package/dist/adapters/fastify/rpc-handler.js +14 -0
  20. package/dist/adapters/fetch/handler.d.ts +23 -0
  21. package/dist/adapters/fetch/handler.js +22 -0
  22. package/dist/adapters/fetch/index.d.ts +5 -45
  23. package/dist/adapters/fetch/index.js +7 -0
  24. package/dist/adapters/fetch/rpc-handler.d.ts +17 -0
  25. package/dist/adapters/fetch/rpc-handler.js +16 -0
  26. package/dist/adapters/message-port/{index.d.mts → handler.d.ts} +17 -30
  27. package/dist/adapters/message-port/handler.js +86 -0
  28. package/dist/adapters/message-port/index.d.ts +2 -77
  29. package/dist/adapters/message-port/index.js +2 -0
  30. package/dist/adapters/message-port/rpc-handler.d.ts +16 -0
  31. package/dist/adapters/message-port/rpc-handler.js +15 -0
  32. package/dist/adapters/node/handler.d.ts +21 -0
  33. package/dist/adapters/node/handler.js +20 -0
  34. package/dist/adapters/node/index.d.ts +5 -42
  35. package/dist/adapters/node/index.js +7 -0
  36. package/dist/adapters/node/rpc-handler.d.ts +16 -0
  37. package/dist/adapters/node/rpc-handler.js +15 -0
  38. package/dist/adapters/standard/codec.d.ts +16 -0
  39. package/dist/adapters/standard/codec.js +1 -0
  40. package/dist/adapters/standard/handler.d.ts +68 -0
  41. package/dist/adapters/standard/handler.js +205 -0
  42. package/dist/adapters/standard/index.d.ts +6 -95
  43. package/dist/adapters/standard/index.js +5 -0
  44. package/dist/adapters/standard/plugin.d.ts +32 -0
  45. package/dist/adapters/standard/plugin.js +16 -0
  46. package/dist/adapters/standard/rpc-handler-codec.d.ts +46 -0
  47. package/dist/adapters/standard/rpc-handler-codec.js +51 -0
  48. package/dist/adapters/standard/rpc-matcher.d.ts +53 -0
  49. package/dist/adapters/standard/rpc-matcher.js +152 -0
  50. package/dist/adapters/standard/utils.d.ts +10 -0
  51. package/dist/adapters/standard/utils.js +6 -0
  52. package/dist/adapters/standard-peer/index.d.ts +1 -11
  53. package/dist/adapters/standard-peer/index.js +1 -0
  54. package/dist/adapters/standard-peer/utils.d.ts +4 -0
  55. package/dist/adapters/standard-peer/utils.js +7 -0
  56. package/dist/adapters/websocket/{index.d.mts → handler.d.ts} +11 -29
  57. package/dist/adapters/websocket/handler.js +85 -0
  58. package/dist/adapters/websocket/index.d.ts +2 -77
  59. package/dist/adapters/websocket/index.js +2 -0
  60. package/dist/adapters/websocket/rpc-handler.d.ts +16 -0
  61. package/dist/adapters/websocket/rpc-handler.js +15 -0
  62. package/dist/builder-variants.d.ts +226 -0
  63. package/dist/builder-variants.js +1 -0
  64. package/dist/builder.d.ts +127 -0
  65. package/dist/builder.js +226 -0
  66. package/dist/constants.d.ts +2 -0
  67. package/dist/constants.js +2 -0
  68. package/dist/context.d.ts +6 -0
  69. package/dist/context.js +1 -0
  70. package/dist/extensions/callable.d.ts +6 -5
  71. package/dist/extensions/callable.js +7 -0
  72. package/dist/helpers/base64url.d.ts +31 -0
  73. package/dist/helpers/base64url.js +53 -0
  74. package/dist/helpers/cookie.d.ts +51 -0
  75. package/dist/helpers/cookie.js +69 -0
  76. package/dist/helpers/encryption.d.ts +28 -0
  77. package/dist/helpers/encryption.js +80 -0
  78. package/dist/helpers/index.d.ts +4 -167
  79. package/dist/helpers/index.js +4 -0
  80. package/dist/helpers/signing.d.ts +50 -0
  81. package/dist/helpers/signing.js +88 -0
  82. package/dist/implementer-procedure.d.ts +16 -0
  83. package/dist/implementer-procedure.js +44 -0
  84. package/dist/implementer-router.d.ts +40 -0
  85. package/dist/implementer-router.js +117 -0
  86. package/dist/implementer.d.ts +16 -0
  87. package/dist/implementer.js +37 -0
  88. package/dist/index.d.ts +45 -460
  89. package/dist/index.js +48 -0
  90. package/dist/lazy.d.ts +20 -0
  91. package/dist/lazy.js +31 -0
  92. package/dist/middleware-decorated.d.ts +10 -0
  93. package/dist/middleware-decorated.js +71 -0
  94. package/dist/middleware.d.ts +54 -0
  95. package/dist/middleware.js +1 -0
  96. package/dist/plugins/batch.d.ts +102 -0
  97. package/dist/plugins/batch.js +405 -0
  98. package/dist/plugins/cors.d.ts +73 -0
  99. package/dist/plugins/cors.js +112 -0
  100. package/dist/plugins/get-method-csrf-protection.d.ts +23 -0
  101. package/dist/plugins/get-method-csrf-protection.js +64 -0
  102. package/dist/plugins/head-method.d.ts +23 -0
  103. package/dist/plugins/head-method.js +104 -0
  104. package/dist/plugins/index.d.ts +29 -422
  105. package/dist/plugins/index.js +29 -0
  106. package/dist/plugins/method-override.d.ts +38 -0
  107. package/dist/plugins/method-override.js +50 -0
  108. package/dist/plugins/request-compression.d.ts +16 -0
  109. package/dist/plugins/request-compression.js +82 -0
  110. package/dist/plugins/request-headers.d.ts +22 -0
  111. package/dist/plugins/request-headers.js +30 -0
  112. package/dist/{shared/server.CgBF3Vj1.d.ts → plugins/request-limit.d.ts} +4 -8
  113. package/dist/plugins/request-limit.js +68 -0
  114. package/dist/plugins/response-compression.d.ts +59 -0
  115. package/dist/plugins/response-compression.js +209 -0
  116. package/dist/plugins/response-headers.d.ts +30 -0
  117. package/dist/plugins/response-headers.js +47 -0
  118. package/dist/plugins/rethrow.d.ts +33 -0
  119. package/dist/plugins/rethrow.js +59 -0
  120. package/dist/plugins/signal-sync.d.ts +13 -0
  121. package/dist/plugins/signal-sync.js +91 -0
  122. package/dist/plugins/timeout.d.ts +37 -0
  123. package/dist/plugins/timeout.js +59 -0
  124. package/dist/procedure-client.d.ts +19 -0
  125. package/dist/procedure-client.js +175 -0
  126. package/dist/procedure-decorated.d.ts +10 -0
  127. package/dist/procedure-decorated.js +44 -0
  128. package/dist/procedure-utils.d.ts +41 -0
  129. package/dist/procedure-utils.js +49 -0
  130. package/dist/procedure.d.ts +72 -0
  131. package/dist/procedure.js +24 -0
  132. package/dist/router-client.d.ts +23 -0
  133. package/dist/router-client.js +43 -0
  134. package/dist/router-hidden.d.ts +5 -0
  135. package/dist/router-hidden.js +14 -0
  136. package/dist/router-utils.d.ts +48 -0
  137. package/dist/router-utils.js +202 -0
  138. package/dist/{shared/server.Dz7j9BEh.d.ts → router.d.ts} +14 -15
  139. package/dist/router.js +1 -0
  140. package/package.json +84 -86
  141. package/dist/adapters/aws-lambda/index.d.mts +0 -51
  142. package/dist/adapters/aws-lambda/index.mjs +0 -39
  143. package/dist/adapters/crossws/index.d.mts +0 -63
  144. package/dist/adapters/crossws/index.mjs +0 -68
  145. package/dist/adapters/fastify/index.d.mts +0 -42
  146. package/dist/adapters/fastify/index.mjs +0 -39
  147. package/dist/adapters/fetch/index.d.mts +0 -47
  148. package/dist/adapters/fetch/index.mjs +0 -42
  149. package/dist/adapters/message-port/index.mjs +0 -87
  150. package/dist/adapters/node/index.d.mts +0 -44
  151. package/dist/adapters/node/index.mjs +0 -41
  152. package/dist/adapters/standard/index.d.mts +0 -95
  153. package/dist/adapters/standard/index.mjs +0 -8
  154. package/dist/adapters/standard-peer/index.d.mts +0 -11
  155. package/dist/adapters/standard-peer/index.mjs +0 -10
  156. package/dist/adapters/websocket/index.mjs +0 -87
  157. package/dist/extensions/callable.d.mts +0 -10
  158. package/dist/extensions/callable.mjs +0 -11
  159. package/dist/helpers/index.d.mts +0 -167
  160. package/dist/helpers/index.mjs +0 -200
  161. package/dist/index.d.mts +0 -476
  162. package/dist/index.mjs +0 -463
  163. package/dist/plugins/index.d.mts +0 -432
  164. package/dist/plugins/index.mjs +0 -965
  165. package/dist/shared/server.1ExWFMze.mjs +0 -69
  166. package/dist/shared/server.9B1jEm5a.d.mts +0 -105
  167. package/dist/shared/server.BB464y-6.mjs +0 -207
  168. package/dist/shared/server.BC5BIu02.d.mts +0 -73
  169. package/dist/shared/server.BSYGskId.d.ts +0 -14
  170. package/dist/shared/server.BvG3boBj.d.mts +0 -14
  171. package/dist/shared/server.CTV_mC2P.d.mts +0 -34
  172. package/dist/shared/server.CYbIpHbH.mjs +0 -239
  173. package/dist/shared/server.Chq5FpXf.mjs +0 -353
  174. package/dist/shared/server.DFPxP2v3.d.mts +0 -166
  175. package/dist/shared/server.DFPxP2v3.d.ts +0 -166
  176. package/dist/shared/server.DOg7ke6y.d.ts +0 -105
  177. package/dist/shared/server.W91HSRkE.mjs +0 -9
  178. package/dist/shared/server.sBQSgqK3.mjs +0 -41
@@ -0,0 +1,205 @@
1
+ import { ORPCError, toORPCError } from '@orpc/client';
2
+ import { getTracer, intercept, isAsyncIteratorObject, matchesHttpPathPrefix, ORPC_NAME, override, recordSpanError, runWithSpan, toArray, toTracingException, traceAsyncIterator, traceReadableStream, value, wrapAsyncIterator, wrapReadableStream, } from '@orpc/shared';
3
+ import { EventStreamErrorEvent, flattenStandardHeader, parseStandardUrl, } from '@standard-server/core';
4
+ import { createProcedureClient } from '../../procedure-client.js';
5
+ import { CompositeStandardHandlerPlugin } from './plugin.js';
6
+ export class StandardHandler {
7
+ codec;
8
+ routingInterceptors;
9
+ interceptors;
10
+ clientInterceptors;
11
+ constructor(codec, options) {
12
+ this.codec = codec;
13
+ options = new CompositeStandardHandlerPlugin([
14
+ ...toArray(options.plugins),
15
+ new TracingHandlerPlugin(),
16
+ ]).init(options);
17
+ this.routingInterceptors = options.routingInterceptors;
18
+ this.interceptors = options.interceptors;
19
+ this.clientInterceptors = options.clientInterceptors;
20
+ }
21
+ async handle(request, options) {
22
+ const { prefix } = options;
23
+ if (prefix && !matchesHttpPathPrefix(request.url, prefix)) {
24
+ return { matched: false, response: undefined };
25
+ }
26
+ const context = (await value(options.context, request));
27
+ return intercept(this.routingInterceptors, { context, prefix, request }, async ({ context, prefix, request }) => {
28
+ const span = getTracer()?.getActiveSpan();
29
+ let step;
30
+ const matchedOrNot = await runWithSpan('find_procedure', () => this.codec.resolveProcedure(request, { context, prefix }));
31
+ if (!matchedOrNot) {
32
+ /**
33
+ * [Semantic conventions for HTTP spans](https://opentelemetry.io/docs/specs/semconv/http/http-spans/)
34
+ */
35
+ span?.updateName(`${ORPC_NAME}_no_match`);
36
+ span?.setAttribute('http.request.method', request.method);
37
+ span?.setAttribute('url.path', request.url);
38
+ return { matched: false };
39
+ }
40
+ const { path, procedure, decodeInput } = matchedOrNot;
41
+ /**
42
+ * [Semantic conventions for RPC spans](https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/)
43
+ */
44
+ span?.updateName(`${ORPC_NAME}.${path.join('/')}`);
45
+ span?.setAttribute('rpc.system', ORPC_NAME);
46
+ span?.setAttribute('rpc.method', path.join('.'));
47
+ try {
48
+ const response = await intercept(this.interceptors, { context, prefix, request, path, procedure, decodeInput }, async ({ context, prefix, request, path, procedure, decodeInput }) => {
49
+ step = 'decode_input';
50
+ let input = await runWithSpan('decode_input', decodeInput);
51
+ step = undefined;
52
+ if (getTracer() && isAsyncIteratorObject(input)) {
53
+ /**
54
+ * @warning
55
+ * Remember use `override` for AsyncIteratorObject to remain other special properties
56
+ */
57
+ input = override(input, traceAsyncIterator('consume_async_iterator_object_input', input));
58
+ }
59
+ else if (getTracer() && input instanceof ReadableStream) {
60
+ /**
61
+ * @warning
62
+ * Remember use `override` for ReadableStream to remain other special properties
63
+ */
64
+ input = override(input, traceReadableStream('consume_octet_stream_input', input));
65
+ }
66
+ const client = createProcedureClient(procedure, {
67
+ context,
68
+ path,
69
+ interceptors: this.clientInterceptors,
70
+ });
71
+ /**
72
+ * No need to use `runWithSpan` here, because the client already has its own span.
73
+ */
74
+ step = 'call_procedure';
75
+ const output = await client(input, {
76
+ signal: request.signal,
77
+ lastEventId: flattenStandardHeader(request.headers['last-event-id']),
78
+ });
79
+ step = undefined;
80
+ const response = await this.codec.encodeOutput(output, procedure, path, {
81
+ context,
82
+ prefix,
83
+ });
84
+ return response;
85
+ });
86
+ return { matched: true, response };
87
+ }
88
+ catch (e) {
89
+ /**
90
+ * Only errors that happen outside of the `call_procedure` step should be set as an error.
91
+ * Because a business logic error should not be considered as a protocol-level error.
92
+ */
93
+ if (step !== 'call_procedure') {
94
+ recordSpanError(span, e);
95
+ }
96
+ const error = step === 'decode_input' && !(e instanceof ORPCError)
97
+ ? new ORPCError('BAD_REQUEST', {
98
+ message: `Malformed request. Ensure the request body is properly formatted and the 'Content-Type' header is set correctly.`,
99
+ cause: e,
100
+ })
101
+ : toORPCError(e);
102
+ const response = await this.codec.encodeError(error, procedure, path, { context, prefix });
103
+ return { matched: true, response };
104
+ }
105
+ });
106
+ }
107
+ }
108
+ class TracingHandlerPlugin {
109
+ name = '~tracing';
110
+ init(options) {
111
+ return {
112
+ ...options,
113
+ routingInterceptors: [
114
+ // Should be placed before user-provided interceptors to help them access the current active span.
115
+ async ({ next, request }) => {
116
+ const tracer = getTracer();
117
+ if (!tracer) {
118
+ return next();
119
+ }
120
+ const parent = tracer.extract?.(request.headers);
121
+ /**
122
+ * The search part is excluded because span names should have low cardinality.
123
+ * The name is replaced with the procedure path once the request is routed.
124
+ */
125
+ const [pathname] = parseStandardUrl(request.url);
126
+ return tracer.startActiveSpan(`${request.method} ${pathname}`, parent, async (span) => {
127
+ try {
128
+ const result = await next();
129
+ if (!result.matched) {
130
+ span.end();
131
+ return result;
132
+ }
133
+ const body = result.response.body;
134
+ if (isAsyncIteratorObject(body)) {
135
+ return {
136
+ ...result,
137
+ response: {
138
+ ...result.response,
139
+ /**
140
+ * @remarks
141
+ * **Warning**: Remember use `override` for remaining special properties
142
+ */
143
+ body: override(body, wrapAsyncIterator(body, {
144
+ runWith: (fn) => tracer.withActiveSpan(span, fn),
145
+ onError(error) {
146
+ /**
147
+ * Errors here are internal (interceptor/framework) failures,
148
+ * except `EventStreamErrorEvent`: a business error the protocol delivers
149
+ * inside the event stream, already logged by the client interceptor.
150
+ */
151
+ if (!(error instanceof EventStreamErrorEvent)) {
152
+ span.recordException('error', toTracingException(error));
153
+ }
154
+ },
155
+ onFinish() {
156
+ span.end();
157
+ },
158
+ })),
159
+ },
160
+ };
161
+ }
162
+ if (body instanceof ReadableStream) {
163
+ return {
164
+ ...result,
165
+ response: {
166
+ ...result.response,
167
+ /**
168
+ * @remarks
169
+ * **Warning**: Remember use `override` for remaining special properties
170
+ */
171
+ body: override(body, wrapReadableStream(body, {
172
+ runWith: (fn) => tracer.withActiveSpan(span, fn),
173
+ onError(error) {
174
+ /**
175
+ * Any error here is internal (interceptor/framework), not business logic.
176
+ * Indicates unexpected handler failure.
177
+ */
178
+ span.recordException('error', toTracingException(error));
179
+ },
180
+ onFinish() {
181
+ span.end();
182
+ },
183
+ })),
184
+ },
185
+ };
186
+ }
187
+ span.end();
188
+ return result;
189
+ }
190
+ catch (e) {
191
+ /**
192
+ * Any error here is internal (interceptor/framework), not business logic.
193
+ * Always recorded as an error, even when it is an abort error.
194
+ */
195
+ span.recordException('error', toTracingException(e));
196
+ span.end();
197
+ throw e;
198
+ }
199
+ });
200
+ },
201
+ ...toArray(options.routingInterceptors),
202
+ ],
203
+ };
204
+ }
205
+ }
@@ -1,95 +1,6 @@
1
- import { R as ResolvedStandardHandlerHandleOptions, f as StandardHandlerCodec, g as StandardHandlerCodecResolvedProcedure } from '../../shared/server.DOg7ke6y.js';
2
- export { S as StandardHandler, a as StandardHandlerHandleOptions, h as StandardHandlerHandleResult, i as StandardHandlerInterceptor, e as StandardHandlerInterceptorOptions, c as StandardHandlerOptions, b as StandardHandlerPlugin, j as StandardHandlerRoutingInterceptor, d as StandardHandlerRoutingInterceptorOptions } from '../../shared/server.DOg7ke6y.js';
3
- import { RPCSerializer, AnyORPCError } from '@orpc/client';
4
- import { Value, Public, Promisable } from '@orpc/shared';
5
- import { StandardMethod, StandardLazyRequest, StandardResponse } from '@standard-server/core';
6
- import { AnyProcedure, Context } from '../../index.js';
7
- import { AnyRouter } from '../../index.js';
8
- import { AnyProcedureContract } from '@orpc/contract';
9
- export { F as FriendlyStandardHandlerHandleOptions, r as resolveFriendlyStandardHandlerHandleOptions } from '../../shared/server.BSYGskId.js';
10
-
11
- /**
12
- * Methods that can invoke procedures by default. Browsers cannot trigger them
13
- * cross-site without a CORS preflight or an HTML form, unlike `GET`, which a plain
14
- * `<a>` click or redirect can trigger with `SameSite=Lax` cookies attached. Other
15
- * methods (`HEAD`, `OPTIONS`, `QUERY`, ...) have safe semantics that should not
16
- * invoke a procedure that can modify data.
17
- *
18
- * @see {@link https://orpc.dev/docs/rpc/handler#supported-http-methods | RPC Handler - Supported HTTP Methods}
19
- */
20
- declare const RPC_DEFAULT_ALLOW_METHODS: readonly StandardMethod[];
21
- interface RPCMatcherOptions {
22
- /**
23
- * Filter which procedures are exposed for matching. Return `false` to exclude.
24
- *
25
- * @default true
26
- */
27
- filter?: Value<boolean, [procedure: AnyProcedureContract | AnyProcedure, path: string[]]> | undefined;
28
- /**
29
- * Restricts which HTTP methods can invoke procedures, either with a list of allowed
30
- * methods or decided per request via a function. Requests using a disallowed method
31
- * are treated as unmatched. `GET` is excluded by default because it is exposed to
32
- * Cross-Site Request Forgery (CSRF) attacks.
33
- *
34
- * @default RPC_DEFAULT_ALLOW_METHODS (['POST', 'PUT', 'PATCH', 'DELETE'])
35
- * @see {@link https://orpc.dev/docs/rpc/handler#supported-http-methods | RPC Handler - Supported HTTP Methods}
36
- */
37
- allowMethods?: readonly StandardMethod[] | ((method: StandardMethod, procedure: AnyProcedure, path: string[]) => boolean) | undefined;
38
- }
39
- declare class RPCMatcher {
40
- private readonly filter;
41
- private readonly allowMethodsSet;
42
- private readonly allowMethodsFn;
43
- private readonly rootRouter;
44
- private readonly tree;
45
- private readonly pendingLazyRouters;
46
- /** segment count of the deepest pending key, never lowered so it stays an upper bound */
47
- private maxPendingLazyRouterDepth;
48
- constructor(router: AnyRouter, options?: RPCMatcherOptions);
49
- private index;
50
- match(method: StandardMethod, pathname: `/${string}`, prefix: `/${string}` | undefined): Promise<{
51
- path: string[];
52
- procedure: AnyProcedure;
53
- } | undefined>;
54
- private resolvePendingLazyRouters;
55
- private loadPendingLazyRouter;
56
- private indexPendingLazyRouter;
57
- private resolveProcedure;
58
- }
59
-
60
- interface RPCHandlerCodecOptions<T extends Context> extends RPCMatcherOptions {
61
- /**
62
- * Override the default RPC serializer.
63
- */
64
- serializer?: Public<RPCSerializer> | undefined;
65
- /**
66
- * Resolve HTTP status for encoded successful outputs.
67
- *
68
- * Value should be in the `2xx` range and must be less than `400`.
69
- * Return `undefined` or `null` to fallback to default
70
- *
71
- * @default DEFAULT_SUCCESS_STATUS (200)
72
- */
73
- outputStatus?: Value<number | undefined | null, [output: unknown, procedure: AnyProcedure, path: string[], options: ResolvedStandardHandlerHandleOptions<T>]> | undefined;
74
- /**
75
- * Mapping ORPCError Code -> HTTP Status Code
76
- * The status code should be in the `4xx` or `5xx` range (must be greater than or equal to `400`).
77
- * Codes missing from the map fall back to `DEFAULT_ERROR_STATUS` (400).
78
- *
79
- * @default COMMON_ERROR_STATUS_MAP
80
- */
81
- errorStatusMap?: Record<string, number> | undefined;
82
- }
83
- declare class RPCHandlerCodec<T extends Context> implements StandardHandlerCodec<T> {
84
- private readonly matcher;
85
- private readonly serializer;
86
- private readonly errorStatusMap;
87
- private readonly outputStatus;
88
- constructor(router: AnyRouter, options?: RPCHandlerCodecOptions<T>);
89
- resolveProcedure(request: StandardLazyRequest, options: ResolvedStandardHandlerHandleOptions<T>): Promise<StandardHandlerCodecResolvedProcedure | undefined>;
90
- encodeOutput(output: unknown, procedure: AnyProcedure, path: string[], options: ResolvedStandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
91
- encodeError(error: AnyORPCError, _procedure: AnyProcedure, _path: string[], _options: ResolvedStandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
92
- }
93
-
94
- export { RPCHandlerCodec, RPCMatcher, RPC_DEFAULT_ALLOW_METHODS, ResolvedStandardHandlerHandleOptions, StandardHandlerCodec, StandardHandlerCodecResolvedProcedure };
95
- export type { RPCHandlerCodecOptions, RPCMatcherOptions };
1
+ export * from './codec.ts';
2
+ export * from './handler.ts';
3
+ export type { StandardHandlerPlugin } from './plugin.ts';
4
+ export * from './rpc-handler-codec.ts';
5
+ export * from './rpc-matcher.ts';
6
+ export * from './utils.ts';
@@ -0,0 +1,5 @@
1
+ export * from './codec.js';
2
+ export * from './handler.js';
3
+ export * from './rpc-handler-codec.js';
4
+ export * from './rpc-matcher.js';
5
+ export * from './utils.js';
@@ -0,0 +1,32 @@
1
+ import type { OrderablePlugin } from '@orpc/shared';
2
+ import type { Context } from '../../context.ts';
3
+ import type { StandardHandlerOptions } from './handler.ts';
4
+ export interface StandardHandlerPlugin<T extends Context> extends OrderablePlugin {
5
+ /**
6
+ * Initializes the plugin and returns new handler options.
7
+ * Called once per plugin instance during composition.
8
+ *
9
+ * This method allows plugins to wrap, extend, or transform handler options
10
+ * such as interceptors, or configuration.
11
+ *
12
+ * @param options - The current handler options from previous plugins or base configuration
13
+ * @returns Transformed handler options with plugin's modifications applied
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * init(options) {
18
+ * return {
19
+ * ...options,
20
+ * interceptors: [...(options.interceptors || []), myInterceptor]
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+ init?(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
26
+ }
27
+ export declare class CompositeStandardHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
28
+ readonly name = "~composite";
29
+ protected readonly plugins: StandardHandlerPlugin<T>[];
30
+ constructor(plugins?: StandardHandlerPlugin<T>[]);
31
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
32
+ }
@@ -0,0 +1,16 @@
1
+ import { sortPlugins } from '@orpc/shared';
2
+ export class CompositeStandardHandlerPlugin {
3
+ name = '~composite';
4
+ plugins;
5
+ constructor(plugins = []) {
6
+ this.plugins = sortPlugins(plugins);
7
+ }
8
+ init(options) {
9
+ for (const plugin of this.plugins) {
10
+ if (plugin.init) {
11
+ options = plugin.init(options);
12
+ }
13
+ }
14
+ return options;
15
+ }
16
+ }
@@ -0,0 +1,46 @@
1
+ import type { AnyORPCError } from '@orpc/client';
2
+ import { RPCSerializer } from '@orpc/client';
3
+ import type { Promisable, Public, Value } from '@orpc/shared';
4
+ import type { StandardLazyRequest, StandardResponse } from '@standard-server/core';
5
+ import type { Context } from '../../context.ts';
6
+ import type { AnyProcedure } from '../../procedure.ts';
7
+ import type { AnyRouter } from '../../router.ts';
8
+ import type { ResolvedStandardHandlerHandleOptions, StandardHandlerCodec, StandardHandlerCodecResolvedProcedure } from '../standard/index.ts';
9
+ import type { RPCMatcherOptions } from './rpc-matcher.ts';
10
+ export interface RPCHandlerCodecOptions<T extends Context> extends RPCMatcherOptions {
11
+ /**
12
+ * Override the default RPC serializer.
13
+ */
14
+ serializer?: Public<RPCSerializer> | undefined;
15
+ /**
16
+ * Resolve HTTP status for encoded successful outputs.
17
+ *
18
+ * Value should be in the `2xx` range and must be less than `400`.
19
+ *
20
+ * @default DEFAULT_SUCCESS_STATUS (200)
21
+ */
22
+ outputStatus?: Value<number, [
23
+ output: unknown,
24
+ procedure: AnyProcedure,
25
+ path: string[],
26
+ options: ResolvedStandardHandlerHandleOptions<T>
27
+ ]> | undefined;
28
+ /**
29
+ * Mapping ORPCError Code -> HTTP Status Code
30
+ * The status code should be in the `4xx` or `5xx` range (must be greater than or equal to `400`).
31
+ * Codes missing from the map fall back to `DEFAULT_ERROR_STATUS` (400).
32
+ *
33
+ * @default COMMON_ERROR_STATUS_MAP
34
+ */
35
+ errorStatusMap?: Record<string, number> | undefined;
36
+ }
37
+ export declare class RPCHandlerCodec<T extends Context> implements StandardHandlerCodec<T> {
38
+ private readonly matcher;
39
+ private readonly serializer;
40
+ private readonly errorStatusMap;
41
+ private readonly outputStatus;
42
+ constructor(router: AnyRouter, options?: RPCHandlerCodecOptions<T>);
43
+ resolveProcedure(request: StandardLazyRequest, options: ResolvedStandardHandlerHandleOptions<T>): Promise<StandardHandlerCodecResolvedProcedure | undefined>;
44
+ encodeOutput(output: unknown, procedure: AnyProcedure, path: string[], options: ResolvedStandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
45
+ encodeError(error: AnyORPCError, _procedure: AnyProcedure, _path: string[], _options: ResolvedStandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
46
+ }
@@ -0,0 +1,51 @@
1
+ import { COMMON_ERROR_STATUS_MAP, RPCSerializer } from '@orpc/client';
2
+ import { getOwn, parseEmptyableJSON, value } from '@orpc/shared';
3
+ import { parseStandardUrl } from '@standard-server/core';
4
+ import { DEFAULT_ERROR_STATUS, DEFAULT_SUCCESS_STATUS } from '../../constants.js';
5
+ import { RPCMatcher } from './rpc-matcher.js';
6
+ export class RPCHandlerCodec {
7
+ matcher;
8
+ serializer;
9
+ errorStatusMap;
10
+ outputStatus;
11
+ constructor(router, options = {}) {
12
+ this.matcher = new RPCMatcher(router, options);
13
+ this.serializer = options.serializer ?? new RPCSerializer();
14
+ this.errorStatusMap = options.errorStatusMap ?? COMMON_ERROR_STATUS_MAP;
15
+ this.outputStatus = options.outputStatus ?? DEFAULT_SUCCESS_STATUS;
16
+ }
17
+ async resolveProcedure(request, options) {
18
+ const [pathname, query] = parseStandardUrl(request.url);
19
+ const matched = await this.matcher.match(request.method, pathname, options.prefix);
20
+ if (!matched) {
21
+ return undefined;
22
+ }
23
+ return {
24
+ procedure: matched.procedure,
25
+ path: matched.path,
26
+ decodeInput: async () => {
27
+ if (request.method === 'GET') {
28
+ const dataString = new URLSearchParams(query).getAll('data').at(-1);
29
+ return this.serializer.deserialize(parseEmptyableJSON(dataString));
30
+ }
31
+ const body = await request.resolveBody();
32
+ return this.serializer.deserialize(body);
33
+ },
34
+ };
35
+ }
36
+ encodeOutput(output, procedure, path, options) {
37
+ return {
38
+ headers: {},
39
+ status: value(this.outputStatus, output, procedure, path, options),
40
+ body: this.serializer.serialize(output),
41
+ };
42
+ }
43
+ encodeError(error, _procedure, _path, _options) {
44
+ const status = getOwn(this.errorStatusMap, error.code) ?? DEFAULT_ERROR_STATUS;
45
+ return {
46
+ headers: {},
47
+ status,
48
+ body: this.serializer.serialize(error.toJSON()),
49
+ };
50
+ }
51
+ }
@@ -0,0 +1,53 @@
1
+ import type { AnyProcedureContract } from '@orpc/contract';
2
+ import type { Value } from '@orpc/shared';
3
+ import type { StandardMethod } from '@standard-server/core';
4
+ import type { AnyProcedure } from '../../procedure.ts';
5
+ import type { AnyRouter } from '../../router.ts';
6
+ /**
7
+ * Methods that can invoke procedures by default. Browsers cannot trigger them
8
+ * cross-site without a CORS preflight or an HTML form, unlike `GET`, which a plain
9
+ * `<a>` click or redirect can trigger with `SameSite=Lax` cookies attached. Other
10
+ * methods (`HEAD`, `OPTIONS`, `QUERY`, ...) have safe semantics that should not
11
+ * invoke a procedure that can modify data.
12
+ *
13
+ * @see {@link https://orpc.dev/docs/rpc/handler#supported-http-methods | RPC Handler - Supported HTTP Methods}
14
+ */
15
+ export declare const RPC_DEFAULT_ALLOW_METHODS: readonly StandardMethod[];
16
+ export interface RPCMatcherOptions {
17
+ /**
18
+ * Filter which procedures are exposed for matching. Return `false` to exclude.
19
+ *
20
+ * @default true
21
+ */
22
+ filter?: Value<boolean, [procedure: AnyProcedureContract | AnyProcedure, path: string[]]> | undefined;
23
+ /**
24
+ * Restricts which HTTP methods can invoke procedures, either with a list of allowed
25
+ * methods or decided per request via a function. Requests using a disallowed method
26
+ * are treated as unmatched. `GET` is excluded by default because it is exposed to
27
+ * Cross-Site Request Forgery (CSRF) attacks.
28
+ *
29
+ * @default RPC_DEFAULT_ALLOW_METHODS (['POST', 'PUT', 'PATCH', 'DELETE'])
30
+ * @see {@link https://orpc.dev/docs/rpc/handler#supported-http-methods | RPC Handler - Supported HTTP Methods}
31
+ */
32
+ allowMethods?: readonly StandardMethod[] | ((method: StandardMethod, procedure: AnyProcedure, path: string[]) => boolean) | undefined;
33
+ }
34
+ export declare class RPCMatcher {
35
+ private readonly filter;
36
+ private readonly allowMethodsSet;
37
+ private readonly allowMethodsFn;
38
+ private readonly rootRouter;
39
+ private readonly tree;
40
+ private readonly pendingLazyRouters;
41
+ /** segment count of the deepest pending key, never lowered so it stays an upper bound */
42
+ private maxPendingLazyRouterDepth;
43
+ constructor(router: AnyRouter, options?: RPCMatcherOptions);
44
+ private index;
45
+ match(method: StandardMethod, pathname: `/${string}`, prefix: `/${string}` | undefined): Promise<{
46
+ path: string[];
47
+ procedure: AnyProcedure;
48
+ } | undefined>;
49
+ private resolvePendingLazyRouters;
50
+ private loadPendingLazyRouter;
51
+ private indexPendingLazyRouter;
52
+ private resolveProcedure;
53
+ }
@@ -0,0 +1,152 @@
1
+ import { normalizeHttpPath, pathToHttpPath, value } from '@orpc/shared';
2
+ import { unlazy } from '../../lazy.js';
3
+ import { Procedure } from '../../procedure.js';
4
+ import { resolveContractProcedure, walkProcedureContractsSync } from '../../router-utils.js';
5
+ /**
6
+ * Methods that can invoke procedures by default. Browsers cannot trigger them
7
+ * cross-site without a CORS preflight or an HTML form, unlike `GET`, which a plain
8
+ * `<a>` click or redirect can trigger with `SameSite=Lax` cookies attached. Other
9
+ * methods (`HEAD`, `OPTIONS`, `QUERY`, ...) have safe semantics that should not
10
+ * invoke a procedure that can modify data.
11
+ *
12
+ * @see {@link https://orpc.dev/docs/rpc/handler#supported-http-methods | RPC Handler - Supported HTTP Methods}
13
+ */
14
+ export const RPC_DEFAULT_ALLOW_METHODS = [
15
+ 'POST',
16
+ 'PUT',
17
+ 'PATCH',
18
+ 'DELETE',
19
+ ];
20
+ // `%` or a char `encodeURIComponent` encodes, meaning normalization may change the pathname
21
+ const NORMALIZABLE_PATHNAME_REGEX = /[^\w\-.!~*'()/]/;
22
+ export class RPCMatcher {
23
+ filter;
24
+ allowMethodsSet;
25
+ allowMethodsFn;
26
+ rootRouter;
27
+ tree = new Map();
28
+ pendingLazyRouters = new Map();
29
+ /** segment count of the deepest pending key, never lowered so it stays an upper bound */
30
+ maxPendingLazyRouterDepth = 0;
31
+ constructor(router, options = {}) {
32
+ this.filter = options.filter ?? true;
33
+ const allowMethods = options.allowMethods ?? RPC_DEFAULT_ALLOW_METHODS;
34
+ if (typeof allowMethods !== 'function') {
35
+ this.allowMethodsSet = new Set(allowMethods);
36
+ }
37
+ else {
38
+ this.allowMethodsFn = allowMethods;
39
+ }
40
+ this.rootRouter = router;
41
+ this.index(router);
42
+ }
43
+ index(router, path = []) {
44
+ const lazyResults = walkProcedureContractsSync(router, (procedure, procedurePath) => {
45
+ if (!value(this.filter, procedure, procedurePath)) {
46
+ return;
47
+ }
48
+ this.tree.set(pathToHttpPath(procedurePath), {
49
+ path: procedurePath,
50
+ contract: procedure,
51
+ // contract-first entries have no implementation yet, resolveProcedure fills it in on demand
52
+ procedure: procedure instanceof Procedure ? procedure : undefined,
53
+ });
54
+ }, path);
55
+ for (const result of lazyResults) {
56
+ this.pendingLazyRouters.set(pathToHttpPath(result.path), result);
57
+ this.maxPendingLazyRouterDepth = Math.max(this.maxPendingLazyRouterDepth, result.path.length);
58
+ }
59
+ }
60
+ async match(method, pathname, prefix) {
61
+ if (this.allowMethodsSet && !this.allowMethodsSet.has(method)) {
62
+ return undefined;
63
+ }
64
+ if (prefix) {
65
+ if (!pathname.startsWith(prefix)) {
66
+ return undefined;
67
+ }
68
+ const charAfterPrefix = pathname[prefix.length];
69
+ if (charAfterPrefix === '/') {
70
+ pathname = pathname.slice(prefix.length);
71
+ }
72
+ else if (charAfterPrefix === undefined) {
73
+ pathname = '/';
74
+ }
75
+ else if (prefix[prefix.length - 1] === '/') {
76
+ pathname = pathname.slice(prefix.length - 1);
77
+ }
78
+ else {
79
+ return undefined;
80
+ }
81
+ }
82
+ if (pathname.length > 1 && pathname.endsWith('/')) {
83
+ pathname = pathname.slice(0, -1);
84
+ }
85
+ // most requests `await undefined` so conditionally await it to save a microtask turn
86
+ const loading = this.resolvePendingLazyRouters(pathname);
87
+ if (loading !== undefined) {
88
+ await loading;
89
+ }
90
+ let entry = this.tree.get(pathname);
91
+ if (entry === undefined && NORMALIZABLE_PATHNAME_REGEX.test(pathname)) {
92
+ // Retry with a normalized path: users may percent-encode characters that we store
93
+ // unencoded ("a%62c" vs "abc"), or leave raw characters that we store encoded
94
+ // ("a^b" vs "a%5Eb"), so normalization handles both without duplicate entries.
95
+ const normalizedPathname = normalizeHttpPath(pathname);
96
+ // most requests `await undefined` so conditionally await it to save a microtask turn
97
+ const normalizedLoading = this.resolvePendingLazyRouters(normalizedPathname);
98
+ if (normalizedLoading !== undefined) {
99
+ await normalizedLoading;
100
+ }
101
+ entry = this.tree.get(normalizedPathname);
102
+ }
103
+ if (entry === undefined) {
104
+ return undefined;
105
+ }
106
+ const procedure = entry.procedure ?? (await this.resolveProcedure(entry));
107
+ if (this.allowMethodsFn && !this.allowMethodsFn(method, procedure, entry.path)) {
108
+ return undefined;
109
+ }
110
+ return {
111
+ path: entry.path,
112
+ procedure,
113
+ };
114
+ }
115
+ resolvePendingLazyRouters(pathname) {
116
+ if (this.pendingLazyRouters.size === 0) {
117
+ return;
118
+ }
119
+ let slashIndex = 0;
120
+ // a pending key has as many segments as its path, so deeper prefixes can never match,
121
+ // and scanning them would cost quadratic time on slash-heavy pathnames
122
+ for (let depth = 1; slashIndex !== -1 && depth <= this.maxPendingLazyRouterDepth; depth++) {
123
+ const nextSlashIndex = pathname.indexOf('/', slashIndex + 1);
124
+ const httpPathPrefix = nextSlashIndex === -1 ? pathname : pathname.slice(0, nextSlashIndex);
125
+ const pending = this.pendingLazyRouters.get(httpPathPrefix);
126
+ if (pending !== undefined) {
127
+ return this.loadPendingLazyRouter(httpPathPrefix, pending).then(() => this.resolvePendingLazyRouters(pathname));
128
+ }
129
+ slashIndex = nextSlashIndex;
130
+ }
131
+ }
132
+ loadPendingLazyRouter(httpPathPrefix, pending) {
133
+ if (pending.loading === undefined) {
134
+ pending.loading = this.indexPendingLazyRouter(httpPathPrefix, pending).catch((error) => {
135
+ pending.loading = undefined;
136
+ throw error;
137
+ });
138
+ }
139
+ return pending.loading;
140
+ }
141
+ async indexPendingLazyRouter(httpPathPrefix, pending) {
142
+ const { default: router } = await unlazy(pending.router);
143
+ this.index(router, pending.path);
144
+ // removed only once indexed, so a concurrent match never observes this router as
145
+ // neither pending nor indexed
146
+ this.pendingLazyRouters.delete(httpPathPrefix);
147
+ }
148
+ async resolveProcedure(entry) {
149
+ entry.procedure = await resolveContractProcedure(this.rootRouter, entry.contract, entry.path);
150
+ return entry.procedure;
151
+ }
152
+ }