@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.
Files changed (234) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +115 -11
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/client/transport.d.ts +4 -0
  5. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  6. package/dist/adapters/http/client/transport.js +11 -1
  7. package/dist/adapters/http/client/transport.js.map +1 -1
  8. package/dist/adapters/http/client/types.d.ts +19 -0
  9. package/dist/adapters/http/client/types.d.ts.map +1 -1
  10. package/dist/adapters/http/server/cors.d.ts +64 -0
  11. package/dist/adapters/http/server/cors.d.ts.map +1 -0
  12. package/dist/adapters/http/server/cors.js +113 -0
  13. package/dist/adapters/http/server/cors.js.map +1 -0
  14. package/dist/adapters/http/server/handler.d.ts +136 -0
  15. package/dist/adapters/http/server/handler.d.ts.map +1 -0
  16. package/dist/adapters/http/server/handler.js +495 -0
  17. package/dist/adapters/http/server/handler.js.map +1 -0
  18. package/dist/adapters/http/server/strategies.d.ts +14 -5
  19. package/dist/adapters/http/server/strategies.d.ts.map +1 -1
  20. package/dist/adapters/http/server/strategies.js.map +1 -1
  21. package/dist/adapters/http/server/transport.d.ts +36 -46
  22. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  23. package/dist/adapters/http/server/transport.js +58 -433
  24. package/dist/adapters/http/server/transport.js.map +1 -1
  25. package/dist/adapters/http/server/types.d.ts +13 -0
  26. package/dist/adapters/http/server/types.d.ts.map +1 -1
  27. package/dist/adapters/websocket/client/transport.d.ts +7 -3
  28. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  29. package/dist/adapters/websocket/client/transport.js +9 -3
  30. package/dist/adapters/websocket/client/transport.js.map +1 -1
  31. package/dist/adapters/websocket/server/transport.d.ts +56 -1
  32. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  33. package/dist/adapters/websocket/server/transport.js +174 -17
  34. package/dist/adapters/websocket/server/transport.js.map +1 -1
  35. package/dist/adapters/websocket/server/types.d.ts +10 -3
  36. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  37. package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
  38. package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
  39. package/dist/adapters/websocket/server/upgrade-router.js +62 -0
  40. package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
  41. package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
  42. package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
  43. package/dist/adapters/websocket/server/upgrade.js +49 -0
  44. package/dist/adapters/websocket/server/upgrade.js.map +1 -0
  45. package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
  46. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  47. package/dist/adapters/websocket/shared/rpc.js +8 -2
  48. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  49. package/dist/browser.d.ts +14 -6
  50. package/dist/browser.d.ts.map +1 -1
  51. package/dist/browser.js +12 -3
  52. package/dist/browser.js.map +1 -1
  53. package/dist/client/client.d.ts +43 -0
  54. package/dist/client/client.d.ts.map +1 -1
  55. package/dist/client/client.js +69 -1
  56. package/dist/client/client.js.map +1 -1
  57. package/dist/client/errors/registry.d.ts.map +1 -1
  58. package/dist/client/errors/registry.js +1 -0
  59. package/dist/client/errors/registry.js.map +1 -1
  60. package/dist/client/middleware/retry.d.ts +4 -0
  61. package/dist/client/middleware/retry.d.ts.map +1 -1
  62. package/dist/client/middleware/retry.js +86 -78
  63. package/dist/client/middleware/retry.js.map +1 -1
  64. package/dist/events/bus.d.ts +32 -14
  65. package/dist/events/bus.d.ts.map +1 -1
  66. package/dist/events/bus.js +119 -65
  67. package/dist/events/bus.js.map +1 -1
  68. package/dist/events/index.d.ts +3 -2
  69. package/dist/events/index.d.ts.map +1 -1
  70. package/dist/events/index.js.map +1 -1
  71. package/dist/events/publisher.d.ts +14 -0
  72. package/dist/events/publisher.d.ts.map +1 -1
  73. package/dist/events/publisher.js +62 -10
  74. package/dist/events/publisher.js.map +1 -1
  75. package/dist/events/remote.d.ts +80 -18
  76. package/dist/events/remote.d.ts.map +1 -1
  77. package/dist/events/remote.js +448 -58
  78. package/dist/events/remote.js.map +1 -1
  79. package/dist/events/rows.d.ts +31 -0
  80. package/dist/events/rows.d.ts.map +1 -0
  81. package/dist/events/rows.js +39 -0
  82. package/dist/events/rows.js.map +1 -0
  83. package/dist/events/types.d.ts +13 -2
  84. package/dist/events/types.d.ts.map +1 -1
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +1 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/node.d.ts +14 -0
  90. package/dist/node.d.ts.map +1 -1
  91. package/dist/node.js +9 -0
  92. package/dist/node.js.map +1 -1
  93. package/dist/procedures/caller.d.ts +33 -0
  94. package/dist/procedures/caller.d.ts.map +1 -0
  95. package/dist/procedures/caller.js +27 -0
  96. package/dist/procedures/caller.js.map +1 -0
  97. package/dist/procedures/capability.d.ts +6 -0
  98. package/dist/procedures/capability.d.ts.map +1 -1
  99. package/dist/procedures/capability.js +25 -0
  100. package/dist/procedures/capability.js.map +1 -1
  101. package/dist/procedures/collection/changes.d.ts +94 -0
  102. package/dist/procedures/collection/changes.d.ts.map +1 -0
  103. package/dist/procedures/collection/changes.js +182 -0
  104. package/dist/procedures/collection/changes.js.map +1 -0
  105. package/dist/procedures/collection/index.d.ts +8 -3
  106. package/dist/procedures/collection/index.d.ts.map +1 -1
  107. package/dist/procedures/collection/index.js +6 -1
  108. package/dist/procedures/collection/index.js.map +1 -1
  109. package/dist/procedures/collection/procedures.d.ts +39 -2
  110. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  111. package/dist/procedures/collection/procedures.js +131 -8
  112. package/dist/procedures/collection/procedures.js.map +1 -1
  113. package/dist/procedures/collection/query.d.ts +246 -0
  114. package/dist/procedures/collection/query.d.ts.map +1 -0
  115. package/dist/procedures/collection/query.js +854 -0
  116. package/dist/procedures/collection/query.js.map +1 -0
  117. package/dist/procedures/collection/schemas.d.ts +59 -0
  118. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  119. package/dist/procedures/collection/schemas.js +172 -6
  120. package/dist/procedures/collection/schemas.js.map +1 -1
  121. package/dist/procedures/collection/storage.d.ts +64 -6
  122. package/dist/procedures/collection/storage.d.ts.map +1 -1
  123. package/dist/procedures/collection/storage.js +23 -0
  124. package/dist/procedures/collection/storage.js.map +1 -1
  125. package/dist/procedures/elicit.d.ts +190 -0
  126. package/dist/procedures/elicit.d.ts.map +1 -0
  127. package/dist/procedures/elicit.js +127 -0
  128. package/dist/procedures/elicit.js.map +1 -0
  129. package/dist/procedures/hints.d.ts +43 -0
  130. package/dist/procedures/hints.d.ts.map +1 -0
  131. package/dist/procedures/hints.js +74 -0
  132. package/dist/procedures/hints.js.map +1 -0
  133. package/dist/procedures/index.d.ts +12 -3
  134. package/dist/procedures/index.d.ts.map +1 -1
  135. package/dist/procedures/index.js +7 -1
  136. package/dist/procedures/index.js.map +1 -1
  137. package/dist/procedures/invoke.d.ts +20 -0
  138. package/dist/procedures/invoke.d.ts.map +1 -1
  139. package/dist/procedures/invoke.js +16 -0
  140. package/dist/procedures/invoke.js.map +1 -1
  141. package/dist/procedures/progress.d.ts +24 -0
  142. package/dist/procedures/progress.d.ts.map +1 -0
  143. package/dist/procedures/progress.js +11 -0
  144. package/dist/procedures/progress.js.map +1 -0
  145. package/dist/procedures/registry.d.ts.map +1 -1
  146. package/dist/procedures/registry.js +5 -0
  147. package/dist/procedures/registry.js.map +1 -1
  148. package/dist/procedures/types.d.ts +92 -0
  149. package/dist/procedures/types.d.ts.map +1 -1
  150. package/dist/procedures/types.js.map +1 -1
  151. package/dist/procedures/untrusted.d.ts +28 -0
  152. package/dist/procedures/untrusted.d.ts.map +1 -0
  153. package/dist/procedures/untrusted.js +32 -0
  154. package/dist/procedures/untrusted.js.map +1 -0
  155. package/dist/protocol.d.ts +1 -1
  156. package/dist/protocol.d.ts.map +1 -1
  157. package/dist/protocol.js +1 -0
  158. package/dist/protocol.js.map +1 -1
  159. package/dist/server/browser-guard.d.ts +12 -2
  160. package/dist/server/browser-guard.d.ts.map +1 -1
  161. package/dist/server/browser-guard.js +43 -6
  162. package/dist/server/browser-guard.js.map +1 -1
  163. package/dist/server/host-policy.d.ts +10 -1
  164. package/dist/server/host-policy.d.ts.map +1 -1
  165. package/dist/server/host-policy.js +5 -0
  166. package/dist/server/host-policy.js.map +1 -1
  167. package/dist/server/index.d.ts +1 -1
  168. package/dist/server/index.d.ts.map +1 -1
  169. package/dist/server/index.js.map +1 -1
  170. package/dist/server/procedure-server.d.ts +75 -9
  171. package/dist/server/procedure-server.d.ts.map +1 -1
  172. package/dist/server/procedure-server.js +151 -17
  173. package/dist/server/procedure-server.js.map +1 -1
  174. package/dist/server/request-auth.d.ts +51 -4
  175. package/dist/server/request-auth.d.ts.map +1 -1
  176. package/dist/server/request-auth.js +116 -5
  177. package/dist/server/request-auth.js.map +1 -1
  178. package/dist/server/types.d.ts +73 -5
  179. package/dist/server/types.d.ts.map +1 -1
  180. package/dist/server/types.js.map +1 -1
  181. package/dist/timers.d.ts +5 -0
  182. package/dist/timers.d.ts.map +1 -1
  183. package/dist/timers.js +3 -1
  184. package/dist/timers.js.map +1 -1
  185. package/package.json +5 -1
  186. package/src/adapters/http/client/transport.ts +11 -1
  187. package/src/adapters/http/client/types.ts +22 -0
  188. package/src/adapters/http/server/cors.ts +144 -0
  189. package/src/adapters/http/server/handler.ts +624 -0
  190. package/src/adapters/http/server/strategies.ts +16 -6
  191. package/src/adapters/http/server/transport.ts +66 -479
  192. package/src/adapters/http/server/types.ts +15 -0
  193. package/src/adapters/websocket/client/transport.ts +9 -3
  194. package/src/adapters/websocket/server/transport.ts +189 -18
  195. package/src/adapters/websocket/server/types.ts +11 -3
  196. package/src/adapters/websocket/server/upgrade-router.ts +77 -0
  197. package/src/adapters/websocket/server/upgrade.ts +88 -0
  198. package/src/adapters/websocket/shared/rpc.ts +9 -2
  199. package/src/browser.ts +85 -2
  200. package/src/client/client.ts +83 -1
  201. package/src/client/errors/registry.ts +1 -0
  202. package/src/client/middleware/retry.ts +89 -78
  203. package/src/events/bus.ts +141 -73
  204. package/src/events/index.ts +3 -0
  205. package/src/events/publisher.ts +71 -10
  206. package/src/events/remote.ts +575 -73
  207. package/src/events/rows.ts +48 -0
  208. package/src/events/types.ts +15 -2
  209. package/src/index.ts +1 -1
  210. package/src/node.ts +21 -0
  211. package/src/procedures/caller.ts +45 -0
  212. package/src/procedures/capability.ts +21 -0
  213. package/src/procedures/collection/changes.ts +245 -0
  214. package/src/procedures/collection/index.ts +56 -1
  215. package/src/procedures/collection/procedures.ts +185 -7
  216. package/src/procedures/collection/query.ts +1009 -0
  217. package/src/procedures/collection/schemas.ts +233 -7
  218. package/src/procedures/collection/storage.ts +84 -6
  219. package/src/procedures/elicit.ts +320 -0
  220. package/src/procedures/hints.ts +91 -0
  221. package/src/procedures/index.ts +82 -2
  222. package/src/procedures/invoke.ts +31 -0
  223. package/src/procedures/progress.ts +25 -0
  224. package/src/procedures/registry.ts +4 -0
  225. package/src/procedures/types.ts +102 -0
  226. package/src/procedures/untrusted.ts +50 -0
  227. package/src/protocol.ts +3 -1
  228. package/src/server/browser-guard.ts +48 -6
  229. package/src/server/host-policy.ts +13 -1
  230. package/src/server/index.ts +3 -0
  231. package/src/server/procedure-server.ts +185 -20
  232. package/src/server/request-auth.ts +128 -6
  233. package/src/server/types.ts +82 -5
  234. package/src/timers.ts +8 -1
@@ -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
- if (allowed === "*") return true;
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 } : {}),
@@ -48,6 +48,9 @@ export {
48
48
  type RequestHandler,
49
49
  type Credentials,
50
50
  type Principal,
51
+ type PrincipalOf,
52
+ type RequestIdentity,
53
+ type AmbientCredentials,
51
54
  type ServerHandler,
52
55
  type ServerMiddleware,
53
56
  type ServerContext,