@gjermundgaraba/effect-actions 0.18.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +20 -47
  2. package/dist/Action.d.mts +96 -49
  3. package/dist/Action.mjs +12 -16
  4. package/dist/ActionCli.d.mts +82 -44
  5. package/dist/ActionCli.mjs +54 -35
  6. package/dist/ActionHttp.d.mts +84 -45
  7. package/dist/ActionHttp.mjs +3 -4
  8. package/dist/ActionMcp.d.mts +54 -29
  9. package/dist/ActionMcp.mjs +33 -15
  10. package/dist/ActionRpc.d.mts +48 -37
  11. package/dist/ActionRpc.mjs +3 -4
  12. package/dist/ActionToolkit.d.mts +32 -15
  13. package/dist/ActionToolkit.mjs +11 -9
  14. package/dist/Authentication.d.mts +92 -42
  15. package/dist/Authentication.mjs +38 -37
  16. package/dist/Testing.d.mts +23 -27
  17. package/dist/Testing.mjs +1 -2
  18. package/dist/authentication/provider.d.mts +1 -6
  19. package/dist/authentication/provider.mjs +2 -3
  20. package/dist/authentication/refusal.mjs +3 -3
  21. package/dist/cli/commands.d.mts +34 -13
  22. package/dist/cli/commands.mjs +48 -24
  23. package/dist/contract/ast.mjs +1 -2
  24. package/dist/contract/call.d.mts +1 -3
  25. package/dist/contract/call.mjs +1 -2
  26. package/dist/contract/caller.d.mts +1 -3
  27. package/dist/contract/caller.mjs +2 -3
  28. package/dist/contract/declared.d.mts +0 -2
  29. package/dist/contract/declared.mjs +28 -16
  30. package/dist/contract/errors.d.mts +42 -19
  31. package/dist/contract/errors.mjs +43 -19
  32. package/dist/contract/hints.mjs +4 -4
  33. package/dist/contract/implementation.d.mts +7 -14
  34. package/dist/contract/implementation.mjs +31 -19
  35. package/dist/contract/media.d.mts +7 -5
  36. package/dist/contract/media.mjs +7 -4
  37. package/dist/contract/names.mjs +15 -14
  38. package/dist/contract/status.mjs +4 -12
  39. package/dist/http/client.d.mts +15 -6
  40. package/dist/http/client.mjs +2 -3
  41. package/dist/http/schema-errors.mjs +1 -2
  42. package/dist/mcp/client.d.mts +21 -10
  43. package/dist/mcp/client.mjs +8 -11
  44. package/dist/mcp/protocol.mjs +1 -2
  45. package/dist/stdio/console.mjs +1 -2
  46. package/dist/testing/memory.d.mts +1 -3
  47. package/dist/testing/memory.mjs +1 -2
  48. package/docs/Action-failures.md +6 -15
  49. package/docs/Action.md +159 -191
  50. package/docs/ActionCli-failures.md +12 -92
  51. package/docs/ActionCli.md +102 -156
  52. package/docs/ActionHttp-failures.md +14 -76
  53. package/docs/ActionHttp.md +150 -176
  54. package/docs/ActionMcp-failures.md +2 -22
  55. package/docs/ActionMcp.md +79 -170
  56. package/docs/ActionRpc-failures.md +3 -12
  57. package/docs/ActionRpc.md +183 -120
  58. package/docs/ActionToolkit-failures.md +3 -10
  59. package/docs/ActionToolkit.md +112 -109
  60. package/docs/Authentication-advanced.md +171 -108
  61. package/docs/Authentication-failures.md +11 -91
  62. package/docs/Authentication.md +114 -135
  63. package/docs/README.md +45 -30
  64. package/docs/Testing-failures.md +2 -6
  65. package/docs/Testing.md +126 -159
  66. package/docs/setup-failures.md +8 -32
  67. package/docs/setup.md +42 -38
  68. package/examples/README.md +6 -10
  69. package/examples/assistant/README.md +20 -38
  70. package/examples/assistant/src/agent.ts +2 -3
  71. package/examples/assistant/src/cli.ts +4 -9
  72. package/examples/assistant/src/commands.ts +6 -4
  73. package/examples/assistant/src/identity/authorize.ts +3 -3
  74. package/examples/assistant/src/tasks/Tasks.ts +1 -2
  75. package/examples/assistant/src/tasks/handlers.ts +3 -3
  76. package/examples/assistant/src/toolkit.ts +6 -8
  77. package/examples/assistant/test/commands.test.ts +16 -11
  78. package/examples/assistant/test/tasks/tasks.test.ts +0 -2
  79. package/examples/assistant/test/toolkit.test.ts +0 -1
  80. package/examples/kitchen-sink/README.md +30 -52
  81. package/examples/kitchen-sink/src/Actions.ts +2 -3
  82. package/examples/kitchen-sink/src/Api.ts +3 -4
  83. package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
  84. package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
  85. package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
  86. package/examples/kitchen-sink/src/agent.ts +1 -1
  87. package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
  88. package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
  89. package/examples/kitchen-sink/src/attachments/download.ts +5 -8
  90. package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
  91. package/examples/kitchen-sink/src/browser.ts +3 -4
  92. package/examples/kitchen-sink/src/cli.ts +6 -7
  93. package/examples/kitchen-sink/src/config.ts +2 -0
  94. package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
  95. package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
  96. package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
  97. package/examples/kitchen-sink/src/identity/quota.ts +1 -1
  98. package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
  99. package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
  100. package/examples/kitchen-sink/src/issues/guide.ts +2 -3
  101. package/examples/kitchen-sink/src/ops.ts +3 -6
  102. package/examples/kitchen-sink/src/request-policy.ts +8 -4
  103. package/examples/kitchen-sink/src/routes.ts +4 -6
  104. package/examples/kitchen-sink/src/seed.ts +0 -1
  105. package/examples/kitchen-sink/src/server.ts +11 -12
  106. package/examples/kitchen-sink/src/stdio.ts +3 -2
  107. package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
  108. package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
  109. package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
  110. package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
  111. package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
  112. package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
  113. package/examples/kitchen-sink/test/request-policy.test.ts +39 -3
  114. package/examples/kitchen-sink/test/routes.test.ts +3 -3
  115. package/examples/live-rpc/README.md +8 -49
  116. package/examples/live-rpc/src/browser.ts +8 -41
  117. package/examples/live-rpc/src/chat/Chat.ts +9 -3
  118. package/examples/live-rpc/src/chat/room.ts +1 -1
  119. package/examples/live-rpc/src/client.ts +2 -3
  120. package/examples/live-rpc/src/page.ts +34 -0
  121. package/examples/live-rpc/src/routes.ts +6 -7
  122. package/examples/live-rpc/src/server.ts +6 -4
  123. package/examples/live-rpc/test/browser.test.ts +6 -5
  124. package/examples/live-rpc/test/routes.test.ts +16 -15
  125. package/examples/live-rpc/test/server.test.ts +1 -1
  126. package/examples/mcp-tools/README.md +22 -45
  127. package/examples/mcp-tools/src/printers/Printers.ts +3 -7
  128. package/examples/mcp-tools/src/routes.ts +0 -7
  129. package/examples/mcp-tools/src/server.ts +1 -1
  130. package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
  131. package/examples/mcp-tools/test/routes.test.ts +2 -3
  132. package/examples/notes-service/README.md +9 -43
  133. package/examples/notes-service/src/cli.ts +0 -3
  134. package/examples/notes-service/src/commands.ts +7 -17
  135. package/examples/notes-service/src/identity/authorize.ts +2 -5
  136. package/examples/notes-service/src/identity/verifier.ts +3 -4
  137. package/examples/notes-service/src/notes/Notes.ts +2 -6
  138. package/examples/notes-service/src/notes/store.ts +5 -1
  139. package/examples/notes-service/src/routes.ts +6 -8
  140. package/examples/notes-service/src/server.ts +4 -3
  141. package/examples/notes-service/src/stdio.ts +4 -3
  142. package/examples/notes-service/test/commands.test.ts +1 -1
  143. package/examples/notes-service/test/notes/as.ts +6 -0
  144. package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
  145. package/examples/notes-service/test/notes/in-process.test.ts +50 -0
  146. package/examples/notes-service/test/notes/notes.test.ts +3 -42
  147. package/examples/notes-service/test/routes.test.ts +2 -2
  148. package/examples/quickstart/README.md +1 -1
  149. package/examples/quickstart/src/client.ts +1 -0
  150. package/examples/quickstart/src/server.ts +2 -4
  151. package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
  152. package/package.json +12 -16
  153. package/docs/Action-in-process.md +0 -93
  154. package/docs/CONTEXT.md +0 -46
  155. 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. Search for a fragment of the message.
