@gjermundgaraba/effect-actions 0.20.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 (321) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -46
  3. package/dist/Action.mjs +87 -42
  4. package/dist/ActionCli.d.mts +69 -33
  5. package/dist/ActionCli.mjs +51 -32
  6. package/dist/ActionHttp.d.mts +69 -35
  7. package/dist/ActionHttp.mjs +75 -23
  8. package/dist/ActionMcp.d.mts +56 -22
  9. package/dist/ActionMcp.mjs +94 -69
  10. package/dist/ActionRpc.d.mts +222 -55
  11. package/dist/ActionRpc.mjs +299 -46
  12. package/dist/ActionToolkit.d.mts +22 -5
  13. package/dist/ActionToolkit.mjs +36 -12
  14. package/dist/Authentication.d.mts +42 -10
  15. package/dist/Authentication.mjs +37 -30
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.mjs +1 -1
  19. package/dist/Webhook.d.mts +146 -0
  20. package/dist/Webhook.mjs +72 -0
  21. package/dist/authentication/provider.d.mts +35 -10
  22. package/dist/authentication/provider.mjs +1 -1
  23. package/dist/authentication/refusal.mjs +2 -2
  24. package/dist/cli/commands.d.mts +9 -5
  25. package/dist/cli/commands.mjs +84 -33
  26. package/dist/contract/ast.mjs +4 -1
  27. package/dist/contract/authorizer.d.mts +8 -0
  28. package/dist/contract/authorizer.mjs +20 -0
  29. package/dist/contract/call.d.mts +9 -6
  30. package/dist/contract/call.mjs +48 -6
  31. package/dist/contract/callback.d.mts +7 -0
  32. package/dist/contract/callback.mjs +27 -0
  33. package/dist/contract/caller.d.mts +13 -2
  34. package/dist/contract/caller.mjs +22 -3
  35. package/dist/contract/canonical.mjs +10 -0
  36. package/dist/contract/codec.d.mts +9 -0
  37. package/dist/contract/codec.mjs +9 -0
  38. package/dist/contract/codes.mjs +81 -0
  39. package/dist/contract/declared.mjs +25 -9
  40. package/dist/contract/digest.mjs +6 -0
  41. package/dist/contract/errors.d.mts +9 -3
  42. package/dist/contract/errors.mjs +1 -1
  43. package/dist/contract/feed.d.mts +127 -0
  44. package/dist/contract/feed.mjs +249 -0
  45. package/dist/contract/hub.d.mts +94 -0
  46. package/dist/contract/hub.mjs +133 -0
  47. package/dist/contract/idempotency.d.mts +47 -0
  48. package/dist/contract/idempotency.mjs +99 -0
  49. package/dist/contract/implementation.d.mts +73 -47
  50. package/dist/contract/implementation.mjs +92 -58
  51. package/dist/contract/input-check.d.mts +15 -0
  52. package/dist/contract/input-check.mjs +16 -0
  53. package/dist/contract/lease.mjs +28 -0
  54. package/dist/contract/listed.d.mts +4 -0
  55. package/dist/contract/listed.mjs +22 -0
  56. package/dist/contract/log.d.mts +41 -0
  57. package/dist/contract/log.mjs +144 -0
  58. package/dist/contract/names.mjs +14 -4
  59. package/dist/contract/occurrence.d.mts +29 -0
  60. package/dist/contract/occurrence.mjs +45 -0
  61. package/dist/contract/pages.d.mts +13 -0
  62. package/dist/contract/pages.mjs +39 -0
  63. package/dist/contract/progress.d.mts +26 -0
  64. package/dist/contract/progress.mjs +72 -0
  65. package/dist/contract/push.d.mts +20 -0
  66. package/dist/contract/push.mjs +96 -0
  67. package/dist/contract/receiving.d.mts +29 -0
  68. package/dist/contract/receiving.mjs +42 -0
  69. package/dist/contract/recent.mjs +12 -0
  70. package/dist/contract/reported.mjs +16 -0
  71. package/dist/contract/serving.mjs +63 -0
  72. package/dist/contract/status.mjs +14 -5
  73. package/dist/contract/stream.d.mts +34 -0
  74. package/dist/contract/stream.mjs +31 -0
  75. package/dist/contract/subscribe.d.mts +45 -0
  76. package/dist/contract/subscribe.mjs +50 -0
  77. package/dist/contract/transient.mjs +28 -0
  78. package/dist/contract/watch.d.mts +18 -0
  79. package/dist/contract/watch.mjs +105 -0
  80. package/dist/contract/webhooks.d.mts +42 -0
  81. package/dist/contract/webhooks.mjs +53 -0
  82. package/dist/http/client.d.mts +37 -12
  83. package/dist/http/client.mjs +51 -12
  84. package/dist/http/events.d.mts +56 -0
  85. package/dist/http/events.mjs +102 -0
  86. package/dist/http/placed.d.mts +36 -0
  87. package/dist/http/placed.mjs +71 -0
  88. package/dist/http/schema-errors.mjs +1 -1
  89. package/dist/http/streamed.d.mts +21 -0
  90. package/dist/http/streamed.mjs +94 -0
  91. package/dist/http/watch.mjs +50 -0
  92. package/dist/mcp/bridge.mjs +283 -0
  93. package/dist/mcp/client.d.mts +30 -97
  94. package/dist/mcp/client.mjs +59 -132
  95. package/dist/mcp/events.mjs +142 -0
  96. package/dist/mcp/progress.mjs +34 -0
  97. package/dist/mcp/protocol.mjs +39 -13
  98. package/dist/mcp/reply.d.mts +79 -0
  99. package/dist/mcp/reply.mjs +99 -0
  100. package/dist/mcp/request.d.mts +40 -0
  101. package/dist/mcp/request.mjs +51 -0
  102. package/dist/mcp/watch.mjs +115 -0
  103. package/dist/rpc/watch.mjs +39 -0
  104. package/dist/stdio/console.mjs +13 -5
  105. package/dist/testing/memory.d.mts +1 -1
  106. package/dist/testing/memory.mjs +3 -3
  107. package/dist/webhook/address.mjs +47 -0
  108. package/dist/webhook/attempt.mjs +65 -0
  109. package/dist/webhook/receiver.mjs +67 -0
  110. package/dist/webhook/signing.mjs +28 -0
  111. package/dist/webhook/stored.mjs +57 -0
  112. package/dist/webhook/subscriptions.mjs +342 -0
  113. package/docs/Action-failures.md +2 -2
  114. package/docs/Action-streams.md +115 -0
  115. package/docs/Action.md +58 -124
  116. package/docs/ActionCli-failures.md +1 -2
  117. package/docs/ActionCli.md +83 -88
  118. package/docs/ActionHttp-failures.md +3 -3
  119. package/docs/ActionHttp.md +90 -70
  120. package/docs/ActionMcp-failures.md +2 -1
  121. package/docs/ActionMcp.md +120 -92
  122. package/docs/ActionRpc-events.md +126 -0
  123. package/docs/ActionRpc-failures.md +1 -2
  124. package/docs/ActionRpc.md +108 -79
  125. package/docs/ActionToolkit-failures.md +1 -2
  126. package/docs/ActionToolkit.md +33 -90
  127. package/docs/Authentication-advanced.md +52 -60
  128. package/docs/Authentication-failures.md +5 -4
  129. package/docs/Authentication.md +23 -58
  130. package/docs/Event-failures.md +15 -0
  131. package/docs/Event.md +281 -0
  132. package/docs/README.md +51 -82
  133. package/docs/Testing-failures.md +1 -5
  134. package/docs/Testing.md +126 -86
  135. package/docs/Webhook-failures.md +8 -0
  136. package/docs/Webhook.md +319 -0
  137. package/docs/setup-failures.md +2 -3
  138. package/docs/setup.md +32 -52
  139. package/examples/README.md +12 -37
  140. package/examples/assistant/README.md +21 -26
  141. package/examples/assistant/src/agent.ts +66 -24
  142. package/examples/assistant/src/cli.ts +7 -4
  143. package/examples/assistant/src/commands.ts +9 -4
  144. package/examples/assistant/src/identity/Identity.ts +5 -4
  145. package/examples/assistant/src/identity/authorize.ts +13 -5
  146. package/examples/assistant/src/identity/handlers.ts +3 -2
  147. package/examples/assistant/src/identity/permissions.ts +9 -3
  148. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  149. package/examples/assistant/src/scripted.ts +7 -4
  150. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  151. package/examples/assistant/src/tasks/handlers.ts +9 -5
  152. package/examples/assistant/src/tasks/store.ts +38 -11
  153. package/examples/assistant/src/toolkit.ts +11 -7
  154. package/examples/assistant/test/commands.test.ts +53 -27
  155. package/examples/assistant/test/identity/identity.test.ts +27 -11
  156. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  157. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  158. package/examples/assistant/test/toolkit.test.ts +104 -75
  159. package/examples/build-events/README.md +83 -0
  160. package/examples/build-events/src/Api.ts +27 -0
  161. package/examples/build-events/src/Rpc.ts +26 -0
  162. package/examples/build-events/src/builds/Builds.ts +108 -0
  163. package/examples/build-events/src/builds/events.ts +54 -0
  164. package/examples/build-events/src/builds/handlers.ts +58 -0
  165. package/examples/build-events/src/builds/log.ts +30 -0
  166. package/examples/build-events/src/builds/status.ts +28 -0
  167. package/examples/build-events/src/builds/store.ts +130 -0
  168. package/examples/build-events/src/cli.ts +18 -0
  169. package/examples/build-events/src/commands.ts +32 -0
  170. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  171. package/examples/build-events/src/deploys/approvers.ts +55 -0
  172. package/examples/build-events/src/deploys/events.ts +32 -0
  173. package/examples/build-events/src/deploys/handlers.ts +38 -0
  174. package/examples/build-events/src/identity/Identity.ts +29 -0
  175. package/examples/build-events/src/identity/members.ts +7 -0
  176. package/examples/build-events/src/identity/verifier.ts +22 -0
  177. package/examples/build-events/src/notifier/Notices.ts +26 -0
  178. package/examples/build-events/src/notifier/routes.ts +71 -0
  179. package/examples/build-events/src/notifier.ts +48 -0
  180. package/examples/build-events/src/routes.ts +71 -0
  181. package/examples/build-events/src/server.ts +62 -0
  182. package/examples/build-events/test/builds/events.test.ts +68 -0
  183. package/examples/build-events/test/builds/log.test.ts +71 -0
  184. package/examples/build-events/test/builds/progress.test.ts +57 -0
  185. package/examples/build-events/test/builds/status.test.ts +42 -0
  186. package/examples/build-events/test/commands.test.ts +103 -0
  187. package/examples/build-events/test/deploys/events.test.ts +52 -0
  188. package/examples/build-events/test/http.test.ts +95 -0
  189. package/examples/build-events/test/notifier.test.ts +80 -0
  190. package/examples/build-events/test/routes.test.ts +96 -0
  191. package/examples/build-events/test/rpc.test.ts +84 -0
  192. package/examples/build-events/test/support/as.ts +8 -0
  193. package/examples/build-events/test/support/memory.ts +43 -0
  194. package/examples/build-events/test/webhooks.test.ts +143 -0
  195. package/examples/kitchen-sink/README.md +178 -166
  196. package/examples/kitchen-sink/src/Actions.ts +11 -3
  197. package/examples/kitchen-sink/src/Api.ts +12 -6
  198. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  199. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  200. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  201. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  202. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  203. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  204. package/examples/kitchen-sink/src/agent.ts +14 -6
  205. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  206. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  207. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  208. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  209. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  210. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  211. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  212. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  213. package/examples/kitchen-sink/src/browser.ts +10 -4
  214. package/examples/kitchen-sink/src/cli.ts +28 -12
  215. package/examples/kitchen-sink/src/config.ts +1 -1
  216. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  217. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  218. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  219. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  220. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  221. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  222. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  223. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  224. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  225. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  226. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  227. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  228. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  229. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  230. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  231. package/examples/kitchen-sink/src/memory.ts +11 -2
  232. package/examples/kitchen-sink/src/ops.ts +10 -5
  233. package/examples/kitchen-sink/src/request-policy.ts +21 -9
  234. package/examples/kitchen-sink/src/routes.ts +67 -27
  235. package/examples/kitchen-sink/src/sdk.ts +22 -12
  236. package/examples/kitchen-sink/src/seed.ts +15 -9
  237. package/examples/kitchen-sink/src/server.ts +24 -10
  238. package/examples/kitchen-sink/src/stdio.ts +9 -3
  239. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  240. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  241. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  242. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  243. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  244. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  245. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  246. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  247. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  248. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  249. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  250. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  251. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  252. package/examples/kitchen-sink/test/request-policy.test.ts +108 -47
  253. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  254. package/examples/kitchen-sink/test/support/as.ts +9 -0
  255. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  256. package/examples/kitchen-sink/test/support/services.ts +17 -0
  257. package/examples/live-rpc/README.md +33 -35
  258. package/examples/live-rpc/src/Rpc.ts +9 -5
  259. package/examples/live-rpc/src/browser.ts +39 -11
  260. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  261. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  262. package/examples/live-rpc/src/chat/events.ts +74 -0
  263. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  264. package/examples/live-rpc/src/chat/room.ts +9 -5
  265. package/examples/live-rpc/src/chat/store.ts +51 -14
  266. package/examples/live-rpc/src/client.ts +22 -5
  267. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  268. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  269. package/examples/live-rpc/src/page.ts +58 -12
  270. package/examples/live-rpc/src/routes.ts +14 -10
  271. package/examples/live-rpc/src/server.ts +15 -5
  272. package/examples/live-rpc/test/browser.test.ts +127 -50
  273. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  274. package/examples/live-rpc/test/routes.test.ts +130 -46
  275. package/examples/live-rpc/test/server.test.ts +94 -33
  276. package/examples/mcp-tools/README.md +24 -38
  277. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  278. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  279. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  280. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  281. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  282. package/examples/mcp-tools/src/routes.ts +8 -6
  283. package/examples/mcp-tools/src/server.ts +12 -5
  284. package/examples/mcp-tools/src/stdio.ts +3 -2
  285. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  286. package/examples/mcp-tools/test/routes.test.ts +140 -81
  287. package/examples/notes-service/README.md +30 -45
  288. package/examples/notes-service/src/Api.ts +1 -0
  289. package/examples/notes-service/src/browser.ts +15 -14
  290. package/examples/notes-service/src/cli.ts +8 -3
  291. package/examples/notes-service/src/commands.ts +46 -20
  292. package/examples/notes-service/src/identity/Identity.ts +2 -2
  293. package/examples/notes-service/src/identity/authorize.ts +6 -2
  294. package/examples/notes-service/src/identity/handlers.ts +3 -2
  295. package/examples/notes-service/src/identity/users.ts +2 -1
  296. package/examples/notes-service/src/identity/verifier.ts +12 -6
  297. package/examples/notes-service/src/notes/Notes.ts +46 -22
  298. package/examples/notes-service/src/notes/handlers.ts +18 -9
  299. package/examples/notes-service/src/notes/store.ts +70 -43
  300. package/examples/notes-service/src/routes.ts +7 -5
  301. package/examples/notes-service/src/server.ts +12 -5
  302. package/examples/notes-service/src/status/Status.ts +1 -1
  303. package/examples/notes-service/src/status/handlers.ts +5 -3
  304. package/examples/notes-service/src/stdio.ts +7 -3
  305. package/examples/notes-service/src/worker.ts +6 -3
  306. package/examples/notes-service/test/commands.test.ts +164 -91
  307. package/examples/notes-service/test/notes/as.ts +3 -1
  308. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  309. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  310. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  311. package/examples/notes-service/test/routes.test.ts +106 -71
  312. package/examples/notes-service/test/worker.test.ts +21 -13
  313. package/examples/quickstart/README.md +2 -6
  314. package/examples/quickstart/src/Api.ts +1 -0
  315. package/examples/quickstart/src/client.ts +6 -3
  316. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  317. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  318. package/examples/quickstart/src/routes.ts +3 -2
  319. package/examples/quickstart/src/server.ts +6 -2
  320. package/examples/quickstart/test/routes.test.ts +3 -2
  321. package/package.json +10 -4
