@procedures/client 0.7.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 +107 -0
- package/README.md +115 -11
- 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 +36 -46
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +58 -433
- 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 +7 -3
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +9 -3
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +56 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +174 -17
- 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 +14 -6
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +12 -3
- 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/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +1 -0
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/middleware/retry.d.ts +4 -0
- package/dist/client/middleware/retry.d.ts.map +1 -1
- package/dist/client/middleware/retry.js +86 -78
- package/dist/client/middleware/retry.js.map +1 -1
- package/dist/events/bus.d.ts +32 -14
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +119 -65
- package/dist/events/bus.js.map +1 -1
- package/dist/events/index.d.ts +3 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.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 +62 -10
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/remote.d.ts +80 -18
- package/dist/events/remote.d.ts.map +1 -1
- package/dist/events/remote.js +448 -58
- package/dist/events/remote.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/events/types.d.ts +13 -2
- package/dist/events/types.d.ts.map +1 -1
- 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/caller.d.ts +33 -0
- package/dist/procedures/caller.d.ts.map +1 -0
- package/dist/procedures/caller.js +27 -0
- package/dist/procedures/caller.js.map +1 -0
- 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 +131 -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 +59 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +172 -6
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +64 -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 +12 -3
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +7 -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 +16 -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 +92 -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/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -0
- package/dist/protocol.js.map +1 -1
- 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 +75 -9
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +151 -17
- 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/dist/timers.d.ts +5 -0
- package/dist/timers.d.ts.map +1 -1
- package/dist/timers.js +3 -1
- package/dist/timers.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 +66 -479
- package/src/adapters/http/server/types.ts +15 -0
- package/src/adapters/websocket/client/transport.ts +9 -3
- package/src/adapters/websocket/server/transport.ts +189 -18
- 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/client/errors/registry.ts +1 -0
- package/src/client/middleware/retry.ts +89 -78
- package/src/events/bus.ts +141 -73
- package/src/events/index.ts +3 -0
- package/src/events/publisher.ts +71 -10
- package/src/events/remote.ts +575 -73
- package/src/events/rows.ts +48 -0
- package/src/events/types.ts +15 -2
- package/src/index.ts +1 -1
- package/src/node.ts +21 -0
- package/src/procedures/caller.ts +45 -0
- package/src/procedures/capability.ts +21 -0
- package/src/procedures/collection/changes.ts +245 -0
- package/src/procedures/collection/index.ts +56 -1
- package/src/procedures/collection/procedures.ts +185 -7
- package/src/procedures/collection/query.ts +1009 -0
- package/src/procedures/collection/schemas.ts +233 -7
- package/src/procedures/collection/storage.ts +84 -6
- package/src/procedures/elicit.ts +320 -0
- package/src/procedures/hints.ts +91 -0
- package/src/procedures/index.ts +82 -2
- package/src/procedures/invoke.ts +31 -0
- package/src/procedures/progress.ts +25 -0
- package/src/procedures/registry.ts +4 -0
- package/src/procedures/types.ts +102 -0
- package/src/procedures/untrusted.ts +50 -0
- package/src/protocol.ts +3 -1
- 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 +185 -20
- package/src/server/request-auth.ts +128 -6
- package/src/server/types.ts +82 -5
- package/src/timers.ts +8 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Server, type ServerOptions } from "./server.js";
|
|
9
|
-
import type { Method, Principal, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
9
|
+
import type { AmbientCredentials, Method, Principal, PrincipalOf, RequestIdentity, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
10
10
|
import { methodToPath, pathToMethod } from "./method.js";
|
|
11
11
|
import type {
|
|
12
12
|
AnyProcedure,
|
|
@@ -19,14 +19,16 @@ import { ProcedureRegistry, PROCEDURE_REGISTRY } from "../procedures/registry.js
|
|
|
19
19
|
import { createCollectionProcedures } from "../procedures/collection/procedures.js";
|
|
20
20
|
import { InvocationError, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
|
|
21
21
|
import type { Capability, ClearanceOrder, ResourcesOf } from "../procedures/types.js";
|
|
22
|
-
import { TIMEOUT_METADATA_KEY, effectsOf, narrowCapability } from "../procedures/capability.js";
|
|
22
|
+
import { TIMEOUT_METADATA_KEY, effectsOf, joinNarrowing, narrowCapability } from "../procedures/capability.js";
|
|
23
23
|
import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptions } from "../procedures/idempotency.js";
|
|
24
24
|
import { optionalMs, type Duration } from "../duration.js";
|
|
25
25
|
import type { CollectionStorage } from "../procedures/collection/storage.js";
|
|
26
26
|
import { TIMER_DEFAULTS } from "../timers.js";
|
|
27
|
+
import { realClock, type Clock } from "../clock.js";
|
|
27
28
|
import { errorCodeOf } from "../protocol.js";
|
|
28
29
|
import { EventPublisher, type EventPublisherOptions } from "../events/publisher.js";
|
|
29
30
|
import { getGlobalEventBus } from "../events/bus.js";
|
|
31
|
+
import type { CollectionProceduresOptions } from "../procedures/collection/procedures.js";
|
|
30
32
|
|
|
31
33
|
// =============================================================================
|
|
32
34
|
// Types
|
|
@@ -49,8 +51,18 @@ export interface UseStorageOptions {
|
|
|
49
51
|
* flush fails, the old storage stays and `useStorage` throws.
|
|
50
52
|
*/
|
|
51
53
|
drain?: boolean | undefined;
|
|
52
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* True: the server closes the old storage after the change. The close waits for the calls that
|
|
56
|
+
* took the old storage, for the time of `grace` at most.
|
|
57
|
+
*/
|
|
53
58
|
close?: boolean | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* The longest time that the close waits for the calls on the old storage, for example
|
|
61
|
+
* `seconds(5)`. When the time ends, the server closes the old storage. A call that still uses it
|
|
62
|
+
* then fails with the error of a closed storage. 0: the server closes it at once.
|
|
63
|
+
* @default 30 seconds (`TIMER_DEFAULTS.server.storageGraceMs`)
|
|
64
|
+
*/
|
|
65
|
+
grace?: Duration | undefined;
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
/**
|
|
@@ -66,6 +78,12 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
66
78
|
/** Collections to configure with storage backends */
|
|
67
79
|
collections?: Record<string, CollectionStorage<unknown>>;
|
|
68
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The clock of the wait of `useStorage` for the calls on an old storage (roadmap R35). The
|
|
83
|
+
* default is the clock of the platform. A test gives a fake clock.
|
|
84
|
+
*/
|
|
85
|
+
clock?: Clock | undefined;
|
|
86
|
+
|
|
69
87
|
/**
|
|
70
88
|
* The procedures that the callers of this server can reach. `registerFromRegistry()` registers
|
|
71
89
|
* only these procedures. A data-driven procedure (control flow, `eval`, a procedure that
|
|
@@ -122,6 +140,24 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
122
140
|
*/
|
|
123
141
|
authenticate?: ((request: ServerRequest<unknown>) => Principal | undefined | Promise<Principal | undefined>) | undefined;
|
|
124
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The request hook of the server: it reads the credentials of each network request (a cookie, a
|
|
145
|
+
* header, the query) and gives a bearer token, a principal and a narrowing. It runs before the
|
|
146
|
+
* token check: its bearer token replaces the bearer token of the credentials. Its principal
|
|
147
|
+
* replaces the principal of the token check. `authenticate` runs after it and can read
|
|
148
|
+
* `request.principal`. Undefined or an error refuses the request with `UNAUTHORIZED`. A request
|
|
149
|
+
* without credentials (from this process) does not go through the hook.
|
|
150
|
+
*/
|
|
151
|
+
principalOf?: PrincipalOf | undefined;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* How the server can authenticate a request with ambient credentials (cookies), for the CSRF
|
|
155
|
+
* checks of the network faces. The default is `"always"` with `principalOf`, `"cookies"` with
|
|
156
|
+
* `authenticate`, and none without them. `false`: the server reads no cookies, for example an
|
|
157
|
+
* `authenticate` function that reads only the bearer token.
|
|
158
|
+
*/
|
|
159
|
+
ambientCredentials?: AmbientCredentials | false | undefined;
|
|
160
|
+
|
|
125
161
|
/**
|
|
126
162
|
* The remote events of the server (`documentation/EVENTS.md`): a bus and its topic rule, or an
|
|
127
163
|
* `EventPublisher`. The server pins the procedure `events.subscribe`, outside its registry, so a
|
|
@@ -166,6 +202,15 @@ export interface StopOptions {
|
|
|
166
202
|
grace?: Duration | undefined;
|
|
167
203
|
}
|
|
168
204
|
|
|
205
|
+
/** The items of a stream. At the end of the stream, or when its reader stops, `release` runs. */
|
|
206
|
+
async function* releasedAtEnd<T>(items: AsyncIterable<T>, release: () => void): AsyncGenerator<T, void, undefined> {
|
|
207
|
+
try {
|
|
208
|
+
yield* items;
|
|
209
|
+
} finally {
|
|
210
|
+
release();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
169
214
|
/** A signal that aborts when one of the signals aborts. */
|
|
170
215
|
function joinSignals(first: AbortSignal | undefined, second: AbortSignal): AbortSignal {
|
|
171
216
|
if (!first) return second;
|
|
@@ -271,6 +316,10 @@ export function errorResponse(id: string, error: unknown): ServerResponse<unknow
|
|
|
271
316
|
export class ProcedureServer extends Server implements RepositoryProvider {
|
|
272
317
|
private readonly procedureRegistry: ProcedureRegistry;
|
|
273
318
|
private readonly storages = new Map<string, CollectionStorage<unknown>>();
|
|
319
|
+
/** For each storage: the calls in flight that took it, and the closes that wait for them */
|
|
320
|
+
private readonly storageUses = new Map<CollectionStorage<unknown>, { calls: number; waiters: Array<() => void> }>();
|
|
321
|
+
/** The clock of the wait of `useStorage` */
|
|
322
|
+
private readonly clock: Clock;
|
|
274
323
|
/** The procedures that `registerProcedure` pinned, by key */
|
|
275
324
|
private readonly pinnedProcedures = new Map<string, AnyProcedure>();
|
|
276
325
|
/**
|
|
@@ -296,18 +345,25 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
296
345
|
private draining = false;
|
|
297
346
|
/** The calls in flight, a stream until its end */
|
|
298
347
|
private inFlight = 0;
|
|
299
|
-
/** The token of the server, and its authenticate function (R18) */
|
|
348
|
+
/** The token of the server, its request hook and its authenticate function (R18) */
|
|
300
349
|
private readonly token: string | undefined;
|
|
350
|
+
private readonly principalOf: PrincipalOf | undefined;
|
|
301
351
|
private readonly authenticate: ProcedureServerOptions["authenticate"];
|
|
302
352
|
/** The functions that `stop()` waits on: each one runs when no call is in flight */
|
|
303
353
|
private idleWaiters: Array<() => void> = [];
|
|
304
354
|
/** The publisher of the remote events, from the option `events`, or undefined. */
|
|
305
355
|
readonly publisher: EventPublisher | undefined;
|
|
356
|
+
/**
|
|
357
|
+
* How the server can authenticate a request with ambient credentials (cookies). The network faces
|
|
358
|
+
* read it: they trust such a request only from a named origin (CSRF).
|
|
359
|
+
*/
|
|
360
|
+
readonly ambientCredentials: AmbientCredentials | undefined;
|
|
306
361
|
|
|
307
362
|
constructor(options: ProcedureServerOptions = {}) {
|
|
308
363
|
super(options);
|
|
309
364
|
|
|
310
365
|
this.procedureRegistry = options.registry ?? PROCEDURE_REGISTRY;
|
|
366
|
+
this.clock = options.clock ?? realClock;
|
|
311
367
|
this.expose = options.expose;
|
|
312
368
|
this.strictCalls = options.strictCalls ?? false;
|
|
313
369
|
this.interceptors = options.interceptors ?? [];
|
|
@@ -319,7 +375,10 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
319
375
|
};
|
|
320
376
|
this.idempotency = options.idempotency === false ? undefined : new IdempotencyCache(options.idempotency ?? {});
|
|
321
377
|
this.token = options.token;
|
|
378
|
+
this.principalOf = options.principalOf;
|
|
322
379
|
this.authenticate = options.authenticate;
|
|
380
|
+
const ambient = options.ambientCredentials ?? (this.principalOf ? "always" : this.authenticate ? "cookies" : undefined);
|
|
381
|
+
this.ambientCredentials = ambient === false ? undefined : ambient;
|
|
323
382
|
// The remote events: the server pins events.subscribe, as it pins host.health (EVENTS.md)
|
|
324
383
|
this.publisher = options.events instanceof EventPublisher ? options.events : options.events ? new EventPublisher(options.events) : undefined;
|
|
325
384
|
if (this.publisher) this.registerProcedure(this.publisher.procedure);
|
|
@@ -379,7 +438,11 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
379
438
|
* failed flush keeps the old storage and throws. A call that arrives during the flush goes
|
|
380
439
|
* to the old storage.
|
|
381
440
|
* - `close`: the server closes the old storage after the change. The close of a
|
|
382
|
-
* `HybridStorage` sends the writes that arrived during the flush.
|
|
441
|
+
* `HybridStorage` sends the writes that arrived during the flush. An old storage that another
|
|
442
|
+
* collection still uses stays open: the server closes it when the last collection leaves it.
|
|
443
|
+
* - The new calls use the new storage at once. Before the close, the server waits for the calls
|
|
444
|
+
* that took the old storage, for the time of `grace` at most. Before, such a call could fail
|
|
445
|
+
* because the storage was closed.
|
|
383
446
|
*
|
|
384
447
|
* @param name - Collection name
|
|
385
448
|
* @param storage - Storage backend
|
|
@@ -396,20 +459,76 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
396
459
|
return this.replaceStorage(name, storage as CollectionStorage<unknown>, options);
|
|
397
460
|
}
|
|
398
461
|
|
|
399
|
-
/**
|
|
462
|
+
/**
|
|
463
|
+
* The change of `useStorage` with options: flush the old storage, change, wait for the calls on
|
|
464
|
+
* the old storage, and close it.
|
|
465
|
+
*/
|
|
400
466
|
private async replaceStorage(name: string, storage: CollectionStorage<unknown>, options: UseStorageOptions): Promise<this> {
|
|
467
|
+
const graceMs = optionalMs(options.grace, "grace") ?? TIMER_DEFAULTS.server.storageGraceMs;
|
|
401
468
|
const old = this.storages.get(name);
|
|
402
469
|
const other = old !== undefined && old !== storage ? old : undefined;
|
|
403
470
|
if (other && options.drain) {
|
|
404
471
|
await other.flush?.();
|
|
405
472
|
}
|
|
406
473
|
this.storages.set(name, storage);
|
|
407
|
-
|
|
474
|
+
// Another collection that uses the same storage keeps it open. Before, the close broke the
|
|
475
|
+
// calls of that collection (tracker O31).
|
|
476
|
+
if (other && options.close && ![...this.storages.values()].includes(other)) {
|
|
477
|
+
await this.callsOnStorageEnded(other, graceMs);
|
|
408
478
|
await other.close();
|
|
409
479
|
}
|
|
410
480
|
return this;
|
|
411
481
|
}
|
|
412
482
|
|
|
483
|
+
/** A promise that resolves when no call uses the storage, or when `graceMs` ends. */
|
|
484
|
+
private callsOnStorageEnded(storage: CollectionStorage<unknown>, graceMs: number): Promise<void> {
|
|
485
|
+
const use = this.storageUses.get(storage);
|
|
486
|
+
if (!use || graceMs <= 0) return Promise.resolve();
|
|
487
|
+
return new Promise<void>((resolve) => {
|
|
488
|
+
const done = (): void => {
|
|
489
|
+
cancel();
|
|
490
|
+
const index = use.waiters.indexOf(done);
|
|
491
|
+
if (index >= 0) use.waiters.splice(index, 1);
|
|
492
|
+
resolve();
|
|
493
|
+
};
|
|
494
|
+
const cancel = this.clock.after(graceMs, done);
|
|
495
|
+
use.waiters.push(done);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* The repository of one call. The call is a user of each storage that it takes, until
|
|
501
|
+
* `release()`. Then a close of `useStorage` can go on.
|
|
502
|
+
*/
|
|
503
|
+
private callRepository(): { repository: RepositoryProvider; release: () => void } {
|
|
504
|
+
const taken = new Set<CollectionStorage<unknown>>();
|
|
505
|
+
let released = false;
|
|
506
|
+
const repository: RepositoryProvider = {
|
|
507
|
+
getStorage: <T>(collection: string): CollectionStorage<T> => {
|
|
508
|
+
const storage = this.getStorage<unknown>(collection);
|
|
509
|
+
if (!released && !taken.has(storage)) {
|
|
510
|
+
taken.add(storage);
|
|
511
|
+
const use = this.storageUses.get(storage) ?? { calls: 0, waiters: [] };
|
|
512
|
+
use.calls++;
|
|
513
|
+
this.storageUses.set(storage, use);
|
|
514
|
+
}
|
|
515
|
+
return storage as CollectionStorage<T>;
|
|
516
|
+
},
|
|
517
|
+
hasCollection: (collection: string): boolean => this.hasCollection(collection),
|
|
518
|
+
};
|
|
519
|
+
const release = (): void => {
|
|
520
|
+
if (released) return;
|
|
521
|
+
released = true;
|
|
522
|
+
for (const storage of taken) {
|
|
523
|
+
const use = this.storageUses.get(storage);
|
|
524
|
+
if (!use || --use.calls > 0) continue;
|
|
525
|
+
this.storageUses.delete(storage);
|
|
526
|
+
for (const waiter of use.waiters.slice()) waiter();
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
return { repository, release };
|
|
530
|
+
}
|
|
531
|
+
|
|
413
532
|
/**
|
|
414
533
|
* Configure multiple storages at once.
|
|
415
534
|
*
|
|
@@ -551,9 +670,11 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
551
670
|
}
|
|
552
671
|
|
|
553
672
|
/**
|
|
554
|
-
* The token check and the principal of a request (roadmap R18).
|
|
555
|
-
* credentials
|
|
556
|
-
*
|
|
673
|
+
* The token check and the principal of a request (roadmap R18). The request hook reads the
|
|
674
|
+
* credentials of a network request first: a bearer token, a principal and a narrowing. A
|
|
675
|
+
* network request (with credentials) must present the token. The authenticate function gives
|
|
676
|
+
* the principal. The function gives an UNAUTHORIZED response, or undefined when the request
|
|
677
|
+
* passes.
|
|
557
678
|
*/
|
|
558
679
|
private async admit(request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
|
|
559
680
|
const unauthorized = (message: string): ServerResponse<unknown> => ({
|
|
@@ -561,10 +682,26 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
561
682
|
status: { type: "error", code: "UNAUTHORIZED", message, retryable: false },
|
|
562
683
|
metadata: {},
|
|
563
684
|
});
|
|
564
|
-
|
|
565
|
-
|
|
685
|
+
const credentials = request.credentials;
|
|
686
|
+
let identity: RequestIdentity | undefined;
|
|
687
|
+
if (this.principalOf && credentials !== undefined) {
|
|
688
|
+
try {
|
|
689
|
+
identity = await this.principalOf(credentials, request);
|
|
690
|
+
} catch {
|
|
691
|
+
identity = undefined;
|
|
692
|
+
}
|
|
693
|
+
if (!identity) return unauthorized("Unauthorized: the server did not accept the credentials");
|
|
694
|
+
}
|
|
695
|
+
if (this.token !== undefined && credentials !== undefined) {
|
|
696
|
+
const bearer = identity?.bearer ?? credentials.bearer;
|
|
697
|
+
if (!tokenMatches(this.token, bearer)) return unauthorized("Unauthorized: this server requires its token");
|
|
566
698
|
request.principal = { id: "token", via: "token" };
|
|
567
699
|
}
|
|
700
|
+
if (identity?.principal) request.principal = identity.principal;
|
|
701
|
+
if (identity?.narrowing) {
|
|
702
|
+
const order = this.capabilityOptions.clearanceOrder;
|
|
703
|
+
request.narrowing = request.narrowing ? joinNarrowing(request.narrowing, identity.narrowing, order) : identity.narrowing;
|
|
704
|
+
}
|
|
568
705
|
if (this.authenticate) {
|
|
569
706
|
let principal: Principal | undefined;
|
|
570
707
|
try {
|
|
@@ -643,42 +780,69 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
643
780
|
return { ...response, id: request.id };
|
|
644
781
|
}
|
|
645
782
|
|
|
783
|
+
/**
|
|
784
|
+
* The capability of a request. It comes from the server, never from the request. The remaining
|
|
785
|
+
* time of the caller and the narrowing of the request can only make it narrower
|
|
786
|
+
* (CAPABILITIES-2026-10, decision 9). Two different clearances without an order throw.
|
|
787
|
+
*/
|
|
788
|
+
private capabilityOf(request: ServerRequest<unknown>): Capability | undefined {
|
|
789
|
+
const { capability, clearanceOrder } = this.capabilityOptions;
|
|
790
|
+
const base = withRequestTimeout(typeof capability === "function" ? capability(request) : capability, request, clearanceOrder);
|
|
791
|
+
return request.narrowing ? narrowCapability(base, request.narrowing, clearanceOrder) : base;
|
|
792
|
+
}
|
|
793
|
+
|
|
646
794
|
/** Run a procedure for a request with a signal. */
|
|
647
795
|
private async run(procedure: AnyProcedure, request: ServerRequest<unknown>, signal: AbortSignal | undefined): Promise<ServerResponse<unknown>> {
|
|
796
|
+
// The call uses the storages that it takes until its end: the end of a stream, for a stream
|
|
797
|
+
const { repository, release } = this.callRepository();
|
|
798
|
+
let streams = false;
|
|
648
799
|
try {
|
|
649
800
|
// One invocation path for every host (ARCHITECTURE-PROPOSALS P1): input validation, the
|
|
650
801
|
// context, the expose rule for nested calls (BUGS-2026-07 H18) and output validation
|
|
651
|
-
const {
|
|
802
|
+
const { clearanceOrder, resources, admitItem } = this.capabilityOptions;
|
|
803
|
+
let capability: Capability | undefined;
|
|
804
|
+
try {
|
|
805
|
+
capability = this.capabilityOf(request);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
const message = `The server cannot narrow the capability of the request: ${error instanceof Error ? error.message : String(error)}`;
|
|
808
|
+
return errorResponse(request.id, Object.assign(new Error(message), { code: "INTERNAL_ERROR" }));
|
|
809
|
+
}
|
|
652
810
|
const output = await invokeProcedure(procedure, request.payload, {
|
|
653
811
|
registry: this.procedureRegistry,
|
|
654
812
|
metadata: request.metadata,
|
|
655
813
|
signal,
|
|
656
|
-
|
|
814
|
+
// What the face of the call offers: it asks the person, and it shows the progress (CLI-12)
|
|
815
|
+
...(request.elicit ? { elicit: request.elicit } : {}),
|
|
816
|
+
...(request.progress ? { progress: request.progress } : {}),
|
|
817
|
+
repository,
|
|
657
818
|
expose: this.expose,
|
|
658
819
|
strictCalls: this.strictCalls,
|
|
659
820
|
interceptors: this.interceptors,
|
|
660
|
-
|
|
661
|
-
// the caller can only make the deadline earlier (CAPABILITIES-2026-10, decision 9).
|
|
662
|
-
capability: withRequestTimeout(typeof capability === "function" ? capability(request) : capability, request, clearanceOrder),
|
|
821
|
+
capability,
|
|
663
822
|
clearanceOrder,
|
|
664
823
|
resources,
|
|
665
824
|
admitItem,
|
|
825
|
+
// The principal of the authentication: ctx.principal, and the nested calls (R18)
|
|
826
|
+
...(request.principal !== undefined ? { principal: request.principal } : {}),
|
|
666
827
|
// The bus of the remote events is the bus of the procedures (EVENTS.md)
|
|
667
828
|
...(this.publisher ? { bus: this.publisher.bus } : {}),
|
|
668
829
|
});
|
|
669
830
|
if (output.kind === "stream") {
|
|
670
831
|
// The faces that collect a stream read this mark (roadmap R15)
|
|
671
832
|
const unbounded = (procedure.metadata as { stream?: { unbounded?: boolean } } | undefined)?.stream?.unbounded === true;
|
|
833
|
+
streams = true;
|
|
672
834
|
return {
|
|
673
835
|
id: request.id,
|
|
674
836
|
status: { type: "success", code: 200 },
|
|
675
|
-
stream: output.items,
|
|
837
|
+
stream: releasedAtEnd(output.items, release),
|
|
676
838
|
metadata: unbounded ? { [STREAM_METADATA_KEY]: "unbounded" } : {},
|
|
677
839
|
};
|
|
678
840
|
}
|
|
679
841
|
return { id: request.id, status: { type: "success", code: 200 }, payload: output.value, metadata: {} };
|
|
680
842
|
} catch (error) {
|
|
681
843
|
return errorResponse(request.id, error);
|
|
844
|
+
} finally {
|
|
845
|
+
if (!streams) release();
|
|
682
846
|
}
|
|
683
847
|
}
|
|
684
848
|
|
|
@@ -687,11 +851,12 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
687
851
|
* Creates standard CRUD procedures using the configured storage.
|
|
688
852
|
*
|
|
689
853
|
* @param collectionName - Collection name
|
|
854
|
+
* @param options - The query policy of `find`. `$regex` is off unless the policy permits it.
|
|
690
855
|
*/
|
|
691
|
-
registerCollectionProcedures(collectionName: string): void {
|
|
856
|
+
registerCollectionProcedures(collectionName: string, options?: CollectionProceduresOptions): void {
|
|
692
857
|
// Uses a top-level import; require() is not available in this ESM package and
|
|
693
858
|
// previously threw "Cannot find module" on every call. See BUGS-2026-07 H13.
|
|
694
|
-
const procedures = createCollectionProcedures(collectionName);
|
|
859
|
+
const procedures = createCollectionProcedures(collectionName, options);
|
|
695
860
|
|
|
696
861
|
// Register with the procedure registry first
|
|
697
862
|
this.procedureRegistry.registerAll(procedures);
|
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The credentials of a network request (roadmap R18). A server transport reads them into
|
|
3
|
-
* `ServerRequest.credentials
|
|
4
|
-
*
|
|
3
|
+
* `ServerRequest.credentials`. It leaves the secret headers out of the metadata, so a handler and a
|
|
4
|
+
* nested call do not see them. `ProcedureServer` examines them.
|
|
5
|
+
*
|
|
6
|
+
* This module imports no Node code: a server in a browser or a worker can use it too.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import type { Credentials } from "./types.js";
|
|
8
10
|
|
|
9
11
|
/** The headers that carry a secret. They do not go into the metadata of a request. */
|
|
10
|
-
const SECRET_HEADERS = new Set(["authorization", "proxy-authorization", "cookie", "x-api-key"]);
|
|
12
|
+
const SECRET_HEADERS: ReadonlySet<string> = new Set(["authorization", "proxy-authorization", "cookie", "x-api-key"]);
|
|
11
13
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* True when a header carries a secret: a header of the default list (`Authorization`,
|
|
16
|
+
* `Proxy-Authorization`, `Cookie`, `X-Api-Key`), or a header of `more`. The names are lower case.
|
|
17
|
+
*/
|
|
18
|
+
export function isSecretHeader(name: string, more?: ReadonlySet<string>): boolean {
|
|
19
|
+
const key = name.toLowerCase();
|
|
20
|
+
return SECRET_HEADERS.has(key) || (more?.has(key) ?? false);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** This function gives a set of lower-case header names. */
|
|
24
|
+
export function headerNames(names: Iterable<string> | undefined): ReadonlySet<string> {
|
|
25
|
+
const set = new Set<string>();
|
|
26
|
+
for (const name of names ?? []) set.add(name.toLowerCase());
|
|
27
|
+
return set;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The headers without the secret ones, and without the headers that are not text. `secret` names
|
|
32
|
+
* more headers that carry a secret, for example the token header of an application.
|
|
33
|
+
*/
|
|
34
|
+
export function withoutSecretHeaders(headers: Record<string, unknown>, secret?: Iterable<string>): Record<string, string> {
|
|
35
|
+
const more = secret === undefined ? undefined : headerNames(secret);
|
|
14
36
|
const out: Record<string, string> = {};
|
|
15
37
|
for (const [key, value] of Object.entries(headers)) {
|
|
16
|
-
if (!
|
|
38
|
+
if (!isSecretHeader(key, more) && typeof value === "string") out[key] = value;
|
|
17
39
|
}
|
|
18
40
|
return out;
|
|
19
41
|
}
|
|
@@ -33,3 +55,103 @@ export function credentialsOf(request: { headers: Record<string, unknown>; url?:
|
|
|
33
55
|
const query = new URL(request.url ?? "/", "http://localhost").searchParams.get("token");
|
|
34
56
|
return query === null ? {} : { bearer: query };
|
|
35
57
|
}
|
|
58
|
+
|
|
59
|
+
/** The text of a header value: a list of values becomes one text, with ", " between the values. */
|
|
60
|
+
function headerText(value: unknown): string | undefined {
|
|
61
|
+
if (typeof value === "string") return value;
|
|
62
|
+
if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string").join(", ");
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** This function decodes a cookie value. A value that is not valid URI encoding stays as it is. */
|
|
67
|
+
function decodeCookieValue(value: string): string {
|
|
68
|
+
const unquoted = value.length >= 2 && value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
|
|
69
|
+
try {
|
|
70
|
+
return decodeURIComponent(unquoted);
|
|
71
|
+
} catch {
|
|
72
|
+
return unquoted;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The cookies of a `Cookie` header, by name. The function decodes each URI-encoded value when it
|
|
78
|
+
* can. When two cookies have the same name, the first one stays: a browser sends the
|
|
79
|
+
* cookie of the longest path first. A part without `=` is not a cookie.
|
|
80
|
+
*/
|
|
81
|
+
export function cookiesOf(header: unknown): Record<string, string> {
|
|
82
|
+
const text = Array.isArray(header) ? header.filter((item): item is string => typeof item === "string").join("; ") : header;
|
|
83
|
+
if (typeof text !== "string" || text === "") return {};
|
|
84
|
+
const cookies = new Map<string, string>();
|
|
85
|
+
for (const part of text.split(";")) {
|
|
86
|
+
const eq = part.indexOf("=");
|
|
87
|
+
if (eq <= 0) continue;
|
|
88
|
+
const name = part.slice(0, eq).trim();
|
|
89
|
+
if (name === "" || cookies.has(name)) continue;
|
|
90
|
+
cookies.set(name, decodeCookieValue(part.slice(eq + 1).trim()));
|
|
91
|
+
}
|
|
92
|
+
// fromEntries defines own properties: a cookie named "__proto__" changes no prototype
|
|
93
|
+
return Object.fromEntries(cookies);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The parts of a request URL: its path and its query. The function does not resolve the URL
|
|
98
|
+
* against a host, so a path that starts with "//" stays a path.
|
|
99
|
+
*/
|
|
100
|
+
export function urlParts(url: string | undefined): { path: string; query: URLSearchParams } {
|
|
101
|
+
let raw = url ?? "/";
|
|
102
|
+
// The absolute form of a request line (a request through a proxy): the path and the query only
|
|
103
|
+
if (!raw.startsWith("/") && /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
|
|
104
|
+
try {
|
|
105
|
+
const absolute = new URL(raw);
|
|
106
|
+
raw = `${absolute.pathname}${absolute.search}`;
|
|
107
|
+
} catch {
|
|
108
|
+
// Not a URL: the text stays a path
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const mark = raw.indexOf("?");
|
|
112
|
+
return mark === -1 ? { path: raw, query: new URLSearchParams() } : { path: raw.slice(0, mark), query: new URLSearchParams(raw.slice(mark + 1)) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The first value of each query parameter, by name. */
|
|
116
|
+
export function queryRecord(query: URLSearchParams): Record<string, string> {
|
|
117
|
+
const values = new Map<string, string>();
|
|
118
|
+
for (const [key, value] of query) if (!values.has(key)) values.set(key, value);
|
|
119
|
+
return Object.fromEntries(values);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The options of `requestCredentials`. */
|
|
123
|
+
export interface RequestCredentialsOptions {
|
|
124
|
+
/**
|
|
125
|
+
* The query parameter whose value is the bearer token of a request without an
|
|
126
|
+
* `Authorization: Bearer` header. A browser WebSocket cannot set headers, so the WebSocket
|
|
127
|
+
* server reads `token`. `false` reads no query parameter.
|
|
128
|
+
* @default false
|
|
129
|
+
*/
|
|
130
|
+
readonly tokenQuery?: string | false | undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The credentials of a network request (roadmap R18): the bearer token, the cookies, each header
|
|
135
|
+
* (the secret ones too) and the first value of each query parameter. The request hook of a server
|
|
136
|
+
* (`principalOf`) reads them. They are not in the metadata, so a handler does not see them.
|
|
137
|
+
*/
|
|
138
|
+
export function requestCredentials(
|
|
139
|
+
request: { headers: Record<string, unknown>; url?: string | undefined },
|
|
140
|
+
options: RequestCredentialsOptions = {}
|
|
141
|
+
): Credentials {
|
|
142
|
+
const headers = new Map<string, string>();
|
|
143
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
144
|
+
const text = headerText(value);
|
|
145
|
+
if (text !== undefined) headers.set(key.toLowerCase(), text);
|
|
146
|
+
}
|
|
147
|
+
const { query } = urlParts(request.url);
|
|
148
|
+
const fromHeader = bearerOf(headers.get("authorization"));
|
|
149
|
+
const fromQuery = options.tokenQuery ? query.get(options.tokenQuery) ?? undefined : undefined;
|
|
150
|
+
const bearer = fromHeader ?? fromQuery;
|
|
151
|
+
return {
|
|
152
|
+
...(bearer !== undefined ? { bearer } : {}),
|
|
153
|
+
cookies: cookiesOf(headers.get("cookie")),
|
|
154
|
+
headers: Object.fromEntries(headers),
|
|
155
|
+
query: queryRecord(query),
|
|
156
|
+
};
|
|
157
|
+
}
|
package/src/server/types.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* Enables handling HTTP, WebSocket, gRPC, etc. with unified RPC format.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { Elicit } from "../procedures/elicit.js";
|
|
9
|
+
import type { ReportProgress } from "../procedures/progress.js";
|
|
8
10
|
import type { Metadata, Status, Method } from "../client/types.js";
|
|
11
|
+
import type { CapabilityRequest } from "../procedures/types.js";
|
|
9
12
|
|
|
10
13
|
// Re-export Method from client types
|
|
11
14
|
export type { Method };
|
|
@@ -27,27 +30,86 @@ export interface ServerRequest<TReq = unknown> {
|
|
|
27
30
|
/** Request metadata (headers, query params, etc.) */
|
|
28
31
|
metadata: Metadata;
|
|
29
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The function that asks the person at the caller, when the face can ask (MCP elicitation,
|
|
35
|
+
* CLI-12). The procedure gets it as `ctx.elicit`. It is not in the metadata, so a caller cannot
|
|
36
|
+
* send it.
|
|
37
|
+
*/
|
|
38
|
+
elicit?: Elicit | undefined;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The function that tells the caller the progress of the call, when the caller asked for it
|
|
42
|
+
* (an MCP progress token). The procedure gets it as `ctx.progress`.
|
|
43
|
+
*/
|
|
44
|
+
progress?: ReportProgress | undefined;
|
|
45
|
+
|
|
30
46
|
/** Abort signal for cancellation */
|
|
31
47
|
signal?: AbortSignal;
|
|
32
48
|
|
|
33
49
|
/**
|
|
34
|
-
* The credentials that a network transport read from the request
|
|
35
|
-
* `Authorization` header, or of the `token` query of a WebSocket connection
|
|
36
|
-
*
|
|
37
|
-
*
|
|
50
|
+
* The credentials that a network transport read from the request (roadmap R18). They have the
|
|
51
|
+
* bearer token of an `Authorization` header, or of the `token` query of a WebSocket connection.
|
|
52
|
+
* They also have the cookies, the headers and the query of the request. They are not in the
|
|
53
|
+
* metadata, so a handler and a nested call do not see them. A request without credentials comes
|
|
54
|
+
* from this process (a direct `handle()`, MCP over stdio).
|
|
38
55
|
*/
|
|
39
56
|
credentials?: Credentials | undefined;
|
|
40
57
|
|
|
41
58
|
/** The principal of the request: who sends it. `ProcedureServer` sets it when it authenticates the request. */
|
|
42
59
|
principal?: Principal | undefined;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A narrower capability that the request asks for. The request hook of the server
|
|
63
|
+
* (`principalOf`) can set it. The server narrows the capability of the call with it, so the
|
|
64
|
+
* capability does not become wider.
|
|
65
|
+
*/
|
|
66
|
+
narrowing?: CapabilityRequest | undefined;
|
|
43
67
|
}
|
|
44
68
|
|
|
45
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* The credentials of a request. A network transport sets each field that it can read. The request
|
|
71
|
+
* hook of the server (`principalOf`) reads them.
|
|
72
|
+
*/
|
|
46
73
|
export interface Credentials {
|
|
47
74
|
/** The bearer token that the request presents. */
|
|
48
75
|
readonly bearer?: string | undefined;
|
|
76
|
+
/** The cookies of the request, by name (the `Cookie` header of an HTTP request or of an upgrade). */
|
|
77
|
+
readonly cookies?: Readonly<Record<string, string>> | undefined;
|
|
78
|
+
/** Each header of the request, the secret headers too, by lower-case name. */
|
|
79
|
+
readonly headers?: Readonly<Record<string, string>> | undefined;
|
|
80
|
+
/** The first value of each query parameter of the request URL, by name. */
|
|
81
|
+
readonly query?: Readonly<Record<string, string>> | undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* What the request hook of a server (`principalOf`) gives for a network request. Each field is
|
|
86
|
+
* optional: an empty object accepts the request without a principal.
|
|
87
|
+
*/
|
|
88
|
+
export interface RequestIdentity {
|
|
89
|
+
/**
|
|
90
|
+
* The token to compare with the token of the server, for example the value of a cookie or of an
|
|
91
|
+
* application header. Without it, the server compares the bearer token of the credentials.
|
|
92
|
+
*/
|
|
93
|
+
readonly bearer?: string | undefined;
|
|
94
|
+
/** The principal of the request. It replaces the principal of the token check. */
|
|
95
|
+
readonly principal?: Principal | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* A narrower capability that the request asks for, for example a guest mode of a cookie. The
|
|
98
|
+
* server narrows the capability of the call with it. A request for more has no effect.
|
|
99
|
+
*/
|
|
100
|
+
readonly narrowing?: CapabilityRequest | undefined;
|
|
49
101
|
}
|
|
50
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The request hook of a server: it reads the credentials of a network request (a cookie, a header,
|
|
105
|
+
* the query) and gives the identity of the request. Undefined, or an error, refuses the request
|
|
106
|
+
* with `UNAUTHORIZED`. The hook runs before the token check and before `authenticate`.
|
|
107
|
+
*/
|
|
108
|
+
export type PrincipalOf = (
|
|
109
|
+
credentials: Credentials,
|
|
110
|
+
request: ServerRequest<unknown>
|
|
111
|
+
) => RequestIdentity | undefined | Promise<RequestIdentity | undefined>;
|
|
112
|
+
|
|
51
113
|
/** Who sends a request (roadmap R18). */
|
|
52
114
|
export interface Principal {
|
|
53
115
|
/** The id of the principal. With the token of a host: "token". */
|
|
@@ -66,8 +128,23 @@ export interface Principal {
|
|
|
66
128
|
*/
|
|
67
129
|
export interface RequestHandler {
|
|
68
130
|
handle(request: ServerRequest): Promise<ServerResponse>;
|
|
131
|
+
/**
|
|
132
|
+
* How the handler can authenticate a network request with ambient credentials: the cookies that
|
|
133
|
+
* a browser sends by itself, also for a request of another site (CSRF). A network face then
|
|
134
|
+
* trusts such a request only from a named origin. Absent: the handler reads no cookies.
|
|
135
|
+
*/
|
|
136
|
+
readonly ambientCredentials?: AmbientCredentials | undefined;
|
|
69
137
|
}
|
|
70
138
|
|
|
139
|
+
/**
|
|
140
|
+
* How a server can authenticate a request with ambient credentials (cookies):
|
|
141
|
+
* - `"always"`: each network request. The request hook `principalOf` reads the cookies, so a face
|
|
142
|
+
* trusts a request only from a named origin.
|
|
143
|
+
* - `"cookies"`: a request that carries a cookie. An `authenticate` function can read it, so a face
|
|
144
|
+
* trusts a request with a cookie only from a named origin.
|
|
145
|
+
*/
|
|
146
|
+
export type AmbientCredentials = "always" | "cookies";
|
|
147
|
+
|
|
71
148
|
/**
|
|
72
149
|
* Server response in unified RPC format.
|
|
73
150
|
* Converted by transport adapters to protocol-specific responses.
|