@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
package/README.md CHANGED
@@ -1,94 +1,99 @@
1
1
  # effect-actions
2
2
 
3
3
  Define an Effect action once. Serve it over HTTP and MCP, hand it to a model as a native
4
- Toolkit, run it from a CLI, and publish its contract as JSON. Same schemas, same handler,
5
- Effect's own servers and clients underneath.
4
+ Toolkit, and run it from a CLI. Same schemas, same handler, Effect's own servers and clients
5
+ underneath.
6
6
 
7
7
  ## Looks like this
8
8
 
9
- ```ts
10
- import { Effect, Layer, Schema } from "effect";
9
+ ```ts example=quickstart.ts
10
+ import { Schema } from "effect";
11
11
  import * as Action from "@gjermundgaraba/effect-actions/Action";
12
- import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
13
12
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
14
- import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
15
13
 
16
- const Greet = Action.make("greet", {
14
+ // The contract and its HTTP binding import no server code, so any client can import them,
15
+ // a browser page included.
16
+ export const Greet = Action.make("greet", {
17
17
  description: "Greet someone by name.",
18
- input: Schema.Struct({ name: Schema.String }),
18
+ input: { name: Schema.String },
19
19
  success: Schema.String,
20
- access: "read",
20
+ readOnly: true,
21
+ caller: Action.Anyone,
21
22
  });
22
23
 
23
- export const Actions = ActionGroup.make({ name: "greetings" }, Greet);
24
+ export const Http = ActionHttp.make([Greet]);
25
+ ```
24
26
 
25
- export const Http = ActionHttp.make({ apiPath: "/api/actions" }, Actions);
27
+ and a server that implements it, in a module of its own:
26
28
 
27
- const app = Actions.implement({
28
- greet: ({ name }) => Effect.succeed(`Hello, ${name}!`),
29
- });
29
+ ```ts example=quickstart-server.ts
30
+ import { Effect, Layer } from "effect";
31
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
32
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
33
+ import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
34
+ import { Greet, Http } from "./quickstart.js";
35
+
36
+ // The contract states who may call it, here anyone, so its implementation takes no `authorize`.
37
+ const greet = Action.implement(Greet, ({ name }) => Effect.succeed(`Hello, ${name}!`));
30
38
 
31
39
  export const routes = Layer.mergeAll(
32
- Http.layer([app]),
33
- ActionMcp.layerHttp([app], {
34
- name: "greetings",
35
- version: "1.0.0",
36
- path: "/mcp",
37
- }),
40
+ ActionHttp.layer(Http, greet),
41
+ ActionMcp.layerHttp(greet, { name: "greetings", version: "1.0.0" }),
38
42
  );
39
43
  ```
40
44
 
41
- Serve `routes` with Effect's `HttpRouter` and you have `POST /api/actions/greetings/greet`
42
- and an MCP tool named `greet` at `/mcp`. The same `app` is also:
45
+ Serve `routes` with Effect's `HttpRouter.serve` and a platform server, with a request body
46
+ limit, as [examples/server.ts](examples/server.ts) serves the example application, and you
47
+ have `POST /api/greet` and an MCP tool named `greet` at `/mcp`. The same `greet`
48
+ implementation is also:
43
49
 
44
50
  ```ts
45
- const { toolkit, layer } = ActionToolkit.make([app]); // native Effect AI Toolkit and its handler layer
46
- const cli = ActionCli.group(app); // greetings greet --input '{"name":"Ada"}'
47
- const remote = ActionCliClient.command(Http, "greetings", "greet"); // same command, over HTTP
48
- const catalog = ActionCatalog.make(Actions); // offline JSON contract, no handlers acquired
51
+ const { toolkit, layer } = ActionToolkit.make(greet); // native Effect AI Toolkit and its handler layer
52
+ const cli = ActionCli.make(greet, { name: "greetings" }); // greetings greet --name Ada
53
+ const remote = ActionCli.remoteCommand(Http, Greet, { client: { baseUrl } }); // greet --name Ada, over HTTP
54
+ const local = Action.client(greet); // the HTTP client's methods, in process
49
55
  ```
50
56
 
51
- And the client is Effect's own, typed from the same contract:
57
+ And the client is Effect's own `HttpApiClient`, typed from the same contract, one method per action:
52
58
 
53
59
  ```ts
54
60
  const greeting = Effect.gen(function* () {
55
- const client = yield* HttpApiClient.make(Http.api, { baseUrl });
61
+ const client = yield* ActionHttp.client(Http, { baseUrl });
56
62
 
57
- return yield* client.greetings.greet({ payload: { name: "Ada" } });
63
+ return yield* client.greet({ name: "Ada" });
58
64
  });
59
65
  ```
60
66
 
61
- Code that does not run Effects, such as a browser page, gets the same calls as Promises:
62
- `await ActionHttpClient.promise(Http, { baseUrl }).greetings.greet({ name: "Ada" })`.
63
-
64
67
  ## Why
65
68
 
66
- - Input, success, and errors are declared once. Routes, tools, commands, clients, OpenAPI, and the catalog are derived from that declaration, so they cannot disagree.
67
- - A handler may fail only with the errors its action declares. Each surface binds one `before` hook that runs before every handler it serves — after successful input decoding — so a policy such as scope enforcement is written once per surface and no action of that surface can skip it.
68
- - The pieces are Effect's own. `Http.api` is a native `HttpApi`, so `OpenApi.fromApi`, Swagger, Scalar, and `HttpApiClient` work on it unchanged. MCP is Effect's native `McpServer`, one `Tool` per action, with no SDK runtime dependency.
69
- - Every action states its `access` (`"read"` or `"write"`, required), so authorization reads the contract instead of a hand-maintained list of mutation names.
70
- - Build-time services and per-request services are tracked separately in the types. Middleware is per group, so a public group and an authenticated group can share one mount path.
71
- - Tests run in memory. Call the routes through a web handler with the same typed client, or drive the official MCP client, without opening a port.
69
+ - Input, success and errors are declared once. Routes, tools, CLI flags, clients and the OpenAPI document are derived from that declaration, so they cannot disagree.
70
+ - A handler can fail only with the errors its action declares and three built-in ones, and every client decodes them as typed values. Bad input, a missing credential and a refusal come back as those built-in errors (400, 401, 403), never as an unreadable body.
71
+ - Every contract states who may call it: `caller: Action.Anyone`, or the identity service a caller must have. A surface serving a protected action authenticates it before decoding its input, through the authentication descriptor its binding names, and the provider the application builds from its own token verifier.
72
+ - Action-level authorization is written once, on the implementation: `Action.implement(actions, handlers, { authorize })`, required for protected actions. Every surface that serves it runs the rule before each handler, so no surface can leave it out and no handler repeats the access policy; `Action.allowAll` admits every authenticated caller. Operational limits, such as a rate limit, and record-level checks stay in the handler, failing with errors the contract lists.
73
+ - Every action states whether it is read-only (`readOnly`), so that rule reads the contract instead of a hand-maintained list of mutation names.
74
+ - A protected action's handler and rule read the identity its contract names, which only authentication of that request supplies; a startup identity never opens a protected route. Public and protected routes share one binding, one mount path, one OpenAPI document, which states the credentials each needs, and one client, and a handler's startup services are built once per [layer graph](docs/guarantees.md#dependency-lifetimes), however many surfaces serve it.
75
+ - The pieces are Effect's own. `Http.api` is a native `HttpApi`, so OpenAPI, Swagger, Scalar and `HttpApiClient` work on it unchanged. MCP is Effect's native `McpServer`, with no SDK runtime dependency.
76
+ - Tests run in memory. `Action.client` calls an implementation in process, with the typed client's methods, behind its `authorize`, as several callers in one test. Provide `Testing.layer(routes)`, then call the routes with the same typed client and the tools with `Testing.mcpClient`, without opening a port.
72
77
 
73
78
  ## Install
74
79
 
75
80
  ```sh
76
- pnpm add @gjermundgaraba/effect-actions effect@^4.0.0
81
+ pnpm add @gjermundgaraba/effect-actions effect@~4.0.0
77
82
  ```
78
83
 
79
- Add `@effect/platform-node@^4.0.0` to serve from Node. Every module is a subpath import
80
- (`.../Action`, `.../ActionHttp`, ...); there is no package root, so a contracts-only bundle
81
- never loads a server.
84
+ Add `@effect/platform-node@~4.0.0` to serve or run a CLI from Node. Every module is a
85
+ subpath import (`.../Action`, `.../ActionHttp`, ...); there is no package root, so a
86
+ contracts-only bundle never loads a server.
82
87
 
83
88
  ## Docs
84
89
 
85
90
  The reference in [docs/](docs/README.md) is written for coding agents: one card per module
86
91
  with the API, a canonical snippet, the rules, and the failure modes.
87
92
 
88
- - [docs/README.md](docs/README.md): start here, includes the adapter decision list.
93
+ - [docs/README.md](docs/README.md): start here, includes which module to use for which caller.
89
94
  - [docs/guarantees.md](docs/guarantees.md): cross-cutting rules for lifetimes, wire formats, and scope.
90
95
  - [docs/CONTEXT.md](docs/CONTEXT.md): the vocabulary the docs and the code use.
91
- - [examples/](examples/README.md): a runnable authenticated application with three groups, two MCP endpoints, OpenAPI, and every other projection.
96
+ - [examples/](examples/README.md): a runnable authenticated application with public and authenticated actions, an MCP endpoint, OpenAPI, and every other surface.
92
97
 
93
98
  ## For agents
94
99
 
@@ -96,18 +101,20 @@ with the API, a canonical snippet, the rules, and the failure modes.
96
101
  npx skills add gjermundgaraba/effect-actions --skill effect-actions
97
102
  ```
98
103
 
99
- The skill is generated from `docs/`, so pointing an agent at `docs/` (in this repository or
100
- in `node_modules/@gjermundgaraba/effect-actions/docs`) gives the same content.
104
+ The skill is generated from `docs/` on the main branch, which can be ahead of the latest
105
+ release; in a project that installs the package, point the agent at
106
+ `node_modules/@gjermundgaraba/effect-actions/docs`, the pages of the installed version.
101
107
 
102
108
  ## Status
103
109
 
104
- The `effect` peer is `^4.0.0`; the package is built and tested against the Effect release in
105
- its `devDependencies` (see [docs/setup.md](docs/setup.md)). Actions are unary JSON over HTTP and MCP: no streaming, uploads, prompts, or resources.
106
- Authentication and authorization belong to the application. See [docs/setup.md](docs/setup.md).
110
+ The `effect` peer is `~4.0.0`, Effect's 4.0.x patches, since the modules the surfaces build on may
111
+ change in a minor release; the package is built and tested against the Effect release in its
112
+ `devDependencies` (see [docs/setup.md](docs/setup.md)). Actions are unary JSON over HTTP and MCP: no streaming, uploads, prompts, or resources.
113
+ Token verification and the authorization rule belong to the application; the library supplies the authentication descriptor and provider, runs the rule, and answers the refusals. See [docs/setup.md](docs/setup.md).
107
114
 
108
115
  ## Acknowledgements
109
116
 
110
- A few ideas (like the native Toolkit, CLI, and catalog projections) are inspired by the excellent [rat-stack](https://github.com/joelhooks/rat-stack) by Joel Hooks.
117
+ A few ideas (like the native Toolkit and CLI projections) are inspired by the excellent [rat-stack](https://github.com/joelhooks/rat-stack) by Joel Hooks.
111
118
 
112
119
  ## Contributing
113
120
 
package/dist/Action.d.mts CHANGED
@@ -1,95 +1,301 @@
1
- import { Effect, Schema } from "effect";
1
+ import { Anyone } from "./internal/actions.mjs";
2
+ import { BuiltIn, BuiltIns, Forbidden, InvalidInput, Issue, Refusal, Unauthenticated } from "./internal/errors.mjs";
3
+ import { ActionOf, AnyImplementation, Authorize, BuildError, BuildServices, Holding, Implementation, Known, Member, Offered, OptionalUnless, Protected, RequestOf, SelectedOf, Served, Serving } from "./internal/implementation.mjs";
4
+ import { Call } from "./internal/call.mjs";
5
+ import { Context, Effect, Layer, Schema, Scope } from "effect";
2
6
  declare namespace Action_d_exports {
3
- export { Access, Action, Any, Codec, Handler, McpOptions, Options, make };
7
+ export { Action, Any, AnyImplementation, Anyone, Authorize, BuildError, BuildServices, BuiltIn, Client, ClientOptions, Errors, Forbidden, Handler, Handlers, Implementation, InvalidInput, Issue, Mcp, Options, Refusal, Unauthenticated, allowAll, byName, client, implement, layer, make };
4
8
  }
5
9
  /** Any service-free schema. Only handlers may require services. */
6
- export type Codec = Schema.Codec<unknown, unknown, never, never>;
7
- /**
8
- * What an action does to the resource it serves: `"read"` observes, `"write"`
9
- * may change it. Authorization metadata, not an MCP hint.
10
- */
11
- export type Access = "read" | "write";
12
- /** MCP tool metadata; every field has a default derived from the action. */
13
- export interface McpOptions {
14
- /** Tool name; defaults to the action name. Must match `^[A-Za-z0-9_-]{1,128}$`. */
15
- readonly name?: string;
16
- /** `readOnlyHint`; defaults to `access === "read"`. */
17
- readonly readOnly?: boolean;
18
- /** `destructiveHint`; defaults to `!readOnly`, as the MCP spec only defines it for writes. */
19
- readonly destructive?: boolean;
20
- /** `idempotentHint`; defaults to `false`. */
21
- readonly idempotent?: boolean;
22
- /** `openWorldHint`; defaults to `true`. */
23
- readonly openWorld?: boolean;
24
- /**
25
- * A string field of the encoded success that MCP sends as a raw text block rather
26
- * than as structured content, such as a document body. Other surfaces serve the
27
- * whole success.
28
- */
29
- readonly text?: string;
30
- }
31
- /** The fields `mcp.text` may name: those an encoded success has as a string. */
32
- type TextFields<Encoded> = Encoded extends object ? { readonly [K in keyof Encoded]-?: Required<Encoded>[K] extends string ? K : never; }[keyof Encoded] & string : never;
33
- type ResolvedMcp<Name extends string, Mcp extends false | McpOptions | undefined> = Mcp extends false ? false : {
34
- readonly name: Mcp extends McpOptions ? "name" extends keyof Mcp ? Extract<Mcp["name"], string> extends never ? Name : Extract<Mcp["name"], string> | (undefined extends Mcp["name"] ? Name : never) : Name : Name;
35
- readonly readOnly: boolean;
36
- readonly destructive: boolean;
37
- readonly idempotent: boolean;
38
- readonly openWorld: boolean;
39
- readonly text?: string;
10
+ type Codec = Schema.Codec<unknown, unknown, never, never>;
11
+ /** Struct fields, accepted wherever a struct schema is: `{ name: Schema.String }`. */
12
+ type Fields = {
13
+ readonly [key: string]: Codec;
40
14
  };
15
+ /**
16
+ * An object without fields: an action without arguments, `{}` given or not. Effect's own
17
+ * `Tool.EmptyParams`: strict, unlike `Schema.Struct({})`, which accepts any value but `null`,
18
+ * and the object root MCP requires.
19
+ */
20
+ declare const NoInput: Schema.$Record<Schema.String, Schema.Never>;
21
+ /** The schema a `Codec | Fields` option stands for: a schema itself, `NoInput` for no fields. */
22
+ type CodecOf<S extends Codec | Fields> = S extends Codec ? S : S extends Schema.Struct.Fields ? keyof S extends never ? typeof NoInput : Extract<Schema.Struct<S>, Codec> : never;
23
+ /**
24
+ * How the action's tool presents itself over MCP, in MCP's own names; every field is optional
25
+ * and none enforces anything. A hint left out is MCP's default, but `destructiveHint`, which is
26
+ * `false` for a read-only action. `readOnlyHint` is not one: it is always the contract's
27
+ * `readOnly`, so a tool cannot say otherwise than its contract.
28
+ */
29
+ export interface Mcp {
30
+ /** The tool's display name, `annotations.title`, beside its name. */
31
+ readonly title?: string | undefined;
32
+ /** `annotations.destructiveHint`, meaningful for a write only. */
33
+ readonly destructiveHint?: boolean | undefined;
34
+ /** `annotations.idempotentHint`, meaningful for a write only. */
35
+ readonly idempotentHint?: boolean | undefined;
36
+ /** `annotations.openWorldHint`: whether the tool reaches entities outside its own domain. */
37
+ readonly openWorldHint?: boolean | undefined;
38
+ /** The tool's `_meta`, JSON such as an MCP App's UI resource, sent as given. */
39
+ readonly _meta?: {
40
+ readonly [key: string]: Schema.Json;
41
+ } | undefined;
42
+ }
41
43
  /** What `make` needs to define an action. */
42
- export interface Options<Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Acc extends Access = Access, Mcp extends false | McpOptions | undefined = McpOptions | undefined> {
44
+ export interface Options {
45
+ /**
46
+ * Who may call the action: `Action.Anyone`, or the identity service a caller must have,
47
+ * which every surface enforces. Required: an action that names no caller is not public.
48
+ */
49
+ readonly caller: typeof Anyone | Context.Key<unknown, unknown>;
43
50
  readonly description: string;
44
- /** Omit for an action without arguments. */
45
- readonly input?: Input;
46
- readonly success: Output;
47
- /** One schema per declared failure; each keeps its own HTTP status annotation. Defaults to none. */
48
- readonly errors?: Errors;
51
+ /** A schema or struct fields. Omit, or give `{}`, for an action without arguments. */
52
+ readonly input?: Codec | Fields | undefined;
53
+ /** A schema or struct fields. Omit for an action that returns nothing: `Schema.Void`. */
54
+ readonly success?: Codec | Fields | undefined;
55
+ /**
56
+ * The declared failures: one schema, or a list of them, as `HttpApiEndpoint` takes them;
57
+ * each keeps its own HTTP status annotation. Defaults to none. None with a built-in
58
+ * error's `_tag`.
59
+ */
60
+ readonly error?: Codec | ReadonlyArray<Codec> | undefined;
49
61
  /**
50
- * What the action does to its resource. Required: an action nobody classified
51
- * is the one a reviewer must check. Read by a surface's `before` hook; the
52
- * library itself authorizes nothing.
62
+ * Whether the action leaves its resource unchanged. Required: an action nobody classified
63
+ * is the one a reviewer must check. An implementation's `authorize` may read it, and MCP's
64
+ * `readOnlyHint` is it; the library itself authorizes nothing with it.
53
65
  */
54
- readonly access: Acc;
55
- /** `false` hides the action from MCP; otherwise tool metadata. */
66
+ readonly readOnly: boolean;
67
+ /** How the action's tool presents itself over MCP, and on a native Toolkit's tools. */
56
68
  readonly mcp?: Mcp;
57
69
  }
58
- /** A pure contract: schemas and transport metadata. Implementations are bound by `ActionGroup.implement`. */
59
- export interface Action<Name extends string, Input extends Codec, Output extends Codec, Errors extends ReadonlyArray<Codec>, Acc extends Access = Access, Mcp extends false | McpOptions | undefined = McpOptions | undefined> {
70
+ /**
71
+ * The keys beyond `Mcp` in the `mcp` of any member of `O`, given or optional, so a misspelled
72
+ * one in a conditional spread or in one branch of a ternary is refused too.
73
+ */
74
+ type UnknownMcp<O> = O extends {
75
+ readonly mcp?: infer H;
76
+ } ? Exclude<H extends unknown ? keyof H : never, keyof Mcp> : never;
77
+ /** No key beyond `Mcp`, whichever member of `O` gives it. */
78
+ type KnownMcp<O> = {
79
+ readonly mcp?: { readonly [K in UnknownMcp<O>]: never; };
80
+ };
81
+ /** The schemas an `error` option of type `G` declares: one, or each of a list. */
82
+ type ErrorMembers<G> = G extends ReadonlyArray<infer E> ? E : G;
83
+ /**
84
+ * The built-in errors, refused in `error`: every surface declares them already. The types
85
+ * refuse only the built-ins themselves; `make` refuses, when called, an error of your own
86
+ * that encodes with a built-in `_tag`.
87
+ */
88
+ type OwnErrors<O> = O extends {
89
+ readonly error: infer G;
90
+ } ? [Extract<ErrorMembers<G>, BuiltIns>] extends [never] ? unknown : {
91
+ readonly error: Exclude<ErrorMembers<G>, BuiltIns> | ReadonlyArray<Exclude<ErrorMembers<G>, BuiltIns>>;
92
+ } : unknown;
93
+ /**
94
+ * No option beyond `Options` in any member of `O`, so a misspelled one is an unknown
95
+ * property, not ignored: `keyof` a union holds only the keys every member has.
96
+ */
97
+ type OnlyOptions<O> = { readonly [K in Exclude<O extends unknown ? keyof O : never, keyof Options>]: never; };
98
+ /**
99
+ * `caller` is a `Context.Service`: a `Context.Reference` is never missing, so its default
100
+ * would stand in for every caller who supplies none.
101
+ */
102
+ type ServiceCaller<O> = O extends {
103
+ readonly caller: {
104
+ readonly defaultValue: unknown;
105
+ };
106
+ } ? {
107
+ readonly caller: "An identity is a Context.Service, not a Context.Reference";
108
+ } : unknown;
109
+ /**
110
+ * The rules `make` checks beyond `Options`: every option and `mcp` key known, and no built-in
111
+ * error listed. Options that fail `Options` itself infer as `Options`, whose error the
112
+ * compiler already reports, so they are not checked again. Checked after inference: `make`'s
113
+ * options are `O & NoInfer<Rules<O>>`.
114
+ */
115
+ type Rules<O> = Options extends O ? unknown : OnlyOptions<O> & ServiceCaller<O> & OwnErrors<O> & KnownMcp<O>;
116
+ /**
117
+ * Option `K` of `O` as given, or also `Default` wherever it may be omitted or undefined, as
118
+ * at run time, for each member of a union of options.
119
+ */
120
+ type OptionOf<O, K extends keyof Options, Default> = O extends unknown ? K extends keyof O ? Exclude<O[K], undefined> | ({} extends Pick<O, K> ? Default : undefined extends O[K] ? Default : never) : Default : never;
121
+ /** The schema option `K` of `O`, or `Default` when it is omitted. */
122
+ type SchemaOf<O, K extends "input" | "success", Default extends Codec> = CodecOf<Extract<OptionOf<O, K, Default>, Codec | Fields>>;
123
+ /** The declared errors of `O`, none when omitted. */
124
+ type ErrorsOf<O> = ReadonlyArray<ErrorMembers<Extract<OptionOf<O, "error", []>, Codec | ReadonlyArray<Codec>>>>;
125
+ /** A pure contract: schemas and transport metadata. Handlers are bound by `implement`. */
126
+ export interface Action<Name extends string, Input extends Codec, Success extends Codec, Errors extends ReadonlyArray<Codec>, ReadOnly extends boolean = boolean, Caller extends Options["caller"] = Options["caller"]> {
127
+ readonly caller: Caller;
60
128
  readonly name: Name;
61
129
  readonly description: string;
62
130
  readonly input: Input;
63
- readonly success: Output;
64
- readonly errors: Errors;
65
- readonly access: Acc;
66
- readonly mcp: ResolvedMcp<Name, Mcp>;
131
+ readonly success: Success;
132
+ /** The declared failures, as a list: one given alone is a list of one. */
133
+ readonly error: Errors;
134
+ readonly readOnly: ReadOnly;
135
+ /** The `mcp` options as given, none when left out. */
136
+ readonly mcp: Mcp;
67
137
  }
68
138
  /** Any action, with its schemas erased. */
69
- export type Any = Action<string, Codec, Codec, ReadonlyArray<Codec>, Access, false> | Action<string, Codec, Codec, ReadonlyArray<Codec>, Access, McpOptions> | Action<string, Codec, Codec, ReadonlyArray<Codec>, Access, undefined>;
70
- /** Receives decoded input; may fail only with the declared errors. */
71
- export type Handler<A extends Any, R = never> = (input: A["input"]["Type"]) => Effect.Effect<A["success"]["Type"], A["errors"][number]["Type"], R>;
72
- /** An empty object schema that also produces the object root MCP requires. */
73
- declare const NoInput: Schema.$Record<Schema.String, Schema.Never>;
74
- type AnyOptions = Options<Codec, Codec, ReadonlyArray<Codec>, Access, false | McpOptions | undefined>;
75
- /**
76
- * The `mcp` option `make` received. Only a required literal `false` hides the action:
77
- * options that may carry something else, such as a conditional spread, `false | undefined`
78
- * or a broadly typed variable, may serve it, so its requirements are kept.
79
- */
80
- type McpOf<O extends AnyOptions> = [O] extends [{
81
- readonly mcp: false;
82
- }] ? false : O extends {
83
- readonly mcp: infer Mcp extends false | McpOptions | undefined;
84
- } ? Mcp : "mcp" extends keyof O ? O["mcp"] | undefined : undefined;
85
- /**
86
- * Define an action contract. Names are `[A-Za-z0-9_-]+`, other than `then`.
87
- * HTTP serves every action of a group it binds; `mcp: false` hides one from MCP.
88
- */
89
- export declare function make<const Name extends string, const O extends AnyOptions>(name: Name, options: O & { readonly [K in Exclude<keyof O, keyof AnyOptions>]: never; } & {
90
- readonly mcp?: false | (McpOptions & {
91
- readonly text?: TextFields<O["success"]["Encoded"]>;
92
- }) | undefined;
93
- }): Action<Name, "input" extends keyof O ? Exclude<O["input"], undefined> : typeof NoInput, O["success"], "errors" extends keyof O ? Exclude<O["errors"], undefined> : [], O["access"], McpOf<O>>;
139
+ export type Any = Action<string, Codec, Codec, ReadonlyArray<Codec>>;
140
+ /**
141
+ * A list of declared errors, as an action's `error` holds it, and a binding's or an
142
+ * authentication descriptor's: `<E extends Action.Errors>` constrains one.
143
+ */
144
+ export type Errors = ReadonlyArray<Codec>;
145
+ /** Receives decoded input; may fail only with the declared errors and the built-in ones. */
146
+ export type Handler<A extends Any, R = never> = (input: A["input"]["Type"]) => Effect.Effect<A["success"]["Type"], A["error"][number]["Type"] | BuiltIn, R>;
147
+ /**
148
+ * The handlers of the actions `A`, keyed by name, as `implement` takes them: what a builder
149
+ * written apart from `implement` returns, `satisfies Action.Handlers<typeof actions>`, so its
150
+ * handlers are typed from their contracts, as another authorizer of the same handlers needs.
151
+ */
152
+ export type Handlers<A extends ReadonlyArray<Any>, R = never> = { readonly [Action in A[number] as Action["name"]]: Handler<Action, R>; };
153
+ /**
154
+ * Define an action contract. Names are `[A-Za-z0-9_-]{1,128}`, other than `then`: the name
155
+ * is also the route segment, the client method and the tool name.
156
+ */
157
+ export declare function make<const Name extends string, const O extends Options>(name: Name, options: O & NoInfer<Rules<O>>): Action<Name, SchemaOf<O, "input", typeof NoInput>, SchemaOf<O, "success", typeof Schema.Void>, ErrorsOf<O>, O["readOnly"], O["caller"]>;
158
+ /**
159
+ * `actions` keyed by name, each its exact contract: `byName(actions).getUser`, its schemas
160
+ * where a test or a mock reads them, and its types where a helper takes a name,
161
+ * `(typeof contracts)[K]["input"]["Type"]`, which resolves under a generic `K` where
162
+ * `Extract` over the list does not. The list is typed by its members, as every surface types
163
+ * one. A name held twice is refused, as a surface refuses it.
164
+ */
165
+ export declare function byName<const Actions extends ReadonlyArray<Any>>(actions: Actions): { readonly [A in Actions[number] as A["name"]]: A; };
166
+ /** What `implement` binds: one action, or several that share one builder. */
167
+ type Target = Any | ReadonlyArray<Any>;
168
+ /**
169
+ * A target of public actions only, which takes no `authorize`. Named for what a protected
170
+ * target lacks there, as the error a call without `authorize` reports names this type.
171
+ */
172
+ type ProtectedActionsTakeAuthorize = PublicAction | ReadonlyArray<PublicAction>;
173
+ /** An action anyone may call. */
174
+ type PublicAction = Any & {
175
+ readonly caller: typeof Anyone;
176
+ };
177
+ /**
178
+ * The actions `T` stands for. One action is extracted rather than taken as it is, so where `T`
179
+ * is deferred, such as a list of a helper's type parameter, the actions are still actions to
180
+ * TypeScript, and a selection of them reaches a surface.
181
+ */
182
+ type ActionsOf<T extends Target> = T extends ReadonlyArray<Any> ? T[number] : Extract<T, Any>;
183
+ /** The names of the actions `T` stands for. */
184
+ type NamesOf<T extends Target> = ActionsOf<T>["name"];
185
+ /**
186
+ * What a list takes instead of one handler: nothing. A named alias carrying `R`, and it must
187
+ * stay one: while a builder's inner call, such as `Effect.gen`, is inferred, TypeScript keeps
188
+ * an alias's arguments marked as not yet inferred, where `never` or this type written inline
189
+ * becomes a candidate, and the builder's record loses its handlers' parameter types.
190
+ */
191
+ type NoHandler<R> = {
192
+ readonly "~list": R;
193
+ };
194
+ /**
195
+ * One action's handler, owing `R` per request; a list takes none. It is `Handler` written out, as
196
+ * `Authorizer`'s are. While `implement` infers, an alias whose argument `R` is not yet
197
+ * inferred is marked, as a whole, as not inferrable (the mark `NoHandler` relies on), so an
198
+ * `Effect.fn(...)` a builder returns would infer nothing from it and take an `any` input.
199
+ */
200
+ type Single<T extends Target, R> = T extends Any ? (input: T["input"]["Type"]) => Effect.Effect<T["success"]["Type"], T["error"][number]["Type"] | BuiltIn, R> : NoHandler<R>;
201
+ /**
202
+ * A record of handlers, one per key of `RequestServices`, each typed from its own action and
203
+ * owing its entry of `RequestServices` per request. TypeScript infers `RequestServices` from the
204
+ * record, key by key, so each handler, `Effect.fn` included, is typed from its contract; a key
205
+ * no action names takes nothing.
206
+ */
207
+ type Several<T extends Target, RequestServices> = { readonly [K in keyof RequestServices]: K extends NamesOf<T> ? Handler<Extract<ActionsOf<T>, {
208
+ readonly name: K;
209
+ }>, RequestServices[K]> : never; };
210
+ /** What `implement` binds to `T`: one action's handler, or a list's record. */
211
+ type HandlersOf<T extends Target, R, RequestServices> = Single<T, R> | Several<T, RequestServices>;
212
+ /**
213
+ * `T`, never inferred from where it stands: an index TypeScript cannot read until `T` is
214
+ * known, as `NoInfer` would be, but read as `T` itself once it is. So an implementation
215
+ * written inside a surface's arguments takes nothing from what that surface accepts, and a
216
+ * union of services is one union, which `Layer.provide` discharges a member at a time.
217
+ */
218
+ type Deferred<T> = [T][T extends unknown ? 0 : never];
219
+ /**
220
+ * An authorizer, or an Effect that builds it, as a builder builds handlers: `EAX` and `RAX` are
221
+ * startup failures and services, `RA` what the authorizer reads per request. Each authorizer is
222
+ * `Authorize` written out. TypeScript would infer `RA` from only one branch of a conditional
223
+ * authorizer whose other branch is typed `Authorize`, such as
224
+ * `enabled ? authorize : Action.allowAll`. While `implement` infers, an alias whose argument `RA`
225
+ * is not yet inferred is marked, as a whole, as not inferrable, so an `Effect.fn(...)` the
226
+ * Effect returns would infer nothing from it and take an `any` action.
227
+ */
228
+ type Authorizer<A extends Any, RA, EAX, RAX> = ((action: A) => Effect.Effect<void, Refusal, RA>) | Effect.Effect<(action: A) => Effect.Effect<void, Refusal, RA>, EAX, RAX>;
229
+ /**
230
+ * What `implement` returns for `T`: its handlers' per-request requirements keyed by action
231
+ * name, the authorizer's, and the startup failures and services of the builder and the
232
+ * authorizer.
233
+ */
234
+ type Implemented<T extends Target, RequestServices, R, EX, RX, RA, EAX, RAX> = Implementation<ActionsOf<T>, { readonly [K in NamesOf<T>]: Exclude<T extends ReadonlyArray<Any> ? RequestServices[K & keyof RequestServices] : R, Scope.Scope>; }, Deferred<EX>, Deferred<Exclude<RX, Scope.Scope>>, Exclude<RA, Scope.Scope>, Deferred<EAX>, Deferred<Exclude<RAX, Scope.Scope>>>;
235
+ /**
236
+ * Every authenticated caller may call a protected action. The contract's identity
237
+ * requirement is enforced independently, including when its handler reads no identity.
238
+ */
239
+ export declare const allowAll: Authorize<Any>;
240
+ /**
241
+ * Bind handlers to contracts, behind authorization. Pass one action and its handler, or a list of
242
+ * actions and a record of handlers keyed by action name. Either may instead be an Effect
243
+ * that builds them: its services are startup requirements, resolved once per layer graph
244
+ * however many surfaces serve the result, while services a handler yields are per-request
245
+ * requirements.
246
+ *
247
+ * `authorize` is required when the target includes protected contracts and runs only for
248
+ * them. `Action.allowAll` permits every authenticated caller. An Effect may build the
249
+ * authorizer once, with startup requirements separate from its per-call requirements.
250
+ */
251
+ export declare function implement<const T extends Target, RequestServices extends (T extends ReadonlyArray<Any> ? { readonly [K in NamesOf<T>]: unknown; } : never), R = never, EX = never, RX = never, RA = never, EAX = never, RAX = never>(actions: T, handlers: HandlersOf<T, R, RequestServices> | Effect.Effect<HandlersOf<T, R, RequestServices>, EX, RX>, options: {
252
+ readonly authorize: [Protected<ActionsOf<T>>] extends [never] ? never : Authorizer<Protected<ActionsOf<T>>, RA, EAX, RAX>;
253
+ }): Implemented<T, RequestServices, R, EX, RX, RA, EAX, RAX>;
254
+ export declare function implement<const T extends ProtectedActionsTakeAuthorize, RequestServices extends (T extends ReadonlyArray<Any> ? { readonly [K in NamesOf<T>]: unknown; } : never), R = never, EX = never, RX = never, RA = never, EAX = never, RAX = never>(actions: T, handlers: HandlersOf<T, R, RequestServices> | Effect.Effect<HandlersOf<T, R, RequestServices>, EX, RX>, options?: {
255
+ readonly authorize?: never;
256
+ }): Implemented<T, RequestServices, R, EX, RX, RA, EAX, RAX>;
257
+ export declare function implement<const T extends Target, RequestServices extends (T extends ReadonlyArray<Any> ? { readonly [K in NamesOf<T>]: unknown; } : never), R = never, EX = never, RX = never, RA = never, EAX = never, RAX = never>(actions: T, handlers: HandlersOf<T, R, RequestServices> | Effect.Effect<HandlersOf<T, R, RequestServices>, EX, RX>, options: {
258
+ readonly authorize: [Protected<ActionsOf<T>>] extends [never] ? "A public-only target takes no authorize" : Authorizer<Protected<ActionsOf<T>>, RA, EAX, RAX>;
259
+ }): Implemented<T, RequestServices, R, EX, RX, RA, EAX, RAX>;
260
+ /**
261
+ * The builders of `implementations`, as one layer providing nothing. Surfaces build them
262
+ * themselves, once per layer graph, but routes under `HttpRouter.serve` or `Testing.layer`
263
+ * are built in a graph of their own: provided above both, it runs each builder once for every
264
+ * surface, with the services it is given.
265
+ */
266
+ export declare function layer<const Apps extends Served>(implementations: Apps): Layer.Layer<never, BuildError<Member<Apps>>, BuildServices<Member<Apps>>>;
267
+ /**
268
+ * One call of `A` in process: its decoded success, or its declared errors and the built-in
269
+ * ones, owing `R` per call, what its handler and its implementation's `authorize` read.
270
+ */
271
+ type Method<A extends Any, R> = Call<A, Effect.Effect<A["success"]["Type"], A["error"][number]["Type"] | BuiltIn, R>>;
272
+ /**
273
+ * Every action of the implementations `Apps`, one or a list, as `client.<action>(input)`: what
274
+ * `client` gives, each method owing per call what its handler and `authorize` read.
275
+ */
276
+ export type Client<Apps extends Served, Listed extends Any = Extract<ActionOf<Member<Apps>>, Any>> = { readonly [A in Serving<Member<Apps>, Listed> as A["name"]]: Method<A, RequestOf<Member<Apps>, A>>; };
277
+ /** What `client` takes: the actions to call. */
278
+ export interface ClientOptions<A extends Any> {
279
+ /**
280
+ * The actions to call, among the implementations': `[GetUser]`. Defaults to every action of
281
+ * them. Options whose `actions` may be absent are typed as every action, as they may run.
282
+ */
283
+ readonly actions?: ReadonlyArray<A> | undefined;
284
+ }
285
+ /**
286
+ * Call implementations in process: one method per action, taking its input directly, as
287
+ * `ActionHttp.client`'s methods do, `client.renameUser({ id, name })`, so moving between the
288
+ * two changes the line acquiring it. A call runs as a remote one does: its input passes through
289
+ * its JSON codec, encoded then decoded, then the implementation's `authorize` and the handler
290
+ * run, in a scope of their own, and the success or the failure passes through its
291
+ * codec. It fails with the action's errors and the built-in ones, and owes, per call, what
292
+ * they read, the caller's identity included; the caller provides it around the call.
293
+ *
294
+ * Acquiring it builds the implementations' builders, as a layer does, into the layer graph it
295
+ * is acquired in, and its scope holds them: in a builder, it shares their one run with every
296
+ * surface of that graph. Acquire it where builders live, in a builder, a layer or a scoped
297
+ * program, never per request.
298
+ */
299
+ export declare function client<const Apps extends Served, const O extends ClientOptions<ActionOf<Member<Apps>>> = {}>(implementations: Apps, ...options: OptionalUnless<O, ClientOptions<ActionOf<Member<Apps>>> & O & NoInfer<Known<O, ClientOptions<Any>>>>): Effect.Effect<Client<Apps, Offered<O, ActionOf<Member<Apps>>>>, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Scope.Scope>;
94
300
  //#endregion
95
- export { Action_d_exports };
301
+ export { Action_d_exports, type AnyImplementation, Anyone, type Authorize, type BuildError, type BuildServices, BuiltIn, Forbidden, type Implementation, InvalidInput, type Issue, Refusal, Unauthenticated };