@gjermundgaraba/effect-actions 0.19.0 → 0.21.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 (326) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -48
  3. package/dist/Action.mjs +88 -44
  4. package/dist/ActionCli.d.mts +70 -35
  5. package/dist/ActionCli.mjs +53 -34
  6. package/dist/ActionHttp.d.mts +70 -38
  7. package/dist/ActionHttp.mjs +76 -25
  8. package/dist/ActionMcp.d.mts +56 -24
  9. package/dist/ActionMcp.mjs +104 -61
  10. package/dist/ActionRpc.d.mts +222 -57
  11. package/dist/ActionRpc.mjs +301 -49
  12. package/dist/ActionToolkit.d.mts +22 -7
  13. package/dist/ActionToolkit.mjs +37 -14
  14. package/dist/Authentication.d.mts +42 -12
  15. package/dist/Authentication.mjs +38 -32
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.d.mts +1 -3
  19. package/dist/Testing.mjs +2 -3
  20. package/dist/Webhook.d.mts +146 -0
  21. package/dist/Webhook.mjs +72 -0
  22. package/dist/authentication/provider.d.mts +35 -12
  23. package/dist/authentication/provider.mjs +2 -3
  24. package/dist/authentication/refusal.mjs +3 -4
  25. package/dist/cli/commands.d.mts +9 -7
  26. package/dist/cli/commands.mjs +85 -35
  27. package/dist/contract/ast.mjs +5 -3
  28. package/dist/contract/authorizer.d.mts +8 -0
  29. package/dist/contract/authorizer.mjs +20 -0
  30. package/dist/contract/call.d.mts +9 -8
  31. package/dist/contract/call.mjs +49 -8
  32. package/dist/contract/callback.d.mts +7 -0
  33. package/dist/contract/callback.mjs +27 -0
  34. package/dist/contract/caller.d.mts +12 -3
  35. package/dist/contract/caller.mjs +23 -5
  36. package/dist/contract/canonical.mjs +10 -0
  37. package/dist/contract/codec.d.mts +9 -0
  38. package/dist/contract/codec.mjs +9 -0
  39. package/dist/contract/codes.mjs +81 -0
  40. package/dist/contract/declared.d.mts +0 -2
  41. package/dist/contract/declared.mjs +26 -11
  42. package/dist/contract/digest.mjs +6 -0
  43. package/dist/contract/errors.d.mts +9 -5
  44. package/dist/contract/errors.mjs +2 -3
  45. package/dist/contract/feed.d.mts +127 -0
  46. package/dist/contract/feed.mjs +249 -0
  47. package/dist/contract/hints.mjs +1 -2
  48. package/dist/contract/hub.d.mts +94 -0
  49. package/dist/contract/hub.mjs +133 -0
  50. package/dist/contract/idempotency.d.mts +47 -0
  51. package/dist/contract/idempotency.mjs +99 -0
  52. package/dist/contract/implementation.d.mts +73 -51
  53. package/dist/contract/implementation.mjs +94 -60
  54. package/dist/contract/input-check.d.mts +15 -0
  55. package/dist/contract/input-check.mjs +16 -0
  56. package/dist/contract/lease.mjs +28 -0
  57. package/dist/contract/listed.d.mts +4 -0
  58. package/dist/contract/listed.mjs +22 -0
  59. package/dist/contract/log.d.mts +41 -0
  60. package/dist/contract/log.mjs +144 -0
  61. package/dist/contract/media.d.mts +1 -3
  62. package/dist/contract/media.mjs +1 -2
  63. package/dist/contract/names.mjs +15 -6
  64. package/dist/contract/occurrence.d.mts +29 -0
  65. package/dist/contract/occurrence.mjs +45 -0
  66. package/dist/contract/pages.d.mts +13 -0
  67. package/dist/contract/pages.mjs +39 -0
  68. package/dist/contract/progress.d.mts +26 -0
  69. package/dist/contract/progress.mjs +72 -0
  70. package/dist/contract/push.d.mts +20 -0
  71. package/dist/contract/push.mjs +96 -0
  72. package/dist/contract/receiving.d.mts +29 -0
  73. package/dist/contract/receiving.mjs +42 -0
  74. package/dist/contract/recent.mjs +12 -0
  75. package/dist/contract/reported.mjs +16 -0
  76. package/dist/contract/serving.mjs +63 -0
  77. package/dist/contract/status.mjs +15 -7
  78. package/dist/contract/stream.d.mts +34 -0
  79. package/dist/contract/stream.mjs +31 -0
  80. package/dist/contract/subscribe.d.mts +45 -0
  81. package/dist/contract/subscribe.mjs +50 -0
  82. package/dist/contract/transient.mjs +28 -0
  83. package/dist/contract/watch.d.mts +18 -0
  84. package/dist/contract/watch.mjs +105 -0
  85. package/dist/contract/webhooks.d.mts +42 -0
  86. package/dist/contract/webhooks.mjs +53 -0
  87. package/dist/http/client.d.mts +37 -14
  88. package/dist/http/client.mjs +52 -14
  89. package/dist/http/events.d.mts +56 -0
  90. package/dist/http/events.mjs +102 -0
  91. package/dist/http/placed.d.mts +36 -0
  92. package/dist/http/placed.mjs +71 -0
  93. package/dist/http/schema-errors.mjs +2 -3
  94. package/dist/http/streamed.d.mts +21 -0
  95. package/dist/http/streamed.mjs +94 -0
  96. package/dist/http/watch.mjs +50 -0
  97. package/dist/mcp/bridge.mjs +283 -0
  98. package/dist/mcp/client.d.mts +30 -99
  99. package/dist/mcp/client.mjs +60 -134
  100. package/dist/mcp/events.mjs +142 -0
  101. package/dist/mcp/progress.mjs +34 -0
  102. package/dist/mcp/protocol.mjs +40 -15
  103. package/dist/mcp/reply.d.mts +79 -0
  104. package/dist/mcp/reply.mjs +99 -0
  105. package/dist/mcp/request.d.mts +40 -0
  106. package/dist/mcp/request.mjs +51 -0
  107. package/dist/mcp/watch.mjs +115 -0
  108. package/dist/rpc/watch.mjs +39 -0
  109. package/dist/stdio/console.mjs +14 -7
  110. package/dist/testing/memory.d.mts +1 -3
  111. package/dist/testing/memory.mjs +4 -5
  112. package/dist/webhook/address.mjs +47 -0
  113. package/dist/webhook/attempt.mjs +65 -0
  114. package/dist/webhook/receiver.mjs +67 -0
  115. package/dist/webhook/signing.mjs +28 -0
  116. package/dist/webhook/stored.mjs +57 -0
  117. package/dist/webhook/subscriptions.mjs +342 -0
  118. package/docs/Action-failures.md +2 -2
  119. package/docs/Action-streams.md +115 -0
  120. package/docs/Action.md +58 -124
  121. package/docs/ActionCli-failures.md +1 -2
  122. package/docs/ActionCli.md +83 -88
  123. package/docs/ActionHttp-failures.md +3 -3
  124. package/docs/ActionHttp.md +90 -70
  125. package/docs/ActionMcp-failures.md +2 -1
  126. package/docs/ActionMcp.md +120 -92
  127. package/docs/ActionRpc-events.md +126 -0
  128. package/docs/ActionRpc-failures.md +1 -2
  129. package/docs/ActionRpc.md +108 -79
  130. package/docs/ActionToolkit-failures.md +1 -2
  131. package/docs/ActionToolkit.md +33 -90
  132. package/docs/Authentication-advanced.md +57 -61
  133. package/docs/Authentication-failures.md +5 -4
  134. package/docs/Authentication.md +23 -58
  135. package/docs/Event-failures.md +15 -0
  136. package/docs/Event.md +281 -0
  137. package/docs/README.md +51 -82
  138. package/docs/Testing-failures.md +1 -5
  139. package/docs/Testing.md +126 -86
  140. package/docs/Webhook-failures.md +8 -0
  141. package/docs/Webhook.md +319 -0
  142. package/docs/setup-failures.md +2 -3
  143. package/docs/setup.md +32 -53
  144. package/examples/README.md +12 -37
  145. package/examples/assistant/README.md +21 -26
  146. package/examples/assistant/src/agent.ts +66 -24
  147. package/examples/assistant/src/cli.ts +7 -4
  148. package/examples/assistant/src/commands.ts +9 -4
  149. package/examples/assistant/src/identity/Identity.ts +5 -4
  150. package/examples/assistant/src/identity/authorize.ts +13 -5
  151. package/examples/assistant/src/identity/handlers.ts +3 -2
  152. package/examples/assistant/src/identity/permissions.ts +9 -3
  153. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  154. package/examples/assistant/src/scripted.ts +7 -4
  155. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  156. package/examples/assistant/src/tasks/handlers.ts +9 -5
  157. package/examples/assistant/src/tasks/store.ts +38 -11
  158. package/examples/assistant/src/toolkit.ts +11 -7
  159. package/examples/assistant/test/commands.test.ts +53 -27
  160. package/examples/assistant/test/identity/identity.test.ts +27 -11
  161. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  162. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  163. package/examples/assistant/test/toolkit.test.ts +104 -75
  164. package/examples/build-events/README.md +83 -0
  165. package/examples/build-events/src/Api.ts +27 -0
  166. package/examples/build-events/src/Rpc.ts +26 -0
  167. package/examples/build-events/src/builds/Builds.ts +108 -0
  168. package/examples/build-events/src/builds/events.ts +54 -0
  169. package/examples/build-events/src/builds/handlers.ts +58 -0
  170. package/examples/build-events/src/builds/log.ts +30 -0
  171. package/examples/build-events/src/builds/status.ts +28 -0
  172. package/examples/build-events/src/builds/store.ts +130 -0
  173. package/examples/build-events/src/cli.ts +18 -0
  174. package/examples/build-events/src/commands.ts +32 -0
  175. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  176. package/examples/build-events/src/deploys/approvers.ts +55 -0
  177. package/examples/build-events/src/deploys/events.ts +32 -0
  178. package/examples/build-events/src/deploys/handlers.ts +38 -0
  179. package/examples/build-events/src/identity/Identity.ts +29 -0
  180. package/examples/build-events/src/identity/members.ts +7 -0
  181. package/examples/build-events/src/identity/verifier.ts +22 -0
  182. package/examples/build-events/src/notifier/Notices.ts +26 -0
  183. package/examples/build-events/src/notifier/routes.ts +71 -0
  184. package/examples/build-events/src/notifier.ts +48 -0
  185. package/examples/build-events/src/routes.ts +71 -0
  186. package/examples/build-events/src/server.ts +62 -0
  187. package/examples/build-events/test/builds/events.test.ts +68 -0
  188. package/examples/build-events/test/builds/log.test.ts +71 -0
  189. package/examples/build-events/test/builds/progress.test.ts +57 -0
  190. package/examples/build-events/test/builds/status.test.ts +42 -0
  191. package/examples/build-events/test/commands.test.ts +103 -0
  192. package/examples/build-events/test/deploys/events.test.ts +52 -0
  193. package/examples/build-events/test/http.test.ts +95 -0
  194. package/examples/build-events/test/notifier.test.ts +80 -0
  195. package/examples/build-events/test/routes.test.ts +96 -0
  196. package/examples/build-events/test/rpc.test.ts +84 -0
  197. package/examples/build-events/test/support/as.ts +8 -0
  198. package/examples/build-events/test/support/memory.ts +43 -0
  199. package/examples/build-events/test/webhooks.test.ts +143 -0
  200. package/examples/kitchen-sink/README.md +178 -166
  201. package/examples/kitchen-sink/src/Actions.ts +11 -3
  202. package/examples/kitchen-sink/src/Api.ts +12 -6
  203. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  204. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  205. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  206. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  207. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  208. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  209. package/examples/kitchen-sink/src/agent.ts +14 -6
  210. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  211. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  212. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  213. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  214. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  215. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  216. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  217. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  218. package/examples/kitchen-sink/src/browser.ts +10 -4
  219. package/examples/kitchen-sink/src/cli.ts +28 -12
  220. package/examples/kitchen-sink/src/config.ts +1 -1
  221. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  222. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  223. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  224. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  225. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  226. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  227. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  228. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  229. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  230. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  231. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  232. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  233. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  234. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  235. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  236. package/examples/kitchen-sink/src/memory.ts +11 -2
  237. package/examples/kitchen-sink/src/ops.ts +10 -5
  238. package/examples/kitchen-sink/src/request-policy.ts +25 -9
  239. package/examples/kitchen-sink/src/routes.ts +67 -27
  240. package/examples/kitchen-sink/src/sdk.ts +22 -12
  241. package/examples/kitchen-sink/src/seed.ts +15 -9
  242. package/examples/kitchen-sink/src/server.ts +24 -10
  243. package/examples/kitchen-sink/src/stdio.ts +9 -3
  244. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  245. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  246. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  247. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  248. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  249. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  250. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  251. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  252. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  253. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  254. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  255. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  256. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  257. package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
  258. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  259. package/examples/kitchen-sink/test/support/as.ts +9 -0
  260. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  261. package/examples/kitchen-sink/test/support/services.ts +17 -0
  262. package/examples/live-rpc/README.md +33 -35
  263. package/examples/live-rpc/src/Rpc.ts +9 -5
  264. package/examples/live-rpc/src/browser.ts +39 -11
  265. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  266. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  267. package/examples/live-rpc/src/chat/events.ts +74 -0
  268. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  269. package/examples/live-rpc/src/chat/room.ts +9 -5
  270. package/examples/live-rpc/src/chat/store.ts +51 -14
  271. package/examples/live-rpc/src/client.ts +22 -5
  272. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  273. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  274. package/examples/live-rpc/src/page.ts +58 -12
  275. package/examples/live-rpc/src/routes.ts +14 -10
  276. package/examples/live-rpc/src/server.ts +15 -5
  277. package/examples/live-rpc/test/browser.test.ts +127 -50
  278. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  279. package/examples/live-rpc/test/routes.test.ts +130 -46
  280. package/examples/live-rpc/test/server.test.ts +94 -33
  281. package/examples/mcp-tools/README.md +24 -38
  282. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  283. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  284. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  285. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  286. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  287. package/examples/mcp-tools/src/routes.ts +8 -6
  288. package/examples/mcp-tools/src/server.ts +12 -5
  289. package/examples/mcp-tools/src/stdio.ts +3 -2
  290. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  291. package/examples/mcp-tools/test/routes.test.ts +140 -81
  292. package/examples/notes-service/README.md +30 -45
  293. package/examples/notes-service/src/Api.ts +1 -0
  294. package/examples/notes-service/src/browser.ts +15 -14
  295. package/examples/notes-service/src/cli.ts +8 -3
  296. package/examples/notes-service/src/commands.ts +46 -20
  297. package/examples/notes-service/src/identity/Identity.ts +2 -2
  298. package/examples/notes-service/src/identity/authorize.ts +6 -2
  299. package/examples/notes-service/src/identity/handlers.ts +3 -2
  300. package/examples/notes-service/src/identity/users.ts +2 -1
  301. package/examples/notes-service/src/identity/verifier.ts +12 -6
  302. package/examples/notes-service/src/notes/Notes.ts +46 -22
  303. package/examples/notes-service/src/notes/handlers.ts +18 -9
  304. package/examples/notes-service/src/notes/store.ts +70 -43
  305. package/examples/notes-service/src/routes.ts +7 -5
  306. package/examples/notes-service/src/server.ts +12 -5
  307. package/examples/notes-service/src/status/Status.ts +1 -1
  308. package/examples/notes-service/src/status/handlers.ts +5 -3
  309. package/examples/notes-service/src/stdio.ts +7 -3
  310. package/examples/notes-service/src/worker.ts +6 -3
  311. package/examples/notes-service/test/commands.test.ts +164 -91
  312. package/examples/notes-service/test/notes/as.ts +3 -1
  313. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  314. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  315. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  316. package/examples/notes-service/test/routes.test.ts +106 -71
  317. package/examples/notes-service/test/worker.test.ts +21 -13
  318. package/examples/quickstart/README.md +2 -6
  319. package/examples/quickstart/src/Api.ts +1 -0
  320. package/examples/quickstart/src/client.ts +6 -3
  321. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  322. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  323. package/examples/quickstart/src/routes.ts +3 -2
  324. package/examples/quickstart/src/server.ts +6 -2
  325. package/examples/quickstart/test/routes.test.ts +3 -2
  326. package/package.json +19 -17