@@ -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,26 +1,37 @@
1
- import { afterAll, describe, expect, it } from "@effect/vitest";
2
1
  import { NodeHttpServer } from "@effect/platform-node";
3
- import { ConfigProvider, Effect, Layer } from "effect";
4
- import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/http";
2
+ import { afterAll, describe, expect, it } from "@effect/vitest";
5
3
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
6
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
+
7
13
  import { Http } from "../src/Api.js";
8
14
  import { memory } from "../src/memory.js";
9
15
  import { adminRoutes, publicRoutes } from "../src/routes.js";
16
+ import { services } from "./support/services.js";
10
17
 
11
18
  // Each port as a web handler, `Request` in and `Response` out: what a platform without
12
19
  // Effect's server runs, and what a test sends raw requests to. A handler builds its routes
13
20
  // once; the test owns it, and disposes it when done.
14
21
  const web = HttpRouter.toWebHandler(
15
- publicRoutes.pipe(Layer.provide(HttpServer.layerServices), Layer.provide(memory)),
22
+ publicRoutes.pipe(
23
+ Layer.provide(HttpServer.layerServices),
24
+ Layer.provide(services())
25
+ )
16
26
  );
17
27
 
18
28
  const adminWeb = HttpRouter.toWebHandler(
19
- adminRoutes.pipe(Layer.provide(HttpServer.layerServices), Layer.provide(memory)),
29
+ adminRoutes.pipe(
30
+ Layer.provide(HttpServer.layerServices),
31
+ Layer.provide(memory)
32
+ )
20
33
  );
