@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.
- package/CHANGELOG.md +91 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/handler.d.ts.map +1 -1
- package/dist/adapters/http/server/handler.js +5 -3
- package/dist/adapters/http/server/handler.js.map +1 -1
- package/dist/browser.d.ts +3 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +12 -0
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/middleware/rate-limit.d.ts +17 -0
- package/dist/client/middleware/rate-limit.d.ts.map +1 -1
- package/dist/client/middleware/rate-limit.js +94 -39
- package/dist/client/middleware/rate-limit.js.map +1 -1
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js.map +1 -1
- package/dist/events/publisher.d.ts +50 -5
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +99 -10
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/recheck.d.ts +84 -0
- package/dist/events/recheck.d.ts.map +1 -0
- package/dist/events/recheck.js +135 -0
- package/dist/events/recheck.js.map +1 -0
- package/dist/procedures/collection/changes.d.ts +9 -8
- package/dist/procedures/collection/changes.d.ts.map +1 -1
- package/dist/procedures/collection/changes.js +11 -11
- package/dist/procedures/collection/changes.js.map +1 -1
- package/dist/procedures/collection/index.d.ts +1 -0
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +2 -0
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/partition.d.ts +4 -23
- package/dist/procedures/collection/partition.d.ts.map +1 -1
- package/dist/procedures/collection/partition.js +2 -28
- package/dist/procedures/collection/partition.js.map +1 -1
- package/dist/procedures/collection/topic.d.ts +86 -0
- package/dist/procedures/collection/topic.d.ts.map +1 -0
- package/dist/procedures/collection/topic.js +94 -0
- package/dist/procedures/collection/topic.js.map +1 -0
- package/dist/procedures/index.d.ts +1 -1
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +1 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -0
- package/dist/protocol.js.map +1 -1
- package/dist/server/host-policy.d.ts +17 -0
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +7 -0
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/limits.d.ts +122 -0
- package/dist/server/limits.d.ts.map +1 -0
- package/dist/server/limits.js +153 -0
- package/dist/server/limits.js.map +1 -0
- package/dist/server/procedure-server.d.ts +35 -3
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +42 -8
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +5 -1
- package/dist/server/request-auth.d.ts.map +1 -1
- package/dist/server/request-auth.js +19 -2
- package/dist/server/request-auth.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/http/server/handler.ts +5 -3
- package/src/browser.ts +5 -0
- package/src/client/errors/registry.ts +13 -0
- package/src/client/middleware/rate-limit.ts +116 -41
- package/src/events/index.ts +1 -0
- package/src/events/publisher.ts +121 -12
- package/src/events/recheck.ts +182 -0
- package/src/procedures/collection/changes.ts +12 -11
- package/src/procedures/collection/index.ts +2 -0
- package/src/procedures/collection/partition.ts +5 -27
- package/src/procedures/collection/topic.ts +115 -0
- package/src/procedures/index.ts +1 -0
- package/src/protocol.ts +3 -1
- package/src/server/host-policy.ts +22 -0
- package/src/server/limits.ts +254 -0
- package/src/server/procedure-server.ts +59 -9
- 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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
/**
|