@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,20 +1,15 @@
1
- import { BuiltIns } from "./internal/errors.mjs";
2
- import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Offered, OptionalUnless, RequestOf, SelectedOf, Served, Serving } from "./internal/implementation.mjs";
1
+ import { BuiltIns } from "./contract/errors.mjs";
2
+ import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Offered, OptionalUnless, RequestOf, SelectedOf, Served, Serving } from "./contract/implementation.mjs";
3
3
  import { Action_d_exports, Any } from "./Action.mjs";
4
4
  import { Effect, Layer, Schema } from "effect";
5
5
  import { Tool, Toolkit } from "effect/ai";
6
- //#region src/ActionToolkit.d.ts
7
- /** A native tool named after its action, taking and giving JSON. */
6
+ //#region src/toolkit/ActionToolkit.d.ts
8
7
  type NativeTool<A extends Any, R> = Tool.Tool<A["name"], {
9
8
  readonly parameters: Schema.toCodecJson<A["input"]>;
10
9
  readonly success: Schema.toCodecJson<A["success"]>;
11
10
  readonly failure: Schema.toCodecJson<Schema.Union<ReadonlyArray<A["error"][number] | BuiltIns>>>;
12
11
  readonly failureMode: "return";
13
12
  }, R>;
14
- /**
15
- * The tool of each action of `App` among `Listed`. A tool needs what its handler and its
16
- * implementation's authorization need.
17
- */
18
13
  type ToolFor<App, Listed extends Any> = App extends unknown ? Serving<App, Listed> extends (infer A extends Any) ? A extends Any ? NativeTool<A, RequestOf<App, A>> : never : never : never;
19
14
  /**
20
15
  * Native tools bound to their action implementations: a native `Toolkit` and the layer of
@@ -64,6 +59,8 @@ export interface Options<A extends Any> {
64
59
  * when the resulting toolkit handles a call, the identity an implementation's `authorize`
65
60
  * reads included: the caller provides it. `needsApproval` marks the calls a model must have
66
61
  * approved; it authorizes nothing, which stays `authorize`'s.
62
+ * Guide: `docs/ActionToolkit.md` in this package; a loop served behind a route, with approval:
63
+ * `examples/kitchen-sink/src/assistant/`.
67
64
  */
68
65
  export declare function make<const Apps extends Served, const O extends Options<ActionOf<Member<Apps>>> = {}>(implementations: Apps, ...options: OptionalUnless<O, Options<ActionOf<Member<Apps>>> & O & NoInfer<Known<O, Options<Any>>>>): Tools<{ readonly [T in ToolFor<Member<Apps>, Offered<O, ActionOf<Member<Apps>>>> as T["name"]]: T; }, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>;
69
66
  //#endregion
