@gjermundgaraba/effect-actions 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/Action.d.mts +0 -2
  2. package/dist/Action.mjs +1 -2
  3. package/dist/ActionCli.d.mts +1 -2
  4. package/dist/ActionCli.mjs +2 -2
  5. package/dist/ActionHttp.d.mts +1 -3
  6. package/dist/ActionHttp.mjs +1 -2
  7. package/dist/ActionMcp.d.mts +0 -2
  8. package/dist/ActionMcp.mjs +28 -10
  9. package/dist/ActionRpc.d.mts +0 -2
  10. package/dist/ActionRpc.mjs +2 -3
  11. package/dist/ActionToolkit.d.mts +0 -2
  12. package/dist/ActionToolkit.mjs +1 -2
  13. package/dist/Authentication.d.mts +0 -2
  14. package/dist/Authentication.mjs +1 -2
  15. package/dist/Testing.d.mts +1 -3
  16. package/dist/Testing.mjs +1 -2
  17. package/dist/authentication/provider.d.mts +0 -2
  18. package/dist/authentication/provider.mjs +1 -2
  19. package/dist/authentication/refusal.mjs +1 -2
  20. package/dist/cli/commands.d.mts +0 -2
  21. package/dist/cli/commands.mjs +1 -2
  22. package/dist/contract/ast.mjs +1 -2
  23. package/dist/contract/call.d.mts +1 -3
  24. package/dist/contract/call.mjs +1 -2
  25. package/dist/contract/caller.d.mts +1 -3
  26. package/dist/contract/caller.mjs +1 -2
  27. package/dist/contract/declared.d.mts +0 -2
  28. package/dist/contract/declared.mjs +1 -2
  29. package/dist/contract/errors.d.mts +0 -2
  30. package/dist/contract/errors.mjs +1 -2
  31. package/dist/contract/hints.mjs +1 -2
  32. package/dist/contract/implementation.d.mts +2 -6
  33. package/dist/contract/implementation.mjs +2 -2
  34. package/dist/contract/media.d.mts +1 -3
  35. package/dist/contract/media.mjs +1 -2
  36. package/dist/contract/names.mjs +1 -2
  37. package/dist/contract/status.mjs +1 -2
  38. package/dist/http/client.d.mts +0 -2
  39. package/dist/http/client.mjs +1 -2
  40. package/dist/http/schema-errors.mjs +1 -2
  41. package/dist/mcp/client.d.mts +0 -2
  42. package/dist/mcp/client.mjs +2 -3
  43. package/dist/mcp/protocol.mjs +1 -2
  44. package/dist/stdio/console.mjs +1 -2
  45. package/dist/testing/memory.d.mts +1 -3
  46. package/dist/testing/memory.mjs +1 -2
  47. package/docs/Authentication-advanced.md +5 -1
  48. package/docs/setup.md +1 -2
  49. package/examples/kitchen-sink/src/request-policy.ts +5 -1
  50. package/examples/kitchen-sink/test/request-policy.test.ts +38 -2
  51. package/package.json +12 -16
