@gjermundgaraba/effect-actions 0.18.2 → 0.20.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 (155) hide show
  1. package/README.md +20 -47
  2. package/dist/Action.d.mts +96 -49
  3. package/dist/Action.mjs +12 -16
  4. package/dist/ActionCli.d.mts +82 -44
  5. package/dist/ActionCli.mjs +54 -35
  6. package/dist/ActionHttp.d.mts +84 -45
  7. package/dist/ActionHttp.mjs +3 -4
  8. package/dist/ActionMcp.d.mts +54 -29
  9. package/dist/ActionMcp.mjs +33 -15
  10. package/dist/ActionRpc.d.mts +48 -37
  11. package/dist/ActionRpc.mjs +3 -4
  12. package/dist/ActionToolkit.d.mts +32 -15
  13. package/dist/ActionToolkit.mjs +11 -9
  14. package/dist/Authentication.d.mts +92 -42
  15. package/dist/Authentication.mjs +38 -37
  16. package/dist/Testing.d.mts +23 -27
  17. package/dist/Testing.mjs +1 -2
  18. package/dist/authentication/provider.d.mts +1 -6
  19. package/dist/authentication/provider.mjs +2 -3
  20. package/dist/authentication/refusal.mjs +3 -3
  21. package/dist/cli/commands.d.mts +34 -13
  22. package/dist/cli/commands.mjs +48 -24
  23. package/dist/contract/ast.mjs +1 -2
  24. package/dist/contract/call.d.mts +1 -3
  25. package/dist/contract/call.mjs +1 -2
  26. package/dist/contract/caller.d.mts +1 -3
  27. package/dist/contract/caller.mjs +2 -3
  28. package/dist/contract/declared.d.mts +0 -2
  29. package/dist/contract/declared.mjs +28 -16
  30. package/dist/contract/errors.d.mts +42 -19
  31. package/dist/contract/errors.mjs +43 -19
  32. package/dist/contract/hints.mjs +4 -4
  33. package/dist/contract/implementation.d.mts +7 -14
  34. package/dist/contract/implementation.mjs +31 -19
  35. package/dist/contract/media.d.mts +7 -5
  36. package/dist/contract/media.mjs +7 -4
  37. package/dist/contract/names.mjs +15 -14
  38. package/dist/contract/status.mjs +4 -12
  39. package/dist/http/client.d.mts +15 -6
  40. package/dist/http/client.mjs +2 -3
  41. package/dist/http/schema-errors.mjs +1 -2
  42. package/dist/mcp/client.d.mts +21 -10
  43. package/dist/mcp/client.mjs +8 -11
  44. package/dist/mcp/protocol.mjs +1 -2
  45. package/dist/stdio/console.mjs +1 -2
  46. package/dist/testing/memory.d.mts +1 -3
  47. package/dist/testing/memory.mjs +1 -2
  48. package/docs/Action-failures.md +6 -15
  49. package/docs/Action.md +159 -191
  50. package/docs/ActionCli-failures.md +12 -92
  51. package/docs/ActionCli.md +102 -156
  52. package/docs/ActionHttp-failures.md +14 -76
  53. package/docs/ActionHttp.md +150 -176
  54. package/docs/ActionMcp-failures.md +2 -22
  55. package/docs/ActionMcp.md +79 -170
  56. package/docs/ActionRpc-failures.md +3 -12
  57. package/docs/ActionRpc.md +183 -120
  58. package/docs/ActionToolkit-failures.md +3 -10
  59. package/docs/ActionToolkit.md +112 -109
  60. package/docs/Authentication-advanced.md +171 -108
  61. package/docs/Authentication-failures.md +11 -91
  62. package/docs/Authentication.md +114 -135
  63. package/docs/README.md +45 -30
  64. package/docs/Testing-failures.md +2 -6
  65. package/docs/Testing.md +126 -159
  66. package/docs/setup-failures.md +8 -32
  67. package/docs/setup.md +42 -38
  68. package/examples/README.md +6 -10
  69. package/examples/assistant/README.md +20 -38
  70. package/examples/assistant/src/agent.ts +2 -3
  71. package/examples/assistant/src/cli.ts +4 -9
  72. package/examples/assistant/src/commands.ts +6 -4
  73. package/examples/assistant/src/identity/authorize.ts +3 -3
  74. package/examples/assistant/src/tasks/Tasks.ts +1 -2
  75. package/examples/assistant/src/tasks/handlers.ts +3 -3
  76. package/examples/assistant/src/toolkit.ts +6 -8
  77. package/examples/assistant/test/commands.test.ts +16 -11
  78. package/examples/assistant/test/tasks/tasks.test.ts +0 -2
  79. package/examples/assistant/test/toolkit.test.ts +0 -1
  80. package/examples/kitchen-sink/README.md +30 -52
  81. package/examples/kitchen-sink/src/Actions.ts +2 -3
  82. package/examples/kitchen-sink/src/Api.ts +3 -4
  83. package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
  84. package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
  85. package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
  86. package/examples/kitchen-sink/src/agent.ts +1 -1
  87. package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
  88. package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
  89. package/examples/kitchen-sink/src/attachments/download.ts +5 -8
  90. package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
  91. package/examples/kitchen-sink/src/browser.ts +3 -4
  92. package/examples/kitchen-sink/src/cli.ts +6 -7
  93. package/examples/kitchen-sink/src/config.ts +2 -0
  94. package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
  95. package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
  96. package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
  97. package/examples/kitchen-sink/src/identity/quota.ts +1 -1
  98. package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
  99. package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
  100. package/examples/kitchen-sink/src/issues/guide.ts +2 -3
  101. package/examples/kitchen-sink/src/ops.ts +3 -6
  102. package/examples/kitchen-sink/src/request-policy.ts +8 -4
  103. package/examples/kitchen-sink/src/routes.ts +4 -6
  104. package/examples/kitchen-sink/src/seed.ts +0 -1
  105. package/examples/kitchen-sink/src/server.ts +11 -12
  106. package/examples/kitchen-sink/src/stdio.ts +3 -2
  107. package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
  108. package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
  109. package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
  110. package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
  111. package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
  112. package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
  113. package/examples/kitchen-sink/test/request-policy.test.ts +39 -3
  114. package/examples/kitchen-sink/test/routes.test.ts +3 -3
  115. package/examples/live-rpc/README.md +8 -49
  116. package/examples/live-rpc/src/browser.ts +8 -41
  117. package/examples/live-rpc/src/chat/Chat.ts +9 -3
  118. package/examples/live-rpc/src/chat/room.ts +1 -1
  119. package/examples/live-rpc/src/client.ts +2 -3
  120. package/examples/live-rpc/src/page.ts +34 -0
  121. package/examples/live-rpc/src/routes.ts +6 -7
  122. package/examples/live-rpc/src/server.ts +6 -4
  123. package/examples/live-rpc/test/browser.test.ts +6 -5
  124. package/examples/live-rpc/test/routes.test.ts +16 -15
  125. package/examples/live-rpc/test/server.test.ts +1 -1
  126. package/examples/mcp-tools/README.md +22 -45
  127. package/examples/mcp-tools/src/printers/Printers.ts +3 -7
  128. package/examples/mcp-tools/src/routes.ts +0 -7
  129. package/examples/mcp-tools/src/server.ts +1 -1
  130. package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
  131. package/examples/mcp-tools/test/routes.test.ts +2 -3
  132. package/examples/notes-service/README.md +9 -43
  133. package/examples/notes-service/src/cli.ts +0 -3
  134. package/examples/notes-service/src/commands.ts +7 -17
  135. package/examples/notes-service/src/identity/authorize.ts +2 -5
  136. package/examples/notes-service/src/identity/verifier.ts +3 -4
  137. package/examples/notes-service/src/notes/Notes.ts +2 -6
  138. package/examples/notes-service/src/notes/store.ts +5 -1
  139. package/examples/notes-service/src/routes.ts +6 -8
  140. package/examples/notes-service/src/server.ts +4 -3
  141. package/examples/notes-service/src/stdio.ts +4 -3
  142. package/examples/notes-service/test/commands.test.ts +1 -1
  143. package/examples/notes-service/test/notes/as.ts +6 -0
  144. package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
  145. package/examples/notes-service/test/notes/in-process.test.ts +50 -0
  146. package/examples/notes-service/test/notes/notes.test.ts +3 -42
  147. package/examples/notes-service/test/routes.test.ts +2 -2
  148. package/examples/quickstart/README.md +1 -1
  149. package/examples/quickstart/src/client.ts +1 -0
  150. package/examples/quickstart/src/server.ts +2 -4
  151. package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
  152. package/package.json +12 -16
  153. package/docs/Action-in-process.md +0 -93
  154. package/docs/CONTEXT.md +0 -46
  155. package/docs/guarantees.md +0 -44
