@gjermundgaraba/effect-actions 0.11.0 → 0.12.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 +101 -40
- package/dist/Action.d.mts +29 -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 +5 -8
- package/dist/ActionToolkit.mjs +3 -3
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +28 -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 +17 -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 +206 -426
- package/docs/ActionCli-failures.md +102 -0
- package/docs/ActionCli.md +206 -293
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +202 -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 +178 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +100 -119
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +142 -392
- package/docs/CONTEXT.md +39 -24
- package/docs/README.md +71 -35
- package/docs/Testing-failures.md +20 -0
- package/docs/Testing.md +186 -151
- package/docs/guarantees.md +35 -96
- package/docs/setup-failures.md +34 -0
- package/docs/setup.md +87 -91
- 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 +317 -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 +146 -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 +109 -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 +41 -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 +75 -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 +136 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +119 -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 +30 -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 +108 -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 +89 -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 +91 -0
- package/examples/notes-service/src/notes/handlers.ts +33 -0
- package/examples/notes-service/src/notes/store.ts +73 -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 +134 -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 +23 -5
- 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: pages read together are cut. It routes every task, protecting an
|
|
9
|
+
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,13 +90,12 @@ 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
|
|
|
@@ -81,35 +103,73 @@ const greeting = Effect.gen(function* () {
|
|
|
81
103
|
pnpm add @gjermundgaraba/effect-actions effect@~4.0.2
|
|
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.2` 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,10 @@ 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 or updates.
|
|
28
|
+
*/
|
|
34
29
|
readonly destructiveHint?: boolean | undefined;
|
|
35
30
|
/** `annotations.idempotentHint`, meaningful for a write only. */
|
|
36
31
|
readonly idempotentHint?: boolean | undefined;
|
|
@@ -49,14 +44,18 @@ export interface Options {
|
|
|
49
44
|
*/
|
|
50
45
|
readonly caller: typeof Anyone | Context.Key<unknown, unknown>;
|
|
51
46
|
readonly description: string;
|
|
52
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* A schema or struct fields. Omit, or give `{}`, for an action without arguments. A field's
|
|
49
|
+
* `description` annotation is what a model reads in a tool's listing and a flag's help in a CLI.
|
|
50
|
+
*/
|
|
53
51
|
readonly input?: Codec | Fields | undefined;
|
|
54
52
|
/** A schema or struct fields. Omit for an action that returns nothing: `Schema.Void`. */
|
|
55
53
|
readonly success?: Codec | Fields | undefined;
|
|
56
54
|
/**
|
|
57
55
|
* The declared failures: one schema, or a list of them, as `HttpApiEndpoint` takes them;
|
|
58
|
-
* each keeps its own HTTP status annotation
|
|
59
|
-
*
|
|
56
|
+
* each keeps its own HTTP status annotation, the third argument of
|
|
57
|
+
* `Schema.TaggedError<NoteNotFound>()("NoteNotFound", { id: Schema.String }, { httpApiStatus: 404 })`,
|
|
58
|
+
* and answers 422 without one. Defaults to none. None with a built-in error's `_tag`.
|
|
60
59
|
*/
|
|
61
60
|
readonly error?: Codec | ReadonlyArray<Codec> | undefined;
|
|
62
61
|
/**
|
|
@@ -68,38 +67,19 @@ export interface Options {
|
|
|
68
67
|
/** How the action's tool presents itself over MCP, and on a native Toolkit's tools. */
|
|
69
68
|
readonly mcp?: Mcp;
|
|
70
69
|
}
|
|
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
70
|
type UnknownMcp<O> = O extends {
|
|
76
71
|
readonly mcp?: infer H;
|
|
77
72
|
} ? Exclude<H extends unknown ? keyof H : never, keyof Mcp> : never;
|
|
78
|
-
/** No key beyond `Mcp`, whichever member of `O` gives it. */
|
|
79
73
|
type KnownMcp<O> = {
|
|
80
74
|
readonly mcp?: { readonly [K in UnknownMcp<O>]: never; };
|
|
81
75
|
};
|
|
82
|
-
/** The schemas an `error` option of type `G` declares: one, or each of a list. */
|
|
83
76
|
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
77
|
type OwnErrors<O> = O extends {
|
|
90
78
|
readonly error: infer G;
|
|
91
79
|
} ? [Extract<ErrorMembers<G>, BuiltIns>] extends [never] ? unknown : {
|
|
92
80
|
readonly error: Exclude<ErrorMembers<G>, BuiltIns> | ReadonlyArray<Exclude<ErrorMembers<G>, BuiltIns>>;
|
|
93
81
|
} : 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
82
|
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
83
|
type ServiceCaller<O> = O extends {
|
|
104
84
|
readonly caller: {
|
|
105
85
|
readonly defaultValue: unknown;
|
|
@@ -107,27 +87,19 @@ type ServiceCaller<O> = O extends {
|
|
|
107
87
|
} ? {
|
|
108
88
|
readonly caller: "An identity is a Context.Service, not a Context.Reference";
|
|
109
89
|
} : 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
90
|
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
91
|
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
92
|
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>>>>;
|
|
93
|
+
type ErrorSchemasOf<O> = ReadonlyArray<ErrorMembers<Extract<OptionOf<O, "error", []>, Codec | ReadonlyArray<Codec>>>>;
|
|
126
94
|
/** A pure contract: schemas and transport metadata. Handlers are bound by `implement`. */
|
|
127
95
|
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
96
|
readonly caller: Caller;
|
|
129
97
|
readonly name: Name;
|
|
130
98
|
readonly description: string;
|
|
99
|
+
/**
|
|
100
|
+
* The input as a schema, a `Schema.Struct` where fields were given: reuse it as it is, as in
|
|
101
|
+
* `Schema.Array(action.input)`.
|
|
102
|
+
*/
|
|
131
103
|
readonly input: Input;
|
|
132
104
|
readonly success: Success;
|
|
133
105
|
/** The declared failures, as a list: one given alone is a list of one. */
|
|
@@ -154,8 +126,9 @@ export type Handlers<A extends ReadonlyArray<Any>, R = never> = { readonly [Acti
|
|
|
154
126
|
/**
|
|
155
127
|
* Define an action contract. Names are `[A-Za-z0-9_-]{1,128}`, other than `then`: the name
|
|
156
128
|
* is also the route segment, the client method and the tool name.
|
|
129
|
+
* Guide: `docs/Action.md` in this package; in an application: `examples/notes-service/src/notes/`.
|
|
157
130
|
*/
|
|
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>,
|
|
131
|
+
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
132
|
/**
|
|
160
133
|
* `actions` keyed by name, each its exact contract: `byName(actions).getUser`, its schemas
|
|
161
134
|
* where a test or a mock reads them, and its types where a helper takes a name,
|
|
@@ -164,74 +137,20 @@ export declare function make<const Name extends string, const O extends Options>
|
|
|
164
137
|
* one. A name held twice is refused, as a surface refuses it.
|
|
165
138
|
*/
|
|
166
139
|
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
140
|
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
|
-
*/
|
|
141
|
+
type TakesNoAuthorize<T extends Target> = [Protected<ActionsOf<T>>] extends [never] ? unknown : "A protected action takes { authorize }: your rule, or Action.allowAll";
|
|
183
142
|
type ActionsOf<T extends Target> = T extends ReadonlyArray<Any> ? T[number] : Extract<T, Any>;
|
|
184
|
-
/** The names of the actions `T` stands for. */
|
|
185
143
|
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
144
|
type NoHandler<R> = {
|
|
193
145
|
readonly "~list": R;
|
|
194
146
|
};
|
|
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
147
|
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
148
|
type Several<T extends Target, RequestServices> = { readonly [K in keyof RequestServices]: K extends NamesOf<T> ? Handler<Extract<ActionsOf<T>, {
|
|
209
149
|
readonly name: K;
|
|
210
150
|
}>, RequestServices[K]> : never; };
|
|
211
|
-
/** What `implement` binds to `T`: one action's handler, or a list's record. */
|
|
212
151
|
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
152
|
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
153
|
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
154
|
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
155
|
/**
|
|
237
156
|
* Every authenticated caller may call a protected action. The contract's identity
|
|
@@ -249,15 +168,12 @@ export declare const allowAll: Authorize<Any>;
|
|
|
249
168
|
* them. `Action.allowAll` permits every authenticated caller. An Effect may build the
|
|
250
169
|
* authorizer once, with startup requirements separate from its per-call requirements.
|
|
251
170
|
*/
|
|
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>;
|
|
171
|
+
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] ? {
|
|
172
|
+
readonly authorize?: "A public-only target takes no authorize";
|
|
173
|
+
} | undefined : {
|
|
174
|
+
readonly authorize: Authorizer<Protected<ActionsOf<T>>, RA, EAX, RAX>;
|
|
260
175
|
}): Implemented<T, RequestServices, R, EX, RX, RA, EAX, RAX>;
|
|
176
|
+
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
177
|
/**
|
|
262
178
|
* The builders of `implementations`, as one layer providing nothing. Surfaces build them
|
|
263
179
|
* themselves, once per layer graph, but routes under `HttpRouter.serve` or `Testing.layer`
|
|
@@ -265,10 +181,6 @@ export declare function implement<const T extends Target, RequestServices extend
|
|
|
265
181
|
* surface, with the services it is given.
|
|
266
182
|
*/
|
|
267
183
|
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
184
|
type Method<A extends Any, R> = Call<A, Effect.Effect<A["success"]["Type"], A["error"][number]["Type"] | BuiltIn, R>>;
|
|
273
185
|
/**
|
|
274
186
|
* 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);
|