@gjermundgaraba/effect-actions 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -40
- package/dist/Action.d.mts +29 -117
- package/dist/Action.mjs +14 -53
- package/dist/ActionCli.d.mts +60 -28
- package/dist/ActionCli.mjs +54 -48
- package/dist/ActionHttp.d.mts +18 -67
- package/dist/ActionHttp.mjs +20 -70
- package/dist/ActionMcp.d.mts +23 -29
- package/dist/ActionMcp.mjs +56 -114
- package/dist/ActionRpc.d.mts +147 -0
- package/dist/ActionRpc.mjs +113 -0
- package/dist/ActionToolkit.d.mts +5 -8
- package/dist/ActionToolkit.mjs +3 -3
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +28 -23
- package/dist/Testing.mjs +27 -44
- package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
- package/dist/authentication/provider.mjs +12 -0
- package/dist/authentication/refusal.mjs +36 -0
- package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
- package/dist/cli/commands.mjs +302 -0
- package/dist/{internal → contract}/call.d.mts +2 -2
- package/dist/contract/call.mjs +10 -0
- package/dist/{internal → contract}/errors.d.mts +13 -10
- package/dist/{internal → contract}/errors.mjs +9 -14
- package/dist/{internal → contract}/implementation.d.mts +37 -21
- package/dist/{internal → contract}/implementation.mjs +18 -49
- package/dist/contract/media.d.mts +13 -0
- package/dist/contract/media.mjs +18 -0
- package/dist/contract/rules.d.mts +25 -0
- package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
- package/dist/{internal → http}/client.d.mts +7 -9
- package/dist/http/client.mjs +17 -0
- package/dist/http/declared.mjs +20 -0
- package/dist/http/schema-errors.mjs +9 -0
- package/dist/mcp/protocol.d.mts +17 -0
- package/dist/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
- package/dist/stdio/console.mjs +64 -0
- package/dist/{internal → testing}/memory.d.mts +1 -1
- package/dist/{internal → testing}/memory.mjs +5 -17
- package/dist/toolkit/tools.mjs +29 -0
- package/docs/Action-failures.md +47 -0
- package/docs/Action-in-process.md +93 -0
- package/docs/Action.md +206 -426
- package/docs/ActionCli-failures.md +102 -0
- package/docs/ActionCli.md +206 -293
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +202 -282
- package/docs/ActionMcp-failures.md +31 -0
- package/docs/ActionMcp.md +222 -271
- package/docs/ActionRpc-failures.md +21 -0
- package/docs/ActionRpc.md +178 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +100 -119
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +142 -392
- package/docs/CONTEXT.md +39 -24
- package/docs/README.md +71 -35
- package/docs/Testing-failures.md +20 -0
- package/docs/Testing.md +186 -151
- package/docs/guarantees.md +35 -96
- package/docs/setup-failures.md +34 -0
- package/docs/setup.md +87 -91
- package/examples/README.md +68 -0
- package/examples/assistant/README.md +92 -0
- package/examples/assistant/src/agent.ts +92 -0
- package/examples/assistant/src/cli.ts +34 -0
- package/examples/assistant/src/commands.ts +25 -0
- package/examples/assistant/src/identity/Identity.ts +33 -0
- package/examples/assistant/src/identity/authorize.ts +24 -0
- package/examples/assistant/src/identity/handlers.ts +10 -0
- package/examples/assistant/src/identity/members.ts +9 -0
- package/examples/assistant/src/identity/permissions.ts +23 -0
- package/examples/assistant/src/scripted.ts +23 -0
- package/examples/assistant/src/tasks/Tasks.ts +63 -0
- package/examples/assistant/src/tasks/handlers.ts +25 -0
- package/examples/assistant/src/tasks/store.ts +96 -0
- package/examples/assistant/src/toolkit.ts +30 -0
- package/examples/assistant/test/commands.test.ts +57 -0
- package/examples/assistant/test/identity/identity.test.ts +43 -0
- package/examples/assistant/test/tasks/store.test.ts +48 -0
- package/examples/assistant/test/tasks/tasks.test.ts +75 -0
- package/examples/assistant/test/toolkit.test.ts +121 -0
- package/examples/kitchen-sink/README.md +317 -0
- package/examples/kitchen-sink/src/Actions.ts +19 -0
- package/examples/kitchen-sink/src/Api.ts +24 -0
- package/examples/kitchen-sink/src/Rpc.ts +12 -0
- package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
- package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
- package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
- package/examples/kitchen-sink/src/admin/grants.ts +22 -0
- package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
- package/examples/kitchen-sink/src/admin/keys.ts +8 -0
- package/examples/kitchen-sink/src/agent.ts +47 -0
- package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
- package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
- package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
- package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
- package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
- package/examples/kitchen-sink/src/attachments/download.ts +49 -0
- package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
- package/examples/kitchen-sink/src/attachments/store.ts +58 -0
- package/examples/kitchen-sink/src/browser.ts +33 -0
- package/examples/kitchen-sink/src/cli.ts +85 -0
- package/examples/kitchen-sink/src/config.ts +18 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
- package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
- package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
- package/examples/kitchen-sink/src/identity/directory.ts +52 -0
- package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
- package/examples/kitchen-sink/src/identity/members.ts +13 -0
- package/examples/kitchen-sink/src/identity/quota.ts +62 -0
- package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
- package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
- package/examples/kitchen-sink/src/issues/guide.ts +37 -0
- package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
- package/examples/kitchen-sink/src/issues/store.ts +96 -0
- package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
- package/examples/kitchen-sink/src/memory.ts +30 -0
- package/examples/kitchen-sink/src/ops.ts +42 -0
- package/examples/kitchen-sink/src/request-policy.ts +63 -0
- package/examples/kitchen-sink/src/routes.ts +120 -0
- package/examples/kitchen-sink/src/sdk.ts +46 -0
- package/examples/kitchen-sink/src/seed.ts +65 -0
- package/examples/kitchen-sink/src/server.ts +62 -0
- package/examples/kitchen-sink/src/stdio.ts +34 -0
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
- package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
- package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
- package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
- package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
- package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
- package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
- package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
- package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
- package/examples/kitchen-sink/test/routes.test.ts +123 -0
- package/examples/kitchen-sink/test/support/scripted.ts +40 -0
- package/examples/live-rpc/README.md +107 -0
- package/examples/live-rpc/src/Rpc.ts +8 -0
- package/examples/live-rpc/src/browser.ts +98 -0
- package/examples/live-rpc/src/chat/Chat.ts +75 -0
- package/examples/live-rpc/src/chat/authorize.ts +16 -0
- package/examples/live-rpc/src/chat/handlers.ts +50 -0
- package/examples/live-rpc/src/chat/room.ts +23 -0
- package/examples/live-rpc/src/chat/store.ts +54 -0
- package/examples/live-rpc/src/client.ts +37 -0
- package/examples/live-rpc/src/identity/Identity.ts +19 -0
- package/examples/live-rpc/src/identity/members.ts +8 -0
- package/examples/live-rpc/src/identity/verifier.ts +23 -0
- package/examples/live-rpc/src/routes.ts +22 -0
- package/examples/live-rpc/src/server.ts +43 -0
- package/examples/live-rpc/test/browser.test.ts +105 -0
- package/examples/live-rpc/test/chat/chat.test.ts +69 -0
- package/examples/live-rpc/test/routes.test.ts +99 -0
- package/examples/live-rpc/test/server.test.ts +75 -0
- package/examples/mcp-tools/README.md +136 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +119 -0
- package/examples/mcp-tools/src/printers/fake.ts +115 -0
- package/examples/mcp-tools/src/printers/farm.ts +136 -0
- package/examples/mcp-tools/src/printers/handlers.ts +29 -0
- package/examples/mcp-tools/src/routes.ts +52 -0
- package/examples/mcp-tools/src/server.ts +30 -0
- package/examples/mcp-tools/src/stdio.ts +26 -0
- package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
- package/examples/mcp-tools/test/routes.test.ts +202 -0
- package/examples/notes-service/README.md +108 -0
- package/examples/notes-service/src/Api.ts +13 -0
- package/examples/notes-service/src/browser.ts +41 -0
- package/examples/notes-service/src/cli.ts +22 -0
- package/examples/notes-service/src/commands.ts +89 -0
- package/examples/notes-service/src/identity/Identity.ts +31 -0
- package/examples/notes-service/src/identity/authorize.ts +18 -0
- package/examples/notes-service/src/identity/handlers.ts +10 -0
- package/examples/notes-service/src/identity/users.ts +9 -0
- package/examples/notes-service/src/identity/verifier.ts +36 -0
- package/examples/notes-service/src/notes/Notes.ts +91 -0
- package/examples/notes-service/src/notes/handlers.ts +33 -0
- package/examples/notes-service/src/notes/store.ts +73 -0
- package/examples/notes-service/src/routes.ts +45 -0
- package/examples/notes-service/src/server.ts +26 -0
- package/examples/notes-service/src/status/Status.ts +10 -0
- package/examples/notes-service/src/status/handlers.ts +14 -0
- package/examples/notes-service/src/stdio.ts +25 -0
- package/examples/notes-service/test/commands.test.ts +134 -0
- package/examples/notes-service/test/notes/notes.test.ts +103 -0
- package/examples/notes-service/test/routes.test.ts +84 -0
- package/examples/quickstart/README.md +31 -0
- package/examples/quickstart/src/Api.ts +5 -0
- package/examples/quickstart/src/client.ts +13 -0
- package/examples/quickstart/src/greeting/Greeting.ts +12 -0
- package/examples/quickstart/src/greeting/handlers.ts +6 -0
- package/examples/quickstart/src/routes.ts +11 -0
- package/examples/quickstart/src/server.ts +16 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
- package/package.json +23 -5
- package/dist/internal/actions.d.mts +0 -9
- package/dist/internal/authentication.mjs +0 -11
- package/dist/internal/call.mjs +0 -12
- package/dist/internal/cli.mjs +0 -328
- package/dist/internal/client.mjs +0 -25
- package/dist/internal/console.mjs +0 -77
- package/dist/internal/declared.d.mts +0 -13
- package/dist/internal/declared.mjs +0 -34
- package/dist/internal/mcp.d.mts +0 -14
- package/dist/internal/media.d.mts +0 -8
- package/dist/internal/media.mjs +0 -26
- package/dist/internal/refusal.mjs +0 -67
- package/dist/internal/schema-errors.mjs +0 -20
- package/dist/internal/tools.mjs +0 -44
package/docs/ActionHttp.md
CHANGED
|
@@ -1,307 +1,227 @@
|
|
|
1
1
|
# ActionHttp
|
|
2
2
|
|
|
3
|
-
JSON `POST` routes on Effect's `HttpApi
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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`.
|
|
19
|
-
| `Http.actions` | The
|
|
20
|
-
| `Http.error` | The errors every endpoint declares besides its action's own.
|
|
21
|
-
| `Http.prefix` | Where the routes mount
|
|
22
|
-
| `Http.authentication` |
|
|
23
|
-
| `Http.api` |
|
|
24
|
-
| `layer(Http, implementations, options?)` | Mount the routes of the bound actions
|
|
25
|
-
| `client(Http, options?)` | An Effect of a typed client; requires the native `HttpClient
|
|
26
|
-
| `fetchClient(Http, options?)` | The same client
|
|
27
|
-
|
|
28
|
-
Exported types: `Binding`; `Any`, any binding; `Client
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
| `make`: `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
115
|
+
```ts
|
|
116
|
+
Layer.provide(Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1))),
|
|
117
|
+
```
|
|
151
118
|
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
- The
|
|
221
|
-
|
|
222
|
-
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
-
|
|
226
|
-
|
|
227
|
-
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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.
|