@@ -8,20 +8,22 @@ import "./ActionHttp.mjs";
8
8
  import { Effect } from "effect";
9
9
  import { HttpClient } from "effect/http";
10
10
  import { CliError, Command } from "effect/cli";
11
- //#region src/cli/ActionCli.d.ts
12
11
  interface AnyBinding extends Declaring {
13
12
  readonly actions: ReadonlyArray<Any>;
14
13
  }
15
14
  /**
16
15
  * Make a command of your own fail as a derived command does. Give it what the command calls, the
17
- * binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and pipe the command's
18
- * whole Effect through it: a failure that is a declared error, the binding's and its
19
- * authentication's included, or a built-in one becomes a `UserError` whose message is its JSON, and any other, a transport
20
- * failure for one, a `UserError` naming it and the causes beneath it. An Effect CLI error, your
21
- * own `new CliError.UserError({ cause, userMessage })` included, passes through as it is. To
22
- * add context, such as how far a batch got, print it before this in the pipe, with
23
- * `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result would
24
- * replace its JSON.
16
+ * binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and
17
+ * pipe the command's whole Effect through it: a failure that is a declared error, the binding's
18
+ * and its authentication's included, or a built-in one becomes a `UserError` whose message is its
19
+ * JSON, and any other, a transport failure for one, a `UserError` naming it and the causes
20
+ * beneath it. An Effect CLI error, your own `new CliError.UserError({ cause, userMessage })`
21
+ * included, passes through as it is.
22
+ *
23
+ * **Gotchas**
24
+ *
25
+ * - A failure left unmapped, one call's included, is reported as a crash, `ERROR (#n)` with a stack trace: pipe the whole Effect through it, once.
26
+ * - To add context, such as how far a batch got, print it before this in the pipe, `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result puts its JSON mid-sentence and hides the action's failure from `catchReason`.
25
27
  */
26
28
  export declare const toUserError: (calls: AnyBinding | ReadonlyArray<Any>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, UserError<Exclude<E, CliError.CliError>> | Exclude<E, Exclude<E, CliError.CliError>>, R>;
27
29
  /**
@@ -43,11 +45,16 @@ interface Options$1<A extends Any = Any> {
43
45
  readonly commands?: { readonly [K in A as K["name"]]?: Options<K>; };
44
46
  /**
45
47
  * Commands of your own beside the derived ones, as `Command.withSubcommands` takes them:
46
- * `[importLinks]`, each made with Effect CLI's `Command.make`. One fails as you make it fail:
47
- * with a `CliError.UserError` for `Command.run` to print its `userMessage` on stderr. Written
48
- * in the call, or in options held with `satisfies` this type, the list adds its commands'
49
- * failures and services to the aggregate's type; in options annotated with this type, it adds
50
- * none.
48
+ * `[importLinks]`, each made with Effect CLI's `Command.make` and printing its output itself,
49
+ * with `Console.log`. One calls actions through a client in its own handler: `yield*` the
50
+ * Effect `remote` takes as `client`, which needs no scope, or `Action.client`, acquired inside
51
+ * `Effect.scoped`. It fails as you make it fail: through `toUserError` as a derived command
52
+ * does, or with a `CliError.UserError` for `Command.run` to print its `userMessage` on stderr.
53
+ *
54
+ * **Gotchas**
55
+ *
56
+ * - Write the list in the call, or hold the options with `satisfies ActionCli.Options<…>`: in options annotated `: ActionCli.Options<…>` it adds none of its commands' failures and services to the aggregate's type, so the program compiles, then a command's service is not found.
57
+ * - Never pipe `Command.withSubcommands` onto the aggregate instead: it replaces the derived commands, which then answer with the usage text.
51
58
  */
52
59
  readonly subcommands?: Subcommands | undefined;
53
60
  }
