@procedures/client 0.15.0 → 0.16.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 (85) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapters/http/server/handler.d.ts.map +1 -1
  4. package/dist/adapters/http/server/handler.js +5 -3
  5. package/dist/adapters/http/server/handler.js.map +1 -1
  6. package/dist/browser.d.ts +3 -2
  7. package/dist/browser.d.ts.map +1 -1
  8. package/dist/browser.js +1 -1
  9. package/dist/browser.js.map +1 -1
  10. package/dist/client/errors/registry.d.ts.map +1 -1
  11. package/dist/client/errors/registry.js +12 -0
  12. package/dist/client/errors/registry.js.map +1 -1
  13. package/dist/client/middleware/rate-limit.d.ts +17 -0
  14. package/dist/client/middleware/rate-limit.d.ts.map +1 -1
  15. package/dist/client/middleware/rate-limit.js +94 -39
  16. package/dist/client/middleware/rate-limit.js.map +1 -1
  17. package/dist/events/index.d.ts +1 -0
  18. package/dist/events/index.d.ts.map +1 -1
  19. package/dist/events/index.js.map +1 -1
  20. package/dist/events/publisher.d.ts +50 -5
  21. package/dist/events/publisher.d.ts.map +1 -1
  22. package/dist/events/publisher.js +99 -10
  23. package/dist/events/publisher.js.map +1 -1
  24. package/dist/events/recheck.d.ts +84 -0
  25. package/dist/events/recheck.d.ts.map +1 -0
  26. package/dist/events/recheck.js +135 -0
  27. package/dist/events/recheck.js.map +1 -0
  28. package/dist/procedures/collection/changes.d.ts +9 -8
  29. package/dist/procedures/collection/changes.d.ts.map +1 -1
  30. package/dist/procedures/collection/changes.js +11 -11
  31. package/dist/procedures/collection/changes.js.map +1 -1
  32. package/dist/procedures/collection/index.d.ts +1 -0
  33. package/dist/procedures/collection/index.d.ts.map +1 -1
  34. package/dist/procedures/collection/index.js +2 -0
  35. package/dist/procedures/collection/index.js.map +1 -1
  36. package/dist/procedures/collection/partition.d.ts +4 -23
  37. package/dist/procedures/collection/partition.d.ts.map +1 -1
  38. package/dist/procedures/collection/partition.js +2 -28
  39. package/dist/procedures/collection/partition.js.map +1 -1
  40. package/dist/procedures/collection/topic.d.ts +86 -0
  41. package/dist/procedures/collection/topic.d.ts.map +1 -0
  42. package/dist/procedures/collection/topic.js +94 -0
  43. package/dist/procedures/collection/topic.js.map +1 -0
  44. package/dist/procedures/index.d.ts +1 -1
  45. package/dist/procedures/index.d.ts.map +1 -1
  46. package/dist/procedures/index.js +1 -1
  47. package/dist/procedures/index.js.map +1 -1
  48. package/dist/protocol.d.ts +1 -1
  49. package/dist/protocol.d.ts.map +1 -1
  50. package/dist/protocol.js +1 -0
  51. package/dist/protocol.js.map +1 -1
  52. package/dist/server/host-policy.d.ts +17 -0
  53. package/dist/server/host-policy.d.ts.map +1 -1
  54. package/dist/server/host-policy.js +7 -0
  55. package/dist/server/host-policy.js.map +1 -1
  56. package/dist/server/limits.d.ts +122 -0
  57. package/dist/server/limits.d.ts.map +1 -0
  58. package/dist/server/limits.js +153 -0
  59. package/dist/server/limits.js.map +1 -0
  60. package/dist/server/procedure-server.d.ts +35 -3
  61. package/dist/server/procedure-server.d.ts.map +1 -1
  62. package/dist/server/procedure-server.js +42 -8
  63. package/dist/server/procedure-server.js.map +1 -1
  64. package/dist/server/request-auth.d.ts +5 -1
  65. package/dist/server/request-auth.d.ts.map +1 -1
  66. package/dist/server/request-auth.js +19 -2
  67. package/dist/server/request-auth.js.map +1 -1
  68. package/package.json +3 -3
  69. package/src/adapters/http/server/handler.ts +5 -3
  70. package/src/browser.ts +5 -0
  71. package/src/client/errors/registry.ts +13 -0
  72. package/src/client/middleware/rate-limit.ts +116 -41
  73. package/src/events/index.ts +1 -0
  74. package/src/events/publisher.ts +121 -12
  75. package/src/events/recheck.ts +182 -0
  76. package/src/procedures/collection/changes.ts +12 -11
  77. package/src/procedures/collection/index.ts +2 -0
  78. package/src/procedures/collection/partition.ts +5 -27
  79. package/src/procedures/collection/topic.ts +115 -0
  80. package/src/procedures/index.ts +1 -0
  81. package/src/protocol.ts +3 -1
  82. package/src/server/host-policy.ts +22 -0
  83. package/src/server/limits.ts +254 -0
  84. package/src/server/procedure-server.ts +59 -9
  85. package/src/server/request-auth.ts +19 -2