21
34
 
22
- afterAll(() => Promise.all([web.dispose(), adminWeb.dispose()]));
23
-
24
35
  const post = (url: string, headers: Record<string, string> = {}) =>
25
36
  new Request(url, {
26
37
  method: "POST",
@@ -28,32 +39,49 @@ const post = (url: string, headers: Record<string, string> = {}) =>
28
39
  body: "{}",
29
40
  });
30
41
 
42
+ const read = async (url: string, headers: Record<string, string> = {}) =>
43
+ await web.handler(new Request(url, { headers }));
44
+
31
45
  const discovery = "/.well-known/oauth-protected-resource/mcp";
32
46
 
33
47
  describe("the request policy", () => {
48
+ afterAll(async () => {
49
+ await Promise.all([web.dispose(), adminWeb.dispose()]);
50
+ });
51
+
34
52
  it("refuses a foreign Host or Origin", async () => {
35
- 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
+ );
36
56
 
37
57
  const foreignOrigin = await web.handler(
38
- post("http://localhost/api/v1/getStatus", { origin: "http://evil.example" }),
58
+ post("http://localhost/api/v1/getStatus", {
59
+ origin: "http://evil.example",
60
+ })
39
61
  );
40
62
 
41
- expect([foreignHost.status, await foreignHost.text()]).toEqual([403, "Host not allowed"]);
42
- 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
+ ]);
43
71
  });