@@ -1,6 +1,6 @@
1
- import { Implementation, provideHandlers, select, toList } from "./internal/implementation.mjs";
2
- import { bindTools } from "./internal/tools.mjs";
3
- //#region src/ActionToolkit.ts
1
+ import { Implementation, provideHandlers, select, toList } from "./contract/implementation.mjs";
2
+ import { bindTools } from "./toolkit/tools.mjs";
3
+ //#region src/toolkit/ActionToolkit.ts
4
4
  function make(apps, options) {
5
5
  const served = select(toList(apps), options?.actions);
6
6
  const needsApproval = options?.needsApproval;
@@ -1,12 +1,12 @@
1
- import { Refusal } from "./internal/errors.mjs";
2
- import { Known, OptionalUnless } from "./internal/implementation.mjs";
1
+ import { ErrorsOf } from "./contract/rules.mjs";
2
+ import { Refusal } from "./contract/errors.mjs";
3
+ import { Known, OptionalUnless } from "./contract/implementation.mjs";
3
4
  import { Errors } from "./Action.mjs";
4
- import { Any, Descriptor, Provider, Security } from "./internal/authentication.mjs";
5
- import { ErrorsOf } from "./internal/declared.mjs";
5
+ import { Any, Descriptor, Provider, Security } from "./authentication/provider.mjs";
6
6
  import { Array, Context, Effect, Layer, Option, Redacted, Scope } from "effect";
7
7
  import { HttpRouter, HttpServerResponse } from "effect/http";
8
8
  import { HttpApiSecurity } from "effect/http-api";
9
- //#region src/Authentication.d.ts
9
+ //#region src/authentication/Authentication.d.ts
10
10
  /**
11
11
  * The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
12
12
  * `HttpApiSecurity.bearer` reads it for routes and MCP, which a test holds equal. It is for a
@@ -17,7 +17,10 @@ import { HttpApiSecurity } from "effect/http-api";
17
17
  export declare const bearerTokenOf: (authorization: string | null | undefined) => Option.Option<Redacted.Redacted<string>>;
18
18
  /** An OAuth protected resource (RFC 9728), as `layer` publishes it. */
19
19
  export interface ProtectedResource {
20
- /** Exact OAuth resource identifier; its path and query select the discovery path. */
20
+ /**
21
+ * Exact OAuth resource identifier; its path and query select the discovery path. It has no
22
+ * fragment, which no request URL carries, so its discovery would never answer (RFC 9728).
23
+ */
21
24
  readonly resource: string;
22
25
  /** Where clients get tokens: nonempty. */
23
26
  readonly authorizationServers: Array.NonEmptyReadonlyArray<string>;
@@ -50,7 +53,6 @@ export interface RefusalResponseOptions<D extends Any = Any> {
50
53
  */
51
54
  readonly authorization?: string | null | undefined;
52
55
  }
53
- /** What the descriptor `D` declares its verifier fails with besides a refusal. */
54
56
  type DeclaredOf<D> = D extends {
55
57
  readonly error: infer E extends Errors;
56
58
  } ? E[number]["Type"] : never;
@@ -76,7 +78,6 @@ export interface Options {
76
78
  */
77
79
  readonly error?: Errors | Errors[number];
78
80
  }
79
- /** The scheme options `O` give: Bearer too wherever they may leave it out, as at run time. */
80
81
  type SecurityOf<O> = O extends {
81
82
  readonly security: infer S extends Security;
82
83
  } ? S : "security" extends keyof O ? NonNullable<O["security" & keyof O]> | HttpApiSecurity.Http : HttpApiSecurity.Http;
@@ -86,6 +87,7 @@ type SecurityOf<O> = O extends {
86
87
  * as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`. The literal name
87
88
  * identifies the provider, like a native Context.Key name, and the scheme's OpenAPI key, so it
88
89
  * holds only letters, digits, `_`, `.` and `-`; reuse one name only for one declaration.
90
+ * Guide: `docs/Authentication.md` in this package; in an application: `examples/notes-service/src/identity/`.
89
91
  */
90
92
  export declare const make: <const Name extends string, I, A, const O extends Options = {}>(name: Name, identity: Context.Key<I, A>, ...options: OptionalUnless<O, O & NoInfer<Known<O, Options>>>) => Descriptor<I, A, SecurityOf<O>, Name, ErrorsOf<O>>;
91
93
  /**
@@ -97,21 +99,19 @@ export type Verify<A, S extends Security, R, E = never> = (credential: HttpApiSe
97
99
  export interface LayerOptions<EP = never, RP = never> {
98
100
  readonly protectedResource?: ProtectedResource | Effect.Effect<ProtectedResource | undefined, EP, RP>;
99
101
  }
100
- /**
101
- * Only a Bearer scheme publishes an OAuth protected resource. The types tell only an `Http`
102
- * scheme from the others, as its scheme is a `string`: `layer` refuses another `Http` one.
103
- */
104
102
  type ResourceOf<S> = [S] extends [HttpApiSecurity.Http] ? unknown : {
105
103
  readonly protectedResource?: undefined;
106
104
  };
105
+ type Resource = ProtectedResource | Effect.Effect<ProtectedResource | undefined, unknown, unknown> | undefined;
107
106
  /**
108
107
  * The provider of `authentication`: `verify`, or an Effect building it once per layer graph, run on
109
108
  * each remote request a protected action receives, with the credential the descriptor's
110
109
  * scheme decodes. A Bearer scheme answers each 401 with its challenge, and may publish the
111
- * OAuth protected resource `protectedResource`.
110
+ * OAuth protected resource `protectedResource`, on the `HttpRouter` it then requires.
112
111
  */
113
- export declare function layer<const Name extends string, I, A, const S extends Security, E extends Errors, R, EX = never, RX = never, EP = never, RP = never>(authentication: Descriptor<I, A, S, Name, E>, verify: Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>> | Effect.Effect<Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>>, EX, RX>, options?: LayerOptions<EP, RP> & ResourceOf<S>): Layer.Layer<Provider<I, Name>, EX | EP, HttpRouter.HttpRouter | Exclude<RX | RP, Scope.Scope> | HttpRouter.Request.From<"Requires", Exclude<R, HttpRouter.Provided>>>;
114
- /** The identity `I`, or none where it is erased to `unknown`. */
112
+ export declare function layer<const Name extends string, I, A, const S extends Security, E extends Errors, R, EX = never, RX = never, P extends Resource = undefined>(authentication: Descriptor<I, A, S, Name, E>, verify: Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>> | Effect.Effect<Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>>, EX, RX>, options?: {
113
+ readonly protectedResource?: P;
114
+ } & ResourceOf<S>): Layer.Layer<Provider<I, Name>, EX | Effect.Error<P>, ([P] extends [undefined] ? never : HttpRouter.HttpRouter) | Exclude<RX | Effect.Services<P>, Scope.Scope> | HttpRouter.Request.From<"Requires", Exclude<R, HttpRouter.Provided>>>;
115
115
  type Provides<I> = unknown extends I ? never : I;
116
116
  /**
117
117
  * What `protect` returns for a descriptor named `Name` authenticating the identity `I`: router
@@ -1,18 +1,13 @@
1
- import { Unauthenticated, scopeToken } from "./internal/errors.mjs";
2
- import { assertOwnTags, errorList } from "./internal/actions.mjs";
3
- import { declaredResponse } from "./internal/declared.mjs";
4
- import { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain } from "./internal/refusal.mjs";
1
+ import { Unauthenticated, scopeToken } from "./contract/errors.mjs";
2
+ import { assertOwnTags, errorList } from "./contract/rules.mjs";
3
+ import { providerOf } from "./authentication/provider.mjs";
4
+ import { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain } from "./authentication/refusal.mjs";
5
+ import { declaredResponse } from "./http/declared.mjs";
5
6
  import { Context, Effect, Layer, Match, Option, Predicate, Redacted } from "effect";
6
7
  import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
7
8
  import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
8
- //#region src/Authentication.ts
9
- /**
10
- * An `Authorization` header of the `Bearer` scheme, and its token, as Effect's own
11
- * `HttpApiSecurity.bearer` decodes a request's: the scheme, matched case-insensitively, one or
12
- * more spaces, then the rest of the header, whatever it holds, a tab included. The whitespace
13
- * around a header value is no part of it, and an HTTP parser strips it before any route reads it.
14
- */
15
- const bearerScheme = /^[ \t]*Bearer +(.*[^ \t])[ \t]*$/i;
9
+ //#region src/authentication/Authentication.ts
10
+ const bearerAuthorizationHeader = /^[ \t]*Bearer +(.*[^ \t])[ \t]*$/i;
16
11
  /**
17
12
  * The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
18
13
  * `HttpApiSecurity.bearer` reads it for routes and MCP, which a test holds equal. It is for a
@@ -20,44 +15,24 @@ const bearerScheme = /^[ \t]*Bearer +(.*[^ \t])[ \t]*$/i;
20
15
  * own is authenticated by `protect`. The scheme is matched case-insensitively, as RFC 9110
21
16
  * requires, and the token is `Redacted`, as Effect's own decoder gives it.
22
17
  */
23
- const bearerTokenOf = (authorization) => Option.map(Option.fromNullishOr(bearerScheme.exec(authorization ?? "")?.[1]), (token) => Redacted.make(token));
24
- /**
25
- * The RFC 9728 metadata URL of `options`: `/.well-known/oauth-protected-resource` followed
26
- * by the resource's path, where MCP clients look when a 401 names no metadata URL.
27
- */
28
- const metadataUrl = (options) => {
18
+ const bearerTokenOf = (authorization) => Option.map(Option.fromNullishOr(bearerAuthorizationHeader.exec(authorization ?? "")?.[1]), (token) => Redacted.make(token));
19
+ const metadataUrlOf = (options) => {
29
20
  const resource = new URL(options.resource);
30
21
  const url = new URL(resource);
31
22
  url.pathname = `/.well-known/oauth-protected-resource${resource.pathname === "/" ? "" : resource.pathname}`;
32
23
  return url;
33
24
  };
34
- /** What challenges about `options` name. */
35
25
  const namedOf = (options) => ({
36
26
  scope: options?.scopesRequired?.join(" "),
37
- metadataUrl: options === void 0 ? void 0 : metadataUrl(options).href
27
+ metadataUrl: options === void 0 ? void 0 : metadataUrlOf(options).href
38
28
  });
39
- /** Whether a request whose `Authorization` header is `authorization` presented a bearer token. */
40
- const tokenPresented = (authorization) => Option.isSome(bearerTokenOf(authorization));
41
- /**
42
- * The challenge of a 401: `Bearer`, naming the scopes a client requests and the metadata URL
43
- * where it finds its authorization server. A request that presented a bearer token is told
44
- * it is invalid, RFC 6750's `invalid_token`, on which a client may refresh its token before
45
- * it signs in again; one that presented none names no error code.
46
- */
47
- const challengeOf = (named, presented) => bearer([
29
+ const presentsBearerToken = (authorization) => Option.isSome(bearerTokenOf(authorization));
30
+ const bearerChallengeOf = (named, presented) => bearer([
48
31
  ["error", presented ? "invalid_token" : void 0],
49
32
  ["scope", named.scope],
50
33
  ["resource_metadata", named.metadataUrl]
51
34
  ]);
52
- /**
53
- * Refuse a protected resource that cannot be published, as `layer` and `refusalResponse` both
54
- * do: one under a scheme other than Bearer (`oauth` false), a `scopesSupported` or
55
- * `scopesRequired` that is no list of OAuth scope tokens, as `Forbidden` refuses, and a
56
- * resource with a fragment, which no request URL carries, so its discovery would never answer
57
- * (RFC 9728). A resource an Effect builds is checked for its scheme alone, and for the rest
58
- * once built.
59
- */
60
- const assertResource = (oauth, options) => {
35
+ const assertPublishableResource = (oauth, options) => {
61
36
  if (options === void 0) return;
62
37
  if (!oauth) throw new Error("A protected resource is published only for a Bearer scheme");
63
38
  if (Effect.isEffect(options)) return;
@@ -67,11 +42,7 @@ const assertResource = (oauth, options) => {
67
42
  }
68
43
  if (new URL(options.resource).href.includes("#")) throw new Error(`A protected resource has no fragment: ${options.resource}`);
69
44
  };
70
- /**
71
- * `response` as the routes of the authentication answer: `no-store` unless it states its
72
- * own caching, and a 401 challenged with `challenge` unless it names its own.
73
- */
74
- const settle = (response, challenge) => HttpServerResponse.setHeaders(response, {
45
+ const withNoStoreAndChallenge = (response, challenge) => HttpServerResponse.setHeaders(response, {
75
46
  ...response.headers["cache-control"] === void 0 ? { "cache-control": "no-store" } : {},
76
47
  ...challenge !== void 0 && response.status === 401 && response.headers["www-authenticate"] === void 0 ? { "www-authenticate": challenge } : {}
77
48
  });
@@ -89,32 +60,19 @@ const settle = (response, challenge) => HttpServerResponse.setHeaders(response,
89
60
  const refusalResponse = (error, options) => {
90
61
  const authentication = options?.authentication;
91
62
  const oauth = authentication === void 0 || isBearer(authentication.security);
92
- assertResource(oauth, options?.protectedResource);
63
+ assertPublishableResource(oauth, options?.protectedResource);
93
64
  const named = namedOf(options?.protectedResource);
94
- const challenge = authentication !== void 0 && !oauth ? schemeChallenge(authentication.security, authentication.name) : challengeOf(named, tokenPresented(options?.authorization));
65
+ const challenge = authentication !== void 0 && !oauth ? nonBearerChallengeOf(authentication.security, authentication.name) : bearerChallengeOf(named, presentsBearerToken(options?.authorization));
95
66
  if (!isRefusal(error)) {
96
67
  const response = declaredResponse(authentication?.error ?? [], error);
97
68
  if (response === void 0) throw new Error("Not a refusal, nor an error the authentication declares");
98
- return settle(response, challenge);
69
+ return withNoStoreAndChallenge(response, challenge);
99
70
  }
100
- return settle(oauth ? answer(error, named.metadataUrl) : plain(error), challenge);
71
+ return withNoStoreAndChallenge(oauth ? answer(error, named.metadataUrl) : plain(error), challenge);
101
72
  };
102
- /**
103
- * Whether a request URL may resolve to a discovery path, which every other request skips
104
- * parsing: the URL parser copies `.well-known` as it is, removing only a tab or a newline. Node
105
- * refuses a target holding those before routing; a platform passing one through still parses.
106
- */
107
- const mayDiscover = /\.well-known|[\t\n\r]/;
108
- /**
109
- * RFC 9728 discovery of `options` at its metadata URL, as global router middleware: it
110
- * answers before routing, so no route middleware, authentication included, ever covers it.
111
- * The metadata is public to every origin, as a browser client needs it after a 401: it
112
- * carries `Access-Control-Allow-Origin: *`, and discovery answers its own CORS preflight.
113
- * Where the host's CORS middleware runs before it, that policy answers the preflight and
114
- * adds its headers to reads, which keep the `*` where it sets no origin.
115
- */
116
- const discovery = (options) => {
117
- const discoveryUrl = metadataUrl(options);
73
+ const mayResolveToDiscoveryPath = /\.well-known|[\t\n\r]/;
74
+ const discoveryMiddleware = (options) => {
75
+ const discoveryUrl = metadataUrlOf(options);
118
76
  const target = discoveryUrl.href.slice(discoveryUrl.origin.length);
119
77
  const metadata = HttpServerResponse.jsonUnsafe({
120
78
  resource: options.resource,
@@ -123,8 +81,7 @@ const discovery = (options) => {
123
81
  scopes_supported: options.scopesSupported,
124
82
  resource_name: options.resourceName
125
83
  }, { headers: { "access-control-allow-origin": "*" } });
126
- /** The preflight of a cross-origin read, allowing the headers it asks for. */
127
- const preflight = (request) => {
84
+ const corsPreflight = (request) => {
128
85
  const requested = request.headers["access-control-request-headers"];
129
86
  return HttpServerResponse.empty({
130
87
  status: 204,
@@ -139,46 +96,36 @@ const discovery = (options) => {
139
96
  });
140
97
  };
141
98
  return (next) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => {
142
- const targeted = mayDiscover.test(request.url) && URL.canParse(request.url, discoveryUrl.origin) && new URL(request.url, discoveryUrl.origin).href.slice(discoveryUrl.origin.length) === target;
99
+ const targeted = mayResolveToDiscoveryPath.test(request.url) && URL.canParse(request.url, discoveryUrl.origin) && new URL(request.url, discoveryUrl.origin).href.slice(discoveryUrl.origin.length) === target;
143
100
  if (targeted && (request.method === "GET" || request.method === "HEAD")) return Effect.succeed(metadata);
144
- if (targeted && request.method === "OPTIONS") return Effect.succeed(preflight(request));
101
+ if (targeted && request.method === "OPTIONS") return Effect.succeed(corsPreflight(request));
145
102
  return next;
146
103
  });
147
104
  };
148
- /**
149
- * What `layer` answers about the protected resource `options`, written once: its metadata
150
- * URL, each 401's challenge, to a request that presented a bearer token and to one that did
151
- * not, and its discovery, none without a resource.
152
- */
153
- const answersOf = (options) => {
105
+ const resourceAnswersOf = (options) => {
154
106
  const named = namedOf(options);
155
107
  return {
156
108
  metadataUrl: named.metadataUrl,
157
- invalid: challengeOf(named, true),
158
- anonymous: challengeOf(named, false),
159
- published: options === void 0 ? void 0 : discovery(options)
109
+ invalid: bearerChallengeOf(named, true),
110
+ anonymous: bearerChallengeOf(named, false),
111
+ published: options === void 0 ? void 0 : discoveryMiddleware(options)
160
112
  };
161
113
  };
162
- /** Whether `security` is the Bearer scheme, the one OAuth challenges and discovery name. */
163
114
  const isBearer = (security) => Predicate.isTagged(security, "Http") && security.scheme.toLowerCase() === "bearer";
164
- /** What an OpenAPI component key may hold, and so a descriptor's name, which is its key. */
165
- const componentKey = /^[\w.-]+$/;
166
- /**
167
- * The challenge of a 401 under a scheme other than Bearer: the scheme an `Http` one names,
168
- * Basic with its realm, the descriptor's name; an API key has no scheme to name.
169
- */
170
- const schemeChallenge = (security, name) => Match.value(security).pipe(Match.tag("Http", ({ scheme }) => challenge(scheme, [])), Match.tag("Basic", () => challenge("Basic", [["realm", name]])), Match.tag("ApiKey", () => void 0), Match.exhaustive);
115
+ const openApiComponentKey = /^[\w.-]+$/;
116
+ const nonBearerChallengeOf = (security, name) => Match.value(security).pipe(Match.tag("Http", ({ scheme }) => challenge(scheme, [])), Match.tag("Basic", () => challenge("Basic", [["realm", name]])), Match.tag("ApiKey", () => void 0), Match.exhaustive);
171
117
  /**
172
118
  * Browser-safe declaration shared by clients and remote surfaces: the `identity` it
173
119
  * authenticates, by the one native scheme `security`, Bearer unless it names another, such
174
120
  * as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`. The literal name
175
121
  * identifies the provider, like a native Context.Key name, and the scheme's OpenAPI key, so it
176
122
  * holds only letters, digits, `_`, `.` and `-`; reuse one name only for one declaration.
123
+ * Guide: `docs/Authentication.md` in this package; in an application: `examples/notes-service/src/identity/`.
177
124
  */
178
125
  const make = (name, identity, ...options) => {
179
126
  const security = options[0]?.security ?? HttpApiSecurity.bearer;
180
127
  const error = errorList(options[0]?.error);
181
- if (!componentKey.test(name)) throw new Error(`Invalid authentication name: ${JSON.stringify(name)}, not an OpenAPI key`);
128
+ if (!openApiComponentKey.test(name)) throw new Error(`Invalid authentication name: ${JSON.stringify(name)}, not an OpenAPI key`);
182
129
  assertOwnTags(`Authentication "${name}"`, error);
183
130
  if (!Predicate.isTagged(security, "Http") && !Predicate.isTagged(security, "ApiKey") && !Predicate.isTagged(security, "Basic")) throw new Error("Authentication takes one native HttpApiSecurity scheme");
184
131
  return {
@@ -191,27 +138,25 @@ const make = (name, identity, ...options) => {
191
138
  "~verified": Context.Service(`effect-actions/Authentication/Verified/${name}`)
192
139
  };
193
140
  };
194
- /** Whether `credential`, as a native scheme decodes it, holds anything: an absent one is empty. */
195
141
  const presented = (credential) => Redacted.isRedacted(credential) ? Redacted.value(credential) !== "" : credential.username !== "" || Redacted.value(credential.password) !== "";
196
142
  function layer(authentication, verify, options = {}) {
197
143
  const { security } = authentication;
198
144
  const oauth = isBearer(security);
199
145
  const protectedResource = options.protectedResource;
200
- assertResource(oauth, protectedResource);
146
+ assertPublishableResource(oauth, protectedResource);
201
147
  const resource = Effect.isEffect(protectedResource) ? Effect.map(protectedResource, (built) => {
202
- assertResource(oauth, built);
203
- return answersOf(built);
204
- }) : Effect.succeed(answersOf(protectedResource));
148
+ assertPublishableResource(oauth, built);
149
+ return resourceAnswersOf(built);
150
+ }) : Effect.succeed(resourceAnswersOf(protectedResource));
205
151
  const missing = new Unauthenticated({ message: oauth ? "A bearer token is required." : "A credential is required." });
206
152
  return Layer.effect(authentication["~provider"], Effect.gen(function* () {
207
- const router = yield* HttpRouter.HttpRouter;
208
153
  const { metadataUrl, invalid, anonymous, published } = yield* resource;
209
- if (published !== void 0) yield* router.addGlobalMiddleware(published);
154
+ if (published !== void 0) yield* (yield* HttpRouter.HttpRouter).addGlobalMiddleware(published);
210
155
  const verifier = Effect.isEffect(verify) ? yield* verify : verify;
211
156
  if (!Predicate.isFunction(verifier)) return yield* Effect.die(/* @__PURE__ */ new Error("Missing verify: pass a verify function, or an Effect building one"));
212
157
  const authenticate = (credential) => presented(credential) ? verifier(credential) : Effect.fail(missing);
213
- const failed = HttpEffect.appendPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "no-store")));
214
- const challengeOf = (credential) => !oauth ? schemeChallenge(security, authentication.name) : presented(credential) ? invalid : anonymous;
158
+ const noStoreOnFailure = HttpEffect.appendPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "no-store")));
159
+ const challengeOf = (credential) => !oauth ? nonBearerChallengeOf(security, authentication.name) : presented(credential) ? invalid : anonymous;
215
160
  const refuse = (error) => isRefusal(error) ? Effect.succeed(oauth ? answer(error, metadataUrl) : plain(error)) : Effect.suspend(() => {
216
161
  const response = declaredResponse(authentication.error, error);
217
162
  return response === void 0 ? Effect.die(error) : Effect.succeed(response);
@@ -221,10 +166,11 @@ function layer(authentication, verify, options = {}) {
221
166
  return Effect.matchEffect(authenticate(credential), {
222
167
  onFailure: refuse,
223
168
  onSuccess: (actor) => Effect.provideService(route, slot, actor)
224
- }).pipe(Effect.onError(() => failed), HttpEffect.withPreResponseHandler((_request, response) => Effect.succeed(settle(response, challenge))));
169
+ }).pipe(Effect.onError(() => noStoreOnFailure), HttpEffect.withPreResponseHandler((_request, response) => Effect.succeed(withNoStoreAndChallenge(response, challenge))));
225
170
  };
