@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
package/src/procedures/types.ts
CHANGED
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
* Procedures are the foundation for typed RPC calls with schema validation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { Elicit } from "./elicit.js";
|
|
9
|
+
import type { ReportProgress } from "./progress.js";
|
|
10
|
+
import type { ToolHintOverride } from "./hints.js";
|
|
8
11
|
import type { ZodErrorLike } from "../client/validation/types.js";
|
|
9
12
|
import type { ProcedureSchema } from "./schema.js";
|
|
10
13
|
import type { CollectionStorage } from "./collection/storage.js";
|
|
11
14
|
import type { EventBus } from "../events/types.js";
|
|
12
15
|
import type { HydrationEvent } from "./ref.js";
|
|
16
|
+
import type { Principal } from "../server/types.js";
|
|
13
17
|
|
|
14
18
|
// =============================================================================
|
|
15
19
|
// Procedure Path
|
|
@@ -83,6 +87,23 @@ export interface ProcedureMetadata {
|
|
|
83
87
|
*/
|
|
84
88
|
stream?: { readonly unbounded?: boolean } | undefined;
|
|
85
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The output fields that hold text from outside the host: a web page, a mail (CLI-10). The value
|
|
92
|
+
* of each field is a text, an `UntrustedText` with its source, or a list of them. A face that
|
|
93
|
+
* gives text to a model (MCP) shows these texts in envelopes: data, never instructions.
|
|
94
|
+
*/
|
|
95
|
+
untrusted?: readonly string[] | undefined;
|
|
96
|
+
|
|
97
|
+
/** The short name of the procedure for people, for example the title of an MCP tool. */
|
|
98
|
+
title?: string | undefined;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The override of the tool hints of the effects (CLI-10). It can only make the tool look more
|
|
102
|
+
* dangerous: `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`,
|
|
103
|
+
* `openWorldHint: true`. The registry refuses each other value.
|
|
104
|
+
*/
|
|
105
|
+
toolHints?: ToolHintOverride | undefined;
|
|
106
|
+
|
|
86
107
|
/**
|
|
87
108
|
* The rules of the procedure under a root of the files (roadmap R33): the inputs that name a
|
|
88
109
|
* path, or the reason that a root cannot confine it. A host with a root (`--fs-root`,
|
|
@@ -238,6 +259,22 @@ export interface RepositoryProvider {
|
|
|
238
259
|
*/
|
|
239
260
|
getStorage<T>(collection: string): CollectionStorage<T>;
|
|
240
261
|
|
|
262
|
+
/**
|
|
263
|
+
* The storage of one partition of a collection (roadmap CLI-17), for example the items of one
|
|
264
|
+
* tenant. A repository without this method has no partitions: the collection procedures then
|
|
265
|
+
* refuse a call with a partition, and never give it the storage of all partitions.
|
|
266
|
+
*/
|
|
267
|
+
getPartitionStorage?<T>(collection: string, partition: string): CollectionStorage<T>;
|
|
268
|
+
|
|
269
|
+
/** True when the collection has partitions (roadmap CLI-17). Then a call resolves its partition with `resolvePartition`. */
|
|
270
|
+
hasPartitions?(collection: string): boolean;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The partition of a call to a partitioned collection (roadmap CLI-17): from the context (the
|
|
274
|
+
* tenant of `ctx.principal`) and the partition that the call asks for. `null` refuses the call.
|
|
275
|
+
*/
|
|
276
|
+
resolvePartition?(collection: string, ctx: ProcedureContext, requested: string | undefined): string | undefined | null | Promise<string | undefined | null>;
|
|
277
|
+
|
|
241
278
|
/**
|
|
242
279
|
* Check if a collection exists.
|
|
243
280
|
*
|
|
@@ -329,6 +366,20 @@ export interface ProcedureContext {
|
|
|
329
366
|
*/
|
|
330
367
|
onRef?: ((event: HydrationEvent) => void) | undefined;
|
|
331
368
|
|
|
369
|
+
/**
|
|
370
|
+
* The function that asks the person at the caller for input or a confirmation (CLI-12): MCP
|
|
371
|
+
* elicitation, or another face that can ask. Absent when no one can answer. The helpers
|
|
372
|
+
* `elicitInput` and `elicitConfirmation` then give the action `unavailable`. The nested calls
|
|
373
|
+
* get the same function.
|
|
374
|
+
*/
|
|
375
|
+
elicit?: Elicit | undefined;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The function that tells the caller the progress of this call (an MCP progress notification).
|
|
379
|
+
* Absent when the caller asked for no progress. Only the call of the host gets it.
|
|
380
|
+
*/
|
|
381
|
+
progress?: ReportProgress | undefined;
|
|
382
|
+
|
|
332
383
|
/**
|
|
333
384
|
* The capability of this invocation (CAPABILITIES-2026-10): its clearance, permitted effects
|
|
334
385
|
* and deadline. The nested calls of `ctx.client` get it, or a narrower one. Frozen.
|
|
@@ -347,6 +398,20 @@ export interface ProcedureContext {
|
|
|
347
398
|
* it. The root of the files reads it (roadmap R33).
|
|
348
399
|
*/
|
|
349
400
|
caller?: AnyProcedure | undefined;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The principal of the call: who sends it. The server sets it from its authentication (the
|
|
404
|
+
* token, the request hook `principalOf`, `authenticate`). The nested calls get the same
|
|
405
|
+
* principal. An input or the metadata cannot change it. A call from this process has none.
|
|
406
|
+
*/
|
|
407
|
+
principal?: Principal | undefined;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The output filter of the host for this call: its `admitItem`, with this invocation (roadmap
|
|
411
|
+
* CLI-15). A handler that sends the data of other writers later, for example a subscription,
|
|
412
|
+
* examines each row with it. Absent for a nested call, or when the host has no filter.
|
|
413
|
+
*/
|
|
414
|
+
admit?: ((item: unknown) => Promise<boolean>) | undefined;
|
|
350
415
|
}
|
|
351
416
|
|
|
352
417
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text from outside a host: a web page, a mail, a file of a user (CLI-10).
|
|
3
|
+
*
|
|
4
|
+
* A procedure that gives such text declares the fields of its output that hold it
|
|
5
|
+
* (`metadata.untrusted`). A face that gives text to a model (MCP) then shows that text apart from
|
|
6
|
+
* the data. Envelopes tell the model that this text is data, not instructions. Another face gives
|
|
7
|
+
* the output as it is.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AnyProcedure } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/** The values of the source of a text: the attributes of its envelope. */
|
|
13
|
+
export type UntrustedSource = Readonly<Record<string, string | number | boolean | null | undefined>>;
|
|
14
|
+
|
|
15
|
+
/** A text from outside the host, with its source: for example the URL and the date of a page. */
|
|
16
|
+
export interface UntrustedText {
|
|
17
|
+
readonly text: string;
|
|
18
|
+
readonly source?: UntrustedSource | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The value of an untrusted field of an output: a text, a text with its source, or a list of
|
|
23
|
+
* them.
|
|
24
|
+
*/
|
|
25
|
+
export type UntrustedValue = string | UntrustedText | readonly (string | UntrustedText)[];
|
|
26
|
+
|
|
27
|
+
/** This function gives a text with its source, for an untrusted field of an output. */
|
|
28
|
+
export function untrustedText(text: string, source?: UntrustedSource): UntrustedText {
|
|
29
|
+
return source === undefined ? { text } : { text, source };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The names of the output fields that hold untrusted text (`metadata.untrusted`). Empty: none. */
|
|
33
|
+
export function untrustedFieldsOf(procedure: AnyProcedure): readonly string[] {
|
|
34
|
+
const fields = (procedure.metadata as { untrusted?: unknown } | undefined)?.untrusted;
|
|
35
|
+
return Array.isArray(fields) ? fields.filter((field): field is string => typeof field === "string") : [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The texts of the value of an untrusted field, each with its source. Another value gives none. */
|
|
39
|
+
export function untrustedTextsOf(value: unknown): UntrustedText[] {
|
|
40
|
+
const one = (item: unknown): UntrustedText | undefined => {
|
|
41
|
+
if (typeof item === "string") return { text: item };
|
|
42
|
+
if (item !== null && typeof item === "object" && typeof (item as { text?: unknown }).text === "string") {
|
|
43
|
+
const { text, source } = item as { text: string; source?: unknown };
|
|
44
|
+
return source !== null && typeof source === "object" ? { text, source: source as UntrustedSource } : { text };
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
const items = Array.isArray(value) ? value : [value];
|
|
49
|
+
return items.map(one).filter((item): item is UntrustedText => item !== undefined);
|
|
50
|
+
}
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* no allowed origin is refused (an image tag or a form sends no `Origin` for a GET).
|
|
16
16
|
* - HTTP only: a procedure call by GET or HEAD is refused unless `allowGet` is true, and a
|
|
17
17
|
* request body must be JSON.
|
|
18
|
+
* - A request with ambient credentials (the cookies that a browser sends by itself) is trusted
|
|
19
|
+
* only from a named origin. `"*"` does not open the origin check for it (CSRF). A list or a
|
|
20
|
+
* function of origins still does.
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import type { IncomingHttpHeaders } from "node:http";
|
|
@@ -74,8 +77,9 @@ export function hostNameOf(header: string | undefined): string {
|
|
|
74
77
|
return (header.split(":")[0] ?? "").toLowerCase();
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
function originAllowedBy(origin: string, allowed: BrowserGuardOptions["allowedOrigins"]): boolean {
|
|
78
|
-
|
|
80
|
+
function originAllowedBy(origin: string, allowed: BrowserGuardOptions["allowedOrigins"], ambient: boolean): boolean {
|
|
81
|
+
// "*" names no origin: ambient credentials are not trusted from it
|
|
82
|
+
if (allowed === "*") return !ambient;
|
|
79
83
|
if (typeof allowed === "function") return allowed(origin);
|
|
80
84
|
return (allowed ?? []).includes(origin);
|
|
81
85
|
}
|
|
@@ -102,14 +106,52 @@ function hasBody(headers: IncomingHttpHeaders): boolean {
|
|
|
102
106
|
return length > 0 || headers["transfer-encoding"] !== undefined;
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
/**
|
|
110
|
+
* True when a request carries ambient credentials: a `Cookie` header, which a browser sends by
|
|
111
|
+
* itself, also for a request that another site starts.
|
|
112
|
+
*/
|
|
113
|
+
export function hasAmbientCredentials(headers: IncomingHttpHeaders): boolean {
|
|
114
|
+
const cookie = headers.cookie;
|
|
115
|
+
return typeof cookie === "string" && cookie.trim() !== "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* True when a face trusts a request only from a named origin. This is so when the server reads
|
|
120
|
+
* cookies for each request (`"always"`), or when the request carries a cookie that an auth path can
|
|
121
|
+
* read. `cookiesNamed` is true for a face that trusts no cookie from
|
|
122
|
+
* an origin that is not named, with or without an auth path.
|
|
123
|
+
*
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
export function needsNamedOrigin(mode: "always" | "cookies" | undefined, headers: IncomingHttpHeaders, cookiesNamed: boolean): boolean {
|
|
127
|
+
return mode === "always" || (hasAmbientCredentials(headers) && (cookiesNamed || mode !== undefined));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* This function refuses a face whose server reads cookies for each request (`principalOf`) while
|
|
132
|
+
* its browser checks permit each origin (`"*"`). Each site could then send a request with the cookies
|
|
133
|
+
* of the user (CSRF).
|
|
134
|
+
*
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
export function assertNamedOrigins(face: string, mode: "always" | "cookies" | undefined, allowedOrigins: BrowserGuardOptions["allowedOrigins"], originOption: string): void {
|
|
138
|
+
if (mode !== "always" || allowedOrigins !== "*") return;
|
|
139
|
+
throw new Error(
|
|
140
|
+
`${face}: the request hook principalOf reads the cookies of each request, and the origin "*" lets each site send them (CSRF). Give ${originOption} or allowedOrigins a list or a function of the origins that can call.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
/**
|
|
106
145
|
* The checks of the host, the origin and the fetch site, for HTTP and WebSocket requests.
|
|
107
|
-
* `http` adds the checks of the method and the body.
|
|
146
|
+
* `http` adds the checks of the method and the body. `ambient` is true when the server can trust
|
|
147
|
+
* ambient credentials (cookies) for the request. Then `"*"` does not permit another origin. Only a
|
|
148
|
+
* named origin (a list or a function) does.
|
|
108
149
|
*/
|
|
109
150
|
export function checkBrowserRequest(
|
|
110
151
|
req: GuardedRequest,
|
|
111
152
|
options: BrowserGuardOptions = {},
|
|
112
|
-
http = false
|
|
153
|
+
http = false,
|
|
154
|
+
ambient = false
|
|
113
155
|
): GuardResult {
|
|
114
156
|
const { headers } = req;
|
|
115
157
|
|
|
@@ -123,12 +165,12 @@ export function checkBrowserRequest(
|
|
|
123
165
|
|
|
124
166
|
const origin = typeof headers.origin === "string" ? headers.origin : undefined;
|
|
125
167
|
if (origin !== undefined) {
|
|
126
|
-
if (!sameOrigin(origin, headers.host) && !originAllowedBy(origin, options.allowedOrigins)) {
|
|
168
|
+
if (!sameOrigin(origin, headers.host) && !originAllowedBy(origin, options.allowedOrigins, ambient)) {
|
|
127
169
|
return { ok: false, status: 403, code: "FORBIDDEN_ORIGIN", message: `Forbidden: the origin "${origin}" is not allowed` };
|
|
128
170
|
}
|
|
129
171
|
} else {
|
|
130
172
|
const site = headers["sec-fetch-site"];
|
|
131
|
-
if ((site === "cross-site" || site === "same-site") && options.allowedOrigins !== "*") {
|
|
173
|
+
if ((site === "cross-site" || site === "same-site") && (options.allowedOrigins !== "*" || ambient)) {
|
|
132
174
|
return { ok: false, status: 403, code: "FORBIDDEN_ORIGIN", message: "Forbidden: a request from another site" };
|
|
133
175
|
}
|
|
134
176
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* procedures (a host for an outside caller sets it).
|
|
13
13
|
* - `timeout`: the time limit of each call from a caller (not of its nested calls).
|
|
14
14
|
* - `token`, `browser`: the checks of the network transports (`client-server` applies them).
|
|
15
|
+
* - `principalOf`: the request hook. It reads the credentials of a network request (a cookie, a
|
|
16
|
+
* header, the query) and gives a bearer token, a principal and a narrowing.
|
|
15
17
|
* - `capability`, `readOnly`, `clearanceOrder`, `resources`, `admitItem`: the capability of each
|
|
16
18
|
* call and what the host gives for it (CAPABILITIES-2026-10).
|
|
17
19
|
* - `events`: the remote events of the host (`documentation/EVENTS.md`). The server pins
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
import type { Capability, ClearanceOrder, ProcedurePath, ResourcesOf } from "../procedures/types.js";
|
|
22
24
|
import type { AdmitItem, Interceptor, Invocation } from "../procedures/invoke.js";
|
|
23
25
|
import { narrowCapability } from "../procedures/capability.js";
|
|
24
|
-
import type { ServerRequest } from "./types.js";
|
|
26
|
+
import type { PrincipalOf, ServerRequest } from "./types.js";
|
|
25
27
|
import { timeoutInterceptor } from "../procedures/interceptors.js";
|
|
26
28
|
import type { BrowserGuardOptions } from "./browser-guard.js";
|
|
27
29
|
import type { ProcedureRegistry } from "../procedures/registry.js";
|
|
@@ -73,6 +75,11 @@ export interface HostPolicyOptions {
|
|
|
73
75
|
interceptors?: readonly Interceptor[] | undefined;
|
|
74
76
|
/** The token that each network request must send (HTTP and WebSocket transports). */
|
|
75
77
|
token?: string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* The request hook: it reads the credentials of each network request (a cookie, a header, the
|
|
80
|
+
* query) and gives a bearer token, a principal and a narrowing (`ProcedureServerOptions`).
|
|
81
|
+
*/
|
|
82
|
+
principalOf?: PrincipalOf | undefined;
|
|
76
83
|
/** The browser checks of the network transports (`allowedOrigins`, `allowedHosts`, `allowGet`). */
|
|
77
84
|
browser?: BrowserGuardOptions | undefined;
|
|
78
85
|
/**
|
|
@@ -103,6 +110,8 @@ export interface HostPolicy {
|
|
|
103
110
|
readonly strictCalls: boolean;
|
|
104
111
|
readonly interceptors: readonly Interceptor[];
|
|
105
112
|
readonly token: string | undefined;
|
|
113
|
+
/** The request hook of the network requests, or undefined for none. */
|
|
114
|
+
readonly principalOf?: PrincipalOf | undefined;
|
|
106
115
|
readonly browser: BrowserGuardOptions;
|
|
107
116
|
/** The capability of a call from a caller, or undefined for no limits. */
|
|
108
117
|
readonly capabilityOf: ((request?: ServerRequest<unknown>) => Capability | undefined) | undefined;
|
|
@@ -151,6 +160,7 @@ export function hostPolicy(options: HostPolicyOptions = {}): HostPolicy {
|
|
|
151
160
|
strictCalls: options.strictCalls ?? false,
|
|
152
161
|
interceptors,
|
|
153
162
|
token: options.token,
|
|
163
|
+
...(options.principalOf ? { principalOf: options.principalOf } : {}),
|
|
154
164
|
browser: options.browser ?? {},
|
|
155
165
|
capabilityOf: capabilityOfOptions(options),
|
|
156
166
|
clearanceOrder: options.clearanceOrder,
|
|
@@ -169,6 +179,8 @@ export function procedureServerOptions(policy: HostPolicy, registry?: ProcedureR
|
|
|
169
179
|
strictCalls: policy.strictCalls,
|
|
170
180
|
// The token of the policy: ProcedureServer examines it for each network request (R18)
|
|
171
181
|
...(policy.token !== undefined ? { token: policy.token } : {}),
|
|
182
|
+
// The request hook of the policy: the credentials of a cookie, a header or the query (CLI-2a)
|
|
183
|
+
...(policy.principalOf ? { principalOf: policy.principalOf } : {}),
|
|
172
184
|
...(policy.interceptors.length > 0 ? { interceptors: policy.interceptors } : {}),
|
|
173
185
|
...(policy.capabilityOf ? { capability: policy.capabilityOf } : {}),
|
|
174
186
|
...(policy.clearanceOrder ? { clearanceOrder: policy.clearanceOrder } : {}),
|
package/src/server/index.ts
CHANGED
|
@@ -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,
|
|
@@ -18,8 +18,9 @@ import { pathToKey } from "../procedures/types.js";
|
|
|
18
18
|
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
|
-
import type { Capability, ClearanceOrder, ResourcesOf } from "../procedures/types.js";
|
|
22
|
-
import {
|
|
21
|
+
import type { Capability, ClearanceOrder, ProcedureContext, ResourcesOf } from "../procedures/types.js";
|
|
22
|
+
import type { CollectionPartitions } from "../procedures/collection/partition.js";
|
|
23
|
+
import { TIMEOUT_METADATA_KEY, effectsOf, joinNarrowing, narrowCapability } from "../procedures/capability.js";
|
|
23
24
|
import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptions } from "../procedures/idempotency.js";
|
|
24
25
|
import { optionalMs, type Duration } from "../duration.js";
|
|
25
26
|
import type { CollectionStorage } from "../procedures/collection/storage.js";
|
|
@@ -28,6 +29,7 @@ import { realClock, type Clock } from "../clock.js";
|
|
|
28
29
|
import { errorCodeOf } from "../protocol.js";
|
|
29
30
|
import { EventPublisher, type EventPublisherOptions } from "../events/publisher.js";
|
|
30
31
|
import { getGlobalEventBus } from "../events/bus.js";
|
|
32
|
+
import type { CollectionProceduresOptions } from "../procedures/collection/procedures.js";
|
|
31
33
|
|
|
32
34
|
// =============================================================================
|
|
33
35
|
// Types
|
|
@@ -139,6 +141,24 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
139
141
|
*/
|
|
140
142
|
authenticate?: ((request: ServerRequest<unknown>) => Principal | undefined | Promise<Principal | undefined>) | undefined;
|
|
141
143
|
|
|
144
|
+
/**
|
|
145
|
+
* The request hook of the server: it reads the credentials of each network request (a cookie, a
|
|
146
|
+
* header, the query) and gives a bearer token, a principal and a narrowing. It runs before the
|
|
147
|
+
* token check: its bearer token replaces the bearer token of the credentials. Its principal
|
|
148
|
+
* replaces the principal of the token check. `authenticate` runs after it and can read
|
|
149
|
+
* `request.principal`. Undefined or an error refuses the request with `UNAUTHORIZED`. A request
|
|
150
|
+
* without credentials (from this process) does not go through the hook.
|
|
151
|
+
*/
|
|
152
|
+
principalOf?: PrincipalOf | undefined;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* How the server can authenticate a request with ambient credentials (cookies), for the CSRF
|
|
156
|
+
* checks of the network faces. The default is `"always"` with `principalOf`, `"cookies"` with
|
|
157
|
+
* `authenticate`, and none without them. `false`: the server reads no cookies, for example an
|
|
158
|
+
* `authenticate` function that reads only the bearer token.
|
|
159
|
+
*/
|
|
160
|
+
ambientCredentials?: AmbientCredentials | false | undefined;
|
|
161
|
+
|
|
142
162
|
/**
|
|
143
163
|
* The remote events of the server (`documentation/EVENTS.md`): a bus and its topic rule, or an
|
|
144
164
|
* `EventPublisher`. The server pins the procedure `events.subscribe`, outside its registry, so a
|
|
@@ -297,6 +317,8 @@ export function errorResponse(id: string, error: unknown): ServerResponse<unknow
|
|
|
297
317
|
export class ProcedureServer extends Server implements RepositoryProvider {
|
|
298
318
|
private readonly procedureRegistry: ProcedureRegistry;
|
|
299
319
|
private readonly storages = new Map<string, CollectionStorage<unknown>>();
|
|
320
|
+
/** The partitions of each partitioned collection: their storages and their resolver (roadmap CLI-17) */
|
|
321
|
+
private readonly partitions = new Map<string, CollectionPartitions<unknown>>();
|
|
300
322
|
/** For each storage: the calls in flight that took it, and the closes that wait for them */
|
|
301
323
|
private readonly storageUses = new Map<CollectionStorage<unknown>, { calls: number; waiters: Array<() => void> }>();
|
|
302
324
|
/** The clock of the wait of `useStorage` */
|
|
@@ -326,13 +348,19 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
326
348
|
private draining = false;
|
|
327
349
|
/** The calls in flight, a stream until its end */
|
|
328
350
|
private inFlight = 0;
|
|
329
|
-
/** The token of the server, and its authenticate function (R18) */
|
|
351
|
+
/** The token of the server, its request hook and its authenticate function (R18) */
|
|
330
352
|
private readonly token: string | undefined;
|
|
353
|
+
private readonly principalOf: PrincipalOf | undefined;
|
|
331
354
|
private readonly authenticate: ProcedureServerOptions["authenticate"];
|
|
332
355
|
/** The functions that `stop()` waits on: each one runs when no call is in flight */
|
|
333
356
|
private idleWaiters: Array<() => void> = [];
|
|
334
357
|
/** The publisher of the remote events, from the option `events`, or undefined. */
|
|
335
358
|
readonly publisher: EventPublisher | undefined;
|
|
359
|
+
/**
|
|
360
|
+
* How the server can authenticate a request with ambient credentials (cookies). The network faces
|
|
361
|
+
* read it: they trust such a request only from a named origin (CSRF).
|
|
362
|
+
*/
|
|
363
|
+
readonly ambientCredentials: AmbientCredentials | undefined;
|
|
336
364
|
|
|
337
365
|
constructor(options: ProcedureServerOptions = {}) {
|
|
338
366
|
super(options);
|
|
@@ -350,7 +378,10 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
350
378
|
};
|
|
351
379
|
this.idempotency = options.idempotency === false ? undefined : new IdempotencyCache(options.idempotency ?? {});
|
|
352
380
|
this.token = options.token;
|
|
381
|
+
this.principalOf = options.principalOf;
|
|
353
382
|
this.authenticate = options.authenticate;
|
|
383
|
+
const ambient = options.ambientCredentials ?? (this.principalOf ? "always" : this.authenticate ? "cookies" : undefined);
|
|
384
|
+
this.ambientCredentials = ambient === false ? undefined : ambient;
|
|
354
385
|
// The remote events: the server pins events.subscribe, as it pins host.health (EVENTS.md)
|
|
355
386
|
this.publisher = options.events instanceof EventPublisher ? options.events : options.events ? new EventPublisher(options.events) : undefined;
|
|
356
387
|
if (this.publisher) this.registerProcedure(this.publisher.procedure);
|
|
@@ -393,7 +424,54 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
393
424
|
* @param collection - Collection name
|
|
394
425
|
*/
|
|
395
426
|
hasCollection(collection: string): boolean {
|
|
396
|
-
return this.storages.has(collection);
|
|
427
|
+
return this.storages.has(collection) || this.partitions.has(collection);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The storage of one partition of a collection (roadmap CLI-17).
|
|
432
|
+
*
|
|
433
|
+
* @throws Error if the collection has no partitions
|
|
434
|
+
*/
|
|
435
|
+
getPartitionStorage<T>(collection: string, partition: string): CollectionStorage<T> {
|
|
436
|
+
const partitions = this.partitions.get(collection);
|
|
437
|
+
if (!partitions) {
|
|
438
|
+
throw new Error(`Collection has no partitions: ${collection}`);
|
|
439
|
+
}
|
|
440
|
+
return partitions.storageOf(partition) as CollectionStorage<T>;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** True when the collection has partitions (roadmap CLI-17). */
|
|
444
|
+
hasPartitions(collection: string): boolean {
|
|
445
|
+
return this.partitions.has(collection);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The partition of a call to a partitioned collection (roadmap CLI-17), from the resolver of
|
|
450
|
+
* `usePartitions`. `null` refuses the call.
|
|
451
|
+
*
|
|
452
|
+
* @throws Error if the collection has no partitions
|
|
453
|
+
*/
|
|
454
|
+
resolvePartition(collection: string, ctx: ProcedureContext, requested: string | undefined): string | undefined | null | Promise<string | undefined | null> {
|
|
455
|
+
const partitions = this.partitions.get(collection);
|
|
456
|
+
if (!partitions) {
|
|
457
|
+
throw new Error(`Collection has no partitions: ${collection}`);
|
|
458
|
+
}
|
|
459
|
+
return partitions.resolve(ctx, requested);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Configure the partitions of a collection (roadmap CLI-17). `resolve` gives the partition of
|
|
464
|
+
* each call from its context (the tenant of `ctx.principal`) and the partition that the call asks
|
|
465
|
+
* for: the collection procedures and the subscriptions of `EventPublisher` use it. `storageOf`
|
|
466
|
+
* gives the storage of a partition: give the same storage for the same partition each time.
|
|
467
|
+
*
|
|
468
|
+
* @param name - Collection name
|
|
469
|
+
* @param partitions - The storage of each partition, and the resolver
|
|
470
|
+
* @returns this (for chaining)
|
|
471
|
+
*/
|
|
472
|
+
usePartitions<T>(name: string, partitions: CollectionPartitions<T>): this {
|
|
473
|
+
this.partitions.set(name, partitions as CollectionPartitions<unknown>);
|
|
474
|
+
return this;
|
|
397
475
|
}
|
|
398
476
|
|
|
399
477
|
// ===========================================================================
|
|
@@ -475,17 +553,20 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
475
553
|
private callRepository(): { repository: RepositoryProvider; release: () => void } {
|
|
476
554
|
const taken = new Set<CollectionStorage<unknown>>();
|
|
477
555
|
let released = false;
|
|
556
|
+
const take = <T>(storage: CollectionStorage<unknown>): CollectionStorage<T> => {
|
|
557
|
+
if (!released && !taken.has(storage)) {
|
|
558
|
+
taken.add(storage);
|
|
559
|
+
const use = this.storageUses.get(storage) ?? { calls: 0, waiters: [] };
|
|
560
|
+
use.calls++;
|
|
561
|
+
this.storageUses.set(storage, use);
|
|
562
|
+
}
|
|
563
|
+
return storage as CollectionStorage<T>;
|
|
564
|
+
};
|
|
478
565
|
const repository: RepositoryProvider = {
|
|
479
|
-
getStorage: <T>(collection: string): CollectionStorage<T> =>
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const use = this.storageUses.get(storage) ?? { calls: 0, waiters: [] };
|
|
484
|
-
use.calls++;
|
|
485
|
-
this.storageUses.set(storage, use);
|
|
486
|
-
}
|
|
487
|
-
return storage as CollectionStorage<T>;
|
|
488
|
-
},
|
|
566
|
+
getStorage: <T>(collection: string): CollectionStorage<T> => take<T>(this.getStorage<unknown>(collection)),
|
|
567
|
+
getPartitionStorage: <T>(collection: string, partition: string): CollectionStorage<T> => take<T>(this.getPartitionStorage<unknown>(collection, partition)),
|
|
568
|
+
hasPartitions: (collection: string): boolean => this.hasPartitions(collection),
|
|
569
|
+
resolvePartition: (collection: string, ctx: ProcedureContext, requested: string | undefined) => this.resolvePartition(collection, ctx, requested),
|
|
489
570
|
hasCollection: (collection: string): boolean => this.hasCollection(collection),
|
|
490
571
|
};
|
|
491
572
|
const release = (): void => {
|
|
@@ -642,9 +723,11 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
642
723
|
}
|
|
643
724
|
|
|
644
725
|
/**
|
|
645
|
-
* The token check and the principal of a request (roadmap R18).
|
|
646
|
-
* credentials
|
|
647
|
-
*
|
|
726
|
+
* The token check and the principal of a request (roadmap R18). The request hook reads the
|
|
727
|
+
* credentials of a network request first: a bearer token, a principal and a narrowing. A
|
|
728
|
+
* network request (with credentials) must present the token. The authenticate function gives
|
|
729
|
+
* the principal. The function gives an UNAUTHORIZED response, or undefined when the request
|
|
730
|
+
* passes.
|
|
648
731
|
*/
|
|
649
732
|
private async admit(request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
|
|
650
733
|
const unauthorized = (message: string): ServerResponse<unknown> => ({
|
|
@@ -652,10 +735,26 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
652
735
|
status: { type: "error", code: "UNAUTHORIZED", message, retryable: false },
|
|
653
736
|
metadata: {},
|
|
654
737
|
});
|
|
655
|
-
|
|
656
|
-
|
|
738
|
+
const credentials = request.credentials;
|
|
739
|
+
let identity: RequestIdentity | undefined;
|
|
740
|
+
if (this.principalOf && credentials !== undefined) {
|
|
741
|
+
try {
|
|
742
|
+
identity = await this.principalOf(credentials, request);
|
|
743
|
+
} catch {
|
|
744
|
+
identity = undefined;
|
|
745
|
+
}
|
|
746
|
+
if (!identity) return unauthorized("Unauthorized: the server did not accept the credentials");
|
|
747
|
+
}
|
|
748
|
+
if (this.token !== undefined && credentials !== undefined) {
|
|
749
|
+
const bearer = identity?.bearer ?? credentials.bearer;
|
|
750
|
+
if (!tokenMatches(this.token, bearer)) return unauthorized("Unauthorized: this server requires its token");
|
|
657
751
|
request.principal = { id: "token", via: "token" };
|
|
658
752
|
}
|
|
753
|
+
if (identity?.principal) request.principal = identity.principal;
|
|
754
|
+
if (identity?.narrowing) {
|
|
755
|
+
const order = this.capabilityOptions.clearanceOrder;
|
|
756
|
+
request.narrowing = request.narrowing ? joinNarrowing(request.narrowing, identity.narrowing, order) : identity.narrowing;
|
|
757
|
+
}
|
|
659
758
|
if (this.authenticate) {
|
|
660
759
|
let principal: Principal | undefined;
|
|
661
760
|
try {
|
|
@@ -734,6 +833,17 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
734
833
|
return { ...response, id: request.id };
|
|
735
834
|
}
|
|
736
835
|
|
|
836
|
+
/**
|
|
837
|
+
* The capability of a request. It comes from the server, never from the request. The remaining
|
|
838
|
+
* time of the caller and the narrowing of the request can only make it narrower
|
|
839
|
+
* (CAPABILITIES-2026-10, decision 9). Two different clearances without an order throw.
|
|
840
|
+
*/
|
|
841
|
+
private capabilityOf(request: ServerRequest<unknown>): Capability | undefined {
|
|
842
|
+
const { capability, clearanceOrder } = this.capabilityOptions;
|
|
843
|
+
const base = withRequestTimeout(typeof capability === "function" ? capability(request) : capability, request, clearanceOrder);
|
|
844
|
+
return request.narrowing ? narrowCapability(base, request.narrowing, clearanceOrder) : base;
|
|
845
|
+
}
|
|
846
|
+
|
|
737
847
|
/** Run a procedure for a request with a signal. */
|
|
738
848
|
private async run(procedure: AnyProcedure, request: ServerRequest<unknown>, signal: AbortSignal | undefined): Promise<ServerResponse<unknown>> {
|
|
739
849
|
// The call uses the storages that it takes until its end: the end of a stream, for a stream
|
|
@@ -742,21 +852,31 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
742
852
|
try {
|
|
743
853
|
// One invocation path for every host (ARCHITECTURE-PROPOSALS P1): input validation, the
|
|
744
854
|
// context, the expose rule for nested calls (BUGS-2026-07 H18) and output validation
|
|
745
|
-
const {
|
|
855
|
+
const { clearanceOrder, resources, admitItem } = this.capabilityOptions;
|
|
856
|
+
let capability: Capability | undefined;
|
|
857
|
+
try {
|
|
858
|
+
capability = this.capabilityOf(request);
|
|
859
|
+
} catch (error) {
|
|
860
|
+
const message = `The server cannot narrow the capability of the request: ${error instanceof Error ? error.message : String(error)}`;
|
|
861
|
+
return errorResponse(request.id, Object.assign(new Error(message), { code: "INTERNAL_ERROR" }));
|
|
862
|
+
}
|
|
746
863
|
const output = await invokeProcedure(procedure, request.payload, {
|
|
747
864
|
registry: this.procedureRegistry,
|
|
748
865
|
metadata: request.metadata,
|
|
749
866
|
signal,
|
|
867
|
+
// What the face of the call offers: it asks the person, and it shows the progress (CLI-12)
|
|
868
|
+
...(request.elicit ? { elicit: request.elicit } : {}),
|
|
869
|
+
...(request.progress ? { progress: request.progress } : {}),
|
|
750
870
|
repository,
|
|
751
871
|
expose: this.expose,
|
|
752
872
|
strictCalls: this.strictCalls,
|
|
753
873
|
interceptors: this.interceptors,
|
|
754
|
-
|
|
755
|
-
// the caller can only make the deadline earlier (CAPABILITIES-2026-10, decision 9).
|
|
756
|
-
capability: withRequestTimeout(typeof capability === "function" ? capability(request) : capability, request, clearanceOrder),
|
|
874
|
+
capability,
|
|
757
875
|
clearanceOrder,
|
|
758
876
|
resources,
|
|
759
877
|
admitItem,
|
|
878
|
+
// The principal of the authentication: ctx.principal, and the nested calls (R18)
|
|
879
|
+
...(request.principal !== undefined ? { principal: request.principal } : {}),
|
|
760
880
|
// The bus of the remote events is the bus of the procedures (EVENTS.md)
|
|
761
881
|
...(this.publisher ? { bus: this.publisher.bus } : {}),
|
|
762
882
|
});
|
|
@@ -784,11 +904,12 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
784
904
|
* Creates standard CRUD procedures using the configured storage.
|
|
785
905
|
*
|
|
786
906
|
* @param collectionName - Collection name
|
|
907
|
+
* @param options - The query policy of `find`. `$regex` is off unless the policy permits it.
|
|
787
908
|
*/
|
|
788
|
-
registerCollectionProcedures(collectionName: string): void {
|
|
909
|
+
registerCollectionProcedures(collectionName: string, options?: CollectionProceduresOptions): void {
|
|
789
910
|
// Uses a top-level import; require() is not available in this ESM package and
|
|
790
911
|
// previously threw "Cannot find module" on every call. See BUGS-2026-07 H13.
|
|
791
|
-
const procedures = createCollectionProcedures(collectionName);
|
|
912
|
+
const procedures = createCollectionProcedures(collectionName, options);
|
|
792
913
|
|
|
793
914
|
// Register with the procedure registry first
|
|
794
915
|
this.procedureRegistry.registerAll(procedures);
|