@@ -1,24 +1,21 @@
1
1
  # ActionToolkit
2
2
 
3
- Implementations as Effect's native AI `Toolkit`: one tool per action, run in the process that
4
- calls the model, with no server. Reach for it when a `LanguageModel` or a `Chat` in your program
5
- calls your actions. Nothing authenticates: the host names the caller. Code of your own calls
6
- the same implementations typed, with `Action.client`
7
- ([Action.md](Action.md#call-actions-in-process)).
3
+ Implementations as Effect's native AI `Toolkit`: one tool per action, run in the process that calls the model, with no server. Reach for it when a `LanguageModel` or a `Chat` in your program calls your actions. Nothing authenticates: the host names the caller. Code of your own calls the same implementations typed, with `Action.client` ([Action.md](Action.md#call-actions-in-process)).
8
4
 
9
5
  A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [ActionToolkit-failures.md](ActionToolkit-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
10
6
 
11
7
  ## Give a model tools
12
8
 
13
- `ActionToolkit.make` projects implementations into `{ toolkit, layer }`. Pass `toolkit` to
14
- `LanguageModel.generateText` or a `Chat`, and provide `layer` once, around the program:
9
+ `ActionToolkit.make` projects implementations into `{ toolkit, layer }`. Pass `toolkit` to `LanguageModel.generateText` or a `Chat`, and provide `layer` once, around the program:
15
10
 
16
11
  ```ts example=assistant/src/toolkit.ts
12
+ import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
17
13
  import { Effect, Option } from "effect";
18
14
  import { Tool, Toolkit } from "effect/ai";
19
- import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
15
+
20
16
  import { identity } from "./identity/handlers.js";
21
- import { CurrentCaller, type Member } from "./identity/Identity.js";
17
+ import { CurrentCaller } from "./identity/Identity.js";
18
+ import type { Member } from "./identity/Identity.js";
22
19
  import { tasks } from "./tasks/handlers.js";
23
20
 
24
21
  // `actions.layer` holds the handlers, and requires the startup services their builders read;
@@ -33,8 +30,9 @@ const actions = ActionToolkit.make([tasks, identity], {
33
30
  Effect.serviceOption(CurrentCaller),
34
31
  Option.match({
35
32
  onNone: () => true,
36
- onSome: (caller) => call.name !== "addTask" || call.input.assignee !== caller.id,
37
- }),
33
+ onSome: (caller) =>
34
+ call.name !== "addTask" || call.input.assignee !== caller.id,
35
+ })
38
36
  ),
39
37
  });
40
38
 
@@ -42,104 +40,49 @@ const actions = ActionToolkit.make([tasks, identity], {
42
40
  // The later definition wins; annotating the generated tool keeps its handler and approval rule.
43
41
  export const toolkit = Toolkit.merge(
44
42
  actions.toolkit,
45
- Toolkit.make(actions.toolkit.tools.addTask.annotate(Tool.Title, "Add a task")),
43
+ Toolkit.make(actions.toolkit.tools.addTask.annotate(Tool.Title, "Add a task"))
46
44
  );
47
45
 
48
- export const layer = actions.layer;
46
+ export const { layer } = actions;
49
47
 
50
48
  // One toolkit serves every member, so who a turn runs as is provided around that turn. This is
51
49
  // how this example names a model's caller, and it takes a `Member`. The tools themselves accept
52
50
  // any `CurrentCaller`, so keeping the operator away from a model is this function's doing, and
53
51
  // that of every host using it.
54
- export const actingFor = (member: Member) => Effect.provideService(CurrentCaller, member);
52
+ export const actingFor = (member: Member) =>
53
+ Effect.provideService(CurrentCaller, member);
55
54
  ```
56
55
 
57
- [assistant/src/agent.ts](../examples/assistant/src/agent.ts) runs the turns and provides `layer`
58
- with its startup services, with a scripted model in
59
- [assistant/src/scripted.ts](../examples/assistant/src/scripted.ts). What a call returns, a
60
- refusal and arguments that do not decode included, is on `ActionToolkit.make`; the options'
61
- rules are on `ActionToolkit.Options`.
62
-
63
- - **Let the model read its tools' results** by asking again:
64
- [kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts).
65
- - Where a model must not get every action, list the tools
66
- ([Action.md](Action.md#define-a-contract)):
67
- [kitchen-sink/src/issues/assistant.ts](../examples/kitchen-sink/src/issues/assistant.ts).
68
- - A turn's tool calls, the approved calls of an answering turn included, run concurrently unless
69
- `generateText` is given a `concurrency`:
70
- [assistant/src/tasks/store.ts](../examples/assistant/src/tasks/store.ts) locks each change.
71
- - **A model loop behind a route.** Yield `toolkit` and the model in the implementation's
72
- builder, and provide `layer` to the route's layer,
73
- `ActionHttp.layer(Http, ask).pipe(Layer.provide(layer))`, so the tools share the routes' build
74
- ([Action.md](Action.md#share-one-build-across-servers)):
75
- [kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts),
76
- keeping each caller's `Chat` in
77
- [conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts), with a scripted
78
- model in [demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts) that the
79
- entrypoint provides through [kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts),
80
- tested over HTTP in
81
- [kitchen-sink/test/assistant/assistant.test.ts](../examples/kitchen-sink/test/assistant/assistant.test.ts).
82
- Name the files as it does, the route's contract `Assistant.ts` and the loop `handlers.ts`,
83
- from the first one written.
84
- - **The real model comes later?** Provide a scripted one from the entrypoint until then, as the
85
- kitchen-sink does. Reading the model with `Effect.serviceOption` hides the requirement: the
86
- server builds without one and every request fails.
56
+ [assistant/src/agent.ts](../examples/assistant/src/agent.ts) runs the turns and provides `layer` with its startup services, with a scripted model in [assistant/src/scripted.ts](../examples/assistant/src/scripted.ts). What a call returns, a refusal and arguments that do not decode included, is on `ActionToolkit.make`; the options' rules are on `ActionToolkit.Options`.
57
+
58
+ - **Let the model read its tools' results** by asking again: [kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts).
59
+ - Where a model must not get every action, list the tools ([Action.md](Action.md#define-a-contract)): [kitchen-sink/src/issues/assistant.ts](../examples/kitchen-sink/src/issues/assistant.ts).
60
+ - A turn's tool calls, the approved calls of an answering turn included, run concurrently unless `generateText` is given a `concurrency`: [assistant/src/tasks/store.ts](../examples/assistant/src/tasks/store.ts) locks each change.
61
+ - **A model loop behind a route.** Yield `toolkit` and the model in the implementation's builder, and provide `layer` to the route's layer, `ActionHttp.layer(Http, ask).pipe(Layer.provide(layer))`, so the tools share the routes' build ([Action.md](Action.md#share-one-build-across-servers)): [kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts), keeping each caller's `Chat` in [conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts), with a scripted model in [demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts) that the entrypoint provides through [kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts), tested over HTTP in [kitchen-sink/test/assistant/assistant.test.ts](../examples/kitchen-sink/test/assistant/assistant.test.ts). Name the files as it does, the route's contract `Assistant.ts` and the loop `handlers.ts`, from the first one written.
62
+ - **The real model comes later?** Provide a scripted one from the entrypoint until then, as the kitchen-sink does. Reading the model with `Effect.serviceOption` hides the requirement: the server builds without one and every request fails.
87
63
 
88
64
  ### Customize the native tools
89
65
 
90
- Give a generated tool a title, or other annotations, with native `Tool` and `Toolkit` from
91
- `effect/ai`, as `toolkit` above does; [agent.ts](../examples/assistant/src/agent.ts) reads the
92
- title when printing approval requests.
66
+ Give a generated tool a title, or other annotations, with native `Tool` and `Toolkit` from `effect/ai`, as `toolkit` above does; [agent.ts](../examples/assistant/src/agent.ts) reads the title when printing approval requests.
93
67
 
94
- - To add other native tools, use `Toolkit.merge(toolkit, others)` and provide both handler layers
95
- with `Layer.mergeAll(layer, othersHandlers)`.
68
+ - To add other native tools, use `Toolkit.merge(toolkit, others)` and provide both handler layers with `Layer.mergeAll(layer, othersHandlers)`.
96
69
 
97
70
  ### Call a tool by hand
98
71
 
99
- `yield* toolkit` gives `tools.handle(name, arguments)`:
100
- [assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts). Its rules, where
101
- to provide the caller among them, are on `ActionToolkit.Tools`.
72
+ `yield* toolkit` gives `tools.handle(name, arguments)`: [assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts). Its rules, where to provide the caller among them, are on `ActionToolkit.Tools`.
102
73
 
103
74
  ## Name the caller
104
75
 
105
- Provide the identity around each turn, or each call made by hand, as `actingFor` does in
106
- [assistant/src/toolkit.ts](../examples/assistant/src/toolkit.ts). Why never to `layer`, and that
107
- a protected tool owes it whatever its handler reads, are on `Action.Options.caller`
108
- ([Action.md](Action.md#say-who-may-call-an-action)).
76
+ Provide the identity around each turn, or each call made by hand, as `actingFor` does in [assistant/src/toolkit.ts](../examples/assistant/src/toolkit.ts). Why never to `layer`, and that a protected tool owes it whatever its handler reads, are on `Action.Options.caller` ([Action.md](Action.md#say-who-may-call-an-action)).
109
77
 
110
78
  ## Ask for approval
111
79
 
112
- To ask before every write: `needsApproval: (call) => !call.action.readOnly`
113
- ([kitchen-sink/src/issues/assistant.ts](../examples/kitchen-sink/src/issues/assistant.ts)). A
114
- rule reading the call's input and its caller is `toolkit` above; its rules are on
115
- `ActionToolkit.Options`.
116
-
117
- A call that needs approval does not run: the turn ends with a `tool-approval-request` part in
118
- `response.content` in place of its result. Start the next turn with the person's answers, in a
119
- `Chat`, as the caller who was asked:
120
- [assistant/src/agent.ts](../examples/assistant/src/agent.ts) shows both and says why, tested in
121
- [assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts).
122
-
123
- - **Each request is answered on its own**: an approved call runs, a denied one does not, and one
124
- left unanswered stays open for a later turn; to require every answer at once, check it in the
125
- host. List the open ones with `ActionToolkit.openApprovals(yield* Ref.get(chat.history))`.
126
- The kitchen-sink refuses an `approvalId` answered twice
127
- ([Assistant.ts](../examples/kitchen-sink/src/assistant/Assistant.ts)), gives each call its own
128
- id ([demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts)) and lets only its
129
- owner reach a conversation
130
- ([conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts)).
131
- - A denied call's result, `{ type: "execution-denied", reason }`, has `reason` `undefined` unless
132
- the answer gives one, so it is not JSON: a route answering it as a `Schema.Unknown` success
133
- answers an empty 500.
134
- - **If a turn fails or is interrupted once an approved call has started**, the call may have
135
- run: end the conversation and say so in the error, as
136
- [kitchen-sink/src/assistant/conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts)
137
- and [handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts) do. What a failed
138
- `streamText` leaves is on `ActionToolkit.openApprovals`.
139
- - **Show the person what is asked**: [assistant/src/agent.ts](../examples/assistant/src/agent.ts)
140
- joins each request to its call by `toolCallId`; how to read the arguments is on
141
- `ActionToolkit.Approval`.
142
- - **If repeating a write would create an unwanted additional effect, retries need
143
- application-owned deduplication.** Keep the same operation key across attempts, as in
144
- [notes-service's creation example](../examples/notes-service/README.md#retry-a-creation).
145
- Approval history alone does not make the mutation and its result record atomic.
80
+ To ask before every write: `needsApproval: (call) => !call.action.readOnly` ([kitchen-sink/src/issues/assistant.ts](../examples/kitchen-sink/src/issues/assistant.ts)). A rule reading the call's input and its caller is `toolkit` above; its rules are on `ActionToolkit.Options`.
81
+
82
+ A call that needs approval does not run: the turn ends with a `tool-approval-request` part in `response.content` in place of its result. Start the next turn with the person's answers, in a `Chat`, as the caller who was asked: [assistant/src/agent.ts](../examples/assistant/src/agent.ts) shows both and says why, tested in [assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts).
83
+
84
+ - **Each request is answered on its own**: an approved call runs, a denied one does not, and one left unanswered stays open for a later turn; to require every answer at once, check it in the host. List the open ones with `ActionToolkit.openApprovals(yield* Ref.get(chat.history))`. The kitchen-sink refuses an `approvalId` answered twice ([Assistant.ts](../examples/kitchen-sink/src/assistant/Assistant.ts)), gives each call its own id ([demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts)) and lets only its owner reach a conversation ([conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts)).
85
+ - A denied call's result, `{ type: "execution-denied", reason }`, has `reason` `undefined` unless the answer gives one, so it is not JSON: a route answering it as a `Schema.Unknown` success answers an empty 500.
86
+ - **If a turn fails or is interrupted once an approved call has started**, the call may have run: end the conversation and say so in the error, as [kitchen-sink/src/assistant/conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts) and [handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts) do. What a failed `streamText` leaves is on `ActionToolkit.openApprovals`.
87
+ - **Show the person what is asked**: [assistant/src/agent.ts](../examples/assistant/src/agent.ts) joins each request to its call by `toolCallId`; how to read the arguments is on `ActionToolkit.Approval`.
88
+ - **If repeating a write would create an unwanted additional effect, retries need application-owned deduplication.** Keep the same operation key across attempts, as in [notes-service's creation example](../examples/notes-service/README.md#retry-a-creation). Approval history alone does not make the mutation and its result record atomic.
@@ -1,17 +1,12 @@
1
1
  # Authentication beyond a bearer token
2
2
 
3
- A verifier reading the request, an API key or a cookie, a route of your own, a policy before
4
- authentication, and a refusal outside the router.
3
+ A verifier reading the request, an API key or a cookie, a route of your own, a policy before authentication, and a refusal outside the router.
5
4
 
6
- A message the library throws says its fix; for a compiler diagnostic or Effect's own error,
7
- search [Authentication-failures.md](Authentication-failures.md). For one symbol,
8
- `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc
9
- carries its rules and gotchas, and this page only what an example cannot show.
5
+ A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [Authentication-failures.md](Authentication-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
10
6
 
11
7
  ## Read request context in a verifier
12
8
 
13
- A verifier may read per-request services: the request, or one of your own, such as the tenant a
14
- request addresses, which native router middleware provides:
9
+ A verifier may read per-request services: the request, or one of your own, such as the tenant a request addresses, which native router middleware provides:
15
10
 
16
11
  ```ts example=kitchen-sink/src/workspaces/tenant.ts
17
12
  import { Context, Effect, Option } from "effect";
@@ -22,51 +17,43 @@ import { HttpRouter, HttpServerRequest } from "effect/http";
22
17
  * an identity: the verifier reads it to decide which of an account's memberships the caller
23
18
  * acts in. Handlers never read it; they read the workspace of the authenticated member.
24
19
  */
25
- export class Tenant extends Context.Service<Tenant, Option.Option<string>>()("tracker/Tenant") {}
20
+ export class Tenant extends Context.Service<Tenant, Option.Option<string>>()(
21
+ "tracker/Tenant"
22
+ ) {}
26
23
 
27
24
  // Native router middleware. It provides `Tenant` to whatever it is provided to, per request:
28
25
  // to the verifier too, when it is provided after the authentication, in a `Layer.provide` of
29
26
  // its own: in one array with the authentication it would leave `Tenant` owed, since an array's
30
27
  // layers provide to the routes, not to one another.
31
- export const resolveTenant = HttpRouter.middleware<{ provides: Tenant }>()((route) =>
32
- Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) =>
33
- Effect.provideService(route, Tenant, Option.fromNullishOr(request.headers["x-workspace"])),
34
- ),
28
+ export const resolveTenant = HttpRouter.middleware<{ provides: Tenant }>()(
29
+ (route) =>
30
+ Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) =>
31
+ Effect.provideService(
32
+ route,
33
+ Tenant,
34
+ Option.fromNullishOr(request.headers["x-workspace"])
35
+ )
36
+ )
35
37
  );
36
38
  ```
37
39
 
38
- The verifier reading it is `authenticateBearer` in
39
- [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts),
40
- and `publicRoutes` in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts)
41
- provides the middleware.
40
+ The verifier reading it is `authenticateBearer` in [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts), and `publicRoutes` in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) provides the middleware.
42
41
 
43
42
  ## Use a scheme other than Bearer
44
43
 
45
- Name the scheme in `Authentication.Options.security`, whose JSDoc says what each scheme gives
46
- up. See `Session` in
47
- [kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts) and
48
- `authenticateSession` in
49
- [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts)
50
- for a session cookie beside Bearer, and
51
- [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) and
52
- [kitchen-sink/src/admin/authenticate.ts](../examples/kitchen-sink/src/admin/authenticate.ts)
53
- for an API key in a header.
54
-
55
- For a cookie covering rpcs over a WebSocket, see
56
- [ActionRpc.md](ActionRpc.md#send-request-context-with-a-call). A scheme the deployment decides,
57
- such as a cookie name a framework prefixes over HTTPS, is still one declaration, as
58
- `Authentication.layer` says.
44
+ Name the scheme in `Authentication.Options.security`, whose JSDoc says what each scheme gives up. See `Session` in [kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts) and `authenticateSession` in [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts) for a session cookie beside Bearer, and [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) and [kitchen-sink/src/admin/authenticate.ts](../examples/kitchen-sink/src/admin/authenticate.ts) for an API key in a header.
45
+
46
+ For a cookie covering rpcs over a WebSocket, see [ActionRpc.md](ActionRpc.md#send-request-context-with-a-call). A scheme the deployment decides, such as a cookie name a framework prefixes over HTTPS, is still one declaration, as `Authentication.layer` says.
59
47
 
60
48
  ## Protect a route of your own
61
49
 
62
- A route of the host's own, such as a download or a WebSocket upgrade, takes
63
- `Authentication.protect(descriptor)`, whose rules, authorization and caching included, are on
64
- its JSDoc:
50
+ A route of the host's own, such as a download or a WebSocket upgrade, takes `Authentication.protect(descriptor)`, whose rules, authorization and caching included, are on its JSDoc:
65
51
 
66
52
  ```ts example=kitchen-sink/src/attachments/download.ts
53
+ import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
67
54
  import { Effect, Layer } from "effect";
68
55
  import { HttpRouter, HttpServerResponse } from "effect/http";
69
- import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
56
+
70
57
  import { authorize } from "../identity/authorize.js";
71
58
  import { CurrentMember, Session } from "../identity/Identity.js";
72
59
  import { GetAttachment } from "./Attachments.js";
@@ -104,26 +91,24 @@ export const download = Layer.unwrap(
104
91
  });
105
92
  }).pipe(
106
93
  Effect.catchTag("AttachmentNotFound", () =>
107
- Effect.succeed(HttpServerResponse.empty({ status: 404 })),
108
- ),
109
- ),
110
- ).pipe(Layer.provide(Authentication.protect(Session).layer)),
111
- ),
94
+ Effect.succeed(HttpServerResponse.empty({ status: 404 }))
95
+ )
96
+ )
97
+ ).pipe(Layer.provide(Authentication.protect(Session).layer))
98
+ )
112
99
  );
113
100
  ```
114
101
 
115
- [kitchen-sink/test/attachments/attachments.test.ts](../examples/kitchen-sink/test/attachments/attachments.test.ts)
116
- asserts what it answers.
102
+ [kitchen-sink/test/attachments/attachments.test.ts](../examples/kitchen-sink/test/attachments/attachments.test.ts) asserts what it answers.
117
103
 
118
104
  ## Run a request policy before authentication
119
105
 
120
- For a check that must refuse a request before its credential is read, such as a `Host` or
121
- `Origin` allowlist, make global router middleware, `HttpRouter.middleware(check, { global: true })`,
122
- and provide it in the last `Layer.provide`:
106
+ For a check that must refuse a request before its credential is read, such as a `Host` or `Origin` allowlist, make global router middleware, `HttpRouter.middleware(check, { global: true })`, and provide it in the last `Layer.provide`:
123
107
 
124
108
  ```ts example=kitchen-sink/src/request-policy.ts
125
109
  import { Effect, Layer, Option } from "effect";
126
110
  import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
111
+
127
112
  import { allowedOrigins, publicUrl } from "./config.js";
128
113
 
129
114
  const loopback = ["localhost", "127.0.0.1"];
@@ -137,28 +122,39 @@ const loopback = ["localhost", "127.0.0.1"];
137
122
  // authentication, too late.
138
123
  export const requestPolicy = HttpRouter.middleware(
139
124
  Effect.gen(function* () {
140
- const hosts = [...loopback, new URL(yield* publicUrl).hostname];
125
+ const hosts = new Set([...loopback, new URL(yield* publicUrl).hostname]);
141
126
  const origins = yield* allowedOrigins;
142
127
 
143
128
  return (httpEffect) =>
144
129
  Effect.gen(function* () {
145
130
  const request = yield* HttpServerRequest.HttpServerRequest;
146
- const url = HttpServerRequest.toURL(request.modify({ url: request.originalUrl }));
147
131
 
148
- if (Option.isNone(url) || !hosts.includes(url.value.hostname)) {
132
+ const url = HttpServerRequest.toURL(
133
+ request.modify({ url: request.originalUrl })
134
+ );
135
+
136
+ if (Option.isNone(url) || !hosts.has(url.value.hostname)) {
149
137
  return HttpServerResponse.text("Host not allowed", { status: 403 });
150
138
  }
151
139
 
152
- const origin = request.headers.origin;
153
-
154
- if (origin !== undefined && origin !== url.value.origin && !origins.includes(origin)) {
140
+ const { origin } = request.headers;
141
+
142
+ // The request's own origin is that of a page served from this port directly. Behind a
143
+ // proxy ending TLS it is an `http:` one from Effect 4.0.6, whose `originalUrl` takes the
144
+ // scheme of the connection, whatever `X-Forwarded-Proto` says, so the public URL's
145
+ // origin is among `origins`.
146
+ if (
147
+ origin !== undefined &&
148
+ origin !== url.value.origin &&
149
+ !origins.includes(origin)
150
+ ) {
155
151
  return HttpServerResponse.text("Origin not allowed", { status: 403 });
156
152
  }
157
153
 
158
154
  return yield* httpEffect;
159
155
  });
160
156
  }),
161
- { global: true },
157
+ { global: true }
162
158
  );
163
159
 
164
160
  // CORS for the one cross-origin caller, a browser MCP client. `HttpRouter.cors` is global to
@@ -181,18 +177,18 @@ export const cors = Layer.unwrap(
181
177
  "MCP-Name",
182
178
  "Mcp-Session-Id",
183
179
  ],
184
- exposedHeaders: ["WWW-Authenticate", "MCP-Protocol-Version", "Mcp-Session-Id"],
185
- }),
186
- ),
180
+ exposedHeaders: [
181
+ "WWW-Authenticate",
182
+ "MCP-Protocol-Version",
183
+ "Mcp-Session-Id",
184
+ ],
185
+ })
186
+ )
187
187
  );