package/dist/Action.d.mts CHANGED
@@ -4,7 +4,6 @@ import { ActionOf, AnyImplementation, Authorize, BuildError, BuildServices, Hold
4
4
  import { Call } from "./contract/call.mjs";
5
5
  import { Image } from "./contract/media.mjs";
6
6
  import { Context, Effect, Layer, Schema, Scope } from "effect";
7
- //#region src/contract/Action.d.ts
8
7
  type Codec = Schema.Codec<unknown, unknown, never, never>;
9
8
  type Fields = {
10
9
  readonly [key: string]: Codec;
@@ -259,5 +258,4 @@ export interface ClientOptions<A extends Any> {
259
258
  * - A builder acquiring a client of its own implementation is refused by the compiler, TS7022, and with the types bypassed never completes: call the services both use instead.
260
259
  */
261
260
  export declare function client<const Apps extends Served, const O extends ClientOptions<ActionOf<Member<Apps>>> = {}>(implementations: Apps, ...options: OptionalUnless<O, ClientOptions<ActionOf<Member<Apps>>> & O & NoInfer<Known<O, ClientOptions<Any>>>>): Effect.Effect<Client<Apps, Offered<O, ActionOf<Member<Apps>>>>, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Scope.Scope>;
262
- //#endregion
263
261
  export { type AnyImplementation, Anyone, type Authorize, type BuildError, type BuildServices, BuiltIn, Forbidden, Image, type Implementation, InvalidInput, type Issue, Refusal, Unauthenticated };
package/dist/Action.mjs CHANGED
@@ -6,7 +6,7 @@ import { Implementation, builders, built, memoized, select, toList } from "./con
6
6
  import { inputOf } from "./contract/call.mjs";
7
7
  import { Image } from "./contract/media.mjs";
8
8
  import { Array as Array$1, Cause, Context, Effect, Predicate, Schema } from "effect";
9
- //#region src/contract/Action.ts
9
+ // src/contract/Action.ts
10
10
  const NoInput = Schema.Record(Schema.String, Schema.Never);
11
11
  const codecOf = (schema) => Schema.isSchema(schema) ? schema : Reflect.ownKeys(schema).length === 0 ? NoInput : Schema.Struct(schema);
12
12
  function make(name, options) {
@@ -90,5 +90,4 @@ function client(served, options) {
90
90
  assertOnce("action", apps.flatMap((app) => app.actions), heldOnce);
91
91
  return Effect.map(built(apps), (bound) => Object.fromEntries(bound.map(([action, run]) => [action.name, methodOf(action, run)])));
92
92
  }
93
- //#endregion
94
93
  export { Anyone, BuiltIn, Forbidden, Image, InvalidInput, Refusal, Unauthenticated, allowAll, byName, client, implement, layer, make };
@@ -8,7 +8,6 @@ import "./ActionHttp.mjs";
8
8
  import { Effect } from "effect";
9
9
  import { HttpClient } from "effect/http";
10
10
  import { CliError, Command } from "effect/cli";
11
- //#region src/cli/ActionCli.d.ts
12
11
  interface AnyBinding extends Declaring {
13
12
  readonly actions: ReadonlyArray<Any>;
14
13
  }
@@ -81,6 +80,7 @@ type Local<App, A extends Any> = Effect.Effect<A["success"]["Type"], A["error"][
81
80
  *
82
81
  * - A success is its JSON on stdout, indented by two spaces, or `render`'s text; nothing for an action without `success`. Its Effect logs and `Console` output go to stderr; other writes as `logToStderr` says.
83
82
  * - A declared error is its JSON on stderr, under Effect CLI's `ERROR` heading, the command failing with a `UserError` whose `cause` is the error, exit 1 or its `Runtime.errorExitCode`; a refusal as `Action.Unauthenticated` and `Action.Forbidden` say; input that does not decode, in `--input` or a flag's JSON value, is `InvalidInput`'s JSON there, while a missing flag or a rejected choice is Effect CLI's own parse error.
83
+ * - Effect CLI styles what it prints, a failure's JSON included, on a terminal and, from Effect 4.0.6, wherever `FORCE_COLOR` forces colour: for plain text there, set `NO_COLOR` or provide `TerminalCapabilities.TerminalCapabilities` with `canColor: false`.
84
84
  * - A failure no schema encodes, a builder's or an accepted error that does not encode, is a `UserError` printed as its tag or name and its message, then its causes, never its other fields; a defect, an undeclared failure, or a success that does not encode is the command's defect, which `runMain` reports as `logToStderr` says.
85
85
  * - Provide services with `Command.provide` on the command, built when an action runs: provided around `Command.run`, they are built before parsing, so `--help` and a mistyped flag open the database.
86
86
  * - Each run builds the action's implementation, its builder and an authorizer an Effect builds, and releases them after the call, even where the host already built them; implementations passed beside it are not built.
@@ -161,5 +161,4 @@ export declare function remote<const H extends AnyHttp, const O extends Selectio
161
161
  * - It moves the default logger only: a host layer's `Console` output, the global `console.log`, and a logger writing through `Console.log` such as `Logger.consoleJson`, still reach stdout; log JSON with `Logger.withConsoleError(Logger.formatJson)`.
162
162
  */
163
163
  export declare const logToStderr: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, never, R>;
164
- //#endregion
165
164
  export { type Options as CommandOptions, Options$1 as Options, type UserError };
@@ -5,7 +5,7 @@ import { client } from "./ActionHttp.mjs";
5
5
  import { command as command$1, failingAsDerived } from "./cli/commands.mjs";
6
6
  import { Cause, Effect, Logger, Runtime } from "effect";
7
7
  import { Command } from "effect/cli";
8
- //#region src/cli/ActionCli.ts
8
+ // src/cli/ActionCli.ts
9
9
  /**
10
10
  * Make a command of your own fail as a derived command does. Give it what the command calls, the
11
11
  * binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and
@@ -80,6 +80,7 @@ const checkedCommandsOf = (commands, actions) => {
80
80
  *
81
81
  * - A success is its JSON on stdout, indented by two spaces, or `render`'s text; nothing for an action without `success`. Its Effect logs and `Console` output go to stderr; other writes as `logToStderr` says.
82
82
  * - A declared error is its JSON on stderr, under Effect CLI's `ERROR` heading, the command failing with a `UserError` whose `cause` is the error, exit 1 or its `Runtime.errorExitCode`; a refusal as `Action.Unauthenticated` and `Action.Forbidden` say; input that does not decode, in `--input` or a flag's JSON value, is `InvalidInput`'s JSON there, while a missing flag or a rejected choice is Effect CLI's own parse error.
83
+ * - Effect CLI styles what it prints, a failure's JSON included, on a terminal and, from Effect 4.0.6, wherever `FORCE_COLOR` forces colour: for plain text there, set `NO_COLOR` or provide `TerminalCapabilities.TerminalCapabilities` with `canColor: false`.
83
84
  * - A failure no schema encodes, a builder's or an accepted error that does not encode, is a `UserError` printed as its tag or name and its message, then its causes, never its other fields; a defect, an undeclared failure, or a success that does not encode is the command's defect, which `runMain` reports as `logToStderr` says.
84
85
  * - Provide services with `Command.provide` on the command, built when an action runs: provided around `Command.run`, they are built before parsing, so `--help` and a mistyped flag open the database.
85
86
  * - Each run builds the action's implementation, its builder and an authorizer an Effect builds, and releases them after the call, even where the host already built them; implementations passed beside it are not built.
@@ -137,5 +138,4 @@ const logToStderr = (self) => self.pipe(Effect.catchCause((cause) => {
137
138
  });
138
139
  return Effect.andThen(Runtime.getErrorReported(squashed) ? Effect.logError(cause) : Effect.void, Effect.die(reportedDefectKeepingCause));
139
140
  }), Effect.provideService(Logger.LogToStderr, true));
140
- //#endregion
141
141
  export { command, logToStderr, make, remote, remoteCommand, toUserError };
@@ -8,7 +8,6 @@ import { AnyHttp, Client, MethodError, Options } from "./http/client.mjs";
8
8
  import { Context, Effect, FileSystem, Layer, Path, Schema, Types } from "effect";
9
9
  import { Etag, HttpClient, HttpClientError, HttpPlatform, HttpRouter } from "effect/http";
10
10
  import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "effect/http-api";
11
- //#region src/http/ActionHttp.d.ts
12
11
  /** What `fetchClient` takes: `client`'s options, and the `fetch` its calls send with. */
13
12
  export interface FetchClientOptions<E = HttpClientError.HttpClientError, R = never> extends Options<E, R> {
14
13
  /**
@@ -146,7 +145,7 @@ export declare function make<const Actions extends ReadonlyArray<Any>>(actions:
146
145
  * - The 415 keeps from every handler what another site's page can send without a CORS preflight, such as a form post carrying the user's cookie; the server's CORS policy decides the rest.
147
146
  * - Router middleware providing a request service `X` is declared `HttpRouter.middleware<{ provides: X }>()`: without the type argument it runs and the layer still owes `X`.
148
147
  * - `HttpRouter.provideRequest(layer)` builds its layer once, at startup, so it never provides an identity or a tenant.
149
- * - A request body is read whole, however large, unless the host provides `HttpServerRequest.MaxBodySize` to `HttpRouter.serve`, limiting every route that reads its body whole, MCP endpoints included; a body a route reads as a stream is not limited. On Node a request over it gets no response, its connection closed, never a 413; `HttpRouter.toWebHandler` ignores it, so limit bodies in the platform serving the handler.
148
+ * - A request body is read whole, however large, unless the host provides `HttpServerRequest.MaxBodySize` to `HttpRouter.serve`, limiting every route that reads its body whole, MCP endpoints included; a body a route reads as a stream is not limited. On Node a request over it is an empty 413 from Effect 4.0.6, and before it gets no response, its connection closed; `HttpRouter.toWebHandler` ignores it, so limit bodies in the platform serving the handler.
150
149
  * - An `ErrorReporter` the server runs with is given, as `Info`, a handler's declared failure and an input that does not decode, never a refusal or a refused credential: give an error class `override readonly [ErrorReporter.ignore] = true` to keep it out, and drop `Info` for a built-in error, which takes no mark.
151
150
  */
152
151
  export declare function layer<const H extends AnyHttp, const Apps extends Served>(binding: H, implementations: Apps): HttpLayer<H, Apps, []>;
@@ -174,5 +173,4 @@ export declare function client<const H extends AnyHttp, E, R>(binding: H, option
174
173
  */
175
174
  export declare function fetchClient<const H extends AnyHttp>(binding: H, options?: FetchClientOptions): Client<H>;
176
175
  export declare function fetchClient<const H extends AnyHttp, E, R>(binding: H, options: FetchClientOptions<E, R>): Client<H, E, R>;
177
- //#endregion
178
176
  export { type AnyHttp as Any, type Client, type Options as ClientOptions, type MethodError, Options$1 as Options };
@@ -9,7 +9,7 @@ import { SchemaErrors, schemaErrors } from "./http/schema-errors.mjs";
9
9
  import { Context, Effect, Layer, Scope } from "effect";
10
10
  import { FetchHttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
11
11
  import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api";
12
- //#region src/http/ActionHttp.ts
12
+ // src/http/ActionHttp.ts
13
13
  const mountSegments = (prefix) => (prefix ?? "/api").split("/").filter((segment) => segment !== "");
14
14
  const absolutePath = (segments) => `/${segments.join("/")}`;
15
15
  const groupName = (mount) => mount.join("/") || "/";
@@ -77,5 +77,4 @@ function client(binding, options) {
77
77
  function fetchClient(binding, { fetch, ...options } = {}) {
78
78
  return methods(binding, options).pipe(Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.Fetch, fetch ?? ((input, init) => globalThis.fetch(input, init))), Effect.runSync);
79
79
  }
80
- //#endregion
81
80
  export { client, fetchClient, layer, make };
@@ -5,7 +5,6 @@ import { Client, ClientOptions, McpCallError, Params, RequestOptions, client, re
5
5
  import { Cause, Effect, Layer, Stdio } from "effect";
6
6
  import { McpSchema, McpServer } from "effect/ai";
7
7
  import { HttpRouter } from "effect/http";
8
- //#region src/mcp/ActionMcp.d.ts
9
8
  /**
10
9
  * An MCP server, over HTTP or stdio: every native `McpServer.layerStdio` option but `protocols`,
11
10
  * the server information, `instructions` and `extensions` among them, the actions `A` that are
@@ -118,5 +117,4 @@ export declare function layerHttp<const Apps extends Served, const O extends Sel
118
117
  * - The layers provided around it log to stdout, among the protocol's lines: apply `ActionCli.logToStderr` to the program, last, as its JSDoc says.
119
118
  */
120
119
  export declare function runStdio<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, E = never, R = never>(implementations: Apps, options: Options<E, R, ActionOf<Member<Apps>>> & O & NoInfer<Known<O, Options<E, R, Any>>>): Effect.Effect<void, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Cause.IllegalArgumentError | E, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>> | Stdio.Stdio | ToolRequestContext<ServedRequest<Member<Apps>, SelectedOf<O, Apps>>> | Features<R>>;
121
- //#endregion
122
120
  export { type Client, type ClientOptions, McpCallError, type Params, type RequestOptions, client, request };
@@ -9,19 +9,32 @@ import { defaultPath, fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom, p
9
9
  import { recordStepUp } from "./authentication/refusal.mjs";
10
10
  import { withStderrConsole } from "./stdio/console.mjs";
11
11
  import { McpCallError, client, request } from "./mcp/client.mjs";
12
- import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Predicate, Schema } from "effect";
12
+ import { Cause, Context, Effect, ErrorReporter, Exit, Fiber, JsonPointer, Layer, Option, Predicate, Schema, Stdio, Stream } from "effect";
13
13
  import { AiError, McpSchema, McpServer } from "effect/ai";
14
14
  import { HttpRouter, HttpServerRequest } from "effect/http";
15
15
  import { Base64 } from "effect/encoding";
16
- //#region src/mcp/ActionMcp.ts
16
+ // src/mcp/ActionMcp.ts
17
17
  const isToolArgumentsSchema = Schema.is(McpSchema.ToolJson);
18
+ const isJson = Schema.is(Schema.Json);
19
+ const definitionsReferenced = (value) => {
20
+ if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return [];
21
+ if (Array.isArray(value)) return value.flatMap(definitionsReferenced);
22
+ return Object.entries(value).flatMap(([field, nested]) => {
23
+ if (field !== "$ref") return definitionsReferenced(nested);
24
+ const [scope, key] = Predicate.isString(nested) ? JsonPointer.parseUriFragment(nested) ?? [] : [];
25
+ return scope === "$defs" && key !== void 0 ? [key] : [];
26
+ });
27
+ };
18
28
  const toolJsonSchema = (schema, strict) => {
19
29
  const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
20
- const [scope, key] = Predicate.isString(document.schema.$ref) ? JsonPointer.parseUriFragment(document.schema.$ref) ?? [] : [];
21
- const root = scope === "$defs" && key !== void 0 ? document.definitions[key] : document.schema;
22
- return root === void 0 || Object.keys(document.definitions).length === 0 ? root : {
30
+ const [scope, key] = Predicate.isString(document.schema["$ref"]) ? JsonPointer.parseUriFragment(document.schema["$ref"]) ?? [] : [];
31
+ const inlined = scope === "$defs" && key !== void 0 ? key : void 0;
32
+ const root = inlined === void 0 ? document.schema : document.definitions[inlined];
33
+ if (root === void 0) return void 0;
34
+ const definitions = inlined !== void 0 && ![root, ...Object.values(document.definitions)].flatMap((schema) => isJson(schema) ? definitionsReferenced(schema) : []).includes(inlined) ? Object.fromEntries(Object.entries(document.definitions).filter(([name]) => name !== inlined)) : document.definitions;
35
+ return Object.keys(definitions).length === 0 ? root : {
23
36
  ...root,
24
- $defs: document.definitions
37
+ $defs: definitions
25
38
  };
26
39
  };
27
40
  const assertToolShapes = (lifted) => {
@@ -151,6 +164,7 @@ const namesItsMethod = (request) => Effect.match(request.json, {
151
164
  onFailure: () => true,
152
165
  onSuccess: (body) => !Predicate.hasProperty(body, "method") || body.method === request.headers["mcp-method"]
153
166
  });
167
+ const readsItsBody = (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.method === "POST" ? Effect.andThen(request.text, route) : route);
154
168
  function layerHttp(apps, { actions, features, path = defaultPath, authentication, requestScoped, allowSubscriptions = features !== void 0, ...options }) {
155
169
  const selected = select(toList(apps), actions);
156
170
  const served = selected.flatMap((app) => app.actions);
@@ -166,17 +180,21 @@ function layerHttp(apps, { actions, features, path = defaultPath, authentication
166
180
  allowSessionTermination: true,
167
181
  allowSubscriptions
168
182
  }), features, authentication);
169
- if (authentication === void 0) return transport;
183
+ if (authentication === void 0) return transport.pipe(Layer.provide(HttpRouter.middleware(readsItsBody).layer));
170
184
  const required = requiresAuthentication(served);
171
- const middleware = HttpRouter.middleware(Effect.map(providerOf(authentication), (runtime) => (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.method !== "POST" || required(request.headers) ? runtime.http(route, false) : Effect.flatMap(namesItsMethod(request), (named) => runtime.http(route, named)))));
185
+ const middleware = HttpRouter.middleware(Effect.map(providerOf(authentication), (runtime) => (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.method !== "POST" || required(request.headers) ? runtime.http(readsItsBody(route), false) : Effect.flatMap(namesItsMethod(request), (named) => runtime.http(readsItsBody(route), named)))));
172
186
  return transport.pipe(Layer.provide(middleware.layer));
173
187
  }
188
+ const isJsonLine = (line) => Option.isSome(Schema.decodeOption(Schema.fromJsonString(Schema.Json))(line));
189
+ const jsonLinesOnly = Layer.effect(Stdio.Stdio, Effect.map(Stdio.Stdio, (stdio) => Stdio.make({
190
+ ...stdio,
191
+ stdin: stdio.stdin.pipe(Stream.decodeText, Stream.splitLines, Stream.filter(isJsonLine), Stream.map((line) => `${line}\n`), Stream.encodeText)
192
+ })));
174
193
  function runStdio(apps, { actions, features, ...options }) {
175
194
  const transport = server(select(toList(apps), actions), McpServer.layerStdio({
176
195
  ...options,
177
196
  protocols
178
- }), features);
197
+ }).pipe(Layer.provide(jsonLinesOnly)), features);
179
198
  return Layer.launch(transport).pipe(withStderrConsole, Effect.forkChild, Effect.flatMap(Fiber.await), Effect.flatMap((exit) => Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause) ? Effect.failCause(exit.cause) : Effect.void));
180
199
  }
181
- //#endregion
182
200
  export { McpCallError, client, layerHttp, request, runStdio };
@@ -8,7 +8,6 @@ import { Certain, ErrorsOf } from "./contract/declared.mjs";
8
8
  import { Context, Effect, Layer, Schema, Scope, Types } from "effect";
9
9
  import { HttpRouter } from "effect/http";
10
10
  import { Rpc, RpcClient, RpcClientError, RpcGroup, RpcMiddleware, RpcServer } from "effect/rpc";
11
- //#region src/rpc/ActionRpc.d.ts
12
11
  type BindingError = Errors[number];
13
12
  type ErrorOf<A extends Any$1, E extends Errors, D> = Schema.Union<ReadonlyArray<A["error"][number] | E[number] | VerifierError<A, D> | BuiltIns>>;
14
13
  type RpcOf<A extends Any$1, E extends Errors, D> = A extends Any$1 ? Rpc.Rpc<A["name"], A["input"], A["success"], ErrorOf<A, E, D>> : never;
@@ -158,5 +157,4 @@ export type ClientOptions = Omit<NonNullable<Parameters<typeof RpcClient.make>[1
158
157
  * - The socket belongs to the Layer providing the protocol, not to the `Scope` around the client: provided around one call, it opens a socket for that call alone.
159
158
  */
160
159
  export declare function client<const B extends Any>(binding: B, options?: ClientOptions): Effect.Effect<Client<B>, never, RpcClient.Protocol | Scope.Scope>;
161
- //#endregion
162
160
  export {};
@@ -7,7 +7,7 @@ import { Cause, Effect, ErrorReporter, Layer, Predicate, Result, Schema, SchemaT
7
7
  import { HttpClientRequest, HttpServerRequest } from "effect/http";
8
8
  import { HttpApiBuilder } from "effect/http-api";
9
9
  import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer } from "effect/rpc";
10
- //#region src/rpc/ActionRpc.ts
10
+ // src/rpc/ActionRpc.ts
11
11
  function make(actions, options = {}) {
12
12
  const bound = bindingOf("ActionRpc binding", actions, options);
13
13
  const group = RpcGroup.make(...actions.map((action) => Rpc.make(action.name, {
@@ -30,8 +30,8 @@ var InternalError = class extends Error {
30
30
  }
31
31
  };
32
32
  var DeclaredFailure = class {
33
- json;
34
33
  [ErrorReporter.ignore] = true;
34
+ json;
35
35
  constructor(json) {
36
36
  this.json = json;
37
37
  }
@@ -98,5 +98,4 @@ function client(binding, options) {
98
98
  }));
99
99
  });
100
100
  }
101
- //#endregion
102
101
  export { client, layer, make };
@@ -3,7 +3,6 @@ import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Offered, O
3
3
  import { Any } from "./Action.mjs";
4
4
  import { Effect, Layer, Schema } from "effect";
5
5
  import { Prompt, Tool, Toolkit } from "effect/ai";
6
- //#region src/toolkit/ActionToolkit.d.ts
7
6
  type NativeTool<A extends Any, R> = Tool.Tool<A["name"], {
8
7
  readonly parameters: Schema.toCodecJson<A["input"]>;
9
8
  readonly success: Schema.toCodecJson<A["success"]>;
@@ -104,5 +103,4 @@ export interface Approval {
104
103
  * `examples/kitchen-sink/src/assistant/handlers.ts`.
105
104
  */
106
105
  export declare const openApprovals: ({ content: messages }: Prompt.Prompt) => ReadonlyArray<Approval>;
107
- //#endregion
108
106
  export {};
@@ -4,7 +4,7 @@ import { hintsOf } from "./contract/hints.mjs";
4
4
  import { Implementation, acquire, provideHandlers, select, toList } from "./contract/implementation.mjs";
5
5
  import { Effect, Schema } from "effect";
6
6
  import { Tool, Toolkit } from "effect/ai";
7
- //#region src/toolkit/ActionToolkit.ts
7
+ // src/toolkit/ActionToolkit.ts
8
8
  const annotateIfGiven = (tool, key, value) => value === void 0 ? tool : tool.annotate(key, value);
9
9
  const annotateFromContract = (tool, action) => {
10
10
  const { readOnlyHint, destructiveHint, idempotentHint, openWorldHint } = hintsOf(action);
@@ -64,5 +64,4 @@ const openApprovals = ({ content: messages }) => {
64
64
  }];
65
65
  });
66
66
  };
67
- //#endregion
68
67
  export { make, openApprovals };
@@ -6,7 +6,6 @@ import { ErrorsOf } from "./contract/declared.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/Authentication.d.ts
10
9
  /**
11
10
  * The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
12
11
  * `HttpApiSecurity.bearer` reads it for routes and MCP: the scheme matched case-insensitively,
@@ -190,5 +189,4 @@ export type Protection<I, Name extends string> = HttpRouter.Middleware<{
190
189
  * - An erased descriptor, `Any`, provides no service the types can name: its identity is `unknown`, which would discharge every request service the route owes.
191
190
  */
192
191
  export declare const protect: <I, A, S extends Security, Name extends string, E extends Errors>(authentication: Descriptor<I, A, S, Name, E>) => Protection<I, Name>;
193
- //#endregion
194
192
  export type { Any, Descriptor, Provider };
@@ -5,7 +5,7 @@ import { answer, answerStepUp, bearer, challenge, declaredResponse, isRefusal, i
5
5
  import { Context, Effect, Layer, Match, Option, Predicate, Redacted } from "effect";
6
6
  import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
7
7
  import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
8
- //#region src/authentication/Authentication.ts
8
+ // src/authentication/Authentication.ts
9
9
  const bearerAuthorizationHeader = /^[ \t]*Bearer +(.*[^ \t])[ \t]*$/i;
10
10
  /**
11
11
  * The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
@@ -191,5 +191,4 @@ function layer(authentication, verify, options = {}) {
191
191
  * - An erased descriptor, `Any`, provides no service the types can name: its identity is `unknown`, which would discharge every request service the route owes.
192
192
  */
193
193
  const protect = (authentication) => HttpRouter.middleware()(Effect.map(providerOf(authentication), (runtime) => (route) => runtime.route(route)));
194
- //#endregion
195
194
  export { bearerTokenOf, layer, make, protect, refusalResponse };
@@ -1,7 +1,6 @@
1
1
  import { Served } from "./testing/memory.mjs";
2
2
  import { Layer } from "effect";
3
3
  import { HttpClient, HttpRouter } from "effect/http";
4
- //#region src/testing/Testing.d.ts
5
4
  /**
6
5
  * The native `HttpClient`, answered by `handler` instead of the network: a web handler the test
7
6
  * serves and disposes of, which other tests and raw web `Request`s may share, such as
@@ -29,5 +28,4 @@ export declare function layer(handler: (request: Request) => Promise<Response>):
29
28
  * - It never requires `FileSystem`, `Path`, `HttpPlatform` or `Etag.Generator`: one provided around it is the routes', and `HttpServer.layerServices`' defaults stand in for the rest, whose `FileSystem` is a no-op, so a file the routes read is `NotFound` until `NodeFileSystem.layer` is provided around it.
30
29
  * - It answers HTTP requests and cannot upgrade a WebSocket: give an rpc layer `RpcServer.layerProtocolHttp` in the test, and its client `RpcClient.layerProtocolHttp` over this layer.
31
30
  */
32
- 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
- //#endregion
31
+ 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>>;
package/dist/Testing.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { clientOf } from "./testing/memory.mjs";
2
2
  import { Cause, Console, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path } from "effect";
3
3
  import { Etag, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
4
- //#region src/testing/Testing.ts
4
+ // src/testing/Testing.ts
5
5
  function layer(routes) {
6
6
  if (!Layer.isLayer(routes)) return clientOf(routes);
7
7
  return Layer.unwrap(Effect.gen(function* () {
@@ -13,5 +13,4 @@ function layer(routes) {
13
13
  }));
14
14
  }
15
15
  const isReported = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) && !ErrorReporter.isIgnored(reason.error) || Cause.isDieReason(reason) && !ErrorReporter.isIgnored(reason.defect));
16
- //#endregion
17
16
  export { layer };
@@ -5,7 +5,6 @@ import { Any as Any$1, Errors } from "../Action.mjs";
5
5
  import { Context, Effect } from "effect";
6
6
  import { HttpRouter, HttpServerResponse } from "effect/http";
7
7
  import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
8
- //#region src/authentication/provider.d.ts
9
8
  /** One native security scheme: Bearer unless a descriptor names another. */
10
9
  export type Security = HttpApiSecurity.HttpApiSecurity;
11
10
  /** What a native scheme decodes: a `Redacted` token or key, or Basic credentials. */
@@ -127,5 +126,4 @@ export type VerifierError<A extends Any$1, D> = A["caller"] extends typeof Anyon
127
126
  readonly error: infer E extends Errors;
128
127
  } ? E[number] : never;
129
128
  export type ServedProvider<App, A extends Any$1, D> = [Protected<Serving<App, A>>] extends [never] ? never : ProviderOf<D>;
130
- //#endregion
131
129
  export { Runtime$1 as Runtime };
@@ -1,7 +1,6 @@
1
1
  import { Unauthenticated } from "../contract/errors.mjs";
2
2
  import { Effect, Option } from "effect";
3
- //#region src/authentication/provider.ts
3
+ // src/authentication/provider.ts
4
4
  const providerOf = (auth) => Effect.flatMap(Effect.service(auth["~provider"]), (provider) => provider.descriptor === auth ? Effect.succeed(provider) : Effect.die(/* @__PURE__ */ new Error(`Authentication "${auth.name}": the descriptor named here is not the one its provider was built from (build both from one descriptor)`)));
5
5
  const promote = (auth, run) => Effect.flatMap(Effect.serviceOption(auth["~verified"]), (actor) => Option.isSome(actor) ? Effect.provideService(run, auth.identity, actor.value) : Effect.fail(new Unauthenticated()));
6
- //#endregion
7
6
  export { promote, providerOf };
@@ -3,7 +3,7 @@ import { declared, httpApiStatusOf } from "../contract/status.mjs";
3
3
  import { errorSchemas } from "../contract/declared.mjs";
4
4
  import { Cause, Context, Effect, Exit, Option, Predicate, Ref, Schema } from "effect";
5
5
  import { HttpServerResponse } from "effect/http";
6
- //#region src/authentication/refusal.ts
6
+ // src/authentication/refusal.ts
7
7
  const responseOf = (schema, error) => HttpServerResponse.jsonUnsafe(Schema.encodeUnknownSync(Schema.toCodecJson(schema))(error), { status: httpApiStatusOf(schema.ast) ?? 422 });
8
8
  /** `error` answered as an endpoint declaring `errors` answers it, if one of them declares it. */
9
9
  const declaredResponse = (errors, error) => {
@@ -43,5 +43,4 @@ const answerStepUp = (route, metadataUrl) => Effect.gen(function* () {
43
43
  const diedOrInterruptedAfterRefusal = Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause));
44
44
  return Option.isSome(refused) && !diedOrInterruptedAfterRefusal ? answer(refused.value, metadataUrl) : yield* exit;
45
45
  });
46
- //#endregion
47
46
  export { answer, answerStepUp, bearer, challenge, declaredResponse, isRefusal, isStepUp, plain, recordStepUp };
@@ -2,7 +2,6 @@ import "../contract/errors.mjs";
2
2
  import { Any } from "../Action.mjs";
3
3
  import { Effect, Option, Runtime, Types } from "effect";
4
4
  import { CliError, Command } from "effect/cli";
5
- //#region src/cli/commands.d.ts
6
5
  type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
7
6
  type StdinField<A extends Any> = A["input"]["Encoded"] extends (infer E) ? Exclude<Field<A>, { [K in keyof E]-?: {} extends Pick<E, K> ? K : never; }[keyof E]> : never;
8
7
  /** How one command of `A` is named, takes its input and prints its result. */
@@ -67,5 +66,4 @@ export declare class UserError<out E = unknown> extends CliError.UserError {
67
66
  get reason(): E;
68
67
  get [Runtime.errorExitCode](): number;
69
68
  }
70
- //#endregion
71
69
  export {};
@@ -6,7 +6,7 @@ import { decodeInput } from "../contract/call.mjs";
6
6
  import { withStderrConsole } from "../stdio/console.mjs";
7
7
  import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST, Stdio, Stream } from "effect";
8
8
  import { CliError, Command, Flag, Param } from "effect/cli";
9
- //#region src/cli/commands.ts
9
+ // src/cli/commands.ts
10
10
  const kebab = (name) => {
11
11
  const kebabbed = name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
12
12
  return kebabbed === "" ? name : kebabbed;
@@ -324,5 +324,4 @@ const command = (action, execute, options, errors) => {
324
324
  }, ({ input, args: values, json }) => run(parsedFields(input, values), json ? void 0 : render));
325
325
  return stdinHelp === void 0 ? Command.withDescription(command, description) : command.pipe(Command.withDescription(`${description}\n\n${stdinHelp}`), Command.withShortDescription(description));
326
326
  };
327
- //#endregion
328
327
  export { UserError, command, failingAsDerived, kebab };
@@ -1,10 +1,9 @@
1
1
  import { SchemaAST } from "effect";
2
- //#region src/contract/ast.ts
2
+ // src/contract/ast.ts
3
3
  const unsuspended = (ast) => SchemaAST.isSuspend(ast) ? unsuspended(ast.thunk()) : ast;
4
4
  const members = (ast) => {
5
5
  const resolved = unsuspended(ast);
6
6
  return SchemaAST.isUnion(resolved) ? resolved.types.flatMap(members) : [resolved];
7
7
  };
8
8
  const literalValues = (ast) => SchemaAST.isLiteral(ast) ? [ast.literal] : SchemaAST.isEnum(ast) ? ast.enums.map(([, value]) => value) : [void 0];
9
- //#endregion
10
9
  export { literalValues, members, unsuspended };
@@ -2,12 +2,10 @@ import "./errors.mjs";
2
2
  import "./implementation.mjs";
3
3
  import { Any } from "../Action.mjs";
4
4
  import { Effect, Schema } from "effect";
5
- //#region src/contract/call.d.ts
6
5
  /**
7
6
  * A call of `A` answering `R`. It may leave its input out when `{}` is a valid encoded input,
8
7
  * such as for an action declared without `input` or one whose every field has a decoding
9
8
  * default, and then sends the input `{}` decodes to. Every client shares this rule:
10
9
  * `ActionHttp.client`, `ActionRpc.client`, `ActionMcp.client` and `Action.client`.
11
10
  */
12
- export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
13
- //#endregion
11
+ export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
@@ -1,6 +1,6 @@
1
1
  import { InvalidInput } from "./errors.mjs";
2
2
  import { Effect, Schema } from "effect";
3
- //#region src/contract/call.ts
3
+ // src/contract/call.ts
4
4
  /**
5
5
  * How HTTP, RPC, MCP and a command decode a request's input: reporting every issue, and refusing a
6
6
  * field the input does not declare.
@@ -19,5 +19,4 @@ const checked = (action, send) => {
19
19
  const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(action.input), { errors: "all" });
20
20
  return (...args) => inputOf(action, args).pipe(Effect.tap(encode), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap(send));
21
21
  };
22
- //#endregion
23
22
  export { checked, decodeInput, inputOf, inputParseOptions };
@@ -1,9 +1,7 @@
1
1
  import "../Action.mjs";
2
- //#region src/contract/caller.d.ts
3
2
  /**
4
3
  * The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
5
4
  * authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
6
5
  * agree on a name.
7
6
  */
8
- export declare const Anyone: unique symbol;
9
- //#endregion
7
+ export declare const Anyone: unique symbol;
@@ -1,4 +1,4 @@
1
- //#region src/contract/caller.ts
1
+ // src/contract/caller.ts
2
2
  /**
3
3
  * The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
4
4
  * authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
@@ -8,5 +8,4 @@ const Anyone = Symbol.for("@gjermundgaraba/effect-actions/Anyone");
8
8
  const assertAuthentication = (actions, authentication) => {
9
9
  for (const action of actions) if (action.caller !== Anyone && (authentication === void 0 || action.caller.key !== authentication.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor (give it the authentication whose identity the action's caller names)`);
10
10
  };
11
- //#endregion
12
11
  export { Anyone, assertAuthentication };
@@ -1,7 +1,6 @@
1
1
  import "./caller.mjs";
2
2
  import { Errors } from "../Action.mjs";
3
3
  import { Types } from "effect";
4
- //#region src/contract/declared.d.ts
5
4
  type ListOf<G> = G extends Errors ? G : readonly [G];
6
5
  /**
7
6
  * The errors options `O` declare, as at run time: those `error` always gives, or, where it may
@@ -24,5 +23,4 @@ export interface Declaring {
24
23
  readonly error: Errors;
25
24
  } | undefined;
26
25
  }
27
- //#endregion
28
26
  export {};
@@ -4,7 +4,7 @@ import { builtIns } from "./errors.mjs";
4
4
  import { assertOnce } from "./names.mjs";
5
5
  import { httpApiStatusOf } from "./status.mjs";
6
6
  import { Predicate, Schema, SchemaAST } from "effect";
7
- //#region src/contract/declared.ts
7
+ // src/contract/declared.ts
8
8
  const errorList = (error) => error === void 0 ? [] : Schema.isSchema(error) ? [error] : error;
9
9
  const ownsNothing = (ast) => httpApiStatusOf(ast) === void 0 && ast.checks === void 0 && ast.encoding === void 0;
10
10
  const schemasOf = (ast) => !ownsNothing(ast) ? [ast] : SchemaAST.isSuspend(ast) ? schemasOf(ast.thunk()) : SchemaAST.isUnion(ast) ? ast.types.flatMap(schemasOf) : [ast];
@@ -51,5 +51,4 @@ const bindingOf = (what, actions, options) => {
51
51
  for (const action of actions) assertDistinctTags(`${what} of "${action.name}"`, bindingErrors(action, bound));
52
52
  return bound;
53
53
  };
54
- //#endregion
55
54
  export { assertDistinctTags, bindingErrors, bindingOf, errorList, errorSchemas, projectedErrors };
@@ -1,5 +1,4 @@
1
1
  import { Schema } from "effect";
2
- //#region src/contract/errors.d.ts
3
2
  declare const Issue: Schema.Struct<{
4
3
  readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
5
4
  readonly message: Schema.String;
@@ -100,5 +99,4 @@ export type BuiltIns = (typeof builtIns)[number];
100
99
  export declare const BuiltIn: Schema.Union<readonly [typeof InvalidInput, typeof Unauthenticated, typeof Forbidden]>;
101
100
  /** A built-in failure: `InvalidInput | Refusal`. */
102
101
  export type BuiltIn = (typeof BuiltIn)["Type"];
103
- //#endregion
104
102
  export { Issue };
@@ -1,5 +1,5 @@
1
1
  import { Effect, Predicate, Schema, SchemaIssue } from "effect";
2
- //#region src/contract/errors.ts
2
+ // src/contract/errors.ts
3
3
  const defaultedMessage = (fallback) => Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(fallback)));
4
4
  const Issue = Schema.Struct({
5
5
  path: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
@@ -89,5 +89,4 @@ const Refusal = Schema.Union(refusals);
89
89
  const builtIns = [InvalidInput, ...refusals];
90
90
  /** The built-in failures as one schema: `Schema.is(Action.BuiltIn)(error)`. */
91
91
  const BuiltIn = Schema.Union(builtIns);
92
- //#endregion
93
92
  export { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated, builtIns, scopeToken };
@@ -1,4 +1,4 @@
1
- //#region src/contract/hints.ts
1
+ // src/contract/hints.ts
2
2
  /**
3
3
  * An action's MCP tool hints, MCP's defaults where its `mcp` leaves one out, except that a read
4
4
  * is not destructive where MCP's default would say it is: no tool idempotent and every tool
@@ -11,5 +11,4 @@ const hintsOf = ({ readOnly, mcp }) => ({
11
11
  idempotentHint: mcp.idempotentHint ?? false,
12
12
  openWorldHint: mcp.openWorldHint ?? true
13
13
  });
14
- //#endregion
15
14
  export { hintsOf };
@@ -2,7 +2,6 @@ import { Anyone } from "./caller.mjs";
2
2
  import { Refusal } from "./errors.mjs";
3
3
  import { Any } from "../Action.mjs";
4
4
  import { Context, Effect, Layer, Scope } from "effect";
5
- //#region src/contract/implementation.d.ts
6
5
  /** Decoded values at the dispatch boundary every surface shares. */
7
6
  export type ErasedValue = Any["input"]["Type"];
8
7
  /**
@@ -39,8 +38,6 @@ export declare class Implementation<A extends Any, RequestServices extends {
39
38
  readonly [name: string]: unknown;
40
39
  }, EX, RX, RA = never, EAX = never, RAX = never> {
41
40
  #private;
42
- /** The contracts this implementation answers. */
43
- readonly actions: ReadonlyArray<A>;
44
41
  /** Type-only: each action's handler's request-time requirements. */
45
42
  readonly "~request": RequestServices;
46
43
  /** Type-only: what building its handlers fails with. */
@@ -53,9 +50,9 @@ export declare class Implementation<A extends Any, RequestServices extends {
53
50
  readonly "~authorizeBuildError": EAX;
54
51
  /** Type-only: what building its authorizer needs. */
55
52
  readonly "~authorizeBuildServices": RAX;
56
- constructor(
57
53
  /** The contracts this implementation answers. */
58
- actions: ReadonlyArray<A>,
54
+ readonly actions: ReadonlyArray<A>;
55
+ constructor(actions: ReadonlyArray<A>,
59
56
  /** Builds each action paired with its handler: its own builder, or the one it shares. */
60
57
  handlers: Memoized<Bound, EX, RX>,
61
58
  /** Builds its authorizer. */
@@ -189,5 +186,4 @@ export type BuildServices<App, Listed extends Any = OwnActions<App>> = App exten
189
186
  readonly "~buildServices": infer RX;
190
187
  readonly "~authorizeBuildServices": infer RAX;
191
188
  } ? RX | ([Protected<Serving<App, Listed>>] extends [never] ? never : RAX) : never;
192
- //#endregion
193
189
  export {};
@@ -3,7 +3,7 @@ import { Unauthenticated } from "./errors.mjs";
3
3
  import { assertDistinct, assertOnce, heldOnce } from "./names.mjs";
4
4
  import { projectedErrors } from "./declared.mjs";
5
5
  import { Array, Cause, Context, Effect, Exit, Layer, Option, Schema } from "effect";
6
- //#region src/contract/implementation.ts
6
+ // src/contract/implementation.ts
7
7
  const uniqueKey = () => Math.random().toString(36).slice(2);
8
8
  const memoized = (build) => {
9
9
  const key = Context.Service(`effect-actions/Implementation/${uniqueKey()}`);
@@ -22,6 +22,7 @@ const memoized = (build) => {
22
22
  * failures and services of the authorizer's builder.
23
23
  */
24
24
  var Implementation = class Implementation {
25
+ /** The contracts this implementation answers. */
25
26
  actions;
26
27
  #handlers;
27
28
  #authorizer;
@@ -168,5 +169,4 @@ const dispatch = (action, handle, before) => {
168
169
  });
169
170
  return (input) => Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(Effect.suspend(() => handle(input))), Effect.catchCause(declaredOrDefect))).pipe(Effect.annotateLogs(actionAttributes), Effect.withSpan(action.name, { attributes: actionAttributes }, { captureStackTrace: false }));
170
171
  };
171
- //#endregion
172
172
  export { Implementation, acquire, assertHeld, builders, built, memoized, notHeld, provideHandlers, select, servingOf, toList };
@@ -1,5 +1,4 @@
1
1
  import { Schema, SchemaAST } from "effect";
2
- //#region src/contract/media.d.ts
3
2
  /**
4
3
  * An image, a media field of a success: `{ data: Uint8Array, mimeType: string }`, `mimeType` such
5
4
  * as `image/png`. A top-level field of a struct success, one, optional, or a required array, or the
@@ -13,5 +12,4 @@ import { Schema, SchemaAST } from "effect";
13
12
  export declare const Image: Schema.Struct<{
14
13
  readonly data: Schema.Uint8Array;
15
14
  readonly mimeType: Schema.String;
16
- }>;
17
- //#endregion
15
+ }>;
@@ -1,5 +1,5 @@
1
1
  import { Schema } from "effect";
2
- //#region src/contract/media.ts
2
+ // src/contract/media.ts
3
3
  const mediaKindAnnotation = "effect-actions/media";
4
4
  const Media = Schema.Struct({
5
5
  data: Schema.Uint8Array,
@@ -18,5 +18,4 @@ const Media = Schema.Struct({
18
18
  const Image = Media.annotate({ [mediaKindAnnotation]: "image" });
19
19
  const isMarked = (ast) => ast.annotations?.[mediaKindAnnotation] === "image";
20
20
  const kindOf = (ast) => isMarked(ast) && ast.encoding === void 0 ? "image" : void 0;
21
- //#endregion
22
21
  export { Image, Media, isMarked, kindOf };
@@ -1,4 +1,4 @@
1
- //#region src/contract/names.ts
1
+ // src/contract/names.ts
2
2
  const namePattern = "[A-Za-z0-9_-]{1,128}";
3
3
  const validName = new RegExp(`^${namePattern}$`);
4
4
  const assertName = (name) => {
@@ -23,5 +23,4 @@ const assertKnown = (what, keys, names, fix) => {
23
23
  const unknown = keys.filter((key) => !names.includes(key));
24
24
  if (unknown.length > 0) throw new Error(`Unknown ${what}: ${unknown.join(", ")} (${fix})`);
25
25
  };
26
- //#endregion
27
26
  export { assertDistinct, assertKnown, assertName, assertOnce, heldOnce };
@@ -1,5 +1,5 @@
1
1
  import { SchemaAST } from "effect";
2
- //#region src/contract/status.ts
2
+ // src/contract/status.ts
3
3
  const httpApiStatusOf = SchemaAST.resolveAt("httpApiStatus");
4
4
  const statusThroughSuspensions = (ast) => httpApiStatusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThroughSuspensions(ast.thunk()) : void 0);
5
5
  /** One error schema, from `errorSchemas`, as an endpoint declares it: with its status, 422 without one. */
@@ -7,5 +7,4 @@ const declared = (error) => {
7
7
  const status = statusThroughSuspensions(error.ast);
8
8
  return status === void 0 ? error.annotate({ httpApiStatus: 422 }) : httpApiStatusOf(error.ast) === void 0 ? error.annotate({ httpApiStatus: status }) : error;
9
9
  };
10
- //#endregion
11
10
  export { declared, httpApiStatusOf };
@@ -5,7 +5,6 @@ import { Any as Any$1, VerifierError } from "../authentication/provider.mjs";
5
5
  import { Effect, Schema } from "effect";
6
6
  import { HttpClient, HttpClientError } from "effect/http";
7
7
  import { HttpApi, HttpApiClient } from "effect/http-api";
8
- //#region src/http/client.d.ts
9
8
  /**
10
9
  * The native `HttpApiClient.make` options except `transformResponse`, which may change a call's
11
10
  * success, failure or services the method types cannot follow: `baseUrl`, and `transformClient`,
@@ -58,5 +57,4 @@ export interface AnyHttp {
58
57
  * what its `transformClient` adds.
59
58
  */
60
59
  export type Client<H extends AnyHttp, E = never, R = never> = { readonly [A in H["actions"][number] as A["name"]]: Method<A, H["error"][number] | VerifierError<A, H["authentication"]>, E, R>; };
61
- //#endregion
62
60
  export {};
@@ -2,7 +2,7 @@ import { checked } from "../contract/call.mjs";
2
2
  import { Effect } from "effect";
3
3
  import { HttpClient } from "effect/http";
4
4
  import { HttpApiClient } from "effect/http-api";
5
- //#region src/http/client.ts
5
+ // src/http/client.ts
6
6
  const methodsWith = (http, httpClient, baseUrl) => Effect.map(HttpApiClient.makeWith(http.api, {
7
7
  baseUrl,
8
8
  httpClient
@@ -15,5 +15,4 @@ const methodsWith = (http, httpClient, baseUrl) => Effect.map(HttpApiClient.make
15
15
  }));
16
16
  });
17
17
  const methods = (http, { baseUrl, transformClient } = {}) => Effect.flatMap(HttpClient.HttpClient, (native) => transformClient === void 0 ? methodsWith(http, native, baseUrl) : methodsWith(http, transformClient(native), baseUrl));
18
- //#endregion
19
18
  export { methods };
@@ -1,9 +1,8 @@
1
1
  import { InvalidInput } from "../contract/errors.mjs";
2
2
  import { Effect } from "effect";
3
3
  import { HttpApiMiddleware } from "effect/http-api";
4
- //#region src/http/schema-errors.ts
4
+ // src/http/schema-errors.ts
5
5
  const responseEncodingKinds = /* @__PURE__ */ new Set(["Body", "ResponseHeaders"]);
6
6
  var SchemaErrors = class extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: [InvalidInput] }) {};
7
7
  const schemaErrors = () => HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(responseEncodingKinds.has(failure.kind) ? failure : InvalidInput.fromSchemaError(failure.cause)));