@@ -64,25 +71,49 @@ type Local<App, A extends Any> = Effect.Effect<A["success"]["Type"], A["error"][
64
71
  /**
65
72
  * Project one implemented action into a native Effect CLI command that runs its handler in
66
73
  * process, behind its implementation's authorization. It is named after the action in kebab
67
- * case with one flag per field of its input (`--user-id`), or `--input` taking the whole input
68
- * as JSON when it is not a struct. The flag of an array of strings or numbers is repeated for each value, a boolean
69
- * 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
70
- * need; the host provides the identity. It prints the result on stdout; a failure is a
71
- * `UserError`, or an Effect CLI error as it is, such as a builder's `CliError.UserError`, which
72
- * `Command.run` prints on stderr. `remoteCommand` calls a server instead.
74
+ * case, with one flag per top-level field of its input (`--user-id`), or `--input` taking the
75
+ * whole input as JSON when it is not a struct of named fields. The host provides the caller on
76
+ * the command, `Command.provideSync`, beside what the authorization, the handler and the builder
77
+ * need. `remoteCommand` calls a server instead.
78
+ *
79
+ * **Gotchas**
80
+ *
81
+ * - A success is its JSON on stdout, indented by two spaces, or `render`'s text; nothing for an action without `success`. Its Effect logs and `Console` output go to stderr; other writes as `logToStderr` says.
82
+ * - A declared error is its JSON on stderr, under Effect CLI's `ERROR` heading, the command failing with a `UserError` whose `cause` is the error, exit 1 or its `Runtime.errorExitCode`; a refusal as `Action.Unauthenticated` and `Action.Forbidden` say; input that does not decode, in `--input` or a flag's JSON value, is `InvalidInput`'s JSON there, while a missing flag or a rejected choice is Effect CLI's own parse error.
83
+ * - Effect CLI styles what it prints, a failure's JSON included, on a terminal and, from Effect 4.0.6, wherever `FORCE_COLOR` forces colour: for plain text there, set `NO_COLOR` or provide `TerminalCapabilities.TerminalCapabilities` with `canColor: false`.
84
+ * - A failure no schema encodes, a builder's or an accepted error that does not encode, is a `UserError` printed as its tag or name and its message, then its causes, never its other fields; a defect, an undeclared failure, or a success that does not encode is the command's defect, which `runMain` reports as `logToStderr` says.
85
+ * - Provide services with `Command.provide` on the command, built when an action runs: provided around `Command.run`, they are built before parsing, so `--help` and a mistyped flag open the database.
86
+ * - Each run builds the action's implementation, its builder and an authorizer an Effect builds, and releases them after the call, even where the host already built them; implementations passed beside it are not built.
87
+ * - A field named like a global flag (`help`, `version`, `log-level`) is no clash: its flag shadows the global one on this command.
73
88
  */
74
89
  export declare function command<const Apps extends Served, A extends ActionOf<Member<Apps>>, const O = {}>(implementations: Apps, action: A, options?: O & NoInfer<Options<A>> & NoInfer<Known<O, Options<Any>>>): Command.Command<string, never, {}, Failing<Effect.Error<Local<Owning<Member<Apps>, A>, A>>>, Effect.Services<Local<Owning<Member<Apps>, A>, A>>>;
75
90
  /**
76
91
  * Project one action of an HTTP binding into a native Effect CLI command, derived as `command`
77
92
  * derives it, that calls the action through `client`'s method and fails as the method does, or
78
- * as the Effect building the client does. `client` is an Effect of the binding's client, such as
79
- * `ActionHttp.client(Http, { baseUrl })`, run each time the command runs; omitted, it is
80
- * `ActionHttp.client(binding)` on the host's `HttpClient`. It runs nothing locally.
93
+ * as the Effect building the client does. It runs nothing locally and names no caller: the
94
+ * server verifies the credentials its client sends.
81
95
  */
82
- export declare function remoteCommand<const H extends AnyHttp, A extends H["actions"][number], const O = {}, C extends ClientOf<H> | undefined = undefined>(binding: H, action: A, options?: O & NoInfer<Options<A>> & NoInfer<Known<O, RemoteKeys<Options<Any>>>> & {
96
+ export declare function remoteCommand<const H extends AnyHttp, A extends H["actions"][number], const O = {}, C extends ClientOf<H, A> | undefined = undefined>(binding: H, action: A, options?: O & NoInfer<Options<A>> & NoInfer<Known<O, RemoteKeys<Options<Any>>>> & Calling<C>): Command.Command<string, never, {}, Failing<CallFailure<H, C, A>>, CallServices<C, A>>;
97
+ export interface Calling<C> {
98
+ /**
99
+ * The connection: an Effect of the binding's client, such as
100
+ * `ActionHttp.client(Http, { baseUrl, transformClient })`, run each time a command runs, so one
101
+ * reading `Config` is an `Effect.gen` returning `yield* ActionHttp.client(…)` and `--help`
102
+ * reads nothing. What it requires, the host provides. Omitted, it is
103
+ * `ActionHttp.client(binding)` on the host's `HttpClient`, with no `baseUrl`. It needs only the
104
+ * methods of the actions it derives commands for: those `remote`'s `actions` lists, or
105
+ * `remoteCommand`'s one.
106
+ *
107
+ * **Gotchas**
108
+ *
109
+ * - A token read with `Config` and not set fails every command, a public one's too, as `ConfigError: …`, which names no fix: map it to a tagged error whose `message` says what to do, or read an optional one with `Config.option`.
110
+ * - Credentials go in `transformClient`: a command adds no headers of its own, so without them a protected action answers `Unauthenticated`.
111
+ * - Never transform the `HttpClient` around the program instead: it rewrites every request the program makes, an absolute URL too, and sends each one the token.
112
+ */
83
113
  readonly client?: C;
84
- }): Command.Command<string, never, {}, Failing<CallFailure<H, C, A>>, CallServices<C, A>>;
85
- type ClientOf<H extends AnyHttp> = Effect.Effect<Client<H, unknown, unknown>, unknown, unknown>;
114
+ }
115
+ type ClientOf<H extends AnyHttp, A extends Any> = Effect.Effect<Pick<Client<H, unknown, unknown>, A["name"]>, unknown, unknown>;
116
+ type Needed<O, L extends ReadonlyArray<Any>, All extends Any> = "actions" extends (O extends unknown ? keyof O : never) ? Selected<O, All> : L[number];
86
117
  type RemoteKeys<Keys> = Keys & {
87
118
  readonly client?: unknown;
88
119
  };
@@ -91,36 +122,43 @@ type Called<M, A extends Any> = A extends unknown ? A["name"] extends keyof M ?
91
122
  type CallFailure<H extends AnyHttp, C, A extends Any> = C extends Effect.Effect<infer M, infer CE, unknown> ? CE | Effect.Error<Called<M, A>> : Effect.Error<Called<Client<H>, A>>;
92
123
  type CallServices<C, A extends Any> = C extends Effect.Effect<infer M, unknown, infer CR> ? CR | Effect.Services<Called<M, A>> : HttpClient.HttpClient;
93
124
  /**
94
- * Project every implemented action as a subcommand of one aggregate command, each named after
95
- * its action in kebab case and run in process. `commands` gives a subcommand the options
96
- * `command` takes, by action name; `subcommands` adds commands of your own, which
97
- * `Command.withSubcommands` on the result would put in the derived ones' place. `remote` calls a
98
- * server instead. A test runs it in memory, with no process:
99
- * `Command.runWith(command, { version })(args)`.
125
+ * Project the implemented actions, every one or those `actions` selects, as subcommands of one
126
+ * aggregate command, each derived as `command` derives it and run in process. `commands` gives a
127
+ * subcommand the options `command` takes, by action name; `subcommands` adds commands of your
128
+ * own. `remote` calls a server instead. A test runs it in memory, with no process: `Command.runWith(command, { version })(args)`.
100
129
  * Guide: `docs/ActionCli.md` in this package; in an application: `examples/assistant/src/commands.ts`.
130
+ *
131
+ * **Gotchas**
132
+ *
133
+ * - Services provided on it with `Command.provide` are also built when it runs with no subcommand, before it shows its help.
101
134
  */
102
135
  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>>>> & {
103
136
  readonly subcommands?: Own | undefined;
104
137
  }): Command.Command<string, {}, {}, Failing<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>>>;
