@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,45 +1,65 @@
1
- import { Effect, Layer } from "effect";
2
- import { HttpRouter, HttpServerResponse } from "effect/http";
3
- import { HttpApiSwagger, OpenApi } from "effect/http-api";
4
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
5
2
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
6
3
  import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
7
4
  import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
5
+ import * as Webhook from "@gjermundgaraba/effect-actions/Webhook";
6
+ import { Effect, Layer } from "effect";
7
+ import { HttpRouter, HttpServerResponse } from "effect/http";
8
+ import { HttpApiSwagger, OpenApi } from "effect/http-api";
9
+
8
10
  import { MemberActions, Tools } from "./Actions.js";
9
11
  import { authenticateOperator } from "./admin/authenticate.js";
10
- import { assistant } from "./assistant/handlers.js";
11
12
  import { admin } from "./admin/handlers.js";
12
13
  import { AdminHttp, Http } from "./Api.js";
14
+ import { assistant } from "./assistant/handlers.js";
13
15
  import { download } from "./attachments/download.js";
14
16
  import { attachments } from "./attachments/handlers.js";
15
17
  import { allowedOrigins } from "./config.js";
16
- import { authenticateBearer, authenticateSession } from "./identity/authenticate.js";
18
+ import {
19
+ authenticateBearer,
20
+ authenticateSession,
21
+ } from "./identity/authenticate.js";
17
22
  import { identity } from "./identity/handlers.js";
18
23
  import { Login } from "./identity/Identity.js";
19
24
  import { HttpQuota, quota, RpcQuota } from "./identity/quota.js";
20
25
  import { layer as assistantTools } from "./issues/assistant.js";
26
+ import { issueEvents } from "./issues/events.js";
27
+ import { exports } from "./issues/export.js";
21
28
  import { features } from "./issues/guide.js";
22
29
  import { issues } from "./issues/handlers.js";
30
+ import { IssueChanged } from "./issues/Issues.js";
23
31
  import { cors, requestPolicy } from "./request-policy.js";
24
32
  import { Rpc } from "./Rpc.js";
25
33
  import { status } from "./workspaces/handlers.js";
26
34
  import { resolveTenant } from "./workspaces/tenant.js";
27
35
  import { GetStatus } from "./workspaces/Workspaces.js";
28
36
 
29
- // Every domain's implementation of the public port's actions, once. Each surface below takes
30
- // this one list and serves the actions its binding or its `actions` names.
31
- const implementations = [status, identity, issues, attachments, assistant];
37
+ // Every domain's implementation of the public port's actions and events, once. Each surface
38
+ // below takes this one list and serves what its binding or its `actions` names.
39
+ const implementations = [
40
+ status,
41
+ identity,
42
+ issues,
43
+ issueEvents,
44
+ exports,
45
+ attachments,
46
+ assistant,
47
+ ];
32
48
 
33
- // JSON routes at `/api/v1/<action>`, in two layers of one binding, each naming its `actions`.
34
- // The quota requires the caller, and a public route has none, so `getStatus` is served by a
35
- // layer without it.
49
+ // JSON routes at `/api/v1/<action>`, in two layers of one binding, each naming its `actions`,
50
+ // an event among them: a layer given `actions` serves exactly what it names. The quota requires
51
+ // the caller, and a public route has none, so `getStatus` is served by a layer without it.
36
52
  const api = Layer.mergeAll(
37
53
  ActionHttp.layer(Http, implementations, { actions: [GetStatus] }),
38
- ActionHttp.layer(Http, implementations, { actions: MemberActions, middleware: [HttpQuota] }),
54
+ ActionHttp.layer(Http, implementations, {
55
+ actions: [...MemberActions, IssueChanged],
56
+ middleware: [HttpQuota],
57
+ })
39
58
  );
40
59
 
41
60
  // The same actions as MCP tools at `/mcp`: a mixed endpoint, serving the public `getStatus`