8
- //#endregion
9
8
  export { SchemaErrors, schemaErrors };
@@ -4,7 +4,6 @@ import { Any } from "../Action.mjs";
4
4
  import { Any as Any$1, VerifierError } from "../authentication/provider.mjs";
5
5
  import { Effect, Schema } from "effect";
6
6
  import { Headers, HttpClient, HttpClientError, HttpClientRequest } from "effect/http";
7
- //#region src/mcp/client.d.ts
8
7
  /**
9
8
  * An MCP request's parameters, as `request` takes them: JSON, with any `_meta`, leaving
10
9
  * out an `undefined` one, as a caller may. A helper passing on whole params types them as this,
@@ -136,5 +135,4 @@ export declare function client<const Actions extends ReadonlyArray<Any>, E, R, D
136
135
  * a call its types would not send.
137
136
  */
138
137
  export declare const request: (method: string, params?: Params, { headers, url }?: RequestOptions) => HttpClientRequest.HttpClientRequest;
139
- //#endregion
140
138
  export {};
@@ -8,11 +8,11 @@ import { Effect, Match, Option, Predicate, Schema } from "effect";
8
8
  import { McpSchema } from "effect/ai";
9
9
  import { HttpClient, HttpClientRequest } from "effect/http";
10
10
  import { Sse } from "effect/encoding";
