@gjermundgaraba/effect-actions 0.10.0 → 0.12.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 (214) hide show
  1. package/README.md +103 -43
  2. package/dist/Action.d.mts +31 -118
  3. package/dist/Action.mjs +15 -53
  4. package/dist/ActionCli.d.mts +60 -28
  5. package/dist/ActionCli.mjs +54 -48
  6. package/dist/ActionHttp.d.mts +18 -67
  7. package/dist/ActionHttp.mjs +20 -70
  8. package/dist/ActionMcp.d.mts +25 -31
  9. package/dist/ActionMcp.mjs +140 -74
  10. package/dist/ActionRpc.d.mts +147 -0
  11. package/dist/ActionRpc.mjs +113 -0
  12. package/dist/ActionToolkit.d.mts +5 -8
  13. package/dist/ActionToolkit.mjs +3 -3
  14. package/dist/Authentication.d.mts +15 -15
  15. package/dist/Authentication.mjs +42 -96
  16. package/dist/Testing.d.mts +32 -25
  17. package/dist/Testing.mjs +45 -30
  18. package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
  19. package/dist/authentication/provider.mjs +12 -0
  20. package/dist/authentication/refusal.mjs +36 -0
  21. package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
  22. package/dist/cli/commands.mjs +302 -0
  23. package/dist/{internal → contract}/call.d.mts +2 -2
  24. package/dist/contract/call.mjs +10 -0
  25. package/dist/{internal → contract}/errors.d.mts +13 -10
  26. package/dist/{internal → contract}/errors.mjs +9 -14
  27. package/dist/{internal → contract}/implementation.d.mts +37 -21
  28. package/dist/{internal → contract}/implementation.mjs +18 -49
  29. package/dist/contract/media.d.mts +13 -0
  30. package/dist/contract/media.mjs +18 -0
  31. package/dist/contract/rules.d.mts +25 -0
  32. package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
  33. package/dist/{internal → http}/client.d.mts +7 -9
  34. package/dist/http/client.mjs +17 -0
  35. package/dist/http/declared.mjs +20 -0
  36. package/dist/http/schema-errors.mjs +9 -0
  37. package/dist/mcp/protocol.d.mts +17 -0
  38. package/dist/mcp/protocol.mjs +149 -0
  39. package/dist/stdio/console.mjs +64 -0
  40. package/dist/{internal → testing}/memory.d.mts +1 -1
  41. package/dist/{internal → testing}/memory.mjs +5 -17
  42. package/dist/toolkit/tools.mjs +29 -0
  43. package/docs/Action-failures.md +47 -0
  44. package/docs/Action-in-process.md +93 -0
  45. package/docs/Action.md +206 -424
  46. package/docs/ActionCli-failures.md +102 -0
  47. package/docs/ActionCli.md +206 -292
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +202 -281
  50. package/docs/ActionMcp-failures.md +31 -0
  51. package/docs/ActionMcp.md +229 -193
  52. package/docs/ActionRpc-failures.md +21 -0
  53. package/docs/ActionRpc.md +178 -0
  54. package/docs/ActionToolkit-failures.md +15 -0
  55. package/docs/ActionToolkit.md +100 -117
  56. package/docs/Authentication-advanced.md +139 -0
  57. package/docs/Authentication-failures.md +99 -0
  58. package/docs/Authentication.md +142 -392
  59. package/docs/CONTEXT.md +39 -23
  60. package/docs/README.md +71 -35
  61. package/docs/Testing-failures.md +20 -0
  62. package/docs/Testing.md +186 -151
  63. package/docs/guarantees.md +35 -95
  64. package/docs/setup-failures.md +34 -0
  65. package/docs/setup.md +88 -87
  66. package/examples/README.md +68 -0
  67. package/examples/assistant/README.md +92 -0
  68. package/examples/assistant/src/agent.ts +92 -0
  69. package/examples/assistant/src/cli.ts +34 -0
  70. package/examples/assistant/src/commands.ts +25 -0
  71. package/examples/assistant/src/identity/Identity.ts +33 -0
  72. package/examples/assistant/src/identity/authorize.ts +24 -0
  73. package/examples/assistant/src/identity/handlers.ts +10 -0
  74. package/examples/assistant/src/identity/members.ts +9 -0
  75. package/examples/assistant/src/identity/permissions.ts +23 -0
  76. package/examples/assistant/src/scripted.ts +23 -0
  77. package/examples/assistant/src/tasks/Tasks.ts +63 -0
  78. package/examples/assistant/src/tasks/handlers.ts +25 -0
  79. package/examples/assistant/src/tasks/store.ts +96 -0
  80. package/examples/assistant/src/toolkit.ts +30 -0
  81. package/examples/assistant/test/commands.test.ts +57 -0
  82. package/examples/assistant/test/identity/identity.test.ts +43 -0
  83. package/examples/assistant/test/tasks/store.test.ts +48 -0
  84. package/examples/assistant/test/tasks/tasks.test.ts +75 -0
  85. package/examples/assistant/test/toolkit.test.ts +121 -0
  86. package/examples/kitchen-sink/README.md +317 -0
  87. package/examples/kitchen-sink/src/Actions.ts +19 -0
  88. package/examples/kitchen-sink/src/Api.ts +24 -0
  89. package/examples/kitchen-sink/src/Rpc.ts +12 -0
  90. package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
  91. package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
  92. package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
  93. package/examples/kitchen-sink/src/admin/grants.ts +22 -0
  94. package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
  95. package/examples/kitchen-sink/src/admin/keys.ts +8 -0
  96. package/examples/kitchen-sink/src/agent.ts +47 -0
  97. package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
  98. package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
  99. package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
  100. package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
  101. package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
  102. package/examples/kitchen-sink/src/attachments/download.ts +49 -0
  103. package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
  104. package/examples/kitchen-sink/src/attachments/store.ts +58 -0
  105. package/examples/kitchen-sink/src/browser.ts +33 -0
  106. package/examples/kitchen-sink/src/cli.ts +85 -0
  107. package/examples/kitchen-sink/src/config.ts +18 -0
  108. package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
  109. package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
  110. package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
  111. package/examples/kitchen-sink/src/identity/directory.ts +52 -0
  112. package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
  113. package/examples/kitchen-sink/src/identity/members.ts +13 -0
  114. package/examples/kitchen-sink/src/identity/quota.ts +62 -0
  115. package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
  116. package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
  117. package/examples/kitchen-sink/src/issues/guide.ts +37 -0
  118. package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
  119. package/examples/kitchen-sink/src/issues/store.ts +96 -0
  120. package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
  121. package/examples/kitchen-sink/src/memory.ts +30 -0
  122. package/examples/kitchen-sink/src/ops.ts +42 -0
  123. package/examples/kitchen-sink/src/request-policy.ts +63 -0
  124. package/examples/kitchen-sink/src/routes.ts +120 -0
  125. package/examples/kitchen-sink/src/sdk.ts +46 -0
  126. package/examples/kitchen-sink/src/seed.ts +65 -0
  127. package/examples/kitchen-sink/src/server.ts +62 -0
  128. package/examples/kitchen-sink/src/stdio.ts +34 -0
  129. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
  130. package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
  131. package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
  132. package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
  133. package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
  134. package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
  135. package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
  136. package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
  137. package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
  138. package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
  139. package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
  140. package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
  141. package/examples/kitchen-sink/test/routes.test.ts +123 -0
  142. package/examples/kitchen-sink/test/support/scripted.ts +40 -0
  143. package/examples/live-rpc/README.md +107 -0
  144. package/examples/live-rpc/src/Rpc.ts +8 -0
  145. package/examples/live-rpc/src/browser.ts +98 -0
  146. package/examples/live-rpc/src/chat/Chat.ts +75 -0
  147. package/examples/live-rpc/src/chat/authorize.ts +16 -0
  148. package/examples/live-rpc/src/chat/handlers.ts +50 -0
  149. package/examples/live-rpc/src/chat/room.ts +23 -0
  150. package/examples/live-rpc/src/chat/store.ts +54 -0
  151. package/examples/live-rpc/src/client.ts +37 -0
  152. package/examples/live-rpc/src/identity/Identity.ts +19 -0
  153. package/examples/live-rpc/src/identity/members.ts +8 -0
  154. package/examples/live-rpc/src/identity/verifier.ts +23 -0
  155. package/examples/live-rpc/src/routes.ts +22 -0
  156. package/examples/live-rpc/src/server.ts +43 -0
  157. package/examples/live-rpc/test/browser.test.ts +105 -0
  158. package/examples/live-rpc/test/chat/chat.test.ts +69 -0
  159. package/examples/live-rpc/test/routes.test.ts +99 -0
  160. package/examples/live-rpc/test/server.test.ts +75 -0
  161. package/examples/mcp-tools/README.md +136 -0
  162. package/examples/mcp-tools/src/instructions.ts +12 -0
  163. package/examples/mcp-tools/src/printers/Printers.ts +119 -0
  164. package/examples/mcp-tools/src/printers/fake.ts +115 -0
  165. package/examples/mcp-tools/src/printers/farm.ts +136 -0
  166. package/examples/mcp-tools/src/printers/handlers.ts +29 -0
  167. package/examples/mcp-tools/src/routes.ts +52 -0
  168. package/examples/mcp-tools/src/server.ts +30 -0
  169. package/examples/mcp-tools/src/stdio.ts +26 -0
  170. package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
  171. package/examples/mcp-tools/test/routes.test.ts +202 -0
  172. package/examples/notes-service/README.md +108 -0
  173. package/examples/notes-service/src/Api.ts +13 -0
  174. package/examples/notes-service/src/browser.ts +41 -0
  175. package/examples/notes-service/src/cli.ts +22 -0
  176. package/examples/notes-service/src/commands.ts +89 -0
  177. package/examples/notes-service/src/identity/Identity.ts +31 -0
  178. package/examples/notes-service/src/identity/authorize.ts +18 -0
  179. package/examples/notes-service/src/identity/handlers.ts +10 -0
  180. package/examples/notes-service/src/identity/users.ts +9 -0
  181. package/examples/notes-service/src/identity/verifier.ts +36 -0
  182. package/examples/notes-service/src/notes/Notes.ts +91 -0
  183. package/examples/notes-service/src/notes/handlers.ts +33 -0
  184. package/examples/notes-service/src/notes/store.ts +73 -0
  185. package/examples/notes-service/src/routes.ts +45 -0
  186. package/examples/notes-service/src/server.ts +26 -0
  187. package/examples/notes-service/src/status/Status.ts +10 -0
  188. package/examples/notes-service/src/status/handlers.ts +14 -0
  189. package/examples/notes-service/src/stdio.ts +25 -0
  190. package/examples/notes-service/test/commands.test.ts +134 -0
  191. package/examples/notes-service/test/notes/notes.test.ts +103 -0
  192. package/examples/notes-service/test/routes.test.ts +84 -0
  193. package/examples/quickstart/README.md +31 -0
  194. package/examples/quickstart/src/Api.ts +5 -0
  195. package/examples/quickstart/src/client.ts +13 -0
  196. package/examples/quickstart/src/greeting/Greeting.ts +12 -0
  197. package/examples/quickstart/src/greeting/handlers.ts +6 -0
  198. package/examples/quickstart/src/routes.ts +11 -0
  199. package/examples/quickstart/src/server.ts +16 -0
  200. package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
  201. package/package.json +32 -14
  202. package/dist/internal/actions.d.mts +0 -9
  203. package/dist/internal/authentication.mjs +0 -11
  204. package/dist/internal/call.mjs +0 -12
  205. package/dist/internal/cli.mjs +0 -328
  206. package/dist/internal/client.mjs +0 -25
  207. package/dist/internal/console.mjs +0 -77
  208. package/dist/internal/declared.d.mts +0 -13
  209. package/dist/internal/declared.mjs +0 -34
  210. package/dist/internal/mcp.d.mts +0 -11
  211. package/dist/internal/mcp.mjs +0 -52
  212. package/dist/internal/refusal.mjs +0 -67
  213. package/dist/internal/schema-errors.mjs +0 -20
  214. package/dist/internal/tools.mjs +0 -44
