@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,11 +1,11 @@
1
- import { Anyone } from "./actions.mjs";
2
- import { Refusal } from "./errors.mjs";
3
- import { Protected, ServedRequest, Serving } from "./implementation.mjs";
1
+ import { Anyone } from "../contract/rules.mjs";
2
+ import { Refusal } from "../contract/errors.mjs";
3
+ import { BuildServices, Protected, ServedRequest, Serving } from "../contract/implementation.mjs";
4
4
  import { Action_d_exports, Any as Any$1, Errors } from "../Action.mjs";
5
5
  import { Context, Effect } from "effect";
6
6
  import { HttpRouter, HttpServerResponse } from "effect/http";
7
7
  import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
8
- //#region src/internal/authentication.d.ts
8
+ //#region src/authentication/provider.d.ts
9
9
  /** One native security scheme: Bearer unless a descriptor names another. */
10
10
  export type Security = HttpApiSecurity.HttpApiSecurity;
11
11
  /** What a native scheme decodes: a `Redacted` token or key, or Basic credentials. */
@@ -34,6 +34,12 @@ type Response = Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, un
34
34
  interface Runtime$1 {
35
35
  /** The descriptor the provider was built from, the one a binding naming it must hold. */
36
36
  readonly descriptor: unknown;
37
+ /**
38
+ * The verifier itself, for a surface answering refusals its own way, such as RPC: the
39
+ * credential to the identity, or a refusal, or an error the descriptor declares. An empty
40
+ * credential, as the native decoder gives an absent one, is refused without reaching it.
41
+ */
42
+ readonly authenticate: (credential: Credential) => Effect.Effect<unknown, unknown, unknown>;
37
43
  /** The native security middleware's implementation, keyed by its scheme. */
38
44
  readonly middleware: Readonly<Record<string, (route: Response, options: {
39
45
  readonly credential: Credential;
@@ -43,6 +49,11 @@ interface Runtime$1 {
43
49
  * answered with its challenge under Bearer; as it is under another scheme.
44
50
  */
45
51
  readonly stepUp: HttpApiMiddleware.HttpApiMiddleware<never, never, never>;
52
+ /**
53
+ * An MCP endpoint's request: decoded here, and a protected tool's call promotes what it
54
+ * verified. Where `optional`, a request presenting no credential, which the scheme decodes as
55
+ * empty, passes signed out.
56
+ */
46
57
  readonly http: <E, R>(route: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>, optional: boolean) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R | HttpRouter.Provided>;
47
58
  /**
48
59
  * A route of the host's own: the request is decoded here, the route gets the identity
@@ -61,7 +72,7 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
61
72
  readonly security: S;
62
73
  /**
63
74
  * What its verifier may fail with besides a refusal, as a list: every protected endpoint
64
- * declares it, and every remote surface sends it with its status.
75
+ * declares it, and every remote surface sends it, over HTTP with its status.
65
76
  */
66
77
  readonly error: E;
67
78
  readonly "~provider": Context.Key<Provider<I, Name>, Runtime$1>;
@@ -69,12 +80,12 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
69
80
  /** An MCP request's verified identity, which a protected tool's call promotes. */
70
81
  readonly "~verified": Context.Key<never, unknown>;
71
82
  }
72
- export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
73
- export type Identity<A extends Any$1> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
74
83
  /**
75
- * The identity a descriptor for `A` must authenticate: `unknown` for an erased action, whose
76
- * key is known only at run time, where `assertAuthentication` checks it.
84
+ * Any descriptor `make` returns, named so a package emitting declarations can export one, and a
85
+ * binding name one.
77
86
  */
87
+ export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
88
+ export type Identity<A extends Any$1> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
78
89
  type Covered<A extends Any$1> = Exclude<A["caller"], typeof Anyone> extends Context.Key<infer I, unknown> ? I : never;
79
90
  export type Required<A extends Any$1> = [Protected<A>] extends [never] ? {
80
91
  readonly authentication?: Any;
@@ -82,6 +93,16 @@ export type Required<A extends Any$1> = [Protected<A>] extends [never] ? {
82
93
  readonly authentication: Descriptor<Covered<Protected<A>>, unknown, Security, string, Errors>;
83
94
  };
84
95
  export type ProviderOf<D> = D extends Descriptor<infer I, unknown, Security, infer Name, Errors> ? Provider<I, Name> : never;
96
+ /** Actions a binding takes without options: public ones, as a protected one names its descriptor. */
97
+ export type PublicOnly<Actions extends ReadonlyArray<Any$1>> = [Exclude<Actions[number], {
98
+ readonly caller: typeof Anyone;
99
+ }>] extends [never] ? unknown : {
100
+ readonly "Protected actions take options naming their authentication": never;
101
+ };
102
+ /** The descriptor options `O` name, where they always name one. */
103
+ export type DescriptorOf<O> = [O] extends [{
104
+ readonly authentication: infer D extends Any;
105
+ }] ? D : undefined;
85
106
  /** All protected actions on a surface use the descriptor's single identity key. */
86
107
  export type Matching<A extends Any$1, D> = unknown extends Covered<Protected<A>> ? unknown : [Exclude<Covered<Protected<A>>, D extends Descriptor<infer I, unknown, Security, string, Errors> ? I : never>] extends [never] ? unknown : {
87
108
  readonly "Authentication descriptor does not cover every protected action": never;
@@ -90,6 +111,17 @@ export type Matching<A extends Any$1, D> = unknown extends Covered<Protected<A>>
90
111
  export type RemoteRequest<App, A extends Any$1> = ServedRequest<App, Extract<A, {
91
112
  readonly caller: typeof Anyone;
92
113
  }>> | Exclude<ServedRequest<App, Protected<A>>, Identity<Protected<A>>>;
114
+ /**
115
+ * What a server of the actions `A` of the implementations `App` reads at startup, as
116
+ * `BuildServices` states it, with the caller a builder yields named as the mistake it is: a server
117
+ * has its caller per request.
118
+ */
119
+ export type ServerBuildServices<App, A extends Any$1> = App extends unknown ? BuildServices<App, A> extends (infer R) ? R extends Identity<Protected<A>> ? {
120
+ readonly "A builder yields the caller, which a server has per request: yield it in a handler or an authorizer": {
121
+ readonly caller: R;
122
+ readonly builderOf: Serving<App, A>["name"];
123
+ };
124
+ } : R : never : never;
93
125
  /**
94
126
  * What a call of `A` under the descriptor `D` may fail with from its verifier: the errors `D`
95
127
  * declares, for a protected action; none for a public one, which nothing verifies.
@@ -0,0 +1,12 @@
1
+ import { Unauthenticated } from "../contract/errors.mjs";
2
+ import { Anyone } from "../contract/rules.mjs";
3
+ import { Effect, Option } from "effect";
4
+ //#region src/authentication/provider.ts
5
+ const withVerifierErrors = (action, errors, verifierErrors) => action.caller === Anyone || verifierErrors === void 0 ? errors : [...errors, ...verifierErrors];
6
+ const assertAuthentication = (actions, auth) => {
7
+ for (const action of actions) if (action.caller !== Anyone && (auth === void 0 || action.caller.key !== auth.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor`);
8
+ };
9
+ const providerOf = (auth) => Effect.flatMap(Effect.service(auth["~provider"]), (provider) => provider.descriptor === auth ? Effect.succeed(provider) : Effect.die(/* @__PURE__ */ new Error(`Authentication "${auth.name}": the binding's descriptor is not its provider's; build both from one descriptor`)));
10
+ const promote = (auth, run) => Effect.flatMap(Effect.serviceOption(auth["~verified"]), (actor) => Option.isSome(actor) ? Effect.provideService(run, auth.identity, actor.value) : Effect.fail(new Unauthenticated()));
11
+ //#endregion
12
+ export { assertAuthentication, promote, providerOf, withVerifierErrors };
@@ -0,0 +1,36 @@
1
+ import { Refusal, statuses } from "../contract/errors.mjs";
2
+ import { Cause, Context, Effect, Exit, Option, Predicate, Ref, Schema } from "effect";
3
+ import { HttpServerResponse } from "effect/http";
4
+ //#region src/authentication/refusal.ts
5
+ const rfc6750ErrorDescriptionCharacters = /^[\x20\x21\x23-\x5B\x5D-\x7E]+$/;
6
+ const challenge = (scheme, parameters) => {
7
+ const given = parameters.flatMap(([name, value]) => value === void 0 ? [] : [`${name}="${value.replace(/["\\]/g, "\\$&")}"`]);
8
+ return given.length === 0 ? scheme : `${scheme} ${given.join(", ")}`;
9
+ };
10
+ const bearer = (parameters) => challenge("Bearer", parameters);
11
+ const encode = Schema.encodeSync(Refusal);
12
+ const isRefusal = Schema.is(Refusal);
13
+ const insufficientScopeChallenge = (error, metadataUrl) => Predicate.isTagged(error, "Forbidden") && error.scopes !== void 0 ? bearer([
14
+ ["error", "insufficient_scope"],
15
+ ["scope", error.scopes.join(" ")],
16
+ ["resource_metadata", metadataUrl],
17
+ ["error_description", rfc6750ErrorDescriptionCharacters.test(error.message) ? error.message : void 0]
18
+ ]) : void 0;
19
+ const plain = (error) => HttpServerResponse.jsonUnsafe(encode(error), { status: statuses[error._tag] });
20
+ const answer = (error, metadataUrl) => {
21
+ const response = plain(error);
22
+ const scoped = insufficientScopeChallenge(error, metadataUrl);
23
+ return scoped === void 0 ? response : HttpServerResponse.setHeader(response, "www-authenticate", scoped);
24
+ };
25
+ var SteppedUpRefusal = class extends Context.Service()("effect-actions/SteppedUp") {};
26
+ const isStepUp = (error) => isRefusal(error) && (!Predicate.isTagged(error, "Forbidden") || error.scopes !== void 0);
27
+ const recordStepUp = (call) => Effect.tapError(call, (error) => isStepUp(error) ? Effect.flatMap(Effect.serviceOption(SteppedUpRefusal), (slot) => Option.isSome(slot) ? Ref.set(slot.value, Option.some(error)) : Effect.void) : Effect.void);
28
+ const answerStepUp = (route, metadataUrl) => Effect.gen(function* () {
29
+ const slot = yield* Ref.make(Option.none());
30
+ const exit = yield* Effect.exit(Effect.provideService(route, SteppedUpRefusal, slot));
31
+ const refused = yield* Ref.get(slot);
32
+ const diedOrInterruptedAfterRefusal = Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause));
33
+ return Option.isSome(refused) && !diedOrInterruptedAfterRefusal ? answer(refused.value, metadataUrl) : yield* exit;
34
+ });
35
+ //#endregion
36
+ export { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain, recordStepUp };
@@ -1,14 +1,11 @@
1
- import "./errors.mjs";
1
+ import "../contract/errors.mjs";
2
2
  import { Action_d_exports, Any } from "../Action.mjs";
3
3
  import { Effect, Runtime, Types } from "effect";
4
4
  import { CliError, Command } from "effect/cli";
5
- //#region src/internal/cli.d.ts
6
- /**
7
- * The fields of `A`'s input that may be positional: the named top-level fields of a struct
8
- * or class input, as encoded. None for any other input, a record or a union included.
9
- */
10
- export type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
11
- /** How one command is named, takes its input and prints its result. */
5
+ //#region src/cli/commands.d.ts
6
+ type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
7
+ type StdinField<A extends Any> = A["input"]["Encoded"] extends (infer E) ? Exclude<Field<A>, { [K in keyof E]-?: {} extends Pick<E, K> ? K : never; }[keyof E]> : never;
8
+ /** How one command of `A` is named, takes its input and prints its result. */
12
9
  export interface Options<A extends Any> {
13
10
  /** Override the command name. The action name, in kebab case, is used by default. */
14
11
  readonly name?: string;
@@ -24,12 +21,19 @@ export interface Options<A extends Any> {
24
21
  * `Param.withAlias` takes it.
25
22
  */
26
23
  readonly aliases?: { readonly [K in Field<A>]?: string; };
24
+ /**
25
+ * A required field read from stdin instead of a flag, such as a secret, which a flag would
26
+ * leave in the process list and the shell's history.
27
+ */
28
+ readonly stdin?: StdinField<A>;
27
29
  }
28
30
  /**
29
- * What a command fails with when its action fails: Effect CLI's own error for a handler's
30
- * failure, which `Command.run` prints on stderr and marks reported, so `runMain` does not
31
- * print it again. Its `cause` is the action's failure. The process exits with the cause's
32
- * `Runtime.errorExitCode`, 1 by default.
31
+ * What a command fails with when its action fails: Effect CLI's `UserError`, typed with the
32
+ * action's failure as its `cause` and `reason`, and whose message is its JSON, which `Command.run`
33
+ * prints on stderr and marks reported, so `runMain` does not print it again. The process exits with
34
+ * the cause's `Runtime.errorExitCode`, 1 by default. A type only: after `Command.run`, match the
35
+ * reason by its tag, `Effect.catchReason("UserError", "UserNotFound", f)`. A command of your own
36
+ * fails with one through `toUserError`; any other failure is reported as a crash, `ERROR (#n)`.
33
37
  */
34
38
  export declare class UserError<out E = unknown> extends CliError.UserError {
35
39
  readonly cause: E;
@@ -0,0 +1,302 @@
1
+ import { InvalidInput } from "../contract/errors.mjs";
2
+ import { assertDistinct, literalValues, members, projectedErrors, unsuspended } from "../contract/rules.mjs";
3
+ import { withStderrConsole } from "../stdio/console.mjs";
4
+ import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST, Stdio, Stream, flow } from "effect";
5
+ import { CliError, Command, Flag, Param } from "effect/cli";
6
+ //#region src/cli/commands.ts
7
+ const kebab = (name) => {
8
+ const kebabbed = name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
9
+ return kebabbed === "" ? name : kebabbed;
10
+ };
11
+ const renderedCommandJsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print machine-readable JSON"));
12
+ const jsonKinds = [
13
+ "null",
14
+ "boolean",
15
+ "number",
16
+ "string",
17
+ "array",
18
+ "object"
19
+ ];
20
+ const jsonKindOf = (value) => value === null ? "null" : Predicate.isBoolean(value) ? "boolean" : Predicate.isNumber(value) ? "number" : Predicate.isString(value) ? "string" : Array.isArray(value) ? "array" : "object";
21
+ const acceptedJsonKindsOf = (ast) => members(ast).flatMap((member) => {
22
+ if (SchemaAST.isLiteral(member)) return [jsonKindOf(member.literal)];
23
+ if (SchemaAST.isEnum(member)) return member.enums.map(([, value]) => jsonKindOf(value));
24
+ if (SchemaAST.isNull(member)) return ["null"];
25
+ if (SchemaAST.isBoolean(member)) return ["boolean"];
26
+ if (SchemaAST.isNumber(member)) return ["number"];
27
+ if (SchemaAST.isString(member) || SchemaAST.isTemplateLiteral(member)) return ["string"];
28
+ if (SchemaAST.isArrays(member)) return ["array"];
29
+ if (SchemaAST.isObjects(member)) return ["object"];
30
+ return jsonKinds;
31
+ });
32
+ const jsonOfAcceptedKindOrText = (encoded) => {
33
+ const accepted = new Set(acceptedJsonKindsOf(encoded));
34
+ return Schema.Union([Schema.fromJsonString(Schema.Json).check(Schema.makeFilter((json) => accepted.has(jsonKindOf(json)))), Schema.String]);
35
+ };
36
+ const stringChoicesOf = (ast) => {
37
+ const accepted = members(ast).flatMap(literalValues);
38
+ return accepted.every(Predicate.isString) ? accepted : void 0;
39
+ };
40
+ const takesTextAsIs = (encoded) => stringChoicesOf(encoded) !== void 0 || SchemaAST.isString(encoded) || SchemaAST.isTemplateLiteral(encoded);
41
+ const valueParam = (kind, name, field) => {
42
+ const encoded = unsuspended(field);
43
+ if (takesTextAsIs(encoded)) {
44
+ const literals = stringChoicesOf(encoded);
45
+ return literals === void 0 ? Param.String(kind, name) : Param.Literals(kind, name, literals);
46
+ }
47
+ if (SchemaAST.isBoolean(encoded)) return Param.Boolean(kind, name);
48
+ return Param.String(kind, name).pipe(Param.withSchema(jsonOfAcceptedKindOrText(encoded)), Param.withMetavar("value"));
49
+ };
50
+ const withoutOptionalAbsence = (ast) => {
51
+ const members = SchemaAST.isUnion(ast) ? ast.types.filter((member) => !SchemaAST.isNull(member) && !SchemaAST.isUndefined(member)) : [];
52
+ return members.length === 1 && members[0] !== void 0 ? members[0] : ast;
53
+ };
54
+ const optionalAddedNull = (plain) => {
55
+ const types = members(plain);
56
+ return types.some(SchemaAST.isUndefined) && !types.some(SchemaAST.isNull);
57
+ };
58
+ const repeatedElementOf = (ast) => {
59
+ const encoded = unsuspended(ast);
60
+ if (!SchemaAST.isArrays(encoded)) return void 0;
61
+ const [element, ...after] = encoded.rest;
62
+ if (element === void 0 || after.length > 0 || !encoded.elements.every((first) => first === element) || !acceptedJsonKindsOf(element).every((kind) => kind === "string" || kind === "number")) return;
63
+ return {
64
+ element,
65
+ min: encoded.elements.length
66
+ };
67
+ };
68
+ const noElementOccurrence = "[]";
69
+ const jsonHintOf = (encoded) => {
70
+ const value = unsuspended(encoded);
71
+ const kinds = new Set(acceptedJsonKindsOf(value));
72
+ if (kinds.size !== 1) return void 0;
73
+ if (kinds.has("object")) return "A JSON object.";
74
+ if (!kinds.has("array")) return void 0;
75
+ const fields = elementFieldsOf(value);
76
+ return fields === void 0 ? "A JSON array." : `A JSON array of {${fields.join(", ")}}.`;
77
+ };
78
+ const elementFieldsOf = (array) => {
79
+ if (!SchemaAST.isArrays(array)) return void 0;
80
+ const [element, ...others] = [...array.elements, ...array.rest];
81
+ if (element === void 0 || others.some((other) => other !== element)) return void 0;
82
+ const fields = namedFieldsOf(unsuspended(element));
83
+ return fields.size === 0 ? void 0 : Array.from(fields, ([name, field]) => SchemaAST.isOptional(field) ? `${String(name)}?` : String(name));
84
+ };
85
+ const withHint = (description, hint) => {
86
+ if (hint === void 0 || description === void 0) return description ?? hint;
87
+ const stated = description.trimEnd();
88
+ return /[.!?:]$/.test(stated) ? `${stated} ${hint}` : `${stated}. ${hint}`;
89
+ };
90
+ const fieldParam = (kind, name, encoded, plain) => {
91
+ const optional = SchemaAST.isOptional(encoded);
92
+ const value = optional && plain !== void 0 && optionalAddedNull(plain) ? withoutOptionalAbsence(encoded) : encoded;
93
+ const repeats = repeatedElementOf(value);
94
+ if (repeats !== void 0) {
95
+ const literals = stringChoicesOf(unsuspended(repeats.element));
96
+ const elements = literals === void 0 ? valueParam(kind, name, repeats.element) : Param.Literals(kind, name, [...literals, noElementOccurrence]);
97
+ const listed = (values) => Option.some(values.filter((value) => value !== noElementOccurrence));
98
+ return {
99
+ param: optional ? Param.variadic(elements).pipe(Param.map((values) => values.length === 0 ? Option.none() : listed(values))) : Param.variadic(elements, { min: repeats.min }).pipe(Param.map(listed)),
100
+ repeats: true,
101
+ hint: kind === Param.flagKind ? "Repeat for each value." : void 0
102
+ };
103
+ }
104
+ if (optional) return {
105
+ param: Param.optional(valueParam(kind, name, value)),
106
+ repeats: false,
107
+ hint: kind === Param.flagKind && SchemaAST.isBoolean(unsuspended(value)) ? `--no-${name} sends false.` : jsonHintOf(value)
108
+ };
109
+ const isSwitch = kind === Param.flagKind && SchemaAST.isBoolean(unsuspended(encoded));
110
+ return {
111
+ param: isSwitch ? Flag.Boolean(name).pipe(Flag.withDefault(false), Flag.map(Option.some)) : valueParam(kind, name, encoded).pipe(Param.map(Option.some)),
112
+ repeats: false,
113
+ hint: isSwitch ? "Left out, it sends false." : jsonHintOf(encoded)
114
+ };
115
+ };
116
+ const namedFieldsOf = (ast) => {
117
+ const fields = SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast;
118
+ return new Map(fields !== void 0 && SchemaAST.isObjects(fields) ? fields.propertySignatures.map((property) => [property.name, property.type]) : []);
119
+ };
120
+ const isTransformedAsWhole = (ast) => (SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast)?.encoding !== void 0;
121
+ const ownOrSuspendedDescriptionOf = (ast) => ast.context?.annotations?.description ?? SchemaAST.resolveDescription(ast) ?? describedBeforeLastCheck(ast) ?? (SchemaAST.isSuspend(ast) ? ownOrSuspendedDescriptionOf(ast.thunk()) : void 0);
122
+ const describedBeforeLastCheck = (ast) => [ast.annotations, ...(ast.checks ?? []).map((check) => check.annotations)].map((annotations) => annotations?.["description"]).findLast(Predicate.isString);
123
+ const describedThroughAbsence = (ast) => ownOrSuspendedDescriptionOf(ast) ?? ownOrSuspendedDescriptionOf(withoutOptionalAbsence(ast));
124
+ const describedByElement = (ast) => {
125
+ const array = unsuspended(withoutOptionalAbsence(ast));
126
+ const element = SchemaAST.isArrays(array) ? array.rest[0] : void 0;
127
+ return element === void 0 ? void 0 : describedThroughAbsence(element);
128
+ };
129
+ const fieldParams = (encoded, input, positional, aliases) => {
130
+ const plainEncodedFields = namedFieldsOf(SchemaAST.toEncoded(input));
131
+ const declaredFieldsUnlessRenamed = namedFieldsOf(isTransformedAsWhole(input) ? encoded : SchemaAST.toType(input));
132
+ return encoded.propertySignatures.map((property) => {
133
+ const field = String(property.name);
134
+ const declaredField = declaredFieldsUnlessRenamed.get(property.name);
135
+ const kind = positional.includes(field) ? Param.argumentKind : Param.flagKind;
136
+ const { param, repeats, hint } = fieldParam(kind, kebab(field), property.type, plainEncodedFields.get(property.name));
137
+ const alias = kind === Param.flagKind ? aliases[field] : void 0;
138
+ const aliased = alias === void 0 ? param : Param.withAlias(param, alias);
139
+ const documented = declaredField ?? property.type;
140
+ const description = describedThroughAbsence(documented) ?? describedThroughAbsence(property.type) ?? (repeats ? describedByElement(documented) ?? describedByElement(property.type) : void 0);
141
+ const help = withHint(description, hint);
142
+ return {
143
+ field,
144
+ param: help === void 0 ? aliased : Param.withDescription(aliased, help),
145
+ optional: SchemaAST.isOptional(property.type),
146
+ repeats,
147
+ hint,
148
+ description
149
+ };
150
+ });
151
+ };
152
+ const positionalOrder = (params, positional) => {
153
+ assertDistinct("positional argument", positional, (field) => field);
154
+ const ordered = positional.map((field) => {
155
+ const found = params.find((param) => param.field === field);
156
+ if (found === void 0) throw new Error(`Not an input field: ${field}`);
157
+ return found;
158
+ });
159
+ const firstOptional = ordered.findIndex((param) => param.optional);
160
+ const misplaced = ordered.slice(firstOptional === -1 ? ordered.length : firstOptional).find((param) => !param.optional);
161
+ if (misplaced !== void 0) throw new Error(`Required positional argument after an optional one: ${misplaced.field}`);
162
+ const repeatedBeforeLast = ordered.slice(0, -1).find((param) => param.repeats);
163
+ if (repeatedBeforeLast !== void 0) throw new Error(`Repeated positional argument before another one: ${repeatedBeforeLast.field}`);
164
+ return ordered;
165
+ };
166
+ const wholeInputFlag = (encoded) => Flag.String("input").pipe(Flag.withSchema(jsonOfAcceptedKindOrText(encoded)), Flag.withDescription("Whole action input as JSON; omitted, {}"), Flag.optional);
167
+ const printedOutput = (action, value, render) => Effect.map(Effect.orDie(Schema.encodeEffect(Schema.toCodecJson(action.success))(value)), (encoded) => render === void 0 && SchemaAST.isVoid(action.success.ast) ? Option.none() : Option.some(render === void 0 ? JSON.stringify(encoded, null, 2) : render(value)));
168
+ /**
169
+ * What a command fails with when its action fails: Effect CLI's `UserError`, typed with the
170
+ * action's failure as its `cause` and `reason`, and whose message is its JSON, which `Command.run`
171
+ * prints on stderr and marks reported, so `runMain` does not print it again. The process exits with
172
+ * the cause's `Runtime.errorExitCode`, 1 by default. A type only: after `Command.run`, match the
173
+ * reason by its tag, `Effect.catchReason("UserError", "UserNotFound", f)`. A command of your own
174
+ * fails with one through `toUserError`; any other failure is reported as a crash, `ERROR (#n)`.
175
+ */
176
+ var UserError = class extends CliError.UserError {
177
+ /**
178
+ * The action's failure, as Effect names an error's nested one, so the native
179
+ * `Effect.catchReason("UserError", "UserNotFound", f)` and `catchReasons` match it by tag.
180
+ */
181
+ get reason() {
182
+ return this.cause;
183
+ }
184
+ get [Runtime.errorExitCode]() {
185
+ return Runtime.getErrorExitCode(this.cause);
186
+ }
187
+ };
188
+ const ownStringProperty = (error, key) => {
189
+ if (!Predicate.hasProperty(error, key)) return void 0;
190
+ const value = error[key];
191
+ return Predicate.isString(value) ? value : void 0;
192
+ };
193
+ const describedWithoutFields = (error, seen = /* @__PURE__ */ new Set()) => {
194
+ const name = ownStringProperty(error, "_tag") ?? (error instanceof Error ? error.name : "Error");
195
+ const message = Predicate.isString(error) ? error : ownStringProperty(error, "message") ?? "";
196
+ const named = message === "" ? name : `${name}: ${message}`;
197
+ seen.add(error);
198
+ return Predicate.hasProperty(error, "cause") && error.cause !== void 0 && !seen.has(error.cause) ? `${named}: ${describedWithoutFields(error.cause, seen)}` : named;
199
+ };
200
+ const userErrorOf = (errors) => {
201
+ const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))));
202
+ return (error) => encode(error, { onExcessProperty: "error" }).pipe(Effect.catch(() => encode(error)), Effect.orElseSucceed(() => describedWithoutFields(error)), Effect.flatMap((userMessage) => Effect.fail(new UserError({
203
+ cause: error,
204
+ userMessage
205
+ }))));
206
+ };
207
+ const failingAsDerived = (actions, surfaceErrorsOf) => {
208
+ const failure = userErrorOf([...new Set(actions.flatMap((action) => projectedErrors(action, surfaceErrorsOf(action))))]);
209
+ return (self) => Effect.catchIf(self, (error) => !CliError.isCliError(error), failure);
210
+ };
211
+ const unreadableStdinField = (field, message) => new InvalidInput({
212
+ message,
213
+ issues: [{
214
+ path: [field],
215
+ message
216
+ }]
217
+ });
218
+ const fieldValueFromText = (field) => {
219
+ const encoded = unsuspended(field);
220
+ return takesTextAsIs(encoded) ? (text) => text : Schema.decodeSync(jsonOfAcceptedKindOrText(encoded));
221
+ };
222
+ const stdinTextOf = (field) => Effect.gen(function* () {
223
+ const stdio = yield* Effect.serviceOption(Stdio.Stdio);
224
+ if (Option.isNone(stdio)) return yield* Effect.die(/* @__PURE__ */ new Error("Stdio is not provided"));
225
+ if (yield* stdio.value.stdinIsTerminal) return yield* unreadableStdinField(field, `${field} is read from stdin, which is a terminal: pipe it in`);
226
+ return (yield* stdio.value.stdin.pipe(Stream.decodeText(), Stream.mkString, Effect.mapError((error) => unreadableStdinField(field, `stdin cannot be read: ${error.message}`)))).replace(/[\r\n]+$/, "");
227
+ });
228
+ const inputConfig = (action, positional, aliases, stdin) => {
229
+ const codec = Schema.toCodecJson(action.input);
230
+ const encoded = unsuspended(SchemaAST.toEncoded(codec.ast));
231
+ const decodeRefusingUndeclared = Schema.decodeUnknownEffect(codec, { onExcessProperty: "error" });
232
+ const decode = flow(decodeRefusingUndeclared, Effect.mapError(InvalidInput.fromSchemaError));
233
+ if (SchemaAST.isObjects(encoded) && encoded.indexSignatures.every((signature) => SchemaAST.isNever(signature.type))) {
234
+ const params = fieldParams(encoded, unsuspended(action.input.ast), positional, aliases);
235
+ const fromStdin = Option.map(Option.fromUndefinedOr(stdin), (field) => {
236
+ const property = encoded.propertySignatures.find(({ name }) => name === field);
237
+ if (property === void 0 || SchemaAST.isOptional(property.type)) throw new Error(`Not a required input field: ${field}`);
238
+ if (positional.includes(field)) throw new Error(`Both positional and stdin: ${field}`);
239
+ const description = params.find((param) => param.field === field)?.description;
240
+ return {
241
+ field,
242
+ value: fieldValueFromText(property.type),
243
+ help: `Reads ${kebab(field)} from stdin${description === void 0 ? "." : `: ${description}`}`
244
+ };
245
+ });
246
+ const flagged = new Set(params.map(({ field }) => field).filter((field) => !positional.includes(field) && field !== stdin));
247
+ const stray = Object.keys(aliases).find((field) => !flagged.has(field));
248
+ if (stray !== void 0) throw new Error(`Not a flag's input field: ${stray}`);
249
+ return {
250
+ flags: Object.fromEntries(params.flatMap(({ field, param }) => flagged.has(field) ? [[field, param]] : [])),
251
+ positional: positionalOrder(params, positional),
252
+ decode: (parsed) => Option.match(fromStdin, {
253
+ onNone: () => decode(Record.getSomes(parsed)),
254
+ onSome: ({ field, value }) => Effect.flatMap(stdinTextOf(field), (text) => decode({
255
+ ...Record.getSomes(parsed),
256
+ [field]: value(text)
257
+ }))
258
+ }),
259
+ stdinHelp: Option.getOrUndefined(Option.map(fromStdin, ({ help }) => help))
260
+ };
261
+ }
262
+ if (positional.length > 0) throw new Error(`Positional arguments need named input fields: ${positional.join(", ")}`);
263
+ if (Object.keys(aliases).length > 0) throw new Error(`Aliases need named input fields: ${Object.keys(aliases).join(", ")}`);
264
+ if (stdin !== void 0) throw new Error(`Stdin needs named input fields: ${stdin}`);
265
+ return {
266
+ flags: { input: wholeInputFlag(encoded) },
267
+ positional: [],
268
+ decode: (parsed) => decode(Option.getOrElse(parsed["input"] ?? Option.none(), () => ({}))),
269
+ stdinHelp: void 0
270
+ };
271
+ };
272
+ const command = (action, execute, options, errors = []) => {
273
+ const name = options?.name ?? kebab(action.name);
274
+ const render = options?.render;
275
+ const { flags, positional, decode, stdinHelp } = inputConfig(action, options?.positional ?? [], options?.aliases ?? {}, options?.stdin);
276
+ const failure = userErrorOf(projectedErrors(action, errors));
277
+ assertDistinct("flag", [
278
+ ...Object.keys(flags).map((field) => [`--${kebab(field)}`, `field ${field}`]),
279
+ ...render === void 0 ? [] : [["--json", "render's --json"]],
280
+ ...Object.entries(options?.aliases ?? {}).flatMap(([field, alias]) => Predicate.isString(alias) ? [[`--${alias.replace(/^-+/, "")}`, `alias of field ${field}`]] : [])
281
+ ], ([flag]) => flag, ([, claimant]) => claimant);
282
+ const args = positional.map(({ param }) => param);
283
+ const parsedFields = (input, values) => ({
284
+ ...input,
285
+ ...Object.fromEntries(positional.map(({ field }, index) => [field, values[index] ?? Option.none()]))
286
+ });
287
+ const run = (parsed, rendered) => decode(parsed).pipe(Effect.flatMap(execute), Effect.catch(failure), Effect.flatMap((value) => printedOutput(action, value, rendered)), withStderrConsole, Effect.flatMap(Option.match({
288
+ onNone: () => Effect.void,
289
+ onSome: Console.log
290
+ })));
291
+ const command = render === void 0 ? Command.make(name, {
292
+ input: flags,
293
+ args
294
+ }, ({ input, args: values }) => run(parsedFields(input, values), void 0)) : Command.make(name, {
295
+ input: flags,
296
+ args,
297
+ json: renderedCommandJsonFlag
298
+ }, ({ input, args: values, json }) => run(parsedFields(input, values), json ? void 0 : render));
299
+ return stdinHelp === void 0 ? Command.withDescription(command, action.description) : command.pipe(Command.withDescription(`${action.description}\n\n${stdinHelp}`), Command.withShortDescription(action.description));
300
+ };
301
+ //#endregion
302
+ export { UserError, command, failingAsDerived, kebab };
@@ -1,12 +1,12 @@
1
1
  import "./implementation.mjs";