11
- //#region src/mcp/client.ts
11
+ // src/mcp/client.ts
12
12
  /** The routing headers and JSON-RPC body of the request `request` builds. */
13
13
  const statelessRequest = (method, params) => {
14
14
  const { protocolVersion } = httpProtocol;
15
- const name = method === "resources/read" ? params.uri : params.name;
15
+ const name = method === "resources/read" ? params["uri"] : params["name"];
16
16
  return {
17
17
  headers: {
18
18
  accept: "application/json, text/event-stream",
@@ -159,5 +159,4 @@ const request = (method, params = {}, { headers = {}, url = defaultPath } = {})
159
159
  const { headers: routingHeaders, body } = statelessRequest(method, params);
160
160
  return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routingHeaders), HttpClientRequest.bodyJsonUnsafe(body));
161
161
  };
162
- //#endregion
163
162
  export { McpCallError, client, request, statelessRequest };
@@ -3,7 +3,7 @@ import { Media, isMarked, kindOf } from "../contract/media.mjs";
3
3
  import { Predicate, Schema, SchemaAST } from "effect";
4
4
  import { McpProtocol } from "effect/ai";
5
5
  import { Base64 } from "effect/encoding";
6
- //#region src/mcp/protocol.ts
6
+ // src/mcp/protocol.ts
7
7
  const httpProtocol = McpProtocol.v2026_07_28;