105
138
  /**
106
- * Project every action of an HTTP binding as a subcommand of one aggregate command, derived as
107
- * `make` derives it, each calling its action through `client`'s method, as `remoteCommand`
108
- * does. `commands` gives one the options `command` takes; `subcommands` adds commands of your
109
- * own, which call through the same client by yielding the Effect given as `client`.
139
+ * Project the actions of an HTTP binding, every one or those `actions` selects, as subcommands of
140
+ * one aggregate command, derived as `make` derives them, each calling its action through
141
+ * `client`'s method, as `remoteCommand` does. `commands` gives one the options `command` takes;
142
+ * `subcommands` adds commands of your own, which call through the same client by yielding the
143
+ * Effect given as `client`.
144
+ * In an application: `examples/notes-service/src/commands.ts`.
110
145
  */
111
- export declare function remote<const H extends AnyHttp, const O extends Selection<H["actions"][number]> = {}, const Own extends Subcommands = [], C extends ClientOf<H> | undefined = undefined>(binding: H, options: O & NoInfer<Options$1<H["actions"][number]>> & NoInfer<KnownOptions<O, RemoteKeys<Options$1<Any>>, H["actions"][number]>> & {
112
- readonly client?: C;
146
+ export declare function remote<const H extends AnyHttp, const O extends Selection<H["actions"][number]> = {}, const Own extends Subcommands = [], const L extends ReadonlyArray<H["actions"][number]> = H["actions"], C extends ClientOf<H, Needed<O, L, H["actions"][number]>> | undefined = undefined>(binding: H, options: O & NoInfer<Options$1<H["actions"][number]>> & NoInfer<KnownOptions<O, RemoteKeys<Options$1<Any>>, H["actions"][number]>> & Calling<C> & {
147
+ readonly actions?: L | undefined;
113
148
  readonly subcommands?: Own | undefined;
114
149
  }): Command.Command<string, {}, {}, Failing<CallFailure<H, C, Selected<O, H["actions"][number]>>> | Command.Error<OwnCommand<Own>>, CallServices<C, Selected<O, H["actions"][number]>> | Command.Services<OwnCommand<Own>>>;
115
150
  /**
116
151
  * A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
117
152
  * subprocess, applied last, before `NodeRuntime.runMain`: the default logger of every layer
118
- * provided within writes to stderr, and a failure `runMain` would report, such as a defect or
119
- * a failure of a layer the host provides, is reported on stderr instead, once. A command's
120
- * own failure, which `Command.run` has printed, is not reported again. The exit code is the
121
- * one `runMain` gives: the failure's `Runtime.errorExitCode`, 1 by default, or 130 for an
122
- * interruption.
153
+ * provided within writes to stderr, and a failure `runMain` would report on stdout, such as a
154
+ * defect, a success that does not encode or a failure of a layer the host provides, is reported
155
+ * on stderr instead, once. A command's own failure, which `Command.run` has printed, is not
156
+ * reported again. The exit code is the one `runMain` gives: the failure's
157
+ * `Runtime.errorExitCode`, 1 by default, or 130 for an interruption.
158
+ *
159
+ * **Gotchas**
160
+ *
161
+ * - It moves the default logger only: a host layer's `Console` output, the global `console.log`, and a logger writing through `Console.log` such as `Logger.consoleJson`, still reach stdout; log JSON with `Logger.withConsoleError(Logger.formatJson)`.
123
162
  */
124
163
  export declare const logToStderr: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, never, R>;
125
- //#endregion
126
164
  export { type Options as CommandOptions, Options$1 as Options, type UserError };
@@ -1,21 +1,24 @@
1
1
  import { assertDistinct, assertKnown } from "./contract/names.mjs";
2
2
  import { bindingErrors, projectedErrors } from "./contract/declared.mjs";
3
- import { Implementation, acquire, assertHeld, select, toList } from "./contract/implementation.mjs";
3
+ import { Implementation, acquire, assertHeld, notHeld, select, toList } from "./contract/implementation.mjs";
4
4
  import { client } from "./ActionHttp.mjs";
5
5
  import { command as command$1, failingAsDerived } from "./cli/commands.mjs";
6
6
  import { Cause, Effect, Logger, Runtime } from "effect";
7
7
  import { Command } from "effect/cli";
8
- //#region src/cli/ActionCli.ts
8
+ // src/cli/ActionCli.ts
9
9
  /**
10
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.
11
+ * binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and
12
+ * pipe the command's whole Effect through it: a failure that is a declared error, the binding's
13
+ * and its authentication's included, or a built-in one becomes a `UserError` whose message is its
14
+ * JSON, and any other, a transport failure for one, a `UserError` naming it and the causes
15
+ * beneath it. An Effect CLI error, your own `new CliError.UserError({ cause, userMessage })`
16
+ * included, passes through as it is.
17
+ *
18
+ * **Gotchas**
19
+ *
20
+ * - A failure left unmapped, one call's included, is reported as a crash, `ERROR (#n)` with a stack trace: pipe the whole Effect through it, once.
21
+ * - To add context, such as how far a batch got, print it before this in the pipe, `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result puts its JSON mid-sentence and hides the action's failure from `catchReason`.
19
22
  */
20
23
  const toUserError = (calls) => "actions" in calls ? failingAsDerived(calls.actions.flatMap((action) => bindingErrors(action, calls))) : failingAsDerived(calls.flatMap((action) => projectedErrors(action)));
21
24
  const runWithBuildOfItsOwn = (app, action, input) => {
@@ -27,8 +30,8 @@ const runWithBuildOfItsOwn = (app, action, input) => {
27
30
  };
28
31
  const theImplementationOf = (apps, action) => {
29
32
  const [app, ...others] = apps.filter((candidate) => candidate.actions.includes(action));
30
- if (app === void 0) throw new Error(`Action "${action.name}" has no implementation here`);
31
- if (others.length > 0) throw new Error(`Action "${action.name}" is implemented twice here`);
33
+ if (app === void 0) throw new Error(`Action "${action.name}" has no implementation here (pass its implementation, and select with the contract value it implements: an equal-looking action does not match)`);
34
+ if (others.length > 0) throw new Error(`Action "${action.name}" is implemented twice here (pass one of them)`);
32
35
  return app;
33
36
  };
34
37
  const inProcess = (apps, action, options) => {
@@ -36,10 +39,10 @@ const inProcess = (apps, action, options) => {
36
39
  return command$1(action, (input) => runWithBuildOfItsOwn(app, action, input), options, projectedErrors(action));
37
40
  };
38
41
  const overHttp = (binding, action, options, client$1) => {
39
- if (!binding.actions.includes(action)) throw new Error(`Action "${action.name}" is not in this HTTP binding`);
42
+ if (!binding.actions.includes(action)) throw new Error(`Action "${action.name}" is not in this HTTP binding (pass it to the binding's ActionHttp.make, and select with the contract value it received)`);
40
43
  return command$1(action, (input) => Effect.flatMap(client$1 ?? client(binding), (methods) => {
41
44
  const method = methods[action.name];
42
- if (method === void 0) throw new Error(`No client method for ${action.name}`);
45
+ if (method === void 0) throw new Error(`No client method for ${action.name} (give \`client\` a method for this action)`);
43
46
  return method(input);
44
47
  }), options, bindingErrors(action, binding));
45
48
  };
@@ -54,23 +57,34 @@ const aggregateCommand = (name, actions, commands, own, project) => {
54
57
  })), ...own.flatMap((entry) => Command.isCommand(entry) ? [entry] : entry.commands).map((command) => ({
55
58
  claimant: "subcommands",
56
59
  name: command.name
57
- }))], ({ name: claimed }) => claimed, ({ claimant }) => claimant);
60
+ }))], ({ name: claimed }) => claimed, {
61
+ claimantOf: ({ claimant }) => claimant,
62
+ fix: "rename one of your subcommands, or give an action's command another name in commands"
63
+ });
58
64
  return Command.make(name).pipe(Command.withSubcommands([...derived.map(({ command }) => command), ...own]));
