@gjermundgaraba/effect-actions 0.9.0 → 0.11.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 (70) hide show
  1. package/README.md +56 -50
  2. package/dist/Action.d.mts +286 -79
  3. package/dist/Action.mjs +125 -23
  4. package/dist/ActionCli.d.mts +102 -31
  5. package/dist/ActionCli.mjs +114 -16
  6. package/dist/ActionHttp.d.mts +170 -51
  7. package/dist/ActionHttp.mjs +130 -79
  8. package/dist/ActionMcp.d.mts +71 -21
  9. package/dist/ActionMcp.mjs +231 -80
  10. package/dist/ActionToolkit.d.mts +56 -27
  11. package/dist/ActionToolkit.mjs +16 -5
  12. package/dist/Authentication.d.mts +129 -33
  13. package/dist/Authentication.mjs +231 -38
  14. package/dist/Testing.d.mts +84 -57
  15. package/dist/Testing.mjs +121 -79
  16. package/dist/internal/actions.d.mts +5 -25
  17. package/dist/internal/actions.mjs +75 -27
  18. package/dist/internal/authentication.d.mts +102 -0
  19. package/dist/internal/authentication.mjs +11 -0
  20. package/dist/internal/call.d.mts +12 -0
  21. package/dist/internal/call.mjs +12 -0
  22. package/dist/internal/cli.d.mts +36 -22
  23. package/dist/internal/cli.mjs +311 -33
  24. package/dist/internal/client.d.mts +43 -0
  25. package/dist/internal/client.mjs +25 -0
  26. package/dist/internal/console.mjs +77 -0
  27. package/dist/internal/declared.d.mts +13 -0
  28. package/dist/internal/declared.mjs +34 -0
  29. package/dist/internal/errors.d.mts +77 -0
  30. package/dist/internal/errors.mjs +79 -0
  31. package/dist/internal/implementation.d.mts +171 -41
  32. package/dist/internal/implementation.mjs +147 -29
  33. package/dist/internal/mcp.d.mts +14 -0
  34. package/dist/internal/mcp.mjs +182 -0
  35. package/dist/internal/media.d.mts +8 -0
  36. package/dist/internal/media.mjs +26 -0
  37. package/dist/internal/memory.d.mts +6 -0
  38. package/dist/internal/memory.mjs +38 -0
  39. package/dist/internal/refusal.mjs +67 -0
  40. package/dist/internal/schema-errors.mjs +20 -0
  41. package/dist/internal/tools.mjs +27 -57
  42. package/docs/Action.md +403 -73
  43. package/docs/ActionCli.md +281 -51
  44. package/docs/ActionHttp.md +267 -82
  45. package/docs/ActionMcp.md +232 -102
  46. package/docs/ActionToolkit.md +99 -35
  47. package/docs/Authentication.md +369 -62
  48. package/docs/CONTEXT.md +24 -36
  49. package/docs/README.md +29 -73
  50. package/docs/Testing.md +154 -77
  51. package/docs/guarantees.md +82 -43
  52. package/docs/setup.md +95 -27
  53. package/package.json +12 -20
  54. package/dist/ActionCatalog.d.mts +0 -30
  55. package/dist/ActionCatalog.mjs +0 -39
  56. package/dist/ActionCliClient.d.mts +0 -48
  57. package/dist/ActionCliClient.mjs +0 -43
  58. package/dist/ActionGroup.d.mts +0 -44
  59. package/dist/ActionGroup.mjs +0 -35
  60. package/dist/ActionHttpClient.d.mts +0 -33
  61. package/dist/ActionHttpClient.mjs +0 -16
  62. package/dist/TestingClient.d.mts +0 -17
  63. package/dist/TestingClient.mjs +0 -22
  64. package/dist/internal/fetchClient.d.mts +0 -6
  65. package/dist/internal/fetchClient.mjs +0 -8
  66. package/dist/internal/tools.d.mts +0 -22
  67. package/docs/ActionCatalog.md +0 -54
  68. package/docs/ActionCliClient.md +0 -63
  69. package/docs/ActionGroup.md +0 -128
  70. package/docs/ActionHttpClient.md +0 -80
