@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,64 +1,72 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
2
3
  import { Array, Effect, Layer, References } from "effect";
3
4
  import { RateLimiter } from "effect/persistence";
4
- import * as Action from "@gjermundgaraba/effect-actions/Action";
5
- import { CurrentMember } from "../../src/identity/Identity.js";
5
+
6
6
  import { members } from "../../src/identity/members.js";
7
7
  import { issues } from "../../src/issues/handlers.js";
8
8
  import { IssueNotFound, Issues, RateLimited } from "../../src/issues/Issues.js";
9
9
  import { IssueStore } from "../../src/issues/store.js";
10
10
  import { status } from "../../src/workspaces/handlers.js";
11
-
12
- // What the implementation does, with no transport: `Action.client` runs the same decoding,
13
- // `authorize` and handler a server runs. Each call names its caller, as a request would.
14
- const as = (member: keyof typeof members) => Effect.provideService(CurrentMember, members[member]);
11
+ import { as } from "../support/as.js";
15
12
 
16
13
  // The two startup services the issues' builder yields, and nothing else of the application.
17
14
  const services = Layer.mergeAll(
18
15
  IssueStore.layerMemory,
19
- RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory)),
16
+ RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory))
20
17
  );
21
18
 
22
19
  describe("issues, in process", () => {
23
20
  it.effect("keeps each workspace's issues its own", () =>
24
21
  Effect.gen(function* () {
25
22
  const client = yield* Action.client(issues);
26
- const issue = yield* client.createIssue({ title: "Crash", body: "On save." }).pipe(as("ada"));
23
+
24
+ const issue = yield* client
25
+ .createIssue({ title: "Crash", body: "On save." })
26
+ .pipe(as("ada"));
27
27
 
28
28
  expect(issue).toMatchObject({ id: "ACME-1", status: "open" });
29
- expect(yield* client.listIssues().pipe(as("linus"))).toEqual({
29
+ expect(yield* client.listIssues().pipe(as("linus"))).toStrictEqual({
30
30
  issues: [{ id: "ACME-1", title: "Crash", status: "open" }],
31
31
  });
32
- expect(yield* client.listIssues().pipe(as("grace"))).toEqual({ issues: [] });
33
- expect(yield* Effect.flip(client.getIssue({ id: issue.id }).pipe(as("grace")))).toEqual(
34
- new IssueNotFound({ id: "ACME-1" }),
35
- );
36
- }).pipe(Effect.provide(services)),
32
+ expect(yield* client.listIssues().pipe(as("grace"))).toStrictEqual({
33
+ issues: [],
34
+ });
35
+ expect(
36
+ yield* Effect.flip(client.getIssue({ id: issue.id }).pipe(as("grace")))
37
+ ).toStrictEqual(new IssueNotFound({ id: "ACME-1" }));
38
+ }).pipe(Effect.provide(services))
37
39
  );
38
40
 
39
- it.effect("lists twenty by default, newest first, or the status and limit asked for", () =>
40
- Effect.gen(function* () {
41
- const client = yield* Action.client(issues);
42
- const store = yield* IssueStore;
43
-
44
- // Written to the store, not through `createIssue`: twenty-one are over its write limit.
45
- yield* Effect.forEach(Array.range(1, 21), (n) =>
46
- store.create(members.ada.workspace, { title: `Issue ${n}`, body: "" }),
47
- );
48
- yield* client.closeIssue({ id: "ACME-2" }).pipe(as("ada"));
49
-
50
- const all = yield* client.listIssues().pipe(as("ada"));
51
- const closed = yield* client.listIssues({ status: "closed", limit: 20 }).pipe(as("ada"));
52
- const newest = yield* client.listIssues({ limit: 1 }).pipe(as("ada"));
53
-
54
- expect(all.issues.map(({ id }) => id)).toEqual(
55
- Array.range(2, 21)
56
- .map((n) => `ACME-${n}`)
57
- .toReversed(),
58
- );
59
- expect(closed.issues.map(({ id }) => id)).toEqual(["ACME-2"]);
60
- expect(newest.issues.map(({ id }) => id)).toEqual(["ACME-21"]);
61
- }).pipe(Effect.provide(services)),
41
+ it.effect(
42
+ "lists twenty by default, newest first, or the status and limit asked for",
43
+ () =>
44
+ Effect.gen(function* () {
45
+ const client = yield* Action.client(issues);
46
+ const store = yield* IssueStore;
47
+
48
+ // Written to the store, not through `createIssue`: twenty-one are over its write limit.
49
+ yield* Effect.forEach(Array.range(1, 21), (n) =>
50
+ store.create(members.ada.workspace, { title: `Issue ${n}`, body: "" })
51
+ );
52
+ yield* client.closeIssue({ id: "ACME-2" }).pipe(as("ada"));
53
+
54
+ const all = yield* client.listIssues().pipe(as("ada"));
55
+
56
+ const closed = yield* client
57
+ .listIssues({ status: "closed", limit: 20 })
58
+ .pipe(as("ada"));
59
+
60
+ const newest = yield* client.listIssues({ limit: 1 }).pipe(as("ada"));
61
+
62
+ expect(all.issues.map(({ id }) => id)).toStrictEqual(
63
+ Array.range(2, 21)
64
+ .map((n) => `ACME-${n}`)
65
+ .toReversed()
66
+ );
67
+ expect(closed.issues.map(({ id }) => id)).toStrictEqual(["ACME-2"]);
68
+ expect(newest.issues.map(({ id }) => id)).toStrictEqual(["ACME-21"]);
69
+ }).pipe(Effect.provide(services))
62
70
  );
63
71
 
64
72
  it.effect("refuses a write from a member holding the read scope alone", () =>
@@ -68,9 +76,11 @@ describe("issues, in process", () => {
68
76
 
69
77
  const refused = yield* Effect.flip(create.pipe(as("linus")));
70
78
 
71
- expect(refused).toEqual(new Action.Forbidden({ scopes: ["issues:write"] }));
79
+ expect(refused).toStrictEqual(
80
+ new Action.Forbidden({ scopes: ["issues:write"] })
81
+ );
72
82
  expect(refused.message).toBe("Requires issues:write.");
73
- }).pipe(Effect.provide(services)),
83
+ }).pipe(Effect.provide(services))
74
84
  );
75
85
 
76
86
  it.effect("keeps both of two comments made at once", () =>
@@ -84,50 +94,70 @@ describe("issues, in process", () => {
84
94
  // three, the two do not finish.
85
95
  const kept = yield* Effect.forEach(Array.range(3, 60), (steps) =>
86
96
  Effect.gen(function* () {
87
- const { id } = yield* store.create(workspace, { title: "Crash", body: "" });
88
-
89
- yield* Effect.all(
90
- ["One", "Two"].map((text) => store.comment(workspace, id, { author: "ada", text })),
91
- { concurrency: "unbounded" },
97
+ const { id } = yield* store.create(workspace, {
98
+ title: "Crash",
99
+ body: "",
100
+ });
101
+
102
+ yield* Effect.forEach(
103
+ ["One", "Two"],
104
+ (text) => store.comment(workspace, id, { author: "ada", text }),
105
+ { concurrency: "unbounded" }
92
106
  ).pipe(Effect.provideService(References.MaxOpsBeforeYield, steps));
93
107
 
94
108
  return (yield* store.get(workspace, id)).comments.length;
95
- }),
109
+ })
96
110
  );
97
111
 
98
- expect(kept).toEqual(Array.range(3, 60).map(() => 2));
99
- }).pipe(Effect.provide(services)),
112
+ expect(kept).toStrictEqual(Array.range(3, 60).map(() => 2));
113
+ }).pipe(Effect.provide(services))
100
114
  );
