@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.
- 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 +7 -15
- 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/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
|
|
5
|
-
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
18
|
+
input: { name: Schema.String },
|
|
19
19
|
success: Schema.String,
|
|
20
|
-
|
|
20
|
+
readOnly: true,
|
|
21
|
+
caller: Action.Anyone,
|
|
21
22
|
});
|
|
22
23
|
|
|
23
|
-
export const
|
|
24
|
+
export const Http = ActionHttp.make([Greet]);
|
|
25
|
+
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
and a server that implements it, in a module of its own:
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
ActionMcp.layerHttp(
|
|
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
|
|
42
|
-
|
|
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(
|
|
46
|
-
const cli = ActionCli.
|
|
47
|
-
const remote =
|
|
48
|
-
const
|
|
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
|
|
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*
|
|
61
|
+
const client = yield* ActionHttp.client(Http, { baseUrl });
|
|
56
62
|
|
|
57
|
-
return yield* client.
|
|
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
|
|
67
|
-
- A handler
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
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
|
|
81
|
+
pnpm add @gjermundgaraba/effect-actions effect@~4.0.0
|
|
77
82
|
```
|
|
78
83
|
|
|
79
|
-
Add `@effect/platform-node
|
|
80
|
-
(`.../Action`, `.../ActionHttp`, ...); there is no package root, so a
|
|
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
|
|
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
|
|
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
|
|
100
|
-
in
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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?:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
*
|
|
51
|
-
* is the one a reviewer must check.
|
|
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
|
|
55
|
-
/**
|
|
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
|
-
/**
|
|
59
|
-
|
|
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:
|
|
64
|
-
|
|
65
|
-
readonly
|
|
66
|
-
readonly
|
|
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
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
type
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 };
|