@procedures/client 0.18.0 → 0.19.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 +62 -0
- package/README.md +20 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/ndjson.d.ts +3 -1
- package/dist/adapters/http/server/ndjson.d.ts.map +1 -1
- package/dist/adapters/http/server/ndjson.js +3 -1
- package/dist/adapters/http/server/ndjson.js.map +1 -1
- package/dist/adapters/port/transport.d.ts +10 -1
- package/dist/adapters/port/transport.d.ts.map +1 -1
- package/dist/adapters/port/transport.js +5 -1
- package/dist/adapters/port/transport.js.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +8 -2
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/client/types.d.ts +3 -0
- package/dist/adapters/websocket/client/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/types.js.map +1 -1
- package/dist/adapters/websocket/shared/rpc.d.ts +5 -1
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +5 -1
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +3 -3
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +4 -2
- package/dist/browser.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 +48 -0
- package/dist/procedures/collection/partition.d.ts.map +1 -1
- package/dist/procedures/collection/partition.js +66 -0
- package/dist/procedures/collection/partition.js.map +1 -1
- package/dist/procedures/collection/topic.d.ts +2 -0
- package/dist/procedures/collection/topic.d.ts.map +1 -1
- package/dist/procedures/collection/topic.js.map +1 -1
- package/dist/procedures/index.d.ts +1 -1
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +2 -0
- package/dist/procedures/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +37 -9
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +44 -4
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +16 -1
- package/dist/server/request-auth.d.ts.map +1 -1
- package/dist/server/request-auth.js +18 -0
- package/dist/server/request-auth.js.map +1 -1
- package/dist/server/types.d.ts +22 -6
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +6 -0
- package/dist/server/types.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/http/server/ndjson.ts +3 -1
- package/src/adapters/port/transport.ts +15 -2
- package/src/adapters/websocket/client/transport.ts +10 -3
- package/src/adapters/websocket/client/types.ts +3 -0
- package/src/adapters/websocket/shared/rpc.ts +5 -1
- package/src/browser.ts +4 -1
- package/src/procedures/collection/index.ts +2 -0
- package/src/procedures/collection/partition.ts +75 -0
- package/src/procedures/collection/topic.ts +2 -0
- package/src/procedures/index.ts +2 -0
- package/src/server/procedure-server.ts +58 -11
- package/src/server/request-auth.ts +20 -1
- package/src/server/types.ts +23 -6
|
@@ -14,11 +14,15 @@
|
|
|
14
14
|
* - In the topic of a partition, the name encodes `%` and `.` (`topic.ts`, roadmap CLI-42). Thus
|
|
15
15
|
* the name of a collection with partitions can have a dot.
|
|
16
16
|
* - A collection without partitions refuses a call or a topic with a partition.
|
|
17
|
+
* - The plain topic of a dotted name, `collections.a.b`, is also the topic of the partition `b` of
|
|
18
|
+
* `a`. Thus the server refuses a storage of `a.b` while `a` has partitions (roadmap CLI-57,
|
|
19
|
+
* decision D23).
|
|
17
20
|
*/
|
|
18
21
|
import type { ZodLike } from "../../client/validation/types.js";
|
|
19
22
|
import { InvocationError } from "../invoke.js";
|
|
20
23
|
import type { ProcedureContext } from "../types.js";
|
|
21
24
|
import type { CollectionStorage } from "./storage.js";
|
|
25
|
+
import { collectionChangeTopic, collectionTopicPartition } from "./topic.js";
|
|
22
26
|
|
|
23
27
|
/** The characters of a partition: letters, digits and `_ . : @ ~ + | / -`, 256 at most. */
|
|
24
28
|
export const PARTITION_PATTERN: RegExp = /^[A-Za-z0-9_.:@~+|/-]{1,256}$/;
|
|
@@ -131,5 +135,76 @@ export function storageOfCall<T>(ctx: ProcedureContext, collection: string, part
|
|
|
131
135
|
return repository.getPartitionStorage<T>(collection, partition);
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The error of a server that serves two collections with the same topic (roadmap CLI-57, decision
|
|
140
|
+
* D23). The plain topic of a collection with a storage, `collections.a.b`, is also the topic of the
|
|
141
|
+
* partition `b` of the partitioned collection `a`. Thus the readers of `a.b` get the events of the
|
|
142
|
+
* tenant `b` of `a`. To correct it, rename the collection `a.b` to a name without the prefix `a.`.
|
|
143
|
+
*/
|
|
144
|
+
export class CollectionTopicCollisionError extends Error {
|
|
145
|
+
override readonly name = "CollectionTopicCollisionError";
|
|
146
|
+
/** The collection with a storage without a partition. Its topic is `collections.<collection>`. */
|
|
147
|
+
readonly collection: string;
|
|
148
|
+
/** The collection with partitions. */
|
|
149
|
+
readonly partitioned: string;
|
|
150
|
+
/** The partition of `partitioned` whose topic is the topic of `collection`. */
|
|
151
|
+
readonly partition: string;
|
|
152
|
+
/** The topic of the two. */
|
|
153
|
+
readonly topic: string;
|
|
154
|
+
|
|
155
|
+
constructor(collection: string, partitioned: string, partition: string) {
|
|
156
|
+
super(collisionMessage(collection, partitioned, partition));
|
|
157
|
+
this.collection = collection;
|
|
158
|
+
this.partitioned = partitioned;
|
|
159
|
+
this.partition = partition;
|
|
160
|
+
this.topic = collectionChangeTopic(collection);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The message of a `CollectionTopicCollisionError`: the two collections, the topic and the fix. */
|
|
165
|
+
function collisionMessage(collection: string, partitioned: string, partition: string): string {
|
|
166
|
+
// The plain name is the encoded name of the partitioned collection, a dot and the partition
|
|
167
|
+
const prefix = collection.slice(0, collection.length - partition.length);
|
|
168
|
+
return (
|
|
169
|
+
`The collection "${collection}" and the partition "${partition}" of the collection "${partitioned}" have the same topic "${collectionChangeTopic(collection)}", ` +
|
|
170
|
+
`so the readers of "${collection}" get the events of that partition (roadmap CLI-57). ` +
|
|
171
|
+
`Rename the collection "${collection}" to a name that does not start with "${prefix}", for example "${collection.replace(/\./g, "_")}".`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* This function gives the partition whose topic is the plain topic of a collection, or undefined
|
|
177
|
+
* (roadmap CLI-57). The plain topic of `a.b` is `collections.a.b`, the topic of the partition `b`
|
|
178
|
+
* of `a`. A name without a dot gives undefined. It uses the parse of the publisher, so the two agree.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* partitionOfPlainTopic("a.b.c"); // { collection: "a", partition: "b.c" }
|
|
183
|
+
* partitionOfPlainTopic("a%2Eb.c"); // { collection: "a.b", partition: "c" }
|
|
184
|
+
* partitionOfPlainTopic("ab"); // undefined
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export function partitionOfPlainTopic(collection: string): { collection: string; partition: string } | undefined {
|
|
188
|
+
const target = collectionTopicPartition(collectionChangeTopic(collection));
|
|
189
|
+
return target?.partition === undefined ? undefined : { collection: target.collection, partition: target.partition };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* This function refuses two collections with the same topic (roadmap CLI-57, decision D23). It
|
|
194
|
+
* examines each collection with a storage: its plain topic must not be the topic of a partition of
|
|
195
|
+
* a collection with partitions. `ProcedureServer` uses it for each new storage and for each new
|
|
196
|
+
* collection with partitions, in each order.
|
|
197
|
+
*
|
|
198
|
+
* @param plain - The collections with a storage without a partition
|
|
199
|
+
* @param hasPartitions - True for a collection with partitions
|
|
200
|
+
* @throws CollectionTopicCollisionError for the first pair with the same topic
|
|
201
|
+
*/
|
|
202
|
+
export function assertTopicsApart(plain: Iterable<string>, hasPartitions: (collection: string) => boolean): void {
|
|
203
|
+
for (const collection of plain) {
|
|
204
|
+
const shared = partitionOfPlainTopic(collection);
|
|
205
|
+
if (shared && hasPartitions(shared.collection)) throw new CollectionTopicCollisionError(collection, shared.collection, shared.partition);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
134
209
|
// The parse of a topic lives in topic.ts, with the encode (roadmap CLI-42)
|
|
135
210
|
export { collectionTopicPartition, collectionTopicTarget } from "./topic.js";
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* - The topic of a collection whose name has a dot, `collections.a.b`, has the same form as the
|
|
17
17
|
* topic of the partition `b` of the collection `a`. Only the repository knows the collection, so
|
|
18
18
|
* `collectionTopicTarget()` decides with it.
|
|
19
|
+
* - `ProcedureServer` refuses a storage of `a.b` while `a` has partitions, in each order (roadmap
|
|
20
|
+
* CLI-57, decision D23). Thus the readers of `a.b` do not get the events of a partition of `a`.
|
|
19
21
|
*/
|
|
20
22
|
import type { RepositoryProvider } from "../types.js";
|
|
21
23
|
|
package/src/procedures/index.ts
CHANGED
|
@@ -134,6 +134,8 @@ export {
|
|
|
134
134
|
collectionTopicPartition,
|
|
135
135
|
collectionTopicTarget,
|
|
136
136
|
collectionTopicRule,
|
|
137
|
+
// The refusal of a storage whose topic is the topic of a partition (roadmap CLI-57)
|
|
138
|
+
CollectionTopicCollisionError,
|
|
137
139
|
// The rules of the rows of a collection (roadmap CLI-41)
|
|
138
140
|
admittedStorage,
|
|
139
141
|
admitsRow,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { Server, type ServerOptions } from "./server.js";
|
|
9
9
|
import type { AmbientCredentials, Credentials, Method, Principal, PrincipalOf, RequestIdentity, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
10
10
|
import { methodToPath, pathToMethod } from "./method.js";
|
|
11
|
+
import { isLocalCredentials } from "./request-auth.js";
|
|
11
12
|
import type {
|
|
12
13
|
AnyProcedure,
|
|
13
14
|
ProcedurePath,
|
|
@@ -19,7 +20,7 @@ import { ProcedureRegistry, PROCEDURE_REGISTRY } from "../procedures/registry.js
|
|
|
19
20
|
import { createCollectionProcedures } from "../procedures/collection/procedures.js";
|
|
20
21
|
import { InvocationError, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
|
|
21
22
|
import type { Capability, ClearanceOrder, ProcedureContext, ResourcesOf } from "../procedures/types.js";
|
|
22
|
-
import type
|
|
23
|
+
import { assertTopicsApart, type CollectionPartitions } from "../procedures/collection/partition.js";
|
|
23
24
|
import { VERSION_CONFLICTS_METADATA_KEY, versionConflictsOf } from "../procedures/collection/precondition.js";
|
|
24
25
|
import { TIMEOUT_METADATA_KEY, effectsOf, joinNarrowing, narrowCapability } from "../procedures/capability.js";
|
|
25
26
|
import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptions } from "../procedures/idempotency.js";
|
|
@@ -135,10 +136,11 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
135
136
|
idempotency?: IdempotencyOptions | false | undefined;
|
|
136
137
|
|
|
137
138
|
/**
|
|
138
|
-
* The token of the server (roadmap R18). Each request
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
139
|
+
* The token of the server (roadmap R18). Each request must present it as its bearer token, or
|
|
140
|
+
* the server answers `UNAUTHORIZED`. Only a local request (`LOCAL_CREDENTIALS`) skips it. The
|
|
141
|
+
* server refuses a request without credentials (CLI-1). Before, only the Express middleware of
|
|
142
|
+
* the peer examined the token, so a `ProcedureServer` with the HTTP or WebSocket transport of the
|
|
143
|
+
* core and the token of a host policy accepted each request.
|
|
142
144
|
*/
|
|
143
145
|
token?: string | undefined;
|
|
144
146
|
|
|
@@ -154,8 +156,9 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
154
156
|
* header, the query) and gives a bearer token, a principal and a narrowing. It runs before the
|
|
155
157
|
* token check: its bearer token replaces the bearer token of the credentials. Its principal
|
|
156
158
|
* replaces the principal of the token check. `authenticate` runs after it and can read
|
|
157
|
-
* `request.principal`. Undefined or an error refuses the request with `UNAUTHORIZED`. A
|
|
158
|
-
*
|
|
159
|
+
* `request.principal`. Undefined or an error refuses the request with `UNAUTHORIZED`. A local
|
|
160
|
+
* request (`LOCAL_CREDENTIALS`) does not go through the hook. A server with a hook refuses a
|
|
161
|
+
* request without credentials, and the hook does not run for it (CLI-1).
|
|
159
162
|
*/
|
|
160
163
|
principalOf?: PrincipalOf | undefined;
|
|
161
164
|
|
|
@@ -343,6 +346,8 @@ function withRequestTimeout(
|
|
|
343
346
|
* (VALIDATION_ERROR, NOT_EXPOSED, ABORTED and so on). An error of the handler keeps its own code
|
|
344
347
|
* when it is a string, as for a stream (`WIRE-PROTOCOL.md`). Without a code, it is HANDLER_ERROR.
|
|
345
348
|
* An error without a retryable flag gets the flag of its code in `ERROR_CODES`, or false (CLI-48).
|
|
349
|
+
* This default applies only before the stream starts. An in-band end without a flag gets false
|
|
350
|
+
* (CLI-58, `WIRE-PROTOCOL.md`, section "Error codes").
|
|
346
351
|
*/
|
|
347
352
|
export function errorResponse(id: string, error: unknown): ServerResponse<unknown> {
|
|
348
353
|
// An error that says it is retryable stays retryable: before, every error lost the flag
|
|
@@ -495,7 +500,8 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
495
500
|
this.publisher = events instanceof EventPublisher ? events : events ? new EventPublisher({ ...events, clock: events.clock ?? this.clock }) : undefined;
|
|
496
501
|
if (this.publisher) this.registerProcedure(this.publisher.procedure, { alwaysOn: true });
|
|
497
502
|
|
|
498
|
-
// Configure collections
|
|
503
|
+
// Configure collections. A new server has no partitions yet: usePartitions() examines these
|
|
504
|
+
// storages for a shared topic (roadmap CLI-57)
|
|
499
505
|
if (options.collections) {
|
|
500
506
|
for (const [name, storage] of Object.entries(options.collections)) {
|
|
501
507
|
this.storages.set(name, storage);
|
|
@@ -577,15 +583,31 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
577
583
|
* The name can have a dot (roadmap CLI-42). The topic of a partition encodes the name, so the
|
|
578
584
|
* name and the partition of a topic stay apart (`collectionChangeTopic()`).
|
|
579
585
|
*
|
|
586
|
+
* The server refuses partitions of `a` while it has a storage of `a.b` (roadmap CLI-57, decision
|
|
587
|
+
* D23). The plain topic of `a.b` is the topic of the partition `b` of `a`. `useStorage` refuses
|
|
588
|
+
* the other order.
|
|
589
|
+
*
|
|
580
590
|
* @param name - Collection name
|
|
581
591
|
* @param partitions - The storage of each partition, and the resolver
|
|
582
592
|
* @returns this (for chaining)
|
|
593
|
+
* @throws CollectionTopicCollisionError when the topic of a partition is the topic of a collection with a storage
|
|
583
594
|
*/
|
|
584
595
|
usePartitions<T>(name: string, partitions: CollectionPartitions<T>): this {
|
|
596
|
+
assertTopicsApart(this.storages.keys(), (collection) => collection === name);
|
|
585
597
|
this.partitions.set(name, partitions as CollectionPartitions<unknown>);
|
|
586
598
|
return this;
|
|
587
599
|
}
|
|
588
600
|
|
|
601
|
+
/**
|
|
602
|
+
* The check of the topics of new storages (roadmap CLI-57, decision D23). The plain topic of each
|
|
603
|
+
* one must not be the topic of a partition of a collection with partitions.
|
|
604
|
+
*
|
|
605
|
+
* @throws CollectionTopicCollisionError for the first pair with the same topic
|
|
606
|
+
*/
|
|
607
|
+
private assertPlainTopicsApart(names: Iterable<string>): void {
|
|
608
|
+
assertTopicsApart(names, (collection) => this.partitions.has(collection));
|
|
609
|
+
}
|
|
610
|
+
|
|
589
611
|
// ===========================================================================
|
|
590
612
|
// Storage Configuration
|
|
591
613
|
// ===========================================================================
|
|
@@ -606,15 +628,21 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
606
628
|
* that took the old storage, for the time of `grace` at most. Before, such a call could fail
|
|
607
629
|
* because the storage was closed.
|
|
608
630
|
*
|
|
631
|
+
* The server refuses a storage of `a.b` while `a` has partitions (roadmap CLI-57, decision D23).
|
|
632
|
+
* The plain topic of `a.b` is the topic of the partition `b` of `a`. With options, the promise
|
|
633
|
+
* rejects, and the old storage stays as it is.
|
|
634
|
+
*
|
|
609
635
|
* @param name - Collection name
|
|
610
636
|
* @param storage - Storage backend
|
|
611
637
|
* @param options - Drain and close the old storage
|
|
612
638
|
* @returns this (for chaining), or a promise of it when options are given
|
|
639
|
+
* @throws CollectionTopicCollisionError when the topic of the collection is the topic of a partition
|
|
613
640
|
*/
|
|
614
641
|
useStorage<T>(name: string, storage: CollectionStorage<T>): this;
|
|
615
642
|
useStorage<T>(name: string, storage: CollectionStorage<T>, options: UseStorageOptions): Promise<this>;
|
|
616
643
|
useStorage<T>(name: string, storage: CollectionStorage<T>, options?: UseStorageOptions): this | Promise<this> {
|
|
617
644
|
if (options === undefined) {
|
|
645
|
+
this.assertPlainTopicsApart([name]);
|
|
618
646
|
this.storages.set(name, storage as CollectionStorage<unknown>);
|
|
619
647
|
return this;
|
|
620
648
|
}
|
|
@@ -626,6 +654,9 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
626
654
|
* the old storage, and close it.
|
|
627
655
|
*/
|
|
628
656
|
private async replaceStorage(name: string, storage: CollectionStorage<unknown>, options: UseStorageOptions): Promise<this> {
|
|
657
|
+
// Before the flush: a refused storage changes nothing (roadmap CLI-57). With an old storage,
|
|
658
|
+
// usePartitions() sees the name during the flush. Without one, no await comes before the set.
|
|
659
|
+
this.assertPlainTopicsApart([name]);
|
|
629
660
|
const graceMs = optionalMs(options.grace, "grace") ?? TIMER_DEFAULTS.server.storageGraceMs;
|
|
630
661
|
const old = this.storages.get(name);
|
|
631
662
|
const other = old !== undefined && old !== storage ? old : undefined;
|
|
@@ -697,10 +728,15 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
697
728
|
/**
|
|
698
729
|
* Configure multiple storages at once.
|
|
699
730
|
*
|
|
731
|
+
* The server examines all the names before it sets a storage: a refused name sets none (roadmap
|
|
732
|
+
* CLI-57, decision D23).
|
|
733
|
+
*
|
|
700
734
|
* @param storages - Map of collection names to storage backends
|
|
701
735
|
* @returns this (for chaining)
|
|
736
|
+
* @throws CollectionTopicCollisionError when the topic of a collection is the topic of a partition
|
|
702
737
|
*/
|
|
703
738
|
useStorages(storages: Record<string, CollectionStorage<unknown>>): this {
|
|
739
|
+
this.assertPlainTopicsApart(Object.keys(storages));
|
|
704
740
|
for (const [name, storage] of Object.entries(storages)) {
|
|
705
741
|
this.storages.set(name, storage);
|
|
706
742
|
}
|
|
@@ -855,7 +891,9 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
855
891
|
/**
|
|
856
892
|
* The token check and the principal of a request (roadmap R18). The request hook reads the
|
|
857
893
|
* credentials of a network request first: a bearer token, a principal and a narrowing. A
|
|
858
|
-
* network request
|
|
894
|
+
* network request must present the token. Only a local request (`LOCAL_CREDENTIALS`) skips the
|
|
895
|
+
* hook and the token. A request without credentials is not local: a server with a token or a
|
|
896
|
+
* request hook refuses it, and the hook does not run (CLI-1). The authenticate function gives
|
|
859
897
|
* the principal. The function gives an UNAUTHORIZED response, or undefined when the request
|
|
860
898
|
* passes.
|
|
861
899
|
*/
|
|
@@ -865,7 +903,15 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
865
903
|
status: { type: "error", code: "UNAUTHORIZED", message, retryable: false },
|
|
866
904
|
metadata: {},
|
|
867
905
|
});
|
|
868
|
-
|
|
906
|
+
// Only a local request skips the token and the request hook (CLI-1). A face that sets no
|
|
907
|
+
// credentials skips neither: the server refuses it, and the hook does not run. Before, such a
|
|
908
|
+
// request passed as a request of this process.
|
|
909
|
+
if (request.credentials === undefined && (this.token !== undefined || this.principalOf !== undefined)) {
|
|
910
|
+
return unauthorized("Unauthorized: this server authenticates each request, and the request has no credentials");
|
|
911
|
+
}
|
|
912
|
+
// A local face (this process, MCP over stdio, a message port) sets the local marker. The
|
|
913
|
+
// marker key is a symbol, so a request from the network cannot carry it (CLI-1).
|
|
914
|
+
const credentials = isLocalCredentials(request.credentials) ? undefined : request.credentials;
|
|
869
915
|
let identity: RequestIdentity | undefined;
|
|
870
916
|
if (this.principalOf && credentials !== undefined) {
|
|
871
917
|
try {
|
|
@@ -942,7 +988,8 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
942
988
|
* This method tells if the server admits a connection with these credentials, before its first
|
|
943
989
|
* call. It uses the path of each call: the request hook, the token check and the authenticate
|
|
944
990
|
* function (R18, CLI-2a). A face of connections refuses the upgrade when it gives false (CLI-7).
|
|
945
|
-
* A request hook that accepts
|
|
991
|
+
* A request hook that accepts empty credentials (`{}`) with an empty identity admits the
|
|
992
|
+
* connection.
|
|
946
993
|
*/
|
|
947
994
|
async admits(credentials: Credentials): Promise<boolean> {
|
|
948
995
|
const request: ServerRequest<unknown> = { id: "connection", method: { service: "", operation: "" }, payload: undefined, metadata: {}, credentials };
|
|
@@ -6,7 +6,26 @@
|
|
|
6
6
|
* This module imports no Node code: a server in a browser or a worker can use it too.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type
|
|
9
|
+
import { LOCAL_MARKER, type Credentials } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The credentials of a local face (CLI-1): a call from this application, not from the network.
|
|
13
|
+
* The server applies no token and no request hook to a request with them.
|
|
14
|
+
*
|
|
15
|
+
* Only these faces set them:
|
|
16
|
+
* - A call in this process: a direct `handle()` of the code of the application.
|
|
17
|
+
* - MCP over stdio: the peer is the parent process that started this process.
|
|
18
|
+
* - A message port (`PortTransport`): only the holder of the other end of the port can send.
|
|
19
|
+
*
|
|
20
|
+
* A network face (HTTP, WebSocket, MCP over Streamable HTTP) never sets them. The marker key is a
|
|
21
|
+
* symbol, so no request from the network can carry it.
|
|
22
|
+
*/
|
|
23
|
+
export const LOCAL_CREDENTIALS: Credentials = Object.freeze({ [LOCAL_MARKER]: true as const });
|
|
24
|
+
|
|
25
|
+
/** True when the credentials carry the local marker (CLI-1): the request comes from this application. */
|
|
26
|
+
export function isLocalCredentials(credentials: Credentials | undefined): boolean {
|
|
27
|
+
return credentials?.[LOCAL_MARKER] === true;
|
|
28
|
+
}
|
|
10
29
|
|
|
11
30
|
/** The headers that carry a secret. They do not go into the metadata of a request. */
|
|
12
31
|
const SECRET_HEADERS: ReadonlySet<string> = new Set(["authorization", "proxy-authorization", "cookie", "x-api-key"]);
|
package/src/server/types.ts
CHANGED
|
@@ -48,11 +48,14 @@ export interface ServerRequest<TReq = unknown> {
|
|
|
48
48
|
signal?: AbortSignal;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* The credentials
|
|
52
|
-
* bearer token of an `Authorization` header
|
|
53
|
-
*
|
|
54
|
-
* metadata, so a handler and a nested call do not see them.
|
|
55
|
-
*
|
|
51
|
+
* The credentials of the request (roadmap R18). A network face reads them from the request: the
|
|
52
|
+
* bearer token of an `Authorization` header or of the `token` query of a WebSocket connection,
|
|
53
|
+
* the cookies, the headers and the query. A local face sets `LOCAL_CREDENTIALS` (CLI-1). They are
|
|
54
|
+
* not in the metadata, so a handler and a nested call do not see them.
|
|
55
|
+
*
|
|
56
|
+
* A server with a token or a request hook (`principalOf`) refuses a request without credentials
|
|
57
|
+
* with `UNAUTHORIZED`, and the hook does not run (CLI-1). Before, such a request skipped the
|
|
58
|
+
* token check and the hook.
|
|
56
59
|
*/
|
|
57
60
|
credentials?: Credentials | undefined;
|
|
58
61
|
|
|
@@ -67,11 +70,25 @@ export interface ServerRequest<TReq = unknown> {
|
|
|
67
70
|
narrowing?: CapabilityRequest | undefined;
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The key of the local marker of credentials (CLI-1). The key is a symbol. JSON, structured clone,
|
|
75
|
+
* a header, a cookie and a query cannot carry a symbol key. Thus a remote request cannot claim to
|
|
76
|
+
* be local. Use `LOCAL_CREDENTIALS` to set the marker.
|
|
77
|
+
*/
|
|
78
|
+
export const LOCAL_MARKER: unique symbol = Symbol.for("@procedures/client.local-credentials");
|
|
79
|
+
|
|
70
80
|
/**
|
|
71
81
|
* The credentials of a request. A network transport sets each field that it can read. The request
|
|
72
|
-
* hook of the server (`principalOf`) reads them.
|
|
82
|
+
* hook of the server (`principalOf`) reads them. A local face sets only the local marker
|
|
83
|
+
* (`LOCAL_CREDENTIALS`).
|
|
73
84
|
*/
|
|
74
85
|
export interface Credentials {
|
|
86
|
+
/**
|
|
87
|
+
* The local marker (CLI-1): the request comes from this application, not from the network. The
|
|
88
|
+
* server applies no token and no request hook to a local request. Only a local face sets it: a
|
|
89
|
+
* call in this process, MCP over stdio, or a message port. A network face never sets it.
|
|
90
|
+
*/
|
|
91
|
+
readonly [LOCAL_MARKER]?: true | undefined;
|
|
75
92
|
/** The bearer token that the request presents. */
|
|
76
93
|
readonly bearer?: string | undefined;
|
|
77
94
|
/** The cookies of the request, by name (the `Cookie` header of an HTTP request or of an upgrade). */
|