59
65
  };
60
66
  const checkedCommandsOf = (commands, actions) => {
61
67
  const given = commands ?? {};
62
- assertKnown("commands", Object.keys(given), actions.map((action) => action.name));
68
+ assertKnown("commands", Object.keys(given), actions.map((action) => action.name), "key it by the action's own name, not its command's");
63
69
  return given;
64
70
  };
65
71
  /**
66
72
  * Project one implemented action into a native Effect CLI command that runs its handler in
67
73
  * process, behind its implementation's authorization. It is named after the action in kebab
68
- * case with one flag per field of its input (`--user-id`), or `--input` taking the whole input
69
- * as JSON when it is not a struct. The flag of an array of strings or numbers is repeated for each value, a boolean
70
- * 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
71
- * need; the host provides the identity. It prints the result on stdout; a failure is a
72
- * `UserError`, or an Effect CLI error as it is, such as a builder's `CliError.UserError`, which
73
- * `Command.run` prints on stderr. `remoteCommand` calls a server instead.
74
+ * case, with one flag per top-level field of its input (`--user-id`), or `--input` taking the
75
+ * whole input as JSON when it is not a struct of named fields. The host provides the caller on
76
+ * the command, `Command.provideSync`, beside what the authorization, the handler and the builder
77
+ * need. `remoteCommand` calls a server instead.
78
+ *
79
+ * **Gotchas**
80
+ *
81
+ * - A success is its JSON on stdout, indented by two spaces, or `render`'s text; nothing for an action without `success`. Its Effect logs and `Console` output go to stderr; other writes as `logToStderr` says.
82
+ * - A declared error is its JSON on stderr, under Effect CLI's `ERROR` heading, the command failing with a `UserError` whose `cause` is the error, exit 1 or its `Runtime.errorExitCode`; a refusal as `Action.Unauthenticated` and `Action.Forbidden` say; input that does not decode, in `--input` or a flag's JSON value, is `InvalidInput`'s JSON there, while a missing flag or a rejected choice is Effect CLI's own parse error.
83
+ * - Effect CLI styles what it prints, a failure's JSON included, on a terminal and, from Effect 4.0.6, wherever `FORCE_COLOR` forces colour: for plain text there, set `NO_COLOR` or provide `TerminalCapabilities.TerminalCapabilities` with `canColor: false`.
84
+ * - A failure no schema encodes, a builder's or an accepted error that does not encode, is a `UserError` printed as its tag or name and its message, then its causes, never its other fields; a defect, an undeclared failure, or a success that does not encode is the command's defect, which `runMain` reports as `logToStderr` says.
85
+ * - Provide services with `Command.provide` on the command, built when an action runs: provided around `Command.run`, they are built before parsing, so `--help` and a mistyped flag open the database.
86
+ * - Each run builds the action's implementation, its builder and an authorizer an Effect builds, and releases them after the call, even where the host already built them; implementations passed beside it are not built.
87
+ * - A field named like a global flag (`help`, `version`, `log-level`) is no clash: its flag shadows the global one on this command.
74
88
  */
