@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.
Files changed (215) hide show
  1. package/README.md +101 -40
  2. package/dist/Action.d.mts +29 -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 +5 -8
  13. package/dist/ActionToolkit.mjs +3 -3
  14. package/dist/Authentication.d.mts +15 -15
  15. package/dist/Authentication.mjs +42 -96
  16. package/dist/Testing.d.mts +28 -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 +17 -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 +206 -426
  46. package/docs/ActionCli-failures.md +102 -0
  47. package/docs/ActionCli.md +206 -293
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +202 -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 +178 -0
  54. package/docs/ActionToolkit-failures.md +15 -0
  55. package/docs/ActionToolkit.md +100 -119
  56. package/docs/Authentication-advanced.md +139 -0
  57. package/docs/Authentication-failures.md +99 -0
  58. package/docs/Authentication.md +142 -392
  59. package/docs/CONTEXT.md +39 -24
  60. package/docs/README.md +71 -35
  61. package/docs/Testing-failures.md +20 -0
  62. package/docs/Testing.md +186 -151
  63. package/docs/guarantees.md +35 -96
  64. package/docs/setup-failures.md +34 -0
  65. package/docs/setup.md +87 -91
  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 +317 -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 +146 -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 +109 -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 +41 -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 +75 -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 +136 -0
  162. package/examples/mcp-tools/src/instructions.ts +12 -0
  163. package/examples/mcp-tools/src/printers/Printers.ts +119 -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 +30 -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 +108 -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 +89 -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 +91 -0
  183. package/examples/notes-service/src/notes/handlers.ts +33 -0
  184. package/examples/notes-service/src/notes/store.ts +73 -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 +134 -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 +23 -5
  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
@@ -1,307 +1,227 @@
1
1
  # ActionHttp
2
2
 
3
- JSON `POST` routes on Effect's `HttpApi`. `ActionHttp.make` binds actions to a mount path and
4
- names the authentication descriptor of its protected actions; the binding is shared by the
5
- server, every client, and the OpenAPI document. Servers mount it with
6
- `ActionHttp.layer(Http, implementations)`, serving the binding's actions among the
7
- implementations, or those its `actions` lists: each protected route authenticated before its
8
- body is read, then each call
9
- behind its implementation's authorization. Clients call it with
10
- `ActionHttp.client(Http)`.
3
+ JSON `POST` routes on Effect's `HttpApi`, one per action. `ActionHttp.make` binds actions to a
4
+ mount path; the server mounts that one binding, every client calls it and the OpenAPI document is
5
+ generated from it.
6
+
7
+ Read this page alone, in one tool call: pages read together are cut. When something fails,
8
+ search [ActionHttp-failures.md](ActionHttp-failures.md). Reading order: [README.md](README.md).
11
9
 
12
10
  ## API
13
11
 
14
12
  Import `@gjermundgaraba/effect-actions/ActionHttp`.
15
13
 