42
- // beside the members' tools.
61
+ // beside the members' tools, and `issue.changed` as an MCP event, polled, streamed or
62
+ // delivered by webhook.
43
63
  const mcp = Layer.unwrap(
44
64
  Effect.map(allowedOrigins, (origins) =>
45
65
  ActionMcp.layerHttp(implementations, {
@@ -47,24 +67,24 @@ const mcp = Layer.unwrap(
47
67
  version: "1.0.0",
48
68
  instructions: "Read an issue before commenting on it or closing it.",
49
69
  authentication: Login,
50
- actions: Tools,
70
+ actions: [...Tools, IssueChanged],
51
71
  features,
52
72
  // The endpoint's own Origin allowlist, narrower than the request policy, which has
53
73
  // already refused a foreign Origin: that policy also lets through a page of any name
54
74
  // this host answers to, such as http://127.0.0.1:3000. Not listed here, such a page gets
55
75
  // a 403 from the endpoint.
56
76
  allowedOrigins: origins,
57
- }),
58
- ),
77
+ })
78
+ )
59
79
  );
60
80
 
61
81
  // The members' actions as Effect RPC, for the web app, behind its session cookie and the same
62
82
  // quota as native RPC middleware. It still requires its protocol and serialization, chosen by
63
83
  // whoever serves it: server.ts gives it a WebSocket, and a test Effect's HTTP protocol, which
64
84
  // answers in memory.
65
- export const rpc = ActionRpc.layer(Rpc, implementations, { middleware: [RpcQuota] }).pipe(
66
- Layer.provide([authenticateSession, quota, assistantTools]),
67
- );
85
+ export const rpc = ActionRpc.layer(Rpc, implementations, {
86
+ middleware: [RpcQuota],
87
+ }).pipe(Layer.provide([authenticateSession, quota, assistantTools]));
68
88
 
69
89
  // Effect's own tools read the binding's native `HttpApi`: the OpenAPI document and a Swagger
70
90
  // UI, each showing the bearer scheme on every route but `getStatus`.
@@ -72,9 +92,9 @@ const documentation = Layer.mergeAll(
72
92
  HttpRouter.add(
73
93
  "GET",
74
94
  `${Http.prefix}/openapi.json`,
75
- HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api)),
95
+ HttpServerResponse.jsonUnsafe(OpenApi.fromApi(Http.api))
76
96
  ),
77
- HttpApiSwagger.layer(Http.api, { path: "/docs" }),
97
+ HttpApiSwagger.layer(Http.api, { path: "/docs" })
78
98
  );
79
99
 
80
100
  const health = HttpRouter.add("GET", "/healthz", HttpServerResponse.text("ok"));
@@ -94,12 +114,25 @@ const health = HttpRouter.add("GET", "/healthz", HttpServerResponse.text("ok"));
94
114
  // The assistant's builder yields its tools, so their handlers are provided to the layers
95
115
  // serving it, here and on `rpc`: one Layer, built once in the layer graph serving both, where
96
116
  // the tools run the issue implementation the routes serve, with no second build of it.