8
8
  const protocols = [
9
9
  McpProtocol.v2026_07_28,
@@ -133,5 +133,4 @@ const unliftedFrom = (lift, result) => {
133
133
  return field.name === void 0 || value === void 0 ? [] : [[field.name, value]];
134
134
  })]);
135
135
  };
136
- //#endregion
137
136
  export { defaultPath, fieldKey, httpProtocol, isMisplaced, liftOf, liftedFrom, protocols, unliftedFrom };
@@ -1,5 +1,5 @@
1
1
  import { Clock, Console, Effect, Function, Predicate } from "effect";
2
- //#region src/stdio/console.ts
2
+ // src/stdio/console.ts
3
3
  const stderrConsole = (console, clock) => {
4
4
  const counts = /* @__PURE__ */ new Map();
5
5
  const timers = /* @__PURE__ */ new Map();
@@ -60,5 +60,4 @@ const stderrConsole = (console, clock) => {
60
60
  };
61
61
  };
62
62
  const withStderrConsole = (effect) => Effect.provideServiceEffect(effect, Console.Console, Effect.zipWith(Effect.service(Console.Console), Effect.service(Clock.Clock), stderrConsole));
63
- //#endregion
64
63
  export { withStderrConsole };
@@ -1,6 +1,4 @@
1
1
  import { Layer } from "effect";
