@gjermundgaraba/effect-actions 0.9.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.
Files changed (68) hide show
  1. package/README.md +57 -50
  2. package/dist/Action.d.mts +285 -79
  3. package/dist/Action.mjs +124 -23
  4. package/dist/ActionCli.d.mts +102 -31
  5. package/dist/ActionCli.mjs +114 -16
  6. package/dist/ActionHttp.d.mts +170 -51
  7. package/dist/ActionHttp.mjs +130 -79
  8. package/dist/ActionMcp.d.mts +71 -21
  9. package/dist/ActionMcp.mjs +108 -81
  10. package/dist/ActionToolkit.d.mts +56 -27
  11. package/dist/ActionToolkit.mjs +16 -5
  12. package/dist/Authentication.d.mts +129 -33
  13. package/dist/Authentication.mjs +231 -38
  14. package/dist/Testing.d.mts +82 -57
  15. package/dist/Testing.mjs +89 -79
  16. package/dist/internal/actions.d.mts +5 -25
  17. package/dist/internal/actions.mjs +75 -27
  18. package/dist/internal/authentication.d.mts +102 -0
  19. package/dist/internal/authentication.mjs +11 -0
  20. package/dist/internal/call.d.mts +12 -0
  21. package/dist/internal/call.mjs +12 -0
  22. package/dist/internal/cli.d.mts +36 -22
  23. package/dist/internal/cli.mjs +311 -33
  24. package/dist/internal/client.d.mts +43 -0
  25. package/dist/internal/client.mjs +25 -0
  26. package/dist/internal/console.mjs +77 -0
  27. package/dist/internal/declared.d.mts +13 -0
  28. package/dist/internal/declared.mjs +34 -0
  29. package/dist/internal/errors.d.mts +77 -0
  30. package/dist/internal/errors.mjs +79 -0
  31. package/dist/internal/implementation.d.mts +171 -41
  32. package/dist/internal/implementation.mjs +147 -29
  33. package/dist/internal/mcp.d.mts +11 -0
  34. package/dist/internal/mcp.mjs +52 -0
  35. package/dist/internal/memory.d.mts +6 -0
  36. package/dist/internal/memory.mjs +38 -0
  37. package/dist/internal/refusal.mjs +67 -0
  38. package/dist/internal/schema-errors.mjs +20 -0
  39. package/dist/internal/tools.mjs +27 -57
  40. package/docs/Action.md +401 -73
  41. package/docs/ActionCli.md +280 -51
  42. package/docs/ActionHttp.md +266 -82
  43. package/docs/ActionMcp.md +149 -104
  44. package/docs/ActionToolkit.md +97 -35
  45. package/docs/Authentication.md +369 -62
  46. package/docs/CONTEXT.md +23 -36
  47. package/docs/README.md +29 -73
  48. package/docs/Testing.md +154 -77
  49. package/docs/guarantees.md +81 -43
  50. package/docs/setup.md +90 -27
  51. package/package.json +7 -15
  52. package/dist/ActionCatalog.d.mts +0 -30
  53. package/dist/ActionCatalog.mjs +0 -39
  54. package/dist/ActionCliClient.d.mts +0 -48
  55. package/dist/ActionCliClient.mjs +0 -43
  56. package/dist/ActionGroup.d.mts +0 -44
  57. package/dist/ActionGroup.mjs +0 -35
  58. package/dist/ActionHttpClient.d.mts +0 -33
  59. package/dist/ActionHttpClient.mjs +0 -16
  60. package/dist/TestingClient.d.mts +0 -17
  61. package/dist/TestingClient.mjs +0 -22
  62. package/dist/internal/fetchClient.d.mts +0 -6
  63. package/dist/internal/fetchClient.mjs +0 -8
  64. package/dist/internal/tools.d.mts +0 -22
  65. package/docs/ActionCatalog.md +0 -54
  66. package/docs/ActionCliClient.md +0 -63
  67. package/docs/ActionGroup.md +0 -128
  68. package/docs/ActionHttpClient.md +0 -80