2
2
  import { Action_d_exports, Any } from "../Action.mjs";
3
3
  import { Effect, Schema } from "effect";
4
- //#region src/internal/call.d.ts
4
+ //#region src/contract/call.d.ts
5
5
  /**
6
6
  * A call of `A` answering `R`. It may leave its input out when `{}` is a valid encoded input,
7
7
  * such as for an action declared without `input` or one whose every field has a decoding
8
8
  * default, and then sends the input `{}` decodes to. Every client shares this rule:
9
- * `ActionHttp.client`, `Testing.mcpClient` and `Action.client`.
9
+ * `ActionHttp.client`, `ActionRpc.client`, `Testing.mcpClient` and `Action.client`.
10
10
  */
11
11
  export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
12
12
  //#endregion
@@ -0,0 +1,10 @@
1
+ import { InvalidInput } from "./errors.mjs";
2
+ import { Effect, Schema } from "effect";
3
+ //#region src/contract/call.ts
4
+ const inputOf = (action, args) => args.length === 0 ? Schema.decodeUnknownEffect(Schema.toCodecJson(action.input))({}) : Effect.succeed(args[0]);
5
+ const checked = (action, send) => {
6
+ const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(action.input), { errors: "all" });
7
+ return (...args) => inputOf(action, args).pipe(Effect.tap(encode), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap(send));
8
+ };
9
+ //#endregion
10
+ export { checked, inputOf };
@@ -1,14 +1,14 @@
1
1
  import { Schema } from "effect";