@@ -1,122 +1,307 @@
1
1
  # ActionHttp
2
2
 
3
- JSON `POST` routes on Effect's `HttpApi`. One `ActionHttp.make` binds a mount path and a set
4
- of groups into a contract value shared by the server, every client, and the OpenAPI document.
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)`.
5
11
 
6
12
  ## API
7
13
 
8
14
  Import `@gjermundgaraba/effect-actions/ActionHttp`.
9
15
 
10
- | API | Purpose |
11
- | ---------------------------- | ------------------------------------------------------------------------------------ |
12
- | `make(options, ...groups)` | Bind contracts and a mount path; returns `Http`. |
13
- | `Http.groups` | The exact bound contracts, in declaration order. |
14
- | `Http.api` | Native Effect `HttpApi` for clients and OpenAPI. |
15
- | `Http.layer(apps, options?)` | Serve a readonly collection of implementations; omit options when no hook is needed. |
16
- | `Http.openApi(path?)` | Serve the OpenAPI document with `GET path`; defaults to `<apiPath>/openapi.json`. |
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. |
17
27
 
18
- Exported types: `Options`, `LayerOptions`, `Http`, `Api`.
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`.
19
29
 
20
- | Option | Meaning |
21
- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
- | `make`: `apiPath` | Required absolute mount path; no default. |
23
- | `make`: `errors` | Surface error codecs declared on every endpoint; defaults to none. |
24
- | `layer`: `before` | Effectful hook receiving the selected `Action.Any`, after successful input decoding and before its handler. Failures must belong to the binding's `errors`. |
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. |
25
40
 
26
- Layer failures and startup requirements come from the supplied implementations. Every
27
- handler's and the hook's request services remain router request requirements; router/platform services are also required. No request identity is supplied at startup.
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.
28
47
 
29
- Route shape: `POST <apiPath>/<group>/<action>`. Operation ID: `<group>.<action>`.
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)).
30
56
 
31
57
  ## Canonical
32
58
 
33
- ```ts
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], {
76
+ authentication: Login,
77
+ });
78
+ ```
79
+
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
34
86
  import { Layer } from "effect";
35
- import { HttpApiScalar, HttpApiSwagger } from "effect/http-api";
87
+ import { HttpRouter, HttpServerResponse } from "effect/http";
88
+ import { HttpApiSwagger, OpenApi } from "effect/http-api";
36
89
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
37
- import { PublicActions, UserActions } from "./contracts.js";
38
- import { PublicApp, UserApp } from "./handlers.js";
39
- import { authentication, authorize, Forbidden, Unauthenticated } from "./auth.js";
40
-
41
- // Contract: shared by server and clients. `errors` are the failures the surface
42
- // itself renders — here the 401 from `authentication` and the 403 from the hook
43
- // below — so a typed client decodes them instead of reporting a decode error on
44
- // an unexpected status.
45
- export const Http = ActionHttp.make(
46
- { apiPath: "/api/actions", errors: [Unauthenticated, Forbidden] },
47
- PublicActions,
48
- UserActions,
49
- );
90
+ import { authenticate } from "./authentication.js";
91
+ import { Http } from "./binding.js";
92
+ import { double, status, userActions } from "./handlers.js";
50
93
 
51
- // One layer per middleware set and per policy. Middleware provided to a layer
52
- // applies to that layer only, and so does its `before` hook.
53
- const routes = Layer.mergeAll(
54
- Http.layer([PublicApp]),
55
- Http.layer([UserApp], { before: authorize }).pipe(Layer.provide(authentication.layer)),
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),
56
97
  );
57
98
 
58
- // Documents are Effect's own, reading the same contract: `GET /api/actions/openapi.json`.
59
99
  const documentation = Layer.mergeAll(
60
- Http.openApi(),
100
+ HttpRouter.add(
101
+ "GET",
102
+ "/api/openapi.json",
103
+ HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api)),
104
+ ),
61
105
  HttpApiSwagger.layer(Http.api, { path: "/docs" }),