75
89
  function command(implementations, action, options) {
76
90
  return inProcess(toList(implementations), action, options);
@@ -80,13 +94,15 @@ function remoteCommand(binding, action, options) {
80
94
  return overHttp(binding, action, options === void 0 ? void 0 : syntax, client);
81
95
  }
82
96
  /**
83
- * Project every implemented action as a subcommand of one aggregate command, each named after
84
- * its action in kebab case and run in process. `commands` gives a subcommand the options
85
- * `command` takes, by action name; `subcommands` adds commands of your own, which
86
- * `Command.withSubcommands` on the result would put in the derived ones' place. `remote` calls a
87
- * server instead. A test runs it in memory, with no process:
88
- * `Command.runWith(command, { version })(args)`.
97
+ * Project the implemented actions, every one or those `actions` selects, as subcommands of one
98
+ * aggregate command, each derived as `command` derives it and run in process. `commands` gives a
99
+ * subcommand the options `command` takes, by action name; `subcommands` adds commands of your
100
+ * own. `remote` calls a server instead. A test runs it in memory, with no process: `Command.runWith(command, { version })(args)`.
89
101
  * Guide: `docs/ActionCli.md` in this package; in an application: `examples/assistant/src/commands.ts`.
102
+ *
103
+ * **Gotchas**
104
+ *
105
+ * - Services provided on it with `Command.provide` are also built when it runs with no subcommand, before it shows its help.
90
106
  */
91
107
  function make(implementations, options) {
92
108
  const apps = toList(implementations);
@@ -96,18 +112,22 @@ function make(implementations, options) {
96
112
  }
97
113
  function remote(binding, options) {
98
114
  const listed = options.actions;
99
- if (listed !== void 0) assertHeld("the binding does not hold it", listed, binding.actions);
115
+ if (listed !== void 0) assertHeld(notHeld.byBinding, listed, binding.actions);
100
116
  const commands = checkedCommandsOf(options.commands, binding.actions);
101
117
  return aggregateCommand(options.name, binding.actions.filter((action) => listed?.includes(action) ?? true), commands, options.subcommands ?? [], (action, syntax) => overHttp(binding, action, syntax, options.client));
102
118
  }
103
119
  /**
104
120
  * A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
105
121
  * subprocess, applied last, before `NodeRuntime.runMain`: the default logger of every layer
106
- * provided within writes to stderr, and a failure `runMain` would report, such as a defect or
107
- * a failure of a layer the host provides, is reported on stderr instead, once. A command's
108
- * own failure, which `Command.run` has printed, is not reported again. The exit code is the
109
- * one `runMain` gives: the failure's `Runtime.errorExitCode`, 1 by default, or 130 for an
110
- * interruption.
122
+ * provided within writes to stderr, and a failure `runMain` would report on stdout, such as a
123
+ * defect, a success that does not encode or a failure of a layer the host provides, is reported
124
+ * on stderr instead, once. A command's own failure, which `Command.run` has printed, is not
125
+ * reported again. The exit code is the one `runMain` gives: the failure's
126
+ * `Runtime.errorExitCode`, 1 by default, or 130 for an interruption.
127
+ *
128
+ * **Gotchas**
129
+ *
130
+ * - It moves the default logger only: a host layer's `Console` output, the global `console.log`, and a logger writing through `Console.log` such as `Logger.consoleJson`, still reach stdout; log JSON with `Logger.withConsoleError(Logger.formatJson)`.
111
131
  */
112
132
  const logToStderr = (self) => self.pipe(Effect.catchCause((cause) => {
113
133
  if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt;
@@ -118,5 +138,4 @@ const logToStderr = (self) => self.pipe(Effect.catchCause((cause) => {
118
138
  });
119
139
  return Effect.andThen(Runtime.getErrorReported(squashed) ? Effect.logError(cause) : Effect.void, Effect.die(reportedDefectKeepingCause));
120
140
  }), Effect.provideService(Logger.LogToStderr, true));
121
- //#endregion
122
141
  export { command, logToStderr, make, remote, remoteCommand, toUserError };
@@ -8,12 +8,16 @@ import { AnyHttp, Client, MethodError, Options } from "./http/client.mjs";
8
8
  import { Context, Effect, FileSystem, Layer, Path, Schema, Types } from "effect";
9
9
  import { Etag, HttpClient, HttpClientError, HttpPlatform, HttpRouter } from "effect/http";
10
10
  import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "effect/http-api";
11
- //#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<E = HttpClientError.HttpClientError, R = never> extends Options<E, R> {
14
13
  /**
15
14
  * What each call sends with. Omitted: the global `fetch`, looked up on every call, so one
16
15
  * installed after the client is built, such as a test's stub, is used.
16
+ *
17
+ * **Gotchas**
18
+ *
19
+ * - A page on the API's own origin sends its cookies already; from another origin pass `(input, init) => fetch(input, { ...init, credentials: "include" })`, and the server's CORS must allow credentials, `HttpRouter.cors({ allowedOrigins, credentials: true })`.
20
+ * - A `fetch` of your own passes `init.signal` on, or interrupting a call no longer aborts its request: a deadline is `Effect.timeout` on the call.
17
21
  */
18
22
  readonly fetch?: typeof globalThis.fetch | undefined;
19
23
  }
@@ -30,14 +34,16 @@ type HttpLayer<H extends AnyHttp, Apps extends Served, M extends Middleware, A e
30
34
  export interface LayerOptions<M extends Middleware = [], A extends Any = never> {
31
35
  /**
32
36
  * The actions it serves, among the binding's actions the implementations hold: `[GetUser]`.
33
- * Defaults to every one of them. A layer whose middleware requires the identity lists
34
- * protected actions only.
37
+ * Defaults to every one of them.
35
38
  */
36
39
  readonly actions?: ReadonlyArray<A> | undefined;
37
40
  /**
38
41
  * Native endpoint middleware every served route runs, the first innermost: inside the
39
- * authentication of a protected route, outside its decoding. A middleware requiring the
40
- * identity needs a layer serving only protected actions.
42
+ * authentication of a protected route, outside its decoding, so it also sees invalid input.
43
+ *
44
+ * **Gotchas**
45
+ *
46
+ * - A middleware requiring the identity needs a layer serving only protected actions: serve the public ones from a second layer without it.
41
47
  */
42
48
  readonly middleware?: M;
43
49
  }
@@ -46,9 +52,31 @@ type LayerRequest<App, A extends Any, M extends Middleware> = Exclude<Through<M,
46
52
  }>] extends [never] ? Identity<Protected<Serving<App, A>>> : never)>;
47
53
  /** Contract-level configuration shared by servers and clients. */
48
54
  interface Options$1<E extends Errors = Errors> {
55
+ /**
56
+ * The mount path of every route: `/api` by default, `/` at the root. Split a large API, or
57
+ * actions sharing a name, into one binding per area, each with its own `prefix` and client;
58
+ * one host serves them all.
59
+ */
49
60
  readonly prefix?: `/${string}`;
50
- /** Router middleware's errors, which every endpoint declares: one schema, or a list. */
61
+ /**
62
+ * What router middleware around the routes, or the layer's `middleware`, answers with, such as
63
+ * a quota's 429, which every endpoint declares: one schema, or a list.
64
+ *
65
+ * **Gotchas**
66
+ *
67
+ * - An error a handler fails with goes in its action's `error`, not here.
68
+ * - Router middleware answering with one encodes and sends it itself, `HttpServerResponse.schemaJson(QuotaExceeded)(error, { status: 429 })`, at the status `Action.Options.error` gives it: at any other a client fails with an `HttpClientError`.
69
+ */
51
70
  readonly error?: E | E[number];
71
+ /**
72
+ * The descriptor of the binding's protected actions, `Authentication.make(...)`: required
73
+ * when it holds one, and it documents and enforces their credentials as native endpoint
74
+ * security middleware on each of them.
75
+ *
76
+ * **Gotchas**
77
+ *
78
+ * - One descriptor per binding: actions of a second identity take a second binding with its own `prefix`, and a public action may be in both.
79
+ */
52
80
  readonly authentication?: Any$1;
53
81
  }