101
115
 
102
- it.effect("counts every write of a member against one limit, and no one else's", () =>
103
- Effect.gen(function* () {
104
- const client = yield* Action.client(issues);
105
-
106
- yield* client.createIssue({ title: "Crash", body: "" }).pipe(as("ada"));
107
- yield* Effect.forEach(Array.range(1, 9), (n) =>
108
- client.commentOnIssue({ id: "ACME-1", text: `Comment ${n}` }).pipe(as("ada")),
109
- );
110
-
111
- // Ten writes are spent, by two actions: the eleventh, of a third, is refused.
112
- expect(
113
- yield* Effect.flip(client.closeIssue({ id: "ACME-1" }).pipe(as("ada"))),
114
- ).toBeInstanceOf(RateLimited);
115
- expect(yield* client.getIssue({ id: "ACME-1" }).pipe(as("ada"))).toMatchObject({
116
- status: "open",
117
- });
118
- expect(
119
- yield* client.createIssue({ title: "Mine", body: "" }).pipe(as("grace")),
120
- ).toMatchObject({ id: "GLOBEX-1" });
121
- }).pipe(Effect.provide(services)),
116
+ it.effect(
117
+ "counts every write of a member against one limit, and no one else's",
118
+ () =>
119
+ Effect.gen(function* () {
120
+ const client = yield* Action.client(issues);
121
+
122
+ yield* client.createIssue({ title: "Crash", body: "" }).pipe(as("ada"));
123
+ yield* Effect.forEach(Array.range(1, 9), (n) =>
124
+ client
125
+ .commentOnIssue({ id: "ACME-1", text: `Comment ${n}` })
126
+ .pipe(as("ada"))
127
+ );
128
+
129
+ // Ten writes are spent, by two actions: the eleventh, of a third, is refused.
130
+ expect(
131
+ yield* Effect.flip(
132
+ client.closeIssue({ id: "ACME-1" }).pipe(as("ada"))
133
+ )
134
+ ).toBeInstanceOf(RateLimited);
135
+ expect(
136
+ yield* client.getIssue({ id: "ACME-1" }).pipe(as("ada"))
137
+ ).toMatchObject({
138
+ status: "open",
139
+ });
140
+ expect(
141
+ yield* client
142
+ .createIssue({ title: "Mine", body: "" })
143
+ .pipe(as("grace"))
144
+ ).toMatchObject({ id: "GLOBEX-1" });
145
+ }).pipe(Effect.provide(services))
122
146
  );
123
147
 
124
- it.effect("builds only the implementations whose actions a client lists", () =>
125
- Effect.gen(function* () {
126
- // Two implementations are passed, and `actions` selects the issues': `status` is left
127
- // out, unbuilt, so `services` need not hold the registry its builder yields.
128
- const client = yield* Action.client([status, issues], { actions: Issues });
129
-
130
- expect(yield* client.listIssues().pipe(as("ada"))).toEqual({ issues: [] });
131
- }).pipe(Effect.provide(services)),
148
+ it.effect(
149
+ "builds only the implementations whose actions a client lists",
150
+ () =>
151
+ Effect.gen(function* () {
152
+ // Two implementations are passed, and `actions` selects the issues': `status` is left
153
+ // out, unbuilt, so `services` need not hold the registry its builder yields.
154
+ const client = yield* Action.client([status, issues], {
155
+ actions: Issues,
156
+ });
157
+
158
+ expect(yield* client.listIssues().pipe(as("ada"))).toStrictEqual({
159
+ issues: [],
160
+ });
161
+ }).pipe(Effect.provide(services))
132
162
  );
133
163
  });
