@gjermundgaraba/effect-actions 0.11.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 (215) hide show
  1. package/README.md +101 -40
  2. package/dist/Action.d.mts +29 -117
  3. package/dist/Action.mjs +14 -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 +23 -29
  9. package/dist/ActionMcp.mjs +56 -114
  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 +28 -23
  17. package/dist/Testing.mjs +27 -44
  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/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
  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 -426
  46. package/docs/ActionCli-failures.md +102 -0
  47. package/docs/ActionCli.md +206 -293
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +202 -282
  50. package/docs/ActionMcp-failures.md +31 -0
  51. package/docs/ActionMcp.md +222 -271
  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 -119
  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 -24
  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 -96
  64. package/docs/setup-failures.md +34 -0
  65. package/docs/setup.md +87 -91
  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 +23 -5
  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 -14
  211. package/dist/internal/media.d.mts +0 -8
  212. package/dist/internal/media.mjs +0 -26
  213. package/dist/internal/refusal.mjs +0 -67
  214. package/dist/internal/schema-errors.mjs +0 -20
  215. package/dist/internal/tools.mjs +0 -44
@@ -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
@@ -1,25 +1,28 @@
1
- import { Anyone } from "./actions.mjs";
1
+ import { Anyone } from "./rules.mjs";
2
2
  import { Refusal } from "./errors.mjs";
3
3
  import { Action_d_exports, Any } from "../Action.mjs";
4
4
  import { Context, Effect, Layer, Scope } from "effect";
5
- //#region src/internal/implementation.d.ts
5
+ //#region src/contract/implementation.d.ts
6
6
  /** Decoded values at the dispatch boundary every surface shares. */
7
7
  export type ErasedValue = Any["input"]["Type"];
8
+ /**
9
+ * A handler, erased. Method extraction makes the argument bivariant: a surface selects the
10
+ * action before it invokes a handler, so its decoded input is the matching handler input, and
11
+ * handlers are not widened at their public API.
12
+ */
8
13
  export type ErasedHandler<R> = {
9
14
  handle(input: ErasedValue): Effect.Effect<ErasedValue, ErasedValue, R>;
10
15
  }["handle"];
11
- /** Authorization runs only for protected actions, after authentication and before the handler. */
16
+ /**
17
+ * Authorization for protected contracts: it refuses, and fails with nothing else. It runs only for
18
+ * protected actions, after authentication and before the handler.
19
+ */
12
20
  export type Authorize<A extends Any, R = never> = (action: A) => Effect.Effect<void, Refusal, R>;
13
21
  /** An implementation's authorizer, erased. */
14
22
  export type ErasedAuthorize = (action: Any) => Effect.Effect<void, unknown, unknown>;
15
23
  /** Each action of an implementation with its handler, as its builder made them. */
16
24
  export type Bound = ReadonlyArray<readonly [Any, ErasedHandler<unknown>]>;
