@gjermundgaraba/effect-actions 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +103 -43
  2. package/dist/Action.d.mts +31 -118
  3. package/dist/Action.mjs +15 -53
  4. package/dist/ActionCli.d.mts +60 -28
  5. package/dist/ActionCli.mjs +54 -48
  6. package/dist/ActionHttp.d.mts +18 -67
  7. package/dist/ActionHttp.mjs +20 -70
  8. package/dist/ActionMcp.d.mts +25 -31
  9. package/dist/ActionMcp.mjs +140 -74
  10. package/dist/ActionRpc.d.mts +147 -0
  11. package/dist/ActionRpc.mjs +113 -0
  12. package/dist/ActionToolkit.d.mts +5 -8
  13. package/dist/ActionToolkit.mjs +3 -3
  14. package/dist/Authentication.d.mts +15 -15
  15. package/dist/Authentication.mjs +42 -96
  16. package/dist/Testing.d.mts +32 -25
  17. package/dist/Testing.mjs +45 -30
  18. package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
  19. package/dist/authentication/provider.mjs +12 -0
  20. package/dist/authentication/refusal.mjs +36 -0
  21. package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
  22. package/dist/cli/commands.mjs +302 -0
  23. package/dist/{internal → contract}/call.d.mts +2 -2
  24. package/dist/contract/call.mjs +10 -0
  25. package/dist/{internal → contract}/errors.d.mts +13 -10
  26. package/dist/{internal → contract}/errors.mjs +9 -14
  27. package/dist/{internal → contract}/implementation.d.mts +37 -21
  28. package/dist/{internal → contract}/implementation.mjs +18 -49
  29. package/dist/contract/media.d.mts +13 -0
  30. package/dist/contract/media.mjs +18 -0
  31. package/dist/contract/rules.d.mts +25 -0
  32. package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
  33. package/dist/{internal → http}/client.d.mts +7 -9
  34. package/dist/http/client.mjs +17 -0
  35. package/dist/http/declared.mjs +20 -0
  36. package/dist/http/schema-errors.mjs +9 -0
  37. package/dist/mcp/protocol.d.mts +17 -0
  38. package/dist/mcp/protocol.mjs +149 -0
  39. package/dist/stdio/console.mjs +64 -0
  40. package/dist/{internal → testing}/memory.d.mts +1 -1
  41. package/dist/{internal → testing}/memory.mjs +5 -17
  42. package/dist/toolkit/tools.mjs +29 -0
  43. package/docs/Action-failures.md +47 -0
  44. package/docs/Action-in-process.md +93 -0
  45. package/docs/Action.md +206 -424
  46. package/docs/ActionCli-failures.md +102 -0
  47. package/docs/ActionCli.md +206 -292
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +202 -281
  50. package/docs/ActionMcp-failures.md +31 -0
  51. package/docs/ActionMcp.md +229 -193
  52. package/docs/ActionRpc-failures.md +21 -0
  53. package/docs/ActionRpc.md +178 -0
  54. package/docs/ActionToolkit-failures.md +15 -0
  55. package/docs/ActionToolkit.md +100 -117
  56. package/docs/Authentication-advanced.md +139 -0
  57. package/docs/Authentication-failures.md +99 -0
  58. package/docs/Authentication.md +142 -392
  59. package/docs/CONTEXT.md +39 -23
  60. package/docs/README.md +71 -35
  61. package/docs/Testing-failures.md +20 -0
  62. package/docs/Testing.md +186 -151
  63. package/docs/guarantees.md +35 -95
  64. package/docs/setup-failures.md +34 -0
  65. package/docs/setup.md +88 -87
  66. package/examples/README.md +68 -0
  67. package/examples/assistant/README.md +92 -0
  68. package/examples/assistant/src/agent.ts +92 -0
  69. package/examples/assistant/src/cli.ts +34 -0
  70. package/examples/assistant/src/commands.ts +25 -0
  71. package/examples/assistant/src/identity/Identity.ts +33 -0
  72. package/examples/assistant/src/identity/authorize.ts +24 -0
  73. package/examples/assistant/src/identity/handlers.ts +10 -0
  74. package/examples/assistant/src/identity/members.ts +9 -0
  75. package/examples/assistant/src/identity/permissions.ts +23 -0
  76. package/examples/assistant/src/scripted.ts +23 -0
  77. package/examples/assistant/src/tasks/Tasks.ts +63 -0
  78. package/examples/assistant/src/tasks/handlers.ts +25 -0
  79. package/examples/assistant/src/tasks/store.ts +96 -0
  80. package/examples/assistant/src/toolkit.ts +30 -0
  81. package/examples/assistant/test/commands.test.ts +57 -0
  82. package/examples/assistant/test/identity/identity.test.ts +43 -0
  83. package/examples/assistant/test/tasks/store.test.ts +48 -0
  84. package/examples/assistant/test/tasks/tasks.test.ts +75 -0
  85. package/examples/assistant/test/toolkit.test.ts +121 -0
  86. package/examples/kitchen-sink/README.md +317 -0
  87. package/examples/kitchen-sink/src/Actions.ts +19 -0
  88. package/examples/kitchen-sink/src/Api.ts +24 -0
  89. package/examples/kitchen-sink/src/Rpc.ts +12 -0
  90. package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
  91. package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
  92. package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
  93. package/examples/kitchen-sink/src/admin/grants.ts +22 -0
  94. package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
  95. package/examples/kitchen-sink/src/admin/keys.ts +8 -0
  96. package/examples/kitchen-sink/src/agent.ts +47 -0
  97. package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
  98. package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
  99. package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
  100. package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
  101. package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
  102. package/examples/kitchen-sink/src/attachments/download.ts +49 -0
  103. package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
  104. package/examples/kitchen-sink/src/attachments/store.ts +58 -0
  105. package/examples/kitchen-sink/src/browser.ts +33 -0
  106. package/examples/kitchen-sink/src/cli.ts +85 -0
  107. package/examples/kitchen-sink/src/config.ts +18 -0
  108. package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
  109. package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
  110. package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
  111. package/examples/kitchen-sink/src/identity/directory.ts +52 -0
  112. package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
  113. package/examples/kitchen-sink/src/identity/members.ts +13 -0
  114. package/examples/kitchen-sink/src/identity/quota.ts +62 -0
  115. package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
  116. package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
  117. package/examples/kitchen-sink/src/issues/guide.ts +37 -0
  118. package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
  119. package/examples/kitchen-sink/src/issues/store.ts +96 -0
  120. package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
  121. package/examples/kitchen-sink/src/memory.ts +30 -0
  122. package/examples/kitchen-sink/src/ops.ts +42 -0
  123. package/examples/kitchen-sink/src/request-policy.ts +63 -0
  124. package/examples/kitchen-sink/src/routes.ts +120 -0
  125. package/examples/kitchen-sink/src/sdk.ts +46 -0
  126. package/examples/kitchen-sink/src/seed.ts +65 -0
  127. package/examples/kitchen-sink/src/server.ts +62 -0
  128. package/examples/kitchen-sink/src/stdio.ts +34 -0
  129. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
  130. package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
  131. package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
  132. package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
  133. package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
  134. package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
  135. package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
  136. package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
  137. package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
  138. package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
  139. package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
  140. package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
  141. package/examples/kitchen-sink/test/routes.test.ts +123 -0
  142. package/examples/kitchen-sink/test/support/scripted.ts +40 -0
  143. package/examples/live-rpc/README.md +107 -0
  144. package/examples/live-rpc/src/Rpc.ts +8 -0
  145. package/examples/live-rpc/src/browser.ts +98 -0
  146. package/examples/live-rpc/src/chat/Chat.ts +75 -0
  147. package/examples/live-rpc/src/chat/authorize.ts +16 -0
  148. package/examples/live-rpc/src/chat/handlers.ts +50 -0
  149. package/examples/live-rpc/src/chat/room.ts +23 -0
  150. package/examples/live-rpc/src/chat/store.ts +54 -0
  151. package/examples/live-rpc/src/client.ts +37 -0
  152. package/examples/live-rpc/src/identity/Identity.ts +19 -0
  153. package/examples/live-rpc/src/identity/members.ts +8 -0
  154. package/examples/live-rpc/src/identity/verifier.ts +23 -0
  155. package/examples/live-rpc/src/routes.ts +22 -0
  156. package/examples/live-rpc/src/server.ts +43 -0
  157. package/examples/live-rpc/test/browser.test.ts +105 -0
  158. package/examples/live-rpc/test/chat/chat.test.ts +69 -0
  159. package/examples/live-rpc/test/routes.test.ts +99 -0
  160. package/examples/live-rpc/test/server.test.ts +75 -0
  161. package/examples/mcp-tools/README.md +136 -0
  162. package/examples/mcp-tools/src/instructions.ts +12 -0
  163. package/examples/mcp-tools/src/printers/Printers.ts +119 -0
  164. package/examples/mcp-tools/src/printers/fake.ts +115 -0
  165. package/examples/mcp-tools/src/printers/farm.ts +136 -0
  166. package/examples/mcp-tools/src/printers/handlers.ts +29 -0
  167. package/examples/mcp-tools/src/routes.ts +52 -0
  168. package/examples/mcp-tools/src/server.ts +30 -0
  169. package/examples/mcp-tools/src/stdio.ts +26 -0
  170. package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
  171. package/examples/mcp-tools/test/routes.test.ts +202 -0
  172. package/examples/notes-service/README.md +108 -0
  173. package/examples/notes-service/src/Api.ts +13 -0
  174. package/examples/notes-service/src/browser.ts +41 -0
  175. package/examples/notes-service/src/cli.ts +22 -0
  176. package/examples/notes-service/src/commands.ts +89 -0
  177. package/examples/notes-service/src/identity/Identity.ts +31 -0
  178. package/examples/notes-service/src/identity/authorize.ts +18 -0
  179. package/examples/notes-service/src/identity/handlers.ts +10 -0
  180. package/examples/notes-service/src/identity/users.ts +9 -0
  181. package/examples/notes-service/src/identity/verifier.ts +36 -0
  182. package/examples/notes-service/src/notes/Notes.ts +91 -0
  183. package/examples/notes-service/src/notes/handlers.ts +33 -0
  184. package/examples/notes-service/src/notes/store.ts +73 -0
  185. package/examples/notes-service/src/routes.ts +45 -0
  186. package/examples/notes-service/src/server.ts +26 -0
  187. package/examples/notes-service/src/status/Status.ts +10 -0
  188. package/examples/notes-service/src/status/handlers.ts +14 -0
  189. package/examples/notes-service/src/stdio.ts +25 -0
  190. package/examples/notes-service/test/commands.test.ts +134 -0
  191. package/examples/notes-service/test/notes/notes.test.ts +103 -0
  192. package/examples/notes-service/test/routes.test.ts +84 -0
  193. package/examples/quickstart/README.md +31 -0
  194. package/examples/quickstart/src/Api.ts +5 -0
  195. package/examples/quickstart/src/client.ts +13 -0
  196. package/examples/quickstart/src/greeting/Greeting.ts +12 -0
  197. package/examples/quickstart/src/greeting/handlers.ts +6 -0
  198. package/examples/quickstart/src/routes.ts +11 -0
  199. package/examples/quickstart/src/server.ts +16 -0
  200. package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
  201. package/package.json +32 -14
  202. package/dist/internal/actions.d.mts +0 -9
  203. package/dist/internal/authentication.mjs +0 -11
  204. package/dist/internal/call.mjs +0 -12
  205. package/dist/internal/cli.mjs +0 -328
  206. package/dist/internal/client.mjs +0 -25
  207. package/dist/internal/console.mjs +0 -77
  208. package/dist/internal/declared.d.mts +0 -13
  209. package/dist/internal/declared.mjs +0 -34
  210. package/dist/internal/mcp.d.mts +0 -11
  211. package/dist/internal/mcp.mjs +0 -52
  212. package/dist/internal/refusal.mjs +0 -67
  213. package/dist/internal/schema-errors.mjs +0 -20
  214. package/dist/internal/tools.mjs +0 -44