97
- export const publicRoutes = Layer.mergeAll(api, mcp, download, documentation, health).pipe(
117
+ //
118
+ // Webhook delivery serves MCP's `events/subscribe` and HTTP's
119
+ // `POST /api/v1/events/issue.changed/subscribe`: a member names a callback URL and a secret,
120
+ // and each change it may see is POSTed there, signed. One layer holds the subscriptions of
121
+ // both. It POSTs with an `HttpClient` and checks where each callback resolves with `Dns`, which
122
+ // server.ts provides.
123
+ export const publicRoutes = Layer.mergeAll(
124
+ api,
125
+ mcp,
126
+ download,
127
+ documentation,
128
+ health
129
+ ).pipe(
130
+ Layer.provide(Webhook.layer()),
98
131
  Layer.provide(assistantTools),
99
132
  Layer.provide([authenticateBearer, authenticateSession, quota]),
100
133
  Layer.provide(resolveTenant.layer),
101
134
  Layer.provide(cors),
102
- Layer.provide(requestPolicy),
135
+ Layer.provide(requestPolicy)
103
136
  );
104
137
 
105
138
  // The admin port: what operators reach, and nothing else. It is a router of its own, with its
@@ -107,12 +140,19 @@ export const publicRoutes = Layer.mergeAll(api, mcp, download, documentation, he
107
140
  // serves one implementation the public port serves too, `status`.
108
141
  export const adminRoutes = Layer.mergeAll(
109
142
  ActionHttp.layer(AdminHttp, [status, admin]),
110
- health,
143
+ health
111
144
  ).pipe(Layer.provide(authenticateOperator), Layer.provide(requestPolicy));
112
145
 
113
146
  // Every builder as one layer. The two ports are two layer graphs, and each would run the
114
147
  // builder of an implementation it serves: `status`, which both serve, twice, each port then
115
148
  // reporting an `instance` of its own. Provided above both, with the services, each builder
116
- // runs once for every surface of both ports. The assistant's is left out: the public port
117
- // alone serves it, and builds it once for its routes and its rpcs.
118
- export const builders = Action.layer([status, identity, issues, attachments, admin]);
149
+ // runs once for every surface of both ports. The assistant's, the export's and the event's are
150
+ // left out: the public port alone serves them, and builds each once for its routes and its
151
+ // rpcs.
152
+ export const builders = Action.layer([
153
+ status,
154
+ identity,
155
+ issues,
156
+ attachments,
157
+ admin,
158
+ ]);
@@ -1,6 +1,7 @@
1
- import { Effect, flow } from "effect";
2
- import { HttpClient, HttpClientRequest } from "effect/http";
3
1
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
2
+ import { Effect, flow, Stream } from "effect";
3
+ import { HttpClient, HttpClientRequest } from "effect/http";
4
+
4
5
  import { Http } from "./Api.js";
5
6
  import type { Issue } from "./issues/Issues.js";
6
7
 
@@ -19,28 +20,37 @@ export const connect = (options: {
19
20
  transformClient: HttpClient.mapRequest(
20
21
  flow(
21
22
  HttpClientRequest.bearerToken(options.token),
22
- HttpClientRequest.setHeader("x-workspace", options.workspace),
23
- ),
23
+ HttpClientRequest.setHeader("x-workspace", options.workspace)
24
+ )
24
25
  ),
25
26
  });
26
27
 
27
28
  return {
28
29
  // A method takes the action's decoded input, so a field with a decoding default is given
29
30
  // here; only a call without any argument sends `{}` and gets the defaults.
30
- openIssues: (limit = 20) =>
31
- Effect.runPromise(
32
- Effect.map(api.listIssues({ status: "open", limit }), ({ issues }) => issues),
31
+ openIssues: async (limit = 20) =>
32
+ await Effect.runPromise(
33
+ Effect.map(
34
+ api.listIssues({ status: "open", limit }),
35
+ ({ issues }) => issues
36
+ )
33
37
  ),
34
38
 
35
39
  // A declared error is a typed failure, handled by its tag: a missing issue is `undefined`.
36
- issue: (id: string): Promise<Issue | undefined> =>
37
- api.getIssue({ id }).pipe(
38
- Effect.catchTag("IssueNotFound", () => Effect.succeed(undefined)),
39
- Effect.runPromise,
40
+ issue: async (id: string): Promise<Issue | undefined> =>
41
+ await api.getIssue({ id }).pipe(
42
+ Effect.catchTag("IssueNotFound", () => Effect.undefined),
43
+ Effect.runPromise
40
44
  ),
41
45
 
42
46
  // Left unhandled, a declared error rejects the promise as its decoded value, so a caller's
43
47
  // `catch` may test `error instanceof RateLimited`.
44
- file: (title: string, body: string) => Effect.runPromise(api.createIssue({ title, body })),
48
+ file: async (title: string, body: string) =>
49
+ await Effect.runPromise(api.createIssue({ title, body })),
50
+
51
+ // A stream action's method returns a `Stream`; as an async iterable, `for await` reads each
52
+ // issue as the server sends it.
53
+ exportAll: (): AsyncIterable<Issue> =>
54
+ Stream.toAsyncIterable(api.exportIssues({})),
45
55
  };
46
56
  };
@@ -1,8 +1,10 @@
1
1
  import { Buffer } from "node:buffer";
2
+
2
3
  import { NodeRuntime } from "@effect/platform-node";
4
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
3
5
  import { Config, Console, Effect, flow } from "effect";
4
6
  import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
5
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
7
+
6
8
  import { Http } from "./Api.js";
7
9
 
8
10
  // Entrypoint: `node --import tsx examples/kitchen-sink/src/seed.ts`, with the server running.
@@ -13,12 +15,12 @@ import { Http } from "./Api.js";
13
15
  // A one-pixel PNG stands in for a screenshot.
14
16
  const screenshot = Buffer.from(
15
17
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
16
- "base64",
18
+ "base64"
17
19
  );
18
20
 
19
21
  const seed = Effect.gen(function* () {
20
22
  const baseUrl = yield* Config.String("TRACKER_URL").pipe(
21
- Config.withDefault("http://127.0.0.1:3000"),
23
+ Config.withDefault("http://127.0.0.1:3000")
22
24
  );
23
25
 
24
26
  const tracker = yield* ActionHttp.client(Http, {
@@ -27,8 +29,8 @@ const seed = Effect.gen(function* () {
27
29
  transformClient: HttpClient.mapRequest(
28
30
  flow(
29
31
  HttpClientRequest.bearerToken("ada"),
30
- HttpClientRequest.setHeader("x-workspace", "acme"),
31
- ),
32
+ HttpClientRequest.setHeader("x-workspace", "acme")
33
+ )
32
34
  ),
33
35
  });
34
36
 
@@ -48,7 +50,10 @@ const seed = Effect.gen(function* () {
48
50
 
49
51
  const { issues } = yield* tracker.listIssues();
50
52
 
51
- yield* Console.log(`Filed ${issue.id} with attachment ${attachment.id}. Issues:`, issues);
53
+ yield* Console.log(
54
+ `Filed ${issue.id} with attachment ${attachment.id}. Issues:`,
55
+ issues
56
+ );
52
57
  }).pipe(
53
58
  // A declared error is a typed failure, the binding's and the verifier's included: each is
54
59
  // handled by its tag. Anything else, such as an unreachable server, stays Effect's own.
@@ -57,8 +62,9 @@ const seed = Effect.gen(function* () {
57
62
  Console.error(`Write limit reached: retry in ${retryAfterSeconds}s.`),
58
63
  QuotaExceeded: ({ retryAfterSeconds }) =>
59
64
  Console.error(`Request quota spent: retry in ${retryAfterSeconds}s.`),
60
- IssuerUnavailable: () => Console.error("The sign-in service is down: try again later."),
61
- }),
65
+ IssuerUnavailable: () =>
66
+ Console.error("The sign-in service is down: try again later."),
67
+ })
62
68
  );
63
69
 
64
- seed.pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain);
70
+ seed.pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain());
@@ -1,8 +1,10 @@
1
1
  import { createServer } from "node:http";
2
- import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
2
+
3
+ import { NodeDns, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
3
4
  import { ByteSize, Config, Effect, Layer } from "effect";
4
- import { HttpRouter, HttpServerRequest } from "effect/http";
5
+ import { FetchHttpClient, HttpRouter, HttpServerRequest } from "effect/http";
5
6
  import { RpcSerialization, RpcServer } from "effect/rpc";
7
+
6
8
  import { memory } from "./memory.js";
7
9
  import { adminRoutes, builders, publicRoutes, rpc } from "./routes.js";
8
10
 
@@ -14,15 +16,23 @@ import { adminRoutes, builders, publicRoutes, rpc } from "./routes.js";
14
16
  // serves the same layer over Effect's HTTP protocol.
15
17
  const socket = rpc.pipe(
16
18
  Layer.provide(RpcServer.layerProtocolWebsocket({ path: "/rpc" })),
17
- Layer.provide(RpcSerialization.layerJson),
19
+ Layer.provide(RpcSerialization.layerJson)
18
20
  );
19
21
 
20
22
  const server = Layer.unwrap(
21
23
  Effect.gen(function* () {
22
24
  // The loopback default serves this machine alone; HOST=0.0.0.0 lets others reach it.
23
- const host = yield* Config.String("HOST").pipe(Config.withDefault("127.0.0.1"));
24
- const publicPort = yield* Config.Port("PORT").pipe(Config.withDefault(3000));
25
- const adminPort = yield* Config.Port("ADMIN_PORT").pipe(Config.withDefault(3001));
25
+ const host = yield* Config.String("HOST").pipe(
26
+ Config.withDefault("127.0.0.1")
27
+ );
28
+
29
+ const publicPort = yield* Config.Port("PORT").pipe(
30
+ Config.withDefault(3000)
31
+ );
32
+
33
+ const adminPort = yield* Config.Port("ADMIN_PORT").pipe(
34
+ Config.withDefault(3001)
35
+ );
26
36
 
27
37
  const limit = ByteSize.mebibytes(1);
28
38
 
@@ -42,8 +52,10 @@ const server = Layer.unwrap(
42
52
  // it is a route of that router: the request policy, global to the router, refuses a
43
53
  // foreign Origin's upgrade as it does a request, and the rpcs spend the quota the routes
44
54
  // spend, built once.
45
- HttpRouter.serve(Layer.merge(publicRoutes, socket)).pipe(Layer.provide(listen(publicPort))),
46
- HttpRouter.serve(adminRoutes).pipe(Layer.provide(listen(adminPort))),
55
+ HttpRouter.serve(Layer.merge(publicRoutes, socket)).pipe(
56
+ Layer.provide(listen(publicPort))
57
+ ),
58
+ HttpRouter.serve(adminRoutes).pipe(Layer.provide(listen(adminPort)))
47
59
  ).pipe(
48
60
  // Request bodies have no size limit unless the host sets one. This covers every route
49
61
  // that reads its body whole, on both ports, the MCP endpoint included.
@@ -53,9 +65,11 @@ const server = Layer.unwrap(
53
65
  // suspends on one port is refused on the other.
54
66
  Layer.provide(builders),
55
67
  Layer.provide(memory),
68
+ // Webhook delivery POSTs with fetch, and resolves each callback with the system's resolver.
69
+ Layer.provide([FetchHttpClient.layer, NodeDns.layer])
56
70
  );
57
- }),
71
+ })
58
72
  );
59
73
 
60
74
  // NodeRuntime handles signals: the scoped layers stop both listeners and release the services.
61
- Layer.launch(server).pipe(NodeRuntime.runMain);
75
+ Layer.launch(server).pipe(NodeRuntime.runMain());
@@ -1,7 +1,8 @@
1
1
  import { NodeRuntime, NodeStdio } from "@effect/platform-node";
2
- import { Effect } from "effect";
3
2
  import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
4
3
  import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
4
+ import { Effect } from "effect";
5
+
5
6
  import { Tools } from "./Actions.js";
6
7
  import { attachments } from "./attachments/handlers.js";
7
8
  import { identity } from "./identity/handlers.js";
@@ -29,8 +30,13 @@ ActionMcp.runStdio([status, identity, issues, attachments], {
29
30
  // The host names the caller itself, around every call. DEMO ONLY: a real host reads who
30
31
  // started it, from its own login or configuration.
31
32
  Effect.provideService(CurrentMember, members.ada),
32
- Effect.provide([NodeStdio.layer, stores, limiter, WorkspaceRegistry.layerMemory]),
33
+ Effect.provide([
34
+ NodeStdio.layer,
35
+ stores,
36
+ limiter,
37
+ WorkspaceRegistry.layerMemory,
38
+ ]),
33
39
  // stdout carries the protocol alone, so logs and failures go to stderr.
34
40
  ActionCli.logToStderr,
35
- NodeRuntime.runMain,
41
+ NodeRuntime.runMain()
36
42
  );
@@ -1,5 +1,5 @@
1
- import { Schema } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Schema } from "effect";
3
3
 
4
4
  export const Workspace = Schema.Struct({
5
5
  slug: Schema.String,
@@ -9,11 +9,10 @@ export const Workspace = Schema.Struct({
9
9
 
10
10
  export type Workspace = typeof Workspace.Type;
11
11
 
12
- export class WorkspaceNotFound extends Schema.TaggedError<WorkspaceNotFound>()(
13
- "WorkspaceNotFound",
14
- { slug: Schema.String },
15
- { httpApiStatus: 404 },
16
- ) {}
12
+ export class WorkspaceNotFound extends Schema.TaggedError<
13
+ WorkspaceNotFound,
14
+ { readonly brand: unique symbol }
15
+ >()("WorkspaceNotFound", { slug: Schema.String }, { httpApiStatus: 404 }) {}
17
16
 
18
17
  // Public: `caller: Action.Anyone`. It answers before anyone has signed in, on every surface,
19
18
  // so it tells nothing that depends on the caller.
@@ -22,7 +21,11 @@ export const GetStatus = Action.make("getStatus", {
22
21
  "Report whether the tracker is up, since which start, and how many workspaces it serves.",
23
22
  // `instance` names one start of the process: it changes on a restart, and with it the demo's
24
23
  // memory stores are empty again.
25
- success: { service: Schema.String, instance: Schema.String, workspaces: Schema.Finite },
24
+ success: {
25
+ service: Schema.String,
26
+ instance: Schema.String,
27
+ workspaces: Schema.Finite,
28
+ },
26
29
  readOnly: true,
27
30
  caller: Action.Anyone,
28
31
  });
@@ -1,5 +1,6 @@
1
- import { Effect } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Effect } from "effect";
3
+
3
4
  import { WorkspaceRegistry } from "./registry.js";
4
5
  import { GetStatus } from "./Workspaces.js";
5
6
 
@@ -19,5 +20,5 @@ export const status = Action.implement(
19
20
  instance,
20
21
  workspaces: workspaces.length,
21
22
  }));
22
- }),
23
+ })
23
24
  );
