@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,14 +1,18 @@
1
1
  import { literalValues, members, unsuspended } from "../contract/ast.mjs";
2
2
  import { InvalidInput } from "../contract/errors.mjs";
3
- import { assertDistinct } from "../contract/names.mjs";
4
3
  import { errorSchemas } from "../contract/declared.mjs";
4
+ import { assertDistinct } from "../contract/names.mjs";
5
+ import { elementOf } from "../contract/stream.mjs";
6
+ import { elementsOf } from "../contract/implementation.mjs";
5
7
  import { decodeInput } from "../contract/call.mjs";
8
+ import { ReceivedOf } from "../contract/occurrence.mjs";
9
+ import { SubscriptionError } from "../Event.mjs";
6
10
  import { withStderrConsole } from "../stdio/console.mjs";
7
11
  import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST, Stdio, Stream } from "effect";
8
12
  import { CliError, Command, Flag, Param } from "effect/cli";
9
13
  // src/cli/commands.ts
10
14
  const kebab = (name) => {
11
- const kebabbed = name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
15
+ const kebabbed = name.replaceAll(/(?<upper>[A-Z]+)(?<word>[A-Z][a-z])/gu, "$<upper>-$<word>").replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").replaceAll("_", "-").replaceAll(/^-+|-+$/gu, "").toLowerCase();
12
16
  return kebabbed === "" ? name : kebabbed;
13
17
  };
14
18
  const renderedCommandJsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print machine-readable JSON"));
@@ -20,7 +24,13 @@ const jsonKinds = [
20
24
  "array",
21
25
  "object"
22
26
  ];
