@procedures/client 0.26.2 → 0.27.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 +59 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +25 -3
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +39 -3
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/types.d.ts +33 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +1 -1
- package/src/browser.ts +1 -0
- package/src/server/index.ts +1 -0
- package/src/server/procedure-server.ts +47 -4
- package/src/server/types.ts +28 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Server, type ServerOptions } from "./server.js";
|
|
9
|
-
import type { AmbientCredentials, Credentials, Method, Principal, PrincipalOf, RequestIdentity, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
9
|
+
import type { Admission, AmbientCredentials, Credentials, Method, Principal, PrincipalOf, RequestIdentity, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
10
10
|
import { methodToPath, pathToMethod } from "./method.js";
|
|
11
11
|
import { isLocalCredentials } from "./request-auth.js";
|
|
12
12
|
import type {
|
|
@@ -149,6 +149,13 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
149
149
|
* `UNAUTHORIZED`. It runs after the token check. The capability function can read
|
|
150
150
|
* `request.principal`. Like the request hook, it can run more than one time for one request or
|
|
151
151
|
* one task: `admits()` and `identify()` run it too (roadmap CLI-69).
|
|
152
|
+
*
|
|
153
|
+
* On the MCP HTTP face, it runs one time for each HTTP request, in `admission()` (CLI-2b). The
|
|
154
|
+
* tool calls of the request use that admission, so the function does not run for each call. For
|
|
155
|
+
* `admission()`, `admits()` and `identify()`, the request has an empty method
|
|
156
|
+
* (`{ service: "", operation: "" }`), the id `admission`, `connection` or `identify`, no payload
|
|
157
|
+
* and no metadata. Thus the function must not branch on the method, the payload, the metadata
|
|
158
|
+
* or the transport of the request. It reads only the credentials and the principal.
|
|
152
159
|
*/
|
|
153
160
|
authenticate?: ((request: ServerRequest<unknown>) => Principal | undefined | Promise<Principal | undefined>) | undefined;
|
|
154
161
|
|
|
@@ -319,7 +326,7 @@ async function* forgottenAtRetryableEnd<T>(items: AsyncIterable<T>, forget: () =
|
|
|
319
326
|
}
|
|
320
327
|
}
|
|
321
328
|
|
|
322
|
-
/** The request of credentials without a call: `admits()` and `
|
|
329
|
+
/** The request of credentials without a call: `admits()`, `identify()` and `admission()` give it to the checks of each call. */
|
|
323
330
|
function credentialsRequest(id: string, credentials: Credentials): ServerRequest<unknown> {
|
|
324
331
|
return { id, method: { service: "", operation: "" }, payload: undefined, metadata: {}, credentials };
|
|
325
332
|
}
|
|
@@ -478,6 +485,11 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
478
485
|
private readonly token: string | undefined;
|
|
479
486
|
private readonly principalOf: PrincipalOf | undefined;
|
|
480
487
|
private readonly authenticate: ProcedureServerOptions["authenticate"];
|
|
488
|
+
/**
|
|
489
|
+
* The admissions of this server (CLI-2b): the identity of each object of admitted credentials
|
|
490
|
+
* that `admission()` gave. Only this server adds an entry, so a request cannot claim one.
|
|
491
|
+
*/
|
|
492
|
+
private readonly admissions = new WeakMap<Credentials, { readonly principal: Principal | undefined; readonly narrowing: ServerRequest<unknown>["narrowing"] }>();
|
|
481
493
|
/** The functions that `drain()` waits on: each one runs when no call is in flight */
|
|
482
494
|
private idleWaiters: Array<() => void> = [];
|
|
483
495
|
/** The publisher of the remote events, from the option `events`, or undefined. */
|
|
@@ -921,8 +933,9 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
921
933
|
* network request must present the token. Only a local request (`LOCAL_CREDENTIALS`) skips the
|
|
922
934
|
* hook and the token. A request without credentials is not local: a server with a token or a
|
|
923
935
|
* request hook refuses it, and the hook does not run (CLI-1). The authenticate function gives
|
|
924
|
-
* the principal. The
|
|
925
|
-
*
|
|
936
|
+
* the principal. The credentials of an admission of this server (`admission()`) skip the
|
|
937
|
+
* checks: the request gets the principal and the narrowing of that admission (CLI-2b). The
|
|
938
|
+
* function gives an UNAUTHORIZED response, or undefined when the request passes.
|
|
926
939
|
*/
|
|
927
940
|
private async admit(request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
|
|
928
941
|
const unauthorized = (message: string): ServerResponse<unknown> => ({
|
|
@@ -930,6 +943,17 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
930
943
|
status: { type: "error", code: "UNAUTHORIZED", message, retryable: false },
|
|
931
944
|
metadata: {},
|
|
932
945
|
});
|
|
946
|
+
// A face admitted the network request of this call one time (CLI-2b): the checks do not run
|
|
947
|
+
// again. Only this server makes such credentials, so a copy or a forgery has no entry.
|
|
948
|
+
const admitted = request.credentials !== undefined ? this.admissions.get(request.credentials) : undefined;
|
|
949
|
+
if (admitted) {
|
|
950
|
+
if (admitted.principal) request.principal = admitted.principal;
|
|
951
|
+
if (admitted.narrowing) {
|
|
952
|
+
const order = this.capabilityOptions.clearanceOrder;
|
|
953
|
+
request.narrowing = request.narrowing ? joinNarrowing(request.narrowing, admitted.narrowing, order) : admitted.narrowing;
|
|
954
|
+
}
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
933
957
|
// Only a local request skips the token and the request hook (CLI-1). A face that sets no
|
|
934
958
|
// credentials skips neither: the server refuses it, and the hook does not run. Before, such a
|
|
935
959
|
// request passed as a request of this process.
|
|
@@ -1034,6 +1058,25 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
1034
1058
|
return (await this.admit(request)) === undefined ? request.principal : undefined;
|
|
1035
1059
|
}
|
|
1036
1060
|
|
|
1061
|
+
/**
|
|
1062
|
+
* This method admits a network request one time, before its calls (CLI-2b). It runs the checks
|
|
1063
|
+
* of each call: the request hook, the token check and the authenticate function. The admitted
|
|
1064
|
+
* credentials are a new object. A call with them gets the principal and the narrowing of the
|
|
1065
|
+
* admission, and the checks do not run again for it. Only the admitted object skips the checks:
|
|
1066
|
+
* a copy of it and the credentials of another server go through them. A face gives the
|
|
1067
|
+
* admitted credentials only to the calls of the same network request.
|
|
1068
|
+
*/
|
|
1069
|
+
async admission(credentials: Credentials): Promise<Admission> {
|
|
1070
|
+
const request = credentialsRequest("admission", credentials);
|
|
1071
|
+
const refusal = await this.admit(request);
|
|
1072
|
+
if (refusal) {
|
|
1073
|
+
return { ok: false, code: "UNAUTHORIZED", message: refusal.status.type === "error" ? refusal.status.message : "Unauthorized" };
|
|
1074
|
+
}
|
|
1075
|
+
const admitted: Credentials = Object.freeze({ ...credentials });
|
|
1076
|
+
this.admissions.set(admitted, { principal: request.principal, narrowing: request.narrowing });
|
|
1077
|
+
return { ok: true, credentials: admitted, ...(request.principal ? { principal: request.principal } : {}) };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1037
1080
|
/**
|
|
1038
1081
|
* Run a keyed call one time. The run continues when its caller goes away, so that the next try
|
|
1039
1082
|
* gets its result: only the deadline of the call and the stop of the server end it. A stream
|
package/src/server/types.ts
CHANGED
|
@@ -124,8 +124,16 @@ export interface RequestIdentity {
|
|
|
124
124
|
* with `UNAUTHORIZED`. The hook runs before the token check and before `authenticate`.
|
|
125
125
|
*
|
|
126
126
|
* The hook can run more than one time for one request or one task. `admits()` runs it at a
|
|
127
|
-
* WebSocket upgrade, and
|
|
128
|
-
*
|
|
127
|
+
* WebSocket upgrade, and then each call over the connection runs it again. Thus a hook with a
|
|
128
|
+
* side effect, for example a count or a log line, sees each of these runs.
|
|
129
|
+
*
|
|
130
|
+
* On the MCP HTTP face, the hook runs one time for each HTTP request, before its calls (CLI-2b):
|
|
131
|
+
* `admission()` runs it for `initialize`, a tool call, the GET stream and the DELETE of a session.
|
|
132
|
+
* A tool call, and `identify()` before a task (CLI-69), use that admission: the hook does not run
|
|
133
|
+
* again for them. For `admission()`, `admits()` and `identify()`, the request of the hook has an
|
|
134
|
+
* empty method (`{ service: "", operation: "" }`), the id `admission`, `connection` or `identify`,
|
|
135
|
+
* no payload and no metadata. Thus a hook must not branch on the method, the payload, the
|
|
136
|
+
* metadata or the transport of the request. It reads only the credentials.
|
|
129
137
|
*/
|
|
130
138
|
export type PrincipalOf = (
|
|
131
139
|
credentials: Credentials,
|
|
@@ -180,8 +188,26 @@ export interface RequestHandler {
|
|
|
180
188
|
* only in the call.
|
|
181
189
|
*/
|
|
182
190
|
identify?(credentials: Credentials): Promise<Principal | undefined>;
|
|
191
|
+
/**
|
|
192
|
+
* This function admits a network request one time, before its calls (CLI-2b). It uses the
|
|
193
|
+
* checks of each call: the request hook, the token check and the authenticate function. A face
|
|
194
|
+
* that carries the calls of one network request (MCP over Streamable HTTP) gives the admitted
|
|
195
|
+
* credentials to those calls, so the checks run one time for the request. `ProcedureServer` has
|
|
196
|
+
* it. Absent: the handler examines each call, and the face refuses no request before its calls.
|
|
197
|
+
*/
|
|
198
|
+
admission?(credentials: Credentials): Promise<Admission>;
|
|
183
199
|
}
|
|
184
200
|
|
|
201
|
+
/**
|
|
202
|
+
* The result of `admission()` of a request handler (CLI-2b). A request that passes gets its
|
|
203
|
+
* admitted credentials and its principal. Each call of that request with the admitted credentials
|
|
204
|
+
* gets the principal and the narrowing of the admission, and the checks do not run again. A
|
|
205
|
+
* refused request gets `UNAUTHORIZED` and the message of the refusal.
|
|
206
|
+
*/
|
|
207
|
+
export type Admission =
|
|
208
|
+
| { readonly ok: true; readonly credentials: Credentials; readonly principal?: Principal | undefined }
|
|
209
|
+
| { readonly ok: false; readonly code: "UNAUTHORIZED"; readonly message: string };
|
|
210
|
+
|
|
185
211
|
/**
|
|
186
212
|
* How a server can authenticate a request with ambient credentials (cookies):
|
|
187
213
|
* - `"always"`: each network request. The request hook `principalOf` reads the cookies, so a face
|