@@ -1,5 +1,7 @@
1
1
  import { Context, Effect, Layer } from "effect";
2
- import { type Workspace, WorkspaceNotFound } from "./Workspaces.js";
2
+
3
+ import { WorkspaceNotFound } from "./Workspaces.js";
4
+ import type { Workspace } from "./Workspaces.js";
3
5
 
4
6
  /**
5
7
  * The tenants. Operators list and suspend them on the admin port; authentication reads the
@@ -8,11 +10,11 @@ import { type Workspace, WorkspaceNotFound } from "./Workspaces.js";
8
10
  export class WorkspaceRegistry extends Context.Service<
9
11
  WorkspaceRegistry,
10
12
  {
11
- readonly list: Effect.Effect<ReadonlyArray<Workspace>>;
13
+ readonly list: Effect.Effect<readonly Workspace[]>;
12
14
  readonly get: (slug: string) => Effect.Effect<Workspace, WorkspaceNotFound>;
13
15
  readonly setSuspended: (
14
16
  slug: string,
15
- suspended: boolean,
17
+ suspended: boolean
16
18
  ) => Effect.Effect<Workspace, WorkspaceNotFound>;
17
19
  }
18
20
  >()("tracker/WorkspaceRegistry") {
@@ -26,21 +28,25 @@ export class WorkspaceRegistry extends Context.Service<
26
28
  const get = Effect.fn("WorkspaceRegistry.get")(function* (slug: string) {
27
29
  const workspace = workspaces.get(slug);
28
30
 
29
- if (workspace === undefined) return yield* new WorkspaceNotFound({ slug });
31
+ if (workspace === undefined) {
32
+ return yield* new WorkspaceNotFound({ slug });
33
+ }
30
34
 
31
35
  return workspace;
32
36
  });
33
37
 
34
38
  return WorkspaceRegistry.of({
35
- list: Effect.sync(() => Array.from(workspaces.values())),
39
+ list: Effect.sync(() => [...workspaces.values()]),
36
40
  get,
37
- setSuspended: Effect.fn("WorkspaceRegistry.setSuspended")(function* (slug, suspended) {
38
- const workspace = { ...(yield* get(slug)), suspended };
41
+ setSuspended: Effect.fn("WorkspaceRegistry.setSuspended")(
42
+ function* (slug, suspended) {
43
+ const workspace = { ...(yield* get(slug)), suspended };
39
44
 
40
- workspaces.set(slug, workspace);
45
+ workspaces.set(slug, workspace);
41
46
 
42
- return workspace;
43
- }),
47
+ return workspace;
48
+ }
49
+ ),
44
50
  });
45
51
  });
46
52
  }
@@ -6,14 +6,21 @@ import { HttpRouter, HttpServerRequest } from "effect/http";
6
6
  * an identity: the verifier reads it to decide which of an account's memberships the caller
7
7
  * acts in. Handlers never read it; they read the workspace of the authenticated member.
8
8
  */