62
- HttpApiScalar.layer(Http.api, { path: "/reference" }),
63
106
  );
64
107
 
65
108
  export const layer = Layer.mergeAll(routes, documentation);
66
109
  ```
67
110
 
68
- Serve with `HttpRouter.serve(layer).pipe(Layer.provide(NodeHttpServer.layer(createServer, { port })), Layer.launch)`.
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)).
112
+
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)):
116
+
117
+ ```ts
118
+ export const Billing = ActionHttp.make([Invoice, Refund], { prefix: "/api/billing" });
119
+ export const Accounts = ActionHttp.make([GetUser, RenameUser], { prefix: "/api/accounts" });
120
+
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
+ );
126
+
127
+ // One document for both, when no action name repeats across them:
128
+ // HttpApi.make("app").addHttpApi(Billing.api).addHttpApi(Accounts.api)
129
+ ```
69
130
 
70
131
  ### Client
71
132
 
72
- The client is Effect's native grouped `HttpApiClient` over `Http.api`. Code that does not
73
- run Effects uses the Promise client in [ActionHttpClient.md](ActionHttpClient.md).
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)`.
74
135
 
75
- ```ts
76
- import { HttpApiClient } from "effect/http-api";
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
+ });
147
+
148
+ const status = yield* client.status();
149
+ const user = yield* client.getUser({ id: "1" });
150
+ const identity = yield* client.whoAmI();
151
+
152
+ return { status, user, identity };
153
+ });
154
+
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* () {
158
+ const client = yield* ActionHttp.client(Http, { baseUrl: "http://127.0.0.1:3000" });
159
+
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
77
192
  import { Effect } from "effect";
78
- import { FetchHttpClient } from "effect/http";
79
- import { Http } from "./quickstart.js";
193
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
194
+ import { Http } from "./binding.js";
80
195
 
81
- export const greeting = Effect.gen(function* () {
82
- const client = yield* HttpApiClient.make(Http.api, { baseUrl: "http://127.0.0.1:3000" });
196
+ // Built once, outside any Effect: its methods need nothing more.
197
+ export const api = ActionHttp.fetchClient(Http);
83
198
 
84
- return yield* client.greetings.greet({ payload: { name: "Ada" } });
85
- }).pipe(Effect.provide(FetchHttpClient.layer));
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;
86
203
  ```
87
204
 
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
+
88
214
  ## Rules
89
215
 