54
82
  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;
@@ -59,6 +87,7 @@ type Api<Actions extends ReadonlyArray<Any>, E extends Errors, D> = HttpApi.Http
59
87
  * serves the same; `layer` serves it and `client` calls it.
60
88
  */
61
89
  export interface Binding<Actions extends ReadonlyArray<Any>, E extends Errors = [], D extends Any$1 | undefined = undefined> {
90
+ /** Its protected actions' authentication descriptor, or `undefined` without one. */
62
91
  readonly authentication: D;
63
92
  /** The exact actions bound to this binding. */
64
93
  readonly actions: Actions;
@@ -66,16 +95,26 @@ export interface Binding<Actions extends ReadonlyArray<Any>, E extends Errors =
66
95
  readonly error: E;
67
96
  /** Where its routes mount: `/api` by default, `/` at the root, without a trailing slash. */
68
97
  readonly prefix: `/${string}`;
98
+ /**
99
+ * The native `HttpApi`, for `OpenApi.fromApi`, `HttpApiSwagger.layer`, `HttpApiScalar.layer`
100
+ * and Effect's own tools: it lists every action of the binding, served or not.
101
+ *
102
+ * **Gotchas**
103
+ *
104
+ * - Serve it with `layer`, never the native `HttpApiBuilder`: its group name is typed `string`, so `HttpApiBuilder.group(api, "api", …)` is refused as `Endpoint not handled: ${string}`, and given a `string`-typed name a protected action makes the host fail to build with `Service not found: effect-actions/Authentication/Security/…`.
105
+ * - A header that middleware reads is not in its document, whose operations list no `parameters`: rewrite it, `OpenApi.fromApi(Http.api.annotate(OpenApi.Transform, (document) => …))`, or make the value an input.
106
+ * - An OAuth protected resource is documented as a bearer scheme: Effect has no OAuth scheme.
107
+ * - Combined into one document, `HttpApi.make("app").addHttpApi(A.api).addHttpApi(B.api)`, a shared action name makes `OpenApi.fromApi` throw `Duplicate OpenAPI operationId`, and a shared prefix silently drops one binding's actions from the document and a native client: otherwise document each alone.
108
+ * - A binding holding no protected action documents no security scheme, so Swagger shows no Authorize button.
109
+ */
69
110
  readonly api: Api<Actions, E, D>;
70
111
  }
71
112
  /**
72
- * Bind actions once, for servers and clients alike, each at `POST <prefix>/<action>`.
73
- * Every endpoint declares its action's errors, the binding's `error`, and the built-in
74
- * `InvalidInput`, `Unauthenticated` and `Forbidden`, so clients decode each as a typed
75
- * failure. A union's own `httpApiStatus` applies to every member; otherwise, each member
76
- * uses its own status, or 422 without one. Protected contracts receive native endpoint
77
- * security middleware: the same descriptor documents their credentials and enforces them.
78
- * Guide: `docs/ActionHttp.md` in this package; in an application: `examples/notes-service/src/routes.ts`.
113
+ * Bind actions once, for servers and clients alike, each at `POST <prefix>/<action>`, a
114
+ * `readOnly` one too. Every endpoint declares its action's errors, the binding's `error`, and
115
+ * the built-in `InvalidInput`, `Unauthenticated` and `Forbidden`, so clients decode each as a
116
+ * typed failure; a protected one also declares what its descriptor's verifier fails with.
117
+ * Guide: `docs/ActionHttp.md` in this package; in an application: `examples/notes-service/src/Api.ts`.
79
118
  */
80
119
  export declare function make<const Actions extends ReadonlyArray<Any & {
81
120
  readonly caller: typeof Anyone;
@@ -84,41 +123,45 @@ export declare function make<const Actions extends ReadonlyArray<Any>, const O e
84
123
  export declare function make<const Actions extends ReadonlyArray<Any>>(actions: Actions & PublicOnly<Actions>): Binding<Actions, [], undefined>;
85
124
  /**
86
125
  * Serve the binding's actions among `implementations` in one layer, or those its `actions` lists,
87
- * authenticating a protected action's request before decoding it, then running its implementation's
88
- * `authorize` before each handler. The binding decides what may be served:
89
- * an implementation's actions the binding leaves out have no route here, and one holding none of
90
- * those served is not built; implementations holding none of the binding's actions at all are
91
- * refused when `layer` is called. It mounts only the routes of the actions it serves, so one
92
- * binding may be served by several layers, each with its own `actions` and `middleware`, which
93
- * covers its routes only. Each implementation's builder runs once per layer graph however many
94
- * layers serve it.
126
+ * authenticating a protected action's request before decoding it, then running its
127
+ * implementation's `authorize` before each handler. An implementation's actions the binding
128
+ * leaves out have no route here, one holding none of those served is not built, and a non-empty
129
+ * list holding none of the binding's actions is refused when `layer` is called. Several layers
130
+ * may serve one binding, each with its own `actions` and `middleware` covering its routes only.
131
+ * The layer fails as the builders do, needs at startup what they need, and per request what the
132
+ * authorization and handlers of the actions it serves need, until its `middleware` or middleware
133
+ * around it provides them, the identity of a protected action excepted, which its authentication
134
+ * provides; what is provided per request wins over what the layer was built with. A declared
135
+ * error answers with its JSON at the status `Action.Options.error` gives it, input that does not
136
+ * decode a 400 `InvalidInput`, and a refusal as `Action.Unauthenticated` and `Action.Forbidden`
137
+ * say.
138
+ *
139
+ * **Gotchas**
95
140
  *
96
- * The layer fails as the builders do, needs at startup what they need, and per request what
97
- * each implementation's authorization and the handlers of the actions it serves
98
- * need, until its `middleware` or middleware provided around it provides them, the identity
99
- * of a protected action excepted, which its authentication provides; the router provides
100
- * its own, such as the request, to every route. Its routes take only requests typed as JSON,
101
- * and what middleware provides per request wins over what the layer was built with. A request
102
- * body is read whole, however large, unless the host limits it where the routes are served:
103
- * `Layer.provide(Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1)))`.
141
+ * - A success is its JSON as the body; an action without `success` answers an empty 200, no body and no content type, so a hand-written client must not parse it. One without `input` still takes the body `{}` typed as JSON: no body is a 400.
142
+ * - A body that is not JSON is a 400 `InvalidInput`; a request with no `Content-Type` a 415 `Unsupported content-type: none`, one with another type Effect's own 415; each after authentication on a protected route.
143
+ * - A defect, an undeclared failure, or a success or an error that does not encode answers an empty 500: the cause goes to the log and to the call's `ErrorReporter`s, never to the body.
144
+ * - A path no layer serves, or a method other than `POST` on a route, is a 404.
145
+ * - The 415 keeps from every handler what another site's page can send without a CORS preflight, such as a form post carrying the user's cookie; the server's CORS policy decides the rest.
146
+ * - Router middleware providing a request service `X` is declared `HttpRouter.middleware<{ provides: X }>()`: without the type argument it runs and the layer still owes `X`.
147
+ * - `HttpRouter.provideRequest(layer)` builds its layer once, at startup, so it never provides an identity or a tenant.
148
+ * - A request body is read whole, however large, unless the host provides `HttpServerRequest.MaxBodySize` to `HttpRouter.serve`, limiting every route that reads its body whole, MCP endpoints included; a body a route reads as a stream is not limited. On Node a request over it is an empty 413 from Effect 4.0.6, and before it gets no response, its connection closed; `HttpRouter.toWebHandler` ignores it, so limit bodies in the platform serving the handler.
149
+ * - An `ErrorReporter` the server runs with is given, as `Info`, a handler's declared failure and an input that does not decode, never a refusal or a refused credential: give an error class `override readonly [ErrorReporter.ignore] = true` to keep it out, and drop `Info` for a built-in error, which takes no mark.
104
150
  */
105
151
  export declare function layer<const H extends AnyHttp, const Apps extends Served>(binding: H, implementations: Apps): HttpLayer<H, Apps, []>;
106
152
  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]>>;
