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