9
- export class Tenant extends Context.Service<Tenant, Option.Option<string>>()("tracker/Tenant") {}
9
+ export class Tenant extends Context.Service<Tenant, Option.Option<string>>()(
10
+ "tracker/Tenant"
11
+ ) {}
10
12
 
11
13
  // Native router middleware. It provides `Tenant` to whatever it is provided to, per request:
12
14
  // to the verifier too, when it is provided after the authentication, in a `Layer.provide` of
13
15
  // its own: in one array with the authentication it would leave `Tenant` owed, since an array's
14
16
  // layers provide to the routes, not to one another.
15
- export const resolveTenant = HttpRouter.middleware<{ provides: Tenant }>()((route) =>
16
- Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) =>
17
- Effect.provideService(route, Tenant, Option.fromNullishOr(request.headers["x-workspace"])),
18
- ),
17
+ export const resolveTenant = HttpRouter.middleware<{ provides: Tenant }>()(
18
+ (route) =>
19
+ Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) =>
20
+ Effect.provideService(
21
+ route,
22
+ Tenant,
23
+ Option.fromNullishOr(request.headers["x-workspace"])
24
+ )
25
+ )
19
26
  );
@@ -1,9 +1,10 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
- import { Effect, Layer } from "effect";
3
- import { HttpClient, HttpClientRequest } from "effect/http";
4
2
  import * as Action from "@gjermundgaraba/effect-actions/Action";