@@ -33,6 +33,8 @@ import { getGlobalEventBus } from "../events/bus.js";
33
33
  import type { CollectionProceduresOptions } from "../procedures/collection/procedures.js";
34
34
  import type { NarrowingNote } from "../procedures/narrowing.js";
35
35
  import type { ServerDebugReport } from "./types.js";
36
+ import type { KeyedLimiter, LimiterStore } from "@mark1russell7/service/limiter";
37
+ import { callLimiter, limitedCall, rateLimitedResponse, type CallLimit, type CallLimiter } from "./limits.js";
36
38
 
37
39
  // =============================================================================
38
40
  // Types
@@ -169,7 +171,8 @@ export interface ProcedureServerOptions extends ServerOptions {
169
171
  * The remote events of the server (`documentation/EVENTS.md`): a bus and its topic rule, or an
170
172
  * `EventPublisher`. The server pins the procedure `events.subscribe`, outside its registry, so a
171
173
  * peer can subscribe to the topics. The procedures of the server get this bus as `ctx.bus`.
172
- * `stop()` ends each subscription first, with `UNAVAILABLE`.
174
+ * `stop()` ends each subscription first, with `UNAVAILABLE`. A publisher that the server makes
175
+ * from the options gets the clock of the server, unless the options give a clock (CLI-44).
173
176
  */
174
177
  events?: EventPublisherOptions | EventPublisher | undefined;
175
178
 
@@ -181,6 +184,21 @@ export interface ProcedureServerOptions extends ServerOptions {
181
184
  * @default false
182
185
  */
183
186
  debug?: boolean | ((report: ServerDebugReport) => void) | undefined;
187
+
188
+ /**
189
+ * The limits of the calls (CLI-40). Each limit is a keyed limiter of
190
+ * `@mark1russell7/service/limiter` on the clock of the server. The server takes one unit of each
191
+ * limit that covers a call, after the authentication and before the run, for each face. A
192
+ * refusal is `RATE_LIMITED` with the wait in `retry-after`: HTTP 429 with `Retry-After`. Without
193
+ * this option, the server limits nothing.
194
+ *
195
+ * The server takes the units in the sequence of the limits, and the first refusal stops the call.
196
+ * A limit before the refusal keeps its unit: the server does not give it back.
197
+ */
198
+ limits?: readonly CallLimit[] | undefined;
199
+
200
+ /** The stores that the replicas share, by the name of their dependency, for each limit with a shared store. */
201
+ limitStores?: Readonly<Record<string, LimiterStore>> | undefined;
184
202
  }
185
203
 
186
204
  /** The options of `registerProcedure()`, `registerProcedures()` and `registerModule()` (CLI-47). */
@@ -439,6 +457,8 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
439
457
  readonly ambientCredentials: AmbientCredentials | undefined;
440
458
  /** The receiver of the debug reports, or undefined when the option `debug` is off. */
441
459
  private readonly debugReport: ((report: ServerDebugReport) => void) | undefined;
460
+ /** The limits of the calls (CLI-40), or undefined without the option `limits`. */
461
+ private readonly callLimits: CallLimiter | undefined;
442
462
 
443
463
  constructor(options: ProcedureServerOptions = {}) {
444
464
  super(options);
@@ -466,9 +486,13 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
466
486
  : options.debug === true
467
487
  ? (report) => console.debug(`[ProcedureServer] ${report.topic} of ${report.requestId}: ${report.message}`)
468
488
  : undefined;
489
+ // The limits of the calls: one keyed limiter for each limit, on the clock of the server (CLI-40)
490
+ this.callLimits = options.limits && options.limits.length > 0 ? callLimiter(options.limits, { clock: this.clock, stores: options.limitStores }) : undefined;
469
491
  // The remote events: the server pins events.subscribe, as a host pins host.health (EVENTS.md).
470
- // The topic rule is its expose rule, so the pin is always on (CLI-47).
471
- this.publisher = options.events instanceof EventPublisher ? options.events : options.events ? new EventPublisher(options.events) : undefined;
492
+ // The topic rule is its expose rule, so the pin is always on (CLI-47). The publisher that the
493
+ // server makes gets the clock of the server for its re-checks (CLI-44).
494
+ const events = options.events;
495
+ this.publisher = events instanceof EventPublisher ? events : events ? new EventPublisher({ ...events, clock: events.clock ?? this.clock }) : undefined;
472
496
  if (this.publisher) this.registerProcedure(this.publisher.procedure, { alwaysOn: true });
473
497
 
474
498
  // Configure collections
@@ -550,16 +574,14 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
550
574
  * for: the collection procedures and the subscriptions of `EventPublisher` use it. `storageOf`
551
575
  * gives the storage of a partition: give the same storage for the same partition each time.
552
576
  *
553
- * @param name - Collection name, without a dot
577
+ * The name can have a dot (roadmap CLI-42). The topic of a partition encodes the name, so the
578
+ * name and the partition of a topic stay apart (`collectionChangeTopic()`).
579
+ *
580
+ * @param name - Collection name
554
581
  * @param partitions - The storage of each partition, and the resolver
555
582
  * @returns this (for chaining)
556
- * @throws Error if the name has a dot
557
583
  */
558
584
  usePartitions<T>(name: string, partitions: CollectionPartitions<T>): this {
559
- // The topic `collections.<name>.<partition>` names the partition after the first dot
560
- if (name.includes(".")) {
561
- throw new Error(`A collection with partitions cannot have a dot in its name: ${name}`);
562
- }
563
585
  this.partitions.set(name, partitions as CollectionPartitions<unknown>);
564
586
  return this;
565
587
  }
@@ -798,6 +820,9 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
798
820
  }
799
821
  const refusal = await this.admit(request);
800
822
  if (refusal) return refusal;
823
+ // The limits come after the authentication, so a key can read the principal (CLI-40)
824
+ const limited = await this.limit(procedure, request);
825
+ if (limited) return limited;
801
826
  this.calls++;
802
827
  let response: ServerResponse<unknown> | undefined;
803
828
  try {
@@ -873,6 +898,22 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
873
898
  return undefined;
874
899
  }
875
900
 
901
+ /**
902
+ * The limits of a call (CLI-40): one unit of each limit that covers it. The function gives a
903
+ * `RATE_LIMITED` response, or undefined when the call passes. A key or a cover function that
904
+ * throws gives `INTERNAL_ERROR`, so a defect of a limit does not let a call in.
905
+ */
906
+ private async limit(procedure: AnyProcedure, request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
907
+ if (!this.callLimits) return undefined;
908
+ try {
909
+ const refused = await this.callLimits.admit(limitedCall(procedure, request));
910
+ return refused ? rateLimitedResponse(request.id, refused) : undefined;
911
+ } catch (error) {
912
+ const message = `The limits of the server cannot admit the call: ${error instanceof Error ? error.message : String(error)}`;
913
+ return errorResponse(request.id, Object.assign(new Error(message), { code: "INTERNAL_ERROR" }));
914
+ }
915
+ }
916
+
876
917
  /** One call in flight ended: `drain()` can end when it was the last one. */
877
918
  private callEnded(): void {
878
919
  this.calls--;
@@ -1111,6 +1152,15 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
1111
1152
  return this.resolveProcedure(method);
1112
1153
  }
1113
1154
 
1155
+ /**
1156
+ * This method gives the keyed limiters of the option `limits`, in its sequence (CLI-40). A host
1157
+ * declares each one on its service with `svc.limiter(limiter.name, limiter.spec)`, so
1158
+ * `describe()` and `lint()` see it.
1159
+ */
1160
+ limiters(): readonly KeyedLimiter[] {
1161
+ return this.callLimits?.limiters ?? [];
1162
+ }
1163
+
1114
1164
  /**
1115
1165
  * This function publishes an event on the bus of the server: the bus of the option `events`, or
1116
1166
  * the global bus. Each subscription of the topic gets it (`documentation/EVENTS.md`).
@@ -40,9 +40,26 @@ export function withoutSecretHeaders(headers: Record<string, unknown>, secret?:
40
40
  return out;
41
41
  }
42
42
 
43
- /** The token of an `Authorization: Bearer <token>` header, or undefined. */
43
+ /** The start of the header that `bearerOf` read before CLI-60: the scheme `Bearer` and one space. */
44
+ const BEARER_PREFIX = "Bearer ";
45
+
46
+ /**
47
+ * The other forms of a bearer credential (RFC 7235 and RFC 6750, CLI-60). Such a form has the
48
+ * scheme `Bearer` in any case, one or more spaces (`1*SP`) and the token. Spaces or tabs can come
49
+ * after the token. The token has no whitespace (`token68`). A tab is not `1*SP`.
50
+ */
51
+ const BEARER_CREDENTIAL = /^bearer +(\S+)[ \t]*$/i;
52
+
53
+ /**
54
+ * The token of an `Authorization: Bearer <token>` header, or undefined. A header that starts
55
+ * with `Bearer ` gives the rest of the header, as before CLI-60. A header with the scheme in
56
+ * another case, for example `bearer`, gives the token after one or more spaces.
57
+ */
44
58
  export function bearerOf(header: unknown): string | undefined {
45
- return typeof header === "string" && header.startsWith("Bearer ") ? header.slice("Bearer ".length) : undefined;
59
+ if (typeof header !== "string") return undefined;
60
+ // The old form keeps its old result, so each header that passed before gives the same token
61
+ if (header.startsWith(BEARER_PREFIX)) return header.slice(BEARER_PREFIX.length);
62
+ return BEARER_CREDENTIAL.exec(header)?.[1];
46
63
  }
47
64
 
48
65
  /**