@gjermundgaraba/effect-actions 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -41
- package/dist/Action.d.mts +31 -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 +28 -9
- package/dist/ActionToolkit.mjs +31 -4
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +30 -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 +18 -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 +211 -426
- package/docs/ActionCli-failures.md +104 -0
- package/docs/ActionCli.md +208 -293
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +205 -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 +179 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +103 -119
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +143 -392
- package/docs/CONTEXT.md +40 -24
- package/docs/README.md +75 -35
- package/docs/Testing-failures.md +21 -0
- package/docs/Testing.md +193 -151
- package/docs/guarantees.md +35 -96
- package/docs/setup-failures.md +37 -0
- package/docs/setup.md +90 -92
- 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 +318 -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 +126 -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 +111 -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 +42 -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 +77 -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 +141 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +121 -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 +31 -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 +111 -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 +137 -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 +119 -0
- package/examples/notes-service/src/notes/handlers.ts +39 -0
- package/examples/notes-service/src/notes/store.ts +84 -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 +288 -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 +27 -9
- 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/guarantees.md
CHANGED
|
@@ -1,104 +1,43 @@
|
|
|
1
1
|
# Guarantees
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Dependency lifetimes
|
|
6
|
-
|
|
7
|
-
- Handlers receive decoded input, return decoded results, and may fail only with their action's declared errors and the built-in ones. A failure the types let through anyway, such as from plain JavaScript or a cast, is answered as the [wire table](#wire-behavior) says for an undeclared failure.
|
|
8
|
-
- Build-time services are those yielded in the builder Effect passed to `implement`, in an Effect building its authorizer and in a verifier's `Authentication.layer`. Each is a layer Effect memoizes. Within one layer graph it runs once, however many surfaces serve its implementation (HTTP layers, MCP endpoints, a Toolkit) or list its actions, and its scope is the host's. Two `implement` calls are two builders, even of one builder Effect: each runs once per graph.
|
|
9
|
-
- `HttpRouter.serve` and `Testing.layer` each build their routes in a layer graph of their own, which reuses what the host has already built but shares nothing it builds itself: a builder's run, a built authorizer, a service of your own such as an in-memory store. `HttpRouter.provideRequest` also builds the layer it provides in a graph of its own, which reuses what the routes' graph has already built. So one rule places builders and your own services alike: **with one server, put everything the process runs in the layer it serves**, a job or an agent beside the routes included; **otherwise, provide `Action.layer(implementations)` and the services they share above the server and its siblings**, such as a second server, a program running beside it, or several `Testing.layer`s. A sibling given neither builds a builder, and a service whose layer both provide, a second time, with state of its own, whenever the server's graph builds it first: `Layer.mergeAll` builds its layers concurrently, so whichever build reaches the memo map first decides, not the order it lists them in.
|
|
10
|
-
- `Action.layer` builds each builder first, once for every surface and client below it, with the services it is given. Building it acquires every supplied implementation's builder, including implementations no surface serves. Without it, a builder runs only for a surface serving its implementation, or a client of it.
|
|
11
|
-
- A builder runs again only in a separately built layer graph, such as a second `Testing.layer`. A local `ActionCli` command is the exception: it builds the selected implementation, its authorizer included, per invocation, even where the host already built it, and releases it after the call, once the handler's and the authorizer's own finalizers have run.
|
|
12
|
-
- Acquiring an `Action.client` builds its implementations into the layer graph it is acquired in, as a layer of it: in a builder or a layer, the host's graph, where it shares each builder's one run with every surface of that graph, whichever builds first; in a program, the graph of a layer provided around it, which every client acquired there shares; under none, a graph of its own. What it builds stays until its scope closes and nothing else in the graph holds it. Acquired per request, in a handler, it builds for every call unless a surface of the graph serves the same implementation, and its builders' startup services become that handler's request requirements: acquire it where builders live.
|
|
13
|
-
- **Without `Action.layer`, the builder runs with whichever surface's services Effect builds it with first.** Services provided around one surface reach the others too, and so does an `Action.layer` provided to one surface alone: provide its startup services once, to `Action.layer` above every surface that serves it. To give an HTTP or MCP surface a build and startup services of its own, wrap it in `Layer.fresh`, which builds everything inside it anew, and provide what the surfaces share, authentication included, outside it. A Toolkit's tools run on one build of their implementation, whatever layers of it are provided ([ActionToolkit.md](ActionToolkit.md#rules)): separate services take separate implementations.
|
|
14
|
-
- Request-time services are those yielded inside a handler or the authorizer itself. For HTTP-hosted surfaces (`ActionHttp`, `ActionMcp.layerHttp`) they appear as `HttpRouter.Request<"Requires", X>`, one for each service `X`, and are supplied by router middleware around the surface, `HttpRouter.provideRequest`, or request context. A protected action's identity is the exception: only its authentication provider supplies it, so the layer requires that provider instead, and nothing else provided stands in for it. For `ActionToolkit`, `ActionCli`, `ActionMcp.runStdio` and `Action.client` the host supplies them at invocation, the identity of every protected action included, whether or not anything reads it; a client's call reads them from its caller's context alone, never from what the client was acquired with.
|
|
15
|
-
- Each call has a scope of its own, on every surface: what the authorizer and the handler acquire is released when the call ends, the handler's first, before its result is encoded, and over HTTP before the route middleware resumes. A Toolkit call is no exception, whether `tools.handle` makes it or `LanguageModel` runs it, approved or not, and neither is an `Action.client` call. `Scope` is never a request-time requirement. A resource that must outlive a call is the builder's, or a service the host provides around its calls.
|
|
16
|
-
- Use distinct tags for build-time capabilities and request-scoped identity. Everything in a call's context wins over what the surface was built with under the same tag: what authentication, other router middleware, `HttpRouter.provideRequest` or a Toolkit's caller provides, and what the server runs in, as provided around `HttpRouter.serve`, `Testing.layer` or the program. A value provided to one surface's layer only fills in a service or reference the call lacks; to give one surface's requests their own, such as a log level or an `ErrorReporter`, use `HttpRouter.provideRequest` on its layer. So never provide an identity or tenant tag in a server's startup layer or root context: a protected route still requires its authentication provider, and a public handler or a native route reading the tag would serve every caller as that identity. In memory, a test serves protected routes behind an authentication provider, the real one or a test verifier of the same descriptor, and each client sends its own caller's credential ([Testing.md](Testing.md#signed-in-callers)). Types verify presence, not provenance, for every service but a protected action's identity.
|
|
17
|
-
|
|
18
|
-
## Authorization
|
|
19
|
-
|
|
20
|
-
- A contract states who may call it, `caller`: `Action.Anyone`, or the identity service a caller must have. Every surface enforces it, whatever the handler and the authorizer read. A remote surface, `ActionHttp.layer` or `ActionMcp.layerHttp`, names the contracts' authentication descriptor (`authentication: Login`), and its layer requires that descriptor's provider, `Authentication.layer`, which verifies each protected request's credential and provides the identity. A local surface requires the identity per call, in the types, and a call that lacks it anyway, as plain JavaScript may, fails with `Unauthenticated` before the authorizer runs. No surface takes an authorization policy of its own, so none can leave one out or loosen it.
|
|
21
|
-
- A call runs, in order: authentication, then over HTTP the layer's own `middleware` ([ActionHttp.md](ActionHttp.md#rules)), then input decoding, then the authorizer, for a protected action only, then the handler. Each step refuses before the next runs. Over HTTP and MCP a protected request is authenticated before its body is read: without a credential, a protected route answers 401 before a 415 or a 400, and a protected tool's call 401 before its arguments are decoded. On a local surface, the identity is checked after decoding.
|
|
22
|
-
- An implementation of protected actions states its authorizer, `Action.implement(actions, handlers, { authorize })`: leaving it out is a type error, and plain JavaScript leaving it out throws at `implement`. `Action.allowAll` lets every authenticated caller call. An implementation of public actions alone takes none, a type error too, and plain JavaScript passing one throws at `implement`. The authorizer never runs for a public action, even one implemented beside protected ones.
|
|
23
|
-
- The authorizer runs once per call, after input decodes, before the handler and outside its span. It runs on `ActionHttp`, `ActionMcp` over HTTP and stdio, `ActionToolkit`, a local `ActionCli` command, and `Action.client`. There is no per-surface override: a caller trusted on one surface, such as an admin CLI's operator, is an identity the host supplies there, which the same authorizer admits ([ActionCli.md](ActionCli.md#trusted-callers)). Calling a handler function directly bypasses dispatch, so call it through `Action.client`.
|
|
24
|
-
- The authorizer receives the selected action contract, typed as the implementation's own, so a policy reads `action.readOnly` or `action.name` instead of a hand-maintained list. It is not authentication: authentication establishes identity, then the authorizer decides what that identity may do. It fails only with a refusal, `Action.Unauthenticated` or `Action.Forbidden`: any other failure is a type error, and a limit on callers is the handler's. HTTP sends a refusal's JSON with 401 or 403, MCP an `isError` tool result, the Toolkit a returned failure, a CLI command the same JSON on stderr. A step-up refusal on a remote surface is the one exception, below.
|
|
25
|
-
- The services the authorizer yields are request-time requirements, like a handler's, and they join the surface's request context. A local caller (`ActionCli`, `ActionToolkit`, stdio, `Action.client`) supplies them itself. What an Effect building the authorizer yields: [dependency lifetimes](#dependency-lifetimes).
|
|
26
|
-
- A CLI command fails with Effect CLI's `UserError`, whose `cause` is what the authorizer or the handler fails with. Every local command may fail with the action's errors and `Action.BuiltIn` ([ActionCli.md](ActionCli.md#rules)).
|
|
27
|
-
- A step-up refusal is one an OAuth client acts on: `Unauthenticated`, or a `Forbidden` naming `scopes`. Step-up is Bearer's and protected actions': on a protected route of `ActionHttp`, or a protected tool's call on an `ActionMcp.layerHttp` endpoint naming a Bearer authentication descriptor, it answers the request with its HTTP status, 401 or 403, its JSON and its challenge, whether authentication, the authorizer or a handler fails with it. Over HTTP it answers the refusal 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. A public route's or a public tool's refusal never steps up, signed in or not: over HTTP its JSON with its status, over MCP an `isError` tool result.
|
|
28
|
-
- Over MCP that answer replaces the tool result, as MCP authorization defines, unless a handler's notification already started the response ([ActionMcp.md](ActionMcp.md#rules)). On an endpoint without authentication, or naming a descriptor of another scheme, there is no OAuth client to step up: it is answered like a declared error, an `isError` tool result the model reads, and over HTTP its JSON with its status, without an `insufficient_scope` challenge. A refusal of the authentication itself is still its 401 or 403.
|
|
29
|
-
- A `Forbidden` naming scopes carries the `insufficient_scope` challenge an OAuth client re-authorizes on ([Authentication.md](Authentication.md#rules)). A `Forbidden` naming no scopes is always answered like a declared error.
|
|
30
|
-
- Map another service's refusals before failing with them. A handler calling another service through a client that decodes the built-in errors, such as `ActionHttp.client`, gets that service's `Unauthenticated` or `Forbidden` as a value any handler may fail with; passed on as it is, it is this server's own refusal. On a remote surface it steps up: a caller whose token is valid is sent to sign in again, its 401 naming `invalid_token`, or to re-authorize for the other service's scopes. Map it to an error the action declares, or to a defect.
|
|
31
|
-
- Input decodes before the authorizer. Invalid input skips it and the handler, so a caller the authorizer would refuse can receive schema errors (HTTP's `InvalidInput` message describes the input's schema), as can any caller of a public action. To refuse a caller before decoding, over HTTP only, use the layer's `middleware`, inside authentication, or router middleware around the surface: [ActionHttp.md](ActionHttp.md#rules).
|
|
32
|
-
- A limit on callers goes in one of three places. After decoding, on every surface: the handler, failing with an error its action declares, the limiter a startup service its builder yields, so every surface of the graph counts against one ([Action.md](Action.md#implementations)). Before decoding, over HTTP only: the layer's own `middleware`, which runs after authentication and so may key the limit by the caller, or router middleware around the routes, which runs before authentication; either answers with an error of the binding's `error` ([ActionHttp.md](ActionHttp.md#binding-errors)). Input that does not decode never reaches a handler, so only middleware counts it, and a protected request without a valid credential reaches only router middleware.
|
|
33
|
-
- Types cannot verify that a verifier checks what it should, or that an authorizer states the right rule: the authorizer is the one place the library guarantees runs before the handler of a protected action, so put action-level authorization there. A check on one record, such as whether this actor may rename this user, needs the input and its data: make it in the handler's data access.
|
|
34
|
-
- `readOnly` is contract metadata, required on every action and kept as a literal type. It is a tool's read-only hint, a read-only tool's default `destructiveHint` and the `action.read_only` span/log annotation, but enforces no authorization. An authorizer can switch on what an action does instead of on its name.
|
|
3
|
+
What a caller sees on each surface for each outcome of a call, and what the library leaves to the application. Read the table when you write a client by hand, assert on a raw response in a test, or need to know how a failure on one surface looks on another. A typed client decodes all of it for you: [Testing.md](Testing.md#assert-on-errors).
|
|
35
4
|
|
|
36
5
|
## Wire behavior
|
|
37
6
|
|
|
38
|
-
HTTP is a
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
- A
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
- They are produced by decoding, by authentication, by an authorizer, or by any handler. A client decodes a status by trying the schemas declared for it, as it decodes any union: errors sharing a `_tag` are told apart by their other fields, the first that decodes winning, so make them disjoint, by a literal field for one. A built-in error is always tried first, so a loose schema of an action's, such as `Schema.Struct({ message: Schema.String })`, never captures one.
|
|
62
|
-
- HTTP refuses undeclared input fields, nested ones too, as the OpenAPI document's `additionalProperties: false` says: the request is a 400 `InvalidInput` naming the field's path. A client drops them when it encodes, since TypeScript lets a wider value through. A record accepts every key its key schema does. A CLI command refuses them too, with an `InvalidInput`, in `--input` and in a flag's JSON. A Toolkit drops them, as Effect decodes a model's tool call. An action without input, `input: {}` given or not, accepts only `{}`, on every surface; over HTTP, an empty JSON body is a 400. MCP tools are strict (`Tool.Strict`): undeclared arguments are invalid arguments, an `isError` result from 2025-11-25 on and a JSON-RPC `InvalidParams` error on earlier stdio revisions. An input that declares its fields publishes `additionalProperties: false` in its schema over HTTP and on MCP ([ActionMcp.md](ActionMcp.md#rules)).
|
|
63
|
-
- An `InvalidInput` for input that does not decode lists `issues`, one `{ path, message }` per issue its `message` describes: `path` holds the keys and indexes from the input's root, `["lines", 1, "count"]`, a symbol key as its string form, `Symbol(name)`, and is empty for the input as a whole. Over HTTP a wrong or missing top-level literal field is such an issue, reported as the whole expected shape. A handler failing with `InvalidInput` may give `issues` of its own, sent as written, and `issues: []` without them: the field is always sent, and the OpenAPI document requires it. MCP's and a Toolkit's native answers have a message only.
|
|
64
|
-
- A message for input that does not decode says what the schema expects and, where Effect's decoder reports one, each issue's path, as does each of an `InvalidInput`'s `issues`; never a value sent, on every surface: `InvalidInput` over HTTP, from a CLI command and from `Action.client`, MCP's invalid-arguments result, a Toolkit's `ToolParameterValidationError`. A path names each key it passes through, an undeclared key and a record's keys included, so a record keyed by an email or a token names it. Effect CLI's own parser, which reads a flag before the schema decodes it, names a rejected choice, an unknown flag or an unexpected argument as typed. A schema's own `message` annotation, or a filter's message, is sent as written, so never put the input in one. Declare a secret `Schema.Redacted`, so what a handler logs of its decoded input prints `<redacted>`; a tool call's span holds it all the same ([observability](#observability)).
|
|
65
|
-
- A media field, `Action.Image`, is JSON on every surface but MCP: `{ "data": "<base64>", "mimeType": "image/png" }`, the bytes in base64, as Effect's JSON codec encodes a `Uint8Array`. HTTP sends it so, a CLI command prints it so, a Toolkit gives it so to a model, and `Action.client` passes it through that codec. Only an MCP tool lifts it, into an image block beside the rest of the success ([ActionMcp.md](ActionMcp.md#media)).
|
|
66
|
-
- A CLI command prints the success's encoding on stdout, and a failure on stderr as the JSON HTTP sends: the action's declared error, a binding's, a built-in one, `InvalidInput` included. A success that does not encode is a defect there, which the command does not print ([ActionCli.md](ActionCli.md#rules)).
|
|
67
|
-
- An `Action.client` call passes its input, its success and its failure through their JSON codecs, encoded then decoded, as HTTP carries them, so its handler and its caller get what a remote call's would. A success or a failure that does not pass, an error its action does not declare included, is a defect, as it is an empty 500 over HTTP ([Action.md](Action.md#clients)).
|
|
68
|
-
- A failure an action does not declare, which the types refuse but plain JavaScript or a cast lets through, is answered by each surface its own way: `Action.client` dies with a defect, HTTP answers an empty 500, MCP its internal-error tool result, `Tool execution failed due to an internal server error.`, a local CLI command fails with `UserError` whose `cause` is that failure, and a Toolkit call fails with an `AiError`, `Failed to encode result`. Declare every error a handler may fail with.
|
|
69
|
-
|
|
70
|
-
## Names
|
|
71
|
-
|
|
72
|
-
- Action names are unique within one HTTP binding; two bindings with different prefixes may reuse one, but then cannot be combined into one `HttpApi`. A tool is named after its action, so action names are also unique within each Toolkit or MCP endpoint that serves them, and kebab-case command names within each aggregate CLI command. A client's method is too, so they are unique within each `Action.client`.
|
|
73
|
-
- Surfaces validate only the names they serve. HTTP does not check tool names; MCP does not check route names.
|
|
74
|
-
- `ActionHttp.layer`, MCP, a Toolkit and `ActionCli.make` expose the actions their `actions` option lists, or every action of the implementations without it, `ActionHttp.layer` only those its binding holds; `ActionCli.command` exposes only its selected action. `Action.client` exposes every action of the implementations passed to it. A selected action keeps its implementation's authorizer, and its builder's one run. Selection never changes policy: an action's `caller` and authorizer are the same wherever it is served. Builder acquisition follows the [dependency lifetimes](#dependency-lifetimes), including explicit `Action.layer` prebuilding and per-invocation CLI selection: a surface's `actions` leaving out every action of an implementation leaves that implementation unbuilt.
|
|
75
|
-
- Surfaces match an implementation to a contract by object identity, never by name. `ActionHttp.layer` refuses implementations holding none of its binding's actions, and serves no action its binding does not hold, whatever its name; an `actions` option refuses a listed action the binding does not hold, or none of the implementations holds, or `ActionCli.remote` one its binding does not, marking one of a held name as another contract; `ActionCli.command` refuses an action with no implementation in its list.
|
|
76
|
-
- An `actions` option narrows a surface's types only where it is always given. Options whose `actions` may be absent, an optional property or a union with options lacking it, serve every action when it is absent and the listed ones when given, so their types owe what every action owes; `Action.client` offers methods, and `ActionToolkit.make` tools, only for the actions such options may list, which it has either way. An explicit type argument names the options' type, not the actions, and requires the options argument: `ActionMcp.layerHttp<typeof app, { readonly actions: readonly [typeof Status] }>(...)`. A misspelled option is a type error on every surface, beside `actions` too, and in any member of a union of options. A CLI's `commands` key naming no action of the binding or the implementations is refused; one naming an action the list leaves out is unused, so one record serves aggregates of several selections.
|
|
77
|
-
- A list of actions is typed by its members, on every surface and in `Action.byName`: `[A, B]` holds `A` and `B`. A list narrowed at runtime, such as an `Array` of a union, a filtered list or a conditional element, is still typed as holding every member, and what it does not hold is absent where it is read: `byName` has no entry for it, a client no method, and `implement` refuses its handler, `Unknown handlers: <name>`. Build a list from the contracts themselves.
|
|
78
|
-
|
|
79
|
-
## Observability
|
|
80
|
-
|
|
81
|
-
- Each handler runs in a span named after its action, a child of the transport's request span, or of its caller's through `Action.client`, with attributes `action.name` and `action.read_only`, a boolean. Every log line the handler writes carries the same two annotations. Names are unique per binding; the request span's route tells two bindings' same-named actions apart. Authentication, decoding, the authorizer and encoding happen outside the handler span.
|
|
82
|
-
- MCP defects and encoding failures are logged with their cause and answered generically.
|
|
83
|
-
- Over `ActionHttp` and `ActionMcp.layerHttp`, a handler's defect reaches the `ErrorReporter`s provided to the surface's layer once.
|
|
84
|
-
- Every MCP tool call, over HTTP and over stdio, and every Toolkit call runs through Effect's `Toolkit.handle`, which records the call's arguments as sent on the current span, as the attribute `parameters`: a tracer the host installs receives them, a `Schema.Redacted` field's value included, which a handler's logs print `<redacted>`. HTTP records no request body. Where a tracer exports spans, keep an action that takes a secret off tools ([Action.md](Action.md#contracts)).
|
|
85
|
-
|
|
86
|
-
## MCP transport
|
|
87
|
-
|
|
88
|
-
Protocol revisions, transport behavior, cancellation, and tool registry isolation are defined
|
|
89
|
-
in [ActionMcp.md](ActionMcp.md#rules).
|
|
7
|
+
One call, on each surface. An HTTP route is `POST <prefix>/<action>` with a JSON body, the prefix `/api` by default. "Its JSON" is the value as its schema's JSON codec encodes it, such as `{"_tag":"NoteNotFound","id":"9"}`. A built-in error's JSON is `{"_tag":"InvalidInput","message":"…","issues":[{"path":["name"],"message":"…"}]}`, `{"_tag":"Unauthenticated","message":"…"}` or `{"_tag":"Forbidden","message":"…"}`, a `Forbidden` with the `scopes` it names. The CLI column is a command under `Command.run`.
|
|
8
|
+
|
|
9
|
+
| Case | HTTP | MCP | RPC | Toolkit | CLI |
|
|
10
|
+
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| Success | its JSON as the body; an empty **200** for an action without `success` | `isError: false`, `structuredContent` its JSON, and the same JSON as one text block; both `null` for an action without `success` | the call's success | a result with `isFailure: false`: `result` the value, `encodedResult` its JSON | its JSON on stdout, indented, or `render`'s text; nothing for an action without `success` |
|
|
12
|
+
| Input that does not decode | **400** `InvalidInput` | `isError: true`, text `Invalid parameters for tool '<name>': …` | the typed `InvalidInput` | a result with `isFailure: true`: `result` an `AiError` whose `reason` is a `ToolParameterValidationError` | `InvalidInput`'s JSON on stderr, exit 1; a missing flag or a rejected choice is Effect CLI's own parse error instead |
|
|
13
|
+
| Undeclared input field | refused, nested ones too: **400** `InvalidInput` naming its path | refused, as input that does not decode | refused: `InvalidInput` naming its path | **dropped**: the call runs without it | refused, in `--input` and in a flag's JSON value: `InvalidInput` |
|
|
14
|
+
| Declared error | its JSON with its `httpApiStatus`, **422** without one | `isError: true`, its JSON as one text block, no `structuredContent` | the call's typed failure | a result with `isFailure: true`: `result` the error, `encodedResult` its JSON | its JSON on stderr; the command fails with a `UserError` whose `cause` is the error; exit 1 by default |
|
|
15
|
+
| Protected action, credential missing or not verified | **401** `Unauthenticated` with the scheme's challenge, or what the verifier fails with, before the body is read; a public route reads no credential | the same HTTP answer in place of a JSON-RPC one, before the arguments are decoded; on an endpoint that also serves a public tool, that tool's call and `tools/list` need no credential, but one a request presents is still verified ([Protect tools](ActionMcp.md#protect-tools)) | the typed `Unauthenticated`, or what the verifier fails with, before the payload is decoded; never a status | no credential: the host provides the identity around the call ([Name the caller](ActionToolkit.md#name-the-caller)) | no credential: the host provides the identity to the command ([Name the caller](ActionCli.md#name-the-caller)) |
|
|
16
|
+
| `Unauthenticated` or `Forbidden` from the authorizer or a handler | its JSON with **401** or **403**, after the input is decoded; on a protected route under Bearer, a 401 and a `Forbidden` naming `scopes` also carry a challenge | `isError: true`, its JSON as text; on an endpoint under Bearer, a protected tool's `Unauthenticated` or `Forbidden` naming `scopes` is the HTTP **401** or **403** instead, with its JSON and challenge | the call's typed failure; never a status or a challenge | a result with `isFailure: true`, as a declared error | as a declared error |
|
|
17
|
+
| Defect, undeclared failure, or a success or error that does not encode | an empty **500** | `isError: true`, text `Tool execution failed due to an internal server error.` | that call's defect, an `Error` whose message is `Internal server error`; the connection's other calls go on | a defect is the call's defect; the rest fail the call with an `AiError`, `Failed to encode result for tool …` | a failure no schema encodes is a `UserError` whose `cause` is it, printed as its tag and message; a defect or a success that does not encode is the command's defect |
|
|
18
|
+
| Unknown path, method or name | a path no layer serves, or a method other than `POST` on a route: **404** | another HTTP method: an empty **405** with `Allow: POST, DELETE`, signed out the **401** of an endpoint with authentication; an unknown tool: a JSON-RPC error `-32602`, with **200** over HTTP | an rpc the layer does not serve ends its call with `Unknown request tag` | Effect's own | Effect CLI's own |
|
|
19
|
+
| Body that is not JSON, or not typed as JSON | not JSON: **400** `InvalidInput`; `Content-Type` missing or not JSON: **415**; both after authentication on a protected route | not JSON: a JSON-RPC error `-32700` with **200**; `Content-Type` missing or not JSON: an empty **415**; over stdio, a line that is not JSON goes unanswered | the protocol's own | — | a flag or `--input` that is not the JSON its field takes: `InvalidInput` |
|
|
20
|
+
|
|
21
|
+
- **Typed clients.** `ActionHttp.client`, `ActionRpc.client`, `Testing.mcpClient` and `Action.client` fail with a declared or built-in error as its decoded value. Each drops undeclared fields as it encodes an input, since TypeScript lets a wider value through, and fails with `InvalidInput` for an input that does not encode, sending nothing. `Action.client` has no wire but passes input, success and failure through the same JSON codecs; an undeclared failure or a success that does not encode is its call's defect. A verifier's declared error reaches `Testing.mcpClient` as an `McpCallError`: over MCP it is the endpoint's HTTP answer.
|
|
22
|
+
- **An action without input** takes `{}` and nothing else, on every surface. Over HTTP, send the body `{}` typed as JSON: no body is a **400**, and no `Content-Type` a **415**.
|
|
23
|
+
- **A media field**, `Action.Image`, is `{ "data": "<base64>", "mimeType": "image/png" }` inside the JSON on every surface but MCP, which lifts it out of `structuredContent` into a block of its own: [Return images and other media](ActionMcp.md#return-images-and-other-media).
|
|
24
|
+
- **Internal failures are never sent.** The cause of a defect goes to the log and, over HTTP, MCP and RPC, to the call's `ErrorReporter`s: those the server runs with, as provided around `HttpRouter.serve` or `Testing.layer`, win over those provided to one surface's layer, which only fill in what a request lacks.
|
|
25
|
+
- **Messages for invalid input.** A message for input that does not decode names paths and what the schema expects, never a field's value. A path names every key it passes through, a record's keys and an undeclared key included, so never key a record by a secret such as an email or a token. A schema's own `message` annotation, or a filter's message, is sent as written, so never put the input in one.
|
|
26
|
+
- **Earlier MCP revisions.** The MCP column is MCP 2026-07-28. A 2025-11-25 or 2025-06-18 client, over HTTP or stdio, gets `structuredContent` only for a success that is an object, and any other success as its text block alone, a string unquoted; a 2025-06-18 client gets invalid arguments as a JSON-RPC error instead of a result. Over HTTP such a client opens a session, which on an endpoint serving a protected tool only a signed-in caller does: it signs in for every request there, a public tool's call and `tools/list` included. Stdio authenticates nothing: the host provides the identity around `runStdio` ([Serve tools over stdio](ActionMcp.md#serve-tools-over-stdio)).
|
|
27
|
+
- **Request bodies have no size limit** unless the host sets one, on every route of the server, MCP endpoints included ([Serve the routes](ActionHttp.md#serve-the-routes)).
|
|
28
|
+
|
|
29
|
+
[kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts) asserts the 401 and the 403 as sent, and [assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts) a refusal as a Toolkit result.
|
|
90
30
|
|
|
91
31
|
## Scope
|
|
92
32
|
|
|
93
|
-
What the
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
- It defines no scopes and no verifier: a refusal names the scopes a call lacks, and the library answers with the OAuth challenge. A descriptor names one native scheme, Bearer by default: a session cookie, an API key or Basic is the descriptor's scheme, which its verifier receives decoded. OAuth, its challenges, discovery and step-up, is Bearer's. One identity per action, and one per remote surface. Tool discovery is not filtered by actor. Production needs real token validation, request limits, and error reporting.
|
|
33
|
+
What the library does not do, where a reader might expect it to:
|
|
34
|
+
|
|
35
|
+
- **Verify credentials or issue them.** The application writes the verifier ([Verify credentials](Authentication.md#verify-credentials)), and login, consent and token issuance are its own. The library defines no scopes: a `Forbidden` names the scopes a call lacks, and the library answers with the OAuth challenge ([Publish OAuth discovery](Authentication.md#publish-oauth-discovery)). OAuth, its discovery, challenges and step-up, is Bearer's.
|
|
36
|
+
- **Decide who may do what.** It runs the `authorize` an implementation states and writes no rule of its own ([Authorize calls](Action.md#authorize-calls)).
|
|
37
|
+
- **Filter tool discovery by caller.** `tools/list` names every tool its endpoint serves, protected ones included, to anyone who may list them. Serve public tools on an endpoint of their own if signed-out callers must not see the protected ones.
|
|
38
|
+
- **Limit requests.** A rate limit is the handler's, failing with an error its action declares, or middleware's.
|
|
39
|
+
- **Allow browsers across origins.** CORS for JSON routes is the host's, with `HttpRouter.cors`; an MCP endpoint refuses a browser `Origin` it does not list ([Allow browser clients](ActionMcp.md#allow-browser-clients)).
|
|
40
|
+
- **Stream, upload or retry.** Actions are unary JSON calls. One decoded input, one success or one declared error. Every HTTP route is a `POST`, a read-only action's included; RPC needs a protocol speaking JSON.
|
|
41
|
+
- **Expire MCP sessions, or serve MCP features other than tools.** A session a client of a 2025 revision opens over HTTP lasts until that client ends it or the process stops ([Serve tools over HTTP](ActionMcp.md#serve-tools-over-http)). Prompts and resources are Effect's native ones, given as `features` ([Add native prompts and resources](ActionMcp.md#add-native-prompts-and-resources)).
|
|
42
|
+
- **Mix identities.** An action names one identity service, and a remote surface one authentication descriptor.
|
|
43
|
+
- **Harden a deployment.** Real token validation, request limits and error reporting are the application's. The examples mark each shortcut they take `DEMO ONLY`: [examples](../examples/README.md).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Setup failure modes
|
|
2
|
+
|
|
3
|
+
What you see when something is wrong, and the fix. Search for a fragment of the message.
|
|
4
|
+
The tasks are in [setup.md](setup.md).
|
|
5
|
+
|
|
6
|
+
- `TS1149: File name '…/assistant.ts' differs from already included file name '…/Assistant.ts' only in casing`:
|
|
7
|
+
a lowercase file is named for its domain beside the domain's contract. Name lowercase files for
|
|
8
|
+
their role, such as `handlers.ts` or `turns.ts`: on a disk that ignores case the two are one file.
|
|
9
|
+
- `Cannot find module '@gjermundgaraba/effect-actions'`: there is no root export. Import a
|
|
10
|
+
subpath from [Entry points](setup.md#entry-points), never the root or a `dist/` path.
|
|
11
|
+
- A browser build fails with `Could not resolve "node:…"`, or warns
|
|
12
|
+
`Module "node:…" has been externalized for browser compatibility`: the page imports a file that
|
|
13
|
+
also holds server code, such as a contract beside its `Action.implement`, or an authentication
|
|
14
|
+
descriptor beside its `Authentication.layer`. Move contracts, identities, descriptors and
|
|
15
|
+
bindings to files that import no server code.
|
|
16
|
+
- A page that loads Effect from an import map still bundles a second copy of Effect or of some
|
|
17
|
+
of its modules, or fails to resolve `effect/Schema` in the browser when its bundler
|
|
18
|
+
externalizes subpaths, as esbuild's `--external:effect` does: a module of the page imports an
|
|
19
|
+
Effect specifier the map does not serve. Serve the specifiers
|
|
20
|
+
[Bundle for a browser](setup.md#bundle-for-a-browser) lists, and import Effect through them in
|
|
21
|
+
the page's own modules too.
|
|
22
|
+
- A page loading Effect from an import map fails with
|
|
23
|
+
`Failed to resolve module specifier "effect/Cause"` in Chromium: the map points at Effect's
|
|
24
|
+
published files and lacks a specifier Effect imports itself. Map `effect/Cause`,
|
|
25
|
+
`effect/Effect`, `effect/Exit` and `effect/Function` to the same files, or serve Effect from a
|
|
26
|
+
build that resolves its own imports.
|
|
27
|
+
- A page fails at load with `does not provide an export named …`: it serves only the names it
|
|
28
|
+
uses of an Effect specifier, from a vendored module or a trimmed bundle, and the package
|
|
29
|
+
imports another. Serve each specifier whole.
|
|
30
|
+
- Type errors inside `effect/*` modules after install, an unmet peer warning for `effect`,
|
|
31
|
+
such as `found 4.1.0`, or a start failing with `Cannot find module '…/effect/dist/…'` under an
|
|
32
|
+
`@effect/*` package: more than one Effect release is installed, or one outside the peer
|
|
33
|
+
range. Install the `effect` range [Install](setup.md#install) names, with every `@effect/*`
|
|
34
|
+
package at the version `effect` resolved to, such as `@effect/platform-node@4.0.3` beside
|
|
35
|
+
`effect@4.0.3`.
|
|
36
|
+
- `Cannot find module 'effect/unstable/http'` (or `…/httpapi`, `…/cli`, `…/ai`): these modules
|
|
37
|
+
are now `effect/http`, `effect/http-api`, `effect/cli` and `effect/ai`.
|
package/docs/setup.md
CHANGED
|
@@ -1,24 +1,50 @@
|
|
|
1
1
|
# Setup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Install the package, import its modules, write the smallest program, lay out an application and
|
|
4
|
+
bundle a browser client.
|
|
5
|
+
|
|
6
|
+
Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
|
|
7
|
+
When something fails, search [setup-failures.md](setup-failures.md). Reading order:
|
|
8
|
+
[README.md](README.md).
|
|
4
9
|
|
|
5
10
|
## Install
|
|
6
11
|
|
|
7
12
|
```sh
|
|
8
|
-
pnpm add @gjermundgaraba/effect-actions effect@~4.0.
|
|
13
|
+
pnpm add @gjermundgaraba/effect-actions effect@~4.0.3
|
|
9
14
|
```
|
|
10
15
|
|
|
11
|
-
| Need | Add
|
|
12
|
-
| ----------------------------------- |
|
|
13
|
-
| Node HTTP server, stdio host or CLI | `@effect/platform-node@~4.0.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
| Need | Add |
|
|
17
|
+
| ----------------------------------- | ---------------------------------------------------------------- |
|
|
18
|
+
| Node HTTP server, stdio host or CLI | `@effect/platform-node@~4.0.3` |
|
|
19
|
+
| Tests | `vitest@^5.0.3` and `@effect/vitest@~4.0.3`, as dev dependencies |
|
|
20
|
+
| Type checking | `typescript@^7.0.2`, as a dev dependency |
|
|
21
|
+
| Running a TypeScript entrypoint | `tsx`, as a dev dependency: `node --import tsx <entrypoint>.ts` |
|
|
22
|
+
|
|
23
|
+
The `effect` peer admits only the patches of the Effect release the package is tested against:
|
|
24
|
+
a minor release may change the unstable Effect modules the surfaces build on.
|
|
25
|
+
|
|
26
|
+
Keep the tilde in `package.json`: `^4.0.3`, which npm writes for `effect@4.0.3`, admits a minor
|
|
27
|
+
release outside the peer range. `npm install effect@~4.0.3` keeps it.
|
|
28
|
+
|
|
29
|
+
Node `^22.12.0 || ^24.0.0 || >=26.0.0`: the package's `engines`, where its `devEngines` is
|
|
30
|
+
what developing the library itself takes. TypeScript 7 or newer. ESM only. No example ships a
|
|
31
|
+
`tsconfig.json`; this one checks them. The package's modules resolve only through its
|
|
32
|
+
`exports`, which `"module": "nodenext"` reads, as a bundler project's
|
|
33
|
+
`"moduleResolution": "bundler"` does. `"strict": true` is required, by Effect:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"compilerOptions": {
|
|
38
|
+
"target": "es2023",
|
|
39
|
+
"module": "nodenext",
|
|
40
|
+
"strict": true,
|
|
41
|
+
"exactOptionalPropertyTypes": true,
|
|
42
|
+
"noEmit": true,
|
|
43
|
+
"skipLibCheck": true
|
|
44
|
+
},
|
|
45
|
+
"include": ["src", "test"]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
22
48
|
|
|
23
49
|
## Entry points
|
|
24
50
|
|
|
@@ -28,24 +54,25 @@ There is no package root. Import one module per subpath, as a namespace:
|
|
|
28
54
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
29
55
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
30
56
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
57
|
+
import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
|
|
31
58
|
import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
|
|
32
59
|
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
33
60
|
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
34
61
|
import * as Testing from "@gjermundgaraba/effect-actions/Testing";
|
|
35
62
|
```
|
|
36
63
|
|
|
37
|
-
|
|
38
|
-
`
|
|
64
|
+
Effect's own modules are subpaths of `effect` too: `effect/http` (`HttpRouter`, `HttpClient`,
|
|
65
|
+
`FetchHttpClient`), `effect/http-api` (`OpenApi`, `HttpApiMiddleware`), `effect/rpc`
|
|
66
|
+
(`RpcServer`, `RpcClient`), `effect/ai` (`LanguageModel`, `Chat`), `effect/cli` (`Command`) and
|
|
67
|
+
`effect/socket` (`Socket`).
|
|
39
68
|
|
|
40
69
|
## Minimal program
|
|
41
70
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
45
|
-
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
71
|
+
One action, served as a JSON route and as an MCP tool: four declarations, each in a file of
|
|
72
|
+
[quickstart/src/](../examples/quickstart/src/).
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
//
|
|
74
|
+
```ts
|
|
75
|
+
// greeting/Greeting.ts: the contract. It imports no server code.
|
|
49
76
|
export const Greet = Action.make("greet", {
|
|
50
77
|
description: "Greet someone by name.",
|
|
51
78
|
input: { name: Schema.String },
|
|
@@ -54,80 +81,51 @@ export const Greet = Action.make("greet", {
|
|
|
54
81
|
caller: Action.Anyone,
|
|
55
82
|
});
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
The server, in its own module, so that a browser client imports the contract alone:
|
|
61
|
-
|
|
62
|
-
```ts example=quickstart-server.ts
|
|
63
|
-
import { Effect, Layer } from "effect";
|
|
64
|
-
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
65
|
-
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
66
|
-
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
67
|
-
import { Greet, Http } from "./quickstart.js";
|
|
84
|
+
// greeting/handlers.ts: the implementation. Every surface takes this one value.
|
|
85
|
+
export const greeting = Action.implement(Greet, ({ name }) => Effect.succeed(`Hello, ${name}!`));
|
|
68
86
|
|
|
69
|
-
//
|
|
70
|
-
const
|
|
87
|
+
// Api.ts: the HTTP binding, which the server serves and the client calls.
|
|
88
|
+
export const Http = ActionHttp.make([Greet]);
|
|
71
89
|
|
|
90
|
+
// routes.ts: the surfaces as one Layer.
|
|
72
91
|
export const routes = Layer.mergeAll(
|
|
73
|
-
ActionHttp.layer(Http,
|
|
74
|
-
ActionMcp.layerHttp(
|
|
92
|
+
ActionHttp.layer(Http, greeting),
|
|
93
|
+
ActionMcp.layerHttp(greeting, { name: "greetings", version: "1.0.0" }),
|
|
75
94
|
);
|
|
76
95
|
```
|
|
77
96
|
|
|
78
|
-
Serve `routes` with `HttpRouter.serve` and a platform server layer
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
the
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
## Rules
|
|
115
|
-
|
|
116
|
-
- Install a single `effect` version within the peer range, and keep every `@effect/*` package on that same release. Mixed versions can fail at the type level in ways that look like library bugs.
|
|
117
|
-
- Never import from `dist/` paths or from a package root. Only the subpaths above are public.
|
|
118
|
-
- Schemas passed to `Action.make` must be service-free (`Schema.Codec<_, _, never, never>`). Handlers may require services.
|
|
119
|
-
|
|
120
|
-
## Scope
|
|
121
|
-
|
|
122
|
-
What the package does and does not do: [guarantees.md](guarantees.md#scope).
|
|
123
|
-
|
|
124
|
-
## Failure modes
|
|
125
|
-
|
|
126
|
-
- `Cannot find module '@gjermundgaraba/effect-actions'`: there is no root export. Import a subpath.
|
|
127
|
-
- A browser build fails with `Could not resolve "node:…"`, or warns `Module "node:…" has been externalized for browser compatibility`: the page imports a module that also holds server code, such as a contract beside its `Action.implement`, or an authentication descriptor beside its `Authentication.layer` verifier. Move the contracts, their identities, the descriptor and the binding to modules that import no server code.
|
|
128
|
-
- A page that loads Effect from an import map still bundles a second copy of Effect, or of some of its modules: a module of the page imports an Effect specifier the map does not serve, such as `effect/Schema`. Serve `effect`, `effect/http` and `effect/http-api`, all `Action`, `ActionHttp` and `Authentication` import, and import Effect through them in the page's own modules too.
|
|
129
|
-
- A page loading Effect from an import map fails before its modules run, with `Failed to resolve module specifier "effect/Cause"` in Chromium: the map points at Effect's published files and lacks a specifier Effect imports itself. Map `effect/Cause`, `effect/Effect`, `effect/Exit` and `effect/Function` to the same files, or serve Effect from a build that resolves its own imports.
|
|
130
|
-
- Type errors inside `effect/*` modules after install: `effect` version drift. Install one `effect` release, in the peer range, for every package.
|
|
131
|
-
- An unmet peer warning for `effect`, such as `found 4.1.0`: the installed Effect is outside the releases this one is tested against. Install the `effect` range [Install](#install) names, with every `@effect/*` package on the same release.
|
|
132
|
-
- `Cannot find module 'effect/unstable/http'` (or `…/httpapi`, `…/cli`, `…/ai`): Effect `4.0.0-rc.118` moved these modules to `effect/http`, `effect/http-api`, `effect/cli` and `effect/ai`. Import the new paths.
|
|
133
|
-
- `Cannot find module 'effect/http'` (or `effect/http-api`, `effect/cli`, `effect/ai`), or at run time `Cannot find module '…/node_modules/effect/dist/http-api.js'` (or `http.js`, `ai.js`, `cli.js`): `effect` is a release candidate older than `4.0.0-rc.118`, which moved these modules out of `effect/unstable/*`. Install the `effect` range [Install](#install) names.
|
|
97
|
+
Serve `routes` with `HttpRouter.serve` and a platform server layer
|
|
98
|
+
([quickstart/src/server.ts](../examples/quickstart/src/server.ts)) and you have `POST /api/greet`
|
|
99
|
+
and an MCP tool `greet` at `/mcp`. Call the route with the binding's typed client
|
|
100
|
+
([quickstart/src/client.ts](../examples/quickstart/src/client.ts)), and test both in memory
|
|
101
|
+
([quickstart/test/greeting/greeting.test.ts](../examples/quickstart/test/greeting/greeting.test.ts)).
|
|
102
|
+
|
|
103
|
+
A request body has no size limit unless the host sets one
|
|
104
|
+
([ActionHttp.md](ActionHttp.md#serve-the-routes)).
|
|
105
|
+
[notes-service](../examples/notes-service/) is this program grown into a service, in the same
|
|
106
|
+
layout.
|
|
107
|
+
|
|
108
|
+
## Lay out an application
|
|
109
|
+
|
|
110
|
+
Folders are domains; the PascalCase file in each is its contract file, which imports no server
|
|
111
|
+
code; `routes.ts` holds the surfaces; each entrypoint chooses the services. An entrypoint of a
|
|
112
|
+
remote surface never provides an identity: authentication supplies it per request
|
|
113
|
+
([Action.md](Action.md#say-who-may-call-an-action)). The tree:
|
|
114
|
+
[examples/README.md](../examples/README.md#how-they-are-laid-out).
|
|
115
|
+
|
|
116
|
+
## Bundle for a browser
|
|
117
|
+
|
|
118
|
+
Import into the page only the contract files, the binding and a client module of your own:
|
|
119
|
+
[notes-service/src/browser.ts](../examples/notes-service/src/browser.ts) over HTTP, with
|
|
120
|
+
promises, [live-rpc/src/browser.ts](../examples/live-rpc/src/browser.ts) over a WebSocket.
|
|
121
|
+
|
|
122
|
+
- `Action`, `ActionHttp`, `ActionRpc` and `Authentication` are the browser-safe modules: they
|
|
123
|
+
use nothing of Node or a server platform.
|
|
124
|
+
- Keep `Action.implement` and `Authentication.layer` out of every file the page imports: a
|
|
125
|
+
bundler keeps such a call, and what its handlers or verifier import, even unused.
|
|
126
|
+
- Those four modules import Effect through `effect`, `effect/http` and `effect/http-api` alone,
|
|
127
|
+
and `ActionRpc` through `effect/rpc` too. A page loading Effect from an import map serves
|
|
128
|
+
those, and `effect/socket` for a WebSocket client.
|
|
129
|
+
- A page on another origin needs CORS on the host, answered before authentication:
|
|
130
|
+
`HttpRouter.cors`, as in
|
|
131
|
+
[kitchen-sink/src/request-policy.ts](../examples/kitchen-sink/src/request-policy.ts).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Complete applications built with `@gjermundgaraba/effect-actions`. Each is a directory you can
|
|
4
|
+
run, with its own tests and a README that says what it shows and where. They are type-checked,
|
|
5
|
+
linted and tested with the library, and they ship in the package, at
|
|
6
|
+
`node_modules/@gjermundgaraba/effect-actions/examples`.
|
|
7
|
+
|
|
8
|
+
| Example | What it is | Start from it when |
|
|
9
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
10
|
+
| [quickstart](quickstart/) | One action, served as a JSON route and an MCP tool, called with its typed client. | You want the smallest thing that runs. |
|
|
11
|
+
| [notes-service](notes-service/) | A service with bearer authentication: a JSON API and MCP tools from one implementation, a CLI, a browser client. | You are building a service others call. Most applications are this. |
|
|
12
|
+
| [mcp-tools](mcp-tools/) | An MCP server whose tools wrap an upstream HTTP API, over stdio and over HTTP, with an image result. | You are putting an existing API in front of a model. |
|
|
13
|
+
| [assistant](assistant/) | No server: the same implementations as a model's tools, with approval of writes, and as local commands. | A model calls your code in process, or you are building a local CLI. |
|
|
14
|
+
| [live-rpc](live-rpc/) | Effect RPC over a WebSocket with a browser-shaped client, each message carrying its own token. | A client keeps a connection open, or the application speaks Effect RPC. |
|
|
15
|
+
| [kitchen-sink](kitchen-sink/) | The reference: a multi-tenant issue tracker using every module and nearly every option, with an index. | You need a working use of one specific feature. |
|
|
16
|
+
|
|
17
|
+
## Run one
|
|
18
|
+
|
|
19
|
+
From the repository root, after `vp install`, each README's commands work as written, for
|
|
20
|
+
example:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
node --import tsx examples/notes-service/src/server.ts
|
|
24
|
+
vp test examples/notes-service
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
In a project that installs the package, copy an example's directory out of `node_modules` and
|
|
28
|
+
run it the same way: the examples import the package by its name. They need `tsx` and
|
|
29
|
+
`@effect/platform-node` beside `effect`, and `vitest` with `@effect/vitest` for their tests.
|
|
30
|
+
|
|
31
|
+
## How they are laid out
|
|
32
|
+
|
|
33
|
+
Every example follows one layout, which is the one we recommend for an application. Each has
|
|
34
|
+
the parts of it that it needs:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
<example>/
|
|
38
|
+
├── src/
|
|
39
|
+
│ ├── <domain>/
|
|
40
|
+
│ │ ├── <Domain>.ts the contract: schemas, errors, actions
|
|
41
|
+
│ │ ├── <service>.ts a service and its Layer
|
|
42
|
+
│ │ └── handlers.ts the implementation: contract to service
|
|
43
|
+
│ ├── Api.ts, Rpc.ts the bindings, where a surface has one
|
|
44
|
+
│ ├── routes.ts the host's remote surfaces as Layers, where it serves any
|
|
45
|
+
│ └── server.ts … one entrypoint per way of running it
|
|
46
|
+
└── test/
|
|
47
|
+
├── <domain>/ what the domain does
|
|
48
|
+
└── routes.test.ts what the surfaces add, called in memory
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Folders are domains, not mechanisms.** Everything about notes is in `notes/`; there is no
|
|
52
|
+
`contracts/`, `handlers/` or `tools/` folder.
|
|
53
|
+
- **The PascalCase file in a domain is its contract.** It imports no server code, so a browser
|
|
54
|
+
or a CLI can import it. Lowercase files beside it run on the server, each named for its role
|
|
55
|
+
(`handlers.ts`, `store.ts`), never for the domain: `notes.ts` beside `Notes.ts` is one file on
|
|
56
|
+
a disk that ignores case.
|
|
57
|
+
- **An entrypoint is where implementations are chosen**: which store, which platform, and on a
|
|
58
|
+
local surface which caller. Nothing below it knows what it got, so a test chooses its own.
|
|
59
|
+
- **Tests mirror the source.** A domain's tests sit in `test/<domain>/`, most calling its
|
|
60
|
+
implementation in process with `Action.client`; a test of a root file, such as `routes.ts`,
|
|
61
|
+
sits beside them and calls the real routes in memory with `Testing.layer`. One opens a port:
|
|
62
|
+
live-rpc's `server.test.ts`, for the WebSocket no in-memory test reaches.
|
|
63
|
+
|
|
64
|
+
In a monorepo, the contract files and the binding are the package that the server, the CLI and
|
|
65
|
+
the website all depend on; the rest is the server.
|
|
66
|
+
|
|
67
|
+
Every token, user and store in these examples is a demo. Each place that must change before a
|
|
68
|
+
deployment is marked `DEMO ONLY`.
|