@procedures/client 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  4. package/dist/adapters/http/client/transport.js +4 -0
  5. package/dist/adapters/http/client/transport.js.map +1 -1
  6. package/dist/adapters/http/server/handler.d.ts.map +1 -1
  7. package/dist/adapters/http/server/handler.js +8 -3
  8. package/dist/adapters/http/server/handler.js.map +1 -1
  9. package/dist/adapters/local/client/transport.d.ts.map +1 -1
  10. package/dist/adapters/local/client/transport.js +5 -0
  11. package/dist/adapters/local/client/transport.js.map +1 -1
  12. package/dist/browser.d.ts +4 -3
  13. package/dist/browser.d.ts.map +1 -1
  14. package/dist/browser.js +2 -0
  15. package/dist/browser.js.map +1 -1
  16. package/dist/client/client.js +1 -1
  17. package/dist/client/client.js.map +1 -1
  18. package/dist/client/errors/registry.d.ts.map +1 -1
  19. package/dist/client/errors/registry.js +13 -0
  20. package/dist/client/errors/registry.js.map +1 -1
  21. package/dist/client/middleware/rate-limit.d.ts +17 -0
  22. package/dist/client/middleware/rate-limit.d.ts.map +1 -1
  23. package/dist/client/middleware/rate-limit.js +94 -39
  24. package/dist/client/middleware/rate-limit.js.map +1 -1
  25. package/dist/client/types.d.ts +6 -1
  26. package/dist/client/types.d.ts.map +1 -1
  27. package/dist/client/types.js +7 -1
  28. package/dist/client/types.js.map +1 -1
  29. package/dist/events/index.d.ts +1 -0
  30. package/dist/events/index.d.ts.map +1 -1
  31. package/dist/events/index.js.map +1 -1
  32. package/dist/events/publisher.d.ts +46 -1
  33. package/dist/events/publisher.d.ts.map +1 -1
  34. package/dist/events/publisher.js +95 -5
  35. package/dist/events/publisher.js.map +1 -1
  36. package/dist/events/recheck.d.ts +84 -0
  37. package/dist/events/recheck.d.ts.map +1 -0
  38. package/dist/events/recheck.js +135 -0
  39. package/dist/events/recheck.js.map +1 -0
  40. package/dist/procedures/collection/index.d.ts +3 -1
  41. package/dist/procedures/collection/index.d.ts.map +1 -1
  42. package/dist/procedures/collection/index.js +2 -0
  43. package/dist/procedures/collection/index.js.map +1 -1
  44. package/dist/procedures/collection/precondition.d.ts +111 -0
  45. package/dist/procedures/collection/precondition.d.ts.map +1 -0
  46. package/dist/procedures/collection/precondition.js +153 -0
  47. package/dist/procedures/collection/precondition.js.map +1 -0
  48. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  49. package/dist/procedures/collection/procedures.js +16 -3
  50. package/dist/procedures/collection/procedures.js.map +1 -1
  51. package/dist/procedures/collection/query.d.ts +2 -0
  52. package/dist/procedures/collection/query.d.ts.map +1 -1
  53. package/dist/procedures/collection/query.js +3 -0
  54. package/dist/procedures/collection/query.js.map +1 -1
  55. package/dist/procedures/collection/schemas.d.ts +16 -0
  56. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  57. package/dist/procedures/collection/schemas.js +26 -5
  58. package/dist/procedures/collection/schemas.js.map +1 -1
  59. package/dist/procedures/collection/storage.d.ts +34 -6
  60. package/dist/procedures/collection/storage.d.ts.map +1 -1
  61. package/dist/procedures/collection/storage.js +3 -0
  62. package/dist/procedures/collection/storage.js.map +1 -1
  63. package/dist/procedures/index.d.ts +2 -2
  64. package/dist/procedures/index.d.ts.map +1 -1
  65. package/dist/procedures/index.js +1 -1
  66. package/dist/procedures/index.js.map +1 -1
  67. package/dist/protocol.d.ts +1 -1
  68. package/dist/protocol.d.ts.map +1 -1
  69. package/dist/protocol.js +2 -0
  70. package/dist/protocol.js.map +1 -1
  71. package/dist/server/host-policy.d.ts +17 -0
  72. package/dist/server/host-policy.d.ts.map +1 -1
  73. package/dist/server/host-policy.js +7 -0
  74. package/dist/server/host-policy.js.map +1 -1
  75. package/dist/server/limits.d.ts +122 -0
  76. package/dist/server/limits.d.ts.map +1 -0
  77. package/dist/server/limits.js +153 -0
  78. package/dist/server/limits.js.map +1 -0
  79. package/dist/server/procedure-server.d.ts +31 -1
  80. package/dist/server/procedure-server.d.ts.map +1 -1
  81. package/dist/server/procedure-server.js +43 -2
  82. package/dist/server/procedure-server.js.map +1 -1
  83. package/dist/server/request-auth.d.ts +5 -1
  84. package/dist/server/request-auth.d.ts.map +1 -1
  85. package/dist/server/request-auth.js +19 -2
  86. package/dist/server/request-auth.js.map +1 -1
  87. package/package.json +3 -3
  88. package/src/adapters/http/client/transport.ts +3 -0
  89. package/src/adapters/http/server/handler.ts +8 -3
  90. package/src/adapters/local/client/transport.ts +4 -0
  91. package/src/browser.ts +20 -0
  92. package/src/client/client.ts +1 -1
  93. package/src/client/errors/registry.ts +14 -0
  94. package/src/client/middleware/rate-limit.ts +116 -41
  95. package/src/client/types.ts +8 -0
  96. package/src/events/index.ts +1 -0
  97. package/src/events/publisher.ts +116 -6
  98. package/src/events/recheck.ts +182 -0
  99. package/src/procedures/collection/index.ts +17 -0
  100. package/src/procedures/collection/precondition.ts +180 -0
  101. package/src/procedures/collection/procedures.ts +17 -4
  102. package/src/procedures/collection/query.ts +16 -0
  103. package/src/procedures/collection/schemas.ts +43 -6
  104. package/src/procedures/collection/storage.ts +36 -6
  105. package/src/procedures/index.ts +15 -0
  106. package/src/protocol.ts +5 -1
  107. package/src/server/host-policy.ts +22 -0
  108. package/src/server/limits.ts +254 -0
  109. package/src/server/procedure-server.ts +59 -3
  110. package/src/server/request-auth.ts +19 -2
