@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.
Files changed (197) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +95 -8
  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 +30 -46
  22. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  23. package/dist/adapters/http/server/transport.js +48 -432
  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.map +1 -1
  28. package/dist/adapters/websocket/client/transport.js +4 -12
  29. package/dist/adapters/websocket/client/transport.js.map +1 -1
  30. package/dist/adapters/websocket/server/transport.d.ts +46 -1
  31. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  32. package/dist/adapters/websocket/server/transport.js +152 -15
  33. package/dist/adapters/websocket/server/transport.js.map +1 -1
  34. package/dist/adapters/websocket/server/types.d.ts +10 -3
  35. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  36. package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
  37. package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
  38. package/dist/adapters/websocket/server/upgrade-router.js +62 -0
  39. package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
  40. package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
  41. package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
  42. package/dist/adapters/websocket/server/upgrade.js +49 -0
  43. package/dist/adapters/websocket/server/upgrade.js.map +1 -0
  44. package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
  45. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  46. package/dist/adapters/websocket/shared/rpc.js +8 -2
  47. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  48. package/dist/browser.d.ts +12 -4
  49. package/dist/browser.d.ts.map +1 -1
  50. package/dist/browser.js +13 -2
  51. package/dist/browser.js.map +1 -1
  52. package/dist/client/client.d.ts +43 -0
  53. package/dist/client/client.d.ts.map +1 -1
  54. package/dist/client/client.js +69 -1
  55. package/dist/client/client.js.map +1 -1
  56. package/dist/events/publisher.d.ts +25 -1
  57. package/dist/events/publisher.d.ts.map +1 -1
  58. package/dist/events/publisher.js +66 -9
  59. package/dist/events/publisher.js.map +1 -1
  60. package/dist/events/rows.d.ts +31 -0
  61. package/dist/events/rows.d.ts.map +1 -0
  62. package/dist/events/rows.js +39 -0
  63. package/dist/events/rows.js.map +1 -0
  64. package/dist/index.d.ts +1 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/node.d.ts +14 -0
  69. package/dist/node.d.ts.map +1 -1
  70. package/dist/node.js +9 -0
  71. package/dist/node.js.map +1 -1
  72. package/dist/procedures/capability.d.ts +6 -0
  73. package/dist/procedures/capability.d.ts.map +1 -1
  74. package/dist/procedures/capability.js +25 -0
  75. package/dist/procedures/capability.js.map +1 -1
  76. package/dist/procedures/collection/changes.d.ts +94 -0
  77. package/dist/procedures/collection/changes.d.ts.map +1 -0
  78. package/dist/procedures/collection/changes.js +182 -0
  79. package/dist/procedures/collection/changes.js.map +1 -0
  80. package/dist/procedures/collection/index.d.ts +10 -3
  81. package/dist/procedures/collection/index.d.ts.map +1 -1
  82. package/dist/procedures/collection/index.js +8 -1
  83. package/dist/procedures/collection/index.js.map +1 -1
  84. package/dist/procedures/collection/partition.d.ts +67 -0
  85. package/dist/procedures/collection/partition.d.ts.map +1 -0
  86. package/dist/procedures/collection/partition.js +82 -0
  87. package/dist/procedures/collection/partition.js.map +1 -0
  88. package/dist/procedures/collection/procedures.d.ts +39 -2
  89. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  90. package/dist/procedures/collection/procedures.js +147 -44
  91. package/dist/procedures/collection/procedures.js.map +1 -1
  92. package/dist/procedures/collection/query.d.ts +246 -0
  93. package/dist/procedures/collection/query.d.ts.map +1 -0
  94. package/dist/procedures/collection/query.js +854 -0
  95. package/dist/procedures/collection/query.js.map +1 -0
  96. package/dist/procedures/collection/schemas.d.ts +50 -0
  97. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  98. package/dist/procedures/collection/schemas.js +144 -6
  99. package/dist/procedures/collection/schemas.js.map +1 -1
  100. package/dist/procedures/collection/storage.d.ts +58 -6
  101. package/dist/procedures/collection/storage.d.ts.map +1 -1
  102. package/dist/procedures/collection/storage.js +23 -0
  103. package/dist/procedures/collection/storage.js.map +1 -1
  104. package/dist/procedures/elicit.d.ts +190 -0
  105. package/dist/procedures/elicit.d.ts.map +1 -0
  106. package/dist/procedures/elicit.js +127 -0
  107. package/dist/procedures/elicit.js.map +1 -0
  108. package/dist/procedures/hints.d.ts +43 -0
  109. package/dist/procedures/hints.d.ts.map +1 -0
  110. package/dist/procedures/hints.js +74 -0
  111. package/dist/procedures/hints.js.map +1 -0
  112. package/dist/procedures/index.d.ts +9 -2
  113. package/dist/procedures/index.d.ts.map +1 -1
  114. package/dist/procedures/index.js +5 -1
  115. package/dist/procedures/index.js.map +1 -1
  116. package/dist/procedures/invoke.d.ts +24 -2
  117. package/dist/procedures/invoke.d.ts.map +1 -1
  118. package/dist/procedures/invoke.js +14 -1
  119. package/dist/procedures/invoke.js.map +1 -1
  120. package/dist/procedures/progress.d.ts +24 -0
  121. package/dist/procedures/progress.d.ts.map +1 -0
  122. package/dist/procedures/progress.js +11 -0
  123. package/dist/procedures/progress.js.map +1 -0
  124. package/dist/procedures/registry.d.ts.map +1 -1
  125. package/dist/procedures/registry.js +5 -0
  126. package/dist/procedures/registry.js.map +1 -1
  127. package/dist/procedures/types.d.ts +55 -0
  128. package/dist/procedures/types.d.ts.map +1 -1
  129. package/dist/procedures/types.js.map +1 -1
  130. package/dist/procedures/untrusted.d.ts +28 -0
  131. package/dist/procedures/untrusted.d.ts.map +1 -0
  132. package/dist/procedures/untrusted.js +32 -0
  133. package/dist/procedures/untrusted.js.map +1 -0
  134. package/dist/server/browser-guard.d.ts +12 -2
  135. package/dist/server/browser-guard.d.ts.map +1 -1
  136. package/dist/server/browser-guard.js +43 -6
  137. package/dist/server/browser-guard.js.map +1 -1
  138. package/dist/server/host-policy.d.ts +10 -1
  139. package/dist/server/host-policy.d.ts.map +1 -1
  140. package/dist/server/host-policy.js +5 -0
  141. package/dist/server/host-policy.js.map +1 -1
  142. package/dist/server/index.d.ts +1 -1
  143. package/dist/server/index.d.ts.map +1 -1
  144. package/dist/server/index.js.map +1 -1
  145. package/dist/server/procedure-server.d.ts +68 -7
  146. package/dist/server/procedure-server.d.ts.map +1 -1
  147. package/dist/server/procedure-server.js +124 -24
  148. package/dist/server/procedure-server.js.map +1 -1
  149. package/dist/server/request-auth.d.ts +51 -4
  150. package/dist/server/request-auth.d.ts.map +1 -1
  151. package/dist/server/request-auth.js +116 -5
  152. package/dist/server/request-auth.js.map +1 -1
  153. package/dist/server/types.d.ts +73 -5
  154. package/dist/server/types.d.ts.map +1 -1
  155. package/dist/server/types.js.map +1 -1
  156. package/package.json +7 -3
  157. package/src/adapters/http/client/transport.ts +11 -1
  158. package/src/adapters/http/client/types.ts +22 -0
  159. package/src/adapters/http/server/cors.ts +144 -0
  160. package/src/adapters/http/server/handler.ts +624 -0
  161. package/src/adapters/http/server/strategies.ts +16 -6
  162. package/src/adapters/http/server/transport.ts +55 -478
  163. package/src/adapters/http/server/types.ts +15 -0
  164. package/src/adapters/websocket/client/transport.ts +4 -12
  165. package/src/adapters/websocket/server/transport.ts +166 -16
  166. package/src/adapters/websocket/server/types.ts +11 -3
  167. package/src/adapters/websocket/server/upgrade-router.ts +77 -0
  168. package/src/adapters/websocket/server/upgrade.ts +88 -0
  169. package/src/adapters/websocket/shared/rpc.ts +9 -2
  170. package/src/browser.ts +85 -2
  171. package/src/client/client.ts +83 -1
  172. package/src/events/publisher.ts +75 -11
  173. package/src/events/rows.ts +48 -0
  174. package/src/index.ts +1 -1
  175. package/src/node.ts +21 -0
  176. package/src/procedures/capability.ts +21 -0
  177. package/src/procedures/collection/changes.ts +245 -0
  178. package/src/procedures/collection/index.ts +57 -1
  179. package/src/procedures/collection/partition.ts +123 -0
  180. package/src/procedures/collection/procedures.ts +206 -51
  181. package/src/procedures/collection/query.ts +1009 -0
  182. package/src/procedures/collection/schemas.ts +196 -7
  183. package/src/procedures/collection/storage.ts +78 -6
  184. package/src/procedures/elicit.ts +320 -0
  185. package/src/procedures/hints.ts +91 -0
  186. package/src/procedures/index.ts +83 -2
  187. package/src/procedures/invoke.ts +33 -2
  188. package/src/procedures/progress.ts +25 -0
  189. package/src/procedures/registry.ts +4 -0
  190. package/src/procedures/types.ts +65 -0
  191. package/src/procedures/untrusted.ts +50 -0
  192. package/src/server/browser-guard.ts +48 -6
  193. package/src/server/host-policy.ts +13 -1
  194. package/src/server/index.ts +3 -0
  195. package/src/server/procedure-server.ts +147 -26
  196. package/src/server/request-auth.ts +128 -6
  197. package/src/server/types.ts +82 -5
@@ -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
- 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,
@@ -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 { TIMEOUT_METADATA_KEY, effectsOf, narrowCapability } from "../procedures/capability.js";
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
- const storage = this.getStorage<unknown>(collection);
481
- if (!released && !taken.has(storage)) {
482
- taken.add(storage);
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). A network request (with
646
- * credentials) must present the token. The authenticate function gives the principal. The
647
- * function gives an UNAUTHORIZED response, or undefined when the request passes.
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
- if (this.token !== undefined && request.credentials !== undefined) {
656
- if (!tokenMatches(this.token, request.credentials.bearer)) return unauthorized("Unauthorized: this server requires its token");
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 { capability, clearanceOrder, resources, admitItem } = this.capabilityOptions;
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
- // The capability comes from the server, never from the request. The remaining time of
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);