@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
@@ -30,6 +30,7 @@ import type {
30
30
  Method,
31
31
  Metadata,
32
32
  Message,
33
+ ResponseItem,
33
34
  ClientMiddleware,
34
35
  ClientContext,
35
36
  ClientRunner,
@@ -56,8 +57,19 @@ import { RouteResolver, type ResolvedRoute, type RouteResolutionResult } from ".
56
57
  import { BatchExecutor, type ExecutionContext } from "./batch-executor.js";
57
58
  import type { ProcedureRegistry } from "../procedures/registry.js";
58
59
  import type { EventBus } from "../events/types.js";
60
+ import {
61
+ remoteEvents,
62
+ subscribeRemote,
63
+ type EventTransport,
64
+ type RemoteEvent,
65
+ type RemoteEventHandler,
66
+ type RemoteEventsOptions,
67
+ type SubscribeOptions,
68
+ type Subscription,
69
+ } from "../events/remote.js";
59
70
  import { narrowCapability } from "../procedures/capability.js";
60
- import { TIMEOUT_METADATA_KEY, absorbTimeout, writeRemainingTime } from "./deadline.js";
71
+ import { DEADLINE_METADATA_KEY, TIMEOUT_METADATA_KEY, absorbTimeout, writeRemainingTime } from "./deadline.js";
72
+ import { ms } from "../duration.js";
61
73
  import type { Capability, ClearanceOrder, NestedCallOptions } from "../procedures/types.js";
62
74
  import { PROCEDURE_REGISTRY } from "../procedures/registry.js";
63
75
  import { validateSchema } from "../procedures/schema.js";
@@ -169,6 +181,12 @@ function callerOf(options: ExecOptions): CallerContext {
169
181
  return caller;
170
182
  }
171
183
 
184
+ /**
185
+ * The timeouts of a subscription: none. The timeout middleware takes 0 as "no timeout", so the
186
+ * timeouts of the client do not end a subscription, which has no end (roadmap CLI-15).
187
+ */
188
+ const NO_SUBSCRIPTION_TIMEOUT = Object.freeze({ overall: ms(0), perAttempt: ms(0) });
189
+
172
190
  /**
173
191
  * Generate a unique message ID.
174
192
  */
@@ -221,6 +239,8 @@ export class Client<TContext = {}> {
221
239
  private readonly throwOnError: boolean;
222
240
  /** The bus of the local procedures. Undefined: the global bus. */
223
241
  private bus: EventBus | undefined;
242
+ /** The transport of the subscriptions through the middleware, made at the first subscription */
243
+ private subscriptionTransport: EventTransport | undefined;
224
244
  /** The interceptors of the local invocations (roadmap 1.2) */
225
245
  private interceptors: readonly Interceptor[] = [];
226
246
  /** The capability of the local invocations (CAPABILITIES-2026-10). Undefined: no limits. */
@@ -779,6 +799,68 @@ export class Client<TContext = {}> {
779
799
  }
780
800
  }
781
801
 
802
+ /**
803
+ * This method subscribes to topics of the event bus of the server (`documentation/EVENTS.md`).
804
+ * The call of `events.subscribe` goes through the middleware and the context of this client, as
805
+ * `stream()` does. Thus a token of the auth middleware goes with it. The subscriber resubscribes
806
+ * after a failure that a retry can repair, and the subscribers of the same topics share one call.
807
+ *
808
+ * A subscription has no deadline: the timeouts of the client (its timeout middleware, the
809
+ * `timeout` of its context and a `procedure-timeout` of its metadata) do not apply to it. For a
810
+ * deadline, give it in the metadata of the options, for example
811
+ * `{ metadata: { timeout: { overall: minutes(5) } } }`.
812
+ *
813
+ * @param topics - One topic, or a list of 1 to 64 topics
814
+ * @param handler - The handler of each event. A promise that it gives holds the next event.
815
+ * @param options - `onSubscribed`, `onError`, a signal, the metadata of the call, the resubscribe rules
816
+ * @returns The unsubscribe function, with the `ready` promise of the first confirmation
817
+ *
818
+ * @example
819
+ * ```typescript
820
+ * const unsubscribe = client.subscribe("collections.users", (event) => console.log(event));
821
+ * await unsubscribe.ready;
822
+ * unsubscribe();
823
+ * ```
824
+ */
825
+ subscribe<T = unknown>(topics: string | readonly string[], handler: RemoteEventHandler<T>, options: SubscribeOptions = {}): Subscription {
826
+ return subscribeRemote<T>(this.eventTransport(), topics, handler, options);
827
+ }
828
+
829
+ /**
830
+ * This method gives the events of topics of the event bus of the server as an async iterator,
831
+ * through the middleware of this client. A `break` or the signal ends the subscription. As for
832
+ * `subscribe()`, the timeouts of the client do not apply: the metadata of the options can give one.
833
+ *
834
+ * @param topics - One topic, or a list of 1 to 64 topics
835
+ * @param options - A signal, `onSubscribed`, the metadata of the call, the resubscribe rules
836
+ */
837
+ events<T = unknown>(topics: string | readonly string[], options: RemoteEventsOptions = {}): AsyncIterable<RemoteEvent<T>> {
838
+ return remoteEvents<T>(this.eventTransport(), topics, options);
839
+ }
840
+
841
+ /**
842
+ * The transport of the subscriptions of this client: each call goes through the middleware, with
843
+ * the context of the client in its metadata. The subscriptions of one client share it, so they
844
+ * share their calls. The timeouts and the deadline of the client stay out: only the metadata of
845
+ * the subscription can give them.
846
+ */
847
+ private eventTransport(): EventTransport {
848
+ if (!this.subscriptionTransport) {
849
+ const send = <TReq, TRes>(message: Message<TReq>): AsyncIterable<ResponseItem<TRes>> => {
850
+ const metadata: Metadata = { ...this.defaultMetadata, ...(this.getEffectiveContext() as object) };
851
+ delete metadata["timeout"];
852
+ delete metadata[TIMEOUT_METADATA_KEY];
853
+ delete metadata[DEADLINE_METADATA_KEY];
854
+ metadata["timeout"] = NO_SUBSCRIPTION_TIMEOUT as unknown as NonNullable<Metadata["timeout"]>;
855
+ Object.assign(metadata, message.metadata);
856
+ absorbTimeout(metadata);
857
+ return this.composeMiddleware<TReq, TRes>()({ message: { ...message, metadata } });
858
+ };
859
+ this.subscriptionTransport = { send };
860
+ }
861
+ return this.subscriptionTransport;
862
+ }
863
+
782
864
  /**
783
865
  * Compose middleware chain using universal middleware composition.
784
866
  *
@@ -8,11 +8,17 @@
8
8
  *
9
9
  * - The topic rule (`topics`) is the expose rule of the topics. A topic outside it gives
10
10
  * `TOPIC_NOT_FOUND`.
11
+ * - `reads` gives the resources that a subscriber of a topic reads. The capability of the call
12
+ * must permit them, as for a procedure that reads them (roadmap CLI-15). By default, a topic
13
+ * `collections.<name>` reads `collection:<name>`.
11
14
  * - `allow` examines each topic with the capability of the call. A refusal gives `FORBIDDEN` with
12
15
  * the reason `topic-not-allowed`.
13
16
  * - The procedure declares that it reads the resource `events`, so the capability of the host
14
17
  * applies to it as to each procedure.
15
18
  *
19
+ * - A value with rows (`withRows()`) reaches each subscriber through the output filter of its host
20
+ * (`ctx.admit`): each row passes the filter, and the subscriber gets only the admitted rows.
21
+ *
16
22
  * The delivery is at most once. The queue of a subscription holds at most `buffer` events. When
17
23
  * it is full, the publisher drops the oldest event, and the next event tells the number of lost
18
24
  * events (`dropped`). The bus never waits for a subscriber.
@@ -21,10 +27,15 @@
21
27
  import { z } from "zod";
22
28
  import { createProcedure } from "../procedures/define.js";
23
29
  import { InvocationError } from "../procedures/invoke.js";
30
+ import { capabilityRefusal } from "../procedures/capability.js";
31
+ import { collectionChangeTopic, collectionTopicReads } from "../procedures/collection/changes.js";
32
+ import { collectionTopicPartition, partitionOfCall } from "../procedures/collection/partition.js";
24
33
  import type { AnyProcedure, Capability, ProcedureContext } from "../procedures/types.js";
34
+ import type { Principal } from "../server/types.js";
25
35
  import type { EventBus, Unsubscribe } from "./types.js";
26
36
  import { getGlobalEventBus } from "./bus.js";
27
37
  import { EVENTS_SUBSCRIBE_PATH, type EventItem, type SubscribeInput } from "./remote.js";
38
+ import { admittedValue, rowsOf } from "./rows.js";
28
39
 
29
40
  /** The number of topics that one subscription can name. */
30
41
  export const MAX_SUBSCRIPTION_TOPICS = 64;
@@ -44,6 +55,8 @@ export interface TopicCall {
44
55
  readonly capability: Capability;
45
56
  /** The metadata of the call: the connection id and the headers of a network request. */
46
57
  readonly metadata: Readonly<Record<string, unknown>>;
58
+ /** The principal of the call, from the authentication of the host. */
59
+ readonly principal?: Principal | undefined;
47
60
  }
48
61
 
49
62
  /** The options of an `EventPublisher`. */
@@ -64,6 +77,18 @@ export interface EventPublisherOptions {
64
77
  * @default 1024
65
78
  */
66
79
  buffer?: number | undefined;
80
+ /**
81
+ * The resources that a subscriber of a topic reads (roadmap CLI-15). The publisher refuses a
82
+ * topic whose reads the capability of the call does not permit, as for a procedure that reads
83
+ * them: `FORBIDDEN` with the reason `effects-exceeded`.
84
+ * @default `collectionTopicReads`: a topic `collections.<name>` reads `collection:<name>`
85
+ */
86
+ reads?: ((topic: string) => readonly string[]) | undefined;
87
+ }
88
+
89
+ /** A procedure that reads resources, for the admission of a topic under a capability. */
90
+ function topicReader(reads: readonly string[]): AnyProcedure {
91
+ return { path: [...EVENTS_SUBSCRIBE_PATH], metadata: { effects: { reads: [...reads], writes: [] } } } as unknown as AnyProcedure;
67
92
  }
68
93
 
69
94
  /** This function gives the test of a topic rule. */
@@ -131,22 +156,27 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
131
156
  private readonly queue: Array<Extract<EventItem, { type: "event" }>> = [];
132
157
  private seq = 0;
133
158
  private dropped = 0;
134
- private waiter: { resolve: (result: IteratorResult<EventItem>) => void; reject: (error: unknown) => void } | undefined;
159
+ private waiter:
160
+ | { resolve: (result: IteratorResult<EventItem> | PromiseLike<IteratorResult<EventItem>>) => void; reject: (error: unknown) => void }
161
+ | undefined;
135
162
  private closed = false;
136
163
  private failure: unknown;
137
164
  private readonly unsubscribes: Unsubscribe[];
138
165
 
139
166
  constructor(
140
167
  bus: EventBus,
141
- topics: readonly string[],
168
+ /** The topics of the subscriber, each with the topic of the bus that it listens to */
169
+ routes: ReadonlyArray<readonly [topic: string, busTopic: string]>,
142
170
  private readonly buffer: number,
143
171
  private readonly signal: AbortSignal | undefined,
144
172
  private readonly onClose: (subscription: TopicSubscription) => void,
173
+ /** The output filter of the host for this subscriber: each row of a value passes it */
174
+ private readonly admit: ((item: unknown) => Promise<boolean>) | undefined,
145
175
  ) {
146
- this.confirmation = { type: "subscribed", topics };
176
+ this.confirmation = { type: "subscribed", topics: routes.map(([topic]) => topic) };
147
177
  // Before, clear() removed the listeners, and the subscriber saw no end (EVENTS.md)
148
- this.unsubscribes = topics.map((topic) =>
149
- bus.on(topic, (data) => this.push(topic, data), { onClear: () => this.fail(clearedError(topic), { keepQueue: true }) }),
178
+ this.unsubscribes = routes.map(([topic, busTopic]) =>
179
+ bus.on(busTopic, (data) => this.push(topic, data), { onClear: () => this.fail(clearedError(topic), { keepQueue: true }) }),
150
180
  );
151
181
  signal?.addEventListener("abort", this.onAbort, { once: true });
152
182
  }
@@ -166,7 +196,7 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
166
196
  const waiter = this.waiter;
167
197
  if (waiter && this.queue.length === 0) {
168
198
  this.waiter = undefined;
169
- waiter.resolve({ value: this.withDropped(event), done: false });
199
+ waiter.resolve(this.deliver(event));
170
200
  return;
171
201
  }
172
202
  if (this.queue.length >= this.buffer) {
@@ -177,6 +207,16 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
177
207
  this.queue.push(event);
178
208
  }
179
209
 
210
+ /**
211
+ * The item of an event for the subscriber. The rows of its data pass the output filter of the
212
+ * host for this subscriber, and the subscriber gets only the admitted rows.
213
+ */
214
+ private deliver(event: Extract<EventItem, { type: "event" }>): Promise<IteratorResult<EventItem>> {
215
+ const item = this.withDropped(event) as Extract<EventItem, { type: "event" }>;
216
+ if (!this.admit || !rowsOf(item.data)) return Promise.resolve({ value: item, done: false });
217
+ return admittedValue(item.data, this.admit).then((data): IteratorResult<EventItem> => ({ value: { ...item, data }, done: false }));
218
+ }
219
+
180
220
  /** The event with the number of events that the publisher dropped before it. */
181
221
  private withDropped(event: Extract<EventItem, { type: "event" }>): EventItem {
182
222
  if (this.dropped === 0) return event;
@@ -192,7 +232,7 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
192
232
  return Promise.resolve({ value: confirmation, done: false });
193
233
  }
194
234
  const event = this.queue.shift();
195
- if (event) return Promise.resolve({ value: this.withDropped(event), done: false });
235
+ if (event) return this.deliver(event);
196
236
  if (this.failure !== undefined) {
197
237
  const failure = this.failure;
198
238
  this.failure = undefined;
@@ -268,6 +308,7 @@ export class EventPublisher {
268
308
  readonly procedure: AnyProcedure;
269
309
  private readonly known: (topic: string) => boolean;
270
310
  private readonly allow: EventPublisherOptions["allow"];
311
+ private readonly reads: (topic: string) => readonly string[];
271
312
  private readonly buffer: number;
272
313
  private readonly open = new Set<TopicSubscription>();
273
314
 
@@ -275,6 +316,7 @@ export class EventPublisher {
275
316
  this.bus = options.bus ?? getGlobalEventBus();
276
317
  this.known = topicMatcher(options.topics);
277
318
  this.allow = options.allow;
319
+ this.reads = options.reads ?? collectionTopicReads;
278
320
  this.buffer = Math.max(1, Math.floor(options.buffer ?? 1024));
279
321
  this.procedure = createProcedure()
280
322
  .path([...EVENTS_SUBSCRIBE_PATH])
@@ -307,24 +349,46 @@ export class EventPublisher {
307
349
  for (const subscription of [...this.open]) subscription.fail(stoppedError());
308
350
  }
309
351
 
352
+ /**
353
+ * The topic of the bus for a topic of a subscriber (roadmap CLI-17). A topic of a partitioned
354
+ * collection names the partition that the subscriber asks for, as the input of a procedure does:
355
+ * the resolver of the repository gives the partition, so a subscriber cannot name the topic of
356
+ * another tenant. A collection without partitions refuses a topic with a partition.
357
+ */
358
+ private async busTopic(topic: string, ctx: ProcedureContext): Promise<string> {
359
+ const parts = collectionTopicPartition(topic);
360
+ if (!parts) return topic;
361
+ const partition = await partitionOfCall(ctx, parts.collection, parts.partition);
362
+ return collectionChangeTopic(parts.collection, partition);
363
+ }
364
+
310
365
  /**
311
366
  * The stream of one subscription. The publisher examines each topic before it adds a listener:
312
- * the topic rule first, then `allow`. A call with one bad topic makes no subscription.
367
+ * the topic rule, the reads, `allow`, and the partition. A call with one bad topic makes no subscription.
313
368
  */
314
- private subscribe(topics: readonly string[], ctx: ProcedureContext): TopicSubscription {
369
+ private async subscribe(topics: readonly string[], ctx: ProcedureContext): Promise<TopicSubscription> {
315
370
  const unique = [...new Set(topics)];
316
371
  for (const topic of unique) {
317
372
  if (!this.known(topic)) throw new TopicNotFoundError(`The host has no topic "${topic}"`);
318
373
  }
374
+ // A subscriber of a topic reads its resources: the capability of the call must permit them
375
+ for (const topic of unique) {
376
+ const reads = this.reads(topic);
377
+ if (reads.length === 0) continue;
378
+ const refusal = capabilityRefusal(topicReader(reads), ctx.capability);
379
+ if (refusal) throw new InvocationError("FORBIDDEN", `The topic "${topic}": ${refusal.message}`, ctx.path, refusal.reason);
380
+ }
319
381
  if (this.allow) {
320
- const call: TopicCall = { capability: ctx.capability, metadata: ctx.metadata };
382
+ const call: TopicCall = { capability: ctx.capability, metadata: ctx.metadata, ...(ctx.principal ? { principal: ctx.principal } : {}) };
321
383
  for (const topic of unique) {
322
384
  if (!this.allow(topic, call)) {
323
385
  throw new InvocationError("FORBIDDEN", `The capability of the call does not permit the topic "${topic}"`, ctx.path, "topic-not-allowed");
324
386
  }
325
387
  }
326
388
  }
327
- const subscription = new TopicSubscription(this.bus, unique, this.buffer, ctx.signal, (closed) => this.open.delete(closed));
389
+ const routes: Array<readonly [string, string]> = [];
390
+ for (const topic of unique) routes.push([topic, await this.busTopic(topic, ctx)]);
391
+ const subscription = new TopicSubscription(this.bus, routes, this.buffer, ctx.signal, (closed) => this.open.delete(closed), ctx.admit);
328
392
  this.open.add(subscription);
329
393
  return subscription.closeIfAborted();
330
394
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The rows of a published value (roadmap CLI-15).
3
+ *
4
+ * A value on the bus can hold rows of data: the values of a collection change event, for example.
5
+ * The publisher of the remote events gives each row to the output filter of the host (`admitItem`)
6
+ * for each subscriber, and each subscriber gets only the rows that its filter admits. Thus a row
7
+ * reaches a subscriber only through the same filter as a read.
8
+ *
9
+ * The rows are on a symbol key, so they stay in this process: JSON does not take them.
10
+ */
11
+
12
+ /** The rows of a published value, and the function that makes the value of the admitted rows. */
13
+ export interface PublishedRows {
14
+ /** The rows that the output filter examines, one at a time. */
15
+ readonly rows: readonly unknown[];
16
+ /** This function gives the value for a subscriber: `admitted[i]` tells if row `i` stays. */
17
+ keep(admitted: readonly boolean[]): unknown;
18
+ }
19
+
20
+ /** The key of the rows of a published value. */
21
+ const ROWS: unique symbol = Symbol.for("procedures.publishedRows");
22
+
23
+ /**
24
+ * This function gives the value with its rows, for the filter of each subscriber. The rows are not
25
+ * enumerable: a copy or a comparison of the value does not see them.
26
+ */
27
+ export function withRows<T extends object>(value: T, rows: PublishedRows): T {
28
+ return Object.defineProperty(value, ROWS, { value: rows, enumerable: false });
29
+ }
30
+
31
+ /** The rows of a published value, or undefined for a value without rows. */
32
+ export function rowsOf(value: unknown): PublishedRows | undefined {
33
+ if (typeof value !== "object" || value === null) return undefined;
34
+ const rows = (value as { [ROWS]?: unknown })[ROWS];
35
+ return typeof rows === "object" && rows !== null ? (rows as PublishedRows) : undefined;
36
+ }
37
+
38
+ /**
39
+ * The value of a published value for one subscriber: each row passes the filter, and the value
40
+ * keeps the admitted rows. An error of the filter refuses the row. Without rows or a filter, the
41
+ * value stays as it is.
42
+ */
43
+ export async function admittedValue(value: unknown, admit: ((item: unknown) => Promise<boolean>) | undefined): Promise<unknown> {
44
+ const rows = rowsOf(value);
45
+ if (!rows || !admit) return value;
46
+ const admitted = await Promise.all(rows.rows.map((row) => admit(row).catch(() => false)));
47
+ return rows.keep(admitted);
48
+ }
package/src/index.ts CHANGED
@@ -27,7 +27,7 @@ export type {
27
27
  HttpUrlStrategy,
28
28
  } from "./server/index.js";
29
29
  export { WebSocketServerTransport } from "./server/index.js";
30
- export { checkBrowserRequest, isLoopbackHost, isLoopbackAddress, hostNameOf } from "./server/browser-guard.js";
30
+ export { checkBrowserRequest, hasAmbientCredentials, isLoopbackHost, isLoopbackAddress, hostNameOf } from "./server/browser-guard.js";
31
31
  export type { BrowserGuardOptions, GuardedRequest, GuardResult } from "./server/browser-guard.js";
32
32
  export type {
33
33
  WebSocketServerTransportOptions,
package/src/node.ts CHANGED
@@ -9,9 +9,30 @@
9
9
  * This entry also exports `writeNdjsonStream`: the writer of an NDJSON stream response of wire
10
10
  * protocol v1, for a `ServerResponse` of `node:http`. `HttpServerTransport` uses it, so a server
11
11
  * without Express writes the same bytes.
12
+ *
13
+ * The procedure faces of Node, without a framework (CLI-3): `procedureHandler(server, options)`
14
+ * gives a request handler `(req, res) => boolean`, and `upgradeHandler(server, options)` gives an
15
+ * upgrade handler `(req, socket, head) => boolean`. Each one takes the requests of its path and
16
+ * gives `false` for the others, so the routes of the application share the listener.
12
17
  */
13
18
 
14
19
  export { WebSocketTransport } from "./adapters/websocket/client/node.js";
15
20
  export type { WebSocketTransportOptions, WebSocketClass } from "./adapters/websocket/client/types.js";
16
21
  export { writeNdjsonStream } from "./adapters/http/server/ndjson.js";
17
22
  export type { WriteNdjsonStreamOptions, NdjsonError } from "./adapters/http/server/ndjson.js";
23
+ export { procedureHandler } from "./adapters/http/server/handler.js";
24
+ export type { ProcedureHandlerOptions, NodeProcedureHandler, NodeUrlStrategy, UrlRequest } from "./adapters/http/server/handler.js";
25
+ export type { CorsOptions } from "./adapters/http/server/cors.js";
26
+ export type { RoutedRequest } from "./adapters/http/server/strategies.js";
27
+ export { defaultServerUrlStrategy, rpcServerUrlStrategy } from "./adapters/http/server/strategies.js";
28
+ export { upgradeHandler } from "./adapters/websocket/server/upgrade.js";
29
+ export type { UpgradeHandlerOptions, NodeUpgradeHandler } from "./adapters/websocket/server/upgrade.js";
30
+ export { WebSocketServerTransport } from "./adapters/websocket/server/transport.js";
31
+ export type {
32
+ WebSocketServerTransportOptions,
33
+ WebSocketAuthHandler,
34
+ WebSocketConnectionHandler,
35
+ WebSocketMessage,
36
+ TrackedConnection,
37
+ ConnectionEventHandler,
38
+ } from "./adapters/websocket/server/types.js";
@@ -178,6 +178,27 @@ export function narrowCapability<C>(
178
178
  return Object.freeze(result) as Capability<C>;
179
179
  }
180
180
 
181
+ /**
182
+ * This function joins two requests for a narrower capability into one request that asks for
183
+ * both: the meet of the clearances, the intersection of the effects and the shorter time. Two
184
+ * different clearances need an order.
185
+ */
186
+ export function joinNarrowing<C>(first: CapabilityRequest<C>, second: CapabilityRequest<C>, order?: ClearanceOrder<C>): CapabilityRequest<C> {
187
+ const result: CapabilityRequest<C> = {};
188
+ if (first.clearance === undefined || second.clearance === undefined || Object.is(first.clearance, second.clearance)) {
189
+ const clearance = first.clearance ?? second.clearance;
190
+ if (clearance !== undefined) result.clearance = clearance;
191
+ } else {
192
+ if (!order) throw new Error("Two different clearances need a clearance order: give clearanceOrder to the host (hostPolicy)");
193
+ result.clearance = order.meet(first.clearance, second.clearance);
194
+ }
195
+ const effects = first.effects === undefined ? second.effects : narrowEffects(first.effects, second.effects);
196
+ if (effects !== undefined) result.effects = effects;
197
+ const times = [first.timeoutMs, second.timeoutMs].filter((time): time is number => typeof time === "number" && Number.isFinite(time) && time >= 0);
198
+ if (times.length > 0) result.timeoutMs = Math.min(...times);
199
+ return result;
200
+ }
201
+
181
202
  // =============================================================================
182
203
  // Admission
183
204
  // =============================================================================