@@ -1,405 +1,155 @@
1
1
  # Authentication
2
2
 
3
- How a remote caller proves the identity a protected contract declares, and its verification on
4
- the server. A contract states its identity, `caller: CurrentActor` ([Action.md](Action.md#rules)).
5
- `make` declares how a caller proves it: a named, browser-safe descriptor of one native scheme,
6
- Bearer unless it names another, which a binding and an MCP endpoint serving protected actions
7
- name. `layer` is that
8
- descriptor's provider: the server-only verifier, built once per layer graph, which every HTTP or
9
- MCP layer serving protected actions requires, and which authenticates their requests before
10
- decoding them. The surfaces enforce it with Effect's native endpoint security, which also
11
- documents it in OpenAPI. Given an OAuth protected resource, a Bearer provider publishes the resource's
12
- RFC 9728 discovery and names it in every challenge, so an MCP client that is refused finds its
13
- authorization server and steps up to a scope it lacks. Token verification, login, and consent
14
- stay in the application. Action-level authorization belongs in the implementation's
15
- `authorize`; record-level checks belong in the handler's data access
16
- ([guarantees.md](guarantees.md#authorization)).
3
+ How a remote caller proves the identity a protected action declares. `Authentication.make`
4
+ declares it, as a browser-safe descriptor; `Authentication.layer` is that descriptor's provider,
5
+ running the verifier you write. Verifying a token, login and consent stay in the application.
6
+
7
+ Read this page alone, in one tool call: pages read together are cut. When something fails,
8
+ search [Authentication-failures.md](Authentication-failures.md). Reading order: [README.md](README.md).
17
9
 
18
10
  ## API
19
11
 
20
12
  Import `@gjermundgaraba/effect-actions/Authentication`.
21
13
 
22
- | API | Purpose |
23
- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
24
- | `make(name, identity, { security, error }?)` | A browser-safe authentication descriptor: how a caller proves `identity`, one native scheme. |
25
- | `layer(descriptor, verify, options?)` | Its provider: the server-only verifier, or an Effect building it once per layer graph, and discovery. |
26
- | `protect(descriptor)` | Router middleware authenticating a route of the host's own as the descriptor's actions are. |
27
- | `bearerTokenOf(authorization)` | The bearer token of an `Authorization` header value or `null`, an `Option`, for an unrouted caller. |
28
- | `refusalResponse(error, options?)` | The response authentication answers a refusal with, for a caller the router never routes. |
29
- | `Any` | Any descriptor, erased: what a binding's and an MCP endpoint's `authentication` take. |
30
- | `Descriptor`, `Provider`, `Protection` | What `make`, `layer` and `protect` return, to name in a package that emits declarations. |
31
- | `Verify`, `Options`, `LayerOptions` | The verifier's type, of the credential its descriptor's scheme decodes, and `make`'s and `layer`'s options. |
32
- | `ProtectedResource`, `RefusalResponseOptions` | An OAuth protected resource, and what `refusalResponse` takes. |
33
-
34
- `name` is a literal, unique per declaration: it names the one provider that satisfies the
35
- descriptor, so a provider of another descriptor of the same identity does not. `security` is one
36
- native `HttpApiSecurity` scheme, `HttpApiSecurity.bearer` when omitted: another is
37
- `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`, an API key in a header or the query,
38
- or `HttpApiSecurity.basic`. Exported type: `Options`. Options whose `security` may be left
39
- out, an optional property or a union with options lacking it, type the verifier's credential as
40
- that scheme's or Bearer's, as at run time; an explicit type argument naming a scheme requires
41
- the options argument. `error` is what the verifier may fail with besides a refusal, such as
42
- its issuer being unreachable: one schema, or a list, held as a list, refused where it encodes with
43
- a built-in error's `_tag`. A misspelled option is a type error. A descriptor's public fields are
44
- `name`, `identity`, `security` and `error`; its `~` keys are the library's own wiring, which no
45
- host composes.
46
-
47
- `verify` is the verifier itself, or a builder of it, like the one `Action.implement` takes
48
- ([Action.md](Action.md#implementations)): an Effect that yields startup services, such as a token
49
- verifier, and returns the verifier, run once per layer graph.
50
-
51
- A verifier receives the credential the descriptor's native scheme decodes, a `Redacted<string>`
52
- token or key, or Basic's `{ username, password }`, and succeeds with the identity, or fails with
53
- `Action.Unauthenticated`, `Action.Forbidden`, or an error its descriptor declares in `error`. A
54
- missing or empty credential is refused before it runs. It reads the request the router provides;
55
- another service it reads per request comes from router middleware provided after the
56
- authentication.
57
-
58
- `options.protectedResource`, for a Bearer descriptor only, is an OAuth protected resource, which
59
- the provider publishes and names in every challenge, or an Effect that builds one, or none, where
60
- it is known only at startup: it runs with the verifier's build, once per layer graph, and the
61
- provider's layer requires what it yields. Exported type: `ProtectedResource`.
62
-
63
- `refusalResponse` takes `authentication`, the descriptor refusing, `protectedResource`, the one
64
- given to `layer`, and `authorization`, the request's `Authorization` header. Under a descriptor of
65
- another scheme it answers as that descriptor's routes do: the JSON and status, `no-store`, a 401
66
- naming that scheme, `Basic realm="<name>"` or the `Http` scheme, none for an API key, and no step-up
67
- challenge. Left out, or a Bearer descriptor, it answers as Bearer's. It also takes an error
68
- `authentication` declares, answered as the routes answer it. Exported type:
69
- `RefusalResponseOptions`.
70
-
71
- | Protected resource option | Meaning |
72
- | ------------------------- | --------------------------------------------------------------------------------------------- |
73
- | `resource` | Required exact OAuth resource identifier; its path selects the discovery path. |
74
- | `authorizationServers` | Required, nonempty: where clients get tokens. |
75
- | `scopesSupported` | Optional OAuth scope tokens: every scope the resource accepts, published in discovery. |
76
- | `scopesRequired` | Optional, nonempty OAuth scope tokens: every 401 names them, and a first login requests them. |
77
- | `resourceName` | Optional human-readable name. |
78
-
79
- ## Canonical
14
+ | API | Purpose |
15
+ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
16
+ | `make(name, identity, { security, error }?)` | A browser-safe authentication descriptor: how a caller proves `identity`, by one native scheme. |
17
+ | `layer(descriptor, verify, options?)` | Its provider: the server-only verifier, or an Effect building it once per layer graph, and discovery. |
18
+ | `Any` | Any descriptor, erased: what a binding's and an MCP endpoint's `authentication` take. |
19
+ | `Descriptor`, `Provider` | What `make` and `layer` return, to name in a package that emits declarations. |
20
+ | `Verify`, `Options`, `LayerOptions` | The verifier's type, and `make`'s and `layer`'s options. |
21
+ | `ProtectedResource` | An OAuth protected resource. |
22
+ | `protect`, `bearerTokenOf`, `refusalResponse` | In [Authentication-advanced.md](Authentication-advanced.md#api), with their types and options. |
23
+
24
+ | Option | Of | Meaning |
25
+ | ------------------- | ------- | --------------------------------------------------------------------------------------- |
26
+ | `security` | `make` | One native `HttpApiSecurity` scheme; `HttpApiSecurity.bearer` when omitted. |
27
+ | `error` | `make` | What the verifier may fail with besides a refusal: one schema, or a list. |
28
+ | `protectedResource` | `layer` | Bearer only: an OAuth protected resource to publish, or an Effect building one or none. |
29
+
30
+ | Protected resource field | Meaning |
31
+ | ------------------------ | -------------------------------------------------------------------------------------- |
32
+ | `resource` | Required exact OAuth resource identifier; its path and query select the discovery URL. |
33
+ | `authorizationServers` | Required, nonempty: where clients get tokens. |
34
+ | `scopesSupported` | Optional OAuth scope tokens: every scope the resource accepts. |
35
+ | `scopesRequired` | Optional, nonempty OAuth scope tokens: every 401 names them. |
36
+ | `resourceName` | Optional human-readable name. |
37
+
38
+ ## Declare how callers sign in
39
+
40
+ A protected action declares its identity service, `caller: CurrentUser`
41
+ ([Action.md](Action.md#say-who-may-call-an-action)). Declare the descriptor beside that
42
+ service, and name it in every binding and MCP endpoint that serves a protected action:
80
43
 
81
- The example app's authentication, in two modules. The binding, which servers and clients import,
82
- names the descriptor, `Login`, beside its actions ([ActionHttp.md](ActionHttp.md#canonical)):
83
-
84
- ```ts example=binding.ts
85
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
86
- import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
87
- import { CurrentActor } from "./authorization.js";
88
- import { Double, GetUser, RenameUser, Status, WhoAmI } from "./contracts.js";
89
-
90
- // Browser-safe: how a remote caller proves it is CurrentActor, a bearer token unless
91
- // `security` names another native scheme. The verifier lives in authentication.ts. The literal
92
- // name identifies the verifier that may provide it.
93
- export const Login = Authentication.make("example.Login", CurrentActor);
44
+ ```ts
45
+ // identity/Identity.ts
46
+ export const Login = Authentication.make("notes-service.Login", CurrentUser);
94
47
 
95
- // Protected contracts get native bearer security (enforced and documented); `status`,
96
- // declared `caller: Action.Anyone`, gets none.
97
- export const Http = ActionHttp.make([Status, GetUser, RenameUser, Double, WhoAmI], {
48
+ // Api.ts
49
+ export const Http = ActionHttp.make([GetStatus, ...Notes, WhoAmI], {
50
+ prefix: "/api",
98
51
  authentication: Login,
99
52
  });
100
53
  ```
101
54
 
102
- The server provides `Login`'s verifier, `authenticate`, to every layer serving protected
103
- actions ([ActionHttp.md](ActionHttp.md#serving), [ActionMcp.md](ActionMcp.md#canonical)). It
104
- answers a missing or unknown token with the built-in 401 and publishes the resource's discovery.
105
-
106
- ```ts example=authentication.ts
107
- import { Effect, Redacted } from "effect";
108
- import * as Action from "@gjermundgaraba/effect-actions/Action";
109
- import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
110
- import { type Actor, actors } from "./authorization.js";
111
- import { Login } from "./binding.js";
112
-
113
- // DEMO ONLY: a token is an actor's name. Verify real tokens with your authorization
114
- // server's library instead, their audience included: issued for this resource.
115
- const isActorToken = (token: string): token is keyof typeof actors => Object.hasOwn(actors, token);
116
-
117
- // Each request's token, as the native `HttpApiSecurity.bearer` decodes it, to its actor. A
118
- // missing or empty token never reaches it; an unknown one is the built-in `Unauthenticated`,
119
- // a 401 every client decodes, as is a missing one.
120
- export const verify = (
121
- token: Redacted.Redacted<string>,
122
- ): Effect.Effect<Actor, Action.Unauthenticated> => {
123
- const name = Redacted.value(token);
124
-
125
- return isActorToken(name)
126
- ? Effect.succeed(actors[name])
127
- : Effect.fail(new Action.Unauthenticated({ message: "Unknown demo token." }));
128
- };
129
-
130
- // As an OAuth protected resource, it publishes RFC 9728 discovery, public, and every challenge
131
- // names it, so an MCP client that was refused finds the server issuing its tokens. A first
132
- // login requests read only; a write refused for its scope steps up.
133
- export const protectedResource = {
134
- resource: "http://localhost:3000/mcp",
135
- authorizationServers: ["https://auth.example.com"],
136
- scopesSupported: ["users:read", "users:write"],
137
- scopesRequired: ["users:read"],
138
- } satisfies Authentication.ProtectedResource;
139
-
140
- // Server-only: `Login`'s verifier, provided to every layer serving protected actions. An
141
- // Effect building it instead yields startup services, such as a token verifier, as a handler
142
- // builder does; this demo needs none.
143
- export const authenticate = Authentication.layer(Login, verify, {
144
- protectedResource,
145
- });
146
- ```
147
-
148
- ### Outer and inner middleware
149
-
150
- A host serving tenants on their own subdomains: a verifier built once at startup; each request's
151
- tenant from native router middleware, provided after the authentication, which the verifier
152
- reads per request; and endpoint middleware reading the identity, the HTTP layer's `middleware`,
153
- which runs inside the authentication.
154
-
155
- ```ts example=authentication-tenant.ts
156
- import { Context, Effect, Layer, Redacted } from "effect";
157
- import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
158
- import { HttpApiMiddleware } from "effect/http-api";
159
- import * as Action from "@gjermundgaraba/effect-actions/Action";
160
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
161
- import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
162
- import { type Actor, actors, CurrentActor } from "./authorization.js";
163
- import { Http, Login } from "./binding.js";
164
- import { userActions } from "./handlers.js";
165
-
166
- /** Each request's tenant: the first label of its host, `acme` for acme.example.com. */
167
- export class Tenant extends Context.Service<Tenant, string>()("example/Tenant") {}
168
-
169
- // Outer: native router middleware, providing what the verifier reads per request.
170
- const resolveTenant = HttpRouter.middleware<{ provides: Tenant }>()((route) =>
171
- Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) =>
172
- Effect.provideService(route, Tenant, request.headers.host?.split(".")[0] ?? ""),
173
- ),
174
- );
175
-
176
- // Inner: reads the identity, so it is native endpoint middleware, which the layer runs inside
177
- // the authentication of every route it serves, all of them protected.
178
- export class LogCaller extends HttpApiMiddleware.Service<LogCaller, { requires: CurrentActor }>()(
179
- "example/LogCaller",
180
- ) {}
181
-
182
- const LogCallerLive = Layer.succeed(LogCaller, (route) =>
183
- Effect.flatMap(CurrentActor, ({ id }) =>
184
- Effect.map(route, HttpServerResponse.setHeader("x-actor", id)),
185
- ),
186
- );
187
-
188
- /** DEMO ONLY: a startup service verifying tokens, as your authorization server's library would. */
189
- export class Verifier extends Context.Service<
190
- Verifier,
191
- {
192
- readonly verify: (
193
- token: Redacted.Redacted<string>,
194
- ) => Effect.Effect<Actor, Action.Unauthenticated>;
195
- }
196
- >()("example/Verifier") {
197
- static readonly layer = Layer.sync(Verifier, () => {
198
- const known = new Map<string, Actor>(Object.entries(actors));
199
-
200
- return Verifier.of({
201
- verify: (token) =>
202
- Effect.fromNullishOr(known.get(Redacted.value(token))).pipe(
203
- Effect.mapError(() => new Action.Unauthenticated({ message: "Unknown demo token." })),
204
- ),
205
- });
206
- });
207
- }
208
-
209
- // Like a handler builder: the verifier once, at startup; the token and the tenant per
210
- // request. An actor of another tenant is refused.
211
- const authenticate = Authentication.layer(
212
- Login,
213
- Effect.gen(function* () {
214
- const verifier = yield* Verifier;
215
-
216
- return (token: Redacted.Redacted<string>) =>
217
- Effect.gen(function* () {
218
- const actor = yield* verifier.verify(token);
219
-
220
- if (actor.tenantId !== (yield* Tenant)) {
221
- return yield* new Action.Forbidden({ message: "Not a member of this tenant." });
222
- }
223
-
224
- return actor;
225
- });
226
- }),
227
- );
228
-
229
- // Provided in order: each `Layer.provide` gives what the layers before it still owe, so
230
- // `resolveTenant`, last, gives the Tenant the verifier reads per request. One array,
231
- // `Layer.provide([authenticate, resolveTenant.layer])`, would leave it owed: an array's
232
- // members provide to the routes, not to one another.
233
- export const routes = ActionHttp.layer(Http, userActions, { middleware: [LogCaller] }).pipe(
234
- Layer.provide([authenticate, LogCallerLive]),
235
- Layer.provide(Verifier.layer),
236
- Layer.provide(resolveTenant.layer),
237
- );
238
- ```
239
-
240
- ### A scheme other than Bearer
241
-
242
- A descriptor naming another native scheme, here a session cookie, gives its verifier the
243
- credential that scheme decodes. Nothing steps up under it, and it publishes no protected
244
- resource.
245
-
246
- ```ts
247
- export const SessionLogin = Authentication.make("example.SessionLogin", CurrentActor, {
248
- security: HttpApiSecurity.apiKey({ in: "cookie", key: "session" }),
249
- });
250
-
251
- export const authenticateSession = Authentication.layer(
252
- SessionLogin,
253
- Effect.map(
254
- Sessions,
255
- (sessions) => (session: Redacted.Redacted<string>) => sessions.actorOf(Redacted.value(session)),
256
- ),
257
- );
258
- ```
259
-
260
- ### A route of your own
261
-
262
- A descriptor covers action endpoints alone. A route of the host's own beside them, such as an
263
- export, a page frame or a WebSocket upgrade, takes `protect(descriptor)`, native router
264
- middleware: the descriptor's provider verifies the request and gives the route the identity,
265
- and the route is answered as an action's, its refusals, challenges and caching included. Its
266
- own rule, such as a scope, fails with a refusal.
267
-
268
- ```ts example=authentication-route.ts
269
- import { Effect, Layer } from "effect";
270
- import { HttpRouter, HttpServerResponse } from "effect/http";
271
- import * as Action from "@gjermundgaraba/effect-actions/Action";
272
- import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
273
- import { CurrentActor } from "./authorization.js";
274
- import { Login } from "./binding.js";
275
-
276
- // A route of the host's own beside the actions, authenticated as they are: the binding's
277
- // descriptor's provider verifies the request and gives the route the actor. A refusal it
278
- // fails with is answered as an action's, and a scope a caller lacks steps up under Bearer.
279
- export const exportRoute = HttpRouter.add(
280
- "GET",
281
- "/export",
282
- Effect.gen(function* () {
283
- const actor = yield* CurrentActor;
284
-
285
- if (!actor.permissions.includes("users:read")) {
286
- return yield* new Action.Forbidden({
287
- message: "Requires users:read.",
288
- scopes: ["users:read"],
289
- });
290
- }
291
-
292
- return HttpServerResponse.text(`Users of ${actor.tenantId}.`);
293
- }),
294
- ).pipe(Layer.provide(Authentication.protect(Login).layer));
295
- ```
296
-
297
- ### Outside the router
298
-
299
- A caller the router never routes, such as a Node `upgrade` handler admitting a socket,
300
- authenticates its own header and refuses with `refusalResponse`: the response authentication
301
- answers the same refusal with on a route.
302
-
303
- ```ts example=authentication-upgrade.ts
304
- import { Effect } from "effect";
305
- import { HttpServerResponse } from "effect/http";
306
- import * as Action from "@gjermundgaraba/effect-actions/Action";
307
- import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
308
- import { protectedResource, verify } from "./authentication.js";
309
- import type { Actor } from "./authorization.js";
310
-
311
- // A caller the router never routes, such as a Node `upgrade` handler admitting a socket,
312
- // which authenticates the `Authorization` header itself: read as a route's is, with
313
- // `bearerTokenOf`, and verified by the routes' own `verify`. A socket writes, so it needs the
314
- // write scope. It gives the actor, or the response to refuse with, the one authentication
315
- // answers the same refusal with on a route.
316
- export const admit = (authorization: string | undefined): Effect.Effect<Actor, Response> =>
317
- Effect.fromOption(
318
- Authentication.bearerTokenOf(authorization),
319
- () => new Action.Unauthenticated(),
320
- ).pipe(
321
- Effect.flatMap(verify),
322
- Effect.filterOrFail(
323
- (actor) => actor.permissions.includes("users:write"),
324
- () => new Action.Forbidden({ message: "Requires users:write.", scopes: ["users:write"] }),
325
- ),
326
- Effect.mapError((error) =>
327
- HttpServerResponse.toWeb(
328
- Authentication.refusalResponse(error, { protectedResource, authorization }),
329
- ),
330
- ),
331
- );
332
- ```
333
-
334
- ## Rules
335
-
336
- - `make` is browser-safe and declares no verifier: the binding imports it, and so does every client of the binding. Keep `layer`, and what its verifier imports, in server-only modules.
337
- - One descriptor per identity and resource. Its name literal is its type identity: a layer serving protected actions requires `Provider<CurrentActor, "example.Login">`, which only `layer(Login, ...)` provides. A provider of another descriptor, even of the same identity, does not satisfy it. Reuse one name only for one declaration. A scheme the deployment decides, such as a cookie name a framework prefixes over HTTPS, is still one declaration: the server builds its descriptor and binding at startup under the same name and identity, and a browser's binding keeps a static one, since a client never reads the scheme. A server builds its binding and its provider from one descriptor value: `ActionHttp.layer` refuses a binding whose descriptor is not the one its provider was built from.
338
- - The name is also its scheme's OpenAPI component key, as it is, so it holds only letters, digits, `_`, `.` and `-`: `example.Login`, not `example/Login`. `make` throws for any other.
339
- - A descriptor names one native `HttpApiSecurity` scheme, decoded as Effect decodes it: `HttpApiSecurity.bearer` by default, for a credential sent as `Authorization: Bearer`, such as an OAuth access token; or another, such as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })` for a browser session, an API key in a header or the query, or `HttpApiSecurity.basic`. A record of schemes, or anything that is no native scheme, is refused by the types, and throws `Authentication takes one native HttpApiSecurity scheme`. There is no fallback between schemes: a host accepting two credentials serves them on two bindings or endpoints, each naming its own descriptor.
340
- - OAuth is Bearer's: only a Bearer descriptor's provider takes a `protectedResource`, which the types refuse for another scheme and `layer` throws for, and only under a Bearer descriptor does a protected action's refusal step up ([guarantees.md](guarantees.md#authorization)); a public route's or a public tool's never does, signed in or not. Under another scheme, a refusal over HTTP is its JSON and status, with no `insufficient_scope` challenge, and over MCP a tool's refusal, from the authorizer or a handler, is an `isError` tool result, as on an endpoint without authentication; the authentication's own refusal is still its 401 or 403. Its 401 carries the challenge its scheme has: an `Http` scheme names itself, such as `Token`; Basic sends `Basic realm="<descriptor name>"`, quoted and escaped; an API key sends none. Responses keep `Cache-Control: no-store`.
341
- - Basic's credential counts as presented when its user-id or its password is non-empty, so a token sent as the password, `Basic base64(":token")`, reaches the verifier. A cookie-authenticated browser client sends its cookie itself; whether a cross-origin page may is the host's CORS policy ([ActionHttp.md](ActionHttp.md#rules)).
342
- - A binding holding a protected action names its descriptor, `ActionHttp.make(actions, { authentication: Login })`, and so does an MCP endpoint serving one, `ActionMcp.layerHttp(apps, { authentication: Login })`: required, and a descriptor of another identity is refused, in the types and when called. One identity per binding or endpoint: every protected action it holds declares the descriptor's.
343
- - The descriptor both enforces and documents. Each protected endpoint of a binding carries Effect's native security middleware for it, which `OpenApi.fromApi`, Swagger and Scalar show as the operation's `security`, `[{ "example.Login": [] }]`, and in `components.securitySchemes`, keyed by the descriptor's name: distinct descriptors have distinct names, so their schemes never share a key in one combined document; a public endpoint states `security: []`. Clients are unchanged: they send whatever credential `transformClient` adds.
344
- - Every HTTP or MCP layer serving a protected action requires the provider: `Layer.provide(authenticate)`. Give each the same `layer` value: it is built once per layer graph, however many layers it is provided to, and publishes discovery once. A layer serving only public actions requires none.
345
- - Authentication runs before decoding: a protected request without a credential that verifies is refused before its body is read ([guarantees.md](guarantees.md#authorization)).
346
- - The verifier succeeds with the identity value, fails with a refusal, or fails with an error its descriptor declares. `Unauthenticated` is sent as its JSON with **401**; `Forbidden` as its JSON with **403**. Both are the bodies every endpoint declares, so typed clients decode them. A declared error is sent as its JSON with its `httpApiStatus`, or 422 without one, as every protected endpoint of a binding naming the descriptor declares it, so `ActionHttp.client` and a remote CLI command decode it as a typed failure; a public endpoint, which nothing verifies, does not declare it. A route, an MCP endpoint and `protect` send it alike, `no-store`, and challenge it only as a 401. It can fail with nothing else: any other error is a type error, and in plain JavaScript a defect, an empty 500.
347
- - A failure that is not the caller's, such as an issuer the verifier cannot reach, is a declared error, `error: ProviderUnavailable` with `httpApiStatus: 503`, not a refusal: an `Unauthenticated` would have the caller sign in again, which cannot help. Over MCP it is the endpoint's HTTP answer, before any tool runs, not a tool result: `Testing.mcpClient` fails with an `McpCallError` naming its status and body.
348
- - It provides the identity to protected routes alone. A public route never gets it, beside protected ones in one layer too, and over HTTP ignores any credential the request carries. A public tool's call on an MCP endpoint naming a descriptor is verified when it presents a credential, refused with 401 when that does not verify, as MCP authorization requires of an invalid or expired token, and passes without one, or with one its scheme decodes as empty, such as `Authorization: Bearer` alone; the tool still gets no identity.
349
- - Nothing else provides a protected action's identity remotely: neither a value provided at startup, around the server or the program, nor `HttpRouter.provideRequest`, nor other router middleware satisfies a layer serving it, which requires the provider ([guarantees.md](guarantees.md#authorization)).
350
- - Every response to a request it authenticates gets `Cache-Control: no-store` unless its route states its own caching, and every 401 among them its scheme's challenge: [guarantees.md](guarantees.md#wire-behavior).
351
- - A Bearer descriptor's 401 challenge is `Bearer`, or as a protected resource `Bearer scope="<scopesRequired>", resource_metadata="<metadata URL>"`, `scope` only when `scopesRequired` is given.
352
- - A 401 to a request that presented a bearer token also names `error="invalid_token"` first (RFC 6750), whoever refused it: the token did not authenticate it, and a client may refresh it before it signs in again. A malformed token, such as `Bearer a b`, is a presented one (RFC 6750 §3.1). A request without one, or with another scheme such as `Basic`, gets no error code.
353
- - An MCP client requests the scopes a 401 names, or every one of `scopesSupported` when it names none. Give `scopesRequired` whenever some scopes are needed only by some actions, so a first login asks for the least, and a `Forbidden` naming scopes asks for more when a call needs them.
354
- - Step-up refusals follow [guarantees.md](guarantees.md#authorization), including the MCP exception when a notification has already started the **200** stream: a later refusal is an `isError` tool result without an HTTP challenge.
355
- - When a `Forbidden` naming `scopes` is sent as an HTTP refusal under a Bearer descriptor, its **403** carries `WWW-Authenticate: Bearer error="insufficient_scope", scope="<scopes>"`. As a protected resource it adds `resource_metadata`, and `error_description` when the message is a valid one (printable ASCII without `"` or `\`).
356
- - An OAuth client re-authorizes on that challenge with those scopes added, as MCP authorization requires; the official MCP client does. A `Forbidden` naming no scope has no challenge ([guarantees.md](guarantees.md#authorization)).
357
- - Name scopes only when re-authorizing can grant them. A caller whose credential cannot step up, such as an API key, gets a `Forbidden` naming none: a plain 403, and a tool result over MCP, rather than a login prompt that cannot help.
358
- - The descriptor covers actions alone. A route of the host's own, such as a WebSocket upgrade route, `RpcServer.layerProtocolWebsocket`'s, takes `protect(descriptor)`, native router middleware provided to it, `route.pipe(Layer.provide(Authentication.protect(Login).layer))`, which requires the descriptor's provider ([A route of your own](#a-route-of-your-own)). The credential the descriptor's scheme decodes is verified by the provider's verifier, and the route is given the identity. It is answered as an action's route: without a credential that verifies, with the refusal and challenge, or the error the verifier declares; its responses are marked and challenged as an action route's ([guarantees.md](guarantees.md#wire-behavior)). A refusal the route fails with is answered as an action's: `Unauthenticated` as 401, and `Forbidden` as 403, naming its `scopes` in an `insufficient_scope` challenge under Bearer, so an OAuth client steps up. `.combine` puts middleware reading the identity inside it.
359
- - A caller the router never routes, such as a Node `upgrade` handler, verifies its own credential and answers a refusal with `refusalResponse(error, { protectedResource, authorization })`: the status, JSON, `Cache-Control: no-store` and challenge authentication answers that refusal with, by the rules above, `invalid_token` when `authorization` presented a bearer token. An error the descriptor declares takes the descriptor too, `refusalResponse(error, { authentication: Login, protectedResource, authorization })`, and is answered as its routes answer it. Give it the `protectedResource` given to `layer`, or its challenges name no metadata URL and no `scopesRequired`. It is an `HttpServerResponse`; `HttpServerResponse.toWeb` gives a web `Response`. It refuses, by throwing, the resource `layer` refuses: one under a descriptor of another scheme, an invalid `scopesSupported` or `scopesRequired`, or a `resource` with a fragment.
360
- - Such a caller reads its header with `bearerTokenOf(authorization)`, an `Option` of the `Redacted` token, given the header as the web `Headers.get` returns it, `null` when absent: the reading every challenge is decided by and a Bearer descriptor's verifier receives, as Effect's `HttpApiSecurity.bearer` decodes a request's header. Whitespace around the header value is no part of it, as an HTTP parser strips it before a route reads it; then `Bearer`, matched case-insensitively, one or more spaces, and the rest of the header is the token, a malformed one such as `a b` too. No scheme, another scheme, or nothing after the scheme is none.
361
- - Verify that a token was issued for this resource, its audience `resource`, as MCP authorization requires (RFC 8707): one issued for another resource fails with `Unauthenticated`, as any token that does not verify. `layer` publishes `resource` in its discovery, but reads no token.
362
- - An Effect building the verifier runs when the provider's layer is built, once per layer graph however many layers it is provided to, as a handler builder does ([guarantees.md](guarantees.md#dependency-lifetimes)). The services it yields are startup requirements of that layer: `Authentication.layer(Login, build).pipe(Layer.provide(Verifier.layer))`, or provided after it, `routes.pipe(Layer.provide(authenticate), Layer.provide(Verifier.layer))`. A resource it acquires lives as long as the layer; its failure fails the layer, so the server does not start. A verifier needing no startup service is passed as it is, `Authentication.layer(Login, verify)`.
363
- - The services a verifier yields per request, beyond what the router provides, such as the request and its scope, are request requirements, as a handler's are. Native router middleware provides them, provided after the authentication, each in a `Layer.provide` of its own: `routes.pipe(Layer.provide(authenticate), Layer.provide(resolveTenant.layer))`. One array, `Layer.provide([authenticate, resolveTenant.layer])`, gives both to the routes and neither to the other, so the verifier's request requirement stays owed ([Failure modes](#failure-modes)). A startup service, such as a verifier, is yielded by the Effect building the verifier instead, never per request.
364
- - Middleware reading the identity is the HTTP layer's own: `ActionHttp.layer(Http, apps, { middleware: [LogCaller] })`, native `HttpApiMiddleware` that runs inside the authentication of a protected route and before decoding ([ActionHttp.md](ActionHttp.md#rules)). One requiring the identity takes a layer whose `actions` lists only protected ones. An MCP endpoint has none.
365
- - Router middleware provided around the routes runs before their authentication. Middleware that must also cover discovery and unrouted requests, such as a Host or Origin check, or one setting security headers on every answer, is global middleware, `HttpRouter.middleware(check, { global: true })`, merged beside the routes: it runs before routing. Merged before the routes, it also runs before the discovery `layer` publishes.
366
- - A local surface has no remote caller: the host provides the identity service itself, as `Effect.provideService(CurrentActor, actor)`, or on a CLI command, `Command.provideSync(CurrentActor, actor)`. `authorize` still runs. An identity only a host supplies, such as an admin CLI's operator, is typed apart from what a verifier returns, so no token names it ([ActionCli.md](ActionCli.md#trusted-callers)).
367
- - Resources a verifier acquires in the request scope live until that scope closes, including while the handler runs.
368
- - Downstream action errors are handled by their transport. They are never serialized as authentication failures.
369
- - Never provide `CurrentActor` or any identity or tenant tag in a startup layer or root context: [guarantees.md](guarantees.md#dependency-lifetimes).
370
- - A resource read from configuration or a service is given as an Effect: `Authentication.layer(Login, verify, { protectedResource: Effect.map(Settings, ({ resource }) => resource) })`. Succeeding with `undefined` publishes nothing and challenges with a bare `Bearer`.
371
- - A protected resource's discovery is published when the provider's layer is built, once per layer graph, however many layers it is provided to, and needs no route of its own. It publishes what it is given: the deployment must ensure `resource` and `authorizationServers` are valid OAuth URLs (HTTPS, or loopback HTTP in development).
372
- - Discovery is served at `/.well-known/oauth-protected-resource` followed by the resource's path (`/.well-known/oauth-protected-resource/mcp` for `https://host/mcp`), for `GET` and `HEAD`, matching that literal path and query, and answers the CORS preflight there. It answers before routing, so no route middleware, authentication included, covers it. Other requests fall through to the host router. Caching policy is the host's.
373
- - Any origin may read discovery, as a browser MCP client must after a 401: it carries `Access-Control-Allow-Origin: *`, and answers an `OPTIONS` preflight at its URL with **204**, allowing `GET`, `HEAD` and `OPTIONS` and the headers the preflight asks for. Where the host's CORS middleware runs before it, that policy answers discovery's preflight and adds its headers to discovery's reads, so an origin it allows may read discovery, and discovery keeps its `*` where the policy sets no origin. Global middleware runs in the order it registers while layers build, which is the order it is merged in unless something built before it is asynchronous; either way, an origin the host's policy allows may read discovery.
374
- - A host with two resources makes one descriptor and one `layer` per resource; a cookie-authenticated dashboard beside them takes a descriptor of its own, on a binding of its own.
375
- - Tool discovery is never filtered by actor. Action-level authorization belongs in the implementation's `authorize`, which runs with the action contract in hand; write the rule against `action.readOnly` rather than repeating a check in each handler. Record-level checks follow [guarantees.md](guarantees.md#authorization).
376
-
377
- ## Failure modes
378
-
379
- - `Provider<CurrentActor, "example.Login">` among a layer's requirements, `Type 'Provider<...>' is not assignable to type 'never'` where the server is launched, at `Layer.launch` or `NodeRuntime.runMain`, or `Expected 2 arguments, but got 1` at a web handler's `handler(request)`: a layer serves protected actions, and no provider of the binding's or endpoint's descriptor is provided to it. Provide it, `Layer.provide(authenticate)`. A value of the identity provided at startup, around `HttpRouter.serve` or the program, or per request with `HttpRouter.provideRequest`, does not satisfy it, by design. Where the routes require no provider, `Expected 2 arguments` at `handler(request)` means their platform services are not provided instead ([Testing.md](Testing.md#failure-modes)).
380
- - `Provider<CurrentActor, "example.Login">` still owed although a `layer` is provided: it is a provider of another descriptor, of the same identity or not. Provide the provider of the descriptor the binding or endpoint names.
381
- - `No overload matches this call` at `ActionHttp.make`, its last overload naming `"Protected actions take options naming their authentication"`, or a type error naming `authentication` there or at `ActionMcp.layerHttp`: the binding or endpoint serves a protected action and names no descriptor. Give `{ authentication: Login }`. `"Authentication descriptor does not cover every protected action"` instead: the descriptor's identity is not the one the protected actions declare.
382
- - `Protected action '<name>' requires its matching authentication descriptor` thrown by `ActionHttp.make`, `ActionHttp.layer` or `ActionMcp.layerHttp`: plain JavaScript gave no descriptor, or one of another identity. Give the descriptor of the identity the action declares.
383
- - A type error on `security` at `make`, or `Authentication takes one native HttpApiSecurity scheme` thrown by `make`: `security` is a record of schemes, or no native scheme at all. Omit it for Bearer, or give one scheme, `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`.
384
- - A type error on `protectedResource` at `layer`, or `A protected resource is published only for a Bearer scheme` thrown by `layer` or `refusalResponse`: the descriptor names another scheme, which OAuth discovery and challenges do not speak. Drop the resource, or authenticate with a Bearer descriptor.
385
- - `Invalid authentication name: "<name>", not an OpenAPI key` thrown by `make`: the name holds a character an OpenAPI component key cannot, such as `/` or `:`. Use letters, digits, `_`, `.` and `-`: `example.Login`.
386
- - `Missing verify: pass a verify function, or an Effect building one`, a defect when the provider's layer builds: `layer` was given neither a function nor an Effect building one, as plain JavaScript may pass. Pass `verify` itself.
387
- - `Request<"Requires", Tenant>` still owed although the tenant middleware's layer is provided: it was provided in one array with the authentication, `Layer.provide([authenticate, resolveTenant.layer])`, whose members provide to the routes and not to one another. Provide it in a later `Layer.provide` of its own ([Rules](#rules)).
388
- - An action that must be authenticated answers without credentials: its contract states `caller: Action.Anyone`. Make it protected; the surfaces then authenticate it, whatever its handler reads.
389
- - A public tool's call answers 401 on an MCP endpoint serving protected tools too: it presented a credential that does not verify, such as an expired token, and the endpoint verifies a presented credential. Send a valid one, or none.
390
- - An MCP host asks for sign-in when it connects: every tool of the endpoint is protected, so every request authenticates. One serving public tools too lists its tools and runs the public ones without signing in, and a protected tool's call answers the 401 the host signs in on; whether a host signs in then, rather than reporting the error, is the host's.
391
- - Type error at `layer` on `verify`: a verifier 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, and return the identity's type.
392
- - `Authentication "<name>": error _tag "<tag>" is built in, and declared on every surface` thrown by `make`: an `error` entry encodes with the `_tag` of `InvalidInput`, `Unauthenticated` or `Forbidden`. Fail with the built-in refusal itself, which needs no declaring, or give the error a `_tag` of its own.
393
- - A verifier's declared error arrives as 422, which a client reads as its request being wrong: the error states no `httpApiStatus`, and a declared error without one is sent as 422, as an action's is. Give it the status it means, such as `{ httpApiStatus: 503 }` for an issuer the verifier cannot reach.
394
- - `Not a refusal, nor an error the authentication declares` thrown by `refusalResponse`: the error is neither, or the descriptor declaring it was not given. Pass `authentication`.
395
- - `Verifier` unsatisfied at startup: the Effect building the verifier yields it. Provide it to the provider's layer, `Authentication.layer(Login, build).pipe(Layer.provide(Verifier.layer))`, or above it.
396
- - A request the Host or Origin check should refuse gets a 401 and a challenge: the check is the HTTP layer's `middleware`, which runs inside the authentication. Make it router middleware around the routes, or global middleware, merged beside the routes, to cover discovery too ([Rules](#rules)).
397
- - Discovery returns 404: the request path does not match `resource`'s path and query exactly, or the provider is provided to no served layer.
398
- - An MCP client does not find the authorization server: discovery is not at the well-known URL for the endpoint's path. Set `resource` to the endpoint's exact URL.
399
- - An MCP client reports `InsufficientScopeError` instead of re-authorizing: it has no OAuth provider configured, so it cannot step up. Configure one, or grant the scope up front.
400
- - `new Action.Forbidden({ scopes })` throws a schema validation error: a scope is not an OAuth scope token (it is empty, or contains a space, `"` or `\`). Give each scope as its own element.
401
- - An MCP client asks a user who only reads to consent to writes on first login: the 401 names no scope, so it requests every one of `scopesSupported`. Give `scopesRequired`.
402
- - `Authentication "<name>": the binding's descriptor is not its provider's`, a defect when `ActionHttp.layer` builds: the binding names a descriptor built apart from the one `layer(...)` was given, such as a browser's static cookie descriptor served with a provider built at startup from the deployment's, or the same options passed to `make` twice. Give the server's binding the descriptor its provider was built from.
403
- - `Invalid scope in scopesRequired: "<scope>"`, or `in scopesSupported`, thrown by `layer` or `refusalResponse`, or for a built resource a defect when its layer builds: a scope is empty or contains a space, `"` or `\`. Give each scope as its own element.
404
- - `A protected resource has no fragment: <resource>` thrown by `layer` or `refusalResponse`, or for a built resource a defect when its layer builds: `resource` has a `#fragment`, which no request URL carries, so its discovery would never answer (RFC 9728). Drop the fragment.
405
- - A route's `Cache-Control` is replaced by `no-store`: an enclosing middleware serialized the response of a failure. Only a route's own answer keeps its caching.
55
+ See [notes-service/src/identity/Identity.ts](../examples/notes-service/src/identity/Identity.ts)
56
+ and [notes-service/src/Api.ts](../examples/notes-service/src/Api.ts). An MCP endpoint names it
57
+ the same way ([notes-service/src/routes.ts](../examples/notes-service/src/routes.ts)), and so
58
+ does an RPC binding ([live-rpc/src/Rpc.ts](../examples/live-rpc/src/Rpc.ts)).
59
+
60
+ - Every client of the binding imports the descriptor: keep it in the contract file. Keep
61
+ `layer`, and what its verifier imports, in server-only modules.
62
+ - The name is the descriptor's type identity: use one name for one declaration. It holds only
63
+ letters, digits, `_`, `.` and `-`.
64
+ - A descriptor names one scheme, and a binding or an endpoint names one descriptor. There is no
65
+ fallback between schemes: a host accepting two credentials for one identity declares two
66
+ descriptors and serves them on two bindings or endpoints
67
+ ([kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts)).
68
+ - Build a server's surfaces and its provider from one descriptor value. `ActionHttp.layer`,
69
+ `ActionRpc.layer`, `ActionMcp.layerHttp` and `Authentication.protect` refuse, when they build,
70
+ a descriptor that is another object than the one the provider was given, equal name and
71
+ options or not.
72
+ - A local surface has no remote caller and takes no descriptor: the host provides the identity
73
+ itself, for a CLI ([ActionCli.md](ActionCli.md#name-the-caller)), a Toolkit
74
+ ([ActionToolkit.md](ActionToolkit.md#name-the-caller)) and an MCP server over stdio
75
+ ([ActionMcp.md](ActionMcp.md#serve-tools-over-stdio)).
76
+
77
+ ## Verify credentials
78
+
79
+ Write the verifier in a server-only module, give it to `Authentication.layer`, and provide that
80
+ layer to every layer serving a protected action: `routes.pipe(Layer.provide(authenticate))`. The
81
+ verifier takes the credential and succeeds with the identity, or fails with
82
+ `Action.Unauthenticated`, `Action.Forbidden` or an error the descriptor declares. See
83
+ [notes-service/src/identity/verifier.ts](../examples/notes-service/src/identity/verifier.ts) and
84
+ [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts).
85
+
86
+ - **The library reads no token.** Everything that makes a credential valid is the verifier's to
87
+ check, with your authorization server's library: its signature, its expiry, and its audience.
88
+ A token issued for another resource fails with `Unauthenticated`. Only a missing or empty
89
+ credential is refused before the verifier runs.
90
+ - **Never provide the identity service, or a tenant, at startup**, in a layer or around
91
+ `HttpRouter.serve` or the program. It does not satisfy the provider a protected route
92
+ requires, and a public handler or a route of your own reading it would serve every caller as
93
+ that identity. A test sends a credential instead
94
+ ([Testing.md](Testing.md#test-as-a-signed-in-caller)).
95
+ - A verifier that needs startup services, such as a user directory, is passed as an Effect that
96
+ yields them and returns the verifier: a builder ([Action.md](Action.md#implement-actions)),
97
+ run once per layer graph
98
+ ([kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts)).
99
+ - Call `Authentication.layer` once per descriptor and provide that one value wherever a
100
+ provider is required.
101
+ - A failure that is not the caller's, such as an issuer the verifier cannot reach, is an error
102
+ the descriptor declares, with `{ httpApiStatus: 503 }`: an `Unauthenticated` would have the
103
+ caller sign in again
104
+ ([kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts),
105
+ [kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts)).
106
+ - Over RPC the verifier runs for each message of a protected rpc, not once per connection
107
+ ([live-rpc/src/identity/verifier.ts](../examples/live-rpc/src/identity/verifier.ts),
108
+ [kitchen-sink/test/identity/session.test.ts](../examples/kitchen-sink/test/identity/session.test.ts)).
109
+
110
+ What each surface answers a missing or refused credential with:
111
+ [guarantees.md](guarantees.md#wire-behavior), as
112
+ [notes-service/test/routes.test.ts](../examples/notes-service/test/routes.test.ts) asserts.
113
+
114
+ ## Publish OAuth discovery
115
+
116
+ Give a Bearer descriptor's `layer` a `protectedResource`. The provider then publishes OAuth
117
+ protected-resource discovery (RFC 9728) and names it in every challenge, so an MCP client that
118
+ gets a 401 finds the authorization server. Without it the challenge names nowhere to sign in,
119
+ so an MCP endpoint whose users sign in through OAuth publishes it. See
120
+ [notes-service/src/identity/verifier.ts](../examples/notes-service/src/identity/verifier.ts)
121
+ and [kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts).
122
+
123
+ - `resource` is the exact public URL of the endpoint clients connect to: discovery is served at
124
+ `/.well-known/oauth-protected-resource` followed by its path. Only the deployment knows that
125
+ URL, so give the resource as an Effect reading configuration; succeeding with `undefined`
126
+ publishes nothing.
127
+ - That `resource` and `authorizationServers` are HTTPS, or loopback HTTP in development, is the
128
+ deployment's to ensure: the provider does not check it.
129
+ - Give `scopesRequired` whenever some actions need scopes others do not. An MCP client requests
130
+ the scopes a 401 names, or every one of `scopesSupported` when it names none.
131
+ - Discovery is public. It answers before routing, to any origin, so no authentication and no
132
+ route middleware covers it: to restrict who reads it, see
133
+ [Run a request policy before authentication](Authentication-advanced.md#run-a-request-policy-before-authentication).
134
+
135
+ ### Step up for a scope
136
+
137
+ A step-up refusal is one an OAuth client acts on: `Action.Unauthenticated`, which it signs in
138
+ on, or an `Action.Forbidden` naming `scopes`, which it re-authorizes on with those scopes
139
+ added. The authorizer, a handler or the verifier may fail with one:
140
+ [notes-service/src/identity/authorize.ts](../examples/notes-service/src/identity/authorize.ts).
141
+
142
+ - Step-up is a Bearer descriptor's, for protected actions and routes under `protect`: the
143
+ refusal answers the request with 401 or 403 and its challenge, over MCP in place of the tool
144
+ result.
145
+ - On a public action or under another scheme, an authorizer's or a handler's refusal is
146
+ answered like a declared error, and RPC never steps up: a refusal is the call's typed failure
147
+ ([guarantees.md](guarantees.md#wire-behavior)).
148
+ - Name scopes only when re-authorizing can grant them: a server with fixed tokens, such as API
149
+ keys, leaves `scopes` out. A `Forbidden` without them prompts no login. Over HTTP it is a 403 without a
150
+ challenge. Over MCP it is an `isError` tool result, in a 200, from the authorizer or a
151
+ handler, and the endpoint's 403 only from the verifier
152
+ ([kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts)).
153
+ - Map another service's refusals before failing with them
154
+ ([Action.md](Action.md#fail-with-declared-and-built-in-errors)): passed on, a `Forbidden`
155
+ naming that service's scopes sends this server's caller to re-authorize for them.