188
188
  ```
189
189
 
190
- See `publicRoutes` in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts);
191
- [kitchen-sink/test/request-policy.test.ts](../examples/kitchen-sink/test/request-policy.test.ts)
192
- asserts it.
190
+ See `publicRoutes` in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts); [kitchen-sink/test/request-policy.test.ts](../examples/kitchen-sink/test/request-policy.test.ts) asserts it.
193
191
 
194
192
  ## Refuse outside the router
195
193
 
196
- For a caller the router never routes, such as a Node `upgrade` handler admitting a socket, read
197
- the token with `Authentication.bearerTokenOf` and answer a refusal with
198
- `Authentication.refusalResponse`, whose JSDoc holds the rest.
194
+ For a caller the router never routes, such as a Node `upgrade` handler admitting a socket, read the token with `Authentication.bearerTokenOf` and answer a refusal with `Authentication.refusalResponse`, whose JSDoc holds the rest.
@@ -1,9 +1,6 @@
1
1
  # Authentication failure modes
2
2
 
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),
6
- [Authentication-advanced.md](Authentication-advanced.md).
3
+ What you see when something is wrong, and the fix: compiler diagnostics and errors the library does not word. Search for a fragment of the message. A message the library throws says its fix itself. Tasks: [Authentication.md](Authentication.md), [Authentication-advanced.md](Authentication-advanced.md).
7
4
 
8
5
  ## Types and startup
9
6
 
@@ -13,6 +10,10 @@ fix itself. Tasks: [Authentication.md](Authentication.md),
13
10
  - `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