226
171
  return {
227
172
  descriptor: authentication,
173
+ authenticate,
228
174
  middleware: { [authentication.name]: (route, { credential }) => verified(credential, route, authentication.identity) },
229
175
  stepUp: (route) => oauth ? Effect.catchIf(route, isStepUp, (error) => Effect.succeed(answer(error, metadataUrl))) : route,
230
176
  http: ((route, optional) => Effect.flatMap(HttpApiBuilder.securityDecode(security), (credential) => optional && !presented(credential) ? route : verified(credential, oauth ? answerStepUp(route, metadataUrl) : route, authentication["~verified"]))),
@@ -243,6 +189,6 @@ function layer(authentication, verify, options = {}) {
243
189
  * `layer(authentication, ...)`. An erased descriptor, `Any`, provides no service the types can
244
190
  * name: its identity is `unknown`, which would discharge every request service the route owes.
245
191
  */
246
- const protect = (authentication) => HttpRouter.middleware()(Effect.map(Effect.service(authentication["~provider"]), (runtime) => (route) => runtime.route(route)));
192
+ const protect = (authentication) => HttpRouter.middleware()(Effect.map(providerOf(authentication), (runtime) => (route) => runtime.route(route)));
247
193
  //#endregion
248
194
  export { bearerTokenOf, layer, make, protect, refusalResponse };
@@ -1,14 +1,15 @@
1
- import { BuiltIn } from "./internal/errors.mjs";
2
- import { Call } from "./internal/call.mjs";
1
+ import { BuiltIn } from "./contract/errors.mjs";
2
+ import { Call } from "./contract/call.mjs";
3
3
  import { Action_d_exports, Any } from "./Action.mjs";
4
- import { Params } from "./internal/mcp.mjs";
5
- import { Served } from "./internal/memory.mjs";
6
- import { Effect, Layer, Schema } from "effect";
4
+ import { Params } from "./mcp/protocol.mjs";
5
+ import { Served } from "./testing/memory.mjs";
6
+ import { Cause, Effect, Layer, Schema } from "effect";
7
7
  import { Headers, HttpClient, HttpClientError, HttpClientRequest, HttpRouter } from "effect/http";
8
- //#region src/Testing.d.ts
8
+ //#region src/testing/Testing.d.ts
9
9
  /**
10
- * The native `HttpClient`, answered by `handler` instead of the network: a web handler the
11
- * test serves, which other tests may share, such as
10
+ * For a web handler the test already owns; most tests want `layer(routes)`, below, with
11
+ * `ActionHttp.client` and `mcpClient`. The native `HttpClient`, answered by `handler` instead of
12
+ * the network: a web handler the test serves, which other tests may share, such as
12
13
  * `HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices))).handler`,
13
14
  * the routes' platform services provided, as `layer(routes)` provides them itself. It is the
14
15
  * client `layer(routes)` gives, without building anything: the test owns the handler, and
@@ -18,7 +19,9 @@ export declare function layer(handler: (request: Request) => Promise<Response>):
18
19
  /**
19
20
  * The native `HttpClient`, answered in memory by `routes` instead of the network: provide
20
21
  * it to `ActionHttp.client` and to `mcpClient`. The routes are built with this layer and
21
- * released with its scope, without request logs. What they still require is this layer's, as
22
+ * released with its scope, without request logs: only the cause of a defect, of a result that
23
+ * does not encode or of a failure a route of your own leaves unhandled is logged, as an error,
24
+ * where a server's request logger would show it. What they still require is this layer's, as
22
25
  * under `HttpRouter.serve`: a builder's services, and a per-request service no middleware of
23
26
  * theirs provides, including one a global middleware reads. The routes keep their real
24
27
  * authentication: a client sends its caller's credential. Provided around it, the test
@@ -28,6 +31,7 @@ export declare function layer(handler: (request: Request) => Promise<Response>):
28
31
  * the rest, whose `FileSystem` is a no-op. A relative URL resolves against `http://localhost`.
29
32
  * The client is the layer's own: the program's other HTTP clients get none of its requests,
30
33
  * and it none of theirs.
34
+ * Guide: `docs/Testing.md` in this package; in an application: `examples/notes-service/test/`.
31
35
  */
32
36
  export declare function layer<A, E, R>(routes: Layer.Layer<A, E, R>): Layer.Layer<HttpClient.HttpClient, E, Exclude<HttpRouter.Request.Without<R> | HttpRouter.Request.Only<"Requires", R> | HttpRouter.Request.Only<"GlobalRequires", R>, Served>>;
33
37
  /** Where `mcpClient` sends, and through what client. */
@@ -37,7 +41,10 @@ export interface McpClientOptions {
37
41
  * the default `ActionMcp.layerHttp` path.
38
42
  */
39
43
  readonly url?: string;
40
- /** Wraps the native `HttpClient`, as `ActionHttp.client` takes it: a bearer token, say. */
44
+ /**
45
+ * Wraps the native `HttpClient`, as `ActionHttp.client` takes it: a bearer token is
46
+ * `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`.
47
+ */
41
48
  readonly transformClient?: (client: HttpClient.HttpClient) => HttpClient.HttpClient;
42
49
  }
43
50
  /** Where an `mcpRequest` goes, and with what headers. */
@@ -52,34 +59,34 @@ export interface McpRequestOptions {
52
59
  }
53
60
  declare const McpCallError_base: Schema.Class<McpCallError, Schema.TaggedStruct<"McpCallError", {
54
61
  readonly message: Schema.String;
55
- }>, import("effect/Cause").YieldableError>;
62
+ }>, Cause.YieldableError>;
56
63
  /**
57
- * An answer a client method cannot decode as the action's success or a declared error,
58
- * such as the native server's message for invalid arguments or an unknown tool. Its message
59
- * holds the answer.
64
+ * An answer a client method cannot read as the action's success or a declared error, such as
65
+ * the native server's message for invalid arguments or an unknown tool, or a result without
66
+ * its structured content. Its message holds the answer. Structured content that does not
67
+ * decode as the success is a `SchemaError` instead, and a request that fails an
68
+ * `HttpClientError`.
60
69
  */
61
70
  export declare class McpCallError extends McpCallError_base {}
62
- /**
63
- * What one call of `A` fails with: a declared error value (the action's own or a built-in one
64
- * from the tool, or a refusal from the endpoint's authentication), a `SchemaError` when the
65
- * input does not encode or the success does not decode, an `HttpClientError` when the
66
- * endpoint could not be reached, or an `McpCallError` for any other answer.
67
- */
68
71
  type CallError<A extends Any> = A["error"][number]["Type"] | BuiltIn | Schema.SchemaError | HttpClientError.HttpClientError | McpCallError;
69
72
  /** Every action of `Actions` as `client.<action>(input)`, calling its tool. */
70
73
  export type McpClient<Actions extends ReadonlyArray<Any>> = { readonly [A in Actions[number] as A["name"]]: Call<A, Effect.Effect<A["success"]["Type"], CallError<A>>>; };
71
74
  /**
72
75
  * A client of an MCP endpoint served by `ActionMcp.layerHttp`, one method per action calling
73
76
  * its tool with one stateless request, as `ActionHttp.client` calls routes: the input is
74
- * encoded with the action's schema, and the success decoded from its structured content. A
75
- * declared error the tool returns, the action's own or a refusal, is its decoded value, and so
76
- * is a refusal the endpoint's authentication answers with. The argument may be omitted when
77
- * `{}` is a valid input. Requires the native `HttpClient`, such as the one `layer` provides.
77
+ * encoded with the action's schema, and the success decoded from its structured content, each
78
+ * media field from the blocks whose `_meta` names it, and a success that is media, or whose
79
+ * every field is, from its blocks alone. A declared error the tool returns, the action's own
80
+ * or a refusal, is its decoded value, and so is a refusal the endpoint's authentication
81
+ * answers with: a test telling a 401 or 403 from a tool result asserts the status of a request
82
+ * `mcpRequest` builds. The argument may be omitted when `{}` is a valid input. Requires the
83
+ * native `HttpClient`, such as the one `layer` provides.
78
84
  */
79
85
  export declare function mcpClient<const Actions extends ReadonlyArray<Any>>(actions: Actions, options?: McpClientOptions): Effect.Effect<McpClient<Actions>, never, HttpClient.HttpClient>;
80
86
  /**
81
87
  * One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it,
82
- * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers, `mcp-name` from
88
+ * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
89
+ * `MCP-Protocol-Version`, `MCP-Method`), `mcp-name` from
83
90
  * `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
84
91
  * `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
85
92
  * version is always the request's own. The test sends it: `HttpClient.execute` answers the
package/dist/Testing.mjs CHANGED
@@ -1,13 +1,13 @@
1
- import { Refusal } from "./internal/errors.mjs";
2
- import { assertOnce, projectedErrors } from "./internal/actions.mjs";
3
- import { inputOf } from "./internal/call.mjs";
4
- import { defaultPath, statelessRequest } from "./internal/mcp.mjs";
5
- import { clientOf } from "./internal/memory.mjs";
6
- import { Context, Effect, FileSystem, Layer, Option, Path, Predicate, Schema, identity } from "effect";
7
- import { Etag, HttpClient, HttpClientRequest, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
1
+ import { Refusal } from "./contract/errors.mjs";
2
+ import { assertOnce, projectedErrors } from "./contract/rules.mjs";
3
+ import { checked } from "./contract/call.mjs";
4
+ import { defaultPath, liftOf, statelessRequest } from "./mcp/protocol.mjs";
5
+ import { clientOf } from "./testing/memory.mjs";
6
+ import { Cause, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path, Predicate, Schema, identity } from "effect";
8
7
  import { McpSchema } from "effect/ai";
9
- import { Sse } from "effect/encoding";
10
- //#region src/Testing.ts
8
+ import { Etag, HttpClient, HttpClientRequest, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
9
+ import { Base64, Sse } from "effect/encoding";
10
+ //#region src/testing/Testing.ts
11
11
  function layer(routes) {
12
12
  if (!Layer.isLayer(routes)) return clientOf(routes);
13
13
  return Layer.unwrap(Effect.gen(function* () {
@@ -15,27 +15,24 @@ function layer(routes) {
15
15
  const platform = yield* Layer.build(Layer.fresh(Layer.mergeAll(HttpPlatform.layer, Path.layer, Etag.layerWeak).pipe(Layer.provideMerge(Layer.succeed(FileSystem.FileSystem, fileSystem)))));
16
16
  const context = Context.merge(platform, yield* Effect.context());
17
17
  const app = yield* HttpRouter.toHttpEffect(routes).pipe(Effect.provideContext(context));
18
- return clientOf(HttpEffect.toWebHandlerWith(context)(app));
18
+ return clientOf(HttpEffect.toWebHandlerWith(context)(Effect.tapCause(app, (cause) => isReported(cause) ? Effect.logError(cause) : Effect.void)));
19
19
  }));
20
20
  }
21
+ const isReported = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) && !ErrorReporter.isIgnored(reason.error) || Cause.isDieReason(reason) && !ErrorReporter.isIgnored(reason.defect));
21
22
  /**
22
- * An answer a client method cannot decode as the action's success or a declared error,
23
- * such as the native server's message for invalid arguments or an unknown tool. Its message
24
- * holds the answer.
23
+ * An answer a client method cannot read as the action's success or a declared error, such as
24
+ * the native server's message for invalid arguments or an unknown tool, or a result without
25
+ * its structured content. Its message holds the answer. Structured content that does not
26
+ * decode as the success is a `SchemaError` instead, and a request that fails an
27
+ * `HttpClientError`.
25
28
  */
26
29
  var McpCallError = class extends Schema.TaggedError()("McpCallError", { message: Schema.String }) {};
27
- /** The text of a block, none of a block of another kind. */
28
30
  const textOf = (block) => block?.type === "text" ? block.text : void 0;
29
- /** The JSON-RPC response to a `tools/call`: the native tool result, or a protocol error. */
30
- const ToolReply = Schema.Union([Schema.Struct({ result: McpSchema.CallToolResult }), Schema.Struct({ error: Schema.Struct({
31
+ const ToolCallReply = Schema.Union([Schema.Struct({ result: McpSchema.CallToolResult }), Schema.Struct({ error: Schema.Struct({
31
32
  code: Schema.Finite,
32
33
  message: Schema.String
33
34
  }) })]);
34
- const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolReply));
35
- /**
36
- * The reply in a response: its body when it is one JSON message, or the last reply among an
37
- * event stream's events, which notifications may precede.
38
- */
35
+ const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolCallReply));
39
36
  const replyOf = (response, text) => {
40
37
  if (!(response.headers["content-type"] ?? "").startsWith("text/event-stream")) return decodeReply(text);
41
38
  const data = [];
@@ -44,10 +41,26 @@ const replyOf = (response, text) => {
44
41
  }).feed(text);
45
42
  return Option.fromNullishOr(data.flatMap((message) => Option.toArray(decodeReply(message))).at(-1));
46
43
  };
47
- /** Fail with the value of one of `errors` that `text` holds, or else with `otherwise`. */
44
+ const mediaFromBlocks = ({ name, kind, many }, result) => {
45
+ const values = result.content.flatMap((block) => block.type === kind && block._meta?.["effect-actions/field"] === name ? [{
46
+ data: Base64.encode(block.data),
47
+ mimeType: block.mimeType
48
+ }] : []);
49
+ return many || values.length > 1 ? values : values[0];
50
+ };
51
+ const encodedSuccessOf = (lift, result) => {
52
+ const [whole] = lift.fields;
53
+ if (whole !== void 0 && whole.name === void 0) return mediaFromBlocks(whole, result);
54
+ const { structuredContent = {} } = result;
55
+ if (!Predicate.isObject(structuredContent)) return structuredContent;
56
+ const lifted = new Set(lift.fields.map(({ name }) => name));
57
+ return Object.fromEntries([...Object.entries(structuredContent).filter(([key]) => !lifted.has(key)), ...lift.fields.flatMap((field) => {
58
+ const value = mediaFromBlocks(field, result);
59
+ return field.name === void 0 || value === void 0 ? [] : [[field.name, value]];
60
+ })]);
61
+ };
48
62
  const failWith = (errors, text, otherwise) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))))(text).pipe(Effect.mapError(() => otherwise), Effect.flatMap(Effect.fail));