@@ -1,13 +1,32 @@
1
- import { BuiltIn } from "./internal/errors.mjs";
2
- import { ActionOf, BuildError, BuildServices, Holding, Known, Member, RequestOf, Selected, SelectedOf, Selection, Served } from "./internal/implementation.mjs";
3
- import { Action_d_exports, Any } from "./Action.mjs";
4
- import { VerifierError } from "./internal/authentication.mjs";
5
- import { Options, UserError } from "./internal/cli.mjs";
6
- import { AnyHttp, Options as Options$2 } from "./internal/client.mjs";
1
+ import { BuiltIn } from "./contract/errors.mjs";
2
+ import { ActionOf, BuildError, BuildServices, Holding, Known, Member, RequestOf, Selected, SelectedOf, Selection, Served } from "./contract/implementation.mjs";
3
+ import { Action_d_exports, Any, Errors } from "./Action.mjs";
4
+ import { VerifierError } from "./authentication/provider.mjs";
5
+ import { Options, UserError } from "./cli/commands.mjs";
6
+ import { AnyHttp, Options as Options$2 } from "./http/client.mjs";
7
7
  import { Effect, Schema } from "effect";
8
8
  import { HttpClient, HttpClientError } from "effect/http";
9
9
  import { Command } from "effect/cli";
10
- //#region src/ActionCli.d.ts
10
+ //#region src/cli/ActionCli.d.ts
11
+ interface AnyBinding {
12
+ readonly authentication?: {
13
+ readonly error: Errors;
14
+ } | undefined;
15
+ readonly actions: ReadonlyArray<Any>;
16
+ readonly error: Errors;
17
+ }
18
+ /**
19
+ * Make a command of your own fail as a derived command does. Give it what the command calls, the
20
+ * binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and pipe the command's
21
+ * whole Effect through it: a failure that is a declared error, the binding's and its
22
+ * authentication's included, or a built-in one becomes a `UserError` whose message is its JSON, and any other, a transport
23
+ * failure for one, a `UserError` naming it and the causes beneath it. An Effect CLI error, your
24
+ * own `new CliError.UserError({ cause, userMessage })` included, passes through as it is. To
25
+ * add context, such as how far a batch got, print it before this in the pipe, with
26
+ * `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result would
27
+ * replace its JSON.
28
+ */
29
+ export declare const toUserError: (calls: AnyBinding | ReadonlyArray<Any>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, UserError<Exclude<E, import("effect/cli/CliError").CliError>> | Exclude<E, Exclude<E, import("effect/cli/CliError").CliError>>, R>;
11
30
  /**
12
31
  * An aggregate command of the actions `A`: its name, the actions that are its subcommands,
13
32
  * and their options.
@@ -25,33 +44,32 @@ interface Options$1<A extends Any = Any> {
25
44
  * `{ readFile: { positional: ["path"], render } }`.
26
45
  */
27
46
  readonly commands?: { readonly [K in A as K["name"]]?: Options<K>; };
47
+ /**
48
+ * Commands of your own beside the derived ones, as `Command.withSubcommands` takes them:
49
+ * `[importLinks]`, each made with Effect CLI's `Command.make`. One fails as you make it fail:
50
+ * with a `CliError.UserError` for `Command.run` to print its `userMessage` on stderr. Written
51
+ * in the call, or in options held with `satisfies` this type, the list adds its commands'
52
+ * failures and services to the aggregate's type; in options annotated with this type, it adds
53
+ * none.
54
+ */
55
+ readonly subcommands?: Subcommands | undefined;
28
56
  }
