@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
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,10 +87,73 @@ 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
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The rules of the procedure under a root of the files (roadmap R33): the inputs that name a
|
|
109
|
+
* path, or the reason that a root cannot confine it. A host with a root (`--fs-root`,
|
|
110
|
+
* `MCP_FS_ROOT`) reads them. Without the rules, the host decides from `effects`.
|
|
111
|
+
*/
|
|
112
|
+
fsRoot?: FsRootRules | undefined;
|
|
113
|
+
|
|
86
114
|
/** Custom metadata fields */
|
|
87
115
|
[key: string]: unknown;
|
|
88
116
|
}
|
|
89
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The rules of a procedure under a root of the files (roadmap R33). A host with a root keeps each
|
|
120
|
+
* path of these inputs in the root, or it refuses the procedure. `confineToRoot()` of
|
|
121
|
+
* `@procedures/fs` applies the rules. Without a root, the rules change nothing.
|
|
122
|
+
*/
|
|
123
|
+
export interface FsRootRules {
|
|
124
|
+
/**
|
|
125
|
+
* The input that names the working folder. When the call gives no value, the procedure gets the
|
|
126
|
+
* root. A relative value starts at the root.
|
|
127
|
+
*/
|
|
128
|
+
readonly cwd?: string;
|
|
129
|
+
/**
|
|
130
|
+
* The inputs that name a file or a folder: a text, or a list of texts. A relative path starts at
|
|
131
|
+
* the root. The procedure gets the absolute path.
|
|
132
|
+
*/
|
|
133
|
+
readonly paths?: readonly string[];
|
|
134
|
+
/**
|
|
135
|
+
* The inputs that name a file or a folder relative to the working folder, for example the files
|
|
136
|
+
* of an archive. A relative name starts at the working folder, or at the root without `cwd`.
|
|
137
|
+
* The host examines each path, and it does not change the value.
|
|
138
|
+
*/
|
|
139
|
+
readonly names?: readonly string[];
|
|
140
|
+
/**
|
|
141
|
+
* The inputs of `paths` whose default is outside the working folder. Under a root, a call
|
|
142
|
+
* without a value of such an input is refused.
|
|
143
|
+
*/
|
|
144
|
+
readonly required?: readonly string[];
|
|
145
|
+
/** The values of the inputs that are not paths, for example `:memory:` of SQLite. */
|
|
146
|
+
readonly values?: readonly string[];
|
|
147
|
+
/** The reason that a root cannot confine the procedure. Under a root, the host refuses it. */
|
|
148
|
+
readonly refuse?: string;
|
|
149
|
+
/**
|
|
150
|
+
* True for a procedure that starts the program of its input (`shell.run`). Under a root, the
|
|
151
|
+
* host refuses a call of a client. It accepts the nested call of a confined procedure that
|
|
152
|
+
* names this procedure in `metadata.calls`.
|
|
153
|
+
*/
|
|
154
|
+
readonly program?: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
90
157
|
// =============================================================================
|
|
91
158
|
// Capabilities (CAPABILITIES-2026-10)
|
|
92
159
|
// =============================================================================
|
|
@@ -283,6 +350,20 @@ export interface ProcedureContext {
|
|
|
283
350
|
*/
|
|
284
351
|
onRef?: ((event: HydrationEvent) => void) | undefined;
|
|
285
352
|
|
|
353
|
+
/**
|
|
354
|
+
* The function that asks the person at the caller for input or a confirmation (CLI-12): MCP
|
|
355
|
+
* elicitation, or another face that can ask. Absent when no one can answer. The helpers
|
|
356
|
+
* `elicitInput` and `elicitConfirmation` then give the action `unavailable`. The nested calls
|
|
357
|
+
* get the same function.
|
|
358
|
+
*/
|
|
359
|
+
elicit?: Elicit | undefined;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The function that tells the caller the progress of this call (an MCP progress notification).
|
|
363
|
+
* Absent when the caller asked for no progress. Only the call of the host gets it.
|
|
364
|
+
*/
|
|
365
|
+
progress?: ReportProgress | undefined;
|
|
366
|
+
|
|
286
367
|
/**
|
|
287
368
|
* The capability of this invocation (CAPABILITIES-2026-10): its clearance, permitted effects
|
|
288
369
|
* and deadline. The nested calls of `ctx.client` get it, or a narrower one. Frozen.
|
|
@@ -294,6 +375,27 @@ export interface ProcedureContext {
|
|
|
294
375
|
* database for a lower clearance, for example. Empty when the host gives none.
|
|
295
376
|
*/
|
|
296
377
|
resources: ResourcesOf;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The procedure whose handler made this call through its `ctx.client`. A call of a host or a
|
|
381
|
+
* client has no caller. The framework sets it from the invocation, so an input cannot change
|
|
382
|
+
* it. The root of the files reads it (roadmap R33).
|
|
383
|
+
*/
|
|
384
|
+
caller?: AnyProcedure | undefined;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The principal of the call: who sends it. The server sets it from its authentication (the
|
|
388
|
+
* token, the request hook `principalOf`, `authenticate`). The nested calls get the same
|
|
389
|
+
* principal. An input or the metadata cannot change it. A call from this process has none.
|
|
390
|
+
*/
|
|
391
|
+
principal?: Principal | undefined;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The output filter of the host for this call: its `admitItem`, with this invocation (roadmap
|
|
395
|
+
* CLI-15). A handler that sends the data of other writers later, for example a subscription,
|
|
396
|
+
* examines each row with it. Absent for a nested call, or when the host has no filter.
|
|
397
|
+
*/
|
|
398
|
+
admit?: ((item: unknown) => Promise<boolean>) | undefined;
|
|
297
399
|
}
|
|
298
400
|
|
|
299
401
|
/**
|
|
@@ -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
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -47,7 +47,8 @@ export type ProtocolErrorCode =
|
|
|
47
47
|
| "UNAVAILABLE"
|
|
48
48
|
| "CONNECTION_CLOSED"
|
|
49
49
|
| "CONNECTION_NOT_FOUND"
|
|
50
|
-
| "TOPIC_NOT_FOUND"
|
|
50
|
+
| "TOPIC_NOT_FOUND"
|
|
51
|
+
| "TOPIC_CLEARED";
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* The error codes of the protocol, on each face (HTTP, WebSocket, MCP, local). An error that a
|
|
@@ -78,6 +79,7 @@ export const ERROR_CODES: Readonly<Record<ProtocolErrorCode, ErrorCodeInfo>> = {
|
|
|
78
79
|
CONNECTION_CLOSED: { http: 503, retryable: true, meaning: "The connection closed before the call left" },
|
|
79
80
|
CONNECTION_NOT_FOUND: { http: 404, retryable: false, meaning: "The server has no connection with this id" },
|
|
80
81
|
TOPIC_NOT_FOUND: { http: 404, retryable: false, meaning: "The topic rule of the host has no topic with this name" },
|
|
82
|
+
TOPIC_CLEARED: { http: 410, retryable: false, meaning: "The host cleared a topic of the subscription: clear() of its bus removed the listener" },
|
|
81
83
|
};
|
|
82
84
|
|
|
83
85
|
/** True when a code is an error code of the protocol. */
|
|
@@ -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 } : {}),
|