44
72
 
45
73
  it("runs before authentication: a foreign Host is refused, not asked to sign in", async () => {
46
74
  const signedOut = await web.handler(post("http://localhost/api/v1/whoAmI"));
47
- const foreignHost = await web.handler(post("http://tracker.evil.example/api/v1/whoAmI"));
48
75
 
49
- 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]);
50
81
  expect(foreignHost.headers.get("www-authenticate")).toBeNull();
51
82
  });
52
83
 
53
84
  it("runs before the discovery the Bearer provider publishes", async () => {
54
- const read = (url: string, headers: Record<string, string> = {}) =>
55
- web.handler(new Request(url, { headers }));
56
-
57
85
  const here = await read(`http://localhost${discovery}`);
58
86
  const foreignHost = await read(`http://tracker.evil.example${discovery}`);
59
87
 
@@ -61,12 +89,18 @@ describe("the request policy", () => {
61
89
  origin: "http://evil.test",
62
90
  });
63
91
 
64
- 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]);
65
97
  });
66
98
 
67
99
  it("answers the browser MCP client's origin, and its CORS preflight", async () => {
68
100
  const allowed = await web.handler(
69
- post("http://localhost/api/v1/getStatus", { origin: "http://localhost:6274" }),
101
+ post("http://localhost/api/v1/getStatus", {
102
+ origin: "http://localhost:6274",
103
+ })
70
104
  );
71
105
 
72
106
  const preflight = await web.handler(
@@ -75,16 +109,23 @@ describe("the request policy", () => {
75
109
  headers: {
76
110
  origin: "http://localhost:6274",
77
111
  "access-control-request-method": "POST",
78
- "access-control-request-headers": "authorization, mcp-protocol-version",
112
+ "access-control-request-headers":
113
+ "authorization, mcp-protocol-version",
79
114
  },
80
- }),
115
+ })
81
116
  );