2
- //#region src/internal/errors.d.ts
3
- /**
4
- * One thing wrong with an input: where, as the keys and indexes leading to it from the input's
5
- * root, none for the input itself, and what the schema expects there.
6
- */
2
+ //#region src/contract/errors.d.ts
7
3
  declare const Issue: Schema.Struct<{
8
4
  readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
9
5
  readonly message: Schema.String;
10
6
  }>;
11
- /** One thing wrong with an input, as `InvalidInput.issues` lists it. */
7
+ /**
8
+ * One thing wrong with an input, as `InvalidInput.issues` lists it: where, as the keys and
9
+ * indexes leading to it from the input's root, none for the input itself, and what the schema
10
+ * expects there.
11
+ */
12
12
  type Issue = (typeof Issue)["Type"];
13
13
  declare const InvalidInput_base: Schema.Class<InvalidInput, Schema.TaggedStruct<"InvalidInput", {
14
14
  readonly message: Schema.withConstructorDefault<Schema.String>;
@@ -24,7 +24,7 @@ declare const InvalidInput_base: Schema.Class<InvalidInput, Schema.TaggedStruct<
24
24
  * failing with it for input that decodes but cannot be served may name its own.
25
25
  */
26
26
  export declare class InvalidInput extends InvalidInput_base {
27
- private readonly "~builtIn";
27
+ private readonly "~declared in the action's error, or built in";
28
28
  /**
29
29
  * The `InvalidInput` for input that did not decode, as every surface answers it: the
30
30
  * schema's message, and each of its issues by path, for code that decodes input of its own,
@@ -40,7 +40,7 @@ declare const Unauthenticated_base: Schema.Class<Unauthenticated, Schema.TaggedS
40
40
  }>, import("effect/Cause").YieldableError>;
41
41
  /** The caller is not authenticated: authentication or an implementation's `authorize` refuses. */
42
42
  export declare class Unauthenticated extends Unauthenticated_base {
43
- private readonly "~builtIn";
43
+ private readonly "~declared in the action's error, or built in";
44
44
  }
45
45
  declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbidden", {
46
46
  readonly message: Schema.withConstructorDefault<Schema.String>;
@@ -49,10 +49,13 @@ declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbi
49
49
  /**
50
50
  * The caller may not run this action: an implementation's `authorize` refuses. `scopes`,
51
51
  * when given, are every OAuth scope the call needs: a refusal naming them is answered with
52
- * the `insufficient_scope` challenge an OAuth client re-authorizes on.
52
+ * the `insufficient_scope` challenge an OAuth client re-authorizes on, so name them only where
53
+ * re-authorizing can grant them. Over HTTP it is a 403 either way. Over MCP, from an authorizer
54
+ * or a handler, it is a 403 only when it names scopes on an endpoint under Bearer, and
55
+ * otherwise a tool result with `isError: true`, in a 200 response.
53
56
  */
54
57
  export declare class Forbidden extends Forbidden_base {
55
- private readonly "~builtIn";
58
+ private readonly "~declared in the action's error, or built in";
56
59
  }
57
60
  /**
58
61
  * What authentication or an implementation's `authorize` refuses a caller with, instead of
@@ -1,17 +1,11 @@
1
1
  import { Effect, Predicate, Schema, SchemaIssue } from "effect";
2
- //#region src/internal/errors.ts
3
- /** A message field that defaults, so `new Forbidden()` needs no argument. */
4
- const message = (fallback) => Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(fallback)));
5
- /** Each built-in error's HTTP status, by tag: its annotation, and what a refusal answers. */
2
+ //#region src/contract/errors.ts
3
+ const defaultedMessage = (fallback) => Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(fallback)));
6
4
  const statuses = {
7
5
  InvalidInput: 400,
8
6
  Unauthenticated: 401,
9
7
  Forbidden: 403
10
8
  };