2
2
  import { HttpClient, HttpRouter, HttpServer } from "effect/http";
3
- //#region src/testing/memory.d.ts
4
3
  /** What in-memory routes may leave to the host: the router, the platform, nothing per request. */
5
- export type Served = HttpRouter.HttpRouter | HttpRouter.Request<"Requires", never> | HttpRouter.Request<"GlobalRequires", never> | HttpRouter.Request<"Error", any> | HttpRouter.Request<"GlobalError", any> | Layer.Success<typeof HttpServer.layerServices>;
6
- //#endregion
4
+ export type Served = HttpRouter.HttpRouter | HttpRouter.Request<"Requires", never> | HttpRouter.Request<"GlobalRequires", never> | HttpRouter.Request<"Error", any> | HttpRouter.Request<"GlobalError", any> | Layer.Success<typeof HttpServer.layerServices>;
@@ -1,6 +1,6 @@
1
1
  import { Effect, Layer, Predicate, Stream } from "effect";
2
2
  import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/http";
3
- //#region src/testing/memory.ts
3
+ // src/testing/memory.ts
4
4
  const relativeUrlOrigin = "http://localhost/";
5
5
  const withWebStreamBodyInCurrentContext = (request) => {
6
6
  const body = request.body;
@@ -22,5 +22,4 @@ const clientOf = (handler) => {
22
22
  return Effect.flatMap(withWebStreamBodyInCurrentContext(sent), (ready) => native.postprocess(Effect.succeed(ready)).pipe(Effect.provideService(FetchHttpClient.Fetch, fetch)));
23
23
  }), native.preprocess))).pipe(Layer.provide(Layer.fresh(FetchHttpClient.layer)));
