@procedures/client 0.8.0 → 0.9.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/CHANGELOG.md +78 -0
- package/README.md +95 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts +4 -0
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +11 -1
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/client/types.d.ts +19 -0
- package/dist/adapters/http/client/types.d.ts.map +1 -1
- package/dist/adapters/http/server/cors.d.ts +64 -0
- package/dist/adapters/http/server/cors.d.ts.map +1 -0
- package/dist/adapters/http/server/cors.js +113 -0
- package/dist/adapters/http/server/cors.js.map +1 -0
- package/dist/adapters/http/server/handler.d.ts +136 -0
- package/dist/adapters/http/server/handler.d.ts.map +1 -0
- package/dist/adapters/http/server/handler.js +495 -0
- package/dist/adapters/http/server/handler.js.map +1 -0
- package/dist/adapters/http/server/strategies.d.ts +14 -5
- package/dist/adapters/http/server/strategies.d.ts.map +1 -1
- package/dist/adapters/http/server/strategies.js.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +30 -46
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +48 -432
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/server/types.d.ts +13 -0
- package/dist/adapters/http/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +4 -12
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +46 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +152 -15
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +10 -3
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
- package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade-router.js +62 -0
- package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
- package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
- package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade.js +49 -0
- package/dist/adapters/websocket/server/upgrade.js.map +1 -0
- package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +8 -2
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +12 -4
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +13 -2
- package/dist/browser.js.map +1 -1
- package/dist/client/client.d.ts +43 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +69 -1
- package/dist/client/client.js.map +1 -1
- package/dist/events/publisher.d.ts +25 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +66 -9
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/rows.d.ts +31 -0
- package/dist/events/rows.d.ts.map +1 -0
- package/dist/events/rows.js +39 -0
- package/dist/events/rows.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +14 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +9 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/capability.d.ts +6 -0
- package/dist/procedures/capability.d.ts.map +1 -1
- package/dist/procedures/capability.js +25 -0
- package/dist/procedures/capability.js.map +1 -1
- package/dist/procedures/collection/changes.d.ts +94 -0
- package/dist/procedures/collection/changes.d.ts.map +1 -0
- package/dist/procedures/collection/changes.js +182 -0
- package/dist/procedures/collection/changes.js.map +1 -0
- package/dist/procedures/collection/index.d.ts +10 -3
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +8 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/partition.d.ts +67 -0
- package/dist/procedures/collection/partition.d.ts.map +1 -0
- package/dist/procedures/collection/partition.js +82 -0
- package/dist/procedures/collection/partition.js.map +1 -0
- package/dist/procedures/collection/procedures.d.ts +39 -2
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +147 -44
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/query.d.ts +246 -0
- package/dist/procedures/collection/query.d.ts.map +1 -0
- package/dist/procedures/collection/query.js +854 -0
- package/dist/procedures/collection/query.js.map +1 -0
- package/dist/procedures/collection/schemas.d.ts +50 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +144 -6
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +58 -6
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/collection/storage.js +23 -0
- package/dist/procedures/collection/storage.js.map +1 -1
- package/dist/procedures/elicit.d.ts +190 -0
- package/dist/procedures/elicit.d.ts.map +1 -0
- package/dist/procedures/elicit.js +127 -0
- package/dist/procedures/elicit.js.map +1 -0
- package/dist/procedures/hints.d.ts +43 -0
- package/dist/procedures/hints.d.ts.map +1 -0
- package/dist/procedures/hints.js +74 -0
- package/dist/procedures/hints.js.map +1 -0
- package/dist/procedures/index.d.ts +9 -2
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +5 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts +24 -2
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +14 -1
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/progress.d.ts +24 -0
- package/dist/procedures/progress.d.ts.map +1 -0
- package/dist/procedures/progress.js +11 -0
- package/dist/procedures/progress.js.map +1 -0
- package/dist/procedures/registry.d.ts.map +1 -1
- package/dist/procedures/registry.js +5 -0
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/types.d.ts +55 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/procedures/untrusted.d.ts +28 -0
- package/dist/procedures/untrusted.d.ts.map +1 -0
- package/dist/procedures/untrusted.js +32 -0
- package/dist/procedures/untrusted.js.map +1 -0
- package/dist/server/browser-guard.d.ts +12 -2
- package/dist/server/browser-guard.d.ts.map +1 -1
- package/dist/server/browser-guard.js +43 -6
- package/dist/server/browser-guard.js.map +1 -1
- package/dist/server/host-policy.d.ts +10 -1
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +5 -0
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +68 -7
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +124 -24
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +51 -4
- package/dist/server/request-auth.d.ts.map +1 -1
- package/dist/server/request-auth.js +116 -5
- package/dist/server/request-auth.js.map +1 -1
- package/dist/server/types.d.ts +73 -5
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +7 -3
- package/src/adapters/http/client/transport.ts +11 -1
- package/src/adapters/http/client/types.ts +22 -0
- package/src/adapters/http/server/cors.ts +144 -0
- package/src/adapters/http/server/handler.ts +624 -0
- package/src/adapters/http/server/strategies.ts +16 -6
- package/src/adapters/http/server/transport.ts +55 -478
- package/src/adapters/http/server/types.ts +15 -0
- package/src/adapters/websocket/client/transport.ts +4 -12
- package/src/adapters/websocket/server/transport.ts +166 -16
- package/src/adapters/websocket/server/types.ts +11 -3
- package/src/adapters/websocket/server/upgrade-router.ts +77 -0
- package/src/adapters/websocket/server/upgrade.ts +88 -0
- package/src/adapters/websocket/shared/rpc.ts +9 -2
- package/src/browser.ts +85 -2
- package/src/client/client.ts +83 -1
- package/src/events/publisher.ts +75 -11
- package/src/events/rows.ts +48 -0
- package/src/index.ts +1 -1
- package/src/node.ts +21 -0
- package/src/procedures/capability.ts +21 -0
- package/src/procedures/collection/changes.ts +245 -0
- package/src/procedures/collection/index.ts +57 -1
- package/src/procedures/collection/partition.ts +123 -0
- package/src/procedures/collection/procedures.ts +206 -51
- package/src/procedures/collection/query.ts +1009 -0
- package/src/procedures/collection/schemas.ts +196 -7
- package/src/procedures/collection/storage.ts +78 -6
- package/src/procedures/elicit.ts +320 -0
- package/src/procedures/hints.ts +91 -0
- package/src/procedures/index.ts +83 -2
- package/src/procedures/invoke.ts +33 -2
- package/src/procedures/progress.ts +25 -0
- package/src/procedures/registry.ts +4 -0
- package/src/procedures/types.ts +65 -0
- package/src/procedures/untrusted.ts +50 -0
- package/src/server/browser-guard.ts +48 -6
- package/src/server/host-policy.ts +13 -1
- package/src/server/index.ts +3 -0
- package/src/server/procedure-server.ts +147 -26
- package/src/server/request-auth.ts +128 -6
- package/src/server/types.ts +82 -5
|
@@ -1,44 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTTP
|
|
2
|
+
* The HTTP server transport of Express: a shim over `procedureHandler` (CLI-3).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The transport mounts the procedure handler of `handler.ts` on an Express app, with the CORS of
|
|
5
|
+
* the app, and it owns its own `node:http` server unless the caller gives one. The handler does
|
|
6
|
+
* the work: the request, the body, the metadata, the credentials, the response and the NDJSON
|
|
7
|
+
* streams. The transport keeps its former error codes (`SERVER_STOPPED` and the codes of the
|
|
8
|
+
* browser checks), so its behavior stays the same.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import type { Request, Response, Express } from "express";
|
|
9
12
|
import { createServer } from "http";
|
|
10
13
|
import type { Server as HttpServer } from "http";
|
|
11
|
-
import type { ServerTransport
|
|
12
|
-
import type { Metadata } from "../../../client/types.js";
|
|
14
|
+
import type { ServerTransport } from "../../../server/types.js";
|
|
13
15
|
import type { RequestHandler } from "../../../server/types.js";
|
|
14
|
-
import { HTTPMethod, HTTPHeaders, decodeMetadataHeader, METADATA_HEADER_LIMIT } from "../shared/index.js";
|
|
15
|
-
import { ERROR_REGISTRY } from "../../../client/errors/index.js";
|
|
16
16
|
import type { HttpServerTransportOptions } from "./types.js";
|
|
17
17
|
import { createPatternServerUrlStrategy } from "./strategies.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { PROTOCOL_HEADER, PROTOCOL_VERSION, httpStatusOf, protocolVersionOf } from "../../../protocol.js";
|
|
21
|
-
import { realClock, type Clock } from "../../../clock.js";
|
|
22
|
-
import { ms, type Duration } from "../../../duration.js";
|
|
23
|
-
import { NDJSON_MEDIA_TYPE as NDJSON, defaultNdjsonError as errorOf, keepAliveMsOf, writeNdjsonStream } from "./ndjson.js";
|
|
24
|
-
|
|
25
|
-
/** True when a stream response can run without an end (`STREAM_METADATA_KEY` of procedure-server.ts). */
|
|
26
|
-
function isUnboundedStream(response: { metadata?: Record<string, unknown> | undefined }): boolean {
|
|
27
|
-
return response.metadata?.["stream"] === "unbounded";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** The metadata keys that the transport sets: a query parameter or a header cannot replace them. */
|
|
31
|
-
const RESERVED_METADATA = new Set(["headers", "query", "params"]);
|
|
32
|
-
|
|
33
|
-
/** The error of a request body that the transport cannot read. */
|
|
34
|
-
class BodyError extends Error {
|
|
35
|
-
constructor(
|
|
36
|
-
message: string,
|
|
37
|
-
readonly httpStatus: number
|
|
38
|
-
) {
|
|
39
|
-
super(message);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
18
|
+
import { applyCors, corsOrigins, corsPolicy, type CorsPolicy } from "./cors.js";
|
|
19
|
+
import { createProcedureHandler, type NodeProcedureHandler } from "./handler.js";
|
|
42
20
|
|
|
43
21
|
/**
|
|
44
22
|
* HTTP server transport adapter for Express.
|
|
@@ -59,68 +37,58 @@ class BodyError extends Error {
|
|
|
59
37
|
* await httpTransport.start();
|
|
60
38
|
* ```
|
|
61
39
|
*/
|
|
62
|
-
/**
|
|
63
|
-
* The origins that the CORS options allow: a page that CORS lets read the response can also
|
|
64
|
-
* call. With CORS off, no other origin can call.
|
|
65
|
-
*/
|
|
66
|
-
function corsOrigins(options: HttpServerTransportOptions): BrowserGuardOptions["allowedOrigins"] {
|
|
67
|
-
if (!options.cors) return [];
|
|
68
|
-
const origin = options.corsOptions?.origin ?? "*";
|
|
69
|
-
if (origin === "*") return "*";
|
|
70
|
-
return Array.isArray(origin) ? origin : [origin];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
40
|
export class HttpServerTransport implements ServerTransport {
|
|
74
41
|
readonly name = "http";
|
|
75
42
|
|
|
76
43
|
private app: Express;
|
|
77
44
|
private httpServer: HttpServer | null = null;
|
|
78
|
-
private options:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
> & {
|
|
84
|
-
corsOptions: HttpServerTransportOptions["corsOptions"] | undefined;
|
|
85
|
-
httpServer: HttpServer | undefined;
|
|
86
|
-
};
|
|
87
|
-
private server: RequestHandler;
|
|
88
|
-
/** The controllers of the requests in progress: stop() aborts them (deep dive TRN-15) */
|
|
89
|
-
private openRequests = new Set<AbortController>();
|
|
45
|
+
private options: { port: number; host: string; basePath: string; cors: boolean; httpServer: HttpServer | undefined };
|
|
46
|
+
/** The procedure handler that does the work of each request (CLI-3) */
|
|
47
|
+
private readonly handler: NodeProcedureHandler;
|
|
48
|
+
/** The CORS of the app, or undefined when it is off */
|
|
49
|
+
private readonly cors: CorsPolicy | undefined;
|
|
90
50
|
private stopped = false;
|
|
91
|
-
private readonly bodyLimit: number;
|
|
92
|
-
/** The browser checks of each procedure call (see `server/browser-guard.ts`) */
|
|
93
|
-
private readonly guard: BrowserGuardOptions;
|
|
94
|
-
/** The quiet time of an NDJSON stream before a keep-alive line, or false for none */
|
|
95
|
-
private readonly keepAlive: Duration | false;
|
|
96
|
-
/** The clock of the keep-alive timers (roadmap R35) */
|
|
97
|
-
private readonly clock: Clock;
|
|
98
51
|
|
|
99
52
|
constructor(server: RequestHandler, options: HttpServerTransportOptions = {}) {
|
|
100
|
-
this.server = server;
|
|
101
53
|
const basePath = options.basePath ?? "/api";
|
|
102
54
|
this.options = {
|
|
103
55
|
port: options.port ?? 3000,
|
|
104
|
-
// Loopback by default (see BUGS-2026-07 H19)
|
|
105
|
-
// the HTTP client's default URL format instead of inferring the operation from the verb
|
|
106
|
-
// (which lost the payload for get/list/find/watch). See BUGS-2026-07 (C3, bug 16).
|
|
56
|
+
// Loopback by default (see BUGS-2026-07 H19)
|
|
107
57
|
host: options.host ?? "127.0.0.1",
|
|
108
|
-
urlStrategy: options.urlStrategy ?? createPatternServerUrlStrategy(basePath),
|
|
109
58
|
basePath,
|
|
110
59
|
cors: options.cors ?? false,
|
|
111
|
-
corsOptions: options.corsOptions,
|
|
112
60
|
httpServer: options.httpServer,
|
|
113
61
|
};
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
62
|
+
// Route by URL path so the raw server matches the HTTP client's default URL format instead of
|
|
63
|
+
// inferring the operation from the verb (BUGS-2026-07 C3, bug 16)
|
|
64
|
+
const urlStrategy = options.urlStrategy ?? createPatternServerUrlStrategy(basePath);
|
|
65
|
+
const corsOptions = this.options.cors ? (options.corsOptions ?? {}) : undefined;
|
|
66
|
+
// The given options go over the defaults, and the transport sends the credentials by default
|
|
67
|
+
this.cors = corsOptions ? corsPolicy(corsOptions, true) : undefined;
|
|
68
|
+
this.handler = createProcedureHandler(
|
|
69
|
+
server,
|
|
70
|
+
{
|
|
71
|
+
basePath,
|
|
72
|
+
bodyLimit: options.bodyLimit,
|
|
73
|
+
keepAlive: options.keepAlive,
|
|
74
|
+
clock: options.clock,
|
|
75
|
+
allowedOrigins: options.allowedOrigins ?? corsOrigins(corsOptions),
|
|
76
|
+
allowedHosts: options.allowedHosts,
|
|
77
|
+
allowGet: options.allowGet,
|
|
78
|
+
secretHeaders: options.secretHeaders,
|
|
79
|
+
secretQuery: options.secretQuery,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
legacy: true,
|
|
83
|
+
matchAll: true,
|
|
84
|
+
face: { name: "HttpServerTransport", originOption: "corsOptions.origin" },
|
|
85
|
+
corsOutside: true,
|
|
86
|
+
methodOf: (req) => urlStrategy(req as Request),
|
|
87
|
+
sendJson: (res, status, body) => {
|
|
88
|
+
(res as Response).status(status).json(body);
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
);
|
|
124
92
|
|
|
125
93
|
// Use provided app or create new one
|
|
126
94
|
if (options.app) {
|
|
@@ -136,76 +104,15 @@ export class HttpServerTransport implements ServerTransport {
|
|
|
136
104
|
}
|
|
137
105
|
|
|
138
106
|
/**
|
|
139
|
-
* Setup Express routes.
|
|
107
|
+
* Setup Express routes: the CORS of the app, then the procedure handler under the base path.
|
|
140
108
|
*/
|
|
141
109
|
private setupRoutes(): void {
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
//
|
|
145
|
-
// them
|
|
146
|
-
const corsOptions = {
|
|
147
|
-
origin: "*" as string | string[],
|
|
148
|
-
methods: [
|
|
149
|
-
HTTPMethod.GET,
|
|
150
|
-
HTTPMethod.POST,
|
|
151
|
-
HTTPMethod.PUT,
|
|
152
|
-
HTTPMethod.DELETE,
|
|
153
|
-
HTTPMethod.PATCH,
|
|
154
|
-
HTTPMethod.OPTIONS,
|
|
155
|
-
],
|
|
156
|
-
// The headers that HttpTransport sends from the metadata. Without X-Metadata, a page of
|
|
157
|
-
// another origin failed its preflight for every call with custom metadata (pagination).
|
|
158
|
-
allowedHeaders: [
|
|
159
|
-
"Content-Type",
|
|
160
|
-
"Accept",
|
|
161
|
-
HTTPHeaders.AUTHORIZATION,
|
|
162
|
-
HTTPHeaders.API_KEY,
|
|
163
|
-
HTTPHeaders.REQUEST_ID,
|
|
164
|
-
HTTPHeaders.TRACE_ID,
|
|
165
|
-
HTTPHeaders.SPAN_ID,
|
|
166
|
-
HTTPHeaders.PARENT_SPAN_ID,
|
|
167
|
-
"traceparent",
|
|
168
|
-
PROTOCOL_HEADER,
|
|
169
|
-
HTTPHeaders.METADATA,
|
|
170
|
-
"collection",
|
|
171
|
-
"collectionName",
|
|
172
|
-
"database",
|
|
173
|
-
],
|
|
174
|
-
credentials: true,
|
|
175
|
-
...this.options.corsOptions,
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// One allowed origin goes back: the request's origin when it is in the list (a header
|
|
179
|
-
// with several origins is invalid), with Vary: Origin so that caches keep them apart
|
|
180
|
-
const origins = corsOptions.origin ?? "*";
|
|
181
|
-
const allowOrigin = (requestOrigin: string | undefined): string | undefined => {
|
|
182
|
-
if (origins === "*") return "*";
|
|
183
|
-
const list = Array.isArray(origins) ? origins : [origins];
|
|
184
|
-
return requestOrigin !== undefined && list.includes(requestOrigin) ? requestOrigin : undefined;
|
|
185
|
-
};
|
|
186
|
-
|
|
110
|
+
const cors = this.cors;
|
|
111
|
+
if (cors) {
|
|
112
|
+
// One allowed origin goes back: the request's origin when it is in the list, with
|
|
113
|
+
// Vary: Origin so that caches keep them apart. A preflight ends here with 204.
|
|
187
114
|
this.app.use((req, res, next) => {
|
|
188
|
-
|
|
189
|
-
if (origins !== "*") res.vary("Origin");
|
|
190
|
-
if (allowed !== undefined) res.header("Access-Control-Allow-Origin", allowed);
|
|
191
|
-
res.header(
|
|
192
|
-
"Access-Control-Allow-Methods",
|
|
193
|
-
corsOptions.methods?.join(", ")
|
|
194
|
-
);
|
|
195
|
-
res.header(
|
|
196
|
-
"Access-Control-Allow-Headers",
|
|
197
|
-
corsOptions.allowedHeaders?.join(", ")
|
|
198
|
-
);
|
|
199
|
-
if (corsOptions.credentials) {
|
|
200
|
-
res.header("Access-Control-Allow-Credentials", "true");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (req.method === "OPTIONS") {
|
|
204
|
-
res.sendStatus(204);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
next();
|
|
115
|
+
if (!applyCors(req, res, cors)) next();
|
|
209
116
|
});
|
|
210
117
|
}
|
|
211
118
|
|
|
@@ -213,344 +120,14 @@ export class HttpServerTransport implements ServerTransport {
|
|
|
213
120
|
this.app.all(
|
|
214
121
|
new RegExp(`^${this.options.basePath.replace(/\//g, "\\/")}\\/.*`),
|
|
215
122
|
async (req, res) => {
|
|
216
|
-
await this.
|
|
123
|
+
await this.handler.handle(req, res);
|
|
217
124
|
}
|
|
218
125
|
);
|
|
219
126
|
}
|
|
220
127
|
|
|
221
|
-
/**
|
|
222
|
-
* Handle HTTP request.
|
|
223
|
-
* Converts to unified RPC format, processes, and sends response.
|
|
224
|
-
*/
|
|
225
|
-
private async handleHttpRequest(req: Request, res: Response): Promise<void> {
|
|
226
|
-
// The protocol version of this server, on each response (roadmap R25)
|
|
227
|
-
res.setHeader(PROTOCOL_HEADER, String(PROTOCOL_VERSION));
|
|
228
|
-
const version = protocolVersionOf(req.headers[PROTOCOL_HEADER.toLowerCase()]);
|
|
229
|
-
if (version !== undefined && version > PROTOCOL_VERSION) {
|
|
230
|
-
res.status(400).json({
|
|
231
|
-
error: `This server speaks the protocol ${PROTOCOL_VERSION}, and the request uses ${version}`,
|
|
232
|
-
code: "PROTOCOL_VERSION",
|
|
233
|
-
retryable: false,
|
|
234
|
-
protocol: PROTOCOL_VERSION,
|
|
235
|
-
});
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (this.stopped) {
|
|
239
|
-
res.status(503).json({ error: "Server stopped", code: "SERVER_STOPPED", retryable: true });
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
// A web page must not reach the procedures: host, origin, method and body type
|
|
243
|
-
const guard = checkBrowserRequest(req, this.guard, true);
|
|
244
|
-
if (!guard.ok) {
|
|
245
|
-
res.status(guard.status).json({ error: guard.message, code: guard.code, retryable: false });
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
// A closed connection or stop() aborts the request: a stream stops, and its handler gets return()
|
|
249
|
-
const controller = new AbortController();
|
|
250
|
-
this.openRequests.add(controller);
|
|
251
|
-
res.on("close", () => {
|
|
252
|
-
this.openRequests.delete(controller);
|
|
253
|
-
if (!res.writableFinished) controller.abort();
|
|
254
|
-
});
|
|
255
|
-
try {
|
|
256
|
-
// Convert HTTP request to ServerRequest
|
|
257
|
-
let serverRequest: ServerRequest | null;
|
|
258
|
-
try {
|
|
259
|
-
serverRequest = this.httpToServerRequest(req, await this.readBody(req));
|
|
260
|
-
} catch (error) {
|
|
261
|
-
const status = error instanceof BodyError ? error.httpStatus : 400;
|
|
262
|
-
res.status(status).json({ error: (error as Error).message, code: "INVALID_REQUEST", retryable: false });
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (!serverRequest) {
|
|
267
|
-
res.status(400).json({
|
|
268
|
-
error: "Invalid request: could not parse RPC method from URL",
|
|
269
|
-
});
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
serverRequest.signal = controller.signal;
|
|
274
|
-
|
|
275
|
-
// Process request through universal server
|
|
276
|
-
const serverResponse = await this.server.handle(serverRequest);
|
|
277
|
-
|
|
278
|
-
if (serverResponse.stream) {
|
|
279
|
-
await this.streamToHttp(serverResponse, serverResponse.stream, req, res, controller.signal);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Convert ServerResponse to HTTP response
|
|
284
|
-
this.serverResponseToHttp(serverResponse, res);
|
|
285
|
-
} catch (error) {
|
|
286
|
-
if (!res.headersSent) {
|
|
287
|
-
res.status(500).json({
|
|
288
|
-
error: (error as Error).message || "Internal server error",
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
} finally {
|
|
292
|
-
this.openRequests.delete(controller);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* The request body. A body parser of the app (`express.json({ strict: false })`) gives
|
|
298
|
-
* `req.body`. Without one, the transport reads the JSON body itself: before, every payload
|
|
299
|
-
* was {} when the app had no parser, and a parser with `strict: true` rejected a number or
|
|
300
|
-
* a string (deep dive TRN-10).
|
|
301
|
-
*/
|
|
302
|
-
private async readBody(req: Request): Promise<unknown> {
|
|
303
|
-
if (req.body !== undefined) return req.body;
|
|
304
|
-
if (req.method === "GET" || req.method === "HEAD" || req.readableEnded) return undefined;
|
|
305
|
-
const chunks: Buffer[] = [];
|
|
306
|
-
let size = 0;
|
|
307
|
-
for await (const chunk of req as AsyncIterable<Buffer | string>) {
|
|
308
|
-
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
309
|
-
size += buffer.length;
|
|
310
|
-
if (size > this.bodyLimit) throw new BodyError(`The request body is larger than ${this.bodyLimit} bytes`, 413);
|
|
311
|
-
chunks.push(buffer);
|
|
312
|
-
}
|
|
313
|
-
const text = Buffer.concat(chunks).toString("utf8");
|
|
314
|
-
if (text.trim() === "") return undefined;
|
|
315
|
-
try {
|
|
316
|
-
return JSON.parse(text) as unknown;
|
|
317
|
-
} catch {
|
|
318
|
-
throw new BodyError("The request body is not valid JSON", 400);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Send a stream response.
|
|
324
|
-
*
|
|
325
|
-
* A client that accepts `application/x-ndjson` gets one JSON line for each item
|
|
326
|
-
* (`{"type":"item","seq":1,"payload":...}`), then `{"type":"done","seq":...}`, or
|
|
327
|
-
* `{"type":"error","error":...}`. The `seq` of the lines counts from 1, as in the `stream`
|
|
328
|
-
* frames of WebSocket. A quiet stream gets a keep-alive line, a blank line, after each quiet
|
|
329
|
-
* time of `keepAlive`. `writeNdjsonStream` writes these lines.
|
|
330
|
-
* Another client (curl, a browser fetch without the header) gets the last item as plain JSON,
|
|
331
|
-
* the "sponge" mode, so a streaming procedure does not change its response format.
|
|
332
|
-
*/
|
|
333
|
-
private async streamToHttp(
|
|
334
|
-
response: ServerResponse,
|
|
335
|
-
stream: AsyncIterable<unknown>,
|
|
336
|
-
req: Request,
|
|
337
|
-
res: Response,
|
|
338
|
-
signal: AbortSignal
|
|
339
|
-
): Promise<void> {
|
|
340
|
-
const ndjson = String(req.headers.accept ?? "").includes(NDJSON);
|
|
341
|
-
|
|
342
|
-
if (!ndjson && isUnboundedStream(response)) {
|
|
343
|
-
// The last item of a stream without an end never comes: refuse the call, and end the stream
|
|
344
|
-
// (roadmap R15: before, the request waited until a timeout)
|
|
345
|
-
void Promise.resolve(stream[Symbol.asyncIterator]().return?.()).catch(() => undefined);
|
|
346
|
-
this.serverResponseToHttp(
|
|
347
|
-
{
|
|
348
|
-
id: response.id,
|
|
349
|
-
status: {
|
|
350
|
-
type: "error",
|
|
351
|
-
code: "NOT_ACCEPTABLE",
|
|
352
|
-
message: `This procedure gives a stream that can run without an end. Send "Accept: ${NDJSON}" to read its items.`,
|
|
353
|
-
retryable: false,
|
|
354
|
-
},
|
|
355
|
-
metadata: {},
|
|
356
|
-
},
|
|
357
|
-
res
|
|
358
|
-
);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (!ndjson) {
|
|
363
|
-
let last: { value: unknown } | undefined;
|
|
364
|
-
try {
|
|
365
|
-
for await (const item of stream) last = { value: item };
|
|
366
|
-
} catch (error) {
|
|
367
|
-
this.serverResponseToHttp({ id: response.id, status: { type: "error", ...errorOf(error) }, metadata: {} }, res);
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
if (!last) {
|
|
371
|
-
this.serverResponseToHttp(
|
|
372
|
-
{
|
|
373
|
-
id: response.id,
|
|
374
|
-
status: { type: "error", code: "NO_OUTPUT", message: "The procedure gave no output", retryable: false },
|
|
375
|
-
metadata: {},
|
|
376
|
-
},
|
|
377
|
-
res
|
|
378
|
-
);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
this.serverResponseToHttp({ id: response.id, status: response.status, metadata: response.metadata, payload: last.value }, res);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// The lines, the backpressure, the keep-alive and the abort of stop() (WIRE-PROTOCOL.md)
|
|
386
|
-
await writeNdjsonStream(res, stream, { signal, keepAlive: this.keepAlive, errorOf, clock: this.clock });
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* This method converts an HTTP request to a `ServerRequest`.
|
|
391
|
-
*/
|
|
392
|
-
private httpToServerRequest(req: Request, body: unknown): ServerRequest | null {
|
|
393
|
-
// Parse method from URL using strategy
|
|
394
|
-
const method = this.options.urlStrategy(req);
|
|
395
|
-
if (!method) {
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Generate request ID
|
|
400
|
-
const id =
|
|
401
|
-
(req.headers["x-request-id"] as string) ??
|
|
402
|
-
`req-${Date.now()}-${Math.random()}`;
|
|
403
|
-
const bearer = bearerOf(req.headers["authorization"]);
|
|
404
|
-
|
|
405
|
-
// Build metadata from headers and query params. The flattened query parameters cannot
|
|
406
|
-
// replace headers, query or params (before, "?headers=x" replaced metadata.headers).
|
|
407
|
-
// The headers without the secrets: the credentials go in request.credentials, not in the
|
|
408
|
-
// metadata, so a handler and a nested call do not see them (roadmap R18)
|
|
409
|
-
const metadata: Metadata = {
|
|
410
|
-
headers: withoutSecretHeaders(req.headers as Record<string, string>),
|
|
411
|
-
query: req.query,
|
|
412
|
-
params: req.params,
|
|
413
|
-
};
|
|
414
|
-
for (const [key, value] of Object.entries(req.query)) {
|
|
415
|
-
if (!RESERVED_METADATA.has(key) && !key.startsWith("__")) metadata[key] = value;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Extract custom headers to root-level metadata
|
|
419
|
-
// This mirrors the client's metadataToHeaders() behavior
|
|
420
|
-
const standardHeaders = new Set([
|
|
421
|
-
"host",
|
|
422
|
-
"connection",
|
|
423
|
-
"content-length",
|
|
424
|
-
"content-type",
|
|
425
|
-
"user-agent",
|
|
426
|
-
"accept",
|
|
427
|
-
"accept-encoding",
|
|
428
|
-
"accept-language",
|
|
429
|
-
"cache-control",
|
|
430
|
-
"origin",
|
|
431
|
-
"referer",
|
|
432
|
-
"x-request-id",
|
|
433
|
-
"x-trace-id",
|
|
434
|
-
"x-span-id",
|
|
435
|
-
"x-parent-span-id",
|
|
436
|
-
"x-timeout",
|
|
437
|
-
"x-api-key",
|
|
438
|
-
"authorization",
|
|
439
|
-
"x-metadata",
|
|
440
|
-
]);
|
|
441
|
-
|
|
442
|
-
// Known custom header mappings (lowercase → camelCase)
|
|
443
|
-
const headerMappings: Record<string, string> = {
|
|
444
|
-
'collectionname': 'collectionName',
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
448
|
-
if (!standardHeaders.has(key.toLowerCase()) && typeof value === "string") {
|
|
449
|
-
// Use mapped name if available, otherwise use original key
|
|
450
|
-
const normalizedKey = headerMappings[key.toLowerCase()] || key;
|
|
451
|
-
if (!RESERVED_METADATA.has(normalizedKey) && !normalizedKey.startsWith("__")) {
|
|
452
|
-
metadata[normalizedKey] = value;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// The custom metadata of the HTTP client: any JSON value, any key (deep dive TRN-11)
|
|
458
|
-
const encoded = req.headers["x-metadata"];
|
|
459
|
-
if (typeof encoded === "string") {
|
|
460
|
-
if (encoded.length > METADATA_HEADER_LIMIT) {
|
|
461
|
-
throw new BodyError(`The X-Metadata header is longer than ${METADATA_HEADER_LIMIT} characters`, 431);
|
|
462
|
-
}
|
|
463
|
-
const custom = decodeMetadataHeader(encoded);
|
|
464
|
-
if (!custom) throw new BodyError("The X-Metadata header is not URI-encoded JSON of an object", 400);
|
|
465
|
-
for (const [key, value] of Object.entries(custom)) {
|
|
466
|
-
if (!RESERVED_METADATA.has(key) && key !== "auth" && !key.startsWith("__")) metadata[key] = value;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Payload from body (for POST/PUT) or params (for GET)
|
|
471
|
-
let payload: unknown = body;
|
|
472
|
-
if (req.method === "GET" && req.params["id"]) {
|
|
473
|
-
payload = { id: req.params["id"] };
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Default to empty object only if there is no payload (common for GET requests): 0, false,
|
|
477
|
-
// "" and null stay (deep dive TRN-10)
|
|
478
|
-
if (payload === undefined) {
|
|
479
|
-
payload = {};
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return {
|
|
483
|
-
id,
|
|
484
|
-
method,
|
|
485
|
-
payload,
|
|
486
|
-
metadata,
|
|
487
|
-
// A network request: ProcedureServer examines the token (roadmap R18)
|
|
488
|
-
credentials: bearer === undefined ? {} : { bearer },
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Convert ServerResponse to HTTP response.
|
|
494
|
-
*/
|
|
495
|
-
private serverResponseToHttp(
|
|
496
|
-
serverResponse: ServerResponse,
|
|
497
|
-
res: Response
|
|
498
|
-
): void {
|
|
499
|
-
const { status, payload, metadata } = serverResponse;
|
|
500
|
-
|
|
501
|
-
// Set status code (convert string to number if needed)
|
|
502
|
-
let httpStatusCode: number;
|
|
503
|
-
if (typeof status.code === "string") {
|
|
504
|
-
// status.code is a symbolic error code (e.g. "VALIDATION_ERROR", "NOT_FOUND"). Map it to an
|
|
505
|
-
// HTTP status via the error registry rather than parseInt (which yields NaN -> 500, so every
|
|
506
|
-
// error became a 500). See documentation/BUGS-2026-07.md (M7).
|
|
507
|
-
const mapped =
|
|
508
|
-
httpStatusOf(status.code) ??
|
|
509
|
-
(ERROR_REGISTRY as Record<string, { httpStatus?: number }>)[status.code]?.httpStatus;
|
|
510
|
-
httpStatusCode = mapped ?? (status.type === "success" ? 200 : 500);
|
|
511
|
-
} else if (typeof status.code === "number") {
|
|
512
|
-
httpStatusCode = status.code;
|
|
513
|
-
} else {
|
|
514
|
-
// Default to 200 for success, 500 for error if code is null/undefined
|
|
515
|
-
httpStatusCode = status.type === "success" ? 200 : 500;
|
|
516
|
-
}
|
|
517
|
-
res.status(httpStatusCode);
|
|
518
|
-
|
|
519
|
-
// Set headers from metadata
|
|
520
|
-
if (metadata["headers"]) {
|
|
521
|
-
for (const [key, value] of Object.entries(metadata["headers"])) {
|
|
522
|
-
res.setHeader(key, value as string);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Send response. A procedure that gave no result answers 204 with no body: before, the
|
|
527
|
-
// body was empty with a JSON content type, and the client got "" (deep dive TRN-10).
|
|
528
|
-
if (status.type === "success") {
|
|
529
|
-
if (payload === undefined) {
|
|
530
|
-
res.status(204).end();
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
res.json(payload);
|
|
534
|
-
} else {
|
|
535
|
-
// The wait that the error asks for: a Retry-After header in seconds, and the exact
|
|
536
|
-
// milliseconds in the body (R12)
|
|
537
|
-
const retryAfterMs = metadata["retry-after"];
|
|
538
|
-
const hasRetryAfter = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0;
|
|
539
|
-
if (hasRetryAfter) res.setHeader("Retry-After", String(Math.ceil(retryAfterMs / 1000)));
|
|
540
|
-
// The reason of a FORBIDDEN error travels on each face (roadmap R25)
|
|
541
|
-
const reason = typeof metadata["reason"] === "string" ? metadata["reason"] : undefined;
|
|
542
|
-
res.json({
|
|
543
|
-
error: status.message,
|
|
544
|
-
code: status.code,
|
|
545
|
-
retryable: status.retryable,
|
|
546
|
-
...(hasRetryAfter ? { retryAfterMs } : {}),
|
|
547
|
-
...(reason !== undefined ? { reason } : {}),
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
128
|
async start(): Promise<void> {
|
|
553
129
|
this.stopped = false;
|
|
130
|
+
this.handler.start();
|
|
554
131
|
// If an HTTP server was provided, use it (don't create or listen)
|
|
555
132
|
if (this.options.httpServer) {
|
|
556
133
|
this.httpServer = this.options.httpServer;
|
|
@@ -586,8 +163,8 @@ export class HttpServerTransport implements ServerTransport {
|
|
|
586
163
|
*/
|
|
587
164
|
async stop(): Promise<void> {
|
|
588
165
|
this.stopped = true;
|
|
589
|
-
|
|
590
|
-
this.
|
|
166
|
+
this.handler.stopIntake();
|
|
167
|
+
this.handler.close();
|
|
591
168
|
|
|
592
169
|
if (this.options.httpServer || !this.httpServer) {
|
|
593
170
|
return;
|
|
@@ -85,6 +85,21 @@ export interface HttpServerTransportOptions extends BrowserGuardOptions {
|
|
|
85
85
|
*/
|
|
86
86
|
clock?: Clock;
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* More headers that carry a secret, for example the token header of an application. They go
|
|
90
|
+
* into the credentials of the request, not into its metadata. `Authorization`,
|
|
91
|
+
* `Proxy-Authorization`, `Cookie` and `X-Api-Key` are always secret.
|
|
92
|
+
*/
|
|
93
|
+
secretHeaders?: readonly string[];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The query parameters that carry a secret. They go into the credentials of the request, not
|
|
97
|
+
* into its metadata. The default keeps each query parameter in the metadata, as before.
|
|
98
|
+
* `procedureHandler` has the default `["token"]`.
|
|
99
|
+
* @default []
|
|
100
|
+
*/
|
|
101
|
+
secretQuery?: readonly string[];
|
|
102
|
+
|
|
88
103
|
/**
|
|
89
104
|
* Custom CORS options. `origin` is "*", one origin or a list: the response names the
|
|
90
105
|
* request's origin when it is in the list.
|
|
@@ -578,18 +578,10 @@ export class WebSocketTransport implements Transport {
|
|
|
578
578
|
return;
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
-
// The frame type `event` is reserved: no side of protocol v1 sends it.
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
try {
|
|
586
|
-
this.options.onEvent(message.topic, message.data, message.subscriptionId);
|
|
587
|
-
} catch (error) {
|
|
588
|
-
console.error(`[${this.name}] Event handler error:`, error);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
581
|
+
// The frame type `event` is reserved: no side of protocol v1 sends it. The client ignores it,
|
|
582
|
+
// as a frame of an unknown type. Before 0.9.0, it gave the frame to onEvent, for a server
|
|
583
|
+
// outside this repository (EVENTS.md). onEvent gets the events of the subscriptions only.
|
|
584
|
+
if (message.type === "event") return;
|
|
593
585
|
|
|
594
586
|
// Handle response
|
|
595
587
|
const pending = this.pendingRequests.get(message.id);
|