82
117
 
83
118
  expect(allowed.status).toBe(200);
84
- 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
+ );
85
122
  expect(preflight.status).toBe(204);
86
- expect(preflight.headers.get("access-control-allow-headers")).toContain("MCP-Protocol-Version");
87
- 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
+ );
88
129
  });
89
130
 
90
131
  it("lets a browser client of an MCP revision with sessions end its session", async () => {
@@ -96,23 +137,30 @@ describe("the request policy", () => {
96
137
  "access-control-request-method": "DELETE",
97
138
  "access-control-request-headers": "authorization, mcp-session-id",
98
139
  },
99
- }),
140
+ })
100
141
  );
101
142
 
102
143
  expect(preflight.status).toBe(204);
103
- expect(preflight.headers.get("access-control-allow-methods")).toContain("DELETE");
104
- 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
+ );
105
150
  });
106
151
 
107
152
  it("covers the admin port too, a valid key included", async () => {
108
153
  const key = { "x-operator-key": "ops-key" };
109
- 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
+ );
110
158
 
111
159
  const foreignHost = await adminWeb.handler(
112
- post("http://tracker.evil.example/admin/listWorkspaces", key),
160
+ post("http://tracker.evil.example/admin/listWorkspaces", key)
113
161
  );
114
162
 
115
- expect([here.status, foreignHost.status]).toEqual([200, 403]);
163
+ expect([here.status, foreignHost.status]).toStrictEqual([200, 403]);
116
164
  });
