@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.
Files changed (215) hide show
  1. package/README.md +102 -41
  2. package/dist/Action.d.mts +31 -117
  3. package/dist/Action.mjs +14 -53
  4. package/dist/ActionCli.d.mts +60 -28
  5. package/dist/ActionCli.mjs +54 -48
  6. package/dist/ActionHttp.d.mts +18 -67
  7. package/dist/ActionHttp.mjs +20 -70
  8. package/dist/ActionMcp.d.mts +23 -29
  9. package/dist/ActionMcp.mjs +56 -114
  10. package/dist/ActionRpc.d.mts +147 -0
  11. package/dist/ActionRpc.mjs +113 -0
  12. package/dist/ActionToolkit.d.mts +28 -9
  13. package/dist/ActionToolkit.mjs +31 -4
  14. package/dist/Authentication.d.mts +15 -15
  15. package/dist/Authentication.mjs +42 -96
  16. package/dist/Testing.d.mts +30 -23
  17. package/dist/Testing.mjs +27 -44
  18. package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
  19. package/dist/authentication/provider.mjs +12 -0
  20. package/dist/authentication/refusal.mjs +36 -0
  21. package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
  22. package/dist/cli/commands.mjs +302 -0
  23. package/dist/{internal → contract}/call.d.mts +2 -2
  24. package/dist/contract/call.mjs +10 -0
  25. package/dist/{internal → contract}/errors.d.mts +13 -10
  26. package/dist/{internal → contract}/errors.mjs +9 -14
  27. package/dist/{internal → contract}/implementation.d.mts +37 -21
  28. package/dist/{internal → contract}/implementation.mjs +18 -49
  29. package/dist/contract/media.d.mts +13 -0
  30. package/dist/contract/media.mjs +18 -0
  31. package/dist/contract/rules.d.mts +25 -0
  32. package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
  33. package/dist/{internal → http}/client.d.mts +7 -9
  34. package/dist/http/client.mjs +17 -0
  35. package/dist/http/declared.mjs +20 -0
  36. package/dist/http/schema-errors.mjs +9 -0
  37. package/dist/mcp/protocol.d.mts +18 -0
  38. package/dist/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
  39. package/dist/stdio/console.mjs +64 -0
  40. package/dist/{internal → testing}/memory.d.mts +1 -1
  41. package/dist/{internal → testing}/memory.mjs +5 -17
  42. package/dist/toolkit/tools.mjs +29 -0
  43. package/docs/Action-failures.md +47 -0
  44. package/docs/Action-in-process.md +93 -0
  45. package/docs/Action.md +211 -426
  46. package/docs/ActionCli-failures.md +104 -0
  47. package/docs/ActionCli.md +208 -293
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +205 -282
  50. package/docs/ActionMcp-failures.md +31 -0
  51. package/docs/ActionMcp.md +222 -271
  52. package/docs/ActionRpc-failures.md +21 -0
  53. package/docs/ActionRpc.md +179 -0
  54. package/docs/ActionToolkit-failures.md +15 -0
  55. package/docs/ActionToolkit.md +103 -119
  56. package/docs/Authentication-advanced.md +139 -0
  57. package/docs/Authentication-failures.md +99 -0
  58. package/docs/Authentication.md +143 -392
  59. package/docs/CONTEXT.md +40 -24
  60. package/docs/README.md +75 -35
  61. package/docs/Testing-failures.md +21 -0
  62. package/docs/Testing.md +193 -151
  63. package/docs/guarantees.md +35 -96
  64. package/docs/setup-failures.md +37 -0
  65. package/docs/setup.md +90 -92
  66. package/examples/README.md +68 -0
  67. package/examples/assistant/README.md +92 -0
  68. package/examples/assistant/src/agent.ts +92 -0
  69. package/examples/assistant/src/cli.ts +34 -0
  70. package/examples/assistant/src/commands.ts +25 -0
  71. package/examples/assistant/src/identity/Identity.ts +33 -0
  72. package/examples/assistant/src/identity/authorize.ts +24 -0
  73. package/examples/assistant/src/identity/handlers.ts +10 -0
  74. package/examples/assistant/src/identity/members.ts +9 -0
  75. package/examples/assistant/src/identity/permissions.ts +23 -0
  76. package/examples/assistant/src/scripted.ts +23 -0
  77. package/examples/assistant/src/tasks/Tasks.ts +63 -0
  78. package/examples/assistant/src/tasks/handlers.ts +25 -0
  79. package/examples/assistant/src/tasks/store.ts +96 -0
  80. package/examples/assistant/src/toolkit.ts +30 -0
  81. package/examples/assistant/test/commands.test.ts +57 -0
  82. package/examples/assistant/test/identity/identity.test.ts +43 -0
  83. package/examples/assistant/test/tasks/store.test.ts +48 -0
  84. package/examples/assistant/test/tasks/tasks.test.ts +75 -0
  85. package/examples/assistant/test/toolkit.test.ts +121 -0
  86. package/examples/kitchen-sink/README.md +318 -0
  87. package/examples/kitchen-sink/src/Actions.ts +19 -0
  88. package/examples/kitchen-sink/src/Api.ts +24 -0
  89. package/examples/kitchen-sink/src/Rpc.ts +12 -0
  90. package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
  91. package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
  92. package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
  93. package/examples/kitchen-sink/src/admin/grants.ts +22 -0
  94. package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
  95. package/examples/kitchen-sink/src/admin/keys.ts +8 -0
  96. package/examples/kitchen-sink/src/agent.ts +47 -0
  97. package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
  98. package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
  99. package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
  100. package/examples/kitchen-sink/src/assistant/handlers.ts +126 -0
  101. package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
  102. package/examples/kitchen-sink/src/attachments/download.ts +49 -0
  103. package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
  104. package/examples/kitchen-sink/src/attachments/store.ts +58 -0
  105. package/examples/kitchen-sink/src/browser.ts +33 -0
  106. package/examples/kitchen-sink/src/cli.ts +85 -0
  107. package/examples/kitchen-sink/src/config.ts +18 -0
  108. package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
  109. package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
  110. package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
  111. package/examples/kitchen-sink/src/identity/directory.ts +52 -0
  112. package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
  113. package/examples/kitchen-sink/src/identity/members.ts +13 -0
  114. package/examples/kitchen-sink/src/identity/quota.ts +62 -0
  115. package/examples/kitchen-sink/src/issues/Issues.ts +111 -0
  116. package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
  117. package/examples/kitchen-sink/src/issues/guide.ts +37 -0
  118. package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
  119. package/examples/kitchen-sink/src/issues/store.ts +96 -0
  120. package/examples/kitchen-sink/src/issues/write-limit.ts +42 -0
  121. package/examples/kitchen-sink/src/memory.ts +30 -0
  122. package/examples/kitchen-sink/src/ops.ts +42 -0
  123. package/examples/kitchen-sink/src/request-policy.ts +63 -0
  124. package/examples/kitchen-sink/src/routes.ts +120 -0
  125. package/examples/kitchen-sink/src/sdk.ts +46 -0
  126. package/examples/kitchen-sink/src/seed.ts +65 -0
  127. package/examples/kitchen-sink/src/server.ts +62 -0
  128. package/examples/kitchen-sink/src/stdio.ts +34 -0
  129. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
  130. package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
  131. package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
  132. package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
  133. package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
  134. package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
  135. package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
  136. package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
  137. package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
  138. package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
  139. package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
  140. package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
  141. package/examples/kitchen-sink/test/routes.test.ts +123 -0
  142. package/examples/kitchen-sink/test/support/scripted.ts +40 -0
  143. package/examples/live-rpc/README.md +107 -0
  144. package/examples/live-rpc/src/Rpc.ts +8 -0
  145. package/examples/live-rpc/src/browser.ts +98 -0
  146. package/examples/live-rpc/src/chat/Chat.ts +77 -0
  147. package/examples/live-rpc/src/chat/authorize.ts +16 -0
  148. package/examples/live-rpc/src/chat/handlers.ts +50 -0
  149. package/examples/live-rpc/src/chat/room.ts +23 -0
  150. package/examples/live-rpc/src/chat/store.ts +54 -0
  151. package/examples/live-rpc/src/client.ts +37 -0
  152. package/examples/live-rpc/src/identity/Identity.ts +19 -0
  153. package/examples/live-rpc/src/identity/members.ts +8 -0
  154. package/examples/live-rpc/src/identity/verifier.ts +23 -0
  155. package/examples/live-rpc/src/routes.ts +22 -0
  156. package/examples/live-rpc/src/server.ts +43 -0
  157. package/examples/live-rpc/test/browser.test.ts +105 -0
  158. package/examples/live-rpc/test/chat/chat.test.ts +69 -0
  159. package/examples/live-rpc/test/routes.test.ts +99 -0
  160. package/examples/live-rpc/test/server.test.ts +75 -0
  161. package/examples/mcp-tools/README.md +141 -0
  162. package/examples/mcp-tools/src/instructions.ts +12 -0
  163. package/examples/mcp-tools/src/printers/Printers.ts +121 -0
  164. package/examples/mcp-tools/src/printers/fake.ts +115 -0
  165. package/examples/mcp-tools/src/printers/farm.ts +136 -0
  166. package/examples/mcp-tools/src/printers/handlers.ts +29 -0
  167. package/examples/mcp-tools/src/routes.ts +52 -0
  168. package/examples/mcp-tools/src/server.ts +31 -0
  169. package/examples/mcp-tools/src/stdio.ts +26 -0
  170. package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
  171. package/examples/mcp-tools/test/routes.test.ts +202 -0
  172. package/examples/notes-service/README.md +111 -0
  173. package/examples/notes-service/src/Api.ts +13 -0
  174. package/examples/notes-service/src/browser.ts +41 -0
  175. package/examples/notes-service/src/cli.ts +22 -0
  176. package/examples/notes-service/src/commands.ts +137 -0
  177. package/examples/notes-service/src/identity/Identity.ts +31 -0
  178. package/examples/notes-service/src/identity/authorize.ts +18 -0
  179. package/examples/notes-service/src/identity/handlers.ts +10 -0
  180. package/examples/notes-service/src/identity/users.ts +9 -0
  181. package/examples/notes-service/src/identity/verifier.ts +36 -0
  182. package/examples/notes-service/src/notes/Notes.ts +119 -0
  183. package/examples/notes-service/src/notes/handlers.ts +39 -0
  184. package/examples/notes-service/src/notes/store.ts +84 -0
  185. package/examples/notes-service/src/routes.ts +45 -0
  186. package/examples/notes-service/src/server.ts +26 -0
  187. package/examples/notes-service/src/status/Status.ts +10 -0
  188. package/examples/notes-service/src/status/handlers.ts +14 -0
  189. package/examples/notes-service/src/stdio.ts +25 -0
  190. package/examples/notes-service/test/commands.test.ts +288 -0
  191. package/examples/notes-service/test/notes/notes.test.ts +103 -0
  192. package/examples/notes-service/test/routes.test.ts +84 -0
  193. package/examples/quickstart/README.md +31 -0
  194. package/examples/quickstart/src/Api.ts +5 -0
  195. package/examples/quickstart/src/client.ts +13 -0
  196. package/examples/quickstart/src/greeting/Greeting.ts +12 -0
  197. package/examples/quickstart/src/greeting/handlers.ts +6 -0
  198. package/examples/quickstart/src/routes.ts +11 -0
  199. package/examples/quickstart/src/server.ts +16 -0
  200. package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
  201. package/package.json +27 -9
  202. package/dist/internal/actions.d.mts +0 -9
  203. package/dist/internal/authentication.mjs +0 -11
  204. package/dist/internal/call.mjs +0 -12
  205. package/dist/internal/cli.mjs +0 -328
  206. package/dist/internal/client.mjs +0 -25
  207. package/dist/internal/console.mjs +0 -77
  208. package/dist/internal/declared.d.mts +0 -13
  209. package/dist/internal/declared.mjs +0 -34
  210. package/dist/internal/mcp.d.mts +0 -14
  211. package/dist/internal/media.d.mts +0 -8
  212. package/dist/internal/media.mjs +0 -26
  213. package/dist/internal/refusal.mjs +0 -67
  214. package/dist/internal/schema-errors.mjs +0 -20
  215. 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 MCP, hand it to a model as a native
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
- ```ts example=quickstart.ts
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
- // The contract and its HTTP binding import no server code, so any client can import them,
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
- and a server that implements it, in a module of its own:
51
+ And the same implementation is served on every surface the host wants:
28
52
 
29
- ```ts example=quickstart-server.ts
30
- import { Effect, Layer } from "effect";
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 { Greet, Http } from "./quickstart.js";
35
-
36
- // The contract states who may call it, here anyone, so its implementation takes no `authorize`.
37
- const greet = Action.implement(Greet, ({ name }) => Effect.succeed(`Hello, ${name}!`));
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, greet),
41
- ActionMcp.layerHttp(greet, { name: "greetings", version: "1.0.0" }),
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, with a request body
46
- limit, as [examples/server.ts](examples/server.ts) serves the example application, and you
47
- have `POST /api/greet` and an MCP tool named `greet` at `/mcp`. The same `greet`
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(greet); // native Effect AI Toolkit and its handler layer
52
- const cli = ActionCli.make(greet, { name: "greetings" }); // greetings greet --name Ada
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(greet); // the HTTP client's methods, in process
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. Bad input, a missing credential and a refusal come back as those built-in errors (400, 401, 403), never as an unreadable body.
71
- - Every contract states who may call it: `caller: Action.Anyone`, or the identity service a caller must have. A surface serving a protected action authenticates it before decoding its input, through the authentication descriptor its binding names, and the provider the application builds from its own token verifier.
72
- - Action-level authorization is written once, on the implementation: `Action.implement(actions, handlers, { authorize })`, required for protected actions. Every surface that serves it runs the rule before each handler, so no surface can leave it out and no handler repeats the access policy; `Action.allowAll` admits every authenticated caller. Operational limits, such as a rate limit, and record-level checks stay in the handler, failing with errors the contract lists.
73
- - Every action states whether it is read-only (`readOnly`), so that rule reads the contract instead of a hand-maintained list of mutation names.
74
- - A protected action's handler and rule read the identity its contract names, which only authentication of that request supplies; a startup identity never opens a protected route. Public and protected routes share one binding, one mount path, one OpenAPI document, which states the credentials each needs, and one client, and a handler's startup services are built once per [layer graph](docs/guarantees.md#dependency-lifetimes), however many surfaces serve it.
75
- - The pieces are Effect's own. `Http.api` is a native `HttpApi`, so OpenAPI, Swagger, Scalar and `HttpApiClient` work on it unchanged. MCP is Effect's native `McpServer`, with no SDK runtime dependency.
76
- - Tests run in memory. `Action.client` calls an implementation in process, with the typed client's methods, behind its `authorize`, as several callers in one test. Provide `Testing.layer(routes)`, then call the routes with the same typed client and the tools with `Testing.mcpClient`, without opening a port.
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.2
103
+ pnpm add @gjermundgaraba/effect-actions effect@~4.0.3
82
104
  ```
83
105
 
84
- Add `@effect/platform-node@~4.0.2` to serve or run a CLI from Node. Every module is a
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
- The reference in [docs/](docs/README.md) is written for coding agents: one card per module
91
- with the API, a canonical snippet, the rules, and the failure modes.
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, includes which module to use for which caller.
94
- - [docs/guarantees.md](docs/guarantees.md): cross-cutting rules for lifetimes, wire formats, and scope.
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; in a project that installs the package, point the agent at
106
- `node_modules/@gjermundgaraba/effect-actions/docs`, the pages of the installed version.
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 MCP: no streaming, uploads, prompts, or resources.
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/setup.md](docs/setup.md).
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
- See [CONTRIBUTING.md](CONTRIBUTING.md). MIT licensed.
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 "./internal/actions.mjs";
2
- import { BuiltIn, BuiltIns, Forbidden, InvalidInput, Issue, Refusal, Unauthenticated } from "./internal/errors.mjs";
3
- import { ActionOf, AnyImplementation, Authorize, BuildError, BuildServices, Holding, Implementation, Known, Member, Offered, OptionalUnless, Protected, RequestOf, SelectedOf, Served, Serving } from "./internal/implementation.mjs";
4
- import { Call } from "./internal/call.mjs";
5
- import { Image } from "./internal/media.mjs";
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
- /** `annotations.destructiveHint`, meaningful for a write only. */
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
- /** A schema or struct fields. Omit, or give `{}`, for an action without arguments. */
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. Defaults to none. None with a built-in
59
- * error's `_tag`.
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
- /** The declared errors of `O`, none when omitted. */
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>, ErrorsOf<O>, O["readOnly"], O["caller"]>;
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: [Protected<ActionsOf<T>>] extends [never] ? never : Authorizer<Protected<ActionsOf<T>>, RA, EAX, RAX>;
254
- }): Implemented<T, RequestServices, R, EX, RX, RA, EAX, RAX>;
255
- export declare function implement<const T extends ProtectedActionsTakeAuthorize, RequestServices extends (T extends ReadonlyArray<Any> ? { readonly [K in NamesOf<T>]: unknown; } : never), R = never, EX = never, RX = never, RA = never, EAX = never, RAX = never>(actions: T, handlers: HandlersOf<T, R, RequestServices> | Effect.Effect<HandlersOf<T, R, RequestServices>, EX, RX>, options?: {
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 "./internal/errors.mjs";
2
- import { Anyone, assertKnown, assertName, assertOnce, assertOwnTags, errorList, projectedErrors } from "./internal/actions.mjs";
3
- import { inputOf } from "./internal/call.mjs";
4
- import { Implementation, builders, built, memoized, select, toList } from "./internal/implementation.mjs";
5
- import { Image } from "./internal/media.mjs";
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 record = (built) => {
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, record) : Effect.succeed(record(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 = wire(action.input);
124
- const success = wire(action.success);
125
- const failure = wire(Schema.Union(projectedErrors(action)));
126
- return (...args) => inputOf(action, args).pipe(Effect.flatMap(input), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap((value) => Effect.catchCause(run(value), (cause) => Effect.flatMap(mapFailures(cause, failure), Effect.failCause))), Effect.flatMap((value) => Effect.orDie(success(value))));
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);