11
  - `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)).
15
12
 
13
+ ## Signing in
14
+
15
+ - An MCP client reports `Protected resource … does not match expected …` and requests no token: the published `protectedResource.resource` is neither the endpoint's public URL nor its origin. Give one of them.
16
+
16
17
  ## Scopes
17
18
 
18
19
  - `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.
@@ -1,24 +1,17 @@
1
1
  # Authentication
2
2
 
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.
3
+ How a remote caller proves the identity a protected action declares. `Authentication.make` declares it, as a browser-safe descriptor; `Authentication.layer` is that descriptor's provider, running the verifier you write. Verifying a token, login and consent stay in the application.
6
4
 
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.
5
+ A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [Authentication-failures.md](Authentication-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
11
6
 
12
7
  ## Declare how callers sign in
13
8
 
14
- A protected action declares its identity service, `caller: CurrentUser`
15
- ([Action.md](Action.md#say-who-may-call-an-action)). Declare the descriptor beside that
16
- service, in the contract file every client of the binding imports:
9
+ A protected action declares its identity service, `caller: CurrentUser` ([Action.md](Action.md#say-who-may-call-an-action)). Declare the descriptor beside that service, in the contract file every client of the binding imports:
17
10
 
18
11
  ```ts example=notes-service/src/identity/Identity.ts
19
- import { Context, Schema } from "effect";
20
12
  import * as Action from "@gjermundgaraba/effect-actions/Action";
21
13
  import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
14
+ import { Context, Schema } from "effect";
22
15
 
23
16
  export const scopes = { read: "notes:read", write: "notes:write" } as const;
24
17
 
@@ -34,7 +27,7 @@ export interface User {
34
27
  * provides it per request, and a local host, such as the stdio server, provides it itself.
35
28
  */
36
29
  export class CurrentUser extends Context.Service<CurrentUser, User>()(
37
- "notes-service/CurrentUser",
30
+ "notes-service/CurrentUser"
38
31
  ) {}
39
32
 
40
33
  // How a remote caller proves it is CurrentUser: a bearer token, the default scheme. Clients
@@ -49,45 +42,36 @@ export const WhoAmI = Action.make("whoAmI", {
49
42
  });
50
43
  ```
51
44
 
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).
45
+ Name it in every binding and MCP endpoint that serves a protected action: an HTTP binding ([notes-service/src/Api.ts](../examples/notes-service/src/Api.ts)), an MCP endpoint ([notes-service/src/routes.ts](../examples/notes-service/src/routes.ts)) and an RPC binding ([live-rpc/src/Rpc.ts](../examples/live-rpc/src/Rpc.ts)); a local surface takes none ([Action.md](Action.md#say-who-may-call-an-action)). The name's rules are on `Authentication.make`, the scheme's on `Authentication.Options.security`; a second credential for one identity is in [kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts). A descriptor of protected [events](Event.md) may also say which subscriber an identity is, its `principal`, for what a surface holds between one subscriber's requests: [build-events/src/identity/Identity.ts](../examples/build-events/src/identity/Identity.ts). When it is required is on `Authentication.Options.principal`; its `schema`, which keeps an identity past a restart, on `Authentication.Options.schema`.
60
46
 
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.
47
+ - **Protecting an action that tests already call breaks them**, at compile time ([Testing-failures.md](Testing-failures.md), [Authentication-failures.md](Authentication-failures.md#types-and-startup)), and a call over HTTP without a credential gets `Unauthenticated` at run time.
65
48
 
66
49
  ## Verify credentials
67
50
 
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`.
51
+ Write the verifier in a server-only module, give it to `Authentication.layer` and provide that layer as [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts) does. What the verifier takes, fails with and must check itself is on `Authentication.Verify`; where its layer goes, on `Authentication.layer`.
72
52
 
73
53
  ```ts example=notes-service/src/identity/verifier.ts
74
- import { Config, Effect, Redacted } from "effect";
75
54
  import * as Action from "@gjermundgaraba/effect-actions/Action";
76
55
  import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
56
+ import { Config, Effect, Redacted } from "effect";
57
+
77
58
  import { Login, scopes } from "./Identity.js";
78
59
  import { users } from "./users.js";
79
60
 
80
- const isToken = (token: string): token is keyof typeof users => Object.hasOwn(users, token);
61
+ const isToken = (token: string): token is keyof typeof users =>
62
+ Object.hasOwn(users, token);
81
63
 
82
64
  // DEMO ONLY: a token is a user's name. A real verifier checks a signed token with your
83
65
  // authorization server's library, its audience included. An unknown token is the built-in
84
66
  // `Unauthenticated`: a 401 every client decodes.
85
- const verify = (token: Redacted.Redacted<string>) => {
67
+ const verify = (token: Redacted.Redacted) => {
86
68
  const name = Redacted.value(token);
87
69
 
88
70
  return isToken(name)
89
71
  ? Effect.succeed(users[name])
90
- : Effect.fail(new Action.Unauthenticated({ message: "Unknown demo token." }));
72
+ : Effect.fail(
73
+ new Action.Unauthenticated({ message: "Unknown demo token." })
74
+ );
91
75
  };
92
76
 
93
77
  // Server only: `Login`'s provider. As an OAuth protected resource it also publishes discovery,
@@ -95,7 +79,9 @@ const verify = (token: Redacted.Redacted<string>) => {
95
79
  // is the server's public URL, known only at startup, so it is read from config.
96
80
  export const authenticate = Authentication.layer(Login, verify, {
97
81
  protectedResource: Effect.map(
98
- Config.String("PUBLIC_URL").pipe(Config.withDefault("http://localhost:3000")),
82
+ Config.String("PUBLIC_URL").pipe(
83
+ Config.withDefault("http://localhost:3000")
84
+ ),
99
85
  (url) =>
100
86
  ({
101
87
  resource: `${url}/mcp`,
@@ -103,38 +89,17 @@ export const authenticate = Authentication.layer(Login, verify, {
103
89
  authorizationServers: ["https://auth.example.com"],
104
90
  scopesSupported: [scopes.read, scopes.write],
105
91
  scopesRequired: [scopes.read],
106
- }) satisfies Authentication.ProtectedResource,
92
+ }) satisfies Authentication.ProtectedResource
107
93
  ),
108
94
  });
109
95
  ```
110
96
 
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.
97
+ For a verifier built from startup services, see `authenticateBearer` in [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts); for a failure that is not the caller's, `Authentication.Options.error` and `IssuerUnavailable` in [kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts); to test as a signed-in caller, [Testing.md](Testing.md#test-as-a-signed-in-caller). What each surface answers a missing or refused credential with is on `Action.Unauthenticated` and `Action.Forbidden`, as [kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts) asserts, status and challenge included.
120
98
 
121
99
  ## Publish OAuth discovery
122
100
 
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).
101
+ For an MCP endpoint whose users sign in through OAuth, give the Bearer descriptor's `layer` a `protectedResource`, as the verifier above does. Its rules are on `Authentication.LayerOptions.protectedResource` and on each field of `Authentication.ProtectedResource`; [kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts) asserts the discovery it serves. To restrict who reads it, see [Run a request policy before authentication](Authentication-advanced.md#run-a-request-policy-before-authentication).
130
102
 
131
103
  ### Step up for a scope
132
104
 
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.
105
+ To have an OAuth client sign in again, or re-authorize with more scopes, fail the authorizer, a handler or the verifier with `Action.Unauthenticated`, or with `Action.Forbidden` naming `scopes` ([notes-service/src/identity/authorize.ts](../examples/notes-service/src/identity/authorize.ts)); `inService` in [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts) refuses with none. What each answers on each surface, and when to name scopes, are on those two classes.
@@ -0,0 +1,15 @@
1
+ # Event failure modes
2
+
3
+ What you see when something is wrong, and the fix. Search for a fragment of the message. The tasks are in [Event.md](Event.md).
4
+
5
+ ## At `implement`
6
+
7
+ - `is not assignable to parameter of type '... & "A protected event takes { authorize }: your rule, or Event.allowAll"'`: the list holds a protected event type, which takes `{ authorize }`. Give your rule, or `Event.allowAll`.
8
+ - `Type '{ authorize: ... }' is not assignable to type '{ readonly authorize?: "A public-only target takes no authorize" }'`: every event type in the list is public, and no authorizer runs for one. Remove `authorize`.
9
+ - `Object literal may only specify known properties, and 'log' does not exist`, or `Type '{}' is not assignable to type 'undefined'` at a source: it holds both `check` and `log`. Keep one: `check` where a store can be asked what happened since a cursor, `log` where nothing else keeps the occurrences.
10
+ - `Parameter 'input' implicitly has an 'any' type` at a check, a `match` or a `transform`: the builder is wrapped, as in `Effect.gen(…).pipe(Effect.provide(layer))`, and its sources are no longer typed from the contracts. Give `implement` the builder itself.
11
+
12
+ ## At `watch`
13
+
14
+ - `Property 'watch' does not exist on type`: the client was given no event implementation. Pass the event implementations beside the action ones, `Action.client([builds, buildEvents])`.
15
+ - `Argument of type 'Event<...>' is not assignable to parameter of type` at `watch`: the client holds no implementation of that event type. Add its implementation to the list.