23
- const jsonKindOf = (value) => value === null ? "null" : Predicate.isBoolean(value) ? "boolean" : Predicate.isNumber(value) ? "number" : Predicate.isString(value) ? "string" : Array.isArray(value) ? "array" : "object";
27
+ const jsonKindOf = (value) => {
28
+ if (value === null) return "null";
29
+ if (Predicate.isBoolean(value)) return "boolean";
30
+ if (Predicate.isNumber(value)) return "number";
31
+ if (Predicate.isString(value)) return "string";
32
+ return Array.isArray(value) ? "array" : "object";
33
+ };
24
34
  const acceptedJsonKindsOf = (ast) => members(ast).flatMap((member) => {
25
35
  if (SchemaAST.isLiteral(member)) return [jsonKindOf(member.literal)];
26
36
  if (SchemaAST.isEnum(member)) return member.enums.map(([, value]) => jsonKindOf(value));
@@ -51,8 +61,8 @@ const valueParam = (kind, name, field) => {
51
61
  return Param.String(kind, name).pipe(Param.withSchema(jsonOfAcceptedKindOrText(encoded)), Param.withMetavar("value"));
52
62
  };
53
63
  const withoutOptionalAbsence = (ast) => {
54
- const members = SchemaAST.isUnion(ast) ? ast.types.filter((member) => !SchemaAST.isNull(member) && !SchemaAST.isUndefined(member)) : [];
55
- return members.length === 1 && members[0] !== void 0 ? members[0] : ast;
64
+ const present = SchemaAST.isUnion(ast) ? ast.types.filter((member) => !SchemaAST.isNull(member) && !SchemaAST.isUndefined(member)) : [];
65
+ return present.length === 1 && present[0] !== void 0 ? present[0] : ast;
56
66
  };
57
67
  const optionalAddedNull = (plain) => {
58
68
  const types = members(plain);
@@ -60,7 +70,7 @@ const optionalAddedNull = (plain) => {
60
70
  };
61
71
  const repeatedElementOf = (ast) => {
62
72
  const encoded = unsuspended(ast);
63
- if (!SchemaAST.isArrays(encoded)) return void 0;
73
+ if (!SchemaAST.isArrays(encoded)) return;
64
74
  const [element, ...after] = encoded.rest;
65
75
  if (element === void 0 || after.length > 0 || !encoded.elements.every((first) => first === element) || !acceptedJsonKindsOf(element).every((kind) => kind === "string" || kind === "number")) return;
66
76
  return {
@@ -69,26 +79,30 @@ const repeatedElementOf = (ast) => {
69
79
  };
70
80
  };
71
81
  const noElementOccurrence = "[]";
82
+ const namedFieldsOf = (ast) => {
83
+ const fields = SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast;
84
+ return new Map(fields !== void 0 && SchemaAST.isObjects(fields) ? fields.propertySignatures.map((property) => [property.name, property.type]) : []);
85
+ };
86
+ const elementFieldsOf = (array) => {
87
+ if (!SchemaAST.isArrays(array)) return;
88
+ const [element, ...others] = [...array.elements, ...array.rest];
89
+ if (element === void 0 || others.some((other) => other !== element)) return;
90
+ const fields = namedFieldsOf(unsuspended(element));
91
+ return fields.size === 0 ? void 0 : Array.from(fields, ([name, field]) => SchemaAST.isOptional(field) ? `${String(name)}?` : String(name));
92
+ };
72
93
  const jsonHintOf = (encoded) => {
73
94
  const value = unsuspended(encoded);
74
95
  const kinds = new Set(acceptedJsonKindsOf(value));
75
- if (kinds.size !== 1) return void 0;
96
+ if (kinds.size !== 1) return;
76
97
  if (kinds.has("object")) return "A JSON object.";
77
- if (!kinds.has("array")) return void 0;
98
+ if (!kinds.has("array")) return;
78
99
  const fields = elementFieldsOf(value);
79
100
  return fields === void 0 ? "A JSON array." : `A JSON array of {${fields.join(", ")}}.`;
80
101
  };
81
- const elementFieldsOf = (array) => {
82
- if (!SchemaAST.isArrays(array)) return void 0;
83
- const [element, ...others] = [...array.elements, ...array.rest];
84
- if (element === void 0 || others.some((other) => other !== element)) return void 0;
85
- const fields = namedFieldsOf(unsuspended(element));
86
- return fields.size === 0 ? void 0 : Array.from(fields, ([name, field]) => SchemaAST.isOptional(field) ? `${String(name)}?` : String(name));
87
- };
88
102
  const withHint = (description, hint) => {
89
103
  if (hint === void 0 || description === void 0) return description ?? hint;
90
104
  const stated = description.trimEnd();
91
- return /[.!?:]$/.test(stated) ? `${stated} ${hint}` : `${stated}. ${hint}`;
105
+ return /[.!?:]$/u.test(stated) ? `${stated} ${hint}` : `${stated}. ${hint}`;
92
106
  };
93
107
  const fieldParam = (kind, name, encoded, plain) => {
94
108
  const optional = SchemaAST.isOptional(encoded);
@@ -97,7 +111,7 @@ const fieldParam = (kind, name, encoded, plain) => {
97
111
  if (repeats !== void 0) {
98
112
  const literals = stringChoicesOf(unsuspended(repeats.element));
99
113
  const elements = literals === void 0 ? valueParam(kind, name, repeats.element) : Param.Literals(kind, name, [...literals, noElementOccurrence]);
100
- const listed = (values) => Option.some(values.filter((value) => value !== noElementOccurrence));
114
+ const listed = (values) => Option.some(values.filter((given) => given !== noElementOccurrence));
101
115
  return {
102
116
  param: optional ? Param.variadic(elements).pipe(Param.map((values) => values.length === 0 ? Option.none() : listed(values))) : Param.variadic(elements, { min: repeats.min }).pipe(Param.map(listed)),
103
117
  repeats: true,
@@ -116,13 +130,9 @@ const fieldParam = (kind, name, encoded, plain) => {
116
130
  hint: isSwitch ? "Left out, it sends false." : jsonHintOf(encoded)
117
131
  };
118
132
  };
119
- const namedFieldsOf = (ast) => {
120
- const fields = SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast;
121
- return new Map(fields !== void 0 && SchemaAST.isObjects(fields) ? fields.propertySignatures.map((property) => [property.name, property.type]) : []);
122
- };
123
133
  const isTransformedAsWhole = (ast) => (SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast)?.encoding !== void 0;
124
- const ownOrSuspendedDescriptionOf = (ast) => ast.context?.annotations?.description ?? SchemaAST.resolveDescription(ast) ?? describedBeforeLastCheck(ast) ?? (SchemaAST.isSuspend(ast) ? ownOrSuspendedDescriptionOf(ast.thunk()) : void 0);
125
134
  const describedBeforeLastCheck = (ast) => [ast.annotations, ...(ast.checks ?? []).map((check) => check.annotations)].map((annotations) => annotations?.["description"]).findLast(Predicate.isString);
135
+ const ownOrSuspendedDescriptionOf = (ast) => ast.context?.annotations?.description ?? SchemaAST.resolveDescription(ast) ?? describedBeforeLastCheck(ast) ?? (SchemaAST.isSuspend(ast) ? ownOrSuspendedDescriptionOf(ast.thunk()) : void 0);
126
136
  const describedThroughAbsence = (ast) => ownOrSuspendedDescriptionOf(ast) ?? ownOrSuspendedDescriptionOf(withoutOptionalAbsence(ast));
127
137
  const describedByElement = (ast) => {
128
138
  const array = unsuspended(withoutOptionalAbsence(ast));
@@ -191,7 +201,7 @@ var UserError = class extends CliError.UserError {
191
201
  }
192
202
  };
193
203
  const ownStringProperty = (error, key) => {
194
- if (!Predicate.hasProperty(error, key)) return void 0;
204
+ if (!Predicate.hasProperty(error, key)) return;
195
205
  const value = error[key];
196
206
  return Predicate.isString(value) ? value : void 0;
197
207
  };
@@ -228,7 +238,7 @@ const stdinTextOf = (field) => Effect.gen(function* () {
228
238
  const stdio = yield* Effect.serviceOption(Stdio.Stdio);
229
239
  if (Option.isNone(stdio)) return yield* Effect.die(/* @__PURE__ */ new Error("Stdio is not provided (provide NodeServices.layer, or Stdio.layerTest in a test)"));
230
240
  if (yield* stdio.value.stdinIsTerminal) return yield* unreadableStdinField(field, `${field} is read from stdin, which is a terminal: pipe it in`);
231
- return (yield* stdio.value.stdin.pipe(Stream.decodeText(), Stream.mkString, Effect.mapError((error) => unreadableStdinField(field, `stdin cannot be read: ${error.message}`)))).replace(/[\r\n]+$/, "");
241
+ return (yield* stdio.value.stdin.pipe(Stream.decodeText(), Stream.mkString, Effect.mapError((error) => unreadableStdinField(field, `stdin cannot be read: ${error.message}`)))).replace(/[\r\n]+$/u, "");
232
242
  });
233
243
  const inputConfig = (action, positional, aliases, stdin) => {
234
244
  const codec = Schema.toCodecJson(action.input);
@@ -275,6 +285,24 @@ const inputConfig = (action, positional, aliases, stdin) => {
275
285
  };
276
286
  };
277
287
  const aliasFlag = (alias) => alias.length === 1 ? `-${alias}` : `--${alias}`;
288
+ const progressLinesOf = (progress) => {
289
+ if (progress === void 0) return;
290
+ const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(progress)));
291
+ return (value) => Effect.flatMap(Effect.orDie(encode(value)), Console.log);
292
+ };
293
+ const printedOf = (action) => {
294
+ const element = elementOf(action.success);
295
+ if (element === void 0) return (value, render, print) => Effect.flatMap(printedOutput(action, value, render), Option.match({
296
+ onNone: () => Effect.void,
297
+ onSome: (line) => Effect.sync(() => {
298
+ print(line);
299
+ })
300
+ }));
301
+ const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(element)));
302
+ return (value, render, print) => Stream.runForEach(elementsOf(Effect.succeed(value)), (each) => Effect.map(Effect.orDie(encode(each)), (json) => {
303
+ print(render === void 0 ? json : render(each));
304
+ }));
305
+ };
278
306
  const command = (action, execute, options, errors) => {
279
307
  const name = options?.name ?? kebab(action.name);
280
308
  const description = options?.description ?? action.description;
@@ -293,7 +321,7 @@ const command = (action, execute, options, errors) => {
293
321
  "render's --json"
294
322
  ]],
295
323
  ...Object.entries(options?.aliases ?? {}).flatMap(([field, alias]) => {
296
- const bare = Predicate.isString(alias) ? alias.replace(/^-+/, "") : void 0;
324
+ const bare = Predicate.isString(alias) ? alias.replace(/^-+/u, "") : void 0;
297
325
  return bare === void 0 ? [] : [[
298
326
  bare,
299
327
  aliasFlag(bare),
@@ -302,7 +330,7 @@ const command = (action, execute, options, errors) => {
302
330
  })
303
331
  ], ([key]) => key, {
304
332
  labelOf: ([, label]) => label,
305
- claimantOf: ([, , claimant]) => claimant,
333
+ claimantOf: (claim) => claim[2],
306
334
  fix: render === void 0 ? "rename the field or the alias" : "rename the field or the alias, or drop `render`"
307
335
  });
308
336
  const args = positional.map(({ param }) => param);
@@ -310,11 +338,12 @@ const command = (action, execute, options, errors) => {
310
338
  ...input,
311
339
  ...Object.fromEntries(positional.map(({ field }, index) => [field, values[index] ?? Option.none()]))
312
340
  });
313
- const run = (parsed, rendered) => decode(parsed).pipe(Effect.flatMap(execute), failing, Effect.flatMap((value) => printedOutput(action, value, rendered)), withStderrConsole, Effect.flatMap(Option.match({
314
- onNone: () => Effect.void,
315
- onSome: Console.log
316
- })));
317
- const command = render === void 0 ? Command.make(name, {
341
+ const progressLines = progressLinesOf(action.progress);
342
+ const printed = printedOf(action);
343
+ const run = (parsed, rendered) => Console.consoleWith((stdout) => decode(parsed).pipe(Effect.flatMap((input) => execute(input, progressLines)), Effect.flatMap((value) => printed(value, rendered, (line) => {
344
+ stdout.log(line);
345
+ })), failing, withStderrConsole));
346
+ const projected = render === void 0 ? Command.make(name, {
318
347
  input: flags,
319
348
  args
320
349
  }, ({ input, args: values }) => run(parsedFields(input, values), void 0)) : Command.make(name, {
@@ -322,6 +351,28 @@ const command = (action, execute, options, errors) => {
322
351
  args,
323
352
  json: renderedCommandJsonFlag
324
353
  }, ({ input, args: values, json }) => run(parsedFields(input, values), json ? void 0 : render));
325
- return stdinHelp === void 0 ? Command.withDescription(command, description) : command.pipe(Command.withDescription(`${description}\n\n${stdinHelp}`), Command.withShortDescription(description));
354
+ return stdinHelp === void 0 ? Command.withDescription(projected, description) : projected.pipe(Command.withDescription(`${description}\n\n${stdinHelp}`), Command.withShortDescription(description));
355
+ };
356
+ /**
357
+ * The command watching `event`, named after it, with one flag per field of its input as an
358
+ * action's command has, and `--cursor` to resume from: it prints each occurrence and gap `watch`
359
+ * yields as one line of compact JSON on stdout, until it is interrupted or the watch fails.
360
+ */
361
+ const watchCommand = (event, watch, errors) => {
362
+ const { flags, decode } = inputConfig(event, [], {}, void 0);
363
+ const encode = Schema.encodeEffect(Schema.fromJsonString(Schema.toCodecJson(ReceivedOf(event))));
364
+ const failing = failingAsDerived([...errors, SubscriptionError]);
365
+ assertDistinct("flag", [...Object.keys(flags).map((field) => [kebab(field), `field ${field}`]), ["cursor", "--cursor"]], ([key]) => key, {
366
+ labelOf: ([key]) => `--${key}`,
367
+ claimantOf: ([, claimant]) => claimant,
368
+ fix: "rename the field"
369
+ });
370
+ const cursor = Flag.String("cursor").pipe(Flag.withDescription("Where to resume: a cursor an earlier line carried; omitted, from now"), Flag.optional);
371
+ return Command.make(event.name, {
372
+ input: flags,
373
+ cursor
374
+ }, ({ input, cursor: from }) => Console.consoleWith((stdout) => decode(input).pipe(Effect.flatMap((value) => Stream.runForEach(watch(value, Option.getOrUndefined(from)), (received) => Effect.flatMap(Effect.orDie(encode(received)), (line) => Effect.sync(() => {
375
+ stdout.log(line);
376
+ })))), failing, withStderrConsole))).pipe(Command.withDescription(event.description));
326
377
  };
327
- export { UserError, command, failingAsDerived, kebab };
378
+ export { UserError, command, failingAsDerived, kebab, watchCommand };
@@ -5,5 +5,8 @@ const members = (ast) => {
5
5
  const resolved = unsuspended(ast);
6
6
  return SchemaAST.isUnion(resolved) ? resolved.types.flatMap(members) : [resolved];
7
7
  };
8
- const literalValues = (ast) => SchemaAST.isLiteral(ast) ? [ast.literal] : SchemaAST.isEnum(ast) ? ast.enums.map(([, value]) => value) : [void 0];
8
+ const literalValues = (ast) => {
9
+ if (SchemaAST.isLiteral(ast)) return [ast.literal];
10
+ return SchemaAST.isEnum(ast) ? ast.enums.map(([, value]) => value) : [void 0];
11
+ };
9
12
  export { literalValues, members, unsuspended };
@@ -0,0 +1,8 @@
1
+ import { Refusal } from "./errors.mjs";
2
+ import "./implementation.mjs";
3
+ import { Effect } from "effect";
4
+ /** `T`, read only once it is inferred: an implementation's build channels. */
5
+ type Deferred$1<T> = [T][T extends unknown ? 0 : never];
6
+ /** What `implement` takes as `authorize` for contracts `C`: the rule, or an Effect building it. */
7
+ export type Authorizer<C, RA, EAX, RAX> = ((contract: C) => Effect.Effect<void, Refusal, RA>) | Effect.Effect<(contract: C) => Effect.Effect<void, Refusal, RA>, EAX, RAX>;
8
+ export { Deferred$1 as Deferred };
@@ -0,0 +1,20 @@
1
+ import { memoized } from "./implementation.mjs";
2
+ import { Effect, Predicate } from "effect";
3
+ // src/contract/authorizer.ts
4
+ /**
5
+ * The memoized authorizer of an implementation: the `authorize` given where it holds a protected
6
+ * contract, which then requires one, and a rule allowing every call where it holds none, which
7
+ * then takes none.
8
+ */
9
+ const authorizerOf = (holding, holdsProtected, authorize) => {
10
+ const { singular, module } = holding;
11
+ if (holdsProtected && authorize === void 0) throw new Error(`Protected ${singular}s require authorize, or ${module}.allowAll`);
12
+ if (!holdsProtected && authorize !== void 0) throw new Error(`A public-only target takes no authorize (remove it, or list a protected ${singular})`);
13
+ const checked = (rule) => {
14
+ if (!Predicate.isFunction(rule)) throw new Error(`Missing authorize: not a function (pass your rule, ${module}.allowAll, or an Effect returning one)`);
15
+ return rule;
16
+ };
17
+ const given = authorize ?? (() => Effect.void);
18
+ return memoized(Effect.isEffect(given) ? Effect.map(given, checked) : Effect.succeed(checked(given)));
19
+ };
20
+ export { authorizerOf };
@@ -1,11 +1,14 @@
1
+ import "./codec.mjs";
1
2
  import "./errors.mjs";
3
+ import { ProgressArgs } from "./progress.mjs";
2
4
  import "./implementation.mjs";
3
5
  import { Any } from "../Action.mjs";
4
- import { Effect, Schema } from "effect";
6
+ import { Effect, Schema, Stream } from "effect";
5
7
  /**
6
- * A call of `A` answering `R`. It may leave its input out when `{}` is a valid encoded input,
7
- * such as for an action declared without `input` or one whose every field has a decoding
8
- * default, and then sends the input `{}` decodes to. Every client shares this rule:
9
- * `ActionHttp.client`, `ActionRpc.client`, `ActionMcp.client` and `Action.client`.
8
+ * A call of `A` answering `R`, taking `{ onProgress }` after its input where `A` declares
9
+ * `progress`. It may leave its input out when `{}` is a valid encoded input, such as for an
10
+ * action declared without `input` or one whose every field has a decoding default, and then
11
+ * sends the input `{}` decodes to. Every client shares this rule: `ActionHttp.client`,
12
+ * `ActionRpc.client`, `ActionMcp.client` and `Action.client`.
10
13
  */
11
- export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
14
+ export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"], ...ProgressArgs<A>]) => R : (input: A["input"]["Type"], ...options: ProgressArgs<A>) => R;
@@ -1,6 +1,12 @@
1
1
  import { InvalidInput } from "./errors.mjs";
2
- import { Effect, Schema } from "effect";
2
+ import { Effect, Predicate, Schema, Stream } from "effect";
3
3
  // src/contract/call.ts
4
+ const isCallOptions = (options) => Predicate.hasProperty(options, "onProgress") && Predicate.isFunction(options.onProgress);
5
+ /** The `onProgress` a call's arguments give after its input, where they give one. */
6
+ const onProgressOf = (args) => {
7
+ const [, options] = args;
8
+ return isCallOptions(options) ? options.onProgress : void 0;
9
+ };
4
10
  /**
5
11
  * How HTTP, RPC, MCP and a command decode a request's input: reporting every issue, and refusing a
6
12
  * field the input does not declare.
@@ -14,9 +20,45 @@ const decodeInput = (action) => {
14
20
  const decode = Schema.decodeUnknownEffect(Schema.toCodecJson(action.input), inputParseOptions);
15
21
  return (encoded) => Effect.mapError(decode(encoded), InvalidInput.fromSchemaError);
16
22
  };
17
- const inputOf = (action, args) => args.length === 0 ? Schema.decodeUnknownEffect(Schema.toCodecJson(action.input))({}) : Effect.succeed(args[0]);
18
- const checked = (action, send) => {
19
- const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(action.input), { errors: "all" });
20
- return (...args) => inputOf(action, args).pipe(Effect.tap(encode), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap(send));
23
+ const inputOf = (action, args) => args.length === 0 ? Schema.decodeEffect(Schema.toCodecJson(action.input))({}) : Effect.succeed(args[0]);
24
+ /**
25
+ * Values of `schema` as they cross a network: each encoded to its JSON, then decoded back from
26
+ * that JSON's text, which carries `-0` as `0`, every issue reported, answering the JSON sent and
27
+ * the value received.
28
+ */
29
+ const jsonRoundTrip = (schema) => {
30
+ const codec = Schema.toCodecJson(schema);
31
+ const encode = Schema.encodeUnknownEffect(codec, { errors: "all" });
32
+ const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(codec), { errors: "all" });
33
+ return (value) => Effect.flatMap(encode(value), (json) => Effect.map(decode(JSON.stringify(json)), (received) => ({
34
+ json,
35
+ value: received
36
+ })));
37
+ };
38
+ /**
39
+ * A watch's input as every client sends it: read as `inputOf` reads an action's, then through
40
+ * its JSON, dropping undeclared fields, or `InvalidInput` reporting every issue.
41
+ */
42
+ const watchInput = (event, input) => inputOf(event, input === void 0 ? [] : [input]).pipe(Effect.flatMap(jsonRoundTrip(event.input)), Effect.mapError(InvalidInput.fromSchemaError));
43
+ /** Send the input's JSON, for a client handing it to its protocol as it is. */
44
+ const sendingJson = (action) => Schema.encodeUnknownEffect(Schema.toCodecJson(action.input), { errors: "all" });
45
+ /** Send the input itself once its JSON encodes, for a client encoding it by the action's schema. */
46
+ const sendingValue = (action) => {
47
+ const encode = sendingJson(action);
48
+ return (input) => Effect.as(encode(input), input);
49
+ };
50
+ /**
51
+ * A client's method of `action`, its input checked by `check` before `send` sends what the check
52
+ * answers, with the call's `onProgress` where it gave one, taking decoded values, for `send` to
53
+ * use where the action declares `progress`.
54
+ */
55
+ const checked = (action, check, send) => {
56
+ const encode = check(action);
57
+ return (...args) => inputOf(action, args).pipe(Effect.flatMap(encode), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap((input) => send(input, onProgressOf(args))));
58
+ };
59
+ /** A client's method of `action` answering a stream, its input checked as `checked` checks it. */
60
+ const checkedStream = (action, check, send) => {
61
+ const method = checked(action, check, (input, onProgress) => Effect.succeed(send(input, onProgress)));
62
+ return (...args) => Stream.unwrap(method(...args));
21
63
  };
22
- export { checked, decodeInput, inputOf, inputParseOptions };
64
+ export { checked, checkedStream, decodeInput, inputOf, inputParseOptions, jsonRoundTrip, onProgressOf, sendingJson, sendingValue, watchInput };
@@ -0,0 +1,7 @@
1
+ import { Schema } from "effect";
2
+ /** A Standard Webhooks signing secret: as written, `whsec_` and base64, and the key it holds. */
3
+ export declare const SigningSecret: Schema.Struct<{
4
+ readonly text: Schema.String;
5
+ readonly key: Schema.Uint8Array;
6
+ }>;
7
+ export type SigningSecret = typeof SigningSecret.Type;
@@ -0,0 +1,27 @@
1
+ import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect";
2
+ // src/contract/callback.ts
3
+ /** A webhook's callback: an https URL, which each delivery is POSTed to, decoded as a `URL`. */
4
+ const CallbackUrl = Schema.String.annotate({ description: "The https URL each delivery is POSTed to." }).pipe(Schema.decodeTo(Schema.URL, SchemaTransformation.urlFromString), Schema.check(Schema.makeFilter((url) => url.protocol === "https:" || "an https URL")));
5
+ /** A Standard Webhooks signing secret: as written, `whsec_` and base64, and the key it holds. */
6
+ const SigningSecret = Schema.Struct({
7
+ text: Schema.String,
8
+ key: Schema.Uint8Array
9
+ });
10
+ const secretPrefix = "whsec_";
11
+ const decodeBase64 = Schema.decodeUnknownOption(Schema.Uint8ArrayFromBase64);
12
+ const keyOf = (text) => text.startsWith(secretPrefix) ? Option.filter(decodeBase64(text.slice(6)), (key) => key.length >= 24 && key.length <= 64) : Option.none();
13
+ /**
14
+ * A webhook's signing secret, as Standard Webhooks writes one: `whsec_` and the base64 of 24 to
15
+ * 64 bytes, decoded as the `SigningSecret` it is.
16
+ */
17
+ const WebhookSecret = Schema.String.annotate({ description: "whsec_ and the base64 of 24 to 64 random bytes, which signs each delivery." }).pipe(Schema.decodeTo(SigningSecret, SchemaTransformation.transformEffect({
18
+ decode: (text, options) => Option.match(keyOf(text), {
19
+ onNone: () => Effect.fail(new SchemaIssue.InvalidValue({ message: "whsec_ and the base64 of 24 to 64 random bytes" }, text, options)),
20
+ onSome: (key) => Effect.succeed({
21
+ text,
22
+ key
23
+ })
24
+ }),
25
+ encode: ({ text }) => Effect.succeed(text)
26
+ })));
27
+ export { CallbackUrl, SigningSecret, WebhookSecret };
@@ -1,7 +1,18 @@
1
- import "../Action.mjs";
1
+ import { Codec } from "./codec.mjs";
2
+ import { Any } from "../Action.mjs";
3
+ import { Context, Effect } from "effect";
2
4
  /**
3
5
  * The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
4
6
  * authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
5
7
  * agree on a name.
6
8
  */
7
- export declare const Anyone: unique symbol;
9
+ export declare const Anyone: unique symbol;
10
+ /** What names one caller of a surface: its authentication's `principal`, where it has one. */
11
+ export interface Keyed {
12
+ readonly principal?: (identity: Any["input"]["Type"]) => string;
13
+ readonly schema?: Codec | undefined;
14
+ }
15
+ /** A surface's authentication that names each caller: its `principal` given. */
16
+ export interface Named extends Keyed {
17
+ readonly principal: (identity: Any["input"]["Type"]) => string;
18
+ }
@@ -1,3 +1,4 @@
1
+ import { Context, Effect, Function } from "effect";
1
2
  // src/contract/caller.ts
2
3
  /**
3
4
  * The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
@@ -5,7 +6,25 @@
5
6
  * agree on a name.
6
7
  */
7
8
  const Anyone = Symbol.for("@gjermundgaraba/effect-actions/Anyone");
8
- const assertAuthentication = (actions, authentication) => {
9
- for (const action of actions) if (action.caller !== Anyone && (authentication === void 0 || action.caller.key !== authentication.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor (give it the authentication whose identity the action's caller names)`);
9
+ /**
10
+ * The authentication of the surface the running call came through, which names its caller where
11
+ * it has a `principal`: none in process, as `Action.client` calls.
12
+ */
13
+ const CallerKeyed = Context.Reference("effect-actions/CallerKeyed", { defaultValue: Function.constUndefined });
14
+ /** Whether `keyed` names each caller, its `principal` given. */
15
+ const isNamed = (keyed) => keyed?.principal !== void 0;
16
+ /**
17
+ * The principal of the caller of `called`, as `keyed` names it: a poll's subscriber, for its
18
+ * `subscription` hook, and whose idempotency keys a call's are. None where `called` is public or
19
+ * `keyed` has no `principal`: every caller is then one.
20
+ */
21
+ const principalOf = (called, keyed) => keyed?.principal === void 0 || called.caller === Anyone ? Effect.undefined : Effect.map(Effect.service(called.caller), (identity) => keyed.principal?.(identity));
22
+ const assertAuthentication = (contracts, authentication, noun = "action") => {
23
+ for (const contract of contracts) if (contract.caller !== Anyone && (authentication === void 0 || contract.caller.key !== authentication.identity.key)) throw new Error(`Protected ${noun} '${contract.name}' requires its matching authentication descriptor (give it the authentication whose identity the ${noun}'s caller names)`);
24
+ };
25
+ /** Refuses a declared `caller` that is neither `Anyone` nor an identity service. */
26
+ const assertCaller = (caller) => {
27
+ if (caller !== Anyone && !Context.isKey(caller)) throw new Error("Missing caller: declare Action.Anyone or an identity service key");
28
+ if (caller !== Anyone && Context.isReference(caller)) throw new Error("Invalid caller: an identity is a Context.Service, not a Context.Reference");
10
29
  };
11
- export { Anyone, assertAuthentication };
30
+ export { Anyone, CallerKeyed, assertAuthentication, assertCaller, isNamed, principalOf };
@@ -0,0 +1,10 @@
1
+ import { Effect, Predicate, Schema } from "effect";
2
+ // src/contract/canonical.ts
3
+ /**
4
+ * `value`, an encoded input, as JSON written one way whatever the order of its objects' keys:
5
+ * what makes two inputs one, for a subscription's key and an idempotency key's input alike.
6
+ */
7
+ const canonical = (value) => JSON.stringify(value, (_key, nested) => Predicate.isObject(nested) && !Array.isArray(nested) ? Object.fromEntries(Object.entries(nested).toSorted(([a], [b]) => a < b ? -1 : 1)) : nested);
8
+ /** `input`, encoded to JSON by `schema`, as `canonical` writes it. */
9
+ const canonicalInput = (schema, input) => Effect.map(Effect.orDie(Schema.encodeUnknownEffect(Schema.toCodecJson(schema))(input)), canonical);
10
+ export { canonical, canonicalInput };
@@ -0,0 +1,9 @@
1
+ import { Schema } from "effect";
2
+ /** A schema a contract holds: one with no services to decode or encode. */
3
+ export type Codec = Schema.Codec<unknown>;
4
+ /** Struct fields, given in place of a schema. */
5
+ export type Fields = Readonly<Record<string, Codec>>;
6
+ /** The input of a contract declared without one: an empty object. */
7
+ export declare const NoInput: Schema.$Record<Schema.String, Schema.Never>;
8
+ /** The schema a contract holds for `S`, given as a schema or as struct fields. */
9
+ export type CodecOf<S extends Codec | Fields> = S extends Codec ? S : S extends Schema.Struct.Fields ? keyof S extends never ? typeof NoInput : Extract<Schema.Struct<S>, Codec> : never;
@@ -0,0 +1,9 @@
1
+ import { Schema } from "effect";
2
+ // src/contract/codec.ts
3
+ /** The input of a contract declared without one: an empty object. */
4
+ const NoInput = Schema.Record(Schema.String, Schema.Never);
5
+ const codecOf = (schema) => {
6
+ if (Schema.isSchema(schema)) return schema;
7
+ return Reflect.ownKeys(schema).length === 0 ? NoInput : Schema.Struct(schema);
8
+ };
9
+ export { NoInput, codecOf };
@@ -0,0 +1,81 @@
1
+ import { Forbidden, Refusal } from "./errors.mjs";
2
+ import { CallbackCategory, CallbackFailed, NotServed, NotSubscribed, SubscriptionError, Unsupported } from "../Event.mjs";
3
+ import { Option, Schema, SchemaTransformation, Struct } from "effect";
4
+ // src/contract/codes.ts
5
+ /** The error codes SEP-3415 defines, each naming one reason. */
6
+ const eventCodes = {
7
+ notFound: -32023,
8
+ forbidden: -32024,
9
+ unsupported: -32026,
10
+ callbackEndpoint: -32027
11
+ };
12
+ const sent = {
13
+ NotSubscribed: {
14
+ code: eventCodes.notFound,
15
+ data: Schema.Struct({ kind: Schema.Literal("subscription") }).pipe(Schema.decodeTo(Schema.instanceOf(NotSubscribed), SchemaTransformation.transform({
16
+ decode: () => new NotSubscribed(),
17
+ encode: () => ({ kind: "subscription" })
18
+ })))
19
+ },
20
+ NotServed: {
21
+ code: eventCodes.notFound,
22
+ data: Schema.UndefinedOr(Schema.Json).pipe(Schema.decodeTo(Schema.instanceOf(NotServed), SchemaTransformation.transform({
23
+ decode: () => new NotServed(),
24
+ encode: () => ({ kind: "event" })
25
+ })))
26
+ },
27
+ Unsupported: {
28
+ code: eventCodes.unsupported,
29
+ data: Schema.Struct(Struct.omit(Unsupported.fields, ["_tag"])).pipe(Schema.decodeTo(Schema.instanceOf(Unsupported), SchemaTransformation.transform({
30
+ decode: (fields) => new Unsupported(fields),
31
+ encode: ({ feature, value }) => ({
32
+ feature,
33
+ value
34
+ })
35
+ })))
36
+ },
37
+ CallbackFailed: {
38
+ code: eventCodes.callbackEndpoint,
39
+ data: Schema.Struct({ reason: CallbackCategory }).pipe(Schema.decodeTo(Schema.instanceOf(CallbackFailed), SchemaTransformation.transform({
40
+ decode: ({ reason }) => new CallbackFailed({ category: reason }),
41
+ encode: ({ category }) => ({ reason: category })
42
+ })))
43
+ }
44
+ };
45
+ const encodeRefusal = Schema.encodeUnknownSync(Schema.toCodecJson(Refusal));
46
+ const decodeRefusal = Schema.decodeUnknownOption(Schema.toCodecJson(Refusal));
47
+ /**
48
+ * The SEP-3415 error `error` is sent as: a refusal's `-32024`, its message and the refusal as
49
+ * its data, or its reason's code and data beside its message.
50
+ */
51
+ const sentError = (error) => {
52
+ if (Schema.is(Refusal)(error)) return {
53
+ code: eventCodes.forbidden,
54
+ message: error.message,
55
+ data: encodeRefusal(error)
56
+ };
57
+ const { code, data } = sent[error.reason._tag];
58
+ return {
59
+ code,
60
+ message: error.message,
61
+ data: Schema.encodeUnknownSync(Schema.toCodecJson(data))(error.reason)
62
+ };
63
+ };
64
+ /**
65
+ * The refusal a SEP-3415 error carries, as `sentError` writes one: its `data` decoded, or
66
+ * `Forbidden` with its message where `data` is no refusal.
67
+ */
68
+ const refusalOf = (error) => Option.getOrElse(decodeRefusal(error.data), () => new Forbidden({ message: error.message }));
69
+ /**
70
+ * What a SEP-3415 error of the event `name` names, as a subscription's request fails with one and
71
+ * its termination carries one: a reason of `Event.SubscriptionError`, or the refusal; `undefined`
72
+ * for a code SEP-3415 does not define, or one whose `data` does not say what it names.
73
+ */
74
+ const reasonOf = (name, error) => {
75
+ if (error.code === eventCodes.forbidden) return refusalOf(error);
76
+ for (const { code, data } of Object.values(sent)) {
77
+ const reason = code === error.code ? Schema.decodeUnknownOption(data)(error.data) : Option.none();
78
+ if (Option.isSome(reason)) return SubscriptionError.of(name, reason.value);
79
+ }
80
+ };
81
+ export { eventCodes, reasonOf, refusalOf, sentError };
@@ -1,13 +1,19 @@
1
- import { literalValues, members } from "./ast.mjs";
2
1
  import { Anyone, assertAuthentication } from "./caller.mjs";
2
+ import { literalValues, members } from "./ast.mjs";
3
3
  import { builtIns } from "./errors.mjs";
4
- import { assertOnce } from "./names.mjs";
5
4
  import { httpApiStatusOf } from "./status.mjs";
6
5
  import { Predicate, Schema, SchemaAST } from "effect";
7
6
  // src/contract/declared.ts
8
- const errorList = (error) => error === void 0 ? [] : Schema.isSchema(error) ? [error] : error;
7
+ const errorList = (error) => {
8
+ if (error === void 0) return [];
9
+ return Schema.isSchema(error) ? [error] : error;
10
+ };
9
11
  const ownsNothing = (ast) => httpApiStatusOf(ast) === void 0 && ast.checks === void 0 && ast.encoding === void 0;
10
- const schemasOf = (ast) => !ownsNothing(ast) ? [ast] : SchemaAST.isSuspend(ast) ? schemasOf(ast.thunk()) : SchemaAST.isUnion(ast) ? ast.types.flatMap(schemasOf) : [ast];
12
+ const schemasOf = (ast) => {
13
+ if (!ownsNothing(ast)) return [ast];
14
+ if (SchemaAST.isSuspend(ast)) return schemasOf(ast.thunk());
15
+ return SchemaAST.isUnion(ast) ? ast.types.flatMap(schemasOf) : [ast];
16
+ };
11
17
  /** `errors` as the schemas a call decodes a failure with, in order, each once. */
12
18
  const errorSchemas = (errors) => {
13
19
  const schemas = /* @__PURE__ */ new Map();
@@ -24,6 +30,14 @@ const projectedErrors = (action, surface = []) => errorSchemas([
24
30
  * binding's and, where `action` is protected, its authentication's.
25
31
  */
26
32
  const bindingErrors = (action, binding) => projectedErrors(action, action.caller === Anyone || binding.authentication === void 0 ? binding.error : [...binding.error, ...binding.authentication.error]);
33
+ /**
34
+ * Every error a binding's subscription to `event` declares: the built-in ones, the binding's and,
35
+ * where `event` is protected, its authentication's. An event type declares none of its own.
36
+ */
37
+ const eventErrors = (event, binding) => bindingErrors({
38
+ error: [],
39
+ caller: event.caller
40
+ }, binding);
27
41
  const encodedTags = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member) => {
28
42
  const tag = SchemaAST.isObjects(member) ? member.propertySignatures.find((property) => property.name === "_tag")?.type : void 0;
29
43
  return tag === void 0 ? [] : members(tag).flatMap(literalValues).filter(Predicate.isString);
@@ -38,17 +52,19 @@ const assertDistinctTags = (what, errors) => {
38
52
  }
39
53
  };
40
54
  /**
41
- * The checks `ActionHttp.make` and `ActionRpc.make` share: each action once, the descriptor every
42
- * protected action names, and each call's errors distinct by `_tag`. Returns the binding's `error`, as a list, and `authentication`.
55
+ * The checks `ActionHttp.make` and `ActionRpc.make` share: the descriptor every protected action
56
+ * and event type names, and each call's and each subscription's errors distinct by `_tag`.
57
+ * Returns the binding's `error`, as a list, and `authentication`.
43
58
  */
44
- const bindingOf = (what, actions, options) => {
45
- assertOnce("action", actions);
59
+ const bindingOf = (what, actions, events, options) => {
46
60
  const bound = {
47
61
  error: errorList(options.error),
48
62
  authentication: options.authentication
49
63
  };
50
64
  assertAuthentication(actions, bound.authentication);
65
+ assertAuthentication(events, bound.authentication, "event");
51
66
  for (const action of actions) assertDistinctTags(`${what} of "${action.name}"`, bindingErrors(action, bound));
67
+ for (const event of events) assertDistinctTags(`${what} of event "${event.name}"`, eventErrors(event, bound));
52
68
  return bound;
53
69
  };
54
- export { assertDistinctTags, bindingErrors, bindingOf, errorList, errorSchemas, projectedErrors };
70
+ export { assertDistinctTags, bindingErrors, bindingOf, errorList, errorSchemas, eventErrors, projectedErrors };
@@ -0,0 +1,6 @@
1
+ import { Effect } from "effect";
2
+ // src/contract/digest.ts
3
+ const encoder = new TextEncoder();
4
+ /** The SHA-256 of `text`, in hex. */
5
+ const sha256Hex = (text) => Effect.promise(async () => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(text))), (byte) => byte.toString(16).padStart(2, "0")).join(""));
6
+ export { sha256Hex };