29
- /** The `commands` any member of `O` may give. */
57
+ type Subcommands = ReadonlyArray<Command.Command.SubcommandEntry>;
58
+ type OwnCommand<Own extends Subcommands> = Subcommands extends Own ? never : Own[number] extends (infer Entry) ? Entry extends Command.Command<infer _N, infer _I, infer _C, infer _E, infer _R> ? Entry : Entry extends Command.Command.SubcommandGroup<infer Commands> ? Commands[number] : never : never;
30
59
  type CommandsOf<O> = O extends unknown ? "commands" extends keyof O ? NonNullable<O["commands" & keyof O]> : never : never;
31
- /** The keys of any member of `C`. */
32
60
  type KeysOf<C> = C extends unknown ? keyof C : never;
33
- /** The options any member of `C` gives command `K`. */
34
61
  type CommandOf<C, K> = C extends unknown ? (K extends keyof C ? C[K] : never) : never;
35
- /**
36
- * No option, no command beyond the actions `A` of the target, and no command's option, beyond
37
- * `Keys`'s, in any member of `O`.
38
- */
39
62
  type KnownOptions<O, Keys, A extends Any> = Known<O, Keys> & ([CommandsOf<O>] extends [never] ? unknown : {
40
63
  readonly commands?: Known<CommandsOf<O>, { readonly [K in A["name"]]: Options<A>; }> & { readonly [K in KeysOf<CommandsOf<O>>]?: Known<CommandOf<CommandsOf<O>, K>, Options<Any>>; };
41
64
  });
42
- /** The implementation of `A` among `App`. */
43
65
  type Owning<App, A extends Any> = App extends unknown ? A extends ActionOf<App> ? App : never : never;
44
- /**
45
- * What one local command of `A` runs: its implementation's builder and authorization, then
46
- * its handler. Authorization may refuse and any handler fail with a built-in error, so
47
- * every one fails with `Action.BuiltIn` too.
48
- */
49
66
  type Local<App, A extends Any> = Effect.Effect<A["success"]["Type"], A["error"][number]["Type"] | BuildError<App, A> | BuiltIn, RequestOf<App, A> | BuildServices<App, A>>;
50
67
  /**
51
68
  * Project one implemented action into a native Effect CLI command that runs its handler in
52
69
  * process, behind its implementation's authorization. It is named after the action in kebab
53
70
  * case with one flag per field of its input (`--user-id`), or `--input` taking the whole input
54
- * as JSON when it is not a struct. It needs what the authorization, its handler and its builder
71
+ * as JSON when it is not a struct. The flag of an array of strings or numbers is repeated for each value, a boolean
72
+ * field is a switch, `--no-<name>` sending `false` as a required one left out does, and a field's `description` is its flag's help. It needs what the authorization, its handler and its builder
55
73
  * need; the host provides the identity. It prints the result on stdout; a failure is a
56
74
  * `UserError`, which `Command.run` prints on stderr. `remoteCommand` calls a server instead.
57
75
  */
@@ -75,24 +93,38 @@ export type RemoteCommandOptions<A extends Any = Any> = Options<A> & {
75
93
  export type RemoteOptions<A extends Any = Any> = Options$1<A> & {
76
94
  /**
77
95
  * Every subcommand's client options, as `ActionHttp.client` takes them: `baseUrl`, and
78
- * `transformClient` for credentials. They reach this aggregate's requests alone.
96
+ * `transformClient` for credentials. They reach this aggregate's requests alone. A URL or a
97
+ * token read from `Config` goes instead into the `HttpClient` provided on the command, with
98
+ * `Command.provideEffect(HttpClient.HttpClient, …)`: it is read when a command runs, so
99
+ * `--help` needs neither, and a command of your own in `subcommands` calls through it too.
100
+ * A required one that is not set fails that Effect with a `ConfigError`, which is no
101
+ * command's failure and prints as a crash line: map the read,
102
+ * `Effect.mapError((cause) => new CliError.UserError({ cause, userMessage: "TOKEN is not set" }))`.
79
103
  */
80
104
  readonly client?: Options$2;
81
105
  };
82
- /** What an aggregate command over binding `H` fails with, running its actions `A`. */
83
106
  type HttpUserError<H extends AnyHttp, A extends Any> = UserError<A["error"][number]["Type"] | H["error"][number]["Type"] | VerifierError<A, H["authentication"]>["Type"] | BuiltIn | HttpClientError.HttpClientError | Schema.SchemaError>;
84
107
  /**
85
108
  * Project every implemented action as a subcommand of one aggregate command, each named after
86
109
  * its action in kebab case and run in process. `commands` gives a subcommand the options
87
- * `command` takes, by action name. `remote` calls a server instead.
110
+ * `command` takes, by action name; `subcommands` adds commands of your own, which
111
+ * `Command.withSubcommands` on the result would put in the derived ones' place. `remote` calls a
112
+ * server instead. A test runs it in memory, with no process:
113
+ * `Command.runWith(command, { version })(args)`.
114
+ * Guide: `docs/ActionCli.md` in this package; in an application: `examples/notes-service/src/commands.ts`.
88
115
  */
89
- export declare function make<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}>(implementations: Apps, options: O & NoInfer<Options$1<ActionOf<Member<Apps>>>> & NoInfer<KnownOptions<O, Options$1, ActionOf<Member<Apps>>>>): Command.Command<string, {}, {}, UserError<Effect.Error<Local<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>>, Effect.Services<Local<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>>;
116
+ export declare function make<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, const Own extends Subcommands = []>(implementations: Apps, options: O & NoInfer<Options$1<ActionOf<Member<Apps>>>> & NoInfer<KnownOptions<O, Options$1, ActionOf<Member<Apps>>>> & {
117
+ readonly subcommands?: Own | undefined;
118
+ }): Command.Command<string, {}, {}, UserError<Effect.Error<Local<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>> | Command.Error<OwnCommand<Own>>, Effect.Services<Local<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>> | Command.Services<OwnCommand<Own>>>;
90
119
  /**
91
120
  * Project every action of an HTTP binding as a subcommand of one aggregate command, derived as
92
- * `make` derives it, each calling its action over HTTP. `client` configures every
93
- * subcommand's client; `commands` gives a subcommand the options `command` takes.
121
+ * `make` derives it, each calling its action over HTTP. `client` configures every derived
122
+ * subcommand's client; `commands` gives one the options `command` takes; `subcommands` adds
123
+ * commands of your own.
94
124
  */