5
3
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
6
4
  import * as Testing from "@gjermundgaraba/effect-actions/Testing";
5
+ import { Effect, Layer } from "effect";
6
+ import { HttpClient, HttpClientRequest } from "effect/http";
7
+
7
8
  import { Operator } from "../../src/admin/Admin.js";
8
9
  import { Grants } from "../../src/admin/grants.js";
9
10
  import { admin } from "../../src/admin/handlers.js";
@@ -16,7 +17,7 @@ const served = Testing.layer(adminRoutes).pipe(Layer.provide(memory));
16
17
 
17
18
  const key = (operatorKey: string) => ({
18
19
  transformClient: HttpClient.mapRequest(
19
- HttpClientRequest.setHeader("x-operator-key", operatorKey),
20
+ HttpClientRequest.setHeader("x-operator-key", operatorKey)
20
21
  ),
21
22
  });
22
23
 
@@ -28,39 +29,53 @@ describe("the operators", () => {
28
29
 
29
30
  expect((yield* support.listWorkspaces()).workspaces).toHaveLength(2);
30
31
 
31
- const refused = yield* Effect.flip(support.suspendWorkspace({ workspace: "acme" }));
32
+ const refused = yield* Effect.flip(
33
+ support.suspendWorkspace({ workspace: "acme" })
34
+ );
32
35
 
33
36
  expect(refused).toBeInstanceOf(Action.Forbidden);
34
37
  expect(refused).not.toHaveProperty("scopes");
35
38
  expect(refused.message).toBe("Requires the workspaces:suspend grant.");
36
- expect(yield* ops.suspendWorkspace({ workspace: "acme" })).toMatchObject({ suspended: true });
37
- }).pipe(Effect.provide(served)),
39
+ expect(yield* ops.suspendWorkspace({ workspace: "acme" })).toMatchObject({
40
+ suspended: true,
41
+ });
42
+ }).pipe(Effect.provide(served))
38
43
  );