90
- - HTTP serves every action of every group passed to `make`; an action has no HTTP switch. To keep an action off HTTP, put it in a group left out of `make` and serve that group elsewhere (MCP, Toolkit, CLI).
91
- - `apiPath` has no default. Group names must be unique within one `make`. Action names need only be unique within their group; equal action names in different groups do not collide.
92
- - `errors` declares the failures the surface around these endpoints answers with rather than a handler: authentication, authorization, rate limiting, upstream unavailability. They are added to every endpoint's error schemas, so `HttpApiClient`, `ActionCliClient` and the in-memory `Testing.httpClient` decode them as typed failures, and they appear in OpenAPI on every operation. A schema an action already declares is not repeated.
93
- - Schemas reachable from one endpoint must have distinct `_tag`s: the client decodes a response by trying the schemas declared for its status, and two errors may share a status. Effect unions them per status.
94
- - `errors` changes only what is declared. Nothing produces them: the middleware that renders those responses must encode a body that matches the schema, or the client sees a decode error again. Handlers cannot fail with them.
95
- - `Http.layer(apps, options?)` requires each implementation to reference the exact group object passed to `make`; reconstructing an equal-looking group is not sufficient. Each group may occur only once per call. Only supplied implementations are mounted, and a group without actions is not built. Options are optional.
96
- - `before` runs after successful native input decoding and before the selected handler. Invalid input skips the hook and handler. A refusal uses the binding's declared error schema and `httpApiStatus`; its services join handler request requirements. Admission that must precede decoding belongs in outer native HTTP middleware.
97
- - Middleware and the hook are per layer call. Groups that need different middleware or a different policy go in separate `Http.layer` calls, merged with `Layer.mergeAll`.
98
- - `ActionHttp` sets no response headers of its own. The host owns cache policy; `Authentication.middleware` marks its responses `cache-control: no-store`.
99
- - A hook refusal and a handler error are plain declared errors: a JSON body and a status, no other headers. Challenge headers such as `WWW-Authenticate` belong to the admission middleware that runs before the router reaches these routes (see [Authentication.md](Authentication.md)), which sets them on its own response.
100
- - Request-time handler services are `HttpRouter.Request.From<"Requires", R>`. Supply them with router middleware (`Authentication.middleware`, `HttpRouter.middleware`), `HttpRouter.provideRequest`, or the request context. Build-time services are ordinary layer requirements.
101
- - `Http.api` is a plain `HttpApi`. Anything Effect can do with an `HttpApi` works: `OpenApi.fromApi`, `HttpApiSwagger.layer`, `HttpApiScalar.layer`, `Http.api.addHttpApi(other)` to combine with other APIs, `HttpApiClient.make`.
102
- - `Http.openApi(path?)` is `OpenApi.fromApi(Http.api)` as one `GET` route, `<apiPath>/openapi.json` unless a path is given. It documents every bound group, not only the served ones. It is a plain route: middleware provided to its layer covers it, and nothing covers it otherwise.
103
- - Client calls always take `{ payload }`. No-input actions take `{ payload: {} }`. Pass `null` or `undefined` only when the codec accepts it. There is no flat client.
104
- - Client effects fail with the declared errors, the group's policy errors, the binding's `errors`, `SchemaError` for local codec failures (including an undeclared input field, before sending), and native `HttpClientError`.
105
- - Add authentication headers with the native `transformClient` option. Use `HttpApiClient.makeWith` for custom error or service channels. Native per-call response modes are available.
106
- - Wire format without a policy: input failure is an empty 400, success is the encoded body, a declared error is its JSON encoding with its `httpApiStatus`, an encoding failure is an empty 400, a defect is an empty 500. Full table in [guarantees.md](guarantees.md).
107
- - Each handler runs in a span named `<group>.<action>`, a child of the request span, attributed with `action.group`, `action.name` and `action.access`; its log lines carry the same annotations. The hook, decoding and encoding are outside it, in the request span.
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"}]}`.
108
271
 
109
272
  ## Failure modes
110
273
 
111
- - Route returns 404: the implementation was never passed to `Http.layer`, or its group is not bound by `make`, or the path lacks the group segment.
112
- - Type error listing `HttpRouter.Request.From<"Requires", CurrentActor>` as unsatisfied: a handler yields a request service and no middleware provides it. Wrap that `Http.layer` call with the middleware's `.layer`.
113
- - Two groups share a name: `make` throws. Rename one.
114
- - `Implementation of group "x" is not served by this adapter`: `Http.layer` received an implementation whose group was not passed to `ActionHttp.make`. Implement the exact shared group object bound by `make`, or add a genuinely new group to the binding. Matching names and schemas do not establish identity.
115
- - `Duplicate implementation group: <name>`: a single `Http.layer` call received more than one implementation of the same group. Pass one implementation per group.
116
- - Client method missing for an action: its group is not bound by `make`.
117
- - Empty 400 on a valid-looking request: input did not decode, or it carries a field the action does not declare. Set a `schemaError` policy on the group to get a typed body.
118
- - 415 `Unsupported content-type: <type>`: a content type other than `application/json`. A missing `Content-Type` is read as JSON.
119
- - `HttpClientError: Decode error (401 POST ...)` from a typed client: the surface answered with a status no endpoint declares. Add that error schema to `ActionHttp.make`'s `errors`.
120
- - A surface error decodes as the wrong type: two schemas that share a status also share a `_tag`. Give them distinct tags.
121
- - The hook's services appear as unsatisfied `HttpRouter.Request.From<"Requires", ...>`: the hook yields an identity tag and this `Http.layer` call has no middleware providing it. Wrap that call with the middleware's `.layer`.
122
- - A refusal returns 500 instead of its status: the hook failed with an error the binding does not declare. Add its schema to `ActionHttp.make`'s `errors`.
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.