24
24
  };
25
- //#endregion
26
25
  export { clientOf };
@@ -149,8 +149,12 @@ export const requestPolicy = HttpRouter.middleware(
149
149
  return HttpServerResponse.text("Host not allowed", { status: 403 });
150
150
  }
151
151
 
152
- const origin = request.headers.origin;
152
+ const origin = request.headers["origin"];
153
153
 
154
+ // The request's own origin is that of a page served from this port directly. Behind a
155
+ // proxy ending TLS it is an `http:` one from Effect 4.0.6, whose `originalUrl` takes the
156
+ // scheme of the connection, whatever `X-Forwarded-Proto` says, so the public URL's
157
+ // origin is among `origins`.
154
158
  if (origin !== undefined && origin !== url.value.origin && !origins.includes(origin)) {
155
159
  return HttpServerResponse.text("Origin not allowed", { status: 403 });
156
160
  }
package/docs/setup.md CHANGED
@@ -25,8 +25,7 @@ minor release: a minor release may change the unstable Effect modules the surfac
25
25
  Keep the tilde in `package.json`: `^4.0.4`, which npm writes for `effect@4.0.4`, admits a minor
26
26
  release outside the peer range. `npm install effect@~4.0.4` keeps it.
27
27
 
28
- Node `^22.12.0 || ^24.0.0 || >=26.0.0`: the package's `engines`, where its `devEngines` is
29
- what developing the library itself takes. TypeScript 7 or newer. ESM only. The repository checks the
28
+ Node `>=26`: the package's `engines`, the Node the library is developed and tested on. TypeScript 7 or newer. ESM only. The repository checks the
30
29
  examples with a stricter `tsconfig.json`; this is the minimum a project needs. The package's modules resolve only through its
