@gjermundgaraba/effect-actions 0.9.0 → 0.11.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/README.md +56 -50
- package/dist/Action.d.mts +286 -79
- package/dist/Action.mjs +125 -23
- package/dist/ActionCli.d.mts +102 -31
- package/dist/ActionCli.mjs +114 -16
- package/dist/ActionHttp.d.mts +170 -51
- package/dist/ActionHttp.mjs +130 -79
- package/dist/ActionMcp.d.mts +71 -21
- package/dist/ActionMcp.mjs +231 -80
- package/dist/ActionToolkit.d.mts +56 -27
- package/dist/ActionToolkit.mjs +16 -5
- package/dist/Authentication.d.mts +129 -33
- package/dist/Authentication.mjs +231 -38
- package/dist/Testing.d.mts +84 -57
- package/dist/Testing.mjs +121 -79
- package/dist/internal/actions.d.mts +5 -25
- package/dist/internal/actions.mjs +75 -27
- package/dist/internal/authentication.d.mts +102 -0
- package/dist/internal/authentication.mjs +11 -0
- package/dist/internal/call.d.mts +12 -0
- package/dist/internal/call.mjs +12 -0
- package/dist/internal/cli.d.mts +36 -22
- package/dist/internal/cli.mjs +311 -33
- package/dist/internal/client.d.mts +43 -0
- package/dist/internal/client.mjs +25 -0
- package/dist/internal/console.mjs +77 -0
- package/dist/internal/declared.d.mts +13 -0
- package/dist/internal/declared.mjs +34 -0
- package/dist/internal/errors.d.mts +77 -0
- package/dist/internal/errors.mjs +79 -0
- package/dist/internal/implementation.d.mts +171 -41
- package/dist/internal/implementation.mjs +147 -29
- package/dist/internal/mcp.d.mts +14 -0
- package/dist/internal/mcp.mjs +182 -0
- package/dist/internal/media.d.mts +8 -0
- package/dist/internal/media.mjs +26 -0
- package/dist/internal/memory.d.mts +6 -0
- package/dist/internal/memory.mjs +38 -0
- package/dist/internal/refusal.mjs +67 -0
- package/dist/internal/schema-errors.mjs +20 -0
- package/dist/internal/tools.mjs +27 -57
- package/docs/Action.md +403 -73
- package/docs/ActionCli.md +281 -51
- package/docs/ActionHttp.md +267 -82
- package/docs/ActionMcp.md +232 -102
- package/docs/ActionToolkit.md +99 -35
- package/docs/Authentication.md +369 -62
- package/docs/CONTEXT.md +24 -36
- package/docs/README.md +29 -73
- package/docs/Testing.md +154 -77
- package/docs/guarantees.md +82 -43
- package/docs/setup.md +95 -27
- package/package.json +12 -20
- package/dist/ActionCatalog.d.mts +0 -30
- package/dist/ActionCatalog.mjs +0 -39
- package/dist/ActionCliClient.d.mts +0 -48
- package/dist/ActionCliClient.mjs +0 -43
- package/dist/ActionGroup.d.mts +0 -44
- package/dist/ActionGroup.mjs +0 -35
- package/dist/ActionHttpClient.d.mts +0 -33
- package/dist/ActionHttpClient.mjs +0 -16
- package/dist/TestingClient.d.mts +0 -17
- package/dist/TestingClient.mjs +0 -22
- package/dist/internal/fetchClient.d.mts +0 -6
- package/dist/internal/fetchClient.mjs +0 -8
- package/dist/internal/tools.d.mts +0 -22
- package/docs/ActionCatalog.md +0 -54
- package/docs/ActionCliClient.md +0 -63
- package/docs/ActionGroup.md +0 -128
- package/docs/ActionHttpClient.md +0 -80
|
@@ -1,46 +1,142 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Refusal } from "./internal/errors.mjs";
|
|
2
|
+
import { Known, OptionalUnless } from "./internal/implementation.mjs";
|
|
3
|
+
import { Errors } from "./Action.mjs";
|
|
4
|
+
import { Any, Descriptor, Provider, Security } from "./internal/authentication.mjs";
|
|
5
|
+
import { ErrorsOf } from "./internal/declared.mjs";
|
|
6
|
+
import { Array, Context, Effect, Layer, Option, Redacted, Scope } from "effect";
|
|
2
7
|
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
3
|
-
import {
|
|
8
|
+
import { HttpApiSecurity } from "effect/http-api";
|
|
4
9
|
//#region src/Authentication.d.ts
|
|
5
10
|
/**
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* including private failures serialized by enclosing middleware.
|
|
11
|
+
* The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
|
|
12
|
+
* `HttpApiSecurity.bearer` reads it for routes and MCP, which a test holds equal. It is for a
|
|
13
|
+
* caller the router never routes, which holds the header and no request; a route of the host's
|
|
14
|
+
* own is authenticated by `protect`. The scheme is matched case-insensitively, as RFC 9110
|
|
15
|
+
* requires, and the token is `Redacted`, as Effect's own decoder gives it.
|
|
12
16
|
*/
|
|
13
|
-
export declare const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
error: never;
|
|
17
|
-
requires: Exclude<R, HttpRouter.Provided> | Exclude<Exclude<import("effect/Types").NoInfer<I>, I>, HttpRouter.Provided>;
|
|
18
|
-
layerError: never;
|
|
19
|
-
layerRequires: never;
|
|
20
|
-
}>;
|
|
21
|
-
/** RFC 9728 metadata to publish; the host is responsible for these being valid OAuth URLs. */
|
|
22
|
-
export interface ProtectedResourceOptions {
|
|
17
|
+
export declare const bearerTokenOf: (authorization: string | null | undefined) => Option.Option<Redacted.Redacted<string>>;
|
|
18
|
+
/** An OAuth protected resource (RFC 9728), as `layer` publishes it. */
|
|
19
|
+
export interface ProtectedResource {
|
|
23
20
|
/** Exact OAuth resource identifier; its path and query select the discovery path. */
|
|
24
21
|
readonly resource: string;
|
|
25
|
-
|
|
22
|
+
/** Where clients get tokens: nonempty. */
|
|
23
|
+
readonly authorizationServers: Array.NonEmptyReadonlyArray<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Every scope the resource accepts, each an OAuth scope token, which a client requests when a
|
|
26
|
+
* 401 names none.
|
|
27
|
+
*/
|
|
26
28
|
readonly scopesSupported?: ReadonlyArray<string>;
|
|
29
|
+
/**
|
|
30
|
+
* The scopes every 401 names, each an OAuth scope token: what a client requests when it
|
|
31
|
+
* authenticates, rather than every scope supported. A `Forbidden` naming scopes asks for
|
|
32
|
+
* more when a call needs them.
|
|
33
|
+
*/
|
|
34
|
+
readonly scopesRequired?: Array.NonEmptyReadonlyArray<string>;
|
|
27
35
|
readonly resourceName?: string;
|
|
28
36
|
}
|
|
29
|
-
/**
|
|
30
|
-
export interface
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
/** What `refusalResponse` answers about. */
|
|
38
|
+
export interface RefusalResponseOptions<D extends Any = Any> {
|
|
39
|
+
/**
|
|
40
|
+
* The descriptor refusing, whose scheme decides the challenge: Bearer's, below, when left
|
|
41
|
+
* out; another scheme's 401 names that scheme, and nothing steps up under it. An error it
|
|
42
|
+
* declares is sent as its endpoints declare it.
|
|
43
|
+
*/
|
|
44
|
+
readonly authentication?: D | undefined;
|
|
45
|
+
/** The OAuth protected resource refusing: the one given to `layer`. */
|
|
46
|
+
readonly protectedResource?: ProtectedResource | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The request's `Authorization` header, as the web `Headers.get` gives it, which decides whether a
|
|
49
|
+
* 401 names `invalid_token`.
|
|
50
|
+
*/
|
|
51
|
+
readonly authorization?: string | null | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** What the descriptor `D` declares its verifier fails with besides a refusal. */
|
|
54
|
+
type DeclaredOf<D> = D extends {
|
|
55
|
+
readonly error: infer E extends Errors;
|
|
56
|
+
} ? E[number]["Type"] : never;
|
|
57
|
+
/**
|
|
58
|
+
* The response `layer` answers a refusal with, for a caller the router never routes, such as
|
|
59
|
+
* a Node `upgrade` handler admitting a socket: its status, its JSON, `Cache-Control: no-store`
|
|
60
|
+
* and its challenge, an `Unauthenticated`'s `Bearer`, naming `invalid_token` when
|
|
61
|
+
* `authorization` presented a bearer token, or the `insufficient_scope` of a `Forbidden`
|
|
62
|
+
* naming scopes. Given the protected resource `layer` was, every challenge names its metadata
|
|
63
|
+
* URL and a 401's its `scopesRequired`. Under a descriptor of another scheme, it is the JSON
|
|
64
|
+
* and status alone, a 401 naming that scheme. An error `authentication` declares is sent as
|
|
65
|
+
* its endpoints declare it, its JSON with its status. `HttpServerResponse.toWeb` gives it as a
|
|
66
|
+
* web `Response`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const refusalResponse: <D extends Any = never>(error: Refusal | DeclaredOf<D>, options?: RefusalResponseOptions<D>) => HttpServerResponse.HttpServerResponse;
|
|
69
|
+
/** What `make` takes besides the name and the identity. */
|
|
70
|
+
export interface Options {
|
|
71
|
+
/** The one native scheme a caller proves the identity by: Bearer when left out. */
|
|
72
|
+
readonly security?: Security;
|
|
73
|
+
/**
|
|
74
|
+
* What the verifier may fail with besides a refusal, such as its provider being unreachable:
|
|
75
|
+
* one schema, or a list. Every protected endpoint declares it, so clients decode it.
|
|
76
|
+
*/
|
|
77
|
+
readonly error?: Errors | Errors[number];
|
|
78
|
+
}
|
|
79
|
+
/** The scheme options `O` give: Bearer too wherever they may leave it out, as at run time. */
|
|
80
|
+
type SecurityOf<O> = O extends {
|
|
81
|
+
readonly security: infer S extends Security;
|
|
82
|
+
} ? S : "security" extends keyof O ? NonNullable<O["security" & keyof O]> | HttpApiSecurity.Http : HttpApiSecurity.Http;
|
|
83
|
+
/**
|
|
84
|
+
* Browser-safe declaration shared by clients and remote surfaces: the `identity` it
|
|
85
|
+
* authenticates, by the one native scheme `security`, Bearer unless it names another, such
|
|
86
|
+
* as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`. The literal name
|
|
87
|
+
* identifies the provider, like a native Context.Key name, and the scheme's OpenAPI key, so it
|
|
88
|
+
* holds only letters, digits, `_`, `.` and `-`; reuse one name only for one declaration.
|
|
89
|
+
*/
|
|
90
|
+
export declare const make: <const Name extends string, I, A, const O extends Options = {}>(name: Name, identity: Context.Key<I, A>, ...options: OptionalUnless<O, O & NoInfer<Known<O, Options>>>) => Descriptor<I, A, SecurityOf<O>, Name, ErrorsOf<O>>;
|
|
91
|
+
/**
|
|
92
|
+
* A descriptor's verifier: the credential its scheme decodes, as Effect's own decoder gives
|
|
93
|
+
* it, to the identity, or a refusal, or an error the descriptor declares, `E`. Its services
|
|
94
|
+
* belong to the request.
|
|
95
|
+
*/
|
|
96
|
+
export type Verify<A, S extends Security, R, E = never> = (credential: HttpApiSecurity.HttpApiSecurity.Type<S>) => Effect.Effect<A, Refusal | E, R>;
|
|
97
|
+
export interface LayerOptions<EP = never, RP = never> {
|
|
98
|
+
readonly protectedResource?: ProtectedResource | Effect.Effect<ProtectedResource | undefined, EP, RP>;
|
|
35
99
|
}
|
|
36
100
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
101
|
+
* Only a Bearer scheme publishes an OAuth protected resource. The types tell only an `Http`
|
|
102
|
+
* scheme from the others, as its scheme is a `string`: `layer` refuses another `Http` one.
|
|
39
103
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
metadataUrl: string;
|
|
43
|
-
/** The `WWW-Authenticate` value; parameter values are quoted and escaped. */
|
|
44
|
-
challenge: (challenge?: BearerChallengeOptions) => string;
|
|
104
|
+
type ResourceOf<S> = [S] extends [HttpApiSecurity.Http] ? unknown : {
|
|
105
|
+
readonly protectedResource?: undefined;
|
|
45
106
|
};
|
|
46
|
-
|
|
107
|
+
/**
|
|
108
|
+
* The provider of `authentication`: `verify`, or an Effect building it once per layer graph, run on
|
|
109
|
+
* each remote request a protected action receives, with the credential the descriptor's
|
|
110
|
+
* scheme decodes. A Bearer scheme answers each 401 with its challenge, and may publish the
|
|
111
|
+
* OAuth protected resource `protectedResource`.
|
|
112
|
+
*/
|
|
113
|
+
export declare function layer<const Name extends string, I, A, const S extends Security, E extends Errors, R, EX = never, RX = never, EP = never, RP = never>(authentication: Descriptor<I, A, S, Name, E>, verify: Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>> | Effect.Effect<Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>>, EX, RX>, options?: LayerOptions<EP, RP> & ResourceOf<S>): Layer.Layer<Provider<I, Name>, EX | EP, HttpRouter.HttpRouter | Exclude<RX | RP, Scope.Scope> | HttpRouter.Request.From<"Requires", Exclude<R, HttpRouter.Provided>>>;
|
|
114
|
+
/** The identity `I`, or none where it is erased to `unknown`. */
|
|
115
|
+
type Provides<I> = unknown extends I ? never : I;
|
|
116
|
+
/**
|
|
117
|
+
* What `protect` returns for a descriptor named `Name` authenticating the identity `I`: router
|
|
118
|
+
* middleware providing it, answering a refusal its routes fail with, and requiring the
|
|
119
|
+
* descriptor's provider. Named so a package emitting declarations can export one.
|
|
120
|
+
*/
|
|
121
|
+
export type Protection<I, Name extends string> = HttpRouter.Middleware<{
|
|
122
|
+
provides: Provides<I>;
|
|
123
|
+
handles: Refusal;
|
|
124
|
+
error: never;
|
|
125
|
+
requires: never;
|
|
126
|
+
layerError: never;
|
|
127
|
+
layerRequires: Provider<I, Name>;
|
|
128
|
+
}>;
|
|
129
|
+
/**
|
|
130
|
+
* Native router middleware authenticating a route of the host's own, such as an export, a
|
|
131
|
+
* page frame or a WebSocket upgrade, with `authentication`'s provider, as its actions' routes
|
|
132
|
+
* are: the credential its scheme decodes, verified by the same verifier, and the identity
|
|
133
|
+
* provided to the route. It answers as an action route: a missing or invalid credential with
|
|
134
|
+
* the refusal, every response to the request it authenticates `no-store` unless it states its
|
|
135
|
+
* own caching, and a refusal the route fails with, such as a scope check's `Forbidden`, as its
|
|
136
|
+
* status, JSON and challenge, stepping up under Bearer. The layer requires the provider,
|
|
137
|
+
* `layer(authentication, ...)`. An erased descriptor, `Any`, provides no service the types can
|
|
138
|
+
* name: its identity is `unknown`, which would discharge every request service the route owes.
|
|
139
|
+
*/
|
|
140
|
+
export declare const protect: <I, A, S extends Security, Name extends string, E extends Errors>(authentication: Descriptor<I, A, S, Name, E>) => Protection<I, Name>;
|
|
141
|
+
//#endregion
|
|
142
|
+
export type { Any, Descriptor, Provider };
|
package/dist/Authentication.mjs
CHANGED
|
@@ -1,55 +1,248 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Unauthenticated, scopeToken } from "./internal/errors.mjs";
|
|
2
|
+
import { assertOwnTags, errorList } from "./internal/actions.mjs";
|
|
3
|
+
import { declaredResponse } from "./internal/declared.mjs";
|
|
4
|
+
import { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain } from "./internal/refusal.mjs";
|
|
5
|
+
import { Context, Effect, Layer, Match, Option, Predicate, Redacted } from "effect";
|
|
2
6
|
import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
7
|
+
import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
|
|
3
8
|
//#region src/Authentication.ts
|
|
4
9
|
/**
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* while the handler is running. Every response is marked `Cache-Control: no-store`,
|
|
10
|
-
* including private failures serialized by enclosing middleware.
|
|
10
|
+
* An `Authorization` header of the `Bearer` scheme, and its token, as Effect's own
|
|
11
|
+
* `HttpApiSecurity.bearer` decodes a request's: the scheme, matched case-insensitively, one or
|
|
12
|
+
* more spaces, then the rest of the header, whatever it holds, a tab included. The whitespace
|
|
13
|
+
* around a header value is no part of it, and an HTTP parser strips it before any route reads it.
|
|
11
14
|
*/
|
|
12
|
-
const
|
|
13
|
-
onFailure: Effect.succeed,
|
|
14
|
-
onSuccess: (identity) => Effect.provideService(httpEffect, service, identity)
|
|
15
|
-
}), HttpEffect.withPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "no-store")))));
|
|
16
|
-
const quoted = (value) => `"${value.replace(/["\\]/g, "\\$&")}"`;
|
|
15
|
+
const bearerScheme = /^[ \t]*Bearer +(.*[^ \t])[ \t]*$/i;
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
|
|
18
|
+
* `HttpApiSecurity.bearer` reads it for routes and MCP, which a test holds equal. It is for a
|
|
19
|
+
* caller the router never routes, which holds the header and no request; a route of the host's
|
|
20
|
+
* own is authenticated by `protect`. The scheme is matched case-insensitively, as RFC 9110
|
|
21
|
+
* requires, and the token is `Redacted`, as Effect's own decoder gives it.
|
|
20
22
|
*/
|
|
21
|
-
const
|
|
23
|
+
const bearerTokenOf = (authorization) => Option.map(Option.fromNullishOr(bearerScheme.exec(authorization ?? "")?.[1]), (token) => Redacted.make(token));
|
|
24
|
+
/**
|
|
25
|
+
* The RFC 9728 metadata URL of `options`: `/.well-known/oauth-protected-resource` followed
|
|
26
|
+
* by the resource's path, where MCP clients look when a 401 names no metadata URL.
|
|
27
|
+
*/
|
|
28
|
+
const metadataUrl = (options) => {
|
|
22
29
|
const resource = new URL(options.resource);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
const url = new URL(resource);
|
|
31
|
+
url.pathname = `/.well-known/oauth-protected-resource${resource.pathname === "/" ? "" : resource.pathname}`;
|
|
32
|
+
return url;
|
|
33
|
+
};
|
|
34
|
+
/** What challenges about `options` name. */
|
|
35
|
+
const namedOf = (options) => ({
|
|
36
|
+
scope: options?.scopesRequired?.join(" "),
|
|
37
|
+
metadataUrl: options === void 0 ? void 0 : metadataUrl(options).href
|
|
38
|
+
});
|
|
39
|
+
/** Whether a request whose `Authorization` header is `authorization` presented a bearer token. */
|
|
40
|
+
const tokenPresented = (authorization) => Option.isSome(bearerTokenOf(authorization));
|
|
41
|
+
/**
|
|
42
|
+
* The challenge of a 401: `Bearer`, naming the scopes a client requests and the metadata URL
|
|
43
|
+
* where it finds its authorization server. A request that presented a bearer token is told
|
|
44
|
+
* it is invalid, RFC 6750's `invalid_token`, on which a client may refresh its token before
|
|
45
|
+
* it signs in again; one that presented none names no error code.
|
|
46
|
+
*/
|
|
47
|
+
const challengeOf = (named, presented) => bearer([
|
|
48
|
+
["error", presented ? "invalid_token" : void 0],
|
|
49
|
+
["scope", named.scope],
|
|
50
|
+
["resource_metadata", named.metadataUrl]
|
|
51
|
+
]);
|
|
52
|
+
/**
|
|
53
|
+
* Refuse a protected resource that cannot be published, as `layer` and `refusalResponse` both
|
|
54
|
+
* do: one under a scheme other than Bearer (`oauth` false), a `scopesSupported` or
|
|
55
|
+
* `scopesRequired` that is no list of OAuth scope tokens, as `Forbidden` refuses, and a
|
|
56
|
+
* resource with a fragment, which no request URL carries, so its discovery would never answer
|
|
57
|
+
* (RFC 9728). A resource an Effect builds is checked for its scheme alone, and for the rest
|
|
58
|
+
* once built.
|
|
59
|
+
*/
|
|
60
|
+
const assertResource = (oauth, options) => {
|
|
61
|
+
if (options === void 0) return;
|
|
62
|
+
if (!oauth) throw new Error("A protected resource is published only for a Bearer scheme");
|
|
63
|
+
if (Effect.isEffect(options)) return;
|
|
64
|
+
for (const key of ["scopesSupported", "scopesRequired"]) {
|
|
65
|
+
const invalid = options[key]?.find((scope) => !scopeToken.test(scope));
|
|
66
|
+
if (invalid !== void 0) throw new Error(`Invalid scope in ${key}: "${invalid}"`);
|
|
67
|
+
}
|
|
68
|
+
if (new URL(options.resource).href.includes("#")) throw new Error(`A protected resource has no fragment: ${options.resource}`);
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* `response` as the routes of the authentication answer: `no-store` unless it states its
|
|
72
|
+
* own caching, and a 401 challenged with `challenge` unless it names its own.
|
|
73
|
+
*/
|
|
74
|
+
const settle = (response, challenge) => HttpServerResponse.setHeaders(response, {
|
|
75
|
+
...response.headers["cache-control"] === void 0 ? { "cache-control": "no-store" } : {},
|
|
76
|
+
...challenge !== void 0 && response.status === 401 && response.headers["www-authenticate"] === void 0 ? { "www-authenticate": challenge } : {}
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* The response `layer` answers a refusal with, for a caller the router never routes, such as
|
|
80
|
+
* a Node `upgrade` handler admitting a socket: its status, its JSON, `Cache-Control: no-store`
|
|
81
|
+
* and its challenge, an `Unauthenticated`'s `Bearer`, naming `invalid_token` when
|
|
82
|
+
* `authorization` presented a bearer token, or the `insufficient_scope` of a `Forbidden`
|
|
83
|
+
* naming scopes. Given the protected resource `layer` was, every challenge names its metadata
|
|
84
|
+
* URL and a 401's its `scopesRequired`. Under a descriptor of another scheme, it is the JSON
|
|
85
|
+
* and status alone, a 401 naming that scheme. An error `authentication` declares is sent as
|
|
86
|
+
* its endpoints declare it, its JSON with its status. `HttpServerResponse.toWeb` gives it as a
|
|
87
|
+
* web `Response`.
|
|
88
|
+
*/
|
|
89
|
+
const refusalResponse = (error, options) => {
|
|
90
|
+
const authentication = options?.authentication;
|
|
91
|
+
const oauth = authentication === void 0 || isBearer(authentication.security);
|
|
92
|
+
assertResource(oauth, options?.protectedResource);
|
|
93
|
+
const named = namedOf(options?.protectedResource);
|
|
94
|
+
const challenge = authentication !== void 0 && !oauth ? schemeChallenge(authentication.security, authentication.name) : challengeOf(named, tokenPresented(options?.authorization));
|
|
95
|
+
if (!isRefusal(error)) {
|
|
96
|
+
const response = declaredResponse(authentication?.error ?? [], error);
|
|
97
|
+
if (response === void 0) throw new Error("Not a refusal, nor an error the authentication declares");
|
|
98
|
+
return settle(response, challenge);
|
|
99
|
+
}
|
|
100
|
+
return settle(oauth ? answer(error, named.metadataUrl) : plain(error), challenge);
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Whether a request URL may resolve to a discovery path, which every other request skips
|
|
104
|
+
* parsing: the URL parser copies `.well-known` as it is, removing only a tab or a newline. Node
|
|
105
|
+
* refuses a target holding those before routing; a platform passing one through still parses.
|
|
106
|
+
*/
|
|
107
|
+
const mayDiscover = /\.well-known|[\t\n\r]/;
|
|
108
|
+
/**
|
|
109
|
+
* RFC 9728 discovery of `options` at its metadata URL, as global router middleware: it
|
|
110
|
+
* answers before routing, so no route middleware, authentication included, ever covers it.
|
|
111
|
+
* The metadata is public to every origin, as a browser client needs it after a 401: it
|
|
112
|
+
* carries `Access-Control-Allow-Origin: *`, and discovery answers its own CORS preflight.
|
|
113
|
+
* Where the host's CORS middleware runs before it, that policy answers the preflight and
|
|
114
|
+
* adds its headers to reads, which keep the `*` where it sets no origin.
|
|
115
|
+
*/
|
|
116
|
+
const discovery = (options) => {
|
|
117
|
+
const discoveryUrl = metadataUrl(options);
|
|
118
|
+
const target = discoveryUrl.href.slice(discoveryUrl.origin.length);
|
|
119
|
+
const metadata = HttpServerResponse.jsonUnsafe({
|
|
29
120
|
resource: options.resource,
|
|
30
121
|
authorization_servers: options.authorizationServers,
|
|
31
122
|
bearer_methods_supported: ["header"],
|
|
32
123
|
scopes_supported: options.scopesSupported,
|
|
33
124
|
resource_name: options.resourceName
|
|
125
|
+
}, { headers: { "access-control-allow-origin": "*" } });
|
|
126
|
+
/** The preflight of a cross-origin read, allowing the headers it asks for. */
|
|
127
|
+
const preflight = (request) => {
|
|
128
|
+
const requested = request.headers["access-control-request-headers"];
|
|
129
|
+
return HttpServerResponse.empty({
|
|
130
|
+
status: 204,
|
|
131
|
+
headers: {
|
|
132
|
+
"access-control-allow-origin": "*",
|
|
133
|
+
"access-control-allow-methods": "GET, HEAD, OPTIONS",
|
|
134
|
+
...requested === void 0 ? {} : {
|
|
135
|
+
"access-control-allow-headers": requested,
|
|
136
|
+
vary: "Access-Control-Request-Headers"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
return (next) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => {
|
|
142
|
+
const targeted = mayDiscover.test(request.url) && URL.canParse(request.url, discoveryUrl.origin) && new URL(request.url, discoveryUrl.origin).href.slice(discoveryUrl.origin.length) === target;
|
|
143
|
+
if (targeted && (request.method === "GET" || request.method === "HEAD")) return Effect.succeed(metadata);
|
|
144
|
+
if (targeted && request.method === "OPTIONS") return Effect.succeed(preflight(request));
|
|
145
|
+
return next;
|
|
34
146
|
});
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* What `layer` answers about the protected resource `options`, written once: its metadata
|
|
150
|
+
* URL, each 401's challenge, to a request that presented a bearer token and to one that did
|
|
151
|
+
* not, and its discovery, none without a resource.
|
|
152
|
+
*/
|
|
153
|
+
const answersOf = (options) => {
|
|
154
|
+
const named = namedOf(options);
|
|
35
155
|
return {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return yield* next;
|
|
41
|
-
}), { global: true }),
|
|
42
|
-
metadataUrl,
|
|
43
|
-
/** The `WWW-Authenticate` value; parameter values are quoted and escaped. */
|
|
44
|
-
challenge: (challenge = {}) => {
|
|
45
|
-
return `Bearer ${[
|
|
46
|
-
["resource_metadata", metadataUrl],
|
|
47
|
-
["error", challenge.error],
|
|
48
|
-
["error_description", challenge.errorDescription],
|
|
49
|
-
["scope", challenge.scope]
|
|
50
|
-
].flatMap(([name, value]) => value === void 0 ? [] : [`${name}=${quoted(value)}`]).join(", ")}`;
|
|
51
|
-
}
|
|
156
|
+
metadataUrl: named.metadataUrl,
|
|
157
|
+
invalid: challengeOf(named, true),
|
|
158
|
+
anonymous: challengeOf(named, false),
|
|
159
|
+
published: options === void 0 ? void 0 : discovery(options)
|
|
52
160
|
};
|
|
53
161
|
};
|
|
162
|
+
/** Whether `security` is the Bearer scheme, the one OAuth challenges and discovery name. */
|
|
163
|
+
const isBearer = (security) => Predicate.isTagged(security, "Http") && security.scheme.toLowerCase() === "bearer";
|
|
164
|
+
/** What an OpenAPI component key may hold, and so a descriptor's name, which is its key. */
|
|
165
|
+
const componentKey = /^[\w.-]+$/;
|
|
166
|
+
/**
|
|
167
|
+
* The challenge of a 401 under a scheme other than Bearer: the scheme an `Http` one names,
|
|
168
|
+
* Basic with its realm, the descriptor's name; an API key has no scheme to name.
|
|
169
|
+
*/
|
|
170
|
+
const schemeChallenge = (security, name) => Match.value(security).pipe(Match.tag("Http", ({ scheme }) => challenge(scheme, [])), Match.tag("Basic", () => challenge("Basic", [["realm", name]])), Match.tag("ApiKey", () => void 0), Match.exhaustive);
|
|
171
|
+
/**
|
|
172
|
+
* Browser-safe declaration shared by clients and remote surfaces: the `identity` it
|
|
173
|
+
* authenticates, by the one native scheme `security`, Bearer unless it names another, such
|
|
174
|
+
* as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`. The literal name
|
|
175
|
+
* identifies the provider, like a native Context.Key name, and the scheme's OpenAPI key, so it
|
|
176
|
+
* holds only letters, digits, `_`, `.` and `-`; reuse one name only for one declaration.
|
|
177
|
+
*/
|
|
178
|
+
const make = (name, identity, ...options) => {
|
|
179
|
+
const security = options[0]?.security ?? HttpApiSecurity.bearer;
|
|
180
|
+
const error = errorList(options[0]?.error);
|
|
181
|
+
if (!componentKey.test(name)) throw new Error(`Invalid authentication name: ${JSON.stringify(name)}, not an OpenAPI key`);
|
|
182
|
+
assertOwnTags(`Authentication "${name}"`, error);
|
|
183
|
+
if (!Predicate.isTagged(security, "Http") && !Predicate.isTagged(security, "ApiKey") && !Predicate.isTagged(security, "Basic")) throw new Error("Authentication takes one native HttpApiSecurity scheme");
|
|
184
|
+
return {
|
|
185
|
+
name,
|
|
186
|
+
identity,
|
|
187
|
+
security,
|
|
188
|
+
error,
|
|
189
|
+
"~provider": Context.Service(`effect-actions/Authentication/Provider/${name}`),
|
|
190
|
+
"~middleware": HttpApiMiddleware.Service()(`effect-actions/Authentication/Security/${name}`, { security: { [name]: security } }),
|
|
191
|
+
"~verified": Context.Service(`effect-actions/Authentication/Verified/${name}`)
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
/** Whether `credential`, as a native scheme decodes it, holds anything: an absent one is empty. */
|
|
195
|
+
const presented = (credential) => Redacted.isRedacted(credential) ? Redacted.value(credential) !== "" : credential.username !== "" || Redacted.value(credential.password) !== "";
|
|
196
|
+
function layer(authentication, verify, options = {}) {
|
|
197
|
+
const { security } = authentication;
|
|
198
|
+
const oauth = isBearer(security);
|
|
199
|
+
const protectedResource = options.protectedResource;
|
|
200
|
+
assertResource(oauth, protectedResource);
|
|
201
|
+
const resource = Effect.isEffect(protectedResource) ? Effect.map(protectedResource, (built) => {
|
|
202
|
+
assertResource(oauth, built);
|
|
203
|
+
return answersOf(built);
|
|
204
|
+
}) : Effect.succeed(answersOf(protectedResource));
|
|
205
|
+
const missing = new Unauthenticated({ message: oauth ? "A bearer token is required." : "A credential is required." });
|
|
206
|
+
return Layer.effect(authentication["~provider"], Effect.gen(function* () {
|
|
207
|
+
const router = yield* HttpRouter.HttpRouter;
|
|
208
|
+
const { metadataUrl, invalid, anonymous, published } = yield* resource;
|
|
209
|
+
if (published !== void 0) yield* router.addGlobalMiddleware(published);
|
|
210
|
+
const verifier = Effect.isEffect(verify) ? yield* verify : verify;
|
|
211
|
+
if (!Predicate.isFunction(verifier)) return yield* Effect.die(/* @__PURE__ */ new Error("Missing verify: pass a verify function, or an Effect building one"));
|
|
212
|
+
const authenticate = (credential) => presented(credential) ? verifier(credential) : Effect.fail(missing);
|
|
213
|
+
const failed = HttpEffect.appendPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "no-store")));
|
|
214
|
+
const challengeOf = (credential) => !oauth ? schemeChallenge(security, authentication.name) : presented(credential) ? invalid : anonymous;
|
|
215
|
+
const refuse = (error) => isRefusal(error) ? Effect.succeed(oauth ? answer(error, metadataUrl) : plain(error)) : Effect.suspend(() => {
|
|
216
|
+
const response = declaredResponse(authentication.error, error);
|
|
217
|
+
return response === void 0 ? Effect.die(error) : Effect.succeed(response);
|
|
218
|
+
});
|
|
219
|
+
const verified = (credential, route, slot) => {
|
|
220
|
+
const challenge = challengeOf(credential);
|
|
221
|
+
return Effect.matchEffect(authenticate(credential), {
|
|
222
|
+
onFailure: refuse,
|
|
223
|
+
onSuccess: (actor) => Effect.provideService(route, slot, actor)
|
|
224
|
+
}).pipe(Effect.onError(() => failed), HttpEffect.withPreResponseHandler((_request, response) => Effect.succeed(settle(response, challenge))));
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
descriptor: authentication,
|
|
228
|
+
middleware: { [authentication.name]: (route, { credential }) => verified(credential, route, authentication.identity) },
|
|
229
|
+
stepUp: (route) => oauth ? Effect.catchIf(route, isStepUp, (error) => Effect.succeed(answer(error, metadataUrl))) : route,
|
|
230
|
+
http: ((route, optional) => Effect.flatMap(HttpApiBuilder.securityDecode(security), (credential) => optional && !presented(credential) ? route : verified(credential, oauth ? answerStepUp(route, metadataUrl) : route, authentication["~verified"]))),
|
|
231
|
+
route: ((route) => Effect.flatMap(HttpApiBuilder.securityDecode(security), (credential) => verified(credential, Effect.catchIf(route, isRefusal, refuse), authentication.identity)))
|
|
232
|
+
};
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Native router middleware authenticating a route of the host's own, such as an export, a
|
|
237
|
+
* page frame or a WebSocket upgrade, with `authentication`'s provider, as its actions' routes
|
|
238
|
+
* are: the credential its scheme decodes, verified by the same verifier, and the identity
|
|
239
|
+
* provided to the route. It answers as an action route: a missing or invalid credential with
|
|
240
|
+
* the refusal, every response to the request it authenticates `no-store` unless it states its
|
|
241
|
+
* own caching, and a refusal the route fails with, such as a scope check's `Forbidden`, as its
|
|
242
|
+
* status, JSON and challenge, stepping up under Bearer. The layer requires the provider,
|
|
243
|
+
* `layer(authentication, ...)`. An erased descriptor, `Any`, provides no service the types can
|
|
244
|
+
* name: its identity is `unknown`, which would discharge every request service the route owes.
|
|
245
|
+
*/
|
|
246
|
+
const protect = (authentication) => HttpRouter.middleware()(Effect.map(Effect.service(authentication["~provider"]), (runtime) => (route) => runtime.route(route)));
|
|
54
247
|
//#endregion
|
|
55
|
-
export {
|
|
248
|
+
export { bearerTokenOf, layer, make, protect, refusalResponse };
|
package/dist/Testing.d.mts
CHANGED
|
@@ -1,68 +1,95 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { BuiltIn } from "./internal/errors.mjs";
|
|
2
|
+
import { Call } from "./internal/call.mjs";
|
|
3
|
+
import { Action_d_exports, Any } from "./Action.mjs";
|
|
4
|
+
import { Params } from "./internal/mcp.mjs";
|
|
5
|
+
import { Served } from "./internal/memory.mjs";
|
|
6
|
+
import { Effect, Layer, Schema } from "effect";
|
|
7
|
+
import { Headers, HttpClient, HttpClientError, HttpClientRequest, HttpRouter } from "effect/http";
|
|
4
8
|
//#region src/Testing.d.ts
|
|
5
|
-
/** A web handler, such as `HttpRouter.toWebHandler(routes).handler`. */
|
|
6
|
-
export type Handler = (request: Request) => Promise<Response>;
|
|
7
9
|
/**
|
|
8
|
-
* The native
|
|
9
|
-
*
|
|
10
|
+
* The native `HttpClient`, answered by `handler` instead of the network: a web handler the
|
|
11
|
+
* test serves, which other tests may share, such as
|
|
12
|
+
* `HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices))).handler`,
|
|
13
|
+
* the routes' platform services provided, as `layer(routes)` provides them itself. It is the
|
|
14
|
+
* client `layer(routes)` gives, without building anything: the test owns the handler, and
|
|
15
|
+
* disposes of it.
|
|
10
16
|
*/
|
|
11
|
-
export declare
|
|
12
|
-
readonly topLevel: false;
|
|
13
|
-
}> as HttpApiGroup.Identifier<Group>]: import("effect/http-api/HttpApiClient").Client.GroupByEndpoint<Group, never, never>; } & import("effect/http-api/HttpApiClient").Client.TopLevelMethods<Groups, never, never>>, never, Exclude<Exclude<import("effect/http-api/HttpApiMiddleware").MiddlewareClient<import("effect/http-api/HttpApiEndpoint").Middleware<HttpApiGroup.Endpoints<Groups>>>, import("effect/http/HttpClient").HttpClient>, never>>;
|
|
14
|
-
/** A stateless 2026-07-28 request. */
|
|
15
|
-
export interface McpRequestOptions {
|
|
16
|
-
readonly url: string | URL;
|
|
17
|
-
readonly method: string;
|
|
18
|
-
readonly params?: McpRequestParams;
|
|
19
|
-
readonly headers?: ConstructorParameters<typeof Headers>[0];
|
|
20
|
-
}
|
|
17
|
+
export declare function layer(handler: (request: Request) => Promise<Response>): Layer.Layer<HttpClient.HttpClient>;
|
|
21
18
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* The native `HttpClient`, answered in memory by `routes` instead of the network: provide
|
|
20
|
+
* it to `ActionHttp.client` and to `mcpClient`. The routes are built with this layer and
|
|
21
|
+
* released with its scope, without request logs. What they still require is this layer's, as
|
|
22
|
+
* under `HttpRouter.serve`: a builder's services, and a per-request service no middleware of
|
|
23
|
+
* theirs provides, including one a global middleware reads. The routes keep their real
|
|
24
|
+
* authentication: a client sends its caller's credential. Provided around it, the test
|
|
25
|
+
* program shares them. It never requires
|
|
26
|
+
* the platform services, `FileSystem`, `Path`, `HttpPlatform` and `Etag.Generator`: one
|
|
27
|
+
* provided around it is the routes' too, and `HttpServer.layerServices`' defaults stand in for
|
|
28
|
+
* the rest, whose `FileSystem` is a no-op. A relative URL resolves against `http://localhost`.
|
|
29
|
+
* The client is the layer's own: the program's other HTTP clients get none of its requests,
|
|
30
|
+
* and it none of theirs.
|
|
24
31
|
*/
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
export declare function layer<A, E, R>(routes: Layer.Layer<A, E, R>): Layer.Layer<HttpClient.HttpClient, E, Exclude<HttpRouter.Request.Without<R> | HttpRouter.Request.Only<"Requires", R> | HttpRouter.Request.Only<"GlobalRequires", R>, Served>>;
|
|
33
|
+
/** Where `mcpClient` sends, and through what client. */
|
|
34
|
+
export interface McpClientOptions {
|
|
35
|
+
/**
|
|
36
|
+
* The endpoint, resolved by the `HttpClient`: relative under `layer`. Defaults to `/mcp`,
|
|
37
|
+
* the default `ActionMcp.layerHttp` path.
|
|
38
|
+
*/
|
|
39
|
+
readonly url?: string;
|
|
40
|
+
/** Wraps the native `HttpClient`, as `ActionHttp.client` takes it: a bearer token, say. */
|
|
41
|
+
readonly transformClient?: (client: HttpClient.HttpClient) => HttpClient.HttpClient;
|
|
34
42
|
}
|
|
35
|
-
/**
|
|
36
|
-
export
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
readonly
|
|
44
|
-
readonly [key: string]: McpRequestValue;
|
|
45
|
-
};
|
|
46
|
-
readonly headers?: ConstructorParameters<typeof Headers>[0];
|
|
43
|
+
/** Where an `mcpRequest` goes, and with what headers. */
|
|
44
|
+
export interface McpRequestOptions {
|
|
45
|
+
/**
|
|
46
|
+
* The endpoint, resolved by the `HttpClient` that sends the request: relative under `layer`.
|
|
47
|
+
* Defaults to `/mcp`, as `mcpClient`'s does; converting to a web `Request` needs an absolute
|
|
48
|
+
* one.
|
|
49
|
+
*/
|
|
50
|
+
readonly url?: string;
|
|
51
|
+
readonly headers?: Headers.Input;
|
|
47
52
|
}
|
|
53
|
+
declare const McpCallError_base: Schema.Class<McpCallError, Schema.TaggedStruct<"McpCallError", {
|
|
54
|
+
readonly message: Schema.String;
|
|
55
|
+
}>, import("effect/Cause").YieldableError>;
|
|
56
|
+
/**
|
|
57
|
+
* An answer a client method cannot decode as the action's success or a declared error,
|
|
58
|
+
* such as the native server's message for invalid arguments or an unknown tool. Its message
|
|
59
|
+
* holds the answer.
|
|
60
|
+
*/
|
|
61
|
+
export declare class McpCallError extends McpCallError_base {}
|
|
62
|
+
/**
|
|
63
|
+
* What one call of `A` fails with: a declared error value (the action's own or a built-in one
|
|
64
|
+
* from the tool, or a refusal from the endpoint's authentication), a `SchemaError` when the
|
|
65
|
+
* input does not encode or the success does not decode, an `HttpClientError` when the
|
|
66
|
+
* endpoint could not be reached, or an `McpCallError` for any other answer.
|
|
67
|
+
*/
|
|
68
|
+
type CallError<A extends Any> = A["error"][number]["Type"] | BuiltIn | Schema.SchemaError | HttpClientError.HttpClientError | McpCallError;
|
|
69
|
+
/** Every action of `Actions` as `client.<action>(input)`, calling its tool. */
|
|
70
|
+
export type McpClient<Actions extends ReadonlyArray<Any>> = { readonly [A in Actions[number] as A["name"]]: Call<A, Effect.Effect<A["success"]["Type"], CallError<A>>>; };
|
|
48
71
|
/**
|
|
49
|
-
* A
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
72
|
+
* A client of an MCP endpoint served by `ActionMcp.layerHttp`, one method per action calling
|
|
73
|
+
* its tool with one stateless request, as `ActionHttp.client` calls routes: the input is
|
|
74
|
+
* encoded with the action's schema, and the success decoded from its structured content, each
|
|
75
|
+
* media field from the blocks whose `_meta` names it, and a success that is media, or whose
|
|
76
|
+
* every field is, from its blocks alone. A declared error the tool returns, the action's own
|
|
77
|
+
* or a refusal, is its decoded value, and so is a refusal the endpoint's authentication
|
|
78
|
+
* answers with. The argument may be omitted when `{}` is a valid input. Requires the native
|
|
79
|
+
* `HttpClient`, such as the one `layer` provides.
|
|
53
80
|
*/
|
|
54
|
-
export
|
|
55
|
-
readonly isError: false;
|
|
56
|
-
readonly value: Schema.Json;
|
|
57
|
-
readonly text?: string;
|
|
58
|
-
} | {
|
|
59
|
-
readonly isError: true;
|
|
60
|
-
readonly error: Schema.Json;
|
|
61
|
-
};
|
|
81
|
+
export declare function mcpClient<const Actions extends ReadonlyArray<Any>>(actions: Actions, options?: McpClientOptions): Effect.Effect<McpClient<Actions>, never, HttpClient.HttpClient>;
|
|
62
82
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
83
|
+
* One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it,
|
|
84
|
+
* as the native request value: the JSON-RPC envelope, the 2026-07-28 headers, `mcp-name` from
|
|
85
|
+
* `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
|
|
86
|
+
* `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
|
|
87
|
+
* version is always the request's own. The test sends it: `HttpClient.execute` answers the
|
|
88
|
+
* response as the endpoint sent it, whatever its status, and `HttpClientRequest.toWebResult`
|
|
89
|
+
* gives the web `Request` a web handler or `fetch` takes, once `url` is absolute. It is for a test
|
|
90
|
+
* asserting on what `mcpClient` decodes away, such as `tools/list`, a refusal's challenge, or
|
|
91
|
+
* a call its types would not send.
|
|
66
92
|
*/
|
|
67
|
-
export declare const
|
|
68
|
-
//#endregion
|
|
93
|
+
export declare const mcpRequest: (method: string, params?: Params, { headers, url }?: McpRequestOptions) => HttpClientRequest.HttpClientRequest;
|
|
94
|
+
//#endregion
|
|
95
|
+
export type { Params as McpParams };
|