@@ -1,25 +1,37 @@
1
+ import { NodeHttpServer } from "@effect/platform-node";
1
2
  import { afterAll, describe, expect, it } from "@effect/vitest";
2
- import { Effect, Layer } from "effect";
3
- import { HttpRouter, HttpServer } from "effect/http";
4
3
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
5
4
  import * as Testing from "@gjermundgaraba/effect-actions/Testing";
5
+ import { ConfigProvider, Effect, Layer } from "effect";
6
+ import {
7
+ HttpClient,
8
+ HttpClientRequest,
9
+ HttpRouter,
10
+ HttpServer,
11
+ } from "effect/http";
12
+
6
13
  import { Http } from "../src/Api.js";
7
14
  import { memory } from "../src/memory.js";
8
15
  import { adminRoutes, publicRoutes } from "../src/routes.js";
16
+ import { services } from "./support/services.js";
9
17
 
10
18
  // Each port as a web handler, `Request` in and `Response` out: what a platform without
11
19
  // Effect's server runs, and what a test sends raw requests to. A handler builds its routes
12
20
  // once; the test owns it, and disposes it when done.
13
21
  const web = HttpRouter.toWebHandler(
14
- publicRoutes.pipe(Layer.provide(HttpServer.layerServices), Layer.provide(memory)),
22
+ publicRoutes.pipe(
23
+ Layer.provide(HttpServer.layerServices),
24
+ Layer.provide(services())
25
+ )
15
26
  );
16
27
 
17
28
  const adminWeb = HttpRouter.toWebHandler(
18
- adminRoutes.pipe(Layer.provide(HttpServer.layerServices), Layer.provide(memory)),
29
+ adminRoutes.pipe(
30
+ Layer.provide(HttpServer.layerServices),
31
+ Layer.provide(memory)
32
+ )
19
33
  );
20
34
 
21
- afterAll(() => Promise.all([web.dispose(), adminWeb.dispose()]));
22
-
23
35
  const post = (url: string, headers: Record<string, string> = {}) =>
24
36
  new Request(url, {
25
37
  method: "POST",
@@ -27,32 +39,49 @@ const post = (url: string, headers: Record<string, string> = {}) =>
27
39
  body: "{}",
28
40
  });
29
41
 