95
- export declare function remote<const H extends AnyHttp, const O extends Selection<H["actions"][number]> = {}>(binding: H, options: O & NoInfer<RemoteOptions<H["actions"][number]>> & NoInfer<KnownOptions<O, RemoteOptions<Any>, H["actions"][number]>>): Command.Command<string, {}, {}, HttpUserError<H, Selected<O, H["actions"][number]>>, HttpClient.HttpClient>;
125
+ export declare function remote<const H extends AnyHttp, const O extends Selection<H["actions"][number]> = {}, const Own extends Subcommands = []>(binding: H, options: O & NoInfer<RemoteOptions<H["actions"][number]>> & NoInfer<KnownOptions<O, RemoteOptions<Any>, H["actions"][number]>> & {
126
+ readonly subcommands?: Own | undefined;
127
+ }): Command.Command<string, {}, {}, HttpUserError<H, Selected<O, H["actions"][number]>> | Command.Error<OwnCommand<Own>>, HttpClient.HttpClient | Command.Services<OwnCommand<Own>>>;
96
128
  /**
97
129
  * A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
98
130
  * subprocess, applied last, before `NodeRuntime.runMain`: the default logger of every layer
@@ -1,60 +1,60 @@
1
- import { assertDistinct, assertKnown } from "./internal/actions.mjs";
2
- import { Implementation, acquire, assertHeld, select, toList } from "./internal/implementation.mjs";
3
- import { assertInBinding, methods } from "./internal/client.mjs";
4
- import { command as command$1 } from "./internal/cli.mjs";
1
+ import { assertDistinct, assertKnown } from "./contract/rules.mjs";
2
+ import { Implementation, acquire, assertHeld, select, toList } from "./contract/implementation.mjs";
3
+ import { withVerifierErrors } from "./authentication/provider.mjs";
4
+ import { assertInBinding, methods } from "./http/client.mjs";
5
+ import { command as command$1, failingAsDerived } from "./cli/commands.mjs";
5
6
  import { Cause, Effect, Logger, Runtime } from "effect";
6
7
  import { Command } from "effect/cli";
7
- //#region src/ActionCli.ts
8
+ //#region src/cli/ActionCli.ts
8
9
  /**
9
- * Build the implementation's handlers for this call alone, run one action through its
10
- * authorization and handler, and release them: every local command, selected or aggregated.
11
- * The call's own scope, which every call has, closes first, so its finalizers run while the
12
- * builder's resources are still open; a local build never reuses one the host already made.
10
+ * Make a command of your own fail as a derived command does. Give it what the command calls, the
11
+ * binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and pipe the command's
12
+ * whole Effect through it: a failure that is a declared error, the binding's and its
13
+ * authentication's included, or a built-in one becomes a `UserError` whose message is its JSON, and any other, a transport
14
+ * failure for one, a `UserError` naming it and the causes beneath it. An Effect CLI error, your
15
+ * own `new CliError.UserError({ cause, userMessage })` included, passes through as it is. To
16
+ * add context, such as how far a batch got, print it before this in the pipe, with
17
+ * `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result would
18
+ * replace its JSON.
13
19
  */