11
- /**
12
- * One thing wrong with an input: where, as the keys and indexes leading to it from the input's
13
- * root, none for the input itself, and what the schema expects there.
14
- */
15
9
  const Issue = Schema.Struct({
16
10
  path: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
17
11
  message: Schema.String
@@ -24,7 +18,7 @@ const standardIssues = SchemaIssue.makeFormatterStandardSchemaV1();
24
18
  * failing with it for input that decodes but cannot be served may name its own.
25
19
  */
26
20
  var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput", {
27
- message: message("The input does not match the action's input."),
21
+ message: defaultedMessage("The input does not match the action's input."),
28
22
  issues: Schema.Array(Issue).pipe(Schema.withConstructorDefault(Effect.succeed([])))
29
23
  }, { httpApiStatus: statuses.InvalidInput }) {
30
24
  /**
@@ -47,20 +41,21 @@ var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput
47
41
  });
48
42
  };
49
43
  /** The caller is not authenticated: authentication or an implementation's `authorize` refuses. */
50
- var Unauthenticated = class extends Schema.TaggedError()("Unauthenticated", { message: message("Authentication is required.") }, { httpApiStatus: statuses.Unauthenticated }) {};
51
- /** An OAuth scope token (RFC 6749 §3.3): printable ASCII but space, `"` and `\`. */
44
+ var Unauthenticated = class extends Schema.TaggedError()("Unauthenticated", { message: defaultedMessage("Authentication is required.") }, { httpApiStatus: statuses.Unauthenticated }) {};
52
45
  const scopeToken = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
53
46
  const ScopeToken = Schema.String.check(Schema.isPattern(scopeToken));
54
47
  /**
55
48
  * The caller may not run this action: an implementation's `authorize` refuses. `scopes`,
56
49
  * when given, are every OAuth scope the call needs: a refusal naming them is answered with
57
- * the `insufficient_scope` challenge an OAuth client re-authorizes on.
50
+ * the `insufficient_scope` challenge an OAuth client re-authorizes on, so name them only where
51
+ * re-authorizing can grant them. Over HTTP it is a 403 either way. Over MCP, from an authorizer
52
+ * or a handler, it is a 403 only when it names scopes on an endpoint under Bearer, and
53
+ * otherwise a tool result with `isError: true`, in a 200 response.
58
54
  */
59
55
  var Forbidden = class extends Schema.TaggedError()("Forbidden", {
60
- message: message("Not allowed."),
56
+ message: defaultedMessage("Not allowed."),
61
57
  scopes: Schema.optionalKey(Schema.NonEmptyArray(ScopeToken))
62
58
  }, { httpApiStatus: statuses.Forbidden }) {};
63
- /** The refusals, as schemas. */
64
59
  const refusals = [Unauthenticated, Forbidden];
65
60
  /**
66
61
  * What authentication or an implementation's `authorize` refuses a caller with, instead of