42
+ const read = async (url: string, headers: Record<string, string> = {}) =>
43
+ await web.handler(new Request(url, { headers }));
44
+
30
45
  const discovery = "/.well-known/oauth-protected-resource/mcp";
31
46
 
32
47
  describe("the request policy", () => {
48
+ afterAll(async () => {
49
+ await Promise.all([web.dispose(), adminWeb.dispose()]);
50
+ });
51
+
33
52
  it("refuses a foreign Host or Origin", async () => {
34
- const foreignHost = await web.handler(post("http://tracker.evil.example/api/v1/getStatus"));
53
+ const foreignHost = await web.handler(
54
+ post("http://tracker.evil.example/api/v1/getStatus")
55
+ );
35
56
 
36
57
  const foreignOrigin = await web.handler(
37
- post("http://localhost/api/v1/getStatus", { origin: "http://evil.example" }),
58
+ post("http://localhost/api/v1/getStatus", {
59
+ origin: "http://evil.example",
60
+ })
38
61
  );
39
62
 
40
- expect([foreignHost.status, await foreignHost.text()]).toEqual([403, "Host not allowed"]);
41
- expect([foreignOrigin.status, await foreignOrigin.text()]).toEqual([403, "Origin not allowed"]);
63
+ expect([foreignHost.status, await foreignHost.text()]).toStrictEqual([
64
+ 403,
65
+ "Host not allowed",
66
+ ]);
67
+ expect([foreignOrigin.status, await foreignOrigin.text()]).toStrictEqual([
68
+ 403,
69
+ "Origin not allowed",
70
+ ]);
42
71
  });
43
72
 
44
73
  it("runs before authentication: a foreign Host is refused, not asked to sign in", async () => {
45
74
  const signedOut = await web.handler(post("http://localhost/api/v1/whoAmI"));
46
- const foreignHost = await web.handler(post("http://tracker.evil.example/api/v1/whoAmI"));
47
75
 
48
- expect([signedOut.status, foreignHost.status]).toEqual([401, 403]);
76
+ const foreignHost = await web.handler(
77
+ post("http://tracker.evil.example/api/v1/whoAmI")
78
+ );
79
+
80
+ expect([signedOut.status, foreignHost.status]).toStrictEqual([401, 403]);
49
81
  expect(foreignHost.headers.get("www-authenticate")).toBeNull();
50
82
  });
51
83
 
52
84
  it("runs before the discovery the Bearer provider publishes", async () => {
53
- const read = (url: string, headers: Record<string, string> = {}) =>
54
- web.handler(new Request(url, { headers }));
55
-
56
85
  const here = await read(`http://localhost${discovery}`);
57
86
  const foreignHost = await read(`http://tracker.evil.example${discovery}`);
58
87
 
@@ -60,12 +89,18 @@ describe("the request policy", () => {
60
89
  origin: "http://evil.test",
61
90
  });
62
91
 
63
- expect([here.status, foreignHost.status, foreignOrigin.status]).toEqual([200, 403, 403]);
92
+ expect([
93
+ here.status,
94
+ foreignHost.status,
95
+ foreignOrigin.status,
96
+ ]).toStrictEqual([200, 403, 403]);
64
97
  });
65
98
 
66
99
  it("answers the browser MCP client's origin, and its CORS preflight", async () => {
67
100
  const allowed = await web.handler(
68
- post("http://localhost/api/v1/getStatus", { origin: "http://localhost:6274" }),
101
+ post("http://localhost/api/v1/getStatus", {
102
+ origin: "http://localhost:6274",
103
+ })
69
104
  );
70
105
 
71
106
  const preflight = await web.handler(
@@ -74,16 +109,23 @@ describe("the request policy", () => {
74
109
  headers: {
75
110
  origin: "http://localhost:6274",
76
111
  "access-control-request-method": "POST",
77
- "access-control-request-headers": "authorization, mcp-protocol-version",
112
+ "access-control-request-headers":
113
+ "authorization, mcp-protocol-version",
78
114
  },
79
- }),
115
+ })
80
116
  );
81
117
 
82
118
  expect(allowed.status).toBe(200);
83
- expect(allowed.headers.get("access-control-allow-origin")).toBe("http://localhost:6274");
119
+ expect(allowed.headers.get("access-control-allow-origin")).toBe(
120
+ "http://localhost:6274"
121
+ );
84
122
  expect(preflight.status).toBe(204);
85
- expect(preflight.headers.get("access-control-allow-headers")).toContain("MCP-Protocol-Version");
86
- expect(allowed.headers.get("access-control-expose-headers")).toContain("Mcp-Session-Id");
123
+ expect(preflight.headers.get("access-control-allow-headers")).toContain(
124
+ "MCP-Protocol-Version"
125
+ );
126
+ expect(allowed.headers.get("access-control-expose-headers")).toContain(
127
+ "Mcp-Session-Id"
128
+ );
87
129
  });