117
165
 
118
166
  // Behind a proxy ending TLS, Node's server sees `http:`: the public URL's origin is allowed by
@@ -125,37 +173,50 @@ describe("the request policy", () => {
125
173
  client
126
174
  .execute(
127
175
  HttpClientRequest.post("/api/v1/getStatus").pipe(
128
- HttpClientRequest.setHeaders({ origin, "x-forwarded-proto": "https" }),
129
- HttpClientRequest.bodyJsonUnsafe({}),
130
- ),
176
+ HttpClientRequest.setHeaders({
177
+ origin,
178
+ "x-forwarded-proto": "https",
179
+ }),
180
+ HttpClientRequest.bodyJsonUnsafe({})
181
+ )
131
182
  )
132
183
  .pipe(Effect.map((response) => response.status));
133
184
 
134
185
  expect([
135
186
  yield* status("https://tracker.example"),
136
187
  yield* status("https://evil.example"),
137
- ]).toEqual([200, 403]);
188
+ ]).toStrictEqual([200, 403]);
138
189
  }).pipe(
139
190
  Effect.provide(
140
- HttpRouter.serve(publicRoutes, { disableLogger: true, disableListenLog: true }).pipe(
141
- Layer.provide(memory),
191
+ HttpRouter.serve(publicRoutes, {
192
+ disableLogger: true,
193
+ disableListenLog: true,
194
+ }).pipe(
195
+ Layer.provide(services()),
142
196
  Layer.provideMerge(NodeHttpServer.layerTest),
143
197
  Layer.provide(
144
198
  ConfigProvider.layer(
145
- ConfigProvider.fromEnvRecord({ PUBLIC_URL: "https://tracker.example" }),
146
- ),
147
- ),
148
- ),
149
- ),
150
- ),
199
+ ConfigProvider.fromEnvRecord({
200
+ PUBLIC_URL: "https://tracker.example",
201
+ })
202
+ )
203
+ )
204
+ )
205
+ )
206
+ )
151
207
  );
152
208
 
153
209
  // `Testing.layer(handler)` gives the typed clients the same handler the raw requests hit.
154
- it.effect("leaves the typed client, served from the same web handler, alone", () =>
155
- Effect.gen(function* () {
156
- const client = yield* ActionHttp.client(Http);
157
-
158
- expect(yield* client.getStatus()).toMatchObject({ service: "tracker", workspaces: 2 });
159
- }).pipe(Effect.provide(Testing.layer(web.handler))),
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)))
160
221
  );
161
222
  });