@gjermundgaraba/effect-actions 0.11.0 → 0.13.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 +102 -41
- package/dist/Action.d.mts +31 -117
- package/dist/Action.mjs +14 -53
- package/dist/ActionCli.d.mts +60 -28
- package/dist/ActionCli.mjs +54 -48
- package/dist/ActionHttp.d.mts +18 -67
- package/dist/ActionHttp.mjs +20 -70
- package/dist/ActionMcp.d.mts +23 -29
- package/dist/ActionMcp.mjs +56 -114
- package/dist/ActionRpc.d.mts +147 -0
- package/dist/ActionRpc.mjs +113 -0
- package/dist/ActionToolkit.d.mts +28 -9
- package/dist/ActionToolkit.mjs +31 -4
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +30 -23
- package/dist/Testing.mjs +27 -44
- package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
- package/dist/authentication/provider.mjs +12 -0
- package/dist/authentication/refusal.mjs +36 -0
- package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
- package/dist/cli/commands.mjs +302 -0
- package/dist/{internal → contract}/call.d.mts +2 -2
- package/dist/contract/call.mjs +10 -0
- package/dist/{internal → contract}/errors.d.mts +13 -10
- package/dist/{internal → contract}/errors.mjs +9 -14
- package/dist/{internal → contract}/implementation.d.mts +37 -21
- package/dist/{internal → contract}/implementation.mjs +18 -49
- package/dist/contract/media.d.mts +13 -0
- package/dist/contract/media.mjs +18 -0
- package/dist/contract/rules.d.mts +25 -0
- package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
- package/dist/{internal → http}/client.d.mts +7 -9
- package/dist/http/client.mjs +17 -0
- package/dist/http/declared.mjs +20 -0
- package/dist/http/schema-errors.mjs +9 -0
- package/dist/mcp/protocol.d.mts +18 -0
- package/dist/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
- package/dist/stdio/console.mjs +64 -0
- package/dist/{internal → testing}/memory.d.mts +1 -1
- package/dist/{internal → testing}/memory.mjs +5 -17
- package/dist/toolkit/tools.mjs +29 -0
- package/docs/Action-failures.md +47 -0
- package/docs/Action-in-process.md +93 -0
- package/docs/Action.md +211 -426
- package/docs/ActionCli-failures.md +104 -0
- package/docs/ActionCli.md +208 -293
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +205 -282
- package/docs/ActionMcp-failures.md +31 -0
- package/docs/ActionMcp.md +222 -271
- package/docs/ActionRpc-failures.md +21 -0
- package/docs/ActionRpc.md +179 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +103 -119
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +143 -392
- package/docs/CONTEXT.md +40 -24
- package/docs/README.md +75 -35
- package/docs/Testing-failures.md +21 -0
- package/docs/Testing.md +193 -151
- package/docs/guarantees.md +35 -96
- package/docs/setup-failures.md +37 -0
- package/docs/setup.md +90 -92
- package/examples/README.md +68 -0
- package/examples/assistant/README.md +92 -0
- package/examples/assistant/src/agent.ts +92 -0
- package/examples/assistant/src/cli.ts +34 -0
- package/examples/assistant/src/commands.ts +25 -0
- package/examples/assistant/src/identity/Identity.ts +33 -0
- package/examples/assistant/src/identity/authorize.ts +24 -0
- package/examples/assistant/src/identity/handlers.ts +10 -0
- package/examples/assistant/src/identity/members.ts +9 -0
- package/examples/assistant/src/identity/permissions.ts +23 -0
- package/examples/assistant/src/scripted.ts +23 -0
- package/examples/assistant/src/tasks/Tasks.ts +63 -0
- package/examples/assistant/src/tasks/handlers.ts +25 -0
- package/examples/assistant/src/tasks/store.ts +96 -0
- package/examples/assistant/src/toolkit.ts +30 -0
- package/examples/assistant/test/commands.test.ts +57 -0
- package/examples/assistant/test/identity/identity.test.ts +43 -0
- package/examples/assistant/test/tasks/store.test.ts +48 -0
- package/examples/assistant/test/tasks/tasks.test.ts +75 -0
- package/examples/assistant/test/toolkit.test.ts +121 -0
- package/examples/kitchen-sink/README.md +318 -0
- package/examples/kitchen-sink/src/Actions.ts +19 -0
- package/examples/kitchen-sink/src/Api.ts +24 -0
- package/examples/kitchen-sink/src/Rpc.ts +12 -0
- package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
- package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
- package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
- package/examples/kitchen-sink/src/admin/grants.ts +22 -0
- package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
- package/examples/kitchen-sink/src/admin/keys.ts +8 -0
- package/examples/kitchen-sink/src/agent.ts +47 -0
- package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
- package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
- package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
- package/examples/kitchen-sink/src/assistant/handlers.ts +126 -0
- package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
- package/examples/kitchen-sink/src/attachments/download.ts +49 -0
- package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
- package/examples/kitchen-sink/src/attachments/store.ts +58 -0
- package/examples/kitchen-sink/src/browser.ts +33 -0
- package/examples/kitchen-sink/src/cli.ts +85 -0
- package/examples/kitchen-sink/src/config.ts +18 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
- package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
- package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
- package/examples/kitchen-sink/src/identity/directory.ts +52 -0
- package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
- package/examples/kitchen-sink/src/identity/members.ts +13 -0
- package/examples/kitchen-sink/src/identity/quota.ts +62 -0
- package/examples/kitchen-sink/src/issues/Issues.ts +111 -0
- package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
- package/examples/kitchen-sink/src/issues/guide.ts +37 -0
- package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
- package/examples/kitchen-sink/src/issues/store.ts +96 -0
- package/examples/kitchen-sink/src/issues/write-limit.ts +42 -0
- package/examples/kitchen-sink/src/memory.ts +30 -0
- package/examples/kitchen-sink/src/ops.ts +42 -0
- package/examples/kitchen-sink/src/request-policy.ts +63 -0
- package/examples/kitchen-sink/src/routes.ts +120 -0
- package/examples/kitchen-sink/src/sdk.ts +46 -0
- package/examples/kitchen-sink/src/seed.ts +65 -0
- package/examples/kitchen-sink/src/server.ts +62 -0
- package/examples/kitchen-sink/src/stdio.ts +34 -0
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
- package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
- package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
- package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
- package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
- package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
- package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
- package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
- package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
- package/examples/kitchen-sink/test/routes.test.ts +123 -0
- package/examples/kitchen-sink/test/support/scripted.ts +40 -0
- package/examples/live-rpc/README.md +107 -0
- package/examples/live-rpc/src/Rpc.ts +8 -0
- package/examples/live-rpc/src/browser.ts +98 -0
- package/examples/live-rpc/src/chat/Chat.ts +77 -0
- package/examples/live-rpc/src/chat/authorize.ts +16 -0
- package/examples/live-rpc/src/chat/handlers.ts +50 -0
- package/examples/live-rpc/src/chat/room.ts +23 -0
- package/examples/live-rpc/src/chat/store.ts +54 -0
- package/examples/live-rpc/src/client.ts +37 -0
- package/examples/live-rpc/src/identity/Identity.ts +19 -0
- package/examples/live-rpc/src/identity/members.ts +8 -0
- package/examples/live-rpc/src/identity/verifier.ts +23 -0
- package/examples/live-rpc/src/routes.ts +22 -0
- package/examples/live-rpc/src/server.ts +43 -0
- package/examples/live-rpc/test/browser.test.ts +105 -0
- package/examples/live-rpc/test/chat/chat.test.ts +69 -0
- package/examples/live-rpc/test/routes.test.ts +99 -0
- package/examples/live-rpc/test/server.test.ts +75 -0
- package/examples/mcp-tools/README.md +141 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +121 -0
- package/examples/mcp-tools/src/printers/fake.ts +115 -0
- package/examples/mcp-tools/src/printers/farm.ts +136 -0
- package/examples/mcp-tools/src/printers/handlers.ts +29 -0
- package/examples/mcp-tools/src/routes.ts +52 -0
- package/examples/mcp-tools/src/server.ts +31 -0
- package/examples/mcp-tools/src/stdio.ts +26 -0
- package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
- package/examples/mcp-tools/test/routes.test.ts +202 -0
- package/examples/notes-service/README.md +111 -0
- package/examples/notes-service/src/Api.ts +13 -0
- package/examples/notes-service/src/browser.ts +41 -0
- package/examples/notes-service/src/cli.ts +22 -0
- package/examples/notes-service/src/commands.ts +137 -0
- package/examples/notes-service/src/identity/Identity.ts +31 -0
- package/examples/notes-service/src/identity/authorize.ts +18 -0
- package/examples/notes-service/src/identity/handlers.ts +10 -0
- package/examples/notes-service/src/identity/users.ts +9 -0
- package/examples/notes-service/src/identity/verifier.ts +36 -0
- package/examples/notes-service/src/notes/Notes.ts +119 -0
- package/examples/notes-service/src/notes/handlers.ts +39 -0
- package/examples/notes-service/src/notes/store.ts +84 -0
- package/examples/notes-service/src/routes.ts +45 -0
- package/examples/notes-service/src/server.ts +26 -0
- package/examples/notes-service/src/status/Status.ts +10 -0
- package/examples/notes-service/src/status/handlers.ts +14 -0
- package/examples/notes-service/src/stdio.ts +25 -0
- package/examples/notes-service/test/commands.test.ts +288 -0
- package/examples/notes-service/test/notes/notes.test.ts +103 -0
- package/examples/notes-service/test/routes.test.ts +84 -0
- package/examples/quickstart/README.md +31 -0
- package/examples/quickstart/src/Api.ts +5 -0
- package/examples/quickstart/src/client.ts +13 -0
- package/examples/quickstart/src/greeting/Greeting.ts +12 -0
- package/examples/quickstart/src/greeting/handlers.ts +6 -0
- package/examples/quickstart/src/routes.ts +11 -0
- package/examples/quickstart/src/server.ts +16 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
- package/package.json +27 -9
- package/dist/internal/actions.d.mts +0 -9
- package/dist/internal/authentication.mjs +0 -11
- package/dist/internal/call.mjs +0 -12
- package/dist/internal/cli.mjs +0 -328
- package/dist/internal/client.mjs +0 -25
- package/dist/internal/console.mjs +0 -77
- package/dist/internal/declared.d.mts +0 -13
- package/dist/internal/declared.mjs +0 -34
- package/dist/internal/mcp.d.mts +0 -14
- package/dist/internal/media.d.mts +0 -8
- package/dist/internal/media.mjs +0 -26
- package/dist/internal/refusal.mjs +0 -67
- package/dist/internal/schema-errors.mjs +0 -20
- package/dist/internal/tools.mjs +0 -44
package/README.md
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
# effect-actions
|
|
2
2
|
|
|
3
|
-
Define an Effect action once. Serve it over HTTP and
|
|
4
|
-
Toolkit, and run it from a CLI. Same schemas, same handler, Effect's own servers and clients
|
|
3
|
+
Define an Effect action once. Serve it over HTTP, MCP and Effect RPC, hand it to a model as a
|
|
4
|
+
native Toolkit, and run it from a CLI. Same schemas, same handler, Effect's own servers and clients
|
|
5
5
|
underneath.
|
|
6
6
|
|
|
7
|
+
**If you are a coding agent**, start at [docs/README.md](docs/README.md), not here, and read
|
|
8
|
+
one page per tool call, again alone if its output says it was truncated. It routes every task,
|
|
9
|
+
protecting an action and limiting request bodies included, to its page and its example.
|
|
10
|
+
|
|
7
11
|
## Looks like this
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
A contract says what an action takes and answers, and who may call it:
|
|
14
|
+
|
|
15
|
+
```ts example=quickstart/src/greeting/Greeting.ts
|
|
10
16
|
import { Schema } from "effect";
|
|
11
17
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
12
|
-
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
13
18
|
|
|
14
|
-
//
|
|
15
|
-
// a browser page included.
|
|
19
|
+
// A contract: what the action takes and answers, whether it changes anything, and who may
|
|
20
|
+
// call it. It imports no server code, so any client can import it, a browser page included.
|
|
16
21
|
export const Greet = Action.make("greet", {
|
|
17
22
|
description: "Greet someone by name.",
|
|
18
23
|
input: { name: Schema.String },
|
|
@@ -20,38 +25,56 @@ export const Greet = Action.make("greet", {
|
|
|
20
25
|
readOnly: true,
|
|
21
26
|
caller: Action.Anyone,
|
|
22
27
|
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
An implementation binds it to its handler:
|
|
31
|
+
|
|
32
|
+
```ts example=quickstart/src/greeting/handlers.ts
|
|
33
|
+
import { Effect } from "effect";
|
|
34
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
35
|
+
import { Greet } from "./Greeting.js";
|
|
23
36
|
|
|
37
|
+
// The implementation: the contract bound to its handler. Every surface takes this one value.
|
|
38
|
+
export const greeting = Action.implement(Greet, ({ name }) => Effect.succeed(`Hello, ${name}!`));
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A binding gives it a route, for the server to serve and a client to call:
|
|
42
|
+
|
|
43
|
+
```ts example=quickstart/src/Api.ts
|
|
44
|
+
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
45
|
+
import { Greet } from "./greeting/Greeting.js";
|
|
46
|
+
|
|
47
|
+
// The HTTP binding: `POST /api/greet`. The server serves it and the client calls it.
|
|
24
48
|
export const Http = ActionHttp.make([Greet]);
|
|
25
49
|
```
|
|
26
50
|
|
|
27
|
-
|
|
51
|
+
And the same implementation is served on every surface the host wants:
|
|
28
52
|
|
|
29
|
-
```ts example=quickstart
|
|
30
|
-
import {
|
|
31
|
-
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
53
|
+
```ts example=quickstart/src/routes.ts
|
|
54
|
+
import { Layer } from "effect";
|
|
32
55
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
33
56
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
34
|
-
import {
|
|
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}!`));
|
|
57
|
+
import { Http } from "./Api.js";
|
|
58
|
+
import { greeting } from "./greeting/handlers.js";
|
|
38
59
|
|
|
60
|
+
// One implementation, two surfaces: a JSON route and an MCP tool at `/mcp`.
|
|
39
61
|
export const routes = Layer.mergeAll(
|
|
40
|
-
ActionHttp.layer(Http,
|
|
41
|
-
ActionMcp.layerHttp(
|
|
62
|
+
ActionHttp.layer(Http, greeting),
|
|
63
|
+
ActionMcp.layerHttp(greeting, { name: "greetings", version: "1.0.0" }),
|
|
42
64
|
);
|
|
43
65
|
```
|
|
44
66
|
|
|
45
|
-
Serve `routes` with Effect's `HttpRouter.serve` and a platform server,
|
|
46
|
-
|
|
47
|
-
|
|
67
|
+
Serve `routes` with Effect's `HttpRouter.serve` and a platform server, as
|
|
68
|
+
[examples/quickstart/src/server.ts](examples/quickstart/src/server.ts) does, and you have
|
|
69
|
+
`POST /api/greet` and an MCP tool named `greet` at `/mcp`. The same `greeting`
|
|
48
70
|
implementation is also:
|
|
49
71
|
|
|
50
72
|
```ts
|
|
51
|
-
const { toolkit, layer } = ActionToolkit.make(
|
|
52
|
-
const cli = ActionCli.make(
|
|
73
|
+
const { toolkit, layer } = ActionToolkit.make(greeting); // native Effect AI Toolkit and its handler layer
|
|
74
|
+
const cli = ActionCli.make(greeting, { name: "greetings" }); // greetings greet --name Ada
|
|
53
75
|
const remote = ActionCli.remoteCommand(Http, Greet, { client: { baseUrl } }); // greet --name Ada, over HTTP
|
|
54
|
-
const local = Action.client(
|
|
76
|
+
const local = Action.client(greeting); // the HTTP client's methods, in process
|
|
77
|
+
const rpc = ActionRpc.layer(ActionRpc.make([Greet]), greeting); // Effect RPC, on the protocol the host provides
|
|
55
78
|
```
|
|
56
79
|
|
|
57
80
|
And the client is Effect's own `HttpApiClient`, typed from the same contract, one method per action:
|
|
@@ -67,49 +90,86 @@ const greeting = Effect.gen(function* () {
|
|
|
67
90
|
## Why
|
|
68
91
|
|
|
69
92
|
- 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.
|
|
71
|
-
- Every contract states who may call it: `caller: Action.Anyone`, or the identity service a caller must have. A surface
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
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.
|
|
93
|
+
- A handler can fail only with the errors its action declares and three built-in ones (400, 401, 403), and every client decodes them as typed values.
|
|
94
|
+
- Every contract states who may call it: `caller: Action.Anyone`, or the identity service a caller must have. A surface authenticates a protected action's request before decoding its input, with the provider the application builds from its own token verifier.
|
|
95
|
+
- Authorization is written once, on the implementation: `Action.implement(actions, handlers, { authorize })`, required for protected actions. Every surface runs it before each handler, and the rule reads the contract's `readOnly` instead of a hand-maintained list of mutation names.
|
|
96
|
+
- Public and protected routes share one binding, one OpenAPI document and one client, and a handler's startup services are built once per [layer graph](docs/Action.md#implement-actions), however many surfaces serve it.
|
|
97
|
+
- The pieces are Effect's own: a native `HttpApi`, `McpServer`, `RpcServer` and `RpcClient`, with no SDK runtime dependency, so OpenAPI, Swagger, Scalar and `HttpApiClient` work unchanged.
|
|
98
|
+
- Tests run in memory: `Action.client` calls an implementation in process, and `Testing.layer(routes)` serves the routes and tools to typed clients without opening a port.
|
|
77
99
|
|
|
78
100
|
## Install
|
|
79
101
|
|
|
80
102
|
```sh
|
|
81
|
-
pnpm add @gjermundgaraba/effect-actions effect@~4.0.
|
|
103
|
+
pnpm add @gjermundgaraba/effect-actions effect@~4.0.3
|
|
82
104
|
```
|
|
83
105
|
|
|
84
|
-
|
|
106
|
+
It needs TypeScript 7 or newer and is ESM only
|
|
107
|
+
([docs/setup.md](docs/setup.md#install)). Add `@effect/platform-node@~4.0.3` to serve or run a CLI from Node. Every module is a
|
|
85
108
|
subpath import (`.../Action`, `.../ActionHttp`, ...); there is no package root, so a
|
|
86
109
|
contracts-only bundle never loads a server.
|
|
87
110
|
|
|
111
|
+
## Examples
|
|
112
|
+
|
|
113
|
+
[examples/](examples/README.md) holds complete applications, each a directory you can run, with
|
|
114
|
+
its own tests, laid out the way we recommend for an application: a folder per domain, the
|
|
115
|
+
contract in one file a browser can import, one entrypoint per way of running it. They are
|
|
116
|
+
type-checked and tested with the library and ship in the package.
|
|
117
|
+
|
|
118
|
+
- [quickstart](examples/quickstart/): the program above.
|
|
119
|
+
- [notes-service](examples/notes-service/): a service with bearer authentication, a JSON API
|
|
120
|
+
and MCP tools from one implementation, a CLI and a browser client. Most applications are this.
|
|
121
|
+
- [mcp-tools](examples/mcp-tools/): an MCP server whose tools wrap an upstream HTTP API.
|
|
122
|
+
- [assistant](examples/assistant/): no server, the same implementations as a model's tools
|
|
123
|
+
and as local commands.
|
|
124
|
+
- [live-rpc](examples/live-rpc/): Effect RPC over a WebSocket.
|
|
125
|
+
- [kitchen-sink](examples/kitchen-sink/): every module and nearly every option in one
|
|
126
|
+
application, with an index from each to the file that shows it.
|
|
127
|
+
|
|
88
128
|
## Docs
|
|
89
129
|
|
|
90
|
-
|
|
91
|
-
|
|
130
|
+
[docs/](docs/README.md) is written for coding agents: one short page per module, organized by
|
|
131
|
+
task, each linking to the example files that show the task working.
|
|
92
132
|
|
|
93
|
-
- [docs/README.md](docs/README.md): start here
|
|
94
|
-
|
|
133
|
+
- [docs/README.md](docs/README.md): start here: which page answers which task, and which
|
|
134
|
+
surface to use for which caller.
|
|
135
|
+
- [docs/guarantees.md](docs/guarantees.md): what each case looks like on the wire, on every
|
|
136
|
+
surface, and what the library leaves to the application.
|
|
95
137
|
- [docs/CONTEXT.md](docs/CONTEXT.md): the vocabulary the docs and the code use.
|
|
96
|
-
- [examples/](examples/README.md): a runnable authenticated application with public and authenticated actions, an MCP endpoint, OpenAPI, and every other surface.
|
|
97
138
|
|
|
98
139
|
## For agents
|
|
99
140
|
|
|
141
|
+
The `effect-actions` skill puts the docs in front of a coding agent. With the
|
|
142
|
+
[skills CLI](https://github.com/vercel-labs/skills), for any agent it supports:
|
|
143
|
+
|
|
100
144
|
```sh
|
|
101
145
|
npx skills add gjermundgaraba/effect-actions --skill effect-actions
|
|
102
146
|
```
|
|
103
147
|
|
|
148
|
+
As a Claude Code plugin:
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
/plugin marketplace add gjermundgaraba/effect-actions
|
|
152
|
+
/plugin install effect-actions@effect-actions-marketplace
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
As a Codex plugin:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
codex plugin marketplace add gjermundgaraba/effect-actions
|
|
159
|
+
codex plugin add effect-actions@effect-actions-marketplace
|
|
160
|
+
```
|
|
161
|
+
|
|
104
162
|
The skill is generated from `docs/` on the main branch, which can be ahead of the latest
|
|
105
|
-
release
|
|
106
|
-
`node_modules/@gjermundgaraba/effect-actions/
|
|
163
|
+
release. Its links to the examples are paths into the installed package,
|
|
164
|
+
`node_modules/@gjermundgaraba/effect-actions/examples`, which an agent reads without a network;
|
|
165
|
+
the pages of the installed version are beside them, in
|
|
166
|
+
`node_modules/@gjermundgaraba/effect-actions/docs`.
|
|
107
167
|
|
|
108
168
|
## Status
|
|
109
169
|
|
|
110
170
|
The `effect` peer admits the patches of the Effect release the package is tested against, since
|
|
111
|
-
the modules the surfaces build on may change in a minor release (see [docs/setup.md](docs/setup.md#install)). Actions are unary JSON over HTTP and
|
|
112
|
-
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/
|
|
171
|
+
the modules the surfaces build on may change in a minor release (see [docs/setup.md](docs/setup.md#install)). Actions are unary JSON over HTTP, MCP and Effect RPC, whose protocols speak JSON alone: no streaming or uploads.
|
|
172
|
+
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/guarantees.md](docs/guarantees.md#scope).
|
|
113
173
|
|
|
114
174
|
## Acknowledgements
|
|
115
175
|
|
|
@@ -117,4 +177,5 @@ A few ideas (like the native Toolkit and CLI projections) are inspired by the ex
|
|
|
117
177
|
|
|
118
178
|
## Contributing
|
|
119
179
|
|
|
120
|
-
|
|
180
|
+
The rules for changing the library are in
|
|
181
|
+
[AGENTS.md](https://github.com/gjermundgaraba/effect-actions/blob/main/AGENTS.md). MIT licensed.
|
package/dist/Action.d.mts
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
import { Anyone } from "./
|
|
2
|
-
import { BuiltIn, BuiltIns, Forbidden, InvalidInput, Issue, Refusal, Unauthenticated } from "./
|
|
3
|
-
import { ActionOf, AnyImplementation, Authorize, BuildError, BuildServices, Holding, Implementation, Known, Member, Offered, OptionalUnless, Protected, RequestOf, SelectedOf, Served, Serving } from "./
|
|
4
|
-
import { Call } from "./
|
|
5
|
-
import { Image } from "./
|
|
1
|
+
import { Anyone } from "./contract/rules.mjs";
|
|
2
|
+
import { BuiltIn, BuiltIns, Forbidden, InvalidInput, Issue, Refusal, Unauthenticated } from "./contract/errors.mjs";
|
|
3
|
+
import { ActionOf, AnyImplementation, Authorize, BuildError, BuildServices, Holding, Implementation, Known, Member, Offered, OptionalUnless, Protected, RequestOf, SelectedOf, Served, Serving } from "./contract/implementation.mjs";
|
|
4
|
+
import { Call } from "./contract/call.mjs";
|
|
5
|
+
import { Image } from "./contract/media.mjs";
|
|
6
6
|
import { Context, Effect, Layer, Schema, Scope } from "effect";
|
|
7
7
|
declare namespace Action_d_exports {
|
|
8
8
|
export { Action, Any, AnyImplementation, Anyone, Authorize, BuildError, BuildServices, BuiltIn, Client, ClientOptions, Errors, Forbidden, Handler, Handlers, Image, Implementation, InvalidInput, Issue, Mcp, Options, Refusal, Unauthenticated, allowAll, byName, client, implement, layer, make };
|
|
9
9
|
}
|
|
10
|
-
/** Any service-free schema. Only handlers may require services. */
|
|
11
10
|
type Codec = Schema.Codec<unknown, unknown, never, never>;
|
|
12
|
-
/** Struct fields, accepted wherever a struct schema is: `{ name: Schema.String }`. */
|
|
13
11
|
type Fields = {
|
|
14
12
|
readonly [key: string]: Codec;
|
|
15
13
|
};
|
|
16
|
-
/**
|
|
17
|
-
* An object without fields: an action without arguments, `{}` given or not. Effect's own
|
|
18
|
-
* `Tool.EmptyParams`: strict, unlike `Schema.Struct({})`, which accepts any value but `null`,
|
|
19
|
-
* and the object root MCP requires.
|
|
20
|
-
*/
|
|
21
14
|
declare const NoInput: Schema.$Record<Schema.String, Schema.Never>;
|
|
22
|
-
/** The schema a `Codec | Fields` option stands for: a schema itself, `NoInput` for no fields. */
|
|
23
15
|
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;
|
|
24
16
|
/**
|
|
25
17
|
* How the action's tool presents itself over MCP, in MCP's own names; every field is optional
|
|
@@ -30,7 +22,12 @@ type CodecOf<S extends Codec | Fields> = S extends Codec ? S : S extends Schema.
|
|
|
30
22
|
export interface Mcp {
|
|
31
23
|
/** The tool's display name, `annotations.title`, beside its name. */
|
|
32
24
|
readonly title?: string | undefined;
|
|
33
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* `annotations.destructiveHint`, meaningful for a write only. Left out, a write is listed as
|
|
27
|
+
* destructive, MCP's default: state `false` for one that only adds, MCP's "only additive
|
|
28
|
+
* updates". One that replaces, removes or interrupts, such as an edit or a restart, stays
|
|
29
|
+
* destructive.
|
|
30
|
+
*/
|
|
34
31
|
readonly destructiveHint?: boolean | undefined;
|
|
35
32
|
/** `annotations.idempotentHint`, meaningful for a write only. */
|
|
36
33
|
readonly idempotentHint?: boolean | undefined;
|
|
@@ -49,14 +46,18 @@ export interface Options {
|
|
|
49
46
|
*/
|
|
50
47
|
readonly caller: typeof Anyone | Context.Key<unknown, unknown>;
|
|
51
48
|
readonly description: string;
|
|
52
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* A schema or struct fields. Omit, or give `{}`, for an action without arguments. A field's
|
|
51
|
+
* `description` annotation is what a model reads in a tool's listing and a flag's help in a CLI.
|
|
52
|
+
*/
|
|
53
53
|
readonly input?: Codec | Fields | undefined;
|
|
54
54
|
/** A schema or struct fields. Omit for an action that returns nothing: `Schema.Void`. */
|
|
55
55
|
readonly success?: Codec | Fields | undefined;
|
|
56
56
|
/**
|
|
57
57
|
* The declared failures: one schema, or a list of them, as `HttpApiEndpoint` takes them;
|
|
58
|
-
* each keeps its own HTTP status annotation
|
|
59
|
-
*
|
|
58
|
+
* each keeps its own HTTP status annotation, the third argument of
|
|
59
|
+
* `Schema.TaggedError<NoteNotFound>()("NoteNotFound", { id: Schema.String }, { httpApiStatus: 404 })`,
|
|
60
|
+
* and answers 422 without one. Defaults to none. None with a built-in error's `_tag`.
|
|
60
61
|
*/
|
|
61
62
|
readonly error?: Codec | ReadonlyArray<Codec> | undefined;
|
|
62
63
|
/**
|
|
@@ -68,38 +69,19 @@ export interface Options {
|
|
|
68
69
|
/** How the action's tool presents itself over MCP, and on a native Toolkit's tools. */
|
|
69
70
|
readonly mcp?: Mcp;
|
|
70
71
|
}
|
|
71
|
-
/**
|
|
72
|
-
* The keys beyond `Mcp` in the `mcp` of any member of `O`, given or optional, so a misspelled
|
|
73
|
-
* one in a conditional spread or in one branch of a ternary is refused too.
|
|
74
|
-
*/
|
|
75
72
|
type UnknownMcp<O> = O extends {
|
|
76
73
|
readonly mcp?: infer H;
|
|
77
74
|
} ? Exclude<H extends unknown ? keyof H : never, keyof Mcp> : never;
|
|
78
|
-
/** No key beyond `Mcp`, whichever member of `O` gives it. */
|
|
79
75
|
type KnownMcp<O> = {
|
|
80
76
|
readonly mcp?: { readonly [K in UnknownMcp<O>]: never; };
|
|
81
77
|
};
|
|
82
|
-
/** The schemas an `error` option of type `G` declares: one, or each of a list. */
|
|
83
78
|
type ErrorMembers<G> = G extends ReadonlyArray<infer E> ? E : G;
|
|
84
|
-
/**
|
|
85
|
-
* The built-in errors, refused in `error`: every surface declares them already. The types
|
|
86
|
-
* refuse only the built-ins themselves; `make` refuses, when called, an error of your own
|
|
87
|
-
* that encodes with a built-in `_tag`.
|
|
88
|
-
*/
|
|
89
79
|
type OwnErrors<O> = O extends {
|
|
90
80
|
readonly error: infer G;
|
|
91
81
|
} ? [Extract<ErrorMembers<G>, BuiltIns>] extends [never] ? unknown : {
|
|
92
82
|
readonly error: Exclude<ErrorMembers<G>, BuiltIns> | ReadonlyArray<Exclude<ErrorMembers<G>, BuiltIns>>;
|
|
93
83
|
} : unknown;
|
|
94
|
-
/**
|
|
95
|
-
* No option beyond `Options` in any member of `O`, so a misspelled one is an unknown
|
|
96
|
-
* property, not ignored: `keyof` a union holds only the keys every member has.
|
|
97
|
-
*/
|
|
98
84
|
type OnlyOptions<O> = { readonly [K in Exclude<O extends unknown ? keyof O : never, keyof Options>]: never; };
|
|
99
|
-
/**
|
|
100
|
-
* `caller` is a `Context.Service`: a `Context.Reference` is never missing, so its default
|
|
101
|
-
* would stand in for every caller who supplies none.
|
|
102
|
-
*/
|
|
103
85
|
type ServiceCaller<O> = O extends {
|
|
104
86
|
readonly caller: {
|
|
105
87
|
readonly defaultValue: unknown;
|
|
@@ -107,27 +89,19 @@ type ServiceCaller<O> = O extends {
|
|
|
107
89
|
} ? {
|
|
108
90
|
readonly caller: "An identity is a Context.Service, not a Context.Reference";
|
|
109
91
|
} : unknown;
|
|
110
|
-
/**
|
|
111
|
-
* The rules `make` checks beyond `Options`: every option and `mcp` key known, and no built-in
|
|
112
|
-
* error listed. Options that fail `Options` itself infer as `Options`, whose error the
|
|
113
|
-
* compiler already reports, so they are not checked again. Checked after inference: `make`'s
|
|
114
|
-
* options are `O & NoInfer<Rules<O>>`.
|
|
115
|
-
*/
|
|
116
92
|
type Rules<O> = Options extends O ? unknown : OnlyOptions<O> & ServiceCaller<O> & OwnErrors<O> & KnownMcp<O>;
|
|
117
|
-
/**
|
|
118
|
-
* Option `K` of `O` as given, or also `Default` wherever it may be omitted or undefined, as
|
|
119
|
-
* at run time, for each member of a union of options.
|
|
120
|
-
*/
|
|
121
93
|
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;
|
|
122
|
-
/** The schema option `K` of `O`, or `Default` when it is omitted. */
|
|
123
94
|
type SchemaOf<O, K extends "input" | "success", Default extends Codec> = CodecOf<Extract<OptionOf<O, K, Default>, Codec | Fields>>;
|
|
124
|
-
|
|
125
|
-
type ErrorsOf<O> = ReadonlyArray<ErrorMembers<Extract<OptionOf<O, "error", []>, Codec | ReadonlyArray<Codec>>>>;
|
|
95
|
+
type ErrorSchemasOf<O> = ReadonlyArray<ErrorMembers<Extract<OptionOf<O, "error", []>, Codec | ReadonlyArray<Codec>>>>;
|
|
126
96
|
/** A pure contract: schemas and transport metadata. Handlers are bound by `implement`. */
|
|
127
97
|
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"]> {
|
|
128
98
|
readonly caller: Caller;
|
|
129
99
|
readonly name: Name;
|
|
130
100
|
readonly description: string;
|
|
101
|
+
/**
|
|
102
|
+
* The input as a schema, a `Schema.Struct` where fields were given: reuse it as it is, as in
|
|
103
|
+
* `Schema.Array(action.input)`.
|
|
104
|
+
*/
|
|
131
105
|
readonly input: Input;
|
|
132
106
|
readonly success: Success;
|
|
133
107
|
/** The declared failures, as a list: one given alone is a list of one. */
|
|
@@ -154,8 +128,9 @@ export type Handlers<A extends ReadonlyArray<Any>, R = never> = { readonly [Acti
|
|
|
154
128
|
/**
|
|
155
129
|
* Define an action contract. Names are `[A-Za-z0-9_-]{1,128}`, other than `then`: the name
|
|
156
130
|
* is also the route segment, the client method and the tool name.
|
|
131
|
+
* Guide: `docs/Action.md` in this package; in an application: `examples/notes-service/src/notes/`.
|
|
157
132
|
*/
|
|
158
|
-
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>,
|
|
133
|
+
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>, ErrorSchemasOf<O>, O["readOnly"], O["caller"]>;
|
|
159
134
|
/**
|
|
160
135
|
* `actions` keyed by name, each its exact contract: `byName(actions).getUser`, its schemas
|
|
161
136
|
* where a test or a mock reads them, and its types where a helper takes a name,
|
|
@@ -164,74 +139,20 @@ export declare function make<const Name extends string, const O extends Options>
|
|
|
164
139
|
* one. A name held twice is refused, as a surface refuses it.
|
|
165
140
|
*/
|
|
166
141
|
export declare function byName<const Actions extends ReadonlyArray<Any>>(actions: Actions): { readonly [A in Actions[number] as A["name"]]: A; };
|
|
167
|
-
/** What `implement` binds: one action, or several that share one builder. */
|
|
168
142
|
type Target = Any | ReadonlyArray<Any>;
|
|
169
|
-
|
|
170
|
-
* A target of public actions only, which takes no `authorize`. Named for what a protected
|
|
171
|
-
* target lacks there, as the error a call without `authorize` reports names this type.
|
|
172
|
-
*/
|
|
173
|
-
type ProtectedActionsTakeAuthorize = PublicAction | ReadonlyArray<PublicAction>;
|
|
174
|
-
/** An action anyone may call. */
|
|
175
|
-
type PublicAction = Any & {
|
|
176
|
-
readonly caller: typeof Anyone;
|
|
177
|
-
};
|
|
178
|
-
/**
|
|
179
|
-
* The actions `T` stands for. One action is extracted rather than taken as it is, so where `T`
|
|
180
|
-
* is deferred, such as a list of a helper's type parameter, the actions are still actions to
|
|
181
|
-
* TypeScript, and a selection of them reaches a surface.
|
|
182
|
-
*/
|
|
143
|
+
type TakesNoAuthorize<T extends Target> = [Protected<ActionsOf<T>>] extends [never] ? unknown : "A protected action takes { authorize }: your rule, or Action.allowAll";
|
|
183
144
|
type ActionsOf<T extends Target> = T extends ReadonlyArray<Any> ? T[number] : Extract<T, Any>;
|
|
184
|
-
/** The names of the actions `T` stands for. */
|
|
185
145
|
type NamesOf<T extends Target> = ActionsOf<T>["name"];
|
|
186
|
-
/**
|
|
187
|
-
* What a list takes instead of one handler: nothing. A named alias carrying `R`, and it must
|
|
188
|
-
* stay one: while a builder's inner call, such as `Effect.gen`, is inferred, TypeScript keeps
|
|
189
|
-
* an alias's arguments marked as not yet inferred, where `never` or this type written inline
|
|
190
|
-
* becomes a candidate, and the builder's record loses its handlers' parameter types.
|
|
191
|
-
*/
|
|
192
146
|
type NoHandler<R> = {
|
|
193
147
|
readonly "~list": R;
|
|
194
148
|
};
|
|
195
|
-
/**
|
|
196
|
-
* One action's handler, owing `R` per request; a list takes none. It is `Handler` written out, as
|
|
197
|
-
* `Authorizer`'s are. While `implement` infers, an alias whose argument `R` is not yet
|
|
198
|
-
* inferred is marked, as a whole, as not inferrable (the mark `NoHandler` relies on), so an
|
|
199
|
-
* `Effect.fn(...)` a builder returns would infer nothing from it and take an `any` input.
|
|
200
|
-
*/
|
|
201
149
|
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>;
|
|
202
|
-
/**
|
|
203
|
-
* A record of handlers, one per key of `RequestServices`, each typed from its own action and
|
|
204
|
-
* owing its entry of `RequestServices` per request. TypeScript infers `RequestServices` from the
|
|
205
|
-
* record, key by key, so each handler, `Effect.fn` included, is typed from its contract; a key
|
|
206
|
-
* no action names takes nothing.
|
|
207
|
-
*/
|
|
208
150
|
type Several<T extends Target, RequestServices> = { readonly [K in keyof RequestServices]: K extends NamesOf<T> ? Handler<Extract<ActionsOf<T>, {
|
|
209
151
|
readonly name: K;
|
|
210
152
|
}>, RequestServices[K]> : never; };
|
|
211
|
-
/** What `implement` binds to `T`: one action's handler, or a list's record. */
|
|
212
153
|
type HandlersOf<T extends Target, R, RequestServices> = Single<T, R> | Several<T, RequestServices>;
|
|
213
|
-
/**
|
|
214
|
-
* `T`, never inferred from where it stands: an index TypeScript cannot read until `T` is
|
|
215
|
-
* known, as `NoInfer` would be, but read as `T` itself once it is. So an implementation
|
|
216
|
-
* written inside a surface's arguments takes nothing from what that surface accepts, and a
|
|
217
|
-
* union of services is one union, which `Layer.provide` discharges a member at a time.
|
|
218
|
-
*/
|
|
219
154
|
type Deferred<T> = [T][T extends unknown ? 0 : never];
|
|
220
|
-
/**
|
|
221
|
-
* An authorizer, or an Effect that builds it, as a builder builds handlers: `EAX` and `RAX` are
|
|
222
|
-
* startup failures and services, `RA` what the authorizer reads per request. Each authorizer is
|
|
223
|
-
* `Authorize` written out. TypeScript would infer `RA` from only one branch of a conditional
|
|
224
|
-
* authorizer whose other branch is typed `Authorize`, such as
|
|
225
|
-
* `enabled ? authorize : Action.allowAll`. While `implement` infers, an alias whose argument `RA`
|
|
226
|
-
* is not yet inferred is marked, as a whole, as not inferrable, so an `Effect.fn(...)` the
|
|
227
|
-
* Effect returns would infer nothing from it and take an `any` action.
|
|
228
|
-
*/
|
|
229
155
|
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>;
|
|
230
|
-
/**
|
|
231
|
-
* What `implement` returns for `T`: its handlers' per-request requirements keyed by action
|
|
232
|
-
* name, the authorizer's, and the startup failures and services of the builder and the
|
|
233
|
-
* authorizer.
|
|
234
|
-
*/
|
|
235
156
|
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>>>;
|
|
236
157
|
/**
|
|
237
158
|
* Every authenticated caller may call a protected action. The contract's identity
|
|
@@ -249,15 +170,12 @@ export declare const allowAll: Authorize<Any>;
|
|
|
249
170
|
* them. `Action.allowAll` permits every authenticated caller. An Effect may build the
|
|
250
171
|
* authorizer once, with startup requirements separate from its per-call requirements.
|
|
251
172
|
*/
|
|
252
|
-
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: {
|
|
253
|
-
readonly authorize
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
readonly authorize?: never;
|
|
257
|
-
}): Implemented<T, RequestServices, R, EX, RX, RA, EAX, RAX>;
|
|
258
|
-
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: {
|
|
259
|
-
readonly authorize: [Protected<ActionsOf<T>>] extends [never] ? "A public-only target takes no authorize" : Authorizer<Protected<ActionsOf<T>>, RA, EAX, RAX>;
|
|
173
|
+
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: [Protected<ActionsOf<T>>] extends [never] ? {
|
|
174
|
+
readonly authorize?: "A public-only target takes no authorize";
|
|
175
|
+
} | undefined : {
|
|
176
|
+
readonly authorize: Authorizer<Protected<ActionsOf<T>>, RA, EAX, RAX>;
|
|
260
177
|
}): Implemented<T, RequestServices, R, EX, RX, RA, EAX, RAX>;
|
|
178
|
+
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>(actions: T & TakesNoAuthorize<T>, handlers: HandlersOf<T, R, RequestServices> | Effect.Effect<HandlersOf<T, R, RequestServices>, EX, RX>): Implemented<T, RequestServices, R, EX, RX, never, never, never>;
|
|
261
179
|
/**
|
|
262
180
|
* The builders of `implementations`, as one layer providing nothing. Surfaces build them
|
|
263
181
|
* themselves, once per layer graph, but routes under `HttpRouter.serve` or `Testing.layer`
|
|
@@ -265,10 +183,6 @@ export declare function implement<const T extends Target, RequestServices extend
|
|
|
265
183
|
* surface, with the services it is given.
|
|
266
184
|
*/
|
|
267
185
|
export declare function layer<const Apps extends Served>(implementations: Apps): Layer.Layer<never, BuildError<Member<Apps>>, BuildServices<Member<Apps>>>;
|
|
268
|
-
/**
|
|
269
|
-
* One call of `A` in process: its decoded success, or its declared errors and the built-in
|
|
270
|
-
* ones, owing `R` per call, what its handler and its implementation's `authorize` read.
|
|
271
|
-
*/
|
|
272
186
|
type Method<A extends Any, R> = Call<A, Effect.Effect<A["success"]["Type"], A["error"][number]["Type"] | BuiltIn, R>>;
|
|
273
187
|
/**
|
|
274
188
|
* Every action of the implementations `Apps`, one or a list, as `client.<action>(input)`: what
|
package/dist/Action.mjs
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated } from "./
|
|
2
|
-
import { Anyone, assertKnown, assertName, assertOnce, assertOwnTags, errorList, projectedErrors } from "./
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { Image } from "./
|
|
1
|
+
import { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated } from "./contract/errors.mjs";
|
|
2
|
+
import { Anyone, assertKnown, assertName, assertOnce, assertOwnTags, errorList, projectedErrors } from "./contract/rules.mjs";
|
|
3
|
+
import { Implementation, builders, built, memoized, select, toList } from "./contract/implementation.mjs";
|
|
4
|
+
import { inputOf } from "./contract/call.mjs";
|
|
5
|
+
import { Image } from "./contract/media.mjs";
|
|
6
6
|
import { Array as Array$1, Cause, Context, Effect, Predicate, Schema } from "effect";
|
|
7
|
-
//#region src/Action.ts
|
|
8
|
-
/**
|
|
9
|
-
* An object without fields: an action without arguments, `{}` given or not. Effect's own
|
|
10
|
-
* `Tool.EmptyParams`: strict, unlike `Schema.Struct({})`, which accepts any value but `null`,
|
|
11
|
-
* and the object root MCP requires.
|
|
12
|
-
*/
|
|
7
|
+
//#region src/contract/Action.ts
|
|
13
8
|
const NoInput = Schema.Record(Schema.String, Schema.Never);
|
|
14
|
-
/**
|
|
15
|
-
* The schema an option stands for: a schema as given, `NoInput` for no fields, or the struct
|
|
16
|
-
* of the fields, which may be keyed by symbols, as in a struct.
|
|
17
|
-
*/
|
|
18
9
|
const codecOf = (schema) => Schema.isSchema(schema) ? schema : Reflect.ownKeys(schema).length === 0 ? NoInput : Schema.Struct(schema);
|
|
19
10
|
function make(name, options) {
|
|
20
11
|
assertName("action name", name);
|
|
@@ -44,36 +35,25 @@ function byName(actions) {
|
|
|
44
35
|
* requirement is enforced independently, including when its handler reads no identity.
|
|
45
36
|
*/
|
|
46
37
|
const allowAll = () => Effect.void;
|
|
47
|
-
/**
|
|
48
|
-
* `authorize`, checked: the types require a function, and plain JavaScript can still pass
|
|
49
|
-
* none, which must not serve every caller.
|
|
50
|
-
*/
|
|
51
38
|
const assertAuthorizer = (before) => {
|
|
52
39
|
if (!Predicate.isFunction(before)) throw new Error("Missing authorize: pass an authorization function, or Action.allowAll");
|
|
53
40
|
return before;
|
|
54
41
|
};
|
|
55
|
-
/** `authorize`, present wherever a protected action is implemented. */
|
|
56
42
|
const assertAuthorization = (authorize) => {
|
|
57
43
|
if (authorize === void 0) throw new Error("Protected actions require authorize, or Action.allowAll");
|
|
58
44
|
return authorize;
|
|
59
45
|
};
|
|
60
|
-
/** No `authorize` where only public actions are implemented: it would never run. */
|
|
61
46
|
const assertNoAuthorization = (authorize) => {
|
|
62
47
|
if (authorize !== void 0) throw new Error("A public-only target takes no authorize");
|
|
63
48
|
return allowAll;
|
|
64
49
|
};
|
|
65
|
-
/**
|
|
66
|
-
* The layer building `authorize`: a plain function as it is, a built one once its Effect
|
|
67
|
-
* runs, checked when the layer builds, as a builder's record is. `Effect.isEffect` tells them
|
|
68
|
-
* apart, so one written with `Effect.fn` stays plain.
|
|
69
|
-
*/
|
|
70
50
|
const authorizerOf = (before) => memoized(Effect.isEffect(before) ? Effect.map(before, assertAuthorizer) : Effect.succeed(assertAuthorizer(before)));
|
|
71
51
|
function implement(actions, handlers, options) {
|
|
72
52
|
const listed = Array$1.ensure(actions);
|
|
73
53
|
const names = listed.map((action) => action.name);
|
|
74
54
|
assertOnce("action", listed);
|
|
75
55
|
const authorizer = authorizerOf(listed.some((action) => action.caller !== Anyone) ? assertAuthorization(options?.authorize) : assertNoAuthorization(options?.authorize));
|
|
76
|
-
const
|
|
56
|
+
const pairWithActions = (built) => {
|
|
77
57
|
const keyed = Predicate.isFunction(built) ? Object.fromEntries(Array.isArray(actions) ? [] : listed.map(({ name }) => [name, built])) : built;
|
|
78
58
|
assertKnown("handlers", Object.keys(keyed), names);
|
|
79
59
|
const bound = listed.flatMap((action) => {
|
|
@@ -84,46 +64,27 @@ function implement(actions, handlers, options) {
|
|
|
84
64
|
if (missing.length > 0) throw new Error(`Missing handlers: ${missing.join(", ")}`);
|
|
85
65
|
return bound;
|
|
86
66
|
};
|
|
87
|
-
const recorded = Effect.isEffect(handlers) ? Effect.map(handlers,
|
|
67
|
+
const recorded = Effect.isEffect(handlers) ? Effect.map(handlers, pairWithActions) : Effect.succeed(pairWithActions(handlers));
|
|
88
68
|
return new Implementation(listed, memoized(recorded), authorizer);
|
|
89
69
|
}
|
|
90
70
|
function layer(implementations) {
|
|
91
71
|
return builders(toList(implementations));
|
|
92
72
|
}
|
|
93
|
-
|
|
94
|
-
* A value through `schema`'s JSON codec, as a wire carries it: encoded to JSON text, then
|
|
95
|
-
* decoded, so what comes out is what a remote call's other side decodes, `-0` as `0`. Every
|
|
96
|
-
* issue is reported.
|
|
97
|
-
*/
|
|
98
|
-
const wire = (schema) => {
|
|
73
|
+
const jsonRoundTrip = (schema) => {
|
|
99
74
|
const codec = Schema.fromJsonString(Schema.toCodecJson(schema));
|
|
100
75
|
const encode = Schema.encodeUnknownEffect(codec, { errors: "all" });
|
|
101
76
|
const decode = Schema.decodeUnknownEffect(codec, { errors: "all" });
|
|
102
77
|
return (value) => Effect.flatMap(encode(value), (encoded) => decode(encoded));
|
|
103
78
|
};
|
|
104
|
-
|
|
105
|
-
* `cause` with each failure passed through `failure` where it stands, so it keeps its
|
|
106
|
-
* annotations, the span it failed in among them; failed anew, it would lose them. One that
|
|
107
|
-
* passes keeps the stack of where it was made, where the decoded error's would be the codec's.
|
|
108
|
-
* One that does not, such as an error the action does not declare, is a defect: its
|
|
109
|
-
* `SchemaError`, then the failure itself.
|
|
110
|
-
*/
|
|
111
|
-
const mapFailures = (cause, failure) => Effect.map(Effect.forEach(cause.reasons, (reason) => Cause.isFailReason(reason) ? Effect.match(failure(reason.error), {
|
|
79
|
+
const mapFailuresInPlace = (cause, failure) => Effect.map(Effect.forEach(cause.reasons, (reason) => Cause.isFailReason(reason) ? Effect.match(failure(reason.error), {
|
|
112
80
|
onFailure: (refused) => Cause.combine(Cause.die(refused), Cause.die(reason.error)),
|
|
113
81
|
onSuccess: (decoded) => Cause.fail(Predicate.isError(reason.error) && Predicate.isError(decoded) ? Object.assign(decoded, { stack: reason.error.stack }) : decoded)
|
|
114
82
|
}).pipe(Effect.map((mapped) => Cause.annotate(mapped, Cause.reasonAnnotations(reason)))) : Effect.succeed(Cause.fromReasons([reason]))), (causes) => Cause.fromReasons(causes.flatMap(({ reasons }) => reasons)));
|
|
115
|
-
/**
|
|
116
|
-
* `action`'s method, running `run`, its handler behind authorization, as a remote
|
|
117
|
-
* call runs: input that does not pass through its codec is `InvalidInput`, and nothing else
|
|
118
|
-
* runs; a success or a failure that does not is a defect, as it is an empty 500 over HTTP,
|
|
119
|
-
* since the handler broke its contract. A failure passes through the codec of
|
|
120
|
-
* every error the action declares, the built-in ones included.
|
|
121
|
-
*/
|
|
122
83
|
const methodOf = (action, run) => {
|
|
123
|
-
const input =
|
|
124
|
-
const success =
|
|
125
|
-
const failure =
|
|
126
|
-
return (...args) => inputOf(action, args).pipe(Effect.flatMap(input), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap((value) => Effect.catchCause(run(value), (cause) => Effect.flatMap(
|
|
84
|
+
const input = jsonRoundTrip(action.input);
|
|
85
|
+
const success = jsonRoundTrip(action.success);
|
|
86
|
+
const failure = jsonRoundTrip(Schema.Union(projectedErrors(action)));
|
|
87
|
+
return (...args) => inputOf(action, args).pipe(Effect.flatMap(input), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap((value) => Effect.catchCause(run(value), (cause) => Effect.flatMap(mapFailuresInPlace(cause, failure), Effect.failCause))), Effect.flatMap((value) => Effect.orDie(success(value))));
|
|
127
88
|
};
|
|
128
89
|
function client(served, options) {
|
|
129
90
|
const apps = select(toList(served), options?.actions);
|