31
30
  `exports`, which `"module": "nodenext"` reads, as a bundler project's
32
31
  `"moduleResolution": "bundler"` does. `"strict": true` is required, by Effect:
@@ -25,8 +25,12 @@ export const requestPolicy = HttpRouter.middleware(
25
25
  return HttpServerResponse.text("Host not allowed", { status: 403 });
26
26
  }
27
27
 
28
- const origin = request.headers.origin;
28
+ const origin = request.headers["origin"];
29
29
 
30
+ // The request's own origin is that of a page served from this port directly. Behind a
31
+ // proxy ending TLS it is an `http:` one from Effect 4.0.6, whose `originalUrl` takes the
32
+ // scheme of the connection, whatever `X-Forwarded-Proto` says, so the public URL's
33
+ // origin is among `origins`.
30
34
  if (origin !== undefined && origin !== url.value.origin && !origins.includes(origin)) {
31
35
  return HttpServerResponse.text("Origin not allowed", { status: 403 });
32
36
  }
@@ -1,6 +1,7 @@
1
1
  import { afterAll, describe, expect, it } from "@effect/vitest";
2
- import { Effect, Layer } from "effect";
3
- import { HttpRouter, HttpServer } from "effect/http";
2
+ import { NodeHttpServer } from "@effect/platform-node";
3
+ import { ConfigProvider, Effect, Layer } from "effect";
4
+ import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/http";
4
5
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
5
6
  import * as Testing from "@gjermundgaraba/effect-actions/Testing";
6
7
  import { Http } from "../src/Api.js";
@@ -114,6 +115,41 @@ describe("the request policy", () => {
114
115
  expect([here.status, foreignHost.status]).toEqual([200, 403]);
115
116
  });
116
117
 
118
+ // Behind a proxy ending TLS, Node's server sees `http:`: the public URL's origin is allowed by
119
+ // name, not as the request's own, so a page of it is answered whatever the proxy forwards.
120
+ it.effect("answers the public URL's page behind a proxy ending TLS", () =>
121
+ Effect.gen(function* () {
122
+ const client = yield* HttpClient.HttpClient;
123
+
124
+ const status = (origin: string) =>
125
+ client
126
+ .execute(
127
+ HttpClientRequest.post("/api/v1/getStatus").pipe(
128
+ HttpClientRequest.setHeaders({ origin, "x-forwarded-proto": "https" }),
129
+ HttpClientRequest.bodyJsonUnsafe({}),
130
+ ),
131
+ )
132
+ .pipe(Effect.map((response) => response.status));
133
+
134
+ expect([
135
+ yield* status("https://tracker.example"),
136
+ yield* status("https://evil.example"),
137
+ ]).toEqual([200, 403]);
138
+ }).pipe(
139
+ Effect.provide(
140
+ HttpRouter.serve(publicRoutes, { disableLogger: true, disableListenLog: true }).pipe(
141
+ Layer.provide(memory),
142
+ Layer.provideMerge(NodeHttpServer.layerTest),
143
+ Layer.provide(
144
+ ConfigProvider.layer(
145
+ ConfigProvider.fromEnvRecord({ PUBLIC_URL: "https://tracker.example" }),
146
+ ),
147
+ ),
148
+ ),
149
+ ),
150
+ ),
151
+ );
152
+
117
153
  // `Testing.layer(handler)` gives the typed clients the same handler the raw requests hit.
118
154
  it.effect("leaves the typed client, served from the same web handler, alone", () =>
119
155
  Effect.gen(function* () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjermundgaraba/effect-actions",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Define Effect action contracts once and project them onto HTTP, MCP, Effect RPC, native Toolkits, and CLIs.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -49,15 +49,15 @@
49
49
  "access": "public"
50
50
  },
51
51
  "devDependencies": {
52
- "@effect/platform-node": "4.0.5",
53
- "@effect/vitest": "4.0.5",
52
+ "@effect/platform-node": "4.0.6",
53
+ "@effect/vitest": "4.0.6",
54
54
  "@modelcontextprotocol/client": "2.3.1",
55
55
  "@types/node": "26.6.5",
56
- "effect": "4.0.5",
56
+ "effect": "4.0.6",
57
57
  "tsx": "4.23.15",
58
58
  "typescript": "7.0.2",
59
- "vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
60
- "vite-plus": "1.3.0",
59
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
60
+ "vite-plus": "1.4.0",
61
61
  "vitest": "5.0.5"
62
62
  },
63
63
  "peerDependencies": {
@@ -66,25 +66,21 @@
66
66
  "devEngines": {
67
67
  "packageManager": {
68
68
  "name": "pnpm",
69
- "version": "12.14.0",
69
+ "version": "12.15.0",
70
70
  "onFail": "download"
71
- },
72
- "runtime": {
73
- "name": "node",
74
- "version": "^22.18.0 || ^24.11.0 || >=26.0.0",
75
- "onFail": "error"
76
71
  }
77
72
  },
78
73
  "engines": {
79
- "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
74
+ "node": ">=26"
80
75
  },
81
76
  "scripts": {
82
77
  "build": "vp pack",
83
78
  "dev": "vp pack --watch",
84
79
  "test": "vp test",
85
80
  "check": "vp check",
86
- "example": "node --import tsx examples/notes-service/src/server.ts",
87
- "test:package": "vp run build && node --import tsx scripts/test-package.ts",
88
- "docs:sync": "node --import tsx scripts/sync-skill.ts"
81
+ "example": "vpx examples/notes-service/src/server.ts",
82
+ "test:package": "vp run build && vpx scripts/test-package.ts",
83
+ "docs:sync": "vpx scripts/sync-skill.ts",
84
+ "ready": "vp check && vp test && vp run test:package"
89
85
  }
90
86
  }