@gjermundgaraba/effect-actions 0.8.0 → 0.10.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 +57 -50
- package/dist/Action.d.mts +285 -79
- package/dist/Action.mjs +124 -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 +108 -81
- 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 +82 -57
- package/dist/Testing.mjs +89 -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 +11 -0
- package/dist/internal/mcp.mjs +52 -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 +401 -73
- package/docs/ActionCli.md +280 -51
- package/docs/ActionHttp.md +266 -82
- package/docs/ActionMcp.md +149 -104
- package/docs/ActionToolkit.md +97 -35
- package/docs/Authentication.md +369 -62
- package/docs/CONTEXT.md +23 -36
- package/docs/README.md +29 -73
- package/docs/Testing.md +154 -77
- package/docs/guarantees.md +81 -43
- package/docs/setup.md +90 -27
- package/package.json +9 -17
- 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
package/dist/ActionHttp.d.mts
CHANGED
|
@@ -1,62 +1,181 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { Path } from "effect
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
export interface
|
|
14
|
-
readonly apiPath: `/${string}`;
|
|
15
|
-
/**
|
|
16
|
-
* Failures the surface around these endpoints answers with instead of a
|
|
17
|
-
* handler: authentication, authorization, rate limits. Declared on every
|
|
18
|
-
* endpoint, so `HttpApiClient` and `ActionCliClient` decode them as typed
|
|
19
|
-
* failures rather than reporting a decode error. Each schema keeps its own
|
|
20
|
-
* `httpApiStatus`, and their `_tag`s must be distinct.
|
|
21
|
-
*/
|
|
22
|
-
readonly errors?: Errors;
|
|
23
|
-
}
|
|
24
|
-
/** What `Http.layer` binds around the implementations it serves. */
|
|
25
|
-
export interface LayerOptions<Errors extends ReadonlyArray<Codec>, R> {
|
|
1
|
+
import { Anyone } from "./internal/actions.mjs";
|
|
2
|
+
import { BuiltIn, BuiltIns } from "./internal/errors.mjs";
|
|
3
|
+
import { BuildError, BuildServices, Holding, Known, Member, Protected, Selected, Served, Serving } from "./internal/implementation.mjs";
|
|
4
|
+
import { Action_d_exports, Any, Errors } from "./Action.mjs";
|
|
5
|
+
import { Any as Any$1, Identity, Matching, RemoteRequest, Required, ServedProvider, VerifierError } from "./internal/authentication.mjs";
|
|
6
|
+
import { AnyHttp, Client, MethodError, Options } from "./internal/client.mjs";
|
|
7
|
+
import { ErrorsOf } from "./internal/declared.mjs";
|
|
8
|
+
import { Context, Effect, FileSystem, Layer, Path, Schema, Types } from "effect";
|
|
9
|
+
import { Etag, HttpClient, HttpPlatform, HttpRouter } from "effect/http";
|
|
10
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "effect/http-api";
|
|
11
|
+
//#region src/ActionHttp.d.ts
|
|
12
|
+
/** What `fetchClient` takes: `client`'s options, and the `fetch` its calls send with. */
|
|
13
|
+
export interface FetchClientOptions extends Options {
|
|
26
14
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* exactly like a declared error. Its services are request-time
|
|
30
|
-
* requirements, like a handler's.
|
|
15
|
+
* What each call sends with. Omitted: the global `fetch`, looked up on every call, so one
|
|
16
|
+
* installed after the client is built, such as a test's stub, is used.
|
|
31
17
|
*/
|
|
32
|
-
readonly
|
|
18
|
+
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
33
19
|
}
|
|
34
|
-
|
|
35
|
-
type
|
|
36
|
-
/** The
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
20
|
+
/** Native endpoint middleware, as `HttpApiMiddleware.Service` declares it. */
|
|
21
|
+
type Middleware = ReadonlyArray<Context.Key<HttpApiMiddleware.AnyId, unknown>>;
|
|
22
|
+
/** The identifier a middleware's service key declares. */
|
|
23
|
+
type IdOf<K> = K extends Context.Key<infer I extends HttpApiMiddleware.AnyId, unknown> ? I : never;
|
|
24
|
+
/**
|
|
25
|
+
* What the request owes past the middleware `M`, the first innermost, around what `R` owes:
|
|
26
|
+
* each removes what it provides and adds what it requires, as native endpoints apply them.
|
|
27
|
+
* An array of unknown length, which may hold none of them, provides nothing and owes what
|
|
28
|
+
* any of them requires; so does a slot that may hold one of several, as only one runs.
|
|
29
|
+
*/
|
|
30
|
+
type Through<M extends Middleware, R> = number extends M["length"] ? R | HttpApiMiddleware.Requires<IdOf<M[number]>> : M extends readonly [infer K, ...infer Rest extends Middleware] ? Through<Rest, true extends Types.IsUnion<IdOf<K>> ? R | HttpApiMiddleware.Requires<IdOf<K>> : HttpApiMiddleware.ApplyServices<IdOf<K>, R>> : R;
|
|
31
|
+
/**
|
|
32
|
+
* The middleware options `O` install: its `middleware` when always given, an array of unknown
|
|
33
|
+
* length when it may be absent, which installs none or all of them, and none otherwise.
|
|
34
|
+
*/
|
|
35
|
+
type MiddlewareOf<O> = O extends {
|
|
36
|
+
readonly middleware: infer M extends Middleware;
|
|
37
|
+
} ? M : O extends {
|
|
38
|
+
readonly middleware?: infer M extends Middleware | undefined;
|
|
39
|
+
} ? "middleware" extends keyof O ? ReadonlyArray<NonNullable<M>[number]> : [] : [];
|
|
40
|
+
/**
|
|
41
|
+
* The layer's middleware, refused when one fails with an error neither the binding, `E`, nor
|
|
42
|
+
* every endpoint declares, or needs a client counterpart: neither reaches the binding's
|
|
43
|
+
* clients, which decode only what the binding and the built-ins declare. A failure an
|
|
44
|
+
* action's callers see after decoding is its handler's, declared on the contract and decoded
|
|
45
|
+
* by every client.
|
|
46
|
+
*/
|
|
47
|
+
type ServerOnly<M extends Middleware, E extends Errors> = [Exclude<HttpApiMiddleware.Error<IdOf<M[number]>>, Extract<Certain<E>, Errors[number]>["Type"] | BuiltIn> | HttpApiMiddleware.MiddlewareClient<IdOf<M[number]>>] extends [never] ? unknown : {
|
|
48
|
+
readonly "Layer middleware fails only with the binding's errors and needs no client": never;
|
|
49
|
+
};
|
|
50
|
+
/** `T` where it is one type, not a union: what a list's slot of type `T` surely holds. */
|
|
51
|
+
type Single<T> = true extends Types.IsUnion<T> ? never : T;
|
|
52
|
+
/**
|
|
53
|
+
* The errors a binding of errors `E` surely declares: each slot of one error in a list of
|
|
54
|
+
* fixed length, never one of an array of unknown length, which may be empty, nor of a list
|
|
55
|
+
* `E` may be one of several.
|
|
56
|
+
*/
|
|
57
|
+
type Certain<E extends Errors> = true extends Types.IsUnion<E> ? never : number extends E["length"] ? never : { readonly [K in keyof E]: Single<E[K]>; }[number];
|
|
58
|
+
/**
|
|
59
|
+
* The layer serving the actions `A` of `Apps` through the binding `H`, behind the middleware
|
|
60
|
+
* `M`: an implementation holding none of them is not built.
|
|
61
|
+
*/
|
|
62
|
+
type HttpLayer<H extends AnyHttp, Apps extends Served, M extends Middleware, A extends Any = H["actions"][number]> = Layer.Layer<never, BuildError<Holding<Member<Apps>, A>, A>, BuildServices<Holding<Member<Apps>, A>, A> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", LayerRequest<Member<Apps>, A, M>> | ServedProvider<Member<Apps>, A, H extends {
|
|
63
|
+
readonly authentication: infer D;
|
|
64
|
+
} ? D : never> | IdOf<M[number]> | Etag.Generator | FileSystem.FileSystem | HttpPlatform.HttpPlatform | Path.Path>;
|
|
65
|
+
/** `ActionHttp.layer`'s options. */
|
|
66
|
+
export interface LayerOptions<M extends Middleware = [], A extends Any = never> {
|
|
43
67
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
68
|
+
* The actions it serves, among the binding's actions the implementations hold: `[GetUser]`.
|
|
69
|
+
* Defaults to every one of them. A layer whose middleware requires the identity lists
|
|
70
|
+
* protected actions only.
|
|
47
71
|
*/
|
|
48
|
-
readonly
|
|
72
|
+
readonly actions?: ReadonlyArray<A> | undefined;
|
|
49
73
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
74
|
+
* Native endpoint middleware every served route runs, the first innermost: inside the
|
|
75
|
+
* authentication of a protected route, outside its decoding. A middleware requiring the
|
|
76
|
+
* identity needs a layer serving only protected actions.
|
|
53
77
|
*/
|
|
54
|
-
readonly
|
|
78
|
+
readonly middleware?: M;
|
|
79
|
+
}
|
|
80
|
+
/** What the layer serving `A` of `App` behind middleware `M` owes per request. */
|
|
81
|
+
type LayerRequest<App, A extends Any, M extends Middleware> = Exclude<Through<M, RemoteRequest<App, A>>, HttpRouter.Provided | ([Extract<Serving<App, A>, {
|
|
82
|
+
readonly caller: typeof Anyone;
|
|
83
|
+
}>] extends [never] ? Identity<Protected<Serving<App, A>>> : never)>;
|
|
84
|
+
/** Actions a binding takes without options: public ones, as a protected one names its descriptor. */
|
|
85
|
+
type PublicOnly<Actions extends ReadonlyArray<Any>> = [Exclude<Actions[number], {
|
|
86
|
+
readonly caller: typeof Anyone;
|
|
87
|
+
}>] extends [never] ? unknown : {
|
|
88
|
+
readonly "Protected actions take options naming their authentication": never;
|
|
89
|
+
};
|
|
90
|
+
/** The descriptor options `O` name, where they always name one. */
|
|
91
|
+
type DescriptorOf<O> = [O] extends [{
|
|
92
|
+
readonly authentication: infer D extends Any$1;
|
|
93
|
+
}] ? D : undefined;
|
|
94
|
+
/** Contract-level configuration shared by servers and clients. */
|
|
95
|
+
interface Options$1<E extends Errors = Errors> {
|
|
96
|
+
readonly prefix?: `/${string}`;
|
|
97
|
+
/** Router middleware's errors, which every endpoint declares: one schema, or a list. */
|
|
98
|
+
readonly error?: E | E[number];
|
|
99
|
+
readonly authentication?: Any$1;
|
|
100
|
+
}
|
|
101
|
+
type Endpoint<A extends Any, E extends Errors, D> = A extends Any ? HttpApiEndpoint.HttpApiEndpoint<A["name"], "POST", `/${string}`, never, never, Schema.toCodecJson<A["input"]>, never, Schema.toCodecJson<A["success"]>, Schema.toCodecJson<A["error"][number] | E[number] | VerifierError<A, D> | BuiltIns>, never> : never;
|
|
102
|
+
/**
|
|
103
|
+
* The native `HttpApi` of `Actions`: one top-level group, so native client methods are
|
|
104
|
+
* not nested, named after the binding's mount path, `/` at the root, so bindings composed
|
|
105
|
+
* into one host API keep their own groups.
|
|
106
|
+
*/
|
|
107
|
+
type Api<Actions extends ReadonlyArray<Any>, E extends Errors, D> = HttpApi.HttpApi<"actions", HttpApiGroup.HttpApiGroup<string, Endpoint<Actions[number], E, D>, true>>;
|
|
108
|
+
/**
|
|
109
|
+
* An HTTP binding: actions, the errors every endpoint declares, and where they are mounted.
|
|
110
|
+
* Plain data, so a copy of it, or one made by another installed copy of this package,
|
|
111
|
+
* serves the same; `layer` serves it and `client` calls it.
|
|
112
|
+
*/
|
|
113
|
+
export interface Binding<Actions extends ReadonlyArray<Any>, E extends Errors = [], D extends Any$1 | undefined = undefined> {
|
|
114
|
+
readonly authentication: D;
|
|
115
|
+
/** The exact actions bound to this binding. */
|
|
116
|
+
readonly actions: Actions;
|
|
117
|
+
/** The errors every endpoint declares besides its action's own, as a list. */
|
|
118
|
+
readonly error: E;
|
|
119
|
+
/** Where its routes mount: `/api` by default, `/` at the root, without a trailing slash. */
|
|
120
|
+
readonly prefix: `/${string}`;
|
|
121
|
+
readonly api: Api<Actions, E, D>;
|
|
55
122
|
}
|
|
56
123
|
/**
|
|
57
|
-
* Bind
|
|
58
|
-
*
|
|
124
|
+
* Bind actions once, for servers and clients alike, each at `POST <prefix>/<action>`.
|
|
125
|
+
* Every endpoint declares its action's errors, the binding's `errors`, and the built-in
|
|
126
|
+
* `InvalidInput`, `Unauthenticated` and `Forbidden`, so clients decode each as a typed
|
|
127
|
+
* failure. A union's own `httpApiStatus` applies to every member; otherwise, each member
|
|
128
|
+
* uses its own status, or 422 without one. Protected contracts receive native endpoint
|
|
129
|
+
* security middleware: the same descriptor documents their credentials and enforces them.
|
|
130
|
+
*/
|
|
131
|
+
export declare function make<const Actions extends ReadonlyArray<Any & {
|
|
132
|
+
readonly caller: typeof Anyone;
|
|
133
|
+
}>>(actions: Actions): Binding<Actions, [], undefined>;
|
|
134
|
+
export declare function make<const Actions extends ReadonlyArray<Any>, const O extends Options$1 = {}>(actions: Actions, options: O & Required<Actions[number]> & Matching<Actions[number], NoInfer<DescriptorOf<O>>> & NoInfer<Known<O, Options$1>>): Binding<Actions, ErrorsOf<O>, DescriptorOf<O>>;
|
|
135
|
+
export declare function make<const Actions extends ReadonlyArray<Any>>(actions: Actions & PublicOnly<Actions>): Binding<Actions, [], undefined>;
|
|
136
|
+
/**
|
|
137
|
+
* Serve the binding's actions among `implementations` in one layer, or those its `actions` lists,
|
|
138
|
+
* authenticating a protected action's request before decoding it, then running its implementation's
|
|
139
|
+
* `authorize` before each handler. The binding decides what may be served:
|
|
140
|
+
* an implementation's actions the binding leaves out have no route here, and one holding none of
|
|
141
|
+
* those served is not built; implementations holding none of the binding's actions at all are
|
|
142
|
+
* refused when `layer` is called. It mounts only the routes of the actions it serves, so one
|
|
143
|
+
* binding may be served by several layers, each with its own `actions` and `middleware`, which
|
|
144
|
+
* covers its routes only. Each implementation's builder runs once per layer graph however many
|
|
145
|
+
* layers serve it.
|
|
146
|
+
*
|
|
147
|
+
* The layer fails as the builders do, needs at startup what they need, and per request what
|
|
148
|
+
* each implementation's authorization and the handlers of the actions it serves
|
|
149
|
+
* need, until its `middleware` or middleware provided around it provides them, the identity
|
|
150
|
+
* of a protected action excepted, which its authentication provides; the router provides
|
|
151
|
+
* its own, such as the request, to every route. Its routes take only requests typed as JSON,
|
|
152
|
+
* and what middleware provides per request wins over what the layer was built with.
|
|
153
|
+
*/
|
|
154
|
+
export declare function layer<const H extends AnyHttp, const Apps extends Served>(binding: H, implementations: Apps): HttpLayer<H, Apps, []>;
|
|
155
|
+
export declare function layer<const H extends AnyHttp, const Apps extends Served, const O extends LayerOptions<Middleware, H["actions"][number]>>(binding: H, implementations: Apps, options: O & ServerOnly<MiddlewareOf<O>, H["error"]> & NoInfer<Known<O, LayerOptions<Middleware>>>): HttpLayer<H, Apps, MiddlewareOf<O>, Selected<O, H["actions"][number]>>;
|
|
156
|
+
/**
|
|
157
|
+
* Effect's native `HttpApiClient` for a binding, one method per action taking the
|
|
158
|
+
* action's input directly: `client.greet({ name })`. The argument may be omitted when `{}`
|
|
159
|
+
* is a valid input. Requires the native `HttpClient`, as `HttpApiClient.make` does.
|
|
160
|
+
* The options are the native ones, `baseUrl` and `transformClient`. The native client
|
|
161
|
+
* itself stays available: `HttpApiClient.make(Http.api)`.
|
|
162
|
+
*
|
|
163
|
+
* A call fails with a declared error value (the action's own, or a built-in
|
|
164
|
+
* `InvalidInput`, `Unauthenticated` or `Forbidden`), a native `HttpClientError` when the
|
|
165
|
+
* server could not be reached or answered with a status or body the contract does not
|
|
166
|
+
* declare, or a `SchemaError` when the input does not encode or the success does not
|
|
167
|
+
* decode.
|
|
168
|
+
*/
|
|
169
|
+
export declare function client<const H extends AnyHttp>(binding: H, options?: Options): Effect.Effect<Client<H>, never, HttpClient.HttpClient>;
|
|
170
|
+
/**
|
|
171
|
+
* `client` built once over `fetch`, for code that holds a client outside an Effect, such as
|
|
172
|
+
* a browser app: its methods are the same Effects and require nothing, so each call runs
|
|
173
|
+
* alone, as `Effect.runPromise(api.greet({ name }))`. The options are `client`'s, and
|
|
174
|
+
* `fetch`.
|
|
175
|
+
*
|
|
176
|
+
* Each call sends with `options.fetch`, or with the global `fetch` as it is when the call
|
|
177
|
+
* runs.
|
|
59
178
|
*/
|
|
60
|
-
export declare function
|
|
179
|
+
export declare function fetchClient<const H extends AnyHttp>(binding: H, options?: FetchClientOptions): Client<H>;
|
|
61
180
|
//#endregion
|
|
62
|
-
export {
|
|
181
|
+
export { type AnyHttp as Any, type Client, type Options as ClientOptions, type MethodError, Options$1 as Options };
|
package/dist/ActionHttp.mjs
CHANGED
|
@@ -1,90 +1,141 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { Anyone, assertDistinct, assertOnce, assertOwnTags, errorList, projectedErrors } from "./internal/actions.mjs";
|
|
2
|
+
import { acquire, assertHeld, provideHandlers, select, toList } from "./internal/implementation.mjs";
|
|
3
|
+
import { assertAuthentication } from "./internal/authentication.mjs";
|
|
4
|
+
import { declared } from "./internal/declared.mjs";
|
|
5
|
+
import { methods } from "./internal/client.mjs";
|
|
6
|
+
import { SchemaErrors, schemaErrors } from "./internal/schema-errors.mjs";
|
|
7
|
+
import { Context, Effect, Layer, Scope } from "effect";
|
|
8
|
+
import { FetchHttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
9
|
+
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api";
|
|
6
10
|
//#region src/ActionHttp.ts
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
11
|
+
/** The mount path's segments: `api` by default; none for `/`. */
|
|
12
|
+
const mountSegments = (prefix) => (prefix ?? "/api").split("/").filter((segment) => segment !== "");
|
|
13
|
+
/** An absolute route from path segments. */
|
|
14
|
+
const route = (segments) => `/${segments.join("/")}`;
|
|
15
|
+
/**
|
|
16
|
+
* The name of a binding's one native group: its OpenAPI tag, by which the native `addHttpApi`
|
|
17
|
+
* keys groups. It is the mount path, `/` at the root, which no segment contains.
|
|
18
|
+
*/
|
|
19
|
+
const groupName = (mount) => mount.join("/") || "/";
|
|
20
|
+
/** A native API of one top-level group of `endpoints`. */
|
|
21
|
+
const apiOf = (group, endpoints) => {
|
|
22
|
+
const empty = HttpApiGroup.make(group, { topLevel: true });
|
|
23
|
+
const [first, ...rest] = endpoints;
|
|
24
|
+
return HttpApi.make("actions").annotate(HttpApi.ParseOptions, { errors: "all" }).add(first === void 0 ? empty : empty.add(first, ...rest));
|
|
19
25
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/**
|
|
27
|
+
* `apps` narrowed to the actions the layer serves, matched by identity: those `listed`, which
|
|
28
|
+
* the binding must hold, or every one of the binding's they hold, each once. An
|
|
29
|
+
* implementation holding none of them is left out, and not built. Implementations holding
|
|
30
|
+
* none of the binding's actions are refused, as the wrong implementations or the wrong
|
|
31
|
+
* binding; a name the binding holds is marked as another contract's, as a second copy of the
|
|
32
|
+
* contracts module makes one.
|
|
33
|
+
*/
|
|
34
|
+
const servedBy = (binding, apps, listed) => {
|
|
35
|
+
if (listed !== void 0) assertHeld("the binding does not hold it", listed, binding.actions);
|
|
36
|
+
const held = apps.flatMap((app) => app.actions);
|
|
37
|
+
const served = select(apps, listed ?? binding.actions.filter((action) => held.includes(action)));
|
|
38
|
+
const actions = served.flatMap((app) => app.actions);
|
|
39
|
+
if (actions.length === 0 && listed === void 0 && apps.length > 0) {
|
|
40
|
+
const bound = new Set(binding.actions.map(({ name }) => name));
|
|
41
|
+
throw new Error(`No action of these implementations is in this HTTP binding: ${held.map(({ name }) => bound.has(name) ? `${name} (another contract)` : name).join(", ") || "none"}`);
|
|
42
|
+
}
|
|
43
|
+
assertOnce("served action", actions);
|
|
44
|
+
return served;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The native endpoint of `action` at `path`, declaring its action's errors, the binding's
|
|
48
|
+
* `errors` and the built-in ones: what `make` documents and `layer` serves. Its payload is
|
|
49
|
+
* JSON whatever encoding the input is annotated with, which no other surface reads either:
|
|
50
|
+
* `HttpApi` would take a form or text body for it, which a page sends without a preflight.
|
|
51
|
+
*/
|
|
52
|
+
const endpointOf = (action, path, errors) => HttpApiEndpoint.post(action.name, path, {
|
|
53
|
+
payload: action.input.pipe(HttpApiSchema.asJson()),
|
|
54
|
+
success: action.success,
|
|
55
|
+
error: projectedErrors(action, errors).flatMap(declared)
|
|
56
|
+
});
|
|
57
|
+
/** An endpoint wrapped in a layer's middleware, the first innermost. */
|
|
58
|
+
const within = (endpoint, middleware = []) => middleware.reduce((inner, key) => inner.middleware(key), endpoint);
|
|
59
|
+
function make(actions, options = {}) {
|
|
60
|
+
assertOnce("action", actions);
|
|
61
|
+
const errors = errorList(options.error);
|
|
62
|
+
assertOwnTags("ActionHttp binding", errors);
|
|
63
|
+
assertAuthentication(actions, options.authentication);
|
|
64
|
+
const mount = mountSegments(options.prefix);
|
|
65
|
+
const security = options.authentication?.["~middleware"];
|
|
66
|
+
const verifierErrors = options.authentication?.error ?? [];
|
|
67
|
+
const endpoints = actions.map((action) => {
|
|
68
|
+
const isPublic = security === void 0 || action.caller === Anyone;
|
|
69
|
+
const endpoint = endpointOf(action, route([...mount, action.name]), isPublic ? errors : [...errors, ...verifierErrors]).annotate(OpenApi.Description, action.description);
|
|
70
|
+
return isPublic ? endpoint : endpoint.middleware(security);
|
|
71
|
+
});
|
|
72
|
+
const api = apiOf(groupName(mount), endpoints);
|
|
29
73
|
return {
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
actions,
|
|
75
|
+
error: errors,
|
|
76
|
+
prefix: route(mount),
|
|
77
|
+
api,
|
|
78
|
+
authentication: options.authentication
|
|
32
79
|
};
|
|
33
80
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
81
|
+
/**
|
|
82
|
+
* `group`, built in an empty context rather than the one around it: `HttpApiBuilder` lays
|
|
83
|
+
* the context a group is built in over every request's, where a startup value would replace
|
|
84
|
+
* what middleware provides for the request. `entry` puts that context beneath it instead.
|
|
85
|
+
*/
|
|
86
|
+
const buildAlone = (group) => Layer.fromBuild((memoMap, scope) => Effect.setContext(Layer.buildWithMemoMap(group, memoMap, scope), Context.empty()));
|
|
87
|
+
/**
|
|
88
|
+
* Restore startup dependencies beneath request context, so a caller's services win.
|
|
89
|
+
*/
|
|
90
|
+
const entry = () => HttpRouter.middleware(Effect.map(Effect.context(), (built) => {
|
|
91
|
+
const startup = Context.omit(Scope.Scope)(built);
|
|
92
|
+
return (route) => Effect.updateContext(route, (current) => Context.merge(startup, current));
|
|
93
|
+
})).layer;
|
|
94
|
+
/**
|
|
95
|
+
* Answers a protected route's step-up refusal, as it leaves the layer's middleware, with its
|
|
96
|
+
* challenge: the provider's `stepUp`, inside the authentication.
|
|
97
|
+
*/
|
|
98
|
+
var StepUp = class extends HttpApiMiddleware.Service()("effect-actions/ActionHttp/StepUp") {};
|
|
99
|
+
/** Require JSON media typing after endpoint authentication, before payload decoding. */
|
|
100
|
+
var JsonContentType = class extends HttpApiMiddleware.Service()("effect-actions/ActionHttp/JsonContentType") {};
|
|
101
|
+
/** `JsonContentType`'s server side, built by `layer` alone, so a client's bundle leaves it out. */
|
|
102
|
+
const jsonContentType = () => Layer.succeed(JsonContentType, (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.headers["content-type"] === void 0 ? Effect.succeed(HttpServerResponse.text("Unsupported content-type: none", { status: 415 })) : route));
|
|
103
|
+
function layer(binding, served, options = {}) {
|
|
104
|
+
const apps = servedBy(binding, toList(served), options.actions);
|
|
105
|
+
const actions = apps.flatMap((app) => app.actions);
|
|
106
|
+
assertAuthentication(actions, binding.authentication);
|
|
107
|
+
assertDistinct("middleware", options.middleware ?? [], (key) => key.key);
|
|
108
|
+
const mount = mountSegments(binding.prefix);
|
|
109
|
+
const name = groupName(mount);
|
|
110
|
+
const api = apiOf(name, actions.map((action) => {
|
|
111
|
+
const endpoint = within(endpointOf(action, route([...mount, action.name]), binding.error).middleware(JsonContentType).middleware(SchemaErrors), options.middleware);
|
|
112
|
+
return action.caller === Anyone || binding.authentication === void 0 ? endpoint : endpoint.middleware(StepUp).middleware(binding.authentication["~middleware"]);
|
|
113
|
+
})).annotate(HttpApi.PayloadParseOptions, {
|
|
37
114
|
errors: "all",
|
|
38
115
|
onExcessProperty: "error"
|
|
39
116
|
});
|
|
40
|
-
|
|
117
|
+
const handlers = Layer.unwrap(Effect.gen(function* () {
|
|
118
|
+
const bound = yield* acquire(apps);
|
|
119
|
+
const auth = actions.some((action) => action.caller !== Anyone) ? binding.authentication : void 0;
|
|
120
|
+
const authentication = auth === void 0 ? Layer.empty : yield* Effect.flatMap(auth["~provider"], (provider) => provider.descriptor === auth ? Effect.succeed(Layer.mergeAll(Layer.succeed(auth["~middleware"], provider.middleware), Layer.succeed(StepUp, provider.stepUp))) : Effect.die(/* @__PURE__ */ new Error(`Authentication "${auth.name}": the binding's descriptor is not its provider's; build both from one descriptor`)));
|
|
121
|
+
const middleware = yield* Effect.forEach(options.middleware ?? [], (key) => Effect.map(Effect.service(key), (service) => Layer.succeed(key, service)));
|
|
122
|
+
const byName = Object.fromEntries(bound.map(([action, run]) => [action.name, (request) => run(request.payload)]));
|
|
123
|
+
return HttpApiBuilder.group(api, name, (builder) => builder.handleAll(byName)).pipe(Layer.provide([
|
|
124
|
+
schemaErrors(),
|
|
125
|
+
jsonContentType(),
|
|
126
|
+
authentication,
|
|
127
|
+
...middleware
|
|
128
|
+
]), buildAlone);
|
|
129
|
+
})).pipe(provideHandlers(apps));
|
|
130
|
+
return HttpApiBuilder.layer(api).pipe(Layer.provide(handlers), Layer.provide(entry()));
|
|
41
131
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const erasedHandlers = (handlers) => {
|
|
50
|
-
return handlers;
|
|
51
|
-
};
|
|
52
|
-
/** Register one group's handlers. Handler erasure is limited to native dynamic endpoint registration. */
|
|
53
|
-
const groupHandlers = (api, app, middleware, before) => {
|
|
54
|
-
const entries = (record) => Object.fromEntries(app.group.actions.map((action) => {
|
|
55
|
-
const run = dispatch(app.group, action, record, before);
|
|
56
|
-
return [action.name, (request) => run(request.payload)];
|
|
57
|
-
}));
|
|
58
|
-
return Layer.unwrap(Effect.map(app.build, (record) => HttpApiBuilder.group(api, app.group.name, (builder) => builder.handleAll(entries(erasedHandlers(record)))))).pipe(Layer.provide(middleware));
|
|
59
|
-
};
|
|
60
|
-
function make(options, ...groups) {
|
|
61
|
-
assertDistinct("action group", groups.map((group) => group.name));
|
|
62
|
-
const bound = new Map(groups.flatMap((group) => {
|
|
63
|
-
const binding = bind(options, group);
|
|
64
|
-
return binding === void 0 ? [] : [[group, binding]];
|
|
65
|
-
}));
|
|
66
|
-
const layer = (apps, layerOptions = {}) => {
|
|
67
|
-
assertDistinct("implementation group", apps.map((app) => app.group.name));
|
|
68
|
-
for (const app of apps) if (!groups.includes(app.group)) throw new Error(`Implementation of group "${app.group.name}" is not served by this adapter`);
|
|
69
|
-
const served = apps.flatMap((app) => {
|
|
70
|
-
const binding = bound.get(app.group);
|
|
71
|
-
return binding === void 0 ? [] : [{
|
|
72
|
-
app,
|
|
73
|
-
...binding
|
|
74
|
-
}];
|
|
75
|
-
});
|
|
76
|
-
const api = apiOf(served.map(({ native }) => native));
|
|
77
|
-
const [first, ...rest] = served.map(({ app, middleware }) => groupHandlers(api, app, middleware, layerOptions.before));
|
|
78
|
-
return first === void 0 ? HttpApiBuilder.layer(api) : HttpApiBuilder.layer(api).pipe(Layer.provide([first, ...rest]));
|
|
79
|
-
};
|
|
80
|
-
const api = apiOf([...bound.values()].map(({ native }) => native));
|
|
81
|
-
const openApi = (path = `${options.apiPath}/openapi.json`) => HttpRouter.add("GET", path, HttpServerResponse.jsonUnsafe(OpenApi.fromApi(api)));
|
|
82
|
-
return {
|
|
83
|
-
groups,
|
|
84
|
-
api,
|
|
85
|
-
layer,
|
|
86
|
-
openApi
|
|
87
|
-
};
|
|
132
|
+
/** A binding's client, erased: `client` and `fetchClient` restore its exact type. */
|
|
133
|
+
const erasedClient = (binding, options) => Effect.map(methods(binding, options), (methodOf) => Object.fromEntries(binding.actions.map((action) => [action.name, methodOf(action)])));
|
|
134
|
+
function client(binding, options) {
|
|
135
|
+
return erasedClient(binding, options);
|
|
136
|
+
}
|
|
137
|
+
function fetchClient(binding, { fetch, ...options } = {}) {
|
|
138
|
+
return erasedClient(binding, options).pipe(Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.Fetch, fetch ?? ((input, init) => globalThis.fetch(input, init))), Effect.runSync);
|
|
88
139
|
}
|
|
89
140
|
//#endregion
|
|
90
|
-
export { make };
|
|
141
|
+
export { client, fetchClient, layer, make };
|
package/dist/ActionMcp.d.mts
CHANGED
|
@@ -1,35 +1,85 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Cause, Layer } from "effect";
|
|
1
|
+
import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Protected, SelectedOf, Selection, Served, ServedRequest } from "./internal/implementation.mjs";
|
|
2
|
+
import { Action_d_exports, Any } from "./Action.mjs";
|
|
3
|
+
import { Any as Any$1, Matching, ProviderOf, RemoteRequest, Required } from "./internal/authentication.mjs";
|
|
4
|
+
import { Cause, Effect, Layer, Stdio } from "effect";
|
|
5
5
|
import { HttpRouter } from "effect/http";
|
|
6
6
|
import { McpSchema, McpServer } from "effect/ai";
|
|
7
|
-
import { Stdio } from "effect/Stdio";
|
|
8
7
|
//#region src/ActionMcp.d.ts
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
11
|
-
* `protocols
|
|
12
|
-
*
|
|
9
|
+
* An MCP server, over HTTP or stdio: every native `McpServer.layerStdio` option except
|
|
10
|
+
* `protocols`, the server information, `instructions` and `extensions`, the actions `A` that
|
|
11
|
+
* are its tools, and the native features it serves beside them.
|
|
13
12
|
*/
|
|
14
|
-
export interface Options<
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
export interface Options<E = never, R = never, A extends Any = Any> extends Omit<Parameters<typeof McpServer.layerStdio>[0], "protocols"> {
|
|
14
|
+
/**
|
|
15
|
+
* The actions that are tools, among the implementations' actions: `[GetUser, RenameUser]`.
|
|
16
|
+
* Each keeps its implementation's authorization and builder. Defaults to every action of them.
|
|
17
|
+
*/
|
|
18
|
+
readonly actions?: ReadonlyArray<A> | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Native MCP features served beside the actions' tools, such as `McpServer.resource`,
|
|
21
|
+
* `McpServer.prompt` and `McpServer.toolkit` layers merged into one. They register on this
|
|
22
|
+
* server's registry; merged beside it instead, they register on another and are not served.
|
|
23
|
+
*/
|
|
24
|
+
readonly features?: Layer.Layer<never, E, R> | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One Streamable HTTP MCP endpoint: `Options`, and every native `McpServer.layerHttp` option
|
|
28
|
+
* except `protocols`, `allowedOrigins` among them, with `path` defaulting to `/mcp`.
|
|
29
|
+
*/
|
|
30
|
+
export interface LayerHttpOptions<E = never, R = never, A extends Any = Any> extends Options<E, R, A>, Omit<Parameters<typeof McpServer.layerHttp>[0], "protocols" | "path"> {
|
|
31
|
+
/** The endpoint's route; defaults to `/mcp`. */
|
|
32
|
+
readonly path?: HttpRouter.PathInput;
|
|
33
|
+
readonly authentication?: Any$1;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The native server supplies its own request context to every tool call, and the registry
|
|
37
|
+
* it registers the tools on, the endpoint's own; over HTTP the router its own, such as the
|
|
38
|
+
* request.
|
|
39
|
+
*/
|
|
40
|
+
type ToolRequestContext<R> = Exclude<R, McpSchema.McpRequestContext | McpServer.McpServer>;
|
|
41
|
+
type HttpToolRequestContext<R> = Exclude<ToolRequestContext<R>, HttpRouter.Provided>;
|
|
42
|
+
/**
|
|
43
|
+
* What the native features owe, `R`, beside the registry, which the endpoint provides them,
|
|
44
|
+
* so one registering through `McpServer.registerResource` owes it no host.
|
|
45
|
+
*/
|
|
46
|
+
type Features<R> = Exclude<R, McpServer.McpServer>;
|
|
47
|
+
/**
|
|
48
|
+
* Where `D`, the descriptor given, is inferred, so the provider owed is that descriptor's.
|
|
49
|
+
* Without one, tools `A` all public take none, even past a `D` given explicitly; protected
|
|
50
|
+
* ones state theirs in `RequiredAuthentication` alone, as an absent `authentication` and the
|
|
51
|
+
* one they require would intersect to `never`, reading as every option being wrong.
|
|
52
|
+
*/
|
|
53
|
+
type Inferring<D, A extends Any> = [D] extends [undefined] ? [Protected<A>] extends [never] ? {
|
|
54
|
+
readonly authentication?: undefined;
|
|
55
|
+
} : unknown : {
|
|
56
|
+
readonly authentication?: D;
|
|
57
|
+
};
|
|
19
58
|
/**
|
|
20
59
|
* Serve MCP tools over one Streamable HTTP endpoint, speaking MCP 2026-07-28 only.
|
|
21
60
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
61
|
+
* Protected tools need the `authentication` descriptor their contracts name, and the
|
|
62
|
+
* provider `Authentication.layer` builds from it. On an endpoint of protected tools only,
|
|
63
|
+
* every request authenticates before it is decoded. On a mixed endpoint, anyone may discover
|
|
64
|
+
* the server, list its tools, cancel, and call a public tool; everything else authenticates
|
|
65
|
+
* first, so a protected tool's description is listed to anyone but run only for its caller.
|
|
66
|
+
* A call refused with `Unauthenticated`, or with `Forbidden` naming scopes, is answered with
|
|
67
|
+
* its HTTP status and challenge, 401 or 403, as MCP authorization requires; any other
|
|
68
|
+
* failure is a tool result. In a tool call, what middleware provides per request wins over
|
|
69
|
+
* what the endpoint was built with; native `features` read only what they were built with,
|
|
70
|
+
* never a request's. Never provide an identity at startup: a tool call takes its identity
|
|
71
|
+
* only from the request it authenticated.
|
|
24
72
|
*/
|
|
25
|
-
export declare function layerHttp<const Apps extends
|
|
73
|
+
export declare function layerHttp<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, E = never, R = never, const D extends Any$1 | undefined = undefined>(implementations: Apps, options: LayerHttpOptions<E, R, ActionOf<Member<Apps>>> & O & NoInfer<Known<O, LayerHttpOptions<E, R, Any>>> & NoInfer<Required<SelectedOf<O, Apps>>> & Matching<NoInfer<SelectedOf<O, Apps>>, NoInfer<D>> & Inferring<D, NoInfer<SelectedOf<O, Apps>>>): Layer.Layer<never, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Cause.IllegalArgumentError | E, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", HttpToolRequestContext<RemoteRequest<Member<Apps>, SelectedOf<O, Apps>>>> | ProviderOf<D> | Features<R>>;
|
|
26
74
|
/**
|
|
27
75
|
* Serve MCP tools through newline-delimited JSON-RPC on standard I/O, speaking MCP
|
|
28
|
-
* 2026-07-28
|
|
29
|
-
*
|
|
76
|
+
* 2026-07-28, 2025-11-25 or 2025-06-18, as the host negotiates: the whole
|
|
77
|
+
* program of an MCP subprocess, which succeeds once the host closes its side and the calls in
|
|
78
|
+
* flight, which that interrupts, have stopped. A signal interrupts it.
|
|
30
79
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
80
|
+
* Effect logs go to stderr, since stdout carries the protocol. The host supplies the
|
|
81
|
+
* `Stdio` service and the identity. Arguments are tool input only and never establish
|
|
82
|
+
* request identity or authority; each implementation's `authorize` runs for its protected actions.
|
|
33
83
|
*/
|
|
34
|
-
export declare function
|
|
84
|
+
export declare function runStdio<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, E = never, R = never>(implementations: Apps, options: Options<E, R, ActionOf<Member<Apps>>> & O & NoInfer<Known<O, Options<E, R, Any>>>): Effect.Effect<void, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Cause.IllegalArgumentError | E, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Stdio.Stdio | ToolRequestContext<ServedRequest<Member<Apps>, SelectedOf<O, Apps>>> | Features<R>>;
|
|
35
85
|
//#endregion
|