39
44
 
40
45
  it.effect("are not the members: a member's token is no credential here", () =>
41
46
  Effect.gen(function* () {
42
47
  const member = yield* ActionHttp.client(AdminHttp, {
43
- transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken("ada")),
48
+ transformClient: HttpClient.mapRequest(
49
+ HttpClientRequest.bearerToken("ada")
50
+ ),
44
51
  });
45
52
 
46
- expect(yield* Effect.flip(member.listWorkspaces())).toBeInstanceOf(Action.Unauthenticated);
47
- }).pipe(Effect.provide(served)),
53
+ expect(yield* Effect.flip(member.listWorkspaces())).toBeInstanceOf(
54
+ Action.Unauthenticated
55
+ );
56
+ }).pipe(Effect.provide(served))
48
57
  );
49
58
 
50
- it.effect("include the console's trusted operator, admitted by the same rule in process", () =>
51
- Effect.gen(function* () {
52
- const client = yield* Action.client(admin);
53
- const suspend = client.suspendWorkspace({ workspace: "globex" });
59
+ it.effect(
60
+ "include the console's trusted operator, admitted by the same rule in process",
61
+ () =>
62
+ Effect.gen(function* () {
63
+ const client = yield* Action.client(admin);
64
+ const suspend = client.suspendWorkspace({ workspace: "globex" });
54
65
 
55
- // No grants are stored for either: only the operator a host supplies is trusted.
56
- expect(
57
- yield* suspend.pipe(Effect.provideService(Operator, { id: "console", trusted: true })),
58
- ).toMatchObject({ suspended: true });
59
- expect(
60
- yield* Effect.flip(
61
- suspend.pipe(Effect.provideService(Operator, { id: "intern", trusted: false })),
62
- ),
63
- ).toBeInstanceOf(Action.Forbidden);
64
- }).pipe(Effect.provide([WorkspaceRegistry.layerMemory, Grants.layerDemo])),
66
+ // No grants are stored for either: only the operator a host supplies is trusted.
67
+ expect(
68
+ yield* suspend.pipe(
69
+ Effect.provideService(Operator, { id: "console", trusted: true })
70
+ )
71
+ ).toMatchObject({ suspended: true });
72
+ expect(
73
+ yield* Effect.flip(
74
+ suspend.pipe(
75
+ Effect.provideService(Operator, { id: "intern", trusted: false })
76
+ )
77
+ )
78
+ ).toBeInstanceOf(Action.Forbidden);
79
+ }).pipe(Effect.provide([WorkspaceRegistry.layerMemory, Grants.layerDemo]))
65
80
  );
66
81
  });