14
- const local = (app, action, input) => {
15
- const own = Implementation.share([action], app);
16
- return Effect.flatMap(acquire([own]), (bound) => {
20
+ const toUserError = (calls) => "actions" in calls ? failingAsDerived(calls.actions, (action) => endpointErrorsOf(calls, action)) : failingAsDerived(calls, () => []);
21
+ const endpointErrorsOf = (binding, action) => withVerifierErrors(action, binding.error, binding.authentication?.error);
22
+ const runWithBuildOfItsOwn = (app, action, input) => {
23
+ const thisActionOnly = Implementation.share([action], app);
24
+ return Effect.flatMap(acquire([thisActionOnly]), (bound) => {
17
25
  const [, run] = bound.find(([candidate]) => candidate === action) ?? [];
18
26
  return run === void 0 ? Effect.die(/* @__PURE__ */ new Error(`No handler for ${action.name}`)) : run(input);
19
- }).pipe(Effect.provide(Implementation.layerOf(own), { local: true }));
27
+ }).pipe(Effect.provide(Implementation.layerOf(thisActionOnly), { local: true }));
20
28
  };
21
- /**
22
- * The one implementation of `action` among `apps`. One implemented twice is refused rather
23
- * than the first run; other actions' names are not this command's to check.
24
- */
25
- const implementationOf = (apps, action) => {
29
+ const theImplementationOf = (apps, action) => {
26
30
  const [app, ...others] = apps.filter((candidate) => candidate.actions.includes(action));
27
31
  if (app === void 0) throw new Error(`Action "${action.name}" has no implementation here`);
28
32
  if (others.length > 0) throw new Error(`Action "${action.name}" is implemented twice here`);
29
33
  return app;
30
34
  };
31
- /** One command running `action` in process, by its one implementation among `apps`. */
32
35
  const inProcess = (apps, action, options) => {
33
- const app = implementationOf(apps, action);
34
- return command$1(action, (input) => local(app, action, input), options);
36
+ const app = theImplementationOf(apps, action);
37
+ return command$1(action, (input) => runWithBuildOfItsOwn(app, action, input), options);
35
38
  };
36
- /**
37
- * One command calling `action` through the binding's client, made with `client` on the
38
- * host's `HttpClient`, whose failures include the binding's errors.
39
- */
40
39
  const overHttp = (binding, action, options, client) => {
41
40
  assertInBinding(binding.actions, action);
42
- return command$1(action, (input) => Effect.flatMap(methods(binding, client), (methodOf) => methodOf(action)(input)), options, binding.error);
41
+ return command$1(action, (input) => Effect.flatMap(methods(binding, client), (methodOf) => methodOf(action)(input)), options, endpointErrorsOf(binding, action));
43
42
  };
44
- /**
45
- * One aggregate command named `name`, a subcommand per action of `actions`, each made by
46
- * `project` with its options in `commands`: a local aggregate's and a remote one's.
47
- */
48
- const aggregate = (name, actions, commands, project) => {
49
- const subcommands = actions.map((action) => ({
50
- action,
43
+ const aggregateCommand = (name, actions, commands, own, project) => {
44
+ const derived = actions.map((action) => ({
45
+ claimant: `action ${action.name}`,
51
46
  command: project(action, Object.hasOwn(commands, action.name) ? commands[action.name] : void 0)
52
47
  }));
53
- assertDistinct("command", subcommands, ({ command }) => command.name, ({ action }) => `action ${action.name}`);
54
- return Command.make(name).pipe(Command.withSubcommands(subcommands.map(({ command }) => command)));
48
+ assertDistinct("command", [...derived.map(({ claimant, command }) => ({
49
+ claimant,
50
+ name: command.name
51
+ })), ...own.flatMap((entry) => Command.isCommand(entry) ? [entry] : entry.commands).map((command) => ({
52
+ claimant: "subcommands",
53
+ name: command.name
54
+ }))], ({ name: claimed }) => claimed, ({ claimant }) => claimant);
55
+ return Command.make(name).pipe(Command.withSubcommands([...derived.map(({ command }) => command), ...own]));
55
56
  };
56
- /** A command of any action of the target, so one record serves several selections. */
57
- const commandsOf = (commands, actions) => {
57
+ const checkedCommandsOf = (commands, actions) => {
58
58
  const given = commands ?? {};
59
59
  assertKnown("commands", Object.keys(given), actions.map((action) => action.name));
60
60
  return given;
@@ -63,7 +63,8 @@ const commandsOf = (commands, actions) => {
63
63
  * Project one implemented action into a native Effect CLI command that runs its handler in
64
64
  * process, behind its implementation's authorization. It is named after the action in kebab
65
65
  * case with one flag per field of its input (`--user-id`), or `--input` taking the whole input
66
- * as JSON when it is not a struct. It needs what the authorization, its handler and its builder
66
+ * as JSON when it is not a struct. The flag of an array of strings or numbers is repeated for each value, a boolean
67
+ * field is a switch, `--no-<name>` sending `false` as a required one left out does, and a field's `description` is its flag's help. It needs what the authorization, its handler and its builder
67
68
  * need; the host provides the identity. It prints the result on stdout; a failure is a
68
69
  * `UserError`, which `Command.run` prints on stderr. `remoteCommand` calls a server instead.
69
70
  */
@@ -83,24 +84,29 @@ function remoteCommand(binding, action, options) {
83
84
  /**
84
85
  * Project every implemented action as a subcommand of one aggregate command, each named after
85
86
  * its action in kebab case and run in process. `commands` gives a subcommand the options
86
- * `command` takes, by action name. `remote` calls a server instead.
87
+ * `command` takes, by action name; `subcommands` adds commands of your own, which
88
+ * `Command.withSubcommands` on the result would put in the derived ones' place. `remote` calls a
89
+ * server instead. A test runs it in memory, with no process:
90
+ * `Command.runWith(command, { version })(args)`.
91
+ * Guide: `docs/ActionCli.md` in this package; in an application: `examples/notes-service/src/commands.ts`.
87
92
  */
88
93
  function make(implementations, options) {
89
94
  const apps = toList(implementations);
90
- const commands = commandsOf(options.commands, apps.flatMap((app) => app.actions));
95
+ const commands = checkedCommandsOf(options.commands, apps.flatMap((app) => app.actions));
91
96
  const served = select(apps, options.actions);
92
- return aggregate(options.name, served.flatMap((app) => app.actions), commands, (action, syntax) => inProcess(served, action, syntax));
97
+ return aggregateCommand(options.name, served.flatMap((app) => app.actions), commands, options.subcommands ?? [], (action, syntax) => inProcess(served, action, syntax));
93
98
  }
94
99
  /**
95
100
  * Project every action of an HTTP binding as a subcommand of one aggregate command, derived as
96
- * `make` derives it, each calling its action over HTTP. `client` configures every
97
- * subcommand's client; `commands` gives a subcommand the options `command` takes.
101
+ * `make` derives it, each calling its action over HTTP. `client` configures every derived
102
+ * subcommand's client; `commands` gives one the options `command` takes; `subcommands` adds
103
+ * commands of your own.
98
104
  */
99
105
  function remote(binding, options) {
100
106
  const listed = options.actions;
101
107
  if (listed !== void 0) assertHeld("the binding does not hold it", listed, binding.actions);
102
- const commands = commandsOf(options.commands, binding.actions);
103
- return aggregate(options.name, binding.actions.filter((action) => listed?.includes(action) ?? true), commands, (action, syntax) => overHttp(binding, action, syntax, options.client));
108
+ const commands = checkedCommandsOf(options.commands, binding.actions);
109
+ return aggregateCommand(options.name, binding.actions.filter((action) => listed?.includes(action) ?? true), commands, options.subcommands ?? [], (action, syntax) => overHttp(binding, action, syntax, options.client));
104
110
  }
105
111
  /**
106
112
  * A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
@@ -114,11 +120,11 @@ function remote(binding, options) {
114
120
  const logToStderr = (self) => self.pipe(Effect.catchCause((cause) => {
115
121
  if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt;
116
122
  const squashed = Cause.squash(cause);
117
- const reported = Object.assign(new Error("Reported on stderr", { cause }), {
123
+ const reportedDefectKeepingCause = Object.assign(new Error("Reported on stderr", { cause }), {
118
124
  [Runtime.errorReported]: false,
119
125
  [Runtime.errorExitCode]: Runtime.getErrorExitCode(squashed)
120
126
  });
121
- return Effect.andThen(Runtime.getErrorReported(squashed) ? Effect.logError(cause) : Effect.void, Effect.die(reported));
127
+ return Effect.andThen(Runtime.getErrorReported(squashed) ? Effect.logError(cause) : Effect.void, Effect.die(reportedDefectKeepingCause));
122
128
  }), Effect.provideService(Logger.LogToStderr, true));
123
129
  //#endregion
124
- export { command, logToStderr, make, remote, remoteCommand };
130
+ export { command, logToStderr, make, remote, remoteCommand, toUserError };
@@ -1,14 +1,13 @@
1
- import { Anyone } from "./internal/actions.mjs";
2
- import { BuiltIn, BuiltIns } from "./internal/errors.mjs";
3
- import { BuildError, BuildServices, Holding, Known, Member, Protected, Selected, Served, Serving } from "./internal/implementation.mjs";
1
+ import { Anyone, Certain, ErrorsOf } from "./contract/rules.mjs";
2
+ import { BuiltIn, BuiltIns } from "./contract/errors.mjs";
3
+ import { BuildError, Holding, Known, Member, MiddlewareOf, Protected, Selected, Served, Serving } from "./contract/implementation.mjs";
4
4
  import { Action_d_exports, Any, Errors } from "./Action.mjs";
5
- import { Any as Any$1, Identity, Matching, RemoteRequest, Required, ServedProvider, VerifierError } from "./internal/authentication.mjs";
6
- import { AnyHttp, Client, MethodError, Options } from "./internal/client.mjs";
7
- import { ErrorsOf } from "./internal/declared.mjs";
5
+ import { Any as Any$1, DescriptorOf, Identity, Matching, PublicOnly, RemoteRequest, Required, ServedProvider, ServerBuildServices, VerifierError } from "./authentication/provider.mjs";
6
+ import { AnyHttp, Client, MethodError, Options } from "./http/client.mjs";
8
7
  import { Context, Effect, FileSystem, Layer, Path, Schema, Types } from "effect";
9
8
  import { Etag, HttpClient, HttpPlatform, HttpRouter } from "effect/http";
10
9
  import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "effect/http-api";
11
- //#region src/ActionHttp.d.ts
10
+ //#region src/http/ActionHttp.d.ts
12
11
  /** What `fetchClient` takes: `client`'s options, and the `fetch` its calls send with. */
13
12
  export interface FetchClientOptions extends Options {
14
13
  /**
@@ -17,49 +16,13 @@ export interface FetchClientOptions extends Options {
17
16
  */
18
17
  readonly fetch?: typeof globalThis.fetch | undefined;
19
18
  }
20
- /** Native endpoint middleware, as `HttpApiMiddleware.Service` declares it. */
21
19
  type Middleware = ReadonlyArray<Context.Key<HttpApiMiddleware.AnyId, unknown>>;
22
- /** The identifier a middleware's service key declares. */
23
20
  type IdOf<K> = K extends Context.Key<infer I extends HttpApiMiddleware.AnyId, unknown> ? I : never;
24
- /**
25
- * What the request owes past the middleware `M`, the first innermost, around what `R` owes:
26
- * each removes what it provides and adds what it requires, as native endpoints apply them.
27
- * An array of unknown length, which may hold none of them, provides nothing and owes what
28
- * any of them requires; so does a slot that may hold one of several, as only one runs.
29
- */
30
21
  type Through<M extends Middleware, R> = number extends M["length"] ? R | HttpApiMiddleware.Requires<IdOf<M[number]>> : M extends readonly [infer K, ...infer Rest extends Middleware] ? Through<Rest, true extends Types.IsUnion<IdOf<K>> ? R | HttpApiMiddleware.Requires<IdOf<K>> : HttpApiMiddleware.ApplyServices<IdOf<K>, R>> : R;
31
- /**
32
- * The middleware options `O` install: its `middleware` when always given, an array of unknown
33
- * length when it may be absent, which installs none or all of them, and none otherwise.
34
- */
35
- type MiddlewareOf<O> = O extends {
36
- readonly middleware: infer M extends Middleware;
37
- } ? M : O extends {
38
- readonly middleware?: infer M extends Middleware | undefined;
39
- } ? "middleware" extends keyof O ? ReadonlyArray<NonNullable<M>[number]> : [] : [];
40
- /**
41
- * The layer's middleware, refused when one fails with an error neither the binding, `E`, nor
42
- * every endpoint declares, or needs a client counterpart: neither reaches the binding's
43
- * clients, which decode only what the binding and the built-ins declare. A failure an
44
- * action's callers see after decoding is its handler's, declared on the contract and decoded
45
- * by every client.
46
- */
47
22
  type ServerOnly<M extends Middleware, E extends Errors> = [Exclude<HttpApiMiddleware.Error<IdOf<M[number]>>, Extract<Certain<E>, Errors[number]>["Type"] | BuiltIn> | HttpApiMiddleware.MiddlewareClient<IdOf<M[number]>>] extends [never] ? unknown : {
48
23
  readonly "Layer middleware fails only with the binding's errors and needs no client": never;
49
24
  };
50
- /** `T` where it is one type, not a union: what a list's slot of type `T` surely holds. */
51
- type Single<T> = true extends Types.IsUnion<T> ? never : T;
52
- /**
53
- * The errors a binding of errors `E` surely declares: each slot of one error in a list of
54
- * fixed length, never one of an array of unknown length, which may be empty, nor of a list
55
- * `E` may be one of several.
56
- */
57
- type Certain<E extends Errors> = true extends Types.IsUnion<E> ? never : number extends E["length"] ? never : { readonly [K in keyof E]: Single<E[K]>; }[number];
58
- /**
59
- * The layer serving the actions `A` of `Apps` through the binding `H`, behind the middleware
60
- * `M`: an implementation holding none of them is not built.
61
- */
62
- type HttpLayer<H extends AnyHttp, Apps extends Served, M extends Middleware, A extends Any = H["actions"][number]> = Layer.Layer<never, BuildError<Holding<Member<Apps>, A>, A>, BuildServices<Holding<Member<Apps>, A>, A> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", LayerRequest<Member<Apps>, A, M>> | ServedProvider<Member<Apps>, A, H extends {
25
+ type HttpLayer<H extends AnyHttp, Apps extends Served, M extends Middleware, A extends Any = H["actions"][number]> = Layer.Layer<never, BuildError<Holding<Member<Apps>, A>, A>, ServerBuildServices<Holding<Member<Apps>, A>, A> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", LayerRequest<Member<Apps>, A, M>> | ServedProvider<Member<Apps>, A, H extends {
63
26
  readonly authentication: infer D;
64
27
  } ? D : never> | IdOf<M[number]> | Etag.Generator | FileSystem.FileSystem | HttpPlatform.HttpPlatform | Path.Path>;
65
28
  /** `ActionHttp.layer`'s options. */
@@ -77,20 +40,9 @@ export interface LayerOptions<M extends Middleware = [], A extends Any = never>
77
40
  */
78
41
  readonly middleware?: M;
79
42
  }
80
- /** What the layer serving `A` of `App` behind middleware `M` owes per request. */
81
43
  type LayerRequest<App, A extends Any, M extends Middleware> = Exclude<Through<M, RemoteRequest<App, A>>, HttpRouter.Provided | ([Extract<Serving<App, A>, {
82
44
  readonly caller: typeof Anyone;
83
45
  }>] extends [never] ? Identity<Protected<Serving<App, A>>> : never)>;
84
- /** Actions a binding takes without options: public ones, as a protected one names its descriptor. */
85
- type PublicOnly<Actions extends ReadonlyArray<Any>> = [Exclude<Actions[number], {
86
- readonly caller: typeof Anyone;
87
- }>] extends [never] ? unknown : {
88
- readonly "Protected actions take options naming their authentication": never;
89
- };
90
- /** The descriptor options `O` name, where they always name one. */
91
- type DescriptorOf<O> = [O] extends [{
92
- readonly authentication: infer D extends Any$1;
93
- }] ? D : undefined;
94
46
  /** Contract-level configuration shared by servers and clients. */
95
47
  interface Options$1<E extends Errors = Errors> {
96
48
  readonly prefix?: `/${string}`;
@@ -99,11 +51,6 @@ interface Options$1<E extends Errors = Errors> {
99
51
  readonly authentication?: Any$1;
100
52
  }
101
53
  type Endpoint<A extends Any, E extends Errors, D> = A extends Any ? HttpApiEndpoint.HttpApiEndpoint<A["name"], "POST", `/${string}`, never, never, Schema.toCodecJson<A["input"]>, never, Schema.toCodecJson<A["success"]>, Schema.toCodecJson<A["error"][number] | E[number] | VerifierError<A, D> | BuiltIns>, never> : never;
102
- /**
103
- * The native `HttpApi` of `Actions`: one top-level group, so native client methods are
104
- * not nested, named after the binding's mount path, `/` at the root, so bindings composed
105
- * into one host API keep their own groups.
106
- */
107
54
  type Api<Actions extends ReadonlyArray<Any>, E extends Errors, D> = HttpApi.HttpApi<"actions", HttpApiGroup.HttpApiGroup<string, Endpoint<Actions[number], E, D>, true>>;
108
55
  /**
109
56
  * An HTTP binding: actions, the errors every endpoint declares, and where they are mounted.
@@ -122,11 +69,12 @@ export interface Binding<Actions extends ReadonlyArray<Any>, E extends Errors =
122
69
  }
123
70
  /**
124
71
  * Bind actions once, for servers and clients alike, each at `POST <prefix>/<action>`.
125
- * Every endpoint declares its action's errors, the binding's `errors`, and the built-in
72
+ * Every endpoint declares its action's errors, the binding's `error`, and the built-in
126
73
  * `InvalidInput`, `Unauthenticated` and `Forbidden`, so clients decode each as a typed
127
74
  * failure. A union's own `httpApiStatus` applies to every member; otherwise, each member
128
75
  * uses its own status, or 422 without one. Protected contracts receive native endpoint
129
76
  * security middleware: the same descriptor documents their credentials and enforces them.
77
+ * Guide: `docs/ActionHttp.md` in this package; in an application: `examples/notes-service/src/routes.ts`.
130
78
  */
131
79
  export declare function make<const Actions extends ReadonlyArray<Any & {
132
80
  readonly caller: typeof Anyone;
@@ -149,22 +97,25 @@ export declare function make<const Actions extends ReadonlyArray<Any>>(actions:
149
97
  * need, until its `middleware` or middleware provided around it provides them, the identity
150
98
  * of a protected action excepted, which its authentication provides; the router provides
151
99
  * its own, such as the request, to every route. Its routes take only requests typed as JSON,
152
- * and what middleware provides per request wins over what the layer was built with.
100
+ * and what middleware provides per request wins over what the layer was built with. A request
101
+ * body is read whole, however large, unless the host limits it where the routes are served:
102
+ * `Layer.provide(Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1)))`.
153
103
  */
154
104
  export declare function layer<const H extends AnyHttp, const Apps extends Served>(binding: H, implementations: Apps): HttpLayer<H, Apps, []>;
155
- export declare function layer<const H extends AnyHttp, const Apps extends Served, const O extends LayerOptions<Middleware, H["actions"][number]>>(binding: H, implementations: Apps, options: O & ServerOnly<MiddlewareOf<O>, H["error"]> & NoInfer<Known<O, LayerOptions<Middleware>>>): HttpLayer<H, Apps, MiddlewareOf<O>, Selected<O, H["actions"][number]>>;
105
+ export declare function layer<const H extends AnyHttp, const Apps extends Served, const O extends LayerOptions<Middleware, H["actions"][number]>>(binding: H, implementations: Apps, options: O & ServerOnly<MiddlewareOf<O, Middleware>, H["error"]> & NoInfer<Known<O, LayerOptions<Middleware>>>): HttpLayer<H, Apps, MiddlewareOf<O, Middleware>, Selected<O, H["actions"][number]>>;
156
106
  /**
157
107
  * Effect's native `HttpApiClient` for a binding, one method per action taking the
158
108
  * action's input directly: `client.greet({ name })`. The argument may be omitted when `{}`
159
109
  * is a valid input. Requires the native `HttpClient`, as `HttpApiClient.make` does.
160
- * The options are the native ones, `baseUrl` and `transformClient`. The native client
161
- * itself stays available: `HttpApiClient.make(Http.api)`.
110
+ * The options are the native ones, `baseUrl` and `transformClient`, a bearer token being
111
+ * `transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. The native
112
+ * client itself stays available: `HttpApiClient.make(Http.api)`.
162
113
  *
163
114
  * A call fails with a declared error value (the action's own, or a built-in
164
115
  * `InvalidInput`, `Unauthenticated` or `Forbidden`), a native `HttpClientError` when the
165
116
  * server could not be reached or answered with a status or body the contract does not
166
- * declare, or a `SchemaError` when the input does not encode or the success does not
167
- * decode.
117
+ * declare, or a `SchemaError` when the success does not decode. An input that does not encode
118
+ * fails with `InvalidInput`, and nothing is sent.
168
119
  */
169
120
  export declare function client<const H extends AnyHttp>(binding: H, options?: Options): Effect.Effect<Client<H>, never, HttpClient.HttpClient>;
170
121
  /**
@@ -1,61 +1,27 @@
1
- import { Anyone, assertDistinct, assertOnce, assertOwnTags, errorList, projectedErrors } from "./internal/actions.mjs";
2
- import { acquire, assertHeld, provideHandlers, select, toList } from "./internal/implementation.mjs";
3
- import { assertAuthentication } from "./internal/authentication.mjs";
4
- import { declared } from "./internal/declared.mjs";
5
- import { methods } from "./internal/client.mjs";
6
- import { SchemaErrors, schemaErrors } from "./internal/schema-errors.mjs";
1
+ import { Anyone, assertDistinct, assertOnce, assertOwnTags, errorList, projectedErrors } from "./contract/rules.mjs";
2
+ import { acquire, provideHandlers, servedBy, toList } from "./contract/implementation.mjs";
3
+ import { assertAuthentication, providerOf, withVerifierErrors } from "./authentication/provider.mjs";
4
+ import { declared } from "./http/declared.mjs";
5
+ import { methods } from "./http/client.mjs";
6
+ import { SchemaErrors, schemaErrors } from "./http/schema-errors.mjs";
7
7
  import { Context, Effect, Layer, Scope } from "effect";
8
8
  import { FetchHttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
9
9
  import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api";
10
- //#region src/ActionHttp.ts
11
- /** The mount path's segments: `api` by default; none for `/`. */
10
+ //#region src/http/ActionHttp.ts
12
11
  const mountSegments = (prefix) => (prefix ?? "/api").split("/").filter((segment) => segment !== "");
13
- /** An absolute route from path segments. */
14
- const route = (segments) => `/${segments.join("/")}`;
15
- /**
16
- * The name of a binding's one native group: its OpenAPI tag, by which the native `addHttpApi`
17
- * keys groups. It is the mount path, `/` at the root, which no segment contains.
18
- */
12
+ const absolutePath = (segments) => `/${segments.join("/")}`;
19
13
  const groupName = (mount) => mount.join("/") || "/";
20
- /** A native API of one top-level group of `endpoints`. */
21
14
  const apiOf = (group, endpoints) => {
22
15
  const empty = HttpApiGroup.make(group, { topLevel: true });
23
16
  const [first, ...rest] = endpoints;
24
17
  return HttpApi.make("actions").annotate(HttpApi.ParseOptions, { errors: "all" }).add(first === void 0 ? empty : empty.add(first, ...rest));
25
18
  };
26
- /**
27
- * `apps` narrowed to the actions the layer serves, matched by identity: those `listed`, which
28
- * the binding must hold, or every one of the binding's they hold, each once. An
29
- * implementation holding none of them is left out, and not built. Implementations holding
30
- * none of the binding's actions are refused, as the wrong implementations or the wrong
31
- * binding; a name the binding holds is marked as another contract's, as a second copy of the
32
- * contracts module makes one.
33
- */
34
- const servedBy = (binding, apps, listed) => {
35
- if (listed !== void 0) assertHeld("the binding does not hold it", listed, binding.actions);
36
- const held = apps.flatMap((app) => app.actions);
37
- const served = select(apps, listed ?? binding.actions.filter((action) => held.includes(action)));
38
- const actions = served.flatMap((app) => app.actions);
39
- if (actions.length === 0 && listed === void 0 && apps.length > 0) {
40
- const bound = new Set(binding.actions.map(({ name }) => name));
41
- throw new Error(`No action of these implementations is in this HTTP binding: ${held.map(({ name }) => bound.has(name) ? `${name} (another contract)` : name).join(", ") || "none"}`);
42
- }
43
- assertOnce("served action", actions);
44
- return served;
45
- };
46
- /**
47
- * The native endpoint of `action` at `path`, declaring its action's errors, the binding's
48
- * `errors` and the built-in ones: what `make` documents and `layer` serves. Its payload is
49
- * JSON whatever encoding the input is annotated with, which no other surface reads either:
50
- * `HttpApi` would take a form or text body for it, which a page sends without a preflight.
51
- */
52
19
  const endpointOf = (action, path, errors) => HttpApiEndpoint.post(action.name, path, {
53
20
  payload: action.input.pipe(HttpApiSchema.asJson()),
54
21
  success: action.success,
55
22
  error: projectedErrors(action, errors).flatMap(declared)
56
23
  });
57
- /** An endpoint wrapped in a layer's middleware, the first innermost. */
58
- const within = (endpoint, middleware = []) => middleware.reduce((inner, key) => inner.middleware(key), endpoint);
24
+ const withinLayerMiddleware = (endpoint, middleware = []) => middleware.reduce((inner, key) => inner.middleware(key), endpoint);
59
25
  function make(actions, options = {}) {
60
26
  assertOnce("action", actions);
61
27
  const errors = errorList(options.error);
@@ -63,52 +29,37 @@ function make(actions, options = {}) {
63
29
  assertAuthentication(actions, options.authentication);
64
30
  const mount = mountSegments(options.prefix);
65
31
  const security = options.authentication?.["~middleware"];
66
- const verifierErrors = options.authentication?.error ?? [];
67
32
  const endpoints = actions.map((action) => {
68
33
  const isPublic = security === void 0 || action.caller === Anyone;
69
- const endpoint = endpointOf(action, route([...mount, action.name]), isPublic ? errors : [...errors, ...verifierErrors]).annotate(OpenApi.Description, action.description);
34
+ const endpoint = endpointOf(action, absolutePath([...mount, action.name]), withVerifierErrors(action, errors, options.authentication?.error)).annotate(OpenApi.Description, action.description);
70
35
  return isPublic ? endpoint : endpoint.middleware(security);
71
36
  });
72
37
  const api = apiOf(groupName(mount), endpoints);
73
38
  return {
74
39
  actions,
75
40
  error: errors,
76
- prefix: route(mount),
41
+ prefix: absolutePath(mount),
77
42
  api,
78
43
  authentication: options.authentication
79
44
  };
80
45
  }
81
- /**
82
- * `group`, built in an empty context rather than the one around it: `HttpApiBuilder` lays
83
- * the context a group is built in over every request's, where a startup value would replace
84
- * what middleware provides for the request. `entry` puts that context beneath it instead.
85
- */
86
- const buildAlone = (group) => Layer.fromBuild((memoMap, scope) => Effect.setContext(Layer.buildWithMemoMap(group, memoMap, scope), Context.empty()));
87
- /**
88
- * Restore startup dependencies beneath request context, so a caller's services win.
89
- */
90
- const entry = () => HttpRouter.middleware(Effect.map(Effect.context(), (built) => {
46
+ const buildInEmptyContext = (group) => Layer.fromBuild((memoMap, scope) => Effect.setContext(Layer.buildWithMemoMap(group, memoMap, scope), Context.empty()));
47
+ const startupBeneathRequest = () => HttpRouter.middleware(Effect.map(Effect.context(), (built) => {
91
48
  const startup = Context.omit(Scope.Scope)(built);
92
49
  return (route) => Effect.updateContext(route, (current) => Context.merge(startup, current));
93
50
  })).layer;
94
- /**
95
- * Answers a protected route's step-up refusal, as it leaves the layer's middleware, with its
96
- * challenge: the provider's `stepUp`, inside the authentication.
97
- */
98
51
  var StepUp = class extends HttpApiMiddleware.Service()("effect-actions/ActionHttp/StepUp") {};
99
- /** Require JSON media typing after endpoint authentication, before payload decoding. */
100
52
  var JsonContentType = class extends HttpApiMiddleware.Service()("effect-actions/ActionHttp/JsonContentType") {};
101
- /** `JsonContentType`'s server side, built by `layer` alone, so a client's bundle leaves it out. */
102
53
  const jsonContentType = () => Layer.succeed(JsonContentType, (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.headers["content-type"] === void 0 ? Effect.succeed(HttpServerResponse.text("Unsupported content-type: none", { status: 415 })) : route));
103
54
  function layer(binding, served, options = {}) {
104
- const apps = servedBy(binding, toList(served), options.actions);
55
+ const apps = servedBy("HTTP binding", binding.actions, toList(served), options.actions);
105
56
  const actions = apps.flatMap((app) => app.actions);
106
57
  assertAuthentication(actions, binding.authentication);
107
58
  assertDistinct("middleware", options.middleware ?? [], (key) => key.key);
108
59
  const mount = mountSegments(binding.prefix);
109
60
  const name = groupName(mount);
110
61
  const api = apiOf(name, actions.map((action) => {
111
- const endpoint = within(endpointOf(action, route([...mount, action.name]), binding.error).middleware(JsonContentType).middleware(SchemaErrors), options.middleware);
62
+ const endpoint = withinLayerMiddleware(endpointOf(action, absolutePath([...mount, action.name]), binding.error).middleware(JsonContentType).middleware(SchemaErrors), options.middleware);
112
63
  return action.caller === Anyone || binding.authentication === void 0 ? endpoint : endpoint.middleware(StepUp).middleware(binding.authentication["~middleware"]);
113
64
  })).annotate(HttpApi.PayloadParseOptions, {
114
65
  errors: "all",
@@ -117,19 +68,18 @@ function layer(binding, served, options = {}) {
117
68
  const handlers = Layer.unwrap(Effect.gen(function* () {
118
69
  const bound = yield* acquire(apps);
119
70
  const auth = actions.some((action) => action.caller !== Anyone) ? binding.authentication : void 0;
120
- const authentication = auth === void 0 ? Layer.empty : yield* Effect.flatMap(auth["~provider"], (provider) => provider.descriptor === auth ? Effect.succeed(Layer.mergeAll(Layer.succeed(auth["~middleware"], provider.middleware), Layer.succeed(StepUp, provider.stepUp))) : Effect.die(/* @__PURE__ */ new Error(`Authentication "${auth.name}": the binding's descriptor is not its provider's; build both from one descriptor`)));
121
- const middleware = yield* Effect.forEach(options.middleware ?? [], (key) => Effect.map(Effect.service(key), (service) => Layer.succeed(key, service)));
71
+ const authentication = auth === void 0 ? Layer.empty : yield* Effect.map(providerOf(auth), (provider) => Layer.mergeAll(Layer.succeed(auth["~middleware"], provider.middleware), Layer.succeed(StepUp, provider.stepUp)));
72
+ const hostProvidedMiddleware = yield* Effect.forEach(options.middleware ?? [], (key) => Effect.map(Effect.service(key), (service) => Layer.succeed(key, service)));
122
73
  const byName = Object.fromEntries(bound.map(([action, run]) => [action.name, (request) => run(request.payload)]));
123
74
  return HttpApiBuilder.group(api, name, (builder) => builder.handleAll(byName)).pipe(Layer.provide([
124
75
  schemaErrors(),
125
76
  jsonContentType(),
126
77
  authentication,
127
- ...middleware
128
- ]), buildAlone);
78
+ ...hostProvidedMiddleware
79
+ ]), buildInEmptyContext);
129
80
  })).pipe(provideHandlers(apps));
130
- return HttpApiBuilder.layer(api).pipe(Layer.provide(handlers), Layer.provide(entry()));
81
+ return HttpApiBuilder.layer(api).pipe(Layer.provide(handlers), Layer.provide(startupBeneathRequest()));
131
82
  }
132
- /** A binding's client, erased: `client` and `fetchClient` restore its exact type. */
133
83
  const erasedClient = (binding, options) => Effect.map(methods(binding, options), (methodOf) => Object.fromEntries(binding.actions.map((action) => [action.name, methodOf(action)])));
134
84
  function client(binding, options) {
135
85
  return erasedClient(binding, options);