@procedures/client 0.8.0 → 0.8.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.
- package/CHANGELOG.md +58 -0
- package/README.md +95 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts +4 -0
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +11 -1
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/client/types.d.ts +19 -0
- package/dist/adapters/http/client/types.d.ts.map +1 -1
- package/dist/adapters/http/server/cors.d.ts +64 -0
- package/dist/adapters/http/server/cors.d.ts.map +1 -0
- package/dist/adapters/http/server/cors.js +113 -0
- package/dist/adapters/http/server/cors.js.map +1 -0
- package/dist/adapters/http/server/handler.d.ts +136 -0
- package/dist/adapters/http/server/handler.d.ts.map +1 -0
- package/dist/adapters/http/server/handler.js +495 -0
- package/dist/adapters/http/server/handler.js.map +1 -0
- package/dist/adapters/http/server/strategies.d.ts +14 -5
- package/dist/adapters/http/server/strategies.d.ts.map +1 -1
- package/dist/adapters/http/server/strategies.js.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +30 -46
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +48 -432
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/server/types.d.ts +13 -0
- package/dist/adapters/http/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +46 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +152 -15
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +10 -3
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
- package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade-router.js +62 -0
- package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
- package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
- package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade.js +49 -0
- package/dist/adapters/websocket/server/upgrade.js.map +1 -0
- package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +8 -2
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +12 -4
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +11 -2
- package/dist/browser.js.map +1 -1
- package/dist/client/client.d.ts +43 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +69 -1
- package/dist/client/client.js.map +1 -1
- package/dist/events/publisher.d.ts +14 -0
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +42 -4
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/rows.d.ts +31 -0
- package/dist/events/rows.d.ts.map +1 -0
- package/dist/events/rows.js +39 -0
- package/dist/events/rows.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +14 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +9 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/capability.d.ts +6 -0
- package/dist/procedures/capability.d.ts.map +1 -1
- package/dist/procedures/capability.js +25 -0
- package/dist/procedures/capability.js.map +1 -1
- package/dist/procedures/collection/changes.d.ts +94 -0
- package/dist/procedures/collection/changes.d.ts.map +1 -0
- package/dist/procedures/collection/changes.js +182 -0
- package/dist/procedures/collection/changes.js.map +1 -0
- package/dist/procedures/collection/index.d.ts +8 -3
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +6 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/procedures.d.ts +39 -2
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +111 -8
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/query.d.ts +246 -0
- package/dist/procedures/collection/query.d.ts.map +1 -0
- package/dist/procedures/collection/query.js +854 -0
- package/dist/procedures/collection/query.js.map +1 -0
- package/dist/procedures/collection/schemas.d.ts +50 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +144 -6
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +58 -6
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/collection/storage.js +23 -0
- package/dist/procedures/collection/storage.js.map +1 -1
- package/dist/procedures/elicit.d.ts +190 -0
- package/dist/procedures/elicit.d.ts.map +1 -0
- package/dist/procedures/elicit.js +127 -0
- package/dist/procedures/elicit.js.map +1 -0
- package/dist/procedures/hints.d.ts +43 -0
- package/dist/procedures/hints.d.ts.map +1 -0
- package/dist/procedures/hints.js +74 -0
- package/dist/procedures/hints.js.map +1 -0
- package/dist/procedures/index.d.ts +9 -2
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +5 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts +20 -0
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +13 -0
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/progress.d.ts +24 -0
- package/dist/procedures/progress.d.ts.map +1 -0
- package/dist/procedures/progress.js +11 -0
- package/dist/procedures/progress.js.map +1 -0
- package/dist/procedures/registry.d.ts.map +1 -1
- package/dist/procedures/registry.js +5 -0
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/types.d.ts +42 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/procedures/untrusted.d.ts +28 -0
- package/dist/procedures/untrusted.d.ts.map +1 -0
- package/dist/procedures/untrusted.js +32 -0
- package/dist/procedures/untrusted.js.map +1 -0
- package/dist/server/browser-guard.d.ts +12 -2
- package/dist/server/browser-guard.d.ts.map +1 -1
- package/dist/server/browser-guard.js +43 -6
- package/dist/server/browser-guard.js.map +1 -1
- package/dist/server/host-policy.d.ts +10 -1
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +5 -0
- package/dist/server/host-policy.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 +38 -6
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +65 -13
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +51 -4
- package/dist/server/request-auth.d.ts.map +1 -1
- package/dist/server/request-auth.js +116 -5
- package/dist/server/request-auth.js.map +1 -1
- package/dist/server/types.d.ts +73 -5
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +5 -1
- package/src/adapters/http/client/transport.ts +11 -1
- package/src/adapters/http/client/types.ts +22 -0
- package/src/adapters/http/server/cors.ts +144 -0
- package/src/adapters/http/server/handler.ts +624 -0
- package/src/adapters/http/server/strategies.ts +16 -6
- package/src/adapters/http/server/transport.ts +55 -478
- package/src/adapters/http/server/types.ts +15 -0
- package/src/adapters/websocket/server/transport.ts +166 -16
- package/src/adapters/websocket/server/types.ts +11 -3
- package/src/adapters/websocket/server/upgrade-router.ts +77 -0
- package/src/adapters/websocket/server/upgrade.ts +88 -0
- package/src/adapters/websocket/shared/rpc.ts +9 -2
- package/src/browser.ts +79 -2
- package/src/client/client.ts +83 -1
- package/src/events/publisher.ts +48 -4
- package/src/events/rows.ts +48 -0
- package/src/index.ts +1 -1
- package/src/node.ts +21 -0
- package/src/procedures/capability.ts +21 -0
- package/src/procedures/collection/changes.ts +245 -0
- package/src/procedures/collection/index.ts +53 -1
- package/src/procedures/collection/procedures.ts +163 -7
- package/src/procedures/collection/query.ts +1009 -0
- package/src/procedures/collection/schemas.ts +196 -7
- package/src/procedures/collection/storage.ts +78 -6
- package/src/procedures/elicit.ts +320 -0
- package/src/procedures/hints.ts +91 -0
- package/src/procedures/index.ts +78 -2
- package/src/procedures/invoke.ts +29 -0
- package/src/procedures/progress.ts +25 -0
- package/src/procedures/registry.ts +4 -0
- package/src/procedures/types.ts +49 -0
- package/src/procedures/untrusted.ts +50 -0
- package/src/server/browser-guard.ts +48 -6
- package/src/server/host-policy.ts +13 -1
- package/src/server/index.ts +3 -0
- package/src/server/procedure-server.ts +82 -14
- package/src/server/request-auth.ts +128 -6
- package/src/server/types.ts +82 -5
package/src/procedures/types.ts
CHANGED
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
* Procedures are the foundation for typed RPC calls with schema validation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { Elicit } from "./elicit.js";
|
|
9
|
+
import type { ReportProgress } from "./progress.js";
|
|
10
|
+
import type { ToolHintOverride } from "./hints.js";
|
|
8
11
|
import type { ZodErrorLike } from "../client/validation/types.js";
|
|
9
12
|
import type { ProcedureSchema } from "./schema.js";
|
|
10
13
|
import type { CollectionStorage } from "./collection/storage.js";
|
|
11
14
|
import type { EventBus } from "../events/types.js";
|
|
12
15
|
import type { HydrationEvent } from "./ref.js";
|
|
16
|
+
import type { Principal } from "../server/types.js";
|
|
13
17
|
|
|
14
18
|
// =============================================================================
|
|
15
19
|
// Procedure Path
|
|
@@ -83,6 +87,23 @@ export interface ProcedureMetadata {
|
|
|
83
87
|
*/
|
|
84
88
|
stream?: { readonly unbounded?: boolean } | undefined;
|
|
85
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The output fields that hold text from outside the host: a web page, a mail (CLI-10). The value
|
|
92
|
+
* of each field is a text, an `UntrustedText` with its source, or a list of them. A face that
|
|
93
|
+
* gives text to a model (MCP) shows these texts in envelopes: data, never instructions.
|
|
94
|
+
*/
|
|
95
|
+
untrusted?: readonly string[] | undefined;
|
|
96
|
+
|
|
97
|
+
/** The short name of the procedure for people, for example the title of an MCP tool. */
|
|
98
|
+
title?: string | undefined;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The override of the tool hints of the effects (CLI-10). It can only make the tool look more
|
|
102
|
+
* dangerous: `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`,
|
|
103
|
+
* `openWorldHint: true`. The registry refuses each other value.
|
|
104
|
+
*/
|
|
105
|
+
toolHints?: ToolHintOverride | undefined;
|
|
106
|
+
|
|
86
107
|
/**
|
|
87
108
|
* The rules of the procedure under a root of the files (roadmap R33): the inputs that name a
|
|
88
109
|
* path, or the reason that a root cannot confine it. A host with a root (`--fs-root`,
|
|
@@ -329,6 +350,20 @@ export interface ProcedureContext {
|
|
|
329
350
|
*/
|
|
330
351
|
onRef?: ((event: HydrationEvent) => void) | undefined;
|
|
331
352
|
|
|
353
|
+
/**
|
|
354
|
+
* The function that asks the person at the caller for input or a confirmation (CLI-12): MCP
|
|
355
|
+
* elicitation, or another face that can ask. Absent when no one can answer. The helpers
|
|
356
|
+
* `elicitInput` and `elicitConfirmation` then give the action `unavailable`. The nested calls
|
|
357
|
+
* get the same function.
|
|
358
|
+
*/
|
|
359
|
+
elicit?: Elicit | undefined;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The function that tells the caller the progress of this call (an MCP progress notification).
|
|
363
|
+
* Absent when the caller asked for no progress. Only the call of the host gets it.
|
|
364
|
+
*/
|
|
365
|
+
progress?: ReportProgress | undefined;
|
|
366
|
+
|
|
332
367
|
/**
|
|
333
368
|
* The capability of this invocation (CAPABILITIES-2026-10): its clearance, permitted effects
|
|
334
369
|
* and deadline. The nested calls of `ctx.client` get it, or a narrower one. Frozen.
|
|
@@ -347,6 +382,20 @@ export interface ProcedureContext {
|
|
|
347
382
|
* it. The root of the files reads it (roadmap R33).
|
|
348
383
|
*/
|
|
349
384
|
caller?: AnyProcedure | undefined;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The principal of the call: who sends it. The server sets it from its authentication (the
|
|
388
|
+
* token, the request hook `principalOf`, `authenticate`). The nested calls get the same
|
|
389
|
+
* principal. An input or the metadata cannot change it. A call from this process has none.
|
|
390
|
+
*/
|
|
391
|
+
principal?: Principal | undefined;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The output filter of the host for this call: its `admitItem`, with this invocation (roadmap
|
|
395
|
+
* CLI-15). A handler that sends the data of other writers later, for example a subscription,
|
|
396
|
+
* examines each row with it. Absent for a nested call, or when the host has no filter.
|
|
397
|
+
*/
|
|
398
|
+
admit?: ((item: unknown) => Promise<boolean>) | undefined;
|
|
350
399
|
}
|
|
351
400
|
|
|
352
401
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text from outside a host: a web page, a mail, a file of a user (CLI-10).
|
|
3
|
+
*
|
|
4
|
+
* A procedure that gives such text declares the fields of its output that hold it
|
|
5
|
+
* (`metadata.untrusted`). A face that gives text to a model (MCP) then shows that text apart from
|
|
6
|
+
* the data. Envelopes tell the model that this text is data, not instructions. Another face gives
|
|
7
|
+
* the output as it is.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AnyProcedure } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/** The values of the source of a text: the attributes of its envelope. */
|
|
13
|
+
export type UntrustedSource = Readonly<Record<string, string | number | boolean | null | undefined>>;
|
|
14
|
+
|
|
15
|
+
/** A text from outside the host, with its source: for example the URL and the date of a page. */
|
|
16
|
+
export interface UntrustedText {
|
|
17
|
+
readonly text: string;
|
|
18
|
+
readonly source?: UntrustedSource | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The value of an untrusted field of an output: a text, a text with its source, or a list of
|
|
23
|
+
* them.
|
|
24
|
+
*/
|
|
25
|
+
export type UntrustedValue = string | UntrustedText | readonly (string | UntrustedText)[];
|
|
26
|
+
|
|
27
|
+
/** This function gives a text with its source, for an untrusted field of an output. */
|
|
28
|
+
export function untrustedText(text: string, source?: UntrustedSource): UntrustedText {
|
|
29
|
+
return source === undefined ? { text } : { text, source };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The names of the output fields that hold untrusted text (`metadata.untrusted`). Empty: none. */
|
|
33
|
+
export function untrustedFieldsOf(procedure: AnyProcedure): readonly string[] {
|
|
34
|
+
const fields = (procedure.metadata as { untrusted?: unknown } | undefined)?.untrusted;
|
|
35
|
+
return Array.isArray(fields) ? fields.filter((field): field is string => typeof field === "string") : [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The texts of the value of an untrusted field, each with its source. Another value gives none. */
|
|
39
|
+
export function untrustedTextsOf(value: unknown): UntrustedText[] {
|
|
40
|
+
const one = (item: unknown): UntrustedText | undefined => {
|
|
41
|
+
if (typeof item === "string") return { text: item };
|
|
42
|
+
if (item !== null && typeof item === "object" && typeof (item as { text?: unknown }).text === "string") {
|
|
43
|
+
const { text, source } = item as { text: string; source?: unknown };
|
|
44
|
+
return source !== null && typeof source === "object" ? { text, source: source as UntrustedSource } : { text };
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
const items = Array.isArray(value) ? value : [value];
|
|
49
|
+
return items.map(one).filter((item): item is UntrustedText => item !== undefined);
|
|
50
|
+
}
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* no allowed origin is refused (an image tag or a form sends no `Origin` for a GET).
|
|
16
16
|
* - HTTP only: a procedure call by GET or HEAD is refused unless `allowGet` is true, and a
|
|
17
17
|
* request body must be JSON.
|
|
18
|
+
* - A request with ambient credentials (the cookies that a browser sends by itself) is trusted
|
|
19
|
+
* only from a named origin. `"*"` does not open the origin check for it (CSRF). A list or a
|
|
20
|
+
* function of origins still does.
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import type { IncomingHttpHeaders } from "node:http";
|
|
@@ -74,8 +77,9 @@ export function hostNameOf(header: string | undefined): string {
|
|
|
74
77
|
return (header.split(":")[0] ?? "").toLowerCase();
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
function originAllowedBy(origin: string, allowed: BrowserGuardOptions["allowedOrigins"]): boolean {
|
|
78
|
-
|
|
80
|
+
function originAllowedBy(origin: string, allowed: BrowserGuardOptions["allowedOrigins"], ambient: boolean): boolean {
|
|
81
|
+
// "*" names no origin: ambient credentials are not trusted from it
|
|
82
|
+
if (allowed === "*") return !ambient;
|
|
79
83
|
if (typeof allowed === "function") return allowed(origin);
|
|
80
84
|
return (allowed ?? []).includes(origin);
|
|
81
85
|
}
|
|
@@ -102,14 +106,52 @@ function hasBody(headers: IncomingHttpHeaders): boolean {
|
|
|
102
106
|
return length > 0 || headers["transfer-encoding"] !== undefined;
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
/**
|
|
110
|
+
* True when a request carries ambient credentials: a `Cookie` header, which a browser sends by
|
|
111
|
+
* itself, also for a request that another site starts.
|
|
112
|
+
*/
|
|
113
|
+
export function hasAmbientCredentials(headers: IncomingHttpHeaders): boolean {
|
|
114
|
+
const cookie = headers.cookie;
|
|
115
|
+
return typeof cookie === "string" && cookie.trim() !== "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* True when a face trusts a request only from a named origin. This is so when the server reads
|
|
120
|
+
* cookies for each request (`"always"`), or when the request carries a cookie that an auth path can
|
|
121
|
+
* read. `cookiesNamed` is true for a face that trusts no cookie from
|
|
122
|
+
* an origin that is not named, with or without an auth path.
|
|
123
|
+
*
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
export function needsNamedOrigin(mode: "always" | "cookies" | undefined, headers: IncomingHttpHeaders, cookiesNamed: boolean): boolean {
|
|
127
|
+
return mode === "always" || (hasAmbientCredentials(headers) && (cookiesNamed || mode !== undefined));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* This function refuses a face whose server reads cookies for each request (`principalOf`) while
|
|
132
|
+
* its browser checks permit each origin (`"*"`). Each site could then send a request with the cookies
|
|
133
|
+
* of the user (CSRF).
|
|
134
|
+
*
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
export function assertNamedOrigins(face: string, mode: "always" | "cookies" | undefined, allowedOrigins: BrowserGuardOptions["allowedOrigins"], originOption: string): void {
|
|
138
|
+
if (mode !== "always" || allowedOrigins !== "*") return;
|
|
139
|
+
throw new Error(
|
|
140
|
+
`${face}: the request hook principalOf reads the cookies of each request, and the origin "*" lets each site send them (CSRF). Give ${originOption} or allowedOrigins a list or a function of the origins that can call.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
/**
|
|
106
145
|
* The checks of the host, the origin and the fetch site, for HTTP and WebSocket requests.
|
|
107
|
-
* `http` adds the checks of the method and the body.
|
|
146
|
+
* `http` adds the checks of the method and the body. `ambient` is true when the server can trust
|
|
147
|
+
* ambient credentials (cookies) for the request. Then `"*"` does not permit another origin. Only a
|
|
148
|
+
* named origin (a list or a function) does.
|
|
108
149
|
*/
|
|
109
150
|
export function checkBrowserRequest(
|
|
110
151
|
req: GuardedRequest,
|
|
111
152
|
options: BrowserGuardOptions = {},
|
|
112
|
-
http = false
|
|
153
|
+
http = false,
|
|
154
|
+
ambient = false
|
|
113
155
|
): GuardResult {
|
|
114
156
|
const { headers } = req;
|
|
115
157
|
|
|
@@ -123,12 +165,12 @@ export function checkBrowserRequest(
|
|
|
123
165
|
|
|
124
166
|
const origin = typeof headers.origin === "string" ? headers.origin : undefined;
|
|
125
167
|
if (origin !== undefined) {
|
|
126
|
-
if (!sameOrigin(origin, headers.host) && !originAllowedBy(origin, options.allowedOrigins)) {
|
|
168
|
+
if (!sameOrigin(origin, headers.host) && !originAllowedBy(origin, options.allowedOrigins, ambient)) {
|
|
127
169
|
return { ok: false, status: 403, code: "FORBIDDEN_ORIGIN", message: `Forbidden: the origin "${origin}" is not allowed` };
|
|
128
170
|
}
|
|
129
171
|
} else {
|
|
130
172
|
const site = headers["sec-fetch-site"];
|
|
131
|
-
if ((site === "cross-site" || site === "same-site") && options.allowedOrigins !== "*") {
|
|
173
|
+
if ((site === "cross-site" || site === "same-site") && (options.allowedOrigins !== "*" || ambient)) {
|
|
132
174
|
return { ok: false, status: 403, code: "FORBIDDEN_ORIGIN", message: "Forbidden: a request from another site" };
|
|
133
175
|
}
|
|
134
176
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* procedures (a host for an outside caller sets it).
|
|
13
13
|
* - `timeout`: the time limit of each call from a caller (not of its nested calls).
|
|
14
14
|
* - `token`, `browser`: the checks of the network transports (`client-server` applies them).
|
|
15
|
+
* - `principalOf`: the request hook. It reads the credentials of a network request (a cookie, a
|
|
16
|
+
* header, the query) and gives a bearer token, a principal and a narrowing.
|
|
15
17
|
* - `capability`, `readOnly`, `clearanceOrder`, `resources`, `admitItem`: the capability of each
|
|
16
18
|
* call and what the host gives for it (CAPABILITIES-2026-10).
|
|
17
19
|
* - `events`: the remote events of the host (`documentation/EVENTS.md`). The server pins
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
import type { Capability, ClearanceOrder, ProcedurePath, ResourcesOf } from "../procedures/types.js";
|
|
22
24
|
import type { AdmitItem, Interceptor, Invocation } from "../procedures/invoke.js";
|
|
23
25
|
import { narrowCapability } from "../procedures/capability.js";
|
|
24
|
-
import type { ServerRequest } from "./types.js";
|
|
26
|
+
import type { PrincipalOf, ServerRequest } from "./types.js";
|
|
25
27
|
import { timeoutInterceptor } from "../procedures/interceptors.js";
|
|
26
28
|
import type { BrowserGuardOptions } from "./browser-guard.js";
|
|
27
29
|
import type { ProcedureRegistry } from "../procedures/registry.js";
|
|
@@ -73,6 +75,11 @@ export interface HostPolicyOptions {
|
|
|
73
75
|
interceptors?: readonly Interceptor[] | undefined;
|
|
74
76
|
/** The token that each network request must send (HTTP and WebSocket transports). */
|
|
75
77
|
token?: string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* The request hook: it reads the credentials of each network request (a cookie, a header, the
|
|
80
|
+
* query) and gives a bearer token, a principal and a narrowing (`ProcedureServerOptions`).
|
|
81
|
+
*/
|
|
82
|
+
principalOf?: PrincipalOf | undefined;
|
|
76
83
|
/** The browser checks of the network transports (`allowedOrigins`, `allowedHosts`, `allowGet`). */
|
|
77
84
|
browser?: BrowserGuardOptions | undefined;
|
|
78
85
|
/**
|
|
@@ -103,6 +110,8 @@ export interface HostPolicy {
|
|
|
103
110
|
readonly strictCalls: boolean;
|
|
104
111
|
readonly interceptors: readonly Interceptor[];
|
|
105
112
|
readonly token: string | undefined;
|
|
113
|
+
/** The request hook of the network requests, or undefined for none. */
|
|
114
|
+
readonly principalOf?: PrincipalOf | undefined;
|
|
106
115
|
readonly browser: BrowserGuardOptions;
|
|
107
116
|
/** The capability of a call from a caller, or undefined for no limits. */
|
|
108
117
|
readonly capabilityOf: ((request?: ServerRequest<unknown>) => Capability | undefined) | undefined;
|
|
@@ -151,6 +160,7 @@ export function hostPolicy(options: HostPolicyOptions = {}): HostPolicy {
|
|
|
151
160
|
strictCalls: options.strictCalls ?? false,
|
|
152
161
|
interceptors,
|
|
153
162
|
token: options.token,
|
|
163
|
+
...(options.principalOf ? { principalOf: options.principalOf } : {}),
|
|
154
164
|
browser: options.browser ?? {},
|
|
155
165
|
capabilityOf: capabilityOfOptions(options),
|
|
156
166
|
clearanceOrder: options.clearanceOrder,
|
|
@@ -169,6 +179,8 @@ export function procedureServerOptions(policy: HostPolicy, registry?: ProcedureR
|
|
|
169
179
|
strictCalls: policy.strictCalls,
|
|
170
180
|
// The token of the policy: ProcedureServer examines it for each network request (R18)
|
|
171
181
|
...(policy.token !== undefined ? { token: policy.token } : {}),
|
|
182
|
+
// The request hook of the policy: the credentials of a cookie, a header or the query (CLI-2a)
|
|
183
|
+
...(policy.principalOf ? { principalOf: policy.principalOf } : {}),
|
|
172
184
|
...(policy.interceptors.length > 0 ? { interceptors: policy.interceptors } : {}),
|
|
173
185
|
...(policy.capabilityOf ? { capability: policy.capabilityOf } : {}),
|
|
174
186
|
...(policy.clearanceOrder ? { clearanceOrder: policy.clearanceOrder } : {}),
|
package/src/server/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Server, type ServerOptions } from "./server.js";
|
|
9
|
-
import type { Method, Principal, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
9
|
+
import type { AmbientCredentials, Method, Principal, PrincipalOf, RequestIdentity, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
10
10
|
import { methodToPath, pathToMethod } from "./method.js";
|
|
11
11
|
import type {
|
|
12
12
|
AnyProcedure,
|
|
@@ -19,7 +19,7 @@ import { ProcedureRegistry, PROCEDURE_REGISTRY } from "../procedures/registry.js
|
|
|
19
19
|
import { createCollectionProcedures } from "../procedures/collection/procedures.js";
|
|
20
20
|
import { InvocationError, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
|
|
21
21
|
import type { Capability, ClearanceOrder, ResourcesOf } from "../procedures/types.js";
|
|
22
|
-
import { TIMEOUT_METADATA_KEY, effectsOf, narrowCapability } from "../procedures/capability.js";
|
|
22
|
+
import { TIMEOUT_METADATA_KEY, effectsOf, joinNarrowing, narrowCapability } from "../procedures/capability.js";
|
|
23
23
|
import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptions } from "../procedures/idempotency.js";
|
|
24
24
|
import { optionalMs, type Duration } from "../duration.js";
|
|
25
25
|
import type { CollectionStorage } from "../procedures/collection/storage.js";
|
|
@@ -28,6 +28,7 @@ import { realClock, type Clock } from "../clock.js";
|
|
|
28
28
|
import { errorCodeOf } from "../protocol.js";
|
|
29
29
|
import { EventPublisher, type EventPublisherOptions } from "../events/publisher.js";
|
|
30
30
|
import { getGlobalEventBus } from "../events/bus.js";
|
|
31
|
+
import type { CollectionProceduresOptions } from "../procedures/collection/procedures.js";
|
|
31
32
|
|
|
32
33
|
// =============================================================================
|
|
33
34
|
// Types
|
|
@@ -139,6 +140,24 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
139
140
|
*/
|
|
140
141
|
authenticate?: ((request: ServerRequest<unknown>) => Principal | undefined | Promise<Principal | undefined>) | undefined;
|
|
141
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The request hook of the server: it reads the credentials of each network request (a cookie, a
|
|
145
|
+
* header, the query) and gives a bearer token, a principal and a narrowing. It runs before the
|
|
146
|
+
* token check: its bearer token replaces the bearer token of the credentials. Its principal
|
|
147
|
+
* replaces the principal of the token check. `authenticate` runs after it and can read
|
|
148
|
+
* `request.principal`. Undefined or an error refuses the request with `UNAUTHORIZED`. A request
|
|
149
|
+
* without credentials (from this process) does not go through the hook.
|
|
150
|
+
*/
|
|
151
|
+
principalOf?: PrincipalOf | undefined;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* How the server can authenticate a request with ambient credentials (cookies), for the CSRF
|
|
155
|
+
* checks of the network faces. The default is `"always"` with `principalOf`, `"cookies"` with
|
|
156
|
+
* `authenticate`, and none without them. `false`: the server reads no cookies, for example an
|
|
157
|
+
* `authenticate` function that reads only the bearer token.
|
|
158
|
+
*/
|
|
159
|
+
ambientCredentials?: AmbientCredentials | false | undefined;
|
|
160
|
+
|
|
142
161
|
/**
|
|
143
162
|
* The remote events of the server (`documentation/EVENTS.md`): a bus and its topic rule, or an
|
|
144
163
|
* `EventPublisher`. The server pins the procedure `events.subscribe`, outside its registry, so a
|
|
@@ -326,13 +345,19 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
326
345
|
private draining = false;
|
|
327
346
|
/** The calls in flight, a stream until its end */
|
|
328
347
|
private inFlight = 0;
|
|
329
|
-
/** The token of the server, and its authenticate function (R18) */
|
|
348
|
+
/** The token of the server, its request hook and its authenticate function (R18) */
|
|
330
349
|
private readonly token: string | undefined;
|
|
350
|
+
private readonly principalOf: PrincipalOf | undefined;
|
|
331
351
|
private readonly authenticate: ProcedureServerOptions["authenticate"];
|
|
332
352
|
/** The functions that `stop()` waits on: each one runs when no call is in flight */
|
|
333
353
|
private idleWaiters: Array<() => void> = [];
|
|
334
354
|
/** The publisher of the remote events, from the option `events`, or undefined. */
|
|
335
355
|
readonly publisher: EventPublisher | undefined;
|
|
356
|
+
/**
|
|
357
|
+
* How the server can authenticate a request with ambient credentials (cookies). The network faces
|
|
358
|
+
* read it: they trust such a request only from a named origin (CSRF).
|
|
359
|
+
*/
|
|
360
|
+
readonly ambientCredentials: AmbientCredentials | undefined;
|
|
336
361
|
|
|
337
362
|
constructor(options: ProcedureServerOptions = {}) {
|
|
338
363
|
super(options);
|
|
@@ -350,7 +375,10 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
350
375
|
};
|
|
351
376
|
this.idempotency = options.idempotency === false ? undefined : new IdempotencyCache(options.idempotency ?? {});
|
|
352
377
|
this.token = options.token;
|
|
378
|
+
this.principalOf = options.principalOf;
|
|
353
379
|
this.authenticate = options.authenticate;
|
|
380
|
+
const ambient = options.ambientCredentials ?? (this.principalOf ? "always" : this.authenticate ? "cookies" : undefined);
|
|
381
|
+
this.ambientCredentials = ambient === false ? undefined : ambient;
|
|
354
382
|
// The remote events: the server pins events.subscribe, as it pins host.health (EVENTS.md)
|
|
355
383
|
this.publisher = options.events instanceof EventPublisher ? options.events : options.events ? new EventPublisher(options.events) : undefined;
|
|
356
384
|
if (this.publisher) this.registerProcedure(this.publisher.procedure);
|
|
@@ -642,9 +670,11 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
642
670
|
}
|
|
643
671
|
|
|
644
672
|
/**
|
|
645
|
-
* The token check and the principal of a request (roadmap R18).
|
|
646
|
-
* credentials
|
|
647
|
-
*
|
|
673
|
+
* The token check and the principal of a request (roadmap R18). The request hook reads the
|
|
674
|
+
* credentials of a network request first: a bearer token, a principal and a narrowing. A
|
|
675
|
+
* network request (with credentials) must present the token. The authenticate function gives
|
|
676
|
+
* the principal. The function gives an UNAUTHORIZED response, or undefined when the request
|
|
677
|
+
* passes.
|
|
648
678
|
*/
|
|
649
679
|
private async admit(request: ServerRequest<unknown>): Promise<ServerResponse<unknown> | undefined> {
|
|
650
680
|
const unauthorized = (message: string): ServerResponse<unknown> => ({
|
|
@@ -652,10 +682,26 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
652
682
|
status: { type: "error", code: "UNAUTHORIZED", message, retryable: false },
|
|
653
683
|
metadata: {},
|
|
654
684
|
});
|
|
655
|
-
|
|
656
|
-
|
|
685
|
+
const credentials = request.credentials;
|
|
686
|
+
let identity: RequestIdentity | undefined;
|
|
687
|
+
if (this.principalOf && credentials !== undefined) {
|
|
688
|
+
try {
|
|
689
|
+
identity = await this.principalOf(credentials, request);
|
|
690
|
+
} catch {
|
|
691
|
+
identity = undefined;
|
|
692
|
+
}
|
|
693
|
+
if (!identity) return unauthorized("Unauthorized: the server did not accept the credentials");
|
|
694
|
+
}
|
|
695
|
+
if (this.token !== undefined && credentials !== undefined) {
|
|
696
|
+
const bearer = identity?.bearer ?? credentials.bearer;
|
|
697
|
+
if (!tokenMatches(this.token, bearer)) return unauthorized("Unauthorized: this server requires its token");
|
|
657
698
|
request.principal = { id: "token", via: "token" };
|
|
658
699
|
}
|
|
700
|
+
if (identity?.principal) request.principal = identity.principal;
|
|
701
|
+
if (identity?.narrowing) {
|
|
702
|
+
const order = this.capabilityOptions.clearanceOrder;
|
|
703
|
+
request.narrowing = request.narrowing ? joinNarrowing(request.narrowing, identity.narrowing, order) : identity.narrowing;
|
|
704
|
+
}
|
|
659
705
|
if (this.authenticate) {
|
|
660
706
|
let principal: Principal | undefined;
|
|
661
707
|
try {
|
|
@@ -734,6 +780,17 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
734
780
|
return { ...response, id: request.id };
|
|
735
781
|
}
|
|
736
782
|
|
|
783
|
+
/**
|
|
784
|
+
* The capability of a request. It comes from the server, never from the request. The remaining
|
|
785
|
+
* time of the caller and the narrowing of the request can only make it narrower
|
|
786
|
+
* (CAPABILITIES-2026-10, decision 9). Two different clearances without an order throw.
|
|
787
|
+
*/
|
|
788
|
+
private capabilityOf(request: ServerRequest<unknown>): Capability | undefined {
|
|
789
|
+
const { capability, clearanceOrder } = this.capabilityOptions;
|
|
790
|
+
const base = withRequestTimeout(typeof capability === "function" ? capability(request) : capability, request, clearanceOrder);
|
|
791
|
+
return request.narrowing ? narrowCapability(base, request.narrowing, clearanceOrder) : base;
|
|
792
|
+
}
|
|
793
|
+
|
|
737
794
|
/** Run a procedure for a request with a signal. */
|
|
738
795
|
private async run(procedure: AnyProcedure, request: ServerRequest<unknown>, signal: AbortSignal | undefined): Promise<ServerResponse<unknown>> {
|
|
739
796
|
// The call uses the storages that it takes until its end: the end of a stream, for a stream
|
|
@@ -742,21 +799,31 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
742
799
|
try {
|
|
743
800
|
// One invocation path for every host (ARCHITECTURE-PROPOSALS P1): input validation, the
|
|
744
801
|
// context, the expose rule for nested calls (BUGS-2026-07 H18) and output validation
|
|
745
|
-
const {
|
|
802
|
+
const { clearanceOrder, resources, admitItem } = this.capabilityOptions;
|
|
803
|
+
let capability: Capability | undefined;
|
|
804
|
+
try {
|
|
805
|
+
capability = this.capabilityOf(request);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
const message = `The server cannot narrow the capability of the request: ${error instanceof Error ? error.message : String(error)}`;
|
|
808
|
+
return errorResponse(request.id, Object.assign(new Error(message), { code: "INTERNAL_ERROR" }));
|
|
809
|
+
}
|
|
746
810
|
const output = await invokeProcedure(procedure, request.payload, {
|
|
747
811
|
registry: this.procedureRegistry,
|
|
748
812
|
metadata: request.metadata,
|
|
749
813
|
signal,
|
|
814
|
+
// What the face of the call offers: it asks the person, and it shows the progress (CLI-12)
|
|
815
|
+
...(request.elicit ? { elicit: request.elicit } : {}),
|
|
816
|
+
...(request.progress ? { progress: request.progress } : {}),
|
|
750
817
|
repository,
|
|
751
818
|
expose: this.expose,
|
|
752
819
|
strictCalls: this.strictCalls,
|
|
753
820
|
interceptors: this.interceptors,
|
|
754
|
-
|
|
755
|
-
// the caller can only make the deadline earlier (CAPABILITIES-2026-10, decision 9).
|
|
756
|
-
capability: withRequestTimeout(typeof capability === "function" ? capability(request) : capability, request, clearanceOrder),
|
|
821
|
+
capability,
|
|
757
822
|
clearanceOrder,
|
|
758
823
|
resources,
|
|
759
824
|
admitItem,
|
|
825
|
+
// The principal of the authentication: ctx.principal, and the nested calls (R18)
|
|
826
|
+
...(request.principal !== undefined ? { principal: request.principal } : {}),
|
|
760
827
|
// The bus of the remote events is the bus of the procedures (EVENTS.md)
|
|
761
828
|
...(this.publisher ? { bus: this.publisher.bus } : {}),
|
|
762
829
|
});
|
|
@@ -784,11 +851,12 @@ export class ProcedureServer extends Server implements RepositoryProvider {
|
|
|
784
851
|
* Creates standard CRUD procedures using the configured storage.
|
|
785
852
|
*
|
|
786
853
|
* @param collectionName - Collection name
|
|
854
|
+
* @param options - The query policy of `find`. `$regex` is off unless the policy permits it.
|
|
787
855
|
*/
|
|
788
|
-
registerCollectionProcedures(collectionName: string): void {
|
|
856
|
+
registerCollectionProcedures(collectionName: string, options?: CollectionProceduresOptions): void {
|
|
789
857
|
// Uses a top-level import; require() is not available in this ESM package and
|
|
790
858
|
// previously threw "Cannot find module" on every call. See BUGS-2026-07 H13.
|
|
791
|
-
const procedures = createCollectionProcedures(collectionName);
|
|
859
|
+
const procedures = createCollectionProcedures(collectionName, options);
|
|
792
860
|
|
|
793
861
|
// Register with the procedure registry first
|
|
794
862
|
this.procedureRegistry.registerAll(procedures);
|
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The credentials of a network request (roadmap R18). A server transport reads them into
|
|
3
|
-
* `ServerRequest.credentials
|
|
4
|
-
*
|
|
3
|
+
* `ServerRequest.credentials`. It leaves the secret headers out of the metadata, so a handler and a
|
|
4
|
+
* nested call do not see them. `ProcedureServer` examines them.
|
|
5
|
+
*
|
|
6
|
+
* This module imports no Node code: a server in a browser or a worker can use it too.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import type { Credentials } from "./types.js";
|
|
8
10
|
|
|
9
11
|
/** The headers that carry a secret. They do not go into the metadata of a request. */
|
|
10
|
-
const SECRET_HEADERS = new Set(["authorization", "proxy-authorization", "cookie", "x-api-key"]);
|
|
12
|
+
const SECRET_HEADERS: ReadonlySet<string> = new Set(["authorization", "proxy-authorization", "cookie", "x-api-key"]);
|
|
11
13
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* True when a header carries a secret: a header of the default list (`Authorization`,
|
|
16
|
+
* `Proxy-Authorization`, `Cookie`, `X-Api-Key`), or a header of `more`. The names are lower case.
|
|
17
|
+
*/
|
|
18
|
+
export function isSecretHeader(name: string, more?: ReadonlySet<string>): boolean {
|
|
19
|
+
const key = name.toLowerCase();
|
|
20
|
+
return SECRET_HEADERS.has(key) || (more?.has(key) ?? false);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** This function gives a set of lower-case header names. */
|
|
24
|
+
export function headerNames(names: Iterable<string> | undefined): ReadonlySet<string> {
|
|
25
|
+
const set = new Set<string>();
|
|
26
|
+
for (const name of names ?? []) set.add(name.toLowerCase());
|
|
27
|
+
return set;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The headers without the secret ones, and without the headers that are not text. `secret` names
|
|
32
|
+
* more headers that carry a secret, for example the token header of an application.
|
|
33
|
+
*/
|
|
34
|
+
export function withoutSecretHeaders(headers: Record<string, unknown>, secret?: Iterable<string>): Record<string, string> {
|
|
35
|
+
const more = secret === undefined ? undefined : headerNames(secret);
|
|
14
36
|
const out: Record<string, string> = {};
|
|
15
37
|
for (const [key, value] of Object.entries(headers)) {
|
|
16
|
-
if (!
|
|
38
|
+
if (!isSecretHeader(key, more) && typeof value === "string") out[key] = value;
|
|
17
39
|
}
|
|
18
40
|
return out;
|
|
19
41
|
}
|
|
@@ -33,3 +55,103 @@ export function credentialsOf(request: { headers: Record<string, unknown>; url?:
|
|
|
33
55
|
const query = new URL(request.url ?? "/", "http://localhost").searchParams.get("token");
|
|
34
56
|
return query === null ? {} : { bearer: query };
|
|
35
57
|
}
|
|
58
|
+
|
|
59
|
+
/** The text of a header value: a list of values becomes one text, with ", " between the values. */
|
|
60
|
+
function headerText(value: unknown): string | undefined {
|
|
61
|
+
if (typeof value === "string") return value;
|
|
62
|
+
if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string").join(", ");
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** This function decodes a cookie value. A value that is not valid URI encoding stays as it is. */
|
|
67
|
+
function decodeCookieValue(value: string): string {
|
|
68
|
+
const unquoted = value.length >= 2 && value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
|
|
69
|
+
try {
|
|
70
|
+
return decodeURIComponent(unquoted);
|
|
71
|
+
} catch {
|
|
72
|
+
return unquoted;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The cookies of a `Cookie` header, by name. The function decodes each URI-encoded value when it
|
|
78
|
+
* can. When two cookies have the same name, the first one stays: a browser sends the
|
|
79
|
+
* cookie of the longest path first. A part without `=` is not a cookie.
|
|
80
|
+
*/
|
|
81
|
+
export function cookiesOf(header: unknown): Record<string, string> {
|
|
82
|
+
const text = Array.isArray(header) ? header.filter((item): item is string => typeof item === "string").join("; ") : header;
|
|
83
|
+
if (typeof text !== "string" || text === "") return {};
|
|
84
|
+
const cookies = new Map<string, string>();
|
|
85
|
+
for (const part of text.split(";")) {
|
|
86
|
+
const eq = part.indexOf("=");
|
|
87
|
+
if (eq <= 0) continue;
|
|
88
|
+
const name = part.slice(0, eq).trim();
|
|
89
|
+
if (name === "" || cookies.has(name)) continue;
|
|
90
|
+
cookies.set(name, decodeCookieValue(part.slice(eq + 1).trim()));
|
|
91
|
+
}
|
|
92
|
+
// fromEntries defines own properties: a cookie named "__proto__" changes no prototype
|
|
93
|
+
return Object.fromEntries(cookies);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The parts of a request URL: its path and its query. The function does not resolve the URL
|
|
98
|
+
* against a host, so a path that starts with "//" stays a path.
|
|
99
|
+
*/
|
|
100
|
+
export function urlParts(url: string | undefined): { path: string; query: URLSearchParams } {
|
|
101
|
+
let raw = url ?? "/";
|
|
102
|
+
// The absolute form of a request line (a request through a proxy): the path and the query only
|
|
103
|
+
if (!raw.startsWith("/") && /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
|
|
104
|
+
try {
|
|
105
|
+
const absolute = new URL(raw);
|
|
106
|
+
raw = `${absolute.pathname}${absolute.search}`;
|
|
107
|
+
} catch {
|
|
108
|
+
// Not a URL: the text stays a path
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const mark = raw.indexOf("?");
|
|
112
|
+
return mark === -1 ? { path: raw, query: new URLSearchParams() } : { path: raw.slice(0, mark), query: new URLSearchParams(raw.slice(mark + 1)) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The first value of each query parameter, by name. */
|
|
116
|
+
export function queryRecord(query: URLSearchParams): Record<string, string> {
|
|
117
|
+
const values = new Map<string, string>();
|
|
118
|
+
for (const [key, value] of query) if (!values.has(key)) values.set(key, value);
|
|
119
|
+
return Object.fromEntries(values);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The options of `requestCredentials`. */
|
|
123
|
+
export interface RequestCredentialsOptions {
|
|
124
|
+
/**
|
|
125
|
+
* The query parameter whose value is the bearer token of a request without an
|
|
126
|
+
* `Authorization: Bearer` header. A browser WebSocket cannot set headers, so the WebSocket
|
|
127
|
+
* server reads `token`. `false` reads no query parameter.
|
|
128
|
+
* @default false
|
|
129
|
+
*/
|
|
130
|
+
readonly tokenQuery?: string | false | undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The credentials of a network request (roadmap R18): the bearer token, the cookies, each header
|
|
135
|
+
* (the secret ones too) and the first value of each query parameter. The request hook of a server
|
|
136
|
+
* (`principalOf`) reads them. They are not in the metadata, so a handler does not see them.
|
|
137
|
+
*/
|
|
138
|
+
export function requestCredentials(
|
|
139
|
+
request: { headers: Record<string, unknown>; url?: string | undefined },
|
|
140
|
+
options: RequestCredentialsOptions = {}
|
|
141
|
+
): Credentials {
|
|
142
|
+
const headers = new Map<string, string>();
|
|
143
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
144
|
+
const text = headerText(value);
|
|
145
|
+
if (text !== undefined) headers.set(key.toLowerCase(), text);
|
|
146
|
+
}
|
|
147
|
+
const { query } = urlParts(request.url);
|
|
148
|
+
const fromHeader = bearerOf(headers.get("authorization"));
|
|
149
|
+
const fromQuery = options.tokenQuery ? query.get(options.tokenQuery) ?? undefined : undefined;
|
|
150
|
+
const bearer = fromHeader ?? fromQuery;
|
|
151
|
+
return {
|
|
152
|
+
...(bearer !== undefined ? { bearer } : {}),
|
|
153
|
+
cookies: cookiesOf(headers.get("cookie")),
|
|
154
|
+
headers: Object.fromEntries(headers),
|
|
155
|
+
query: queryRecord(query),
|
|
156
|
+
};
|
|
157
|
+
}
|