49
- /** The tool call of `action` with `input` on `client`, sending to `url`. */
50
- const callTool = (client, url, action, input) => {
63
+ const callTool = (client, url, action, lift, input) => {
51
64
  const { name } = action;
52
65
  const other = (answer) => new McpCallError({ message: `MCP tools/call "${name}" ${answer}` });
53
66
  return Effect.gen(function* () {
@@ -69,8 +82,8 @@ const callTool = (client, url, action, input) => {
69
82
  const error = textOf(result.content.find((block) => block.type === "text")) ?? "";
70
83
  return yield* failWith(projectedErrors(action), error, other(`returned an error: ${error}`));
71
84
  }
72
- if (result.structuredContent === void 0) return yield* Effect.fail(other(`returned no structured content: ${text}`));
73
- return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(result.structuredContent);
85
+ if (result.structuredContent === void 0 && lift.rest !== void 0) return yield* Effect.fail(other(`returned no structured content: ${text}`));
86
+ return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(encodedSuccessOf(lift, result));
74
87
  });
75
88
  };
76
89
  function mcpClient(actions, { url = defaultPath, transformClient = identity } = {}) {
@@ -78,13 +91,15 @@ function mcpClient(actions, { url = defaultPath, transformClient = identity } =
78
91
  return Effect.map(HttpClient.HttpClient, (native) => {
79
92
  const client = transformClient(native);
80
93
  return Object.fromEntries(actions.map((action) => {
81
- return [action.name, (...args) => Effect.flatMap(inputOf(action, args), (input) => callTool(client, url, action, input))];
94
+ const lift = liftOf(action.success);
95
+ return [action.name, checked(action, (input) => callTool(client, url, action, lift, input))];
82
96
  }));
83
97
  });
84
98
  }
85
99
  /**
86
100
  * One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it,
87
- * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers, `mcp-name` from
101
+ * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
102
+ * `MCP-Protocol-Version`, `MCP-Method`), `mcp-name` from
88
103
  * `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
89
104
  * `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
90
105
  * version is always the request's own. The test sends it: `HttpClient.execute` answers the
@@ -94,8 +109,8 @@ function mcpClient(actions, { url = defaultPath, transformClient = identity } =
94
109
  * a call its types would not send.
95
110
  */
96
111
  const mcpRequest = (method, params = {}, { headers = {}, url = defaultPath } = {}) => {
97
- const { headers: routing, body } = statelessRequest(method, params);
98
- return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routing), HttpClientRequest.bodyJsonUnsafe(body));
112
+ const { headers: routingHeaders, body } = statelessRequest(method, params);
113
+ return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routingHeaders), HttpClientRequest.bodyJsonUnsafe(body));
99
114
  };
100
115
  //#endregion
101
116
  export { McpCallError, layer, mcpClient, mcpRequest };