107
153
  /**
108
154
  * Effect's native `HttpApiClient` for a binding, one method per action taking the
109
- * action's input directly: `client.greet({ name })`. The argument may be omitted when `{}`
110
- * is a valid input. Requires the native `HttpClient`, as `HttpApiClient.make` does.
111
- * The options are the native ones, `baseUrl` and `transformClient`, a bearer token being
112
- * `transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. The native
113
- * client itself stays available: `HttpApiClient.make(Http.api)`.
155
+ * action's input directly: `client.greet({ name })`, the argument omitted when `{}` is a valid
156
+ * input. Requires the native `HttpClient`, as `HttpApiClient.make` does; the native client
157
+ * itself stays available: `HttpApiClient.make(Http.api)`. Each call fails with its
158
+ * `MethodError`, an undeclared answer such as a 502 from a proxy or a defect's empty 500 as an
159
+ * `HttpClientError`, and with what `transformClient` adds.
114
160
  *
115
- * A call fails with a declared error value (the action's own, the binding's, a built-in
116
- * `InvalidInput`, `Unauthenticated` or `Forbidden`, or on a protected action its descriptor's),
117
- * a native `HttpClientError` when the server could not be reached or answered with a status or
118
- * body the contract does not declare, or a `SchemaError` when the success does not decode. An
119
- * input that does not encode fails with `InvalidInput`, and nothing is sent. A call also fails
120
- * with what the client `transformClient` returns fails with, as it is, and requires what that
121
- * client requires.
161
+ * **Gotchas**
162
+ *
163
+ * - An input that does not encode fails with `InvalidInput`, and nothing is sent; undeclared input fields, which TypeScript lets through on a wider value, are dropped as it encodes, since the server would refuse them.
164
+ * - It interprets no status and retries nothing: a failed write may have happened, and only a declared error says what the server did.
122
165
  */
123
166
  export declare function client<const H extends AnyHttp>(binding: H, options?: Options): Effect.Effect<Client<H>, never, HttpClient.HttpClient>;
124
167
  export declare function client<const H extends AnyHttp, E, R>(binding: H, options: Options<E, R>): Effect.Effect<Client<H, E, R>, never, HttpClient.HttpClient>;
@@ -127,11 +170,7 @@ export declare function client<const H extends AnyHttp, E, R>(binding: H, option
127
170
  * a browser app: its methods are the same Effects, which require only what `transformClient`
128
171
  * requires, so a call without one runs alone, as `Effect.runPromise(api.greet({ name }))`. The
129
172
  * options are `client`'s, and `fetch`.
130
- *
131
- * Each call sends with `options.fetch`, or with the global `fetch` as it is when the call
132
- * runs.
133
173
  */
134
174
  export declare function fetchClient<const H extends AnyHttp>(binding: H, options?: FetchClientOptions): Client<H>;
135
175
  export declare function fetchClient<const H extends AnyHttp, E, R>(binding: H, options: FetchClientOptions<E, R>): Client<H, E, R>;
136
- //#endregion
137
176
  export { type AnyHttp as Any, type Client, type Options as ClientOptions, type MethodError, Options$1 as Options };
@@ -1,15 +1,15 @@
1
1
  import { Anyone } from "./contract/caller.mjs";
2
+ import { declared } from "./contract/status.mjs";
2
3
  import { bindingErrors, bindingOf, projectedErrors } from "./contract/declared.mjs";
3
4
  import { acquire, provideHandlers, servingOf } from "./contract/implementation.mjs";
4
5
  import { providerOf } from "./authentication/provider.mjs";
5
6
  import { inputParseOptions } from "./contract/call.mjs";
6
- import { declared } from "./contract/status.mjs";
7
7
  import { methods } from "./http/client.mjs";
8
8
  import { SchemaErrors, schemaErrors } from "./http/schema-errors.mjs";
9
9
  import { Context, Effect, Layer, Scope } from "effect";
10
10
  import { FetchHttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
11
11
  import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api";
12
- //#region src/http/ActionHttp.ts
12
+ // src/http/ActionHttp.ts
13
13
  const mountSegments = (prefix) => (prefix ?? "/api").split("/").filter((segment) => segment !== "");
14
14
  const absolutePath = (segments) => `/${segments.join("/")}`;
15
15
  const groupName = (mount) => mount.join("/") || "/";
@@ -21,7 +21,7 @@ const apiOf = (group, endpoints) => {
21
21
  const endpointOf = (action, path, errors) => HttpApiEndpoint.post(action.name, path, {
22
22
  payload: action.input.pipe(HttpApiSchema.asJson()),
23
23
  success: action.success,
24
- error: errors.flatMap(declared)
24
+ error: errors.map(declared)
25
25
  });
26
26
  const withinLayerMiddleware = (endpoint, middleware = []) => middleware.reduce((inner, key) => inner.middleware(key), endpoint);
27
27
  function make(actions, options = {}) {
@@ -77,5 +77,4 @@ function client(binding, options) {
77
77
  function fetchClient(binding, { fetch, ...options } = {}) {
78
78
  return methods(binding, options).pipe(Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.Fetch, fetch ?? ((input, init) => globalThis.fetch(input, init))), Effect.runSync);
79
79
  }
80
- //#endregion
81
80
  export { client, fetchClient, layer, make };