4
- Tasks: [Authentication.md](Authentication.md),
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
- `Type 'Provider<...>' is not assignable to type 'never'` at `Layer.launch` or
11
- `NodeRuntime.runMain`, or `Expected 2 arguments, but got 1` at a web handler's
12
- `handler(request)`: a layer serving protected actions requires the provider of the descriptor
13
- it names. Provide it, `Layer.provide(authenticate)`. Nothing else satisfies it: not the
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
- ## Requests
16
+ ## Scopes
52
17
 
53
- - An action that must be authenticated answers without credentials: its contract states
54
- `caller: Action.Anyone`. Make it protected; the surfaces then authenticate it, whatever its
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.
@@ -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
- Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
8
- When something fails, search [Authentication-failures.md](Authentication-failures.md). Reading
9
- order: [README.md](README.md).
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, and name it in every binding and MCP endpoint that serves a protected action:
16
+ service, in the contract file every client of the binding imports:
44
17
 
45
- ```ts
46
- // identity/Identity.ts
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
- // Api.ts
50
- export const Http = ActionHttp.make([GetStatus, ...Notes, WhoAmI], {
51
- prefix: "/api",
52
- authentication: Login,
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
- See [notes-service/src/identity/Identity.ts](../examples/notes-service/src/identity/Identity.ts)
57
- and [notes-service/src/Api.ts](../examples/notes-service/src/Api.ts). An MCP endpoint names it
58
- the same way ([notes-service/src/routes.ts](../examples/notes-service/src/routes.ts)), and so
59
- does an RPC binding ([live-rpc/src/Rpc.ts](../examples/live-rpc/src/Rpc.ts)).
60
-
61
- - Every client of the binding imports the descriptor: keep it in the contract file. Keep
62
- `layer`, and what its verifier imports, in server-only modules.
63
- - The name is the descriptor's type identity: use one name for one declaration. It holds only
64
- letters, digits, `_`, `.` and `-`.
65
- - A descriptor names one scheme, and a binding or an endpoint names one descriptor. There is no
66
- fallback between schemes: a host accepting two credentials for one identity declares two
67
- descriptors and serves them on two bindings or endpoints
68
- ([kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts)).
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`, and provide that
87
- layer to every layer serving a protected action: `routes.pipe(Layer.provide(authenticate))`. The
88
- verifier takes the credential and succeeds with the identity, or fails with
89
- `Action.Unauthenticated`, `Action.Forbidden` or an error the descriptor declares. See
90
- [notes-service/src/identity/verifier.ts](../examples/notes-service/src/identity/verifier.ts) and
91
- [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts).
92
-
93
- - **The library reads no token.** Everything that makes a credential valid is the verifier's to
94
- check, with your authorization server's library: its signature, its expiry, and its audience.
95
- A token issued for another resource fails with `Unauthenticated`. Only a missing or empty
96
- credential is refused before the verifier runs.
97
- - **Never provide the identity service, or a tenant, at startup**, in a layer or around
98
- `HttpRouter.serve` or the program. It does not satisfy the provider a protected route
99
- requires, and a public handler or a route of your own reading it would serve every caller as
100
- that identity. A test sends a credential instead
101
- ([Testing.md](Testing.md#test-as-a-signed-in-caller)).
102
- - A verifier that needs startup services, such as a user directory, is passed as an Effect that
103
- yields them and returns the verifier: a builder ([Action.md](Action.md#implement-actions)),
104
- run once per layer graph
105
- ([kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts)).
106
- - Call `Authentication.layer` once per descriptor and provide that one value wherever a
107
- provider is required.
108
- - A failure that is not the caller's, such as an issuer the verifier cannot reach, is an error
109
- the descriptor declares, with `{ httpApiStatus: 503 }`: an `Unauthenticated` would have the
110
- caller sign in again
111
- ([kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts),
112
- [kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts)).
113
- - Over RPC the verifier runs for each message of a protected rpc, not once per connection
114
- ([live-rpc/src/identity/verifier.ts](../examples/live-rpc/src/identity/verifier.ts),
115
- [kitchen-sink/test/identity/session.test.ts](../examples/kitchen-sink/test/identity/session.test.ts)).
116
-
117
- What each surface answers a missing or refused credential with:
118
- [guarantees.md](guarantees.md#wire-behavior), as
119
- [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts) asserts.
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
- Give a Bearer descriptor's `layer` a `protectedResource`. The provider then publishes OAuth
124
- protected-resource discovery (RFC 9728) and names it in every challenge, so an MCP client that
125
- gets a 401 finds the authorization server. Without it the challenge names nowhere to sign in,
126
- so an MCP endpoint whose users sign in through OAuth publishes it. See
127
- [notes-service/src/identity/verifier.ts](../examples/notes-service/src/identity/verifier.ts)
128
- and [kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts).
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
- A step-up refusal is one an OAuth client acts on: `Action.Unauthenticated`, which it signs in
145
- on, or an `Action.Forbidden` naming `scopes`, which it re-authorizes on with those scopes
146
- added. The authorizer, a handler or the verifier may fail with one:
147
- [notes-service/src/identity/authorize.ts](../examples/notes-service/src/identity/authorize.ts).
148
-
149
- - Step-up is a Bearer descriptor's, for protected actions and routes under `protect`: the
150
- refusal answers the request with 401 or 403 and its challenge, over MCP in place of the tool
151
- result unless the response has started ([guarantees.md](guarantees.md#wire-behavior)).
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?** Search the `*-failures.md` pages for a fragment of its message
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, the alternatives or the traps: read steps
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. **When something fails, search the `*-failures.md` pages** for a fragment of the message
26
- without quotes, your own code's first compile error included, before reasoning it out:
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, in `dist/<Module>.d.mts`, all at
37
- the installed version. Install first ([setup.md](setup.md#install)): it needs TypeScript 7 and
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
- | [Action-in-process.md](Action-in-process.md) | call actions from your own code, such as a job; share one build of the implementations across servers and jobs |
48
- | [ActionHttp.md](ActionHttp.md) | bind actions to JSON routes, serve them, limit request bodies, publish OpenAPI, call them from Effect or a browser |
49
- | [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 |
50
- | [ActionRpc.md](ActionRpc.md) | bind actions to Effect rpcs, serve them on a protocol, call them, send request context with a call |
51
- | [ActionToolkit.md](ActionToolkit.md) | give a model actions as a native Effect AI `Toolkit`, name the caller, ask for approval |
52
- | [ActionCli.md](ActionCli.md) | run actions as local commands, or call a server from commands; shape flags, input and output; add a command of your own |
53
- | [Authentication.md](Authentication.md) | declare how callers sign in, verify credentials, publish OAuth discovery, step up for a scope |
54
- | [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 |
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
- Each example's README says where to look for what. The index in
71
- [kitchen-sink](../examples/kitchen-sink/README.md#index) maps every export and option it uses to
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)).
@@ -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 the cause as an error on the process console, under `it.effect` too, and nothing for a request that succeeds or is refused: read the test's output, or collect it with a `Logger` or an `ErrorReporter` provided around the layer.
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
- - A test passes whatever message an error carries: `toEqual` never compares a tagged error's `message`. Assert it separately ([Assert on errors](Testing.md#assert-on-errors)).
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.