88
130
 
89
131
  it("lets a browser client of an MCP revision with sessions end its session", async () => {
@@ -95,31 +137,86 @@ describe("the request policy", () => {
95
137
  "access-control-request-method": "DELETE",
96
138
  "access-control-request-headers": "authorization, mcp-session-id",
97
139
  },
98
- }),
140
+ })
99
141
  );
100
142
 
101
143
  expect(preflight.status).toBe(204);
102
- expect(preflight.headers.get("access-control-allow-methods")).toContain("DELETE");
103
- expect(preflight.headers.get("access-control-allow-headers")).toContain("Mcp-Session-Id");
144
+ expect(preflight.headers.get("access-control-allow-methods")).toContain(
145
+ "DELETE"
146
+ );
147
+ expect(preflight.headers.get("access-control-allow-headers")).toContain(
148
+ "Mcp-Session-Id"
149
+ );
104
150
  });
105
151
 
106
152
  it("covers the admin port too, a valid key included", async () => {
107
153
  const key = { "x-operator-key": "ops-key" };
108
- const here = await adminWeb.handler(post("http://localhost/admin/listWorkspaces", key));
154
+
155
+ const here = await adminWeb.handler(
156
+ post("http://localhost/admin/listWorkspaces", key)
157
+ );
109
158
 
110
159
  const foreignHost = await adminWeb.handler(
111
- post("http://tracker.evil.example/admin/listWorkspaces", key),
160
+ post("http://tracker.evil.example/admin/listWorkspaces", key)
112
161
  );
113
162
 
114
- expect([here.status, foreignHost.status]).toEqual([200, 403]);
163
+ expect([here.status, foreignHost.status]).toStrictEqual([200, 403]);
115
164
  });
116
165
 
117
- // `Testing.layer(handler)` gives the typed clients the same handler the raw requests hit.
118
- it.effect("leaves the typed client, served from the same web handler, alone", () =>
166
+ // Behind a proxy ending TLS, Node's server sees `http:`: the public URL's origin is allowed by
167
+ // name, not as the request's own, so a page of it is answered whatever the proxy forwards.
168
+ it.effect("answers the public URL's page behind a proxy ending TLS", () =>
119
169
  Effect.gen(function* () {
120
- const client = yield* ActionHttp.client(Http);
170
+ const client = yield* HttpClient.HttpClient;
171
+
172
+ const status = (origin: string) =>
173
+ client
174
+ .execute(
175
+ HttpClientRequest.post("/api/v1/getStatus").pipe(
176
+ HttpClientRequest.setHeaders({
177
+ origin,
178
+ "x-forwarded-proto": "https",
179
+ }),
180
+ HttpClientRequest.bodyJsonUnsafe({})
181
+ )
182
+ )
183
+ .pipe(Effect.map((response) => response.status));
184
+
185
+ expect([
186
+ yield* status("https://tracker.example"),
187
+ yield* status("https://evil.example"),
188
+ ]).toStrictEqual([200, 403]);
189
+ }).pipe(
190
+ Effect.provide(
191
+ HttpRouter.serve(publicRoutes, {
192
+ disableLogger: true,
193
+ disableListenLog: true,
194
+ }).pipe(
195
+ Layer.provide(services()),
196
+ Layer.provideMerge(NodeHttpServer.layerTest),
197
+ Layer.provide(
198
+ ConfigProvider.layer(
199
+ ConfigProvider.fromEnvRecord({
200
+ PUBLIC_URL: "https://tracker.example",
201
+ })
202
+ )
203
+ )
204
+ )
205
+ )
206
+ )
207
+ );
121
208
 
122
- expect(yield* client.getStatus()).toMatchObject({ service: "tracker", workspaces: 2 });
123
- }).pipe(Effect.provide(Testing.layer(web.handler))),
209
+ // `Testing.layer(handler)` gives the typed clients the same handler the raw requests hit.
210
+ it.effect(
211
+ "leaves the typed client, served from the same web handler, alone",
212
+ () =>
213
+ Effect.gen(function* () {
214
+ const client = yield* ActionHttp.client(Http);
215
+
216
+ expect(yield* client.getStatus()).toMatchObject({
217
+ service: "tracker",
218
+ workspaces: 2,
219
+ });
220
+ }).pipe(Effect.provide(Testing.layer(web.handler)))
124
221
  );
125
222
  });