@@ -1,62 +1,181 @@
1
- import { Action_d_exports, Any, Codec } from "./Action.mjs";
2
- import { Actions, PolicyError } from "./internal/actions.mjs";
3
- import { AnyImplementation, BuildContext, BuildError, RequestContext } from "./internal/implementation.mjs";
4
- import { Effect, Layer, Schema } from "effect";
5
- import { Etag, HttpPlatform, HttpRouter } from "effect/http";
6
- import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/http-api";
7
- import { FileSystem } from "effect/FileSystem";
8
- import { Path } from "effect/Path";
9
- declare namespace ActionHttp_d_exports {
10
- export { Api, Http, LayerOptions, Options, make };
11
- }
12
- /** Contract-level configuration: servers and clients must agree on it. */
13
- export interface Options<Errors extends ReadonlyArray<Codec> = []> {
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
- * Runs once after successful payload decoding, before the selected handler,
28
- * with its action contract. It fails with the binding's own `errors`, encoded
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 before?: (action: Any) => Effect.Effect<void, Errors[number]["Type"], R>;
18
+ readonly fetch?: typeof globalThis.fetch | undefined;
33
19
  }
34
- type Endpoint<A extends Any, E extends Codec> = A extends Any ? HttpApiEndpoint.HttpApiEndpoint<A["name"], "POST", `/${string}`, never, never, Schema.toCodecJson<A["input"]>, never, Schema.toCodecJson<A["success"]>, Schema.toCodecJson<A["errors"][number] | E>, never> : never;
35
- type ApiGroup<G extends Actions, E extends Codec> = G extends Actions ? [Endpoint<G["actions"][number], never>] extends [never] ? never : HttpApiGroup.HttpApiGroup<G["name"], Endpoint<G["actions"][number], PolicyError<G> | E>> : never;
36
- /** The native `HttpApi` of every action of `G`, one group per action group. */
37
- export type Api<G extends Actions, E extends Codec = never> = HttpApi.HttpApi<"actions", ApiGroup<G, E>>;
38
- /** A namespaced HTTP binding. */
39
- export interface Http<Groups extends ReadonlyArray<Actions>, Errors extends ReadonlyArray<Codec> = []> {
40
- /** The exact contracts bound to this HTTP adapter, in declaration order. */
41
- readonly groups: Groups;
42
- readonly api: Api<Groups[number], Errors[number]>;
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
- * Serve any number of implementations in one layer, with one pre-handler hook
45
- * around every request they answer. Build failures, build services and request
46
- * services are unions over precisely those apps and that hook.
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 layer: <const Apps extends ReadonlyArray<AnyImplementation<Groups[number]>>, RB = never>(apps: readonly [...Apps], options?: LayerOptions<Errors, RB>) => Layer.Layer<never, BuildError<Apps[number]>, BuildContext<Apps[number]> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", RequestContext<Apps[number]> | RB> | Etag.Generator | FileSystem | HttpPlatform.HttpPlatform | Path>;
72
+ readonly actions?: ReadonlyArray<A> | undefined;
49
73
  /**
50
- * Serve the OpenAPI document of `api` with `GET path`, by default
51
- * `<apiPath>/openapi.json`. It is a plain route: middleware provided to this layer
52
- * covers it, and none is applied otherwise.
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 openApi: (path?: HttpRouter.PathInput) => Layer.Layer<never, never, HttpRouter.HttpRouter>;
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 the contract-level configuration once. Every route is `POST
58
- * <apiPath>/<group>/<action>`, so action names may repeat across groups.
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 make<const G extends ReadonlyArray<Actions>, const E extends ReadonlyArray<Codec> = []>(options: Options<E>, ...groups: G): Http<G, E>;
179
+ export declare function fetchClient<const H extends AnyHttp>(binding: H, options?: FetchClientOptions): Client<H>;
61
180
  //#endregion
62
- export { ActionHttp_d_exports };
181
+ export { type AnyHttp as Any, type Client, type Options as ClientOptions, type MethodError, Options$1 as Options };
@@ -1,90 +1,141 @@
1
- import { answerSchemaError, assertDistinct, policyErrors, projectedErrors } from "./internal/actions.mjs";
2
- import { dispatch } from "./internal/implementation.mjs";
3
- import { Effect, Layer } from "effect";
4
- import { HttpRouter, HttpServerResponse } from "effect/http";
5
- import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/http-api";
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
- const endpoint = (options, group, action) => HttpApiEndpoint.post(action.name, `${options.apiPath}/${group.name}/${action.name}`, {
8
- payload: action.input,
9
- success: action.success,
10
- error: projectedErrors(action, options.errors)
11
- }).annotate(OpenApi.Description, action.description);
12
- /** The native middleware declares the policy's errors on every endpoint it wraps. */
13
- const schemaErrorMiddleware = (name, policy) => {
14
- class SchemaErrors extends HttpApiMiddleware.Service()(`effect-actions/http/SchemaErrors/${name}`, { error: policyErrors(policy) }) {}
15
- return {
16
- SchemaErrors,
17
- layer: HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(answerSchemaError(policy, failure)))
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
- function bind(options, group) {
21
- const [first, ...rest] = group.actions.map((action) => endpoint(options, group, action));
22
- if (first === void 0) return void 0;
23
- const bound = HttpApiGroup.make(group.name).add(first, ...rest);
24
- if (group.schemaError === void 0) return {
25
- native: bound,
26
- middleware: Layer.empty
27
- };
28
- const policy = schemaErrorMiddleware(group.name, group.schemaError);
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
- native: bound.middleware(policy.SchemaErrors),
31
- middleware: policy.layer
74
+ actions,
75
+ error: errors,
76
+ prefix: route(mount),
77
+ api,
78
+ authentication: options.authentication
32
79
  };
33
80
  }
34
- function apiOf(groups) {
35
- const [first, ...rest] = groups;
36
- const empty = HttpApi.make("actions").annotate(HttpApi.PayloadParseOptions, {
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
- return first === void 0 ? empty : empty.add(first, ...rest);
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
- * `HttpApiBuilder.handleAll` registers a dynamically assembled record. The
44
- * action selected by the native router determines the input before `dispatch`
45
- * calls its handler, so this is the one adapter boundary where exact handlers
46
- * are viewed as an erased record. `HandlersContext` remains on the dispatch
47
- * effect and is restored by `Http.layer`'s public signature.
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 };
@@ -1,35 +1,85 @@
1
- import { Action_d_exports, Codec } from "./Action.mjs";
2
- import { AnyImplementation, BuildContext, BuildError, HiddenFromMcp, RequestContext } from "./internal/implementation.mjs";
3
- import { SurfaceOptions } from "./internal/tools.mjs";
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
- * One Streamable HTTP MCP endpoint: every native `McpServer.layerHttp` option except
11
- * `protocols` (server information, `path`, `allowedOrigins`, `instructions`,
12
- * `extensions`, ...), plus the surface's `errors` and `before`.
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<Errors extends ReadonlyArray<Codec> = [], R = never> extends Omit<Parameters<typeof McpServer.layerHttp>[0], "protocols">, SurfaceOptions<Errors, R> {}
15
- /** An MCP subprocess on standard I/O: every native `McpServer.layerStdio` option except `protocols`. */
16
- export interface StdioOptions<Errors extends ReadonlyArray<Codec> = [], R = never> extends Omit<Parameters<typeof McpServer.layerStdio>[0], "protocols">, SurfaceOptions<Errors, R> {}
17
- /** The native server supplies its own request context to every tool call. */
18
- type ToolRequestContext<App, RB> = Exclude<RequestContext<App, HiddenFromMcp> | RB, McpSchema.McpRequestContext>;
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
- * Middleware provided around this layer has the normal HTTP lifetime. Native
23
- * context capture applies: never provide request-identity tags at startup.
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 ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options: Options<Errors, RB>): Layer.Layer<never, BuildError<Apps[number], HiddenFromMcp> | Cause.IllegalArgumentError, BuildContext<Apps[number], HiddenFromMcp> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", ToolRequestContext<Apps[number], RB>>>;
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 only. An older host is refused deliberately, for uniformity with HTTP,
29
- * even though stdio has no sessions.
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
- * The host supplies the `Stdio` service. Arguments are tool input only and
32
- * never establish request identity or authority.
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 layerStdio<const Apps extends ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options: StdioOptions<Errors, RB>): Layer.Layer<never, BuildError<Apps[number], HiddenFromMcp> | Cause.IllegalArgumentError, BuildContext<Apps[number], HiddenFromMcp> | Stdio | ToolRequestContext<Apps[number], RB>>;
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