16
- | API | Purpose |
17
- | ---------------------------------------- | -------------------------------------------------------------------------------------------- |
18
- | `make(actions, options?)` | Bind a list of actions; returns a `Binding`. Options are required for a protected action. |
19
- | `Http.actions` | The exact bound actions. |
20
- | `Http.error` | The errors every endpoint declares besides its action's own. |
21
- | `Http.prefix` | Where the routes mount: `/api` by default, `/` at the root, without a trailing slash. |
22
- | `Http.authentication` | The authentication descriptor of its protected actions, or `undefined`. |
23
- | `Http.api` | Native Effect `HttpApi` for clients and OpenAPI. |
24
- | `layer(Http, implementations, options?)` | Mount the routes of the bound actions these implementations hold, or the listed ones. |
25
- | `client(Http, options?)` | An Effect of a typed client; requires the native `HttpClient`, as `HttpApiClient.make` does. |
26
- | `fetchClient(Http, options?)` | The same client, built over `fetch` outside an Effect: its methods require nothing. |
27
-
28
- Exported types: `Binding`; `Any`, any binding; `Client`, a client's type: `Client<typeof Http>`; `MethodError`, what one of its calls fails with; `Options` of `make`, `LayerOptions` of `layer`, `ClientOptions` of `client` and `FetchClientOptions` of `fetchClient`.
29
-
30
- | Option | Meaning |
31
- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
32
- | `make`: `prefix` | Mount path of every route; defaults to `/api`. `/` mounts at the root; a trailing slash is dropped. |
33
- | `make`: `error` | Errors middleware around the routes sends, such as a limit before decoding: declared by every endpoint, so clients decode them. |
34
- | `make`: `authentication` | The descriptor of the protected actions' identity, `Authentication.make(...)`: required when the binding holds one. |
35
- | `layer`: `actions` | The bound actions it serves, among the implementations': `[GetUser]`. Defaults to every one they hold. |
36
- | `layer`: `middleware` | Native `HttpApiMiddleware` run around every route the layer serves, inside authentication, outside decoding; the first innermost. |
37
- | `client`: `baseUrl` | What routes are resolved against, such as `https://api.example.com`. Omitted: relative routes (the page's origin in a browser). |
38
- | `client`: `transformClient` | Wraps the native `HttpClient`. A bearer token: `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. |
39
- | `fetchClient`: `fetch` | What each call sends with; it also takes `client`'s options. Omitted: the global `fetch`, looked up on every call. |
40
-
41
- Routes: each action is served at `POST <prefix>/<action>`, operation ID `<action>`. A
42
- read-only action is a `POST` too: `readOnly` changes neither the method nor caching. The
43
- OpenAPI tag is the mount path's segments (`api`, `v2/api`), or `/` at the root. Every
44
- endpoint declares its action's errors, the binding's `error`, and the built-in
45
- `InvalidInput` (400), `Unauthenticated` (401) and `Forbidden` (403); a protected one also
46
- declares its descriptor's `error`, what the verifier may fail with.
47
-
48
- Layer failures and startup requirements come from the builders of the supplied
49
- implementations, and, where it serves a protected action, the descriptor's provider,
50
- `Authentication.layer`. The request services of each implementation's authorizer and of the
51
- handlers of the actions the layer serves
52
- remain router request requirements until middleware provided around the layer provides them,
53
- the protected actions' identity excepted, which the provider supplies; an action the binding
54
- leaves out owes nothing there. Router/platform services are also required
55
- ([guarantees.md](guarantees.md#dependency-lifetimes)).
56
-
57
- ## Canonical
58
-
59
- The binding, shared by the server and every client, names the authentication descriptor of its
60
- protected actions ([Authentication.md](Authentication.md#canonical)):
61
-
62
- ```ts example=binding.ts
63
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
64
- import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
65
- import { CurrentActor } from "./authorization.js";
66
- import { Double, GetUser, RenameUser, Status, WhoAmI } from "./contracts.js";
67
-
68
- // Browser-safe: how a remote caller proves it is CurrentActor, a bearer token unless
69
- // `security` names another native scheme. The verifier lives in authentication.ts. The literal
70
- // name identifies the verifier that may provide it.
71
- export const Login = Authentication.make("example.Login", CurrentActor);
72
-
73
- // Protected contracts get native bearer security (enforced and documented); `status`,
74
- // declared `caller: Action.Anyone`, gets none.
75
- export const Http = ActionHttp.make([Status, GetUser, RenameUser, Double, WhoAmI], {
14
+ | API | Purpose |
15
+ | ---------------------------------------- | ----------------------------------------------------------------------- |
16
+ | `make(actions, options?)` | Bind a list of actions; returns a `Binding`. |
17
+ | `Http.actions` | The bound actions. |
18
+ | `Http.error` | The errors every endpoint declares besides its action's own, as a list. |
19
+ | `Http.prefix` | Where the routes mount. |
20
+ | `Http.authentication` | Its protected actions' authentication descriptor, or `undefined`. |
21
+ | `Http.api` | The native `HttpApi`, for OpenAPI and Effect's own tools. |
22
+ | `layer(Http, implementations, options?)` | Mount the routes of the bound actions the implementations hold. |
23
+ | `client(Http, options?)` | An Effect of a typed client; requires the native `HttpClient`. |
24
+ | `fetchClient(Http, options?)` | The same client over `fetch`, built outside an Effect. |
25
+
26
+ Exported types: `Binding`; `Any`, any binding; `Client<typeof Http>`, a client's type;
27
+ `MethodError`, what one of its calls fails with; `Options` of `make`, `LayerOptions`,
28
+ `ClientOptions` and `FetchClientOptions`.
29
+
30
+ | Option | Meaning |
31
+ | --------------------------- | ------------------------------------------------------------------------------------------------- |
32
+ | `make`: `prefix` | Mount path of every route; defaults to `/api`. `/` mounts at the root. |
33
+ | `make`: `error` | What middleware around the routes answers with, one schema or a list; every endpoint declares it. |
34
+ | `make`: `authentication` | The protected actions' descriptor, `Authentication.make(...)`; required with a protected action. |
35
+ | `layer`: `actions` | The bound actions it serves: `[GetUser]`. Defaults to every one the implementations hold. |
36
+ | `layer`: `middleware` | Native `HttpApiMiddleware` around every route it serves, the first innermost. |
37
+ | `client`: `baseUrl` | What routes resolve against: `https://api.example.com`. Omitted: relative ones, a page's origin. |
38
+ | `client`: `transformClient` | Wraps the native `HttpClient`: `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. |
39
+ | `fetchClient`: `fetch` | Its `fetch`, beside `client`'s options. Omitted: the global `fetch`, looked up on every call. |
40
+
41
+ Each action is served at `POST <prefix>/<action>`, a `readOnly` one too. One without `success`
42
+ answers an empty 200, with no body and no content type, so a hand-written client must not parse
43
+ it. One without `input` still takes the body `{}`, typed as JSON. Statuses and bodies:
44
+ [guarantees.md](guarantees.md#wire-behavior).
45
+
46
+ ## Bind actions to routes
47
+
48
+ Call `ActionHttp.make` once, in a file that imports contracts and no server code, and export the
49
+ binding: the server, every client, the CLI and the tests import that one value.
50
+
51
+ ```ts
52
+ export const Http = ActionHttp.make([GetStatus, ...Notes, WhoAmI], {
53
+ prefix: "/api",
76
54
  authentication: Login,
77
55
  });
78
56
  ```
79
57
 
80
- ### Serving
81
-
82
- One layer serves the public and the protected actions: each protected route authenticates
83
- before its body is read, and `status` stays open.
84
-
85
- ```ts example=http.ts
86
- import { Layer } from "effect";
87
- import { HttpRouter, HttpServerResponse } from "effect/http";
88
- import { HttpApiSwagger, OpenApi } from "effect/http-api";
89
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
90
- import { authenticate } from "./authentication.js";
91
- import { Http } from "./binding.js";
92
- import { double, status, userActions } from "./handlers.js";
93
-
94
- // One layer: each protected route authenticates before decoding; `status` stays public.
95
- const routes = ActionHttp.layer(Http, [status, userActions, double]).pipe(
96
- Layer.provide(authenticate),
97
- );
98
-
99
- const documentation = Layer.mergeAll(
100
- HttpRouter.add(
101
- "GET",
102
- "/api/openapi.json",
103
- HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api)),
104
- ),
105
- HttpApiSwagger.layer(Http.api, { path: "/docs" }),
106
- );
107
-
108
- export const layer = Layer.mergeAll(routes, documentation);
109
- ```
58
+ Files: [quickstart/src/Api.ts](../examples/quickstart/src/Api.ts) (public actions, no options),
59
+ [notes-service/src/Api.ts](../examples/notes-service/src/Api.ts) (protected actions),
60
+ [kitchen-sink/src/Api.ts](../examples/kitchen-sink/src/Api.ts) (`prefix`, `error`, two bindings).
110
61
 
111
- Serve with `HttpRouter.serve(layer).pipe(Layer.provide(NodeHttpServer.layer(createServer, { port })), Layer.launch)`, and set a request body limit ([guarantees.md](guarantees.md#wire-behavior)). Put a job or an agent the process runs inside `layer`, not beside `HttpRouter.serve`, so it shares the routes' builders and services rather than building its own ([dependency lifetimes](guarantees.md#dependency-lifetimes)).
62
+ - To keep an action off HTTP, leave it out of the binding; its implementation may still serve
63
+ it as a tool or a command.
64
+ - A binding holding a protected action names its descriptor
65
+ ([Authentication.md](Authentication.md#declare-how-callers-sign-in)), and has only one: a
66
+ second identity gets a second binding with its own `prefix`. A public action may be in both.
67
+ - `error` declares what middleware answers with on any route, such as a quota's 429. An error a
68
+ handler fails with goes in its action's `error`
69
+ ([Action.md](Action.md#fail-with-declared-and-built-in-errors)).
70
+ - Action names are unique within a binding. Split a large API, or two actions sharing a name,
71
+ into one binding per area, each with its own `prefix` and client; one host serves them all.
112
72
 
113
- A large API, or two actions that would share a name, gets one binding per area, each with its
114
- own `prefix` and client, served by one host, and each documented on its own where they share a
115
- name ([Failure modes](#failure-modes)):
73
+ ## Serve the routes
116
74
 
117
- ```ts
118
- export const Billing = ActionHttp.make([Invoice, Refund], { prefix: "/api/billing" });
119
- export const Accounts = ActionHttp.make([GetUser, RenameUser], { prefix: "/api/accounts" });
75
+ Give `ActionHttp.layer` the binding and the implementations, merge it with the host's other
76
+ routes, provide the descriptor's provider
77
+ ([Authentication.md](Authentication.md#verify-credentials)), and serve the result with
78
+ `HttpRouter.serve`.
120
79
 
121
- // Each layer takes the whole list, and serves its binding's actions among it.
122
- const routes = Layer.mergeAll(
123
- ActionHttp.layer(Billing, [billing, accounts]),
124
- ActionHttp.layer(Accounts, [billing, accounts]),
125
- );
80
+ ```ts
81
+ const api = ActionHttp.layer(Http, implementations);
126
82
 
127
- // One document for both, when no action name repeats across them:
128
- // HttpApi.make("app").addHttpApi(Billing.api).addHttpApi(Accounts.api)
83
+ export const routes = Layer.mergeAll(api, mcp, health, openApi).pipe(Layer.provide(authenticate));
129
84
  ```
130
85
 
131
- ### Client
132
-
133
- `client(Http)` is Effect's native `HttpApiClient` with one method per action, taking the
134
- action's input directly and answering with its decoded success: `client.<action>(input)`.
135
-
136
- ```ts example=client.ts
137
- import { Console, Effect } from "effect";
138
- import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
139
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
140
- import { Http } from "./binding.js";
141
-
142
- const lookup = Effect.gen(function* () {
143
- const client = yield* ActionHttp.client(Http, {
144
- baseUrl: "http://127.0.0.1:3000",
145
- transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken("alice")),
146
- });
86
+ Files: [quickstart/src/routes.ts](../examples/quickstart/src/routes.ts) and
87
+ [quickstart/src/server.ts](../examples/quickstart/src/server.ts);
88
+ [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts) and
89
+ [notes-service/src/server.ts](../examples/notes-service/src/server.ts).
90
+
91
+ - Pass every surface the same list of implementations: the layer serves the binding's actions
92
+ among them.
93
+ - A service a handler or an authorizer reads per request stays owed by the layer until router
94
+ middleware around it, `HttpRouter.provideRequest` or the layer's own `middleware` provides it.
95
+ Router middleware is the one that reads the request, such as a header:
96
+ `HttpRouter.middleware<{ provides: Tenant }>()((route) => …)`, given as
97
+ `Layer.provide(resolveTenant.layer)`
98
+ ([kitchen-sink/src/workspaces/tenant.ts](../examples/kitchen-sink/src/workspaces/tenant.ts)).
99
+ Without the type argument it runs and the layer still owes the service.
100
+ `HttpRouter.provideRequest(layer)` builds its layer once, at startup, and gives every request
101
+ that one value: never an identity or a tenant
102
+ ([Action.md](Action.md#say-who-may-call-an-action)).
103
+ - Give the server a `host`: `NodeHttpServer.layer(createServer, { port })` without one listens on
104
+ every interface. The examples bind `127.0.0.1`.
105
+ - Two listeners are two `HttpRouter.serve` calls, each building its routes on its own. What they
106
+ share is provided outside both
107
+ ([Action-in-process.md](Action-in-process.md#share-one-build-across-servers),
108
+ [kitchen-sink/src/server.ts](../examples/kitchen-sink/src/server.ts)).
109
+
110
+ Set a request body limit: a body has none unless the host provides one to `HttpRouter.serve`, as
111
+ below. It covers every route of the server, MCP endpoints included, except a body a route reads
112
+ as a stream. On Node (`NodeHttpServer`), a request over it gets no response, not a 413: its connection is
113
+ closed. `HttpRouter.toWebHandler` ignores it, so limit bodies in the platform serving the handler.
147
114
 
148
- const status = yield* client.status();
149
- const user = yield* client.getUser({ id: "1" });
150
- const identity = yield* client.whoAmI();
115
+ ```ts
116
+ Layer.provide(Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1))),
117
+ ```
151
118
 
152
- return { status, user, identity };
153
- });
119
+ An `ErrorReporter` the server runs with is also given, with severity `Info`, what a handler
120
+ or `authorize` fails with and an input that does not decode, a declared 404, a 403 or a 400:
121
+ Effect's `HttpApi` reports a route's failures, of which a refused credential is none. MCP and
122
+ RPC report a defect alone. To keep a declared error out of a tracker, give its class
123
+ `override readonly [ErrorReporter.ignore] = true`; a built-in error takes no mark, so drop
124
+ `Info` there.
125
+
126
+ ### Split a binding across layers, and add middleware
127
+
128
+ Several layers may serve one binding, each with its own `actions` and `middleware`; serve each
129
+ action from exactly one. Files:
130
+ [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) (`api`),
131
+ [kitchen-sink/src/identity/quota.ts](../examples/kitchen-sink/src/identity/quota.ts).
132
+
133
+ | Where | Runs | Use it for |
134
+ | ---------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- |
135
+ | Global router middleware | before routing, on every route | a Host or Origin policy, CORS |
136
+ | Router middleware provided to a `layer` call | around its routes, before authentication | a per-request service; a limit on unauthenticated requests |
137
+ | The layer's `middleware` | after authentication, before decoding | a limit keyed by the caller, counting invalid input too |
138
+ | The handler ([Action.md](Action.md#authorize-calls)) | after decoding and the authorizer | a limit that holds on every surface, MCP included |
139
+
140
+ - A layer middleware requiring the identity needs a layer serving protected actions only. Serve
141
+ the public actions from a second layer without it.
142
+ - Router middleware answering with a binding error encodes and sends it itself,
143
+ `HttpServerResponse.schemaJson(QuotaExceeded)(error, { status: 429 })`, at the error's
144
+ `httpApiStatus`, or 422 without one: at any other status the client fails with an
145
+ `HttpClientError`.
146
+ - A request policy that must run before authentication and OAuth discovery:
147
+ [Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication).
148
+
149
+ ### Publish the OpenAPI document
150
+
151
+ Give `Http.api`, a plain `HttpApi`, to `OpenApi.fromApi`, `HttpApiSwagger.layer` or
152
+ `HttpApiScalar.layer`, on routes of the host's own. Files:
153
+ [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts) (`openApi`),
154
+ [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) (`documentation`).
155
+
156
+ - The document's route is public unless you put middleware around it.
157
+ - A header that middleware reads is not in the document: its operations list no `parameters`.
158
+ Add it by rewriting the document,
159
+ `OpenApi.fromApi(Http.api.annotate(OpenApi.Transform, (document) => …))`, or make the value an
160
+ input.
161
+ - The document and the clients list every action of the binding, served or not. An action no
162
+ layer serves answers 404.
163
+ - An OAuth protected resource is documented as a bearer scheme: Effect has no OAuth scheme
164
+ ([Authentication.md](Authentication.md#publish-oauth-discovery)).
165
+ - Serve the binding with `ActionHttp.layer`, never with the native `HttpApiBuilder`.
166
+ - One document over several bindings, `HttpApi.make("app").addHttpApi(A.api).addHttpApi(B.api)`,
167
+ works only when their prefixes differ and no action name repeats. Otherwise document each alone.
168
+
169
+ ## Call the API from Effect
170
+
171
+ `ActionHttp.client` yields a client with one method per action.
154
172
 
155
- // Every endpoint declares the built-in refusals, so the 401 the authentication
156
- // renders arrives as a typed `Unauthenticated`, not a decode error.
157
- const refused = Effect.gen(function* () {
173
+ ```ts
174
+ Effect.gen(function* () {
158
175
  const client = yield* ActionHttp.client(Http, { baseUrl: "http://127.0.0.1:3000" });
159
176
 
160
- return yield* Effect.flip(client.whoAmI());
161
- });
162
-
163
- await Effect.runPromise(
164
- Effect.all([lookup, refused]).pipe(
165
- Effect.tap(Console.log),
166
- Effect.provide(FetchHttpClient.layer),
167
- ),
168
- );
169
- ```
170
-
171
- The argument may be omitted when `{}` is a valid encoded input, such as for an action declared
172
- without `input` (`client.whoAmI()`) or one whose fields are all optional or have a decoding
173
- default, in a struct or a class; omitting it sends the input `{}` decodes to. Other headers also go through `transformClient`
174
- (`HttpClient.mapRequest(HttpClientRequest.setHeader("x-agent", agent))`). The options are the
175
- native `HttpApiClient.make` options except `transformResponse`, which may change a call's
176
- success, failure or required services, which the method types cannot follow; use
177
- `transformClient`, or the native client.
178
-
179
- `transformClient` sees every response before the contract decodes it, so a status is read
180
- there whatever its body: a page told that its session ended, by the contract's 401 or a
181
- proxy's, with
182
- `HttpClient.tap((response) => Effect.sync(() => { if (response.status === 401) signedOut(); }))`.
183
- The call still fails as typed, with `Unauthenticated` or an `HttpClientError`.
184
-
185
- ### Promise callers
186
-
187
- Code that holds a client outside an Effect, such as a browser app, builds it once with
188
- `fetchClient(Http, options?)`: `client` over `fetch`, its methods the same Effects, requiring
189
- nothing. Each call runs with `Effect.runPromise`.
190
-
191
- ```ts example=promise-client.ts
192
- import { Effect } from "effect";
193
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
194
- import { Http } from "./binding.js";
195
-
196
- // Built once, outside any Effect: its methods need nothing more.
197
- export const api = ActionHttp.fetchClient(Http);
198
-
199
- // A promise per call. A declared error rejects it as its decoded value, so
200
- // `error instanceof UserNotFound` holds in a `catch`.
201
- export const userName = async (id: string): Promise<string> =>
202
- (await Effect.runPromise(api.getUser({ id }))).name;
177
+ yield* Console.log(yield* client.greet({ name: "Ada" }));
178
+ }).pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain);
203
179
  ```
204
180
 
205
- A declared error rejects the promise as its decoded value, so `catch` code matches it with
206
- `instanceof`. A call sends with the `fetch` option, such as one sending the page's cookies,
207
- `(input, init) => fetch(input, { ...init, credentials: "include" })`; without it, with the
208
- global `fetch` as the call finds it, so a test's stub installed after the client is built is
209
- used. A `fetch` of your own keeps `init.signal`, which aborts the request when its call is
210
- interrupted: a deadline is `Effect.timeout` on the call,
211
- `api.getUser({ id }).pipe(Effect.timeout("5 seconds"))`, not a signal that replaces it.
212
- In a browser, relative routes resolve against the page; elsewhere, give `baseUrl`.
213
-
214
- ## Rules
215
-
216
- - The binding decides what HTTP serves: the actions passed to `make`, and no others; an action has no HTTP switch. To keep an action off HTTP, leave it out of the list. Its implementation may still hold it for MCP, a Toolkit or `ActionCli.make`, which serve the actions their `actions` option lists, or every action of the implementations without it.
217
- - Action names are unique within a binding. Two bindings with different prefixes may reuse a name and be served side by side, but not combined into one `HttpApi`.
218
- - `layer(Http, implementations)` mounts the routes of the binding's actions among the implementations it receives, matched by identity: the exact contract values passed to `make`. Their other actions get no route, and their names are not checked. `actions` narrows it to the listed ones, each of the binding's and held by an implementation, as on every surface ([guarantees.md](guarantees.md#names)); an implementation holding none of those served is left out, and not built. Implementations holding none of the binding's actions at all are refused when `layer` is called, as the wrong implementations or the wrong binding. An equal-looking copy of a bound action is not the bound action, so it is not served.
219
- - An action may be served once per call; two implementations there may both hold an action the binding leaves out. An action no layer serves still appears in `Http.api`, OpenAPI and clients, and answers 404.
220
- - The binding is plain data: `layer` and `client` read everything from its fields, so a copy of the binding, or one made by another installed copy of the package, serves the same.
221
- - One layer serves a binding's public and protected actions, from one implementation or several. Each action's contract decides: a protected route is authenticated by the binding's descriptor, before its body is read ([guarantees.md](guarantees.md#authorization)), and a public one is open, ignoring any credential. A binding holding a protected action names its descriptor, and the layer serving it requires the descriptor's provider ([Authentication.md](Authentication.md#rules)); a layer serving only public actions, of a binding or an implementation holding protected ones too, requires none.
222
- - Router middleware provided to a `layer` call covers that call's routes, before their authentication, and no others. Several layers over one binding, such as one per area of the host, still share one binding, one document and one client.
223
- - `middleware` takes native `HttpApiMiddleware` services, which run around every route the layer serves, inside a protected route's authentication and outside its content-type and schema checks, so they see a decoding failure as the failure of `route`. The first listed is innermost, as native `.middleware` chaining is. The layer requires each one's service, and owes what each requires, less what one further out provides, in that order; a list not written as a tuple, or options whose `middleware` may be absent, owe what any of them requires, and provide nothing. `layer` takes options or none; an explicit type argument naming middleware requires the options argument, and a misspelled option beside `middleware` is a type error. A reusable options value names its tuple, `ActionHttp.LayerOptions<readonly [typeof Audit]>`, or is written `as const`: bare `ActionHttp.LayerOptions` takes no middleware.
224
- - A layer middleware may require the identity only where every action the layer serves is protected: on a layer serving a public action too, the identity stays owed. List the protected actions in its `actions`, and serve the public ones from another layer: `ActionHttp.layer(Http, users, { actions: [RenameUser], middleware: [Audit] })` beside `ActionHttp.layer(Http, users, { actions: [GetUser] })`. Options whose `actions` may be absent narrow nothing.
225
- - A layer middleware fails only with the binding's `error` or a built-in error, and needs no client: anything else is a type error, since clients decode only what the binding and every endpoint declare ([Binding errors](#binding-errors)). A middleware refusing a caller, such as an address allowlist, fails with `Action.Forbidden`, which needs no binding error. It is where a limit keyed by the caller runs before decoding, so it counts input that does not decode too; a limit after decoding, on every surface, is the handler's, failing with an error the contract declares ([Action.md](Action.md#implementations)). A middleware observing failures uses `Effect.onExit`, not `Effect.map`, which a failure skips. A step-up refusal is answered as it leaves the layer's middleware: one that recovers from it keeps its own response, and one that turns it into another error the binding declares answers with that error ([guarantees.md](guarantees.md#authorization)). MCP endpoints take no layer middleware.
226
- - Authorization, builders, request-time services and headers follow [guarantees.md](guarantees.md): `ActionHttp` itself sets no header.
227
- - `Http.api` is a plain `HttpApi`, which Effect's own tools document and call: `OpenApi.fromApi`, `HttpApiSwagger.layer`, `HttpApiScalar.layer`, `Http.api.addHttpApi(other)` to combine with other APIs, `HttpApiClient.make`. Serve it with `layer`, not the native `HttpApiBuilder`: its protected endpoints carry the descriptor's native security middleware, which `layer` satisfies with the provider and the native builder lacks.
228
- - The endpoints are one top-level group named after the mount path, so the native client exposes them as `client.<action>({ payload })`.
229
- - Bindings combine into one host API with the native `addHttpApi` method, `HttpApi.make("app").addHttpApi(Http.api)`, for one document or one native client, only when their prefixes differ and no action name repeats across them. An operation ID is the action name, and a group is keyed by its mount path. Serving several bindings with `layer` has neither limit.
230
- - Serve the OpenAPI document as a native route: `HttpRouter.add("GET", "/api/openapi.json", HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api)))`. It documents every bound action, not only the served ones. It is a plain route: middleware provided to its layer covers it, and nothing covers it otherwise.
231
- - A route runs only a request typed as JSON: any other type, or none, is a 415, after authentication on a protected route, as on an MCP endpoint, whatever encoding the input is annotated with, such as `HttpApiSchema.asFormUrlEncoded()`, which `HttpApi` would read and no other surface does. So no request a page on another origin can send without a CORS preflight reaches a handler, whatever credentials, cookies included, it carries; one typed as JSON is preflighted, and the host's CORS policy decides.
232
- - A media field, `Action.Image`, is JSON in the body: `{ "data": "<base64>", "mimeType": "image/png" }`, as Effect's JSON codec encodes a `Uint8Array`. The OpenAPI document lists `data` as `type: string`, `format: byte`, `contentEncoding: base64`, as Effect's JSON Schema does, and `client` decodes it to a `Uint8Array`. Only MCP lifts it into a block of its own ([ActionMcp.md](ActionMcp.md#media)).
233
- - Wire format: [guarantees.md](guarantees.md#wire-behavior). Spans and log annotations: [guarantees.md](guarantees.md#observability).
234
-
235
- ### Binding errors
236
-
237
- - The binding's `error`, one schema or a list, is what middleware answers with, on any endpoint, router middleware around the routes and the layer's own `middleware` alike: `ActionHttp.make(actions, { error: RateLimited })`. A binding holds it as a list, `Http.error`. Every endpoint declares them, OpenAPI shows them, and every client decodes them.
238
- - Middleware sends one as the binding declares it, at the status its `httpApiStatus` states, or 422 without one: for `class RateLimited extends Schema.TaggedError<RateLimited>()("RateLimited", {}, { httpApiStatus: 429 }) {}`, `HttpServerResponse.schemaJson(RateLimited)(error, { status: 429 })`. A union with its own `httpApiStatus` uses that status for every member; otherwise, each member of a plain union is declared at its own status, or 422 without one.
239
- - No handler fails with them: it serves every surface, and only HTTP declares them. An action whose handler fails with one lists it in its own `error`, a limit after decoding included ([guarantees.md](guarantees.md#authorization)). A layer middleware fails with them as a native `HttpApiMiddleware` does, declaring the error, `{ error: RateLimited }`, which the binding declares too.
240
- - They may not reuse a built-in error's tag: `make` refuses such a binding.
241
- - Options that may leave `error` out, such as a value annotated `ActionHttp.Options<readonly [typeof RateLimited]>`, declare those errors or none, `readonly [typeof RateLimited] | []`, as the binding has at run time: clients decode each that may arrive, and a layer middleware failing with one is a type error, since the binding may lack it. Give `error` inline, or in a value whose type requires it. Explicit type arguments naming `error` require the options argument, which alone holds them. A layer middleware may fail only with an error the binding surely declares: one in a slot of its own in a list of fixed length, inline `error: RateLimited`, not in an array of unknown length, `[RateLimited, ...more]` included, which may be empty, a slot that may hold either of several, or options whose `error` may be either of two lists.
242
-
243
- ### Security
244
-
245
- - OpenAPI security follows each contract's `caller`. Every protected endpoint carries the descriptor's native security middleware, the one that enforces it: `OpenApi.fromApi`, `HttpApiSwagger` and `HttpApiScalar` show its scheme in `components.securitySchemes`, keyed by the descriptor's name as it is, `example.Login`, and `[{ "example.Login": [] }]` as the operation's requirement, and a public endpoint states `security: []`. A combined document keeps each binding's: distinct descriptors have distinct names, so their schemes never share a key ([Authentication.md](Authentication.md#rules)).
246
- - The document and the routes cannot disagree: no option documents a scheme the routes do not enforce, or a public action the routes protect.
247
- - Effect has no OAuth or OpenID Connect scheme, so an OAuth protected resource is documented as a bearer scheme; its discovery is the provider's ([Authentication.md](Authentication.md#rules)).
248
- - Clients read none of it: a client sends what `transformClient` adds, whatever the document says.
249
-
250
- ### Client methods
251
-
252
- - A method fails with what the native client fails with, but for input: input that does not encode is `InvalidInput` with its `issues`, as every surface answers it, and nothing is sent. Declared errors arrive as their decoded values: the action's own, the binding's, and the three built-in errors every endpoint declares. Match them with `Effect.catchTag`.
253
- - Anything the contract does not account for is Effect's own error:
254
- - `HttpClientError` with `response` `undefined`: the server could not be reached.
255
- - `HttpClientError` with `reason._tag` `DecodeError`: the server answered with a status no schema declares, such as the empty 500 of a defect.
256
- - `HttpClientError` with `StatusCodeError`: a declared status whose body did not decode.
257
- - `SchemaError`: the success body did not decode.
258
- - The library interprets no status. Which failures mean "signed out" or "try again" is the caller's decision.
259
- - Nothing is retried. A failed write may or may not have happened; only a declared error says what the server did.
260
- - Every action of the binding has a method, whether or not a server serves it. An unserved action answers 404 with no body, so its method fails with `HttpClientError`: `DecodeError`, or `StatusCodeError` when the action declares a 404 error, whose body the empty response is not.
261
- - A given argument is sent as given: `null` or `undefined` is the input itself, for a schema that accepts it.
262
- - The client holds no connections or timers. `client` builds the native client once from the `HttpClient` in context.
263
- - The native client stays available: `HttpApiClient.make(Http.api)` has the same routes, with methods taking `{ payload }`.
264
- - In a browser, import the binding from a module with no server code, so the bundle keeps the client alone ([setup.md](setup.md#browser)).
265
- - In tests, provide `Testing.layer(routes)` instead of a network client; `baseUrl` may be left out ([Testing.md](Testing.md)). `Action.client` has the same methods in process, for what an implementation does without the wire ([Action.md](Action.md#clients)).
266
-
267
- ### Built-in errors
268
-
269
- - Every endpoint declares the built-in errors ([guarantees.md](guarantees.md#wire-behavior)). `client`, a remote `ActionCli` command and the native `HttpApiClient` decode them as typed failures, and OpenAPI shows them on every operation. They cannot be left out; the binding's `error` add to them.
270
- - `InvalidInput`'s `message` is the schema's own description of every issue, and `issues` lists each by its path: `{"_tag":"InvalidInput","message":"Expected string\n at [\"name\"]","issues":[{"path":["name"],"message":"Expected string"}]}`.
271
-
272
- ## Failure modes
273
-
274
- - Route returns 404: the action is not in the binding, its implementation was never passed to a `layer` call, or the path lacks the prefix. An implementation's action is served only if the binding holds that very value: an equal-looking copy, such as a test declaring the contract again, is not.
275
- - `No action of these implementations is in this HTTP binding: x` thrown by `layer`: none of the implementations' actions was passed to this binding's `make`, so they are the wrong implementations or it is the wrong binding. `x (another contract)` is an action of a bound name that is not the bound value, as a second copy of the contracts module makes: implement the exact contract value the binding received. Matching names and schemas do not establish identity.
276
- - `Authentication "<name>": the binding's descriptor is not its provider's`, a defect when `layer` builds: the binding and `Authentication.layer` were given two descriptors of one name. Build both from one descriptor ([Authentication](Authentication.md#failure-modes)).
277
- - `Listed in actions, but the binding does not hold it: <names>`, or `but no implementation holds it`, thrown by `layer`: those actions are not among the binding's or the implementations', by identity. `(another contract)` marks one whose name the binding holds.
278
- - `Duplicate served action: <name>`: one `layer` call received two implementations of the same bound action.
279
- - `Duplicate middleware: <key>` thrown by `layer`: its `middleware` lists one middleware twice, which a native endpoint would run once. List it once.
280
- - `Method 'POST' already declared for route '<prefix>/<action>'` when the host builds: two `layer` calls serve the same action, such as one implementation given to two layers of one binding. Serve each action in one call: one layer serves public and protected actions alike, or give each layer its own `actions`.
281
- - `ActionHttp binding: error _tag "Forbidden" is built in, and declared on every surface` thrown by `make`: a binding error has a built-in tag, or is a built-in error. Drop a built-in error, which every endpoint declares already; rename an error of your own.
282
- - `Duplicate action: <name>` thrown by `make`: two actions share a name, or one action value is listed twice. Rename one, or bind it under another prefix.
283
- - `Duplicate OpenAPI operationId: <name>` from `OpenApi.fromApi` on a combined API: two combined bindings have an action of that name. Rename one, or document each binding on its own.
284
- - A combined document or native client lacks one binding's actions: two combined bindings share a prefix, so one group replaced the other. Give each its own prefix, or bind the actions together.
285
- - `Provider<CurrentActor, "example.Login">` among the layer's requirements, or `Type 'Provider<...>' is not assignable to type 'never'` where the server is launched: the layer serves a protected action and its descriptor's provider is not provided. Provide it, `Layer.provide(authenticate)` ([Authentication.md](Authentication.md#failure-modes)). Nothing else stands in for it.
286
- - `Type 'X' is not assignable to type 'never'` where the server is launched, or `Request<"Requires", X>` in the layer's type: a handler or the authorizer yields a request service `X` that no middleware around the layer provides. Provide it with router middleware, or `HttpRouter.provideRequest`; never an identity at startup. `X` the identity on a layer serving public actions: a public handler reads it, or a layer middleware requires it. No public route gets an identity: make the action protected, or serve the middleware's protected actions from a layer of their own ([Rules](#rules)).
287
- - `No overload matches this call` at `make`, its last overload naming `"Protected actions take options naming their authentication"`, or a type error naming `authentication`: the binding holds a protected action and names no descriptor, or one of another identity. Give `{ authentication: Login }`, never `caller: Action.Anyone` on the contract to silence it.
288
- - `Argument of type 'Options<…> | undefined' is not assignable` at `make`: a helper forwards options that may be absent. `make` takes options or none, two forms, so pass `options ?? {}`.
289
- - A type error on `middleware` naming `"Layer middleware fails only with the binding's errors and needs no client"`: a middleware declares an error neither the binding nor the built-ins hold, or a client counterpart. Add the error to the binding's `error`, or, for a limit after decoding on every surface, move it to the handlers, declaring it on their actions.
290
- - A route answers without credentials: its contract is `caller: Action.Anyone`. Make it protected; its route is then authenticated, whatever its handler reads.
291
- - A protected route answers 401 to a request missing its content type or sending invalid input: it presented no credential that verifies, and authentication runs before anything reads the body. Send the token, then the content type and the input are checked.
292
- - Swagger shows no Authorize button: the binding holds no protected action.
293
- - `Service not found: effect-actions/Authentication/Security/…` when the host builds: `Http.api` of a binding holding a protected action is served with the native `HttpApiBuilder`. Serve it with `layer`.
294
- - 400 `InvalidInput` on a valid-looking request: its `message` names each field that did not decode, or that the action does not declare, such as a misspelling. An action without input takes only `{}`, and a body.
295
- - 415 `Unsupported content-type: <type>` (`none` when it has none): the request is not typed as JSON. It has another content type, such as `fetch`'s default `text/plain` for a string body, or none, as for a `Blob` body. Send `Content-Type: application/json`, as the clients do.
296
- - Empty 500: a defect, or a handler result that does not match the success schema. The cause is in the server's logs.
297
-
298
- ### Client methods
299
-
300
- - Fails with `HttpClientError` whose `reason._tag` is `DecodeError` for a status such as 429: the server answered with a status no schema declares. Declare that error on the binding, `make`'s `error`, when router middleware answers with it; on the action when its handler does; or handle the native error. The built-in 400, 401 and 403 always decode, as long as their body is the built-in error's JSON.
301
- - Fails with `HttpClientError` whose `reason._tag` is `InvalidUrlError` outside a browser: `baseUrl` is omitted, and there is no page to resolve relative routes against. Set `baseUrl`.
302
- - Type error listing `HttpClient` as an unsatisfied requirement of `client`: provide one, such as `FetchHttpClient.layer`.
303
- - Property does not exist on the client: the action is not in the binding.
304
- - `Expected 1 arguments`: `{}` is not a valid encoded input for the action, so it needs its input.
305
- - Type error passing `undefined` to a method whose argument may be omitted: leave the argument out instead.
306
- - Fails with `InvalidInput` `Expected Filters` given a plain object, which TypeScript may let through: the input is a class, `Filters`, which encodes only its instances. Pass `new Filters({ ... })`, or leave out an argument whose fields are all optional.
307
- - Type error passing `{ payload: ... }`: that is the native client's shape. These methods take the input itself.
181
+ Files: [quickstart/src/client.ts](../examples/quickstart/src/client.ts),
182
+ [kitchen-sink/src/seed.ts](../examples/kitchen-sink/src/seed.ts) (`baseUrl` from `Config`,
183
+ headers, errors by tag).
184
+
185
+ - Outside a browser, set `baseUrl`. A token or any other header goes through `transformClient`.
186
+ Where no client is built by hand, as under `ActionCli.remote`, transform the `HttpClient`
187
+ itself (`connection` in [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts)).
188
+ - Leave the argument out when `{}` is a valid input. A given argument is the decoded input, so
189
+ it states a defaulted field.
190
+ - A declared error is a typed failure, as its decoded value: the action's own, the binding's,
191
+ the verifier's, and the built-in `InvalidInput`, `Unauthenticated` and `Forbidden`. Handle
192
+ each with `Effect.catchTag`.
193
+ - Anything else is Effect's own: an `HttpClientError` when the server could not be reached or
194
+ answered with a status or body the binding does not declare, such as a defect's empty 500; a
195
+ `SchemaError` when the success does not decode.
196
+ - The client interprets no status and retries nothing. A failed write may have happened; only a
197
+ declared error says what the server did.
198
+ - A handler calling another service through this client maps that service's `Unauthenticated`
199
+ and `Forbidden` before failing with them
200
+ ([Action.md](Action.md#fail-with-declared-and-built-in-errors)).
201
+ - In a test, provide `Testing.layer(routes)` instead of a network client and leave `baseUrl` out
202
+ ([Testing.md](Testing.md#test-routes-in-memory),
203
+ [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts)).
204
+
205
+ ## Call the API from a browser
206
+
207
+ Code outside an Effect builds the client once with `ActionHttp.fetchClient` and runs each call
208
+ with `Effect.runPromise`. Files:
209
+ [notes-service/src/browser.ts](../examples/notes-service/src/browser.ts),
210
+ [kitchen-sink/src/sdk.ts](../examples/kitchen-sink/src/sdk.ts) (a `baseUrl`, two headers).
211
+
212
+ - A page on another origin passes `baseUrl`, and the server must allow that origin with
213
+ `HttpRouter.cors`
214
+ ([kitchen-sink/src/request-policy.ts](../examples/kitchen-sink/src/request-policy.ts), `cors`).
215
+ - An unhandled declared error rejects the promise as its decoded value, so
216
+ `error instanceof Action.Unauthenticated` works in a `catch`. Handle an expected one by its tag
217
+ before `Effect.runPromise`.
218
+ - A page on the API's own origin sends its cookies already. From another origin, pass `fetch`:
219
+ `(input, init) => fetch(input, { ...init, credentials: "include" })`, and allow credentials in
220
+ the server's CORS, `HttpRouter.cors({ allowedOrigins, credentials: true })`. A `fetch` of your
221
+ own keeps `init.signal`, which aborts the request when its call is interrupted: a deadline is
222
+ `Effect.timeout` on the call.
223
+ - A route answers a request not typed as JSON with 415, so what another site's page can send
224
+ without a CORS preflight, such as a form post carrying the user's cookie, never reaches a
225
+ handler; the server's CORS policy decides the rest.
226
+ - The page imports the binding from a module with no server code
227
+ ([setup.md](setup.md#bundle-for-a-browser)).
@@ -0,0 +1,31 @@
1
+ # ActionMcp failure modes
2
+
3
+ Search for a fragment of the message. The tasks are in [ActionMcp.md](ActionMcp.md).
4
+
5
+ - `MCP tool input must be one object with keys, such as a struct: <name>, ...`: those actions' input is a union, an array, a scalar, or an object without keys such as `Schema.Struct({})`. Wrap it in a field, `input: { notification: Schema.Union([Email, Sms]) }`, omit `input` for no arguments, or leave the action out of `actions`.
6
+ - `MCP media must be the success or an array of it, or a top-level field of a struct success, one, optional or a required array: <name>, ...`: those actions hold an `Action.Image` no tool can send as a block. Make it a top-level field of a `Schema.Struct` success, give an optional array as a required one, empty when absent, or leave the action out of `actions`.
7
+ - `Duplicate MCP tool: <name>`: two implementations serve actions of one name. Split the endpoint or rename one. `actions` tells apart only separate contracts sharing a name; of two implementations of one contract, pass one.
8
+ - `Duplicate MCP tool: <name>, claimed by an action and a native feature`, a defect when the endpoint builds: a native tool among `features` has an action's name. Rename it, or leave the action out of `actions`.
9
+ - `Listed in actions, but no implementation holds it: <names>`: those actions are not among the implementations', by identity. `(another contract)` marks one whose name an implementation holds, as a second copy of the contracts module makes. Pass the implementation holding it, or list the contract it implements.
10
+ - `Property 'authentication' is missing` on `layerHttp`'s options: the endpoint serves a protected action and names no descriptor. `Type 'Other' is not assignable to type 'CurrentUser'` on `authentication`: the descriptor is another identity's. Give the protected actions' descriptor.
11
+ - `Authentication "<name>": the binding's descriptor is not its provider's`, a defect when `layerHttp` builds: give the endpoint the descriptor its provider was built from ([Authentication-failures.md](Authentication-failures.md)).
12
+ - `Type 'X' is not assignable to type 'never'` where the server is launched, or `Request<"Requires", X>` in an MCP endpoint's type: a handler or an authorizer yields a request service no middleware around the endpoint provides. Provide it with router middleware; never an identity at startup. `X` the identity, on an endpoint serving public tools: a public handler reads it, which no public tool gets, signed in or not. Make that action protected.
13
+ - `Type 'CurrentUser' is not assignable to type 'never'` where the server is launched, with `CurrentUser` among an MCP endpoint's startup requirements, not its `Request<"Requires", ...>`: a native feature reads a request service, which features never receive. Serve that content as an action.
14
+ - `Type 'CurrentUser' is not assignable to type 'never'` at a stdio program's `runMain`: it serves a protected tool, whose caller the host supplies. Provide the identity around `runStdio`.
15
+ - A native resource, prompt or toolkit layer is never served (`resources/list` is empty, `prompts/list` is not found, `tools/list` lists only the actions): an endpoint serves only the native layers passed as its `features`; one merged beside it builds without error. Pass it as `features`.
16
+ - A prompt or resource demands credentials: on an endpoint naming `authentication`, every prompt and resource request needs one. Serve them on an endpoint without protected tools.
17
+ - A public tool's call answers 401: a public tool needs no credential, but one a request presents is still verified, and this one failed. Send a valid one, or none.
18
+ - A protected tool's call answers 401 to a signed-in client after a while: its token expired. The client refreshes it, or signs in again, on the challenge's `invalid_token`.
19
+ - An MCP client gets an `isError` refusal, not the 401 or 403 it re-authorizes on: the handler sent a notification first, which starts the response, so its refusal is a tool result. Refuse in `authorize`.
20
+ - A client that opens with `initialize` gets 401 where it lists the tools or calls a public one, on an endpoint that also serves protected tools: a session's every request authenticates. Sign the client in, or serve the public tools on an endpoint of their own.
21
+ - A request over HTTP answers 400, `MCP-Session-Id header is required; send initialize to start a session`: its `MCP-Protocol-Version` header is absent or names a 2025 revision, so it is a session's request, and it follows no `initialize`. One answering 404 names a session that ended, or one of a process since restarted: the client opens another.
22
+ - A hand-written HTTP request answers 400 with JSON-RPC error `-32602`, `Required request metadata is missing` or `io.modelcontextprotocol/clientCapabilities request metadata is required`: it names 2026-07-28 in `MCP-Protocol-Version` and its `params._meta` lacks `io.modelcontextprotocol/protocolVersion`, for the first message, or `io.modelcontextprotocol/clientCapabilities`, for the second. Build it with `Testing.mcpRequest`, or copy the `curl` of [ActionMcp.md](ActionMcp.md#serve-tools-over-stdio).
23
+ - A hand-written HTTP request answers 400 with JSON-RPC error `-32020`, `MCP-Protocol-Version header is required`, `Mcp-Method header does not match request method` or `Mcp-Name header does not match request parameters`: those headers repeat a 2026-07-28 request's revision, method and `tools/call` name, and one is absent or differs from its body. Build it with `Testing.mcpRequest`.
24
+ - A hand-written HTTP request answers an empty 406: its `Accept` header does not list both `application/json` and `text/event-stream`, as `*/*` does not. Send both.
25
+ - A client asking for 2025-03-26 or 2024-11-05 is offered 2025-11-25, and disconnects if it cannot speak it: HTTP and stdio serve 2026-07-28, 2025-11-25 and 2025-06-18. Update the client.
26
+ - A request written by hand to a stdio server answers JSON-RPC error `-32602`, `Invalid request metadata`: it is neither preceded by `initialize` nor carries the 2026-07-28 metadata. Open with `initialize` and `notifications/initialized`, or give the request's `_meta` its `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`.
27
+ - A client reports a broken transport from a stdio subprocess: something wrote to stdout. The global `console.log` did, or a layer provided around `runStdio` did, through the default logger, or through `Console` or a logger such as `Logger.consoleJson`, which `logToStderr` does not move. Remove the write, apply `ActionCli.logToStderr` last, or log JSON with `Logger.withConsoleError(Logger.formatJson)`.
28
+ - A request carrying an `Origin` gets an empty 403: the origin is not in `allowedOrigins`. Add the exact origin, if the deployment trusts it.
29
+ - A disallowed `Origin` gets 401, not 403: the endpoint's authentication, which runs before its Origin check, refused the request.
30
+ - Browser calls fail despite an allowed origin, the preflight answering 405, or 401 on an endpoint with `authentication`, which every method but `POST` meets first: `allowedOrigins` is no CORS. Add `HttpRouter.cors` ([browser clients](ActionMcp.md#allow-browser-clients)).
31
+ - `Tool <name> has an output schema but did not return structured content` raised by a Python MCP client for an action that returns nothing: its success is sent as `null`, which a Python SDK release before the fix of python-sdk#3345 reads as missing. Update the SDK.