@@ -20,6 +20,7 @@ import { createCollectionProcedures } from "../procedures/collection/procedures.
20
20
  import { InvocationError, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
21
21
  import type { Capability, ClearanceOrder, ProcedureContext, ResourcesOf } from "../procedures/types.js";
22
22
  import type { CollectionPartitions } from "../procedures/collection/partition.js";
23
+ import { VERSION_CONFLICTS_METADATA_KEY, versionConflictsOf } from "../procedures/collection/precondition.js";
23
24
  import { TIMEOUT_METADATA_KEY, effectsOf, joinNarrowing, narrowCapability } from "../procedures/capability.js";
24
25
  import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptions } from "../procedures/idempotency.js";
25
26
  import { optionalMs, type Duration } from "../duration.js";
@@ -32,6 +33,8 @@ import { getGlobalEventBus } from "../events/bus.js";
32
33
  import type { CollectionProceduresOptions } from "../procedures/collection/procedures.js";
33
34
  import type { NarrowingNote } from "../procedures/narrowing.js";
34
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";
35
38
 
36
39
  // =============================================================================
37
40
  // Types
@@ -168,7 +171,8 @@ export interface ProcedureServerOptions extends ServerOptions {
168
171
  * The remote events of the server (`documentation/EVENTS.md`): a bus and its topic rule, or an
169
172
  * `EventPublisher`. The server pins the procedure `events.subscribe`, outside its registry, so a
170
173
  * peer can subscribe to the topics. The procedures of the server get this bus as `ctx.bus`.
171
- * `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).
172
176
  */
173
177
  events?: EventPublisherOptions | EventPublisher | undefined;
174
178
 
@@ -180,6 +184,21 @@ export interface ProcedureServerOptions extends ServerOptions {
180
184
  * @default false
181
185
  */
182
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;
183
202
  }
184
203
 
185
204
  /** The options of `registerProcedure()`, `registerProcedures()` and `registerModule()` (CLI-47). */
@@ -335,6 +354,9 @@ export function errorResponse(id: string, error: unknown): ServerResponse<unknow
335
354
  const retryAfterMs = (error as { retryAfterMs?: unknown } | undefined)?.retryAfterMs;
336
355
  const metadata: Record<string, unknown> = reason ? { reason } : {};
337
356
  if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) metadata["retry-after"] = retryAfterMs;
357
+ // A VERSION_CONFLICT error tells the current item of each conflict (roadmap CLI-16)
358
+ const conflicts = versionConflictsOf(error);
359
+ if (conflicts !== undefined) metadata[VERSION_CONFLICTS_METADATA_KEY] = conflicts;
338
360
  const code = errorCodeOf(error);
339
361
  return {
340
362
  id,
@@ -435,6 +457,8 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
435
457
  readonly ambientCredentials: AmbientCredentials | undefined;
436
458
  /** The receiver of the debug reports, or undefined when the option `debug` is off. */
437
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;
438
462
 
439
463
  constructor(options: ProcedureServerOptions = {}) {
440
464
  super(options);
@@ -462,9 +486,13 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
462
486
  : options.debug === true
463
487
  ? (report) => console.debug(`[ProcedureServer] ${report.topic} of ${report.requestId}: ${report.message}`)
464
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;
465
491
  // The remote events: the server pins events.subscribe, as a host pins host.health (EVENTS.md).
466
- // The topic rule is its expose rule, so the pin is always on (CLI-47).
467
- 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;
468
496
  if (this.publisher) this.registerProcedure(this.publisher.procedure, { alwaysOn: true });
469
497
 
470
498
  // Configure collections
@@ -794,6 +822,9 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
794
822
  }
795
823
  const refusal = await this.admit(request);
796
824
  if (refusal) return refusal;
825
+ // The limits come after the authentication, so a key can read the principal (CLI-40)
826
+ const limited = await this.limit(procedure, request);
827
+ if (limited) return limited;
797
828
  this.calls++;
798
829
  let response: ServerResponse<unknown> | undefined;
799
830
  try {
@@ -869,6 +900,22 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
869
900
  return undefined;
870
901
  }
871
902
 
903
+ /**
904
+ * The limits of a call (CLI-40): one unit of each limit that covers it. The function gives a
905
+ * `RATE_LIMITED` response, or undefined when the call passes. A key or a cover function that
906
+ * throws gives `INTERNAL_ERROR`, so a defect of a limit does not let a call in.
907
+ */
908
+ private async limit(procedure: AnyProcedure, request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
909
+ if (!this.callLimits) return undefined;
910
+ try {
911
+ const refused = await this.callLimits.admit(limitedCall(procedure, request));
912
+ return refused ? rateLimitedResponse(request.id, refused) : undefined;
913
+ } catch (error) {
914
+ const message = `The limits of the server cannot admit the call: ${error instanceof Error ? error.message : String(error)}`;
915
+ return errorResponse(request.id, Object.assign(new Error(message), { code: "INTERNAL_ERROR" }));
916
+ }
917
+ }
918
+
872
919
  /** One call in flight ended: `drain()` can end when it was the last one. */
873
920
  private callEnded(): void {
874
921
  this.calls--;
@@ -1107,6 +1154,15 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
1107
1154
  return this.resolveProcedure(method);
1108
1155
  }
1109
1156
 
1157
+ /**
1158
+ * This method gives the keyed limiters of the option `limits`, in its sequence (CLI-40). A host
1159
+ * declares each one on its service with `svc.limiter(limiter.name, limiter.spec)`, so
1160
+ * `describe()` and `lint()` see it.
1161
+ */
1162
+ limiters(): readonly KeyedLimiter[] {
1163
+ return this.callLimits?.limiters ?? [];
1164
+ }
1165
+
1110
1166
  /**
1111
1167
  * This function publishes an event on the bus of the server: the bus of the option `events`, or
1112
1168
  * 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
  /**