@gjermundgaraba/effect-actions 0.18.2 → 0.19.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 +20 -47
- package/dist/Action.d.mts +96 -47
- package/dist/Action.mjs +11 -14
- package/dist/ActionCli.d.mts +81 -42
- package/dist/ActionCli.mjs +52 -33
- package/dist/ActionHttp.d.mts +84 -43
- package/dist/ActionHttp.mjs +2 -2
- package/dist/ActionMcp.d.mts +54 -27
- package/dist/ActionMcp.mjs +5 -5
- package/dist/ActionRpc.d.mts +48 -35
- package/dist/ActionRpc.mjs +1 -1
- package/dist/ActionToolkit.d.mts +32 -13
- package/dist/ActionToolkit.mjs +10 -7
- package/dist/Authentication.d.mts +92 -40
- package/dist/Authentication.mjs +37 -35
- package/dist/Testing.d.mts +22 -24
- package/dist/authentication/provider.d.mts +1 -4
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -1
- package/dist/cli/commands.d.mts +34 -11
- package/dist/cli/commands.mjs +47 -22
- package/dist/contract/caller.mjs +1 -1
- package/dist/contract/declared.mjs +27 -14
- package/dist/contract/errors.d.mts +42 -17
- package/dist/contract/errors.mjs +42 -17
- package/dist/contract/hints.mjs +3 -2
- package/dist/contract/implementation.d.mts +5 -8
- package/dist/contract/implementation.mjs +29 -17
- package/dist/contract/media.d.mts +6 -2
- package/dist/contract/media.mjs +6 -2
- package/dist/contract/names.mjs +14 -12
- package/dist/contract/status.mjs +3 -10
- package/dist/http/client.d.mts +15 -4
- package/dist/http/client.mjs +1 -1
- package/dist/mcp/client.d.mts +21 -8
- package/dist/mcp/client.mjs +6 -8
- package/docs/Action-failures.md +6 -15
- package/docs/Action.md +159 -191
- package/docs/ActionCli-failures.md +12 -92
- package/docs/ActionCli.md +102 -156
- package/docs/ActionHttp-failures.md +14 -76
- package/docs/ActionHttp.md +150 -176
- package/docs/ActionMcp-failures.md +2 -22
- package/docs/ActionMcp.md +79 -170
- package/docs/ActionRpc-failures.md +3 -12
- package/docs/ActionRpc.md +183 -120
- package/docs/ActionToolkit-failures.md +3 -10
- package/docs/ActionToolkit.md +112 -109
- package/docs/Authentication-advanced.md +167 -108
- package/docs/Authentication-failures.md +11 -91
- package/docs/Authentication.md +114 -135
- package/docs/README.md +45 -30
- package/docs/Testing-failures.md +2 -6
- package/docs/Testing.md +126 -159
- package/docs/setup-failures.md +8 -32
- package/docs/setup.md +42 -37
- package/examples/README.md +6 -10
- package/examples/assistant/README.md +20 -38
- package/examples/assistant/src/agent.ts +2 -3
- package/examples/assistant/src/cli.ts +4 -9
- package/examples/assistant/src/commands.ts +6 -4
- package/examples/assistant/src/identity/authorize.ts +3 -3
- package/examples/assistant/src/tasks/Tasks.ts +1 -2
- package/examples/assistant/src/tasks/handlers.ts +3 -3
- package/examples/assistant/src/toolkit.ts +6 -8
- package/examples/assistant/test/commands.test.ts +16 -11
- package/examples/assistant/test/tasks/tasks.test.ts +0 -2
- package/examples/assistant/test/toolkit.test.ts +0 -1
- package/examples/kitchen-sink/README.md +30 -52
- package/examples/kitchen-sink/src/Actions.ts +2 -3
- package/examples/kitchen-sink/src/Api.ts +3 -4
- package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
- package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
- package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
- package/examples/kitchen-sink/src/agent.ts +1 -1
- package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
- package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
- package/examples/kitchen-sink/src/attachments/download.ts +5 -8
- package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
- package/examples/kitchen-sink/src/browser.ts +3 -4
- package/examples/kitchen-sink/src/cli.ts +6 -7
- package/examples/kitchen-sink/src/config.ts +2 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
- package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
- package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
- package/examples/kitchen-sink/src/identity/quota.ts +1 -1
- package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
- package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
- package/examples/kitchen-sink/src/issues/guide.ts +2 -3
- package/examples/kitchen-sink/src/ops.ts +3 -6
- package/examples/kitchen-sink/src/request-policy.ts +3 -3
- package/examples/kitchen-sink/src/routes.ts +4 -6
- package/examples/kitchen-sink/src/seed.ts +0 -1
- package/examples/kitchen-sink/src/server.ts +11 -12
- package/examples/kitchen-sink/src/stdio.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
- package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
- package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
- package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
- package/examples/kitchen-sink/test/request-policy.test.ts +1 -1
- package/examples/kitchen-sink/test/routes.test.ts +3 -3
- package/examples/live-rpc/README.md +8 -49
- package/examples/live-rpc/src/browser.ts +8 -41
- package/examples/live-rpc/src/chat/Chat.ts +9 -3
- package/examples/live-rpc/src/chat/room.ts +1 -1
- package/examples/live-rpc/src/client.ts +2 -3
- package/examples/live-rpc/src/page.ts +34 -0
- package/examples/live-rpc/src/routes.ts +6 -7
- package/examples/live-rpc/src/server.ts +6 -4
- package/examples/live-rpc/test/browser.test.ts +6 -5
- package/examples/live-rpc/test/routes.test.ts +16 -15
- package/examples/live-rpc/test/server.test.ts +1 -1
- package/examples/mcp-tools/README.md +22 -45
- package/examples/mcp-tools/src/printers/Printers.ts +3 -7
- package/examples/mcp-tools/src/routes.ts +0 -7
- package/examples/mcp-tools/src/server.ts +1 -1
- package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
- package/examples/mcp-tools/test/routes.test.ts +2 -3
- package/examples/notes-service/README.md +9 -43
- package/examples/notes-service/src/cli.ts +0 -3
- package/examples/notes-service/src/commands.ts +7 -17
- package/examples/notes-service/src/identity/authorize.ts +2 -5
- package/examples/notes-service/src/identity/verifier.ts +3 -4
- package/examples/notes-service/src/notes/Notes.ts +2 -6
- package/examples/notes-service/src/notes/store.ts +5 -1
- package/examples/notes-service/src/routes.ts +6 -8
- package/examples/notes-service/src/server.ts +4 -3
- package/examples/notes-service/src/stdio.ts +4 -3
- package/examples/notes-service/test/commands.test.ts +1 -1
- package/examples/notes-service/test/notes/as.ts +6 -0
- package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
- package/examples/notes-service/test/notes/in-process.test.ts +50 -0
- package/examples/notes-service/test/notes/notes.test.ts +3 -42
- package/examples/notes-service/test/routes.test.ts +2 -2
- package/examples/quickstart/README.md +1 -1
- package/examples/quickstart/src/client.ts +1 -0
- package/examples/quickstart/src/server.ts +2 -4
- package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
- package/package.json +1 -1
- package/docs/Action-in-process.md +0 -93
- package/docs/CONTEXT.md +0 -46
- package/docs/guarantees.md +0 -44
|
@@ -1,99 +1,19 @@
|
|
|
1
1
|
# Authentication failure modes
|
|
2
2
|
|
|
3
|
-
What you see when something is wrong, and the fix
|
|
4
|
-
|
|
3
|
+
What you see when something is wrong, and the fix: compiler diagnostics and errors the library
|
|
4
|
+
does not word. Search for a fragment of the message. A message the library throws says its
|
|
5
|
+
fix itself. Tasks: [Authentication.md](Authentication.md),
|
|
5
6
|
[Authentication-advanced.md](Authentication-advanced.md).
|
|
6
7
|
|
|
7
8
|
## Types and startup
|
|
8
9
|
|
|
9
|
-
- `Provider<CurrentUser, "notes-service.Login">` among a layer's requirements,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
identity itself, nor the provider of another descriptor, of the same identity or not
|
|
15
|
-
([Verify credentials](Authentication.md#verify-credentials)).
|
|
16
|
-
- `Authentication "<name>": the binding's descriptor is not its provider's`, a defect when
|
|
17
|
-
`ActionHttp.layer`, `ActionRpc.layer`, `ActionMcp.layerHttp` or `Authentication.protect`
|
|
18
|
-
builds: it names a descriptor built apart from the one `Authentication.layer` was given, such
|
|
19
|
-
as the same options passed to `make` twice, or a browser's static cookie descriptor served
|
|
20
|
-
with a provider built at startup. Build the server's surfaces and its provider from one
|
|
21
|
-
descriptor value.
|
|
22
|
-
- `Protected action '<name>' requires its matching authentication descriptor` thrown by `make`
|
|
23
|
-
or `layer` of `ActionHttp` or `ActionRpc`, or by `ActionMcp.layerHttp`, or a type error there,
|
|
24
|
-
such as `No overload matches this call`, naming `authentication`,
|
|
25
|
-
`"Protected actions take options naming their authentication"` or
|
|
26
|
-
`"Authentication descriptor does not cover every protected action"`: a protected action is
|
|
27
|
-
served and no descriptor is named, or one of another identity than the action declares. Give
|
|
28
|
-
`{ authentication: Login }`.
|
|
29
|
-
- `Invalid authentication name: "<name>", not an OpenAPI key` thrown by `make`: the name holds a
|
|
30
|
-
character such as `/` or `:`. Use letters, digits, `_`, `.` and `-`.
|
|
31
|
-
- `Authentication takes one native HttpApiSecurity scheme` thrown by `make`, or a type error on
|
|
32
|
-
its `security`: `security` is a record of schemes, or no native scheme. Omit `security` for
|
|
33
|
-
Bearer, or give one scheme.
|
|
34
|
-
- `A protected resource is published only for a Bearer scheme` thrown by `layer` or
|
|
35
|
-
`refusalResponse`, or a type error on `protectedResource` at `layer`: the descriptor names a
|
|
36
|
-
scheme other than Bearer. Drop `protectedResource`, or authenticate with a Bearer descriptor.
|
|
37
|
-
- Type error at `layer` on `verify`: the verifier fails with an error that is neither a refusal
|
|
38
|
-
nor one its descriptor declares, or succeeds with a value that is not the identity. Declare
|
|
39
|
-
the error in the descriptor's `error`, or map it to a refusal where it is the caller's.
|
|
40
|
-
- `Authentication "<name>": error _tag "<tag>" is built in, and declared on every surface` thrown
|
|
41
|
-
by `make`: an `error` entry has the `_tag` of `InvalidInput`, `Unauthenticated` or `Forbidden`.
|
|
42
|
-
Fail with the built-in refusal itself, or give the error a `_tag` of its own.
|
|
43
|
-
- A startup service that the Effect building a verifier yields is unsatisfied where the server
|
|
44
|
-
launches: provide it to the provider's layer,
|
|
45
|
-
`Authentication.layer(Login, build).pipe(Layer.provide(Directory.layer))`, or above it.
|
|
46
|
-
- `Request<"Requires", Tenant>` still owed although the tenant middleware's layer is provided:
|
|
47
|
-
the middleware's layer is in one `Layer.provide([...])` array with the provider, whose
|
|
48
|
-
members do not provide to one another. Provide it in a later `Layer.provide` of its own
|
|
49
|
-
([Read request context in a verifier](Authentication-advanced.md#read-request-context-in-a-verifier)).
|
|
10
|
+
- `Provider<CurrentUser, "notes-service.Login">` among a layer's requirements, `Type 'Provider<...>' is not assignable to type 'never'` at `Layer.launch` or `NodeRuntime.runMain`, or `Expected 2 arguments, but got 1` at a web handler's `handler(request)`: a layer serving protected actions requires the provider of the descriptor it names, and nothing else satisfies it, neither the identity nor another descriptor's provider. Provide it, `Layer.provide(authenticate)` ([Verify credentials](Authentication.md#verify-credentials)).
|
|
11
|
+
- `Property 'authentication' is missing`, or `No overload matches this call` with `"Protected actions take options naming their authentication"` or `"Authentication descriptor does not cover every protected action"`, at `ActionHttp.make`, `ActionRpc.make` or `ActionMcp.layerHttp`: a protected action is served and no descriptor is named. Give `{ authentication: Login }` ([Declare how callers sign in](Authentication.md#declare-how-callers-sign-in)).
|
|
12
|
+
- `Type 'Other' is not assignable to type 'CurrentUser'` under `Descriptor<…>`, at `authentication`: the descriptor authenticates another identity than the action's `caller`. Name the descriptor made for the caller's identity service.
|
|
13
|
+
- `is not assignable to type 'Forbidden | Unauthenticated'`, or `Property '…' is missing in type`, at `layer`'s verifier: it fails with an error that is neither a refusal nor one its descriptor declares, or succeeds with a value that is not the identity. Declare the error in the descriptor's `error`, or map it to a refusal where it is the caller's.
|
|
14
|
+
- `Request<"Requires", Tenant>` still owed although the tenant middleware's layer is provided: it is in one `Layer.provide([...])` array with the provider, and an array's layers provide to the routes, not to one another. Provide it in a later `Layer.provide` of its own ([Read request context in a verifier](Authentication-advanced.md#read-request-context-in-a-verifier)).
|
|
50
15
|
|
|
51
|
-
##
|
|
16
|
+
## Scopes
|
|
52
17
|
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
handler reads.
|
|
56
|
-
- A request the `Host` or `Origin` check should refuse gets a 401 and a challenge: the check is
|
|
57
|
-
the layer's own `middleware`, which runs inside authentication. Make it global router
|
|
58
|
-
middleware
|
|
59
|
-
([Run a request policy before authentication](Authentication-advanced.md#run-a-request-policy-before-authentication)).
|
|
60
|
-
- An MCP host asks for sign-in when it connects: every tool of the endpoint is protected, or
|
|
61
|
-
the host opens a session with `initialize`, as a client of MCP 2025 does, and a session's
|
|
62
|
-
every request authenticates wherever a protected tool is served. Only a 2026-07-28 request
|
|
63
|
-
lists the tools and calls a public one signed out, on an endpoint that serves public tools
|
|
64
|
-
too ([ActionMcp.md](ActionMcp.md#protect-tools)).
|
|
65
|
-
- A verifier's declared error arrives as 422: the error states no `httpApiStatus`. Give it the
|
|
66
|
-
status it means, such as `{ httpApiStatus: 503 }`.
|
|
67
|
-
- `ActionMcp.client` fails with an `McpCallError` whose `reason` is a `StatusError`, not a typed error, when the verifier fails
|
|
68
|
-
with an error its descriptor declares: over MCP that failure is the endpoint's HTTP answer,
|
|
69
|
-
before any tool runs. Pass the endpoint's descriptor as the client's `authentication`.
|
|
70
|
-
- A route's `Cache-Control` is replaced by `no-store`: only a route's own answer keeps its
|
|
71
|
-
caching, and this response is a failure that enclosing middleware serialized.
|
|
72
|
-
|
|
73
|
-
## Discovery and scopes
|
|
74
|
-
|
|
75
|
-
- OAuth discovery returns 404, or an MCP client does not find the authorization server: the
|
|
76
|
-
request's path and query do not match `resource`'s exactly, or the provider is provided to no
|
|
77
|
-
served layer. Set `resource` to the endpoint's exact public URL, and provide the provider to
|
|
78
|
-
the routes served.
|
|
79
|
-
- A request the `Host` or `Origin` policy refuses elsewhere still reads the OAuth discovery
|
|
80
|
-
document: the policy registered after the provider. Provide the policy in the last
|
|
81
|
-
`Layer.provide`
|
|
82
|
-
([Run a request policy before authentication](Authentication-advanced.md#run-a-request-policy-before-authentication)).
|
|
83
|
-
- An MCP client asks a user who only reads to consent to writes on first login: the 401 names no
|
|
84
|
-
scope, so the client requests every one of `scopesSupported`. Give `scopesRequired`.
|
|
85
|
-
- An MCP client reports `InsufficientScopeError` instead of re-authorizing: it has no OAuth
|
|
86
|
-
provider configured. Configure one, or grant the scope up front.
|
|
87
|
-
- `new Action.Forbidden({ scopes })` throws a schema validation error, or
|
|
88
|
-
`Invalid scope in scopesRequired: "<scope>"`, or `in scopesSupported`, is thrown by `layer` or
|
|
89
|
-
`refusalResponse`, or is a defect when a resource an Effect builds is built: a scope is no
|
|
90
|
-
OAuth scope token (it is empty, or holds a space, `"` or `\`). Give each scope as its own
|
|
91
|
-
element.
|
|
92
|
-
- `A protected resource has no fragment: <resource>` thrown by `layer` or `refusalResponse`, or
|
|
93
|
-
a defect when a resource an Effect builds is built: `resource` has a `#fragment`, which no
|
|
94
|
-
request URL carries. Drop the fragment.
|
|
95
|
-
|
|
96
|
-
## Refusing outside the router
|
|
97
|
-
|
|
98
|
-
- `Not a refusal, nor an error the authentication declares` thrown by `refusalResponse`: the
|
|
99
|
-
error is neither, or the descriptor declaring it was not given. Pass `authentication`.
|
|
18
|
+
- `Schema validation failed` thrown by `new Action.Forbidden({ scopes })`: a scope is no OAuth scope token, being empty or holding a space, `"`, `\` or a character outside printable ASCII. Give each scope as its own element.
|
|
19
|
+
- An MCP client reports `InsufficientScopeError` instead of re-authorizing: it has no OAuth provider configured. Configure one, or grant the scope up front.
|
package/docs/Authentication.md
CHANGED
|
@@ -4,158 +4,137 @@ How a remote caller proves the identity a protected action declares. `Authentica
|
|
|
4
4
|
declares it, as a browser-safe descriptor; `Authentication.layer` is that descriptor's provider,
|
|
5
5
|
running the verifier you write. Verifying a token, login and consent stay in the application.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## API
|
|
12
|
-
|
|
13
|
-
Import `@gjermundgaraba/effect-actions/Authentication`.
|
|
14
|
-
|
|
15
|
-
| API | Purpose |
|
|
16
|
-
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
17
|
-
| `make(name, identity, { security, error }?)` | A browser-safe authentication descriptor: how a caller proves `identity`, by one native scheme. |
|
|
18
|
-
| `layer(descriptor, verify, options?)` | Its provider: the server-only verifier, or an Effect building it once per layer graph, and discovery. |
|
|
19
|
-
| `Any` | Any descriptor, erased: what a binding's and an MCP endpoint's `authentication` take. |
|
|
20
|
-
| `Descriptor`, `Provider` | What `make` and `layer` return, to name in a package that emits declarations. |
|
|
21
|
-
| `Verify`, `Options`, `LayerOptions` | The verifier's type, and `make`'s and `layer`'s options. |
|
|
22
|
-
| `ProtectedResource` | An OAuth protected resource. |
|
|
23
|
-
| `protect`, `bearerTokenOf`, `refusalResponse` | In [Authentication-advanced.md](Authentication-advanced.md#api), with their types and options. |
|
|
24
|
-
|
|
25
|
-
| Option | Of | Meaning |
|
|
26
|
-
| ------------------- | ------- | --------------------------------------------------------------------------------------- |
|
|
27
|
-
| `security` | `make` | One native `HttpApiSecurity` scheme; `HttpApiSecurity.bearer` when omitted. |
|
|
28
|
-
| `error` | `make` | What the verifier may fail with besides a refusal: one schema, or a list. |
|
|
29
|
-
| `protectedResource` | `layer` | Bearer only: an OAuth protected resource to publish, or an Effect building one or none. |
|
|
30
|
-
|
|
31
|
-
| Protected resource field | Meaning |
|
|
32
|
-
| ------------------------ | -------------------------------------------------------------------------------------- |
|
|
33
|
-
| `resource` | Required exact OAuth resource identifier; its path and query select the discovery URL. |
|
|
34
|
-
| `authorizationServers` | Required, nonempty: where clients get tokens. |
|
|
35
|
-
| `scopesSupported` | Optional OAuth scope tokens: every scope the resource accepts. |
|
|
36
|
-
| `scopesRequired` | Optional, nonempty OAuth scope tokens: every 401 names them. |
|
|
37
|
-
| `resourceName` | Optional human-readable name. |
|
|
7
|
+
A message the library throws says its fix; for a compiler diagnostic or Effect's own error,
|
|
8
|
+
search [Authentication-failures.md](Authentication-failures.md). For one symbol,
|
|
9
|
+
`grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc
|
|
10
|
+
carries its rules and gotchas, and this page only what an example cannot show.
|
|
38
11
|
|
|
39
12
|
## Declare how callers sign in
|
|
40
13
|
|
|
41
14
|
A protected action declares its identity service, `caller: CurrentUser`
|
|
42
15
|
([Action.md](Action.md#say-who-may-call-an-action)). Declare the descriptor beside that
|
|
43
|
-
service,
|
|
16
|
+
service, in the contract file every client of the binding imports:
|
|
44
17
|
|
|
45
|
-
```ts
|
|
46
|
-
|
|
18
|
+
```ts example=notes-service/src/identity/Identity.ts
|
|
19
|
+
import { Context, Schema } from "effect";
|
|
20
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
21
|
+
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
22
|
+
|
|
23
|
+
export const scopes = { read: "notes:read", write: "notes:write" } as const;
|
|
24
|
+
|
|
25
|
+
export const Scope = Schema.Literals([scopes.read, scopes.write]);
|
|
26
|
+
|
|
27
|
+
export interface User {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly scopes: ReadonlyArray<typeof Scope.Type>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Who is calling. A protected action declares it, `caller: CurrentUser`; authentication
|
|
34
|
+
* provides it per request, and a local host, such as the stdio server, provides it itself.
|
|
35
|
+
*/
|
|
36
|
+
export class CurrentUser extends Context.Service<CurrentUser, User>()(
|
|
37
|
+
"notes-service/CurrentUser",
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
// How a remote caller proves it is CurrentUser: a bearer token, the default scheme. Clients
|
|
41
|
+
// import this descriptor with the binding; its verifier, in verifier.ts, stays on the server.
|
|
47
42
|
export const Login = Authentication.make("notes-service.Login", CurrentUser);
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
export const WhoAmI = Action.make("whoAmI", {
|
|
45
|
+
description: "Show who the credential identifies and which scopes it holds.",
|
|
46
|
+
success: { id: Schema.String, scopes: Schema.Array(Scope) },
|
|
47
|
+
readOnly: true,
|
|
48
|
+
caller: CurrentUser,
|
|
53
49
|
});
|
|
54
50
|
```
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- Build a server's surfaces and its provider from one descriptor value. `ActionHttp.layer`,
|
|
70
|
-
`ActionRpc.layer`, `ActionMcp.layerHttp` and `Authentication.protect` refuse, when they build,
|
|
71
|
-
a descriptor that is another object than the one the provider was given, equal name and
|
|
72
|
-
options or not.
|
|
73
|
-
- Protecting an action that tests already call breaks them. An `Action.client` call then owes
|
|
74
|
-
`Type 'CurrentUser' is missing … from type 'Scope'`: provide the caller around each call
|
|
75
|
-
([Testing-failures.md](Testing-failures.md)). A test that builds `ActionMcp.layerHttp` itself
|
|
76
|
-
gets `Property 'authentication' is missing`: serve `routes` with `Testing.layer`
|
|
77
|
-
([ActionMcp-failures.md](ActionMcp-failures.md)). A call over HTTP without a credential gets
|
|
78
|
-
`Unauthenticated` at run time.
|
|
79
|
-
- A local surface has no remote caller and takes no descriptor: the host provides the identity
|
|
80
|
-
itself, for a CLI ([ActionCli.md](ActionCli.md#name-the-caller)), a Toolkit
|
|
81
|
-
([ActionToolkit.md](ActionToolkit.md#name-the-caller)) and an MCP server over stdio
|
|
82
|
-
([ActionMcp.md](ActionMcp.md#serve-tools-over-stdio)).
|
|
52
|
+
Name it in every binding and MCP endpoint that serves a protected action: an HTTP binding
|
|
53
|
+
([notes-service/src/Api.ts](../examples/notes-service/src/Api.ts)), an MCP endpoint
|
|
54
|
+
([notes-service/src/routes.ts](../examples/notes-service/src/routes.ts)) and an RPC binding
|
|
55
|
+
([live-rpc/src/Rpc.ts](../examples/live-rpc/src/Rpc.ts)); a local surface takes none
|
|
56
|
+
([Action.md](Action.md#say-who-may-call-an-action)). The name's rules are on
|
|
57
|
+
`Authentication.make`, the scheme's on `Authentication.Options.security`; a second credential
|
|
58
|
+
for one identity is in
|
|
59
|
+
[kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts).
|
|
60
|
+
|
|
61
|
+
- **Protecting an action that tests already call breaks them**, at compile time
|
|
62
|
+
([Testing-failures.md](Testing-failures.md),
|
|
63
|
+
[Authentication-failures.md](Authentication-failures.md#types-and-startup)), and a call over
|
|
64
|
+
HTTP without a credential gets `Unauthenticated` at run time.
|
|
83
65
|
|
|
84
66
|
## Verify credentials
|
|
85
67
|
|
|
86
|
-
Write the verifier in a server-only module, give it to `Authentication.layer
|
|
87
|
-
layer
|
|
88
|
-
verifier takes
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
68
|
+
Write the verifier in a server-only module, give it to `Authentication.layer` and provide that
|
|
69
|
+
layer as [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts) does. What the
|
|
70
|
+
verifier takes, fails with and must check itself is on `Authentication.Verify`; where its layer
|
|
71
|
+
goes, on `Authentication.layer`.
|
|
72
|
+
|
|
73
|
+
```ts example=notes-service/src/identity/verifier.ts
|
|
74
|
+
import { Config, Effect, Redacted } from "effect";
|
|
75
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
76
|
+
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
77
|
+
import { Login, scopes } from "./Identity.js";
|
|
78
|
+
import { users } from "./users.js";
|
|
79
|
+
|
|
80
|
+
const isToken = (token: string): token is keyof typeof users => Object.hasOwn(users, token);
|
|
81
|
+
|
|
82
|
+
// DEMO ONLY: a token is a user's name. A real verifier checks a signed token with your
|
|
83
|
+
// authorization server's library, its audience included. An unknown token is the built-in
|
|
84
|
+
// `Unauthenticated`: a 401 every client decodes.
|
|
85
|
+
const verify = (token: Redacted.Redacted<string>) => {
|
|
86
|
+
const name = Redacted.value(token);
|
|
87
|
+
|
|
88
|
+
return isToken(name)
|
|
89
|
+
? Effect.succeed(users[name])
|
|
90
|
+
: Effect.fail(new Action.Unauthenticated({ message: "Unknown demo token." }));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Server only: `Login`'s provider. As an OAuth protected resource it also publishes discovery,
|
|
94
|
+
// so an MCP client that gets a 401 finds where to sign in, asking for read first. The resource
|
|
95
|
+
// is the server's public URL, known only at startup, so it is read from config.
|
|
96
|
+
export const authenticate = Authentication.layer(Login, verify, {
|
|
97
|
+
protectedResource: Effect.map(
|
|
98
|
+
Config.String("PUBLIC_URL").pipe(Config.withDefault("http://localhost:3000")),
|
|
99
|
+
(url) =>
|
|
100
|
+
({
|
|
101
|
+
resource: `${url}/mcp`,
|
|
102
|
+
// DEMO ONLY: no server is there. Yours is your authorization server's issuer.
|
|
103
|
+
authorizationServers: ["https://auth.example.com"],
|
|
104
|
+
scopesSupported: [scopes.read, scopes.write],
|
|
105
|
+
scopesRequired: [scopes.read],
|
|
106
|
+
}) satisfies Authentication.ProtectedResource,
|
|
107
|
+
),
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For a verifier built from startup services, see `authenticateBearer` in
|
|
112
|
+
[kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts);
|
|
113
|
+
for a failure that is not the caller's, `Authentication.Options.error` and `IssuerUnavailable` in
|
|
114
|
+
[kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts); to
|
|
115
|
+
test as a signed-in caller, [Testing.md](Testing.md#test-as-a-signed-in-caller). What each
|
|
116
|
+
surface answers a missing or refused credential with is on `Action.Unauthenticated` and
|
|
117
|
+
`Action.Forbidden`, as
|
|
118
|
+
[kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts)
|
|
119
|
+
asserts, status and challenge included.
|
|
120
120
|
|
|
121
121
|
## Publish OAuth discovery
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
[
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
- `resource` is the exact public URL of the endpoint clients connect to: discovery is served at
|
|
131
|
-
`/.well-known/oauth-protected-resource` followed by its path. Only the deployment knows that
|
|
132
|
-
URL, so give the resource as an Effect reading configuration; succeeding with `undefined`
|
|
133
|
-
publishes nothing.
|
|
134
|
-
- That `resource` and `authorizationServers` are HTTPS, or loopback HTTP in development, is the
|
|
135
|
-
deployment's to ensure: the provider does not check it.
|
|
136
|
-
- Give `scopesRequired` whenever some actions need scopes others do not. An MCP client requests
|
|
137
|
-
the scopes a 401 names, or every one of `scopesSupported` when it names none.
|
|
138
|
-
- Discovery is public. It answers before routing, to any origin, so no authentication and no
|
|
139
|
-
route middleware covers it: to restrict who reads it, see
|
|
140
|
-
[Run a request policy before authentication](Authentication-advanced.md#run-a-request-policy-before-authentication).
|
|
123
|
+
For an MCP endpoint whose users sign in through OAuth, give the Bearer descriptor's `layer` a
|
|
124
|
+
`protectedResource`, as the verifier above does. Its rules are on
|
|
125
|
+
`Authentication.LayerOptions.protectedResource` and on each field of
|
|
126
|
+
`Authentication.ProtectedResource`;
|
|
127
|
+
[kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts)
|
|
128
|
+
asserts the discovery it serves. To restrict who reads it, see
|
|
129
|
+
[Run a request policy before authentication](Authentication-advanced.md#run-a-request-policy-before-authentication).
|
|
141
130
|
|
|
142
131
|
### Step up for a scope
|
|
143
132
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
[notes-service/src/identity/authorize.ts](../examples/notes-service/src/identity/authorize.ts)
|
|
148
|
-
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
- On a public action or under another scheme, an authorizer's or a handler's refusal is
|
|
153
|
-
answered like a declared error, and RPC never steps up: a refusal is the call's typed failure
|
|
154
|
-
([guarantees.md](guarantees.md#wire-behavior)).
|
|
155
|
-
- Name scopes only when re-authorizing can grant them: a server with fixed tokens, such as API
|
|
156
|
-
keys, leaves `scopes` out. A `Forbidden` without them prompts no login
|
|
157
|
-
([kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts));
|
|
158
|
-
what it answers on each surface is in [guarantees.md](guarantees.md#wire-behavior).
|
|
159
|
-
- Map another service's refusals before failing with them
|
|
160
|
-
([Action.md](Action.md#fail-with-declared-and-built-in-errors)): passed on, a `Forbidden`
|
|
161
|
-
naming that service's scopes sends this server's caller to re-authorize for them.
|
|
133
|
+
To have an OAuth client sign in again, or re-authorize with more scopes, fail the authorizer, a
|
|
134
|
+
handler or the verifier with `Action.Unauthenticated`, or with `Action.Forbidden` naming
|
|
135
|
+
`scopes`
|
|
136
|
+
([notes-service/src/identity/authorize.ts](../examples/notes-service/src/identity/authorize.ts));
|
|
137
|
+
`inService` in
|
|
138
|
+
[kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts)
|
|
139
|
+
refuses with none. What each answers on each surface, and when to name scopes, are on those two
|
|
140
|
+
classes.
|
package/docs/README.md
CHANGED
|
@@ -7,8 +7,7 @@ same schemas, handler and authorization.
|
|
|
7
7
|
|
|
8
8
|
## How to read these pages
|
|
9
9
|
|
|
10
|
-
**Arriving with an error?**
|
|
11
|
-
first (step 5), before the examples and the pages.
|
|
10
|
+
**Arriving with an error?** A message the library throws says its fix; for any other, step 5.
|
|
12
11
|
|
|
13
12
|
1. **Open one page per tool call, with your file-reading tool.** Pages are kept small to reduce
|
|
14
13
|
truncation. Several in one call, however many commands it runs, can be cut by its output
|
|
@@ -17,13 +16,13 @@ first (step 5), before the examples and the pages.
|
|
|
17
16
|
2. **Start from the example closest to what you are building** ([Examples](#examples)). Its
|
|
18
17
|
README has a layout tree, and all but quickstart's a "Where to look" table naming the file
|
|
19
18
|
for each thing. Copy its layout and idioms: the examples are the reference for how code is
|
|
20
|
-
written. An example shows one way, not the rules
|
|
21
|
-
3 and 4 even when it fits.
|
|
19
|
+
written, and its comments carry the traps it shows. An example shows one way, not the rules
|
|
20
|
+
or the alternatives: read steps 3 and 4 even when it fits.
|
|
22
21
|
3. **Read [Action.md](Action.md) completely.** Every surface builds on it.
|
|
23
22
|
4. **Read the page of each surface you serve** ([Pages](#pages)), and the example files its task
|
|
24
23
|
sections link.
|
|
25
|
-
5. **
|
|
26
|
-
without quotes, your own code's first compile error included
|
|
24
|
+
5. **For a compiler diagnostic or Effect's own error, search the `*-failures.md` pages** for a
|
|
25
|
+
fragment of the message without quotes, your own code's first compile error included:
|
|
27
26
|
`grep -rnF "<fragment>" node_modules/@gjermundgaraba/effect-actions/docs/*-failures.md`. Each
|
|
28
27
|
module page has one beside it, such as [Action-failures.md](Action-failures.md).
|
|
29
28
|
|
|
@@ -33,28 +32,23 @@ A service others call is [notes-service](../examples/notes-service/) plus
|
|
|
33
32
|
|
|
34
33
|
Where things are, in a project that installs the package: these pages in
|
|
35
34
|
`node_modules/@gjermundgaraba/effect-actions/docs/`, the examples in `examples/` beside them, and
|
|
36
|
-
the declarations with their JSDoc, the reference for signatures,
|
|
37
|
-
the installed version. Install first ([setup.md](setup.md#install))
|
|
38
|
-
one Effect release. Every module is a subpath import, `@gjermundgaraba/effect-actions/Action`;
|
|
39
|
-
there is no package root.
|
|
35
|
+
the declarations with their JSDoc, the reference for signatures, under `dist/`, all at
|
|
36
|
+
the installed version. Install first ([setup.md](setup.md#install)).
|
|
40
37
|
|
|
41
38
|
## Pages
|
|
42
39
|
|
|
43
|
-
| Read | When you need to
|
|
44
|
-
| -------------------------------------------------------- |
|
|
45
|
-
| [setup.md](setup.md) | install, write `tsconfig.json`, import the modules, write the minimal program, lay out an application, bundle a browser client
|
|
46
|
-
| [Action.md](Action.md) | define a contract and who may call it, implement it, authorize calls, fail with declared and built-in errors
|
|
47
|
-
| [
|
|
48
|
-
| [
|
|
49
|
-
| [
|
|
50
|
-
| [
|
|
51
|
-
| [
|
|
52
|
-
| [
|
|
53
|
-
| [Authentication.md](Authentication.md)
|
|
54
|
-
| [
|
|
55
|
-
| [Testing.md](Testing.md) | test an implementation in process, and routes, tools and rpcs in memory, as a signed-in caller; assert on errors |
|
|
56
|
-
| [guarantees.md](guarantees.md) | see the status and body of one case, such as bad input, a declared error or a refusal, on each surface; what the library leaves to you |
|
|
57
|
-
| [CONTEXT.md](CONTEXT.md) | look up a term the pages use |
|
|
40
|
+
| Read | When you need to |
|
|
41
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| [setup.md](setup.md) | install, write `tsconfig.json`, import the modules, write the minimal program, lay out an application, bundle a browser client |
|
|
43
|
+
| [Action.md](Action.md) | define a contract and who may call it, implement it, authorize calls, fail with declared and built-in errors, call actions from a job, share one build across servers |
|
|
44
|
+
| [ActionHttp.md](ActionHttp.md) | bind actions to JSON routes, serve them, limit request bodies, split a binding across layers and add middleware, publish OpenAPI, call them from Effect or a browser |
|
|
45
|
+
| [ActionMcp.md](ActionMcp.md) | serve MCP tools over HTTP or stdio, describe them to a model, return images, protect them, allow browser clients, add prompts and resources, send notifications |
|
|
46
|
+
| [ActionRpc.md](ActionRpc.md) | bind actions to Effect rpcs, serve them on a protocol, call them, send request context with a call |
|
|
47
|
+
| [ActionToolkit.md](ActionToolkit.md) | give a model actions as a native Effect AI `Toolkit`, name the caller, ask for approval |
|
|
48
|
+
| [ActionCli.md](ActionCli.md) | run actions as local commands, or call a server from commands; shape flags, input and output; name the caller; add a command of your own |
|
|
49
|
+
| [Authentication.md](Authentication.md) | declare how callers sign in, verify credentials, publish OAuth discovery, step up for a scope |
|
|
50
|
+
| [Authentication-advanced.md](Authentication-advanced.md) | use an API key or a cookie, read the request in a verifier, protect a route of your own, run a request policy before authentication |
|
|
51
|
+
| [Testing.md](Testing.md) | test an implementation in process, and routes, tools and rpcs in memory, as a signed-in caller; assert on errors |
|
|
58
52
|
|
|
59
53
|
## Examples
|
|
60
54
|
|
|
@@ -67,10 +61,8 @@ there is no package root.
|
|
|
67
61
|
| [live-rpc](../examples/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. |
|
|
68
62
|
| [kitchen-sink](../examples/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. |
|
|
69
63
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
a file. Every token, user and store in the examples is a demo, marked `DEMO ONLY` where it must
|
|
73
|
-
change before a deployment.
|
|
64
|
+
The index in [kitchen-sink](../examples/kitchen-sink/README.md#index) maps every export and
|
|
65
|
+
option it uses to a file.
|
|
74
66
|
|
|
75
67
|
## Choose a surface
|
|
76
68
|
|
|
@@ -85,4 +77,27 @@ change before a deployment.
|
|
|
85
77
|
|
|
86
78
|
Surfaces combine: every one that runs handlers takes the same implementations, one or a list,
|
|
87
79
|
and enforces each contract's `caller` and its implementation's `authorize`
|
|
88
|
-
([Action.md](Action.md#authorize-calls)).
|
|
80
|
+
([Action.md](Action.md#authorize-calls)). What a call answers on each surface, a success, bad
|
|
81
|
+
input, a declared error, a refusal or a defect, is on `ActionHttp.layer`, `ActionMcp.layerHttp`
|
|
82
|
+
and `runStdio`, `ActionRpc.layer`, `ActionToolkit.make`, `ActionCli.command`, each typed client,
|
|
83
|
+
and each built-in error: grep the symbol.
|
|
84
|
+
|
|
85
|
+
## What the library leaves to you
|
|
86
|
+
|
|
87
|
+
- **Verify credentials or issue them.** The application writes the verifier
|
|
88
|
+
([Authentication.md](Authentication.md#verify-credentials)), and login, consent and token
|
|
89
|
+
issuance are its own. The library defines no scopes: a `Forbidden` names the scopes a call
|
|
90
|
+
needs, and the library answers with the OAuth challenge
|
|
91
|
+
([Authentication.md](Authentication.md#step-up-for-a-scope)); which scheme speaks OAuth is on
|
|
92
|
+
`Authentication.Options`'s `security`.
|
|
93
|
+
- **Decide who may do what.** It runs the `authorize` an implementation states and writes no rule
|
|
94
|
+
of its own ([Action.md](Action.md#authorize-calls)).
|
|
95
|
+
- **Filter tool discovery by caller.** What `tools/list` shows a signed-out caller, and the
|
|
96
|
+
endpoint split that hides protected tools, are on `ActionMcp.LayerHttpOptions`'s
|
|
97
|
+
`authentication`.
|
|
98
|
+
- **Limit requests.** A rate limit is the handler's, failing with an error its action declares, or
|
|
99
|
+
middleware's ([Action.md](Action.md#authorize-calls)); the request body limit is the host's
|
|
100
|
+
([ActionHttp.md](ActionHttp.md#serve-the-routes)).
|
|
101
|
+
- **Allow browsers across origins.** CORS for JSON routes is the host's, with `HttpRouter.cors`;
|
|
102
|
+
an MCP endpoint refuses a browser `Origin` it does not list
|
|
103
|
+
([ActionMcp.md](ActionMcp.md#allow-browser-clients)).
|
package/docs/Testing-failures.md
CHANGED
|
@@ -6,16 +6,12 @@ A missing service's error can start
|
|
|
6
6
|
beneath it, `is missing the following properties from type 'Scope'`, instead.
|
|
7
7
|
The tasks are in [Testing.md](Testing.md).
|
|
8
8
|
|
|
9
|
-
- A route answers an empty 500, which a client method reports as `HttpClientError: Decode error (500 …)`: a defect, or a result that does not encode. `Testing.layer` logs
|
|
10
|
-
- A client method fails with `Action.Unauthenticated`: routes under `Testing.layer` keep their real authentication. Give the client a credential in `transformClient` ([Test as a signed-in caller](Testing.md#test-as-a-signed-in-caller)).
|
|
11
|
-
- A protected route answers 401 where the test expects 415 or 400: authentication runs before the body is read. Send a valid credential.
|
|
9
|
+
- A route answers an empty 500, which a client method reports as `HttpClientError: Decode error (500 …)`: a defect, or a result that does not encode. `Testing.layer(routes)` logs its cause on the process console, under `it.effect` too: read the test's output, or collect it with a `Logger` provided around the layer.
|
|
12
10
|
- `Type 'Provider<CurrentUser, "notes-service.Login">' is missing the following properties from type 'Scope'` at `it.effect`, or `… is not assignable to type 'never'` at `Effect.runPromise`: the routes serve a protected action without a provider of its descriptor. Provide the real one or a test verifier to the routes; a caller provided around `Testing.layer` does not satisfy them.
|
|
13
11
|
- `Type 'CurrentUser' is missing the following properties from type 'Scope'` at `it.effect`, or `… is not assignable to type 'never'` at `Effect.runPromise`: the test still owes the identity. An `Action.client` call of a protected action takes its caller per call: provide it around the call. Or a public action's handler reads the identity, which no route provides to it: make the action protected. Or, where the caller is provided already, a builder yields it: move that `yield*` into the handler ([Action-failures.md](Action-failures.md#where-an-implementation-is-served)).
|
|
14
12
|
- `Type '<Service>' is missing the following properties from type 'Scope'` at every `it.effect`, for a service that is no identity, such as a store, a limiter or a `LanguageModel`: a builder now yields it, and the routes require it. Provide its layer to `Testing.layer(routes)`, as the entrypoint provides it to the server.
|
|
15
13
|
- `Type 'unknown' is not assignable to type 'Scope'` at `it.effect`, or `Argument of type 'Effect<…>' is not assignable` at an `Effect.flip` or `Effect.exit`, around a loop over an array of calls to different actions: their successes and errors differ, so the array has no one element type. Annotate it, `ReadonlyArray<Effect.Effect<unknown, unknown>>`, with the caller as a third parameter for `Action.client` calls of protected actions, or assert each call on its own.
|
|
16
|
-
- An rpc call never answers under `Testing.layer`, and nothing is logged: the layer holds the rpc layer twice, under two protocols, and built it for the other one, a WebSocket. Give the rpc layer its WebSocket in the entrypoint, so the routes a test serves hold it once, under the test's protocol ([Serve the rpcs](ActionRpc.md#serve-the-rpcs)).
|
|
17
14
|
- `Property '<field>' does not exist on type 'MethodError<Action<…>>'`, or on a built-in error, after `expect(error).toBeInstanceOf(…)`: `toBeInstanceOf` does not narrow the union of the action's errors. Compare with `toEqual(new X({ … }))`, or narrow with `instanceof` ([Assert on errors](Testing.md#assert-on-errors)).
|
|
18
|
-
-
|
|
19
|
-
- The test reads a service the handlers never changed: it was provided inside the routes and again to the test, two instances. Provide it once, around `Testing.layer`, with `Layer.provideMerge`.
|
|
15
|
+
- `Type 'Handler<"<rpc>">' is missing the following properties from type 'Scope'` at `it.effect`, after `RpcTest.makeClient(binding.group)`: Effect's `RpcTest` asks for handlers of the group, which `ActionRpc.layer` does not provide. Serve the rpc layer under `Testing.layer` with Effect's HTTP protocol and call it with `ActionRpc.client` ([Test rpcs](Testing.md#test-rpcs)).
|
|
20
16
|
- A builder or handler fails with `NotFound: FileSystem.<method> (<path>)` for a file that exists, or a file route answers 500: with no `FileSystem` provided around `Testing.layer`, the routes read a no-op one. Provide `NodeFileSystem.layer` or `NodeServices.layer`. In a command test, the nearer of two provided is read: provide a fake `FileSystem` on the command, inside `NodeServices.layer`.
|
|
21
17
|
- `Argument of type 'Layer<…, FileSystem | Generator | HttpPlatform | HttpRouter | Path>' is not assignable` at `HttpRouter.toWebHandler(routes)`: a web handler's routes need the platform services `Testing.layer(routes)` supplies itself. Provide `HttpServer.layerServices` to them.
|