17
- /**
18
- * What a layer builds once per layer graph, under a key private to it: an implementation's
19
- * handlers, or its authorizer. Effect memoizes a layer by reference within one layer graph, so
20
- * everything in the graph holding this one shares one run.
21
- */
22
- export interface Memoized<S, E, R> {
25
+ interface Memoized<S, E, R> {
23
26
  readonly key: Context.Key<S, S>;
24
27
  readonly layer: Layer.Layer<S, E, R>;
25
28
  }
@@ -81,7 +84,13 @@ export declare class Implementation<A extends Any, RequestServices extends {
81
84
  /** `app`, if this copy of the module made it; another installed copy's cannot be read. */
82
85
  private static own;
83
86
  }
84
- /** Any implementation, with its actions and channels erased. */
87
+ /**
88
+ * Any implementation, with its actions and channels erased: the constraint of a helper generic over
89
+ * implementations, `<App extends Action.AnyImplementation>`, whatever surface it passes them to;
90
+ * `AnyImplementation<A>` is one of the actions `A`. Each parameter is covariant, so `unknown`
91
+ * admits every implementation, while a value of this type owes `unknown`, which no surface can be
92
+ * given.
93
+ */
85
94
  export type AnyImplementation<A extends Any = Any> = Implementation<A, {
86
95
  readonly [name: string]: unknown;
87
96
  }, unknown, unknown, unknown, unknown, unknown>;
@@ -101,6 +110,16 @@ export interface Selection<A extends Any> {
101
110
  export type Selected<O, All extends Any> = O extends {
102
111
  readonly actions: ReadonlyArray<infer A extends Any>;
103
112
  } ? A : All;
113
+ /**
114
+ * The middleware options `O` install, each a member of `L`: its `middleware` when always given,
115
+ * an array of unknown length when it may be absent, which installs none or all of them, and
116
+ * none otherwise.
117
+ */
118
+ export type MiddlewareOf<O, L extends ReadonlyArray<unknown>> = O extends {
119
+ readonly middleware: infer M extends L;
120
+ } ? M : O extends {
121
+ readonly middleware?: infer M extends L | undefined;
122
+ } ? "middleware" extends keyof O ? ReadonlyArray<NonNullable<M>[number]> : [] : [];
104
123
  /** The actions options `O` select of the implementations `Apps` stand for. */
105
124
  export type SelectedOf<O, Apps extends Served> = Selected<O, ActionOf<Member<Apps>>>;
106
125
  /**
@@ -109,7 +128,6 @@ export type SelectedOf<O, Apps extends Served> = Selected<O, ActionOf<Member<App
109
128
  * given or not; every one where no member lists any.
110
129
  */
111
130
  export type Offered<O, All extends Any> = [Listing<O>] extends [never] ? All : Extract<All, Listing<O> extends ReadonlyArray<infer A extends Any> ? A : never>;
112
- /** The lists of actions the members of `O` may give; none where no member gives one. */
113
131
  type Listing<O> = O extends unknown ? NonNullable<O["actions" & keyof O]> : never;
114
132
  /**
115
133
  * No key of any member of `O` beyond those of `Keys`, so a misspelled option is refused
@@ -127,7 +145,6 @@ export type OptionalUnless<O, Given> = {} extends O ? [options?: Given] : [optio
127
145
  export type ActionOf<App> = App extends {
128
146
  readonly actions: ReadonlyArray<infer A>;
129
147
  } ? A : never;
130
- /** Whether `A` may be one of `Listed`: either is assignable to the other. */
131
148
  type Matches<A extends Any, Listed extends Any> = Listed extends Any ? [A] extends [Listed] ? true : [Listed] extends [A] ? true : never : never;
132
149
  /**
133
150
  * The actions of `App` a surface given the actions `Listed` may serve, as `select` selects
@@ -147,12 +164,7 @@ export type Holding<App, Listed extends Any> = App extends unknown ? [Serving<Ap
147
164
  /** The protected members of a contract union. */
148
165
  export type Protected<A extends Any> = A extends unknown ? A["caller"] extends typeof Anyone ? never : A : never;
149
166
  type ServiceOf<K> = K extends Context.Key<infer I, unknown> ? I : never;
150
- /**
151
- * The identity required by the contract, even when its handler never reads it. A `caller`
152
- * narrowed to `Anyone`, such as `Action.Any & { caller: typeof Anyone }`'s, which TypeScript
153
- * keeps as `typeof Anyone | (Key & typeof Anyone)`, requires none.
154
- */
155
- export type AuthenticationOf<A extends Any> = A["caller"] extends typeof Anyone ? never : ServiceOf<A["caller"]>;
167
+ type AuthenticationOf<A extends Any> = A["caller"] extends typeof Anyone ? never : ServiceOf<A["caller"]>;
156
168
  /**
157
169
  * Per-request requirements of `App`'s authorization and handler for each `A` it
158
170
  * implements: those of every name `A`'s may be, so a union or an erased name owes each it may
@@ -163,14 +175,18 @@ export type RequestOf<App, A extends Any> = App extends {
163
175
  readonly "~authorizeRequest": infer RA;
164
176
  } ? A extends Any ? [A["name"] & keyof R] extends [never] ? never : R[A["name"] & keyof R] | (A["caller"] extends typeof Anyone ? never : RA) | AuthenticationOf<A> : never : never;
165
177
  type OwnActions<App> = Extract<ActionOf<App>, Any>;
166
- /** Builder failures include authorization only if selected protected actions need it. */
178
+ /**
179
+ * What a surface serving the actions `Listed` of the implementations `App` fails with at startup:
180
+ * their builders' and built authorizers' failures, an authorizer's only if a selected protected
181
+ * action needs it.
182
+ */
167
183
  export type BuildError<App, Listed extends Any = OwnActions<App>> = App extends {
168
184
  readonly "~buildError": infer EX;
169
185
  readonly "~authorizeBuildError": infer EAX;
170
186
  } ? EX | ([Protected<Serving<App, Listed>>] extends [never] ? never : EAX) : never;
171
187
  /**
172
- * What a surface serving the actions `Listed` reads at startup: the services the builders of
173
- * their handlers and authorization read.
188
+ * What a surface serving the actions `Listed` of the implementations `App` reads at startup: their
189
+ * builders' and built authorizers' services.
174
190
  */
175
191
  export type BuildServices<App, Listed extends Any = OwnActions<App>> = App extends {
176
192
  readonly "~buildServices": infer RX;
@@ -1,13 +1,8 @@
1
1
  import { Unauthenticated } from "./errors.mjs";
2
- import { Anyone } from "./actions.mjs";
2
+ import { Anyone, assertOnce } from "./rules.mjs";
3
3
  import { Array, Context, Effect, Layer, Option } from "effect";
4
- //#region src/internal/implementation.ts
5
- /**
6
- * A random key segment, unique to its caller even across installed copies of this module,
7
- * where a counter of each copy would repeat another's.
8
- */
4
+ //#region src/contract/implementation.ts
9
5
  const uniqueKey = () => Math.random().toString(36).slice(2);
10
- /** What `build` makes, as a layer of its own. */
11
6
  const memoized = (build) => {
12
7
  const key = Context.Service(`effect-actions/Implementation/${uniqueKey()}`);
13
8
  return {
@@ -76,12 +71,7 @@ var Implementation = class Implementation {
76
71
  return app;
77
72
  }
78
73
  };
79
- /** A surface's implementations as a list. */
80
74
  const toList = (served) => Array.ensure(served);
81
- /**
82
- * Refuse an action of `listed` that `held` lacks, by identity, as a stale list, marking one
83
- * of a held name as another contract's.
84
- */
85
75
  const assertHeld = (why, listed, held) => {
86
76
  const missing = listed.filter((action) => !held.includes(action));
87
77
  if (missing.length > 0) {
@@ -89,12 +79,6 @@ const assertHeld = (why, listed, held) => {
89
79
  throw new Error(`Listed in actions, but ${why}: ${missing.map(({ name }) => names.has(name) ? `${name} (another contract)` : name).join(", ")}`);
90
80
  }
91
81
  };
92
- /**
93
- * Each of `apps` narrowed to the actions `listed` holds, matched by identity, behind its own
94
- * authorization and sharing its builder; one holding none of them is
95
- * left out, and not built. All of `apps` as they are when nothing is listed. A listed action
96
- * none of them holds is refused, as a stale list or another contract of its name.
97
- */
98
82
  const select = (apps, listed) => {
99
83
  if (listed === void 0) return apps;
100
84
  assertHeld("no implementation holds it", listed, apps.flatMap((app) => app.actions));
@@ -104,32 +88,24 @@ const select = (apps, listed) => {
104
88
  return own.length === app.actions.length ? [app] : [Implementation.share(own, app)];
105
89
  });
106
90
  };
107
- /**
108
- * Provide `layer` the handlers of `apps`. Each implementation's layer is memoized, so its
109
- * builder runs once per layer graph however many surfaces serve it.
110
- */
91
+ const servedBy = (what, bound, apps, listed) => {
92
+ if (listed !== void 0) assertHeld("the binding does not hold it", listed, bound);
93
+ const held = apps.flatMap((app) => app.actions);
94
+ const served = select(apps, listed ?? bound.filter((action) => held.includes(action)));
95
+ const actions = served.flatMap((app) => app.actions);
96
+ if (actions.length === 0 && listed === void 0 && apps.length > 0) {
97
+ const names = new Set(bound.map(({ name }) => name));
98
+ throw new Error(`No action of these implementations is in this ${what}: ${held.map(({ name }) => names.has(name) ? `${name} (another contract)` : name).join(", ") || "none"}`);
99
+ }
100
+ assertOnce("served action", actions);
101
+ return served;
102
+ };
111
103
  const provideHandlers = (apps) => (layer) => {
112
104
  const [first, ...rest] = apps.map((app) => Implementation.layerOf(app));
113
105
  return first === void 0 ? layer : Layer.provide(layer, Layer.mergeAll(first, ...rest));
114
106
  };
115
- /**
116
- * The builders of `apps` as one layer providing nothing: built above the surfaces, it runs
117
- * each builder once for all of them, since every surface builds the same memoized layers.
118
- */
119
107
  const builders = (apps) => Layer.empty.pipe(provideHandlers(apps));
120
- /**
121
- * Every action `apps` serve with its handler, behind its authorizer, as `provideHandlers` built
122
- * them.
123
- */
124
108
  const acquire = (apps) => Effect.map(Effect.forEach(apps, (app) => Implementation.boundOf(app)), (bound) => bound.flat());
125
- /**
126
- * Every action `apps` serve with its handler, behind its authorizer, their builders built as layers
127
- * of the graph being built around the caller, into its memo map, as `HttpRouter` builds a
128
- * middleware's dependencies: a builder acquiring them shares each builder's one run with the
129
- * surfaces of its graph, whichever builds first. Outside any graph, a program under
130
- * `Effect.provide` shares that layer's, and one under none builds into a map of its own. The
131
- * caller's scope holds them; a surface serving them too keeps them until it is released.
132
- */
133
109
  const built = (apps) => {
134
110
  const [first, ...rest] = apps.map((app) => Implementation.layerOf(app));
135
111
  return Effect.gen(function* () {
@@ -141,27 +117,20 @@ const built = (apps) => {
141
117
  return yield* Effect.provideContext(acquire(apps), context);
142
118
  });
143
119
  };
144
- /**
145
- * One action's handler behind its admission: a protected action's identity, then its
146
- * `authorize`. These run outside the action's span, so what they fail with is attributed to
147
- * the surface rather than to a handler that never ran. Each call has a scope of its own, on
148
- * every surface: what `authorize` and the handler acquire is released when the call ends, the
149
- * handler's first, so no call needs a `Scope` of its caller.
150
- */
151
120
  const dispatch = (action, handle, before) => {
152
- const attributes = {
121
+ const actionAttributes = {
153
122
  "action.name": action.name,
154
123
  "action.read_only": action.readOnly
155
124
  };
156
125
  const authentication = action.caller === Anyone ? Effect.void : Effect.flatMap(Effect.serviceOption(action.caller), (actor) => Option.isSome(actor) ? Effect.void : Effect.fail(new Unauthenticated()));
157
126
  const authorization = action.caller === Anyone ? Effect.void : Effect.suspend(() => before(action));
158
127
  return (input) => {
159
- const handled = Effect.withSpan(Effect.annotateLogs(Effect.suspend(() => handle(input)), attributes), action.name, {
128
+ const handled = Effect.withSpan(Effect.annotateLogs(Effect.suspend(() => handle(input)), actionAttributes), action.name, {
160
129
  captureStackTrace: false,
161
- attributes
130
+ attributes: actionAttributes
162
131
  });
163
132
  return Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(handled)));
164
133
  };
165
134
  };
166
135
  //#endregion
167
- export { Implementation, acquire, assertHeld, builders, built, memoized, provideHandlers, select, toList, uniqueKey };
136
+ export { Implementation, acquire, assertHeld, builders, built, memoized, provideHandlers, select, servedBy, toList };
@@ -0,0 +1,13 @@
1
+ import { Schema, SchemaAST } from "effect";
2
+ //#region src/contract/media.d.ts
3
+ /**
4
+ * An image, a media field of a success: `{ data: Uint8Array, mimeType: string }`, `mimeType` such
5
+ * as `image/png`. A top-level field of a struct success, optional or an array, or the whole
6
+ * success, one or an array: an MCP tool lifts it into an image block of its own, and every other
7
+ * surface sends it as JSON, the bytes in base64. `ActionMcp` refuses it anywhere else.
8
+ */
9
+ export declare const Image: Schema.Struct<{
10
+ readonly data: Schema.Uint8Array;
11
+ readonly mimeType: Schema.String;
12
+ }>;
13
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import { Schema } from "effect";
2
+ //#region src/contract/media.ts
3
+ const mediaKindAnnotation = "effect-actions/media";
4
+ const Media = Schema.Struct({
5
+ data: Schema.Uint8Array,
6
+ mimeType: Schema.String
7
+ });
8
+ /**
9
+ * An image, a media field of a success: `{ data: Uint8Array, mimeType: string }`, `mimeType` such
10
+ * as `image/png`. A top-level field of a struct success, optional or an array, or the whole
11
+ * success, one or an array: an MCP tool lifts it into an image block of its own, and every other
12
+ * surface sends it as JSON, the bytes in base64. `ActionMcp` refuses it anywhere else.
13
+ */
14
+ const Image = Media.annotate({ [mediaKindAnnotation]: "image" });
15
+ const isMarked = (ast) => ast.annotations?.[mediaKindAnnotation] === "image";
16
+ const kindOf = (ast) => isMarked(ast) && ast.encoding === void 0 ? "image" : void 0;
17
+ //#endregion
18
+ export { Image, Media, isMarked, kindOf };
@@ -0,0 +1,25 @@
1
+ import { Action_d_exports, Errors } from "../Action.mjs";
2
+ import { SchemaAST, Types } from "effect";
3
+ //#region src/contract/rules.d.ts
4
+ type ListOf<G> = G extends Errors ? G : readonly [G];
5
+ /**
6
+ * The errors options `O` declare, as at run time: those `error` always gives, or, where it may
7
+ * be absent, those it gives or none, so clients decode each that may arrive.
8
+ */
9
+ export type ErrorsOf<O> = O extends unknown ? "error" extends keyof O ? O extends {
10
+ readonly error: infer G extends Errors | Errors[number];
11
+ } ? ListOf<G> : ListOf<Extract<O["error" & keyof O], Errors | Errors[number]>> | [] : [] : never;
12
+ type Single<T> = true extends Types.IsUnion<T> ? never : T;
13
+ /**
14
+ * The errors a binding of errors `E` surely declares: each slot of one error in a list of
15
+ * fixed length, never one of an array of unknown length, which may be empty, nor of a list
16
+ * `E` may be one of several.
17
+ */
18
+ export type Certain<E extends Errors> = true extends Types.IsUnion<E> ? never : number extends E["length"] ? never : { readonly [K in keyof E]: Single<E[K]>; }[number];
19
+ /**
20
+ * The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
21
+ * authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
22
+ * agree on a name.
23
+ */
24
+ export declare const Anyone: unique symbol;
25
+ //#endregion
@@ -1,37 +1,22 @@
1
1
  import { builtIns, statuses } from "./errors.mjs";
2
2
  import { Predicate, Schema, SchemaAST } from "effect";
3
- //#region src/internal/actions.ts
4
- /**
5
- * The list an `error` option stands for: one schema, a list as given, or none, as
6
- * `HttpApiEndpoint` takes it.
7
- */
3
+ //#region src/contract/rules.ts
8
4
  const errorList = (error) => error === void 0 ? [] : Schema.isSchema(error) ? [error] : error;
9
- /**
10
- * What a projection of an action declares: the built-in errors, the action's own, and any
11
- * its surface adds, such as a binding's. The built-ins come first, since a failure encodes
12
- * and decodes with the first schema that accepts it, and they are classes, which accept
13
- * only their own instances: a loose schema of the action's never captures one. A schema
14
- * listed twice is not repeated.
15
- */
16
5
  const projectedErrors = (action, surface = []) => [.../* @__PURE__ */ new Set([
17
6
  ...builtIns,
18
7
  ...action.error,
19
8
  ...surface
20
9
  ])];
21
10
  /**
22
- * The caller of a public action: anyone, signed in or not. Registered globally, so two copies
23
- * of the package agree on it, as they agree on a name.
11
+ * The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
12
+ * authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
13
+ * agree on a name.
24
14
  */
25
15
  const Anyone = Symbol.for("@gjermundgaraba/effect-actions/Anyone");
26
16
  const validName = /^[A-Za-z0-9_-]{1,128}$/;
27
- /** Names become path segments, OpenAPI identifiers and client method keys. */
28
17
  const assertName = (what, name) => {
29
18
  if (!validName.test(name) || name === "then") throw new Error(`Invalid ${what}: ${name}`);
30
19
  };
31
- /**
32
- * Names are checked by whoever owns them: an implementation, a binding, a CLI, or the MCP
33
- * tools. `claimantOf` says who claims a name, so a clash between two claimants names both.
34
- */
35
20
  const assertDistinct = (what, items, nameOf, claimantOf = nameOf) => {
36
21
  const seen = /* @__PURE__ */ new Map();
37
22
  for (const item of items) {
@@ -45,44 +30,23 @@ const assertDistinct = (what, items, nameOf, claimantOf = nameOf) => {
45
30
  seen.set(name, claimant);
46
31
  }
47
32
  };
48
- /** Refuse an action name `actions` hold twice. */
49
33
  const assertOnce = (what, actions) => assertDistinct(what, actions, (action) => action.name);
50
- /**
51
- * Refuse a key of options keyed by action name, such as handlers, tools or commands, that no
52
- * action of `names` has, so a stale option cannot outlive its action.
53
- */
54
34
  const assertKnown = (what, keys, names) => {
55
35
  const unknown = keys.filter((key) => !names.includes(key));
56
36
  if (unknown.length > 0) throw new Error(`Unknown ${what}: ${unknown.join(", ")}`);
57
37
  };
58
- /** `ast` with any suspension at its top resolved, as a recursive schema's is. */
59
38
  const unsuspended = (ast) => SchemaAST.isSuspend(ast) ? unsuspended(ast.thunk()) : ast;
60
- /** The members of a union, nested and suspended ones included, or the one type otherwise. */
61
39
  const members = (ast) => {
62
40
  const resolved = unsuspended(ast);
63
41
  return SchemaAST.isUnion(resolved) ? resolved.types.flatMap(members) : [resolved];
64
42
  };
65
- /**
66
- * The values a literal or an enum accepts, or `undefined` for any other type, which has no
67
- * fixed set.
68
- */
69
43
  const literalValues = (ast) => SchemaAST.isLiteral(ast) ? [ast.literal] : SchemaAST.isEnum(ast) ? ast.enums.map(([, value]) => value) : [void 0];
70
- /**
71
- * The `_tag`s a schema's encoding carries: each member's of a union, every value of a union of
72
- * literals or of an enum, and a suspended schema's, as a recursive error is written.
73
- */
74
- const tagsOf = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member) => {
44
+ const encodedTags = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member) => {
75
45
  const tag = SchemaAST.isObjects(member) ? member.propertySignatures.find((property) => property.name === "_tag")?.type : void 0;
76
46
  return tag === void 0 ? [] : members(tag).flatMap(literalValues).filter(Predicate.isString);
77
47
  });
78
- /**
79
- * Refuse an error that encodes with a built-in error's `_tag`, the built-in itself included:
80
- * every endpoint and tool declares the built-in errors already, a client decoding the answer
81
- * could not tell a look-alike from them, and a look-alike of a refusal would step up. Checked
82
- * where the action or the binding is made, so every surface and client of it may rely on it.
83
- */
84
48
  const assertOwnTags = (what, errors) => {
85
- const tag = errors.flatMap((error) => tagsOf(error.ast)).find((tag) => Object.hasOwn(statuses, tag));
49
+ const tag = errors.flatMap((error) => encodedTags(error.ast)).find((tag) => Object.hasOwn(statuses, tag));
86
50
  if (tag !== void 0) throw new Error(`${what}: error _tag "${tag}" is built in, and declared on every surface`);
87
51
  };
88
52
  //#endregion
@@ -1,19 +1,17 @@
1
- import { BuiltIns } from "./errors.mjs";
2
- import "./implementation.mjs";
3
- import { Call } from "./call.mjs";
1
+ import { BuiltIns } from "../contract/errors.mjs";
2
+ import { Call } from "../contract/call.mjs";
4
3
  import { Action_d_exports, Any, Errors } from "../Action.mjs";
5
- import { Any as Any$1, VerifierError } from "./authentication.mjs";
4
+ import { Any as Any$1, VerifierError } from "../authentication/provider.mjs";
6
5
  import { Effect, Schema } from "effect";
7
6
  import { HttpClient, HttpClientError } from "effect/http";
8
7
  import { HttpApi, HttpApiClient } from "effect/http-api";
9
- //#region src/internal/client.d.ts
8
+ //#region src/http/client.d.ts
10
9
  /**
11
10
  * The native `HttpApiClient.make` options except `transformResponse`: `baseUrl` and
12
11
  * `transformClient`. `transformResponse` may change a call's success, failure or
13
12
  * services, which the method types cannot follow.
14
13
  */
15
14
  export type Options = Omit<NonNullable<Parameters<typeof HttpApiClient.make>[1]>, "transformResponse">;
16
- /** An error schema the binding declares on every endpoint. */
17
15
  type BindingError = Errors[number];
18
16
  /**
19
17
  * What one call of `A` fails with: a declared error value (the action's own, one `E` of the
@@ -22,11 +20,11 @@ type BindingError = Errors[number];
22
20
  * the success does not decode. Input that does not encode is `InvalidInput`, and nothing is sent.
23
21
  */
24
22
  export type MethodError<A extends Any, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | HttpClientError.HttpClientError | Schema.SchemaError;
25
- /** One action as an Effect of its decoded success, failing with `MethodError`. */
26
23
  type Method<A extends Any, E extends BindingError> = Call<A, Effect.Effect<A["success"]["Type"], MethodError<A, E>>>;
27
24
  /**
28
- * Any HTTP binding. The API is a native constraint here because native groups are
29
- * invariant in their endpoints; `actions` carries the types.
25
+ * Any HTTP binding, with its actions erased: what `layer` and `client` read. The API is a native
26
+ * constraint here because native groups are invariant in their endpoints; `actions` carries the
27
+ * types.
30
28
  */
31
29
  export interface AnyHttp {
32
30
  readonly authentication?: Any$1 | undefined;
@@ -0,0 +1,17 @@
1
+ import { checked } from "../contract/call.mjs";
2
+ import { Effect } from "effect";
3
+ import { HttpApiClient } from "effect/http-api";
4
+ //#region src/http/client.ts
5
+ const assertInBinding = (actions, action) => {
6
+ if (!actions.includes(action)) throw new Error(`Action "${action.name}" is not in this HTTP binding`);
7
+ };
8
+ const methods = (http, options = {}) => Effect.map(HttpApiClient.make(http.api, options), (client) => {
9
+ const native = client;
10
+ return (action) => {
11
+ const method = native[action.name];
12
+ if (method === void 0) throw new Error(`No client method for ${action.name}`);
13
+ return checked(action, (payload) => method({ payload }));
14
+ };
15
+ });
16
+ //#endregion
17
+ export { assertInBinding, methods };
@@ -0,0 +1,20 @@
1
+ import { unsuspended } from "../contract/rules.mjs";
2
+ import { Schema, SchemaAST } from "effect";
3
+ import { HttpServerResponse } from "effect/http";
4
+ import { HttpApiSchema } from "effect/http-api";
5
+ //#region src/http/declared.ts
6
+ const httpApiStatusOf = SchemaAST.resolveAt("httpApiStatus");
7
+ const statusThroughSuspensions = (ast) => httpApiStatusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThroughSuspensions(ast.thunk()) : void 0);
8
+ const declared = (error) => {
9
+ const status = statusThroughSuspensions(error.ast);
10
+ if (status !== void 0) return [httpApiStatusOf(error.ast) === void 0 ? HttpApiSchema.status(status)(error) : error];
11
+ const resolved = unsuspended(error.ast);
12
+ if (SchemaAST.isUnion(resolved) && resolved.checks === void 0 && resolved.encoding === void 0) return resolved.types.flatMap((member) => declared(Schema.make(member)));
13
+ return [HttpApiSchema.status(422)(error)];
14
+ };
15
+ const declaredResponse = (errors, error) => {
16
+ const schema = errors.flatMap(declared).find((each) => Schema.is(each)(error));
17
+ return schema === void 0 ? void 0 : HttpServerResponse.jsonUnsafe(Schema.encodeUnknownSync(Schema.toCodecJson(schema))(error), { status: httpApiStatusOf(schema.ast) ?? 422 });
18
+ };
19
+ //#endregion
20
+ export { declared, declaredResponse };
@@ -0,0 +1,9 @@
1
+ import { InvalidInput } from "../contract/errors.mjs";
2
+ import { Effect } from "effect";
3
+ import { HttpApiMiddleware } from "effect/http-api";
4
+ //#region src/http/schema-errors.ts
5
+ const responseEncodingKinds = /* @__PURE__ */ new Set(["Body", "ResponseHeaders"]);
6
+ var SchemaErrors = class extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: [InvalidInput] }) {};
7
+ const schemaErrors = () => HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(responseEncodingKinds.has(failure.kind) ? failure : InvalidInput.fromSchemaError(failure.cause)));
8
+ //#endregion
9
+ export { SchemaErrors, schemaErrors };
@@ -0,0 +1,17 @@
1
+ import "../contract/implementation.mjs";
2
+ import "../contract/media.mjs";
3
+ import "../Action.mjs";
4
+ import { Schema } from "effect";
5
+ import { McpProtocol } from "effect/ai";
6
+ //#region src/mcp/protocol.d.ts
7
+ /**
8
+ * An MCP request's parameters, as `Testing.mcpRequest` takes them: JSON, with any `_meta`, leaving
9
+ * out an `undefined` one, as a caller may.
10
+ */
11
+ export interface Params {
12
+ readonly _meta?: {
13
+ readonly [key: string]: Schema.Json;
14
+ };
15
+ readonly [key: string]: Schema.Json | undefined;
16
+ }
17
+ //#endregion