@gjermundgaraba/effect-actions 0.9.0 → 0.11.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 (70) hide show
  1. package/README.md +56 -50
  2. package/dist/Action.d.mts +286 -79
  3. package/dist/Action.mjs +125 -23
  4. package/dist/ActionCli.d.mts +102 -31
  5. package/dist/ActionCli.mjs +114 -16
  6. package/dist/ActionHttp.d.mts +170 -51
  7. package/dist/ActionHttp.mjs +130 -79
  8. package/dist/ActionMcp.d.mts +71 -21
  9. package/dist/ActionMcp.mjs +231 -80
  10. package/dist/ActionToolkit.d.mts +56 -27
  11. package/dist/ActionToolkit.mjs +16 -5
  12. package/dist/Authentication.d.mts +129 -33
  13. package/dist/Authentication.mjs +231 -38
  14. package/dist/Testing.d.mts +84 -57
  15. package/dist/Testing.mjs +121 -79
  16. package/dist/internal/actions.d.mts +5 -25
  17. package/dist/internal/actions.mjs +75 -27
  18. package/dist/internal/authentication.d.mts +102 -0
  19. package/dist/internal/authentication.mjs +11 -0
  20. package/dist/internal/call.d.mts +12 -0
  21. package/dist/internal/call.mjs +12 -0
  22. package/dist/internal/cli.d.mts +36 -22
  23. package/dist/internal/cli.mjs +311 -33
  24. package/dist/internal/client.d.mts +43 -0
  25. package/dist/internal/client.mjs +25 -0
  26. package/dist/internal/console.mjs +77 -0
  27. package/dist/internal/declared.d.mts +13 -0
  28. package/dist/internal/declared.mjs +34 -0
  29. package/dist/internal/errors.d.mts +77 -0
  30. package/dist/internal/errors.mjs +79 -0
  31. package/dist/internal/implementation.d.mts +171 -41
  32. package/dist/internal/implementation.mjs +147 -29
  33. package/dist/internal/mcp.d.mts +14 -0
  34. package/dist/internal/mcp.mjs +182 -0
  35. package/dist/internal/media.d.mts +8 -0
  36. package/dist/internal/media.mjs +26 -0
  37. package/dist/internal/memory.d.mts +6 -0
  38. package/dist/internal/memory.mjs +38 -0
  39. package/dist/internal/refusal.mjs +67 -0
  40. package/dist/internal/schema-errors.mjs +20 -0
  41. package/dist/internal/tools.mjs +27 -57
  42. package/docs/Action.md +403 -73
  43. package/docs/ActionCli.md +281 -51
  44. package/docs/ActionHttp.md +267 -82
  45. package/docs/ActionMcp.md +232 -102
  46. package/docs/ActionToolkit.md +99 -35
  47. package/docs/Authentication.md +369 -62
  48. package/docs/CONTEXT.md +24 -36
  49. package/docs/README.md +29 -73
  50. package/docs/Testing.md +154 -77
  51. package/docs/guarantees.md +82 -43
  52. package/docs/setup.md +95 -27
  53. package/package.json +12 -20
  54. package/dist/ActionCatalog.d.mts +0 -30
  55. package/dist/ActionCatalog.mjs +0 -39
  56. package/dist/ActionCliClient.d.mts +0 -48
  57. package/dist/ActionCliClient.mjs +0 -43
  58. package/dist/ActionGroup.d.mts +0 -44
  59. package/dist/ActionGroup.mjs +0 -35
  60. package/dist/ActionHttpClient.d.mts +0 -33
  61. package/dist/ActionHttpClient.mjs +0 -16
  62. package/dist/TestingClient.d.mts +0 -17
  63. package/dist/TestingClient.mjs +0 -22
  64. package/dist/internal/fetchClient.d.mts +0 -6
  65. package/dist/internal/fetchClient.mjs +0 -8
  66. package/dist/internal/tools.d.mts +0 -22
  67. package/docs/ActionCatalog.md +0 -54
  68. package/docs/ActionCliClient.md +0 -63
  69. package/docs/ActionGroup.md +0 -128
  70. package/docs/ActionHttpClient.md +0 -80
package/dist/Testing.mjs CHANGED
@@ -1,91 +1,133 @@
1
- import { fetchApiClient } from "./internal/fetchClient.mjs";
2
- import { Option, Predicate, Schema } from "effect";
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, liftOf, 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";
8
+ import { McpSchema } from "effect/ai";
9
+ import { Base64, Sse } from "effect/encoding";
3
10
  //#region src/Testing.ts
11
+ function layer(routes) {
12
+ if (!Layer.isLayer(routes)) return clientOf(routes);
13
+ return Layer.unwrap(Effect.gen(function* () {
14
+ const fileSystem = Option.getOrElse(yield* Effect.serviceOption(FileSystem.FileSystem), () => FileSystem.makeNoop({}));
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
+ const context = Context.merge(platform, yield* Effect.context());
17
+ const app = yield* HttpRouter.toHttpEffect(routes).pipe(Effect.provideContext(context));
18
+ return clientOf(HttpEffect.toWebHandlerWith(context)(app));
19
+ }));
20
+ }
4
21
  /**
5
- * The native grouped HTTP client, calling `handler` in memory instead of the network.
6
- * `baseUrl` defaults to `http://localhost`.
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.
7
25
  */
8
- const httpClient = (api, handler, options) => fetchApiClient(api, (input, init) => handler(new Request(input, init)), {
9
- baseUrl: "http://localhost",
10
- ...options
11
- });
12
- /** Build one stateless 2026-07-28 JSON-RPC request, with client metadata defaulted. */
13
- const mcpRequest = ({ url, method, params = {}, headers: init }) => {
14
- const headers = new Headers(init);
15
- headers.set("content-type", "application/json");
16
- headers.set("accept", "application/json, text/event-stream");
17
- headers.set("mcp-protocol-version", "2026-07-28");
18
- headers.set("mcp-method", method);
19
- if (Predicate.isString(params.name)) headers.set("mcp-name", params.name);
20
- return new Request(url, {
21
- method: "POST",
22
- headers,
23
- body: JSON.stringify({
24
- jsonrpc: "2.0",
25
- id: 1,
26
- method,
27
- params: {
28
- ...params,
29
- _meta: Object.assign({
30
- "io.modelcontextprotocol/clientCapabilities": {},
31
- "io.modelcontextprotocol/clientInfo": {
32
- name: "test",
33
- version: "0"
34
- }
35
- }, params._meta, { "io.modelcontextprotocol/protocolVersion": "2026-07-28" })
36
- }
37
- })
38
- });
39
- };
40
- /** The JSON-RPC response to a `tools/call`: a tool result or a protocol error. */
41
- const ToolReply = Schema.Union([Schema.Struct({ result: Schema.Struct({
42
- isError: Schema.optionalKey(Schema.Boolean),
43
- structuredContent: Schema.optionalKey(Schema.Json),
44
- content: Schema.Array(Schema.Struct({
45
- type: Schema.String,
46
- text: Schema.optionalKey(Schema.String)
47
- }))
48
- }) }), Schema.Struct({ error: Schema.Struct({
26
+ var McpCallError = class extends Schema.TaggedError()("McpCallError", { message: Schema.String }) {};
27
+ /** The text of a block, none of a block of another kind. */
28
+ 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({
49
31
  code: Schema.Finite,
50
32
  message: Schema.String
51
33
  }) })]);
52
34
  const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolReply));
53
- const parseJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
54
35
  /**
55
- * Call one tool through `handler` with a stateless 2026-07-28 request and return its
56
- * outcome. A response that is not an HTTP 200 carrying a tool result, such as an
57
- * authentication refusal or a JSON-RPC error, throws; use `mcpRequest` to inspect one.
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
+ */
39
+ const replyOf = (response, text) => {
40
+ if (!(response.headers["content-type"] ?? "").startsWith("text/event-stream")) return decodeReply(text);
41
+ const data = [];
42
+ Sse.makeParser((event) => {
43
+ if (Predicate.isTagged(event, "Event")) data.push(event.data);
44
+ }).feed(text);
45
+ return Option.fromNullishOr(data.flatMap((message) => Option.toArray(decodeReply(message))).at(-1));
46
+ };
47
+ /**
48
+ * The JSON of the media `field` a tool result's blocks hold: those of its kind naming it, or
49
+ * none, for the whole success, in order; an array, or else the one, if any. Surplus blocks for
50
+ * one value stay an array, which its decoding refuses.
51
+ */
52
+ const mediaOf = ({ name, kind, many }, result) => {
53
+ const values = result.content.flatMap((block) => block.type === kind && block._meta?.["effect-actions/field"] === name ? [{
54
+ data: Base64.encode(block.data),
55
+ mimeType: block.mimeType
56
+ }] : []);
57
+ return many || values.length > 1 ? values : values[0];
58
+ };
59
+ /**
60
+ * The encoded success of a tool result lifting the media `lift` finds: the whole success's
61
+ * blocks, or its structured content with each media field taken from its blocks alone, an
62
+ * empty one when nothing else was left.
58
63
  */
59
- const mcpCall = async (handler, { url, name, headers, arguments: args = {} }) => {
60
- const response = await handler(mcpRequest({
61
- url,
62
- method: "tools/call",
63
- params: {
64
+ const reassembled = (lift, result) => {
65
+ const [whole] = lift.fields;
66
+ if (whole !== void 0 && whole.name === void 0) return mediaOf(whole, result);
67
+ const { structuredContent = {} } = result;
68
+ if (!Predicate.isObject(structuredContent)) return structuredContent;
69
+ const lifted = new Set(lift.fields.map(({ name }) => name));
70
+ return Object.fromEntries([...Object.entries(structuredContent).filter(([key]) => !lifted.has(key)), ...lift.fields.flatMap((field) => {
71
+ const value = mediaOf(field, result);
72
+ return field.name === void 0 || value === void 0 ? [] : [[field.name, value]];
73
+ })]);
74
+ };
75
+ /** Fail with the value of one of `errors` that `text` holds, or else with `otherwise`. */
76
+ const failWith = (errors, text, otherwise) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))))(text).pipe(Effect.mapError(() => otherwise), Effect.flatMap(Effect.fail));
77
+ /**
78
+ * The tool call of `action`, whose success its tool lifts as `lift` says, with `input` on
79
+ * `client`, sending to `url`.
80
+ */
81
+ const callTool = (client, url, action, lift, input) => {
82
+ const { name } = action;
83
+ const other = (answer) => new McpCallError({ message: `MCP tools/call "${name}" ${answer}` });
84
+ return Effect.gen(function* () {
85
+ const encoded = yield* Schema.encodeEffect(Schema.toCodecJson(action.input))(input);
86
+ const response = yield* client.execute(mcpRequest("tools/call", {
64
87
  name,
65
- arguments: args
66
- },
67
- ...headers === void 0 ? {} : { headers }
68
- }));
69
- const body = await response.text();
70
- if (response.status !== 200) throw new Error(`MCP tools/call "${name}" answered ${response.status}: ${body}`);
71
- const reply = body.split("\n").map((line) => line.replace(/^data:/, "").trim()).flatMap((line) => Option.toArray(decodeReply(line))).at(-1);
72
- if (reply === void 0) throw new Error(`MCP tools/call "${name}" had no reply: ${body}`);
73
- if ("error" in reply) throw new Error(`MCP tools/call "${name}" failed with ${reply.error.code}: ${reply.error.message}`);
74
- const { result } = reply;
75
- if (result.isError === true) {
76
- const text = result.content.find((content) => content.type === "text")?.text ?? "";
77
- return {
78
- isError: true,
79
- error: Option.getOrElse(parseJson(text), () => text)
80
- };
81
- }
82
- if (result.structuredContent === void 0) throw new Error(`MCP tools/call "${name}" returned no structured content: ${body}`);
83
- const [text] = result.content.length > 1 ? result.content : [];
84
- return {
85
- isError: false,
86
- value: result.structuredContent,
87
- ...text?.text === void 0 ? {} : { text: text.text }
88
- };
88
+ arguments: encoded
89
+ }, { url }));
90
+ const text = yield* response.text;
91
+ if (response.status !== 200) return yield* failWith(Refusal.members, text, other(`answered ${response.status}: ${text}`));
92
+ const reply = replyOf(response, text);
93
+ if (Option.isNone(reply)) return yield* Effect.fail(other(`had no reply: ${text}`));
94
+ if ("error" in reply.value) {
95
+ const { code, message } = reply.value.error;
96
+ return yield* Effect.fail(other(`failed with ${code}: ${message}`));
97
+ }
98
+ const { result } = reply.value;
99
+ if (result.isError === true) {
100
+ const error = textOf(result.content.find((block) => block.type === "text")) ?? "";
101
+ return yield* failWith(projectedErrors(action), error, other(`returned an error: ${error}`));
102
+ }
103
+ if (result.structuredContent === void 0 && lift.rest !== void 0) return yield* Effect.fail(other(`returned no structured content: ${text}`));
104
+ return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(reassembled(lift, result));
105
+ });
106
+ };
107
+ function mcpClient(actions, { url = defaultPath, transformClient = identity } = {}) {
108
+ assertOnce("action", actions);
109
+ return Effect.map(HttpClient.HttpClient, (native) => {
110
+ const client = transformClient(native);
111
+ return Object.fromEntries(actions.map((action) => {
112
+ const lift = liftOf(action.success);
113
+ return [action.name, (...args) => Effect.flatMap(inputOf(action, args), (input) => callTool(client, url, action, lift, input))];
114
+ }));
115
+ });
116
+ }
117
+ /**
118
+ * One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it,
119
+ * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers, `mcp-name` from
120
+ * `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
121
+ * `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
122
+ * version is always the request's own. The test sends it: `HttpClient.execute` answers the
123
+ * response as the endpoint sent it, whatever its status, and `HttpClientRequest.toWebResult`
124
+ * gives the web `Request` a web handler or `fetch` takes, once `url` is absolute. It is for a test
125
+ * asserting on what `mcpClient` decodes away, such as `tools/list`, a refusal's challenge, or
126
+ * a call its types would not send.
127
+ */
128
+ const mcpRequest = (method, params = {}, { headers = {}, url = defaultPath } = {}) => {
129
+ const { headers: routing, body } = statelessRequest(method, params);
130
+ return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routing), HttpClientRequest.bodyJsonUnsafe(body));
89
131
  };
90
132
  //#endregion
91
- export { httpClient, mcpCall, mcpRequest };
133
+ export { McpCallError, layer, mcpClient, mcpRequest };
@@ -1,29 +1,9 @@
1
- import { Action_d_exports, Any, Codec } from "../Action.mjs";
2
- import { HttpApiError } from "effect/http-api";
1
+ import "../Action.mjs";
2
+ import { SchemaAST } from "effect";
3
3
  //#region src/internal/actions.d.ts
4
- /** One side of a schema-error policy: the error it answers with, and how to make it. */
5
- export interface SchemaErrorAnswer<E extends Codec> {
6
- /** A declared error schema; its `httpApiStatus` is the HTTP status of the answer. */
7
- readonly schema: E;
8
- readonly make: (failure: HttpApiError.HttpApiSchemaError) => NoInfer<E["Type"]>;
9
- }
10
4
  /**
11
- * Pure HTTP policy for native schema failures, split by whose fault they are. The
12
- * library owns the split, so every group draws it the same way.
5
+ * The caller of a public action: anyone, signed in or not. Registered globally, so two copies
6
+ * of the package agree on it, as they agree on a name.
13
7
  */
14
- export interface SchemaErrorPolicy<Invalid extends Codec = Codec, Internal extends Codec = Codec> {
15
- /** The request did not decode: its payload, or its params, headers or query. */
16
- readonly invalid: SchemaErrorAnswer<Invalid>;
17
- /** The handler's result did not encode: its body or its response headers. */
18
- readonly internal: SchemaErrorAnswer<Internal>;
19
- }
20
- /** The contract half of a group: what adapters and clients need, without `implement`. */
21
- export interface Actions<Name extends string = string, A extends ReadonlyArray<Any> = ReadonlyArray<Any>, PolicyError extends Codec = Codec> {
22
- readonly name: Name;
23
- readonly actions: A;
24
- /** How HTTP answers failed decoding or encoding; native behavior without one. MCP is unaffected. */
25
- readonly schemaError: SchemaErrorPolicy<PolicyError, PolicyError> | undefined;
26
- }
27
- /** The errors a group's policy may answer with, by lookup rather than a conditional type. */
28
- export type PolicyError<G extends Actions> = NonNullable<G["schemaError"]>[keyof SchemaErrorPolicy]["schema"];
8
+ export declare const Anyone: unique symbol;
29
9
  //#endregion
@@ -1,41 +1,89 @@
1
+ import { builtIns, statuses } from "./errors.mjs";
2
+ import { Predicate, Schema, SchemaAST } from "effect";
1
3
  //#region src/internal/actions.ts
2
- /** Each distinct error a policy may answer with, invalid first. */
3
- const policyErrors = (policy) => [.../* @__PURE__ */ new Set([policy.invalid.schema, policy.internal.schema])];
4
4
  /**
5
- * `HttpApiBuilder` reports these kinds while encoding the handler's answer, after the
6
- * handler ran; every other kind (`Payload`, `Params`, `Headers`, `Query`) comes from
7
- * decoding the request before it.
5
+ * The list an `error` option stands for: one schema, a list as given, or none, as
6
+ * `HttpApiEndpoint` takes it.
8
7
  */
9
- const responseKinds = /* @__PURE__ */ new Set(["Body", "ResponseHeaders"]);
10
- /** The policy's answer to one native failure: `internal` for the response side, else `invalid`. */
11
- const answerSchemaError = (policy, failure) => responseKinds.has(failure.kind) ? policy.internal.make(failure) : policy.invalid.make(failure);
8
+ const errorList = (error) => error === void 0 ? [] : Schema.isSchema(error) ? [error] : error;
12
9
  /**
13
- * An action's own failures plus the ones its surface answers with, which is what
14
- * a projection of that action declares. A schema the action already declares is
15
- * not repeated.
10
+ * What a projection of an action declares: the built-in errors, the action's own, and any
11
+ * its surface adds, such as a binding's. The built-ins come first, since a failure encodes
12
+ * and decodes with the first schema that accepts it, and they are classes, which accept
13
+ * only their own instances: a loose schema of the action's never captures one. A schema
14
+ * listed twice is not repeated.
16
15
  */
17
- const projectedErrors = (action, surface) => [.../* @__PURE__ */ new Set([...action.errors, ...surface ?? []])];
18
- const validName = /^[A-Za-z0-9_-]+$/;
16
+ const projectedErrors = (action, surface = []) => [.../* @__PURE__ */ new Set([
17
+ ...builtIns,
18
+ ...action.error,
19
+ ...surface
20
+ ])];
21
+ /**
22
+ * The caller of a public action: anyone, signed in or not. Registered globally, so two copies
23
+ * of the package agree on it, as they agree on a name.
24
+ */
25
+ const Anyone = Symbol.for("@gjermundgaraba/effect-actions/Anyone");
26
+ const validName = /^[A-Za-z0-9_-]{1,128}$/;
19
27
  /** Names become path segments, OpenAPI identifiers and client method keys. */
20
28
  const assertName = (what, name) => {
21
29
  if (!validName.test(name) || name === "then") throw new Error(`Invalid ${what}: ${name}`);
22
30
  };
23
31
  /**
24
- * The one item of `items` named `name`, narrowed to that contract. A caller passes
25
- * a literal name, so a miss is a programming error reported as `Unknown <what>`.
32
+ * Names are checked by whoever owns them: an implementation, a binding, a CLI, or the MCP
33
+ * tools. `claimantOf` says who claims a name, so a clash between two claimants names both.
26
34
  */
27
- const selectNamed = (items, name, what) => {
28
- const item = items.find((candidate) => candidate.name === name);
29
- if (item === void 0) throw new Error(`Unknown ${what}`);
30
- return item;
31
- };
32
- /** Each namespace is checked by whoever owns it: a group, the routes, or the MCP tools. */
33
- const assertDistinct = (what, names) => {
34
- const seen = /* @__PURE__ */ new Set();
35
- for (const name of names) {
36
- if (seen.has(name)) throw new Error(`Duplicate ${what}: ${name}`);
37
- seen.add(name);
35
+ const assertDistinct = (what, items, nameOf, claimantOf = nameOf) => {
36
+ const seen = /* @__PURE__ */ new Map();
37
+ for (const item of items) {
38
+ const name = nameOf(item);
39
+ const claimant = claimantOf(item);
40
+ const other = seen.get(name);
41
+ if (other !== void 0) {
42
+ const by = other === claimant ? "" : `, claimed by ${other} and ${claimant}`;
43
+ throw new Error(`Duplicate ${what}: ${name}${by}`);
44
+ }
45
+ seen.set(name, claimant);
38
46
  }
39
47
  };
48
+ /** Refuse an action name `actions` hold twice. */
49
+ const assertOnce = (what, actions) => assertDistinct(what, actions, (action) => action.name);
50
+ /**
51
+ * Refuse a key of options keyed by action name, such as handlers, tools or commands, that no
52
+ * action of `names` has, so a stale option cannot outlive its action.
53
+ */
54
+ const assertKnown = (what, keys, names) => {
55
+ const unknown = keys.filter((key) => !names.includes(key));
56
+ if (unknown.length > 0) throw new Error(`Unknown ${what}: ${unknown.join(", ")}`);
57
+ };
58
+ /** `ast` with any suspension at its top resolved, as a recursive schema's is. */
59
+ const unsuspended = (ast) => SchemaAST.isSuspend(ast) ? unsuspended(ast.thunk()) : ast;
60
+ /** The members of a union, nested and suspended ones included, or the one type otherwise. */
61
+ const members = (ast) => {
62
+ const resolved = unsuspended(ast);
63
+ return SchemaAST.isUnion(resolved) ? resolved.types.flatMap(members) : [resolved];
64
+ };
65
+ /**
66
+ * The values a literal or an enum accepts, or `undefined` for any other type, which has no
67
+ * fixed set.
68
+ */
69
+ const literalValues = (ast) => SchemaAST.isLiteral(ast) ? [ast.literal] : SchemaAST.isEnum(ast) ? ast.enums.map(([, value]) => value) : [void 0];
70
+ /**
71
+ * The `_tag`s a schema's encoding carries: each member's of a union, every value of a union of
72
+ * literals or of an enum, and a suspended schema's, as a recursive error is written.
73
+ */
74
+ const tagsOf = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member) => {
75
+ const tag = SchemaAST.isObjects(member) ? member.propertySignatures.find((property) => property.name === "_tag")?.type : void 0;
76
+ return tag === void 0 ? [] : members(tag).flatMap(literalValues).filter(Predicate.isString);
77
+ });
78
+ /**
79
+ * Refuse an error that encodes with a built-in error's `_tag`, the built-in itself included:
80
+ * every endpoint and tool declares the built-in errors already, a client decoding the answer
81
+ * could not tell a look-alike from them, and a look-alike of a refusal would step up. Checked
82
+ * where the action or the binding is made, so every surface and client of it may rely on it.
83
+ */
84
+ const assertOwnTags = (what, errors) => {
85
+ const tag = errors.flatMap((error) => tagsOf(error.ast)).find((tag) => Object.hasOwn(statuses, tag));
86
+ if (tag !== void 0) throw new Error(`${what}: error _tag "${tag}" is built in, and declared on every surface`);
87
+ };
40
88
  //#endregion
41
- export { answerSchemaError, assertDistinct, assertName, policyErrors, projectedErrors, selectNamed };
89
+ export { Anyone, assertDistinct, assertKnown, assertName, assertOnce, assertOwnTags, errorList, literalValues, members, projectedErrors, unsuspended };
@@ -0,0 +1,102 @@
1
+ import { Anyone } from "./actions.mjs";
2
+ import { Refusal } from "./errors.mjs";
3
+ import { Protected, ServedRequest, Serving } from "./implementation.mjs";
4
+ import { Action_d_exports, Any as Any$1, Errors } from "../Action.mjs";
5
+ import { Context, Effect } from "effect";
6
+ import { HttpRouter, HttpServerResponse } from "effect/http";
7
+ import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
8
+ //#region src/internal/authentication.d.ts
9
+ /** One native security scheme: Bearer unless a descriptor names another. */
10
+ export type Security = HttpApiSecurity.HttpApiSecurity;
11
+ /** What a native scheme decodes: a `Redacted` token or key, or Basic credentials. */
12
+ export type Credential = HttpApiSecurity.HttpApiSecurity.Type<Security>;
13
+ /** The provider of a descriptor's verifier: a type of its own, apart from the actor. */
14
+ export interface Provider<I, Name extends string = string> {
15
+ readonly "~effect-actions/Authentication": I;
16
+ readonly "~effect-actions/Authentication/Name": Name;
17
+ }
18
+ /**
19
+ * The native security middleware a descriptor's protected endpoints carry, which provides
20
+ * the identity: an identifier of its own, which no `Authentication.layer` provides, so only
21
+ * `ActionHttp.layer` satisfies it, from the provider.
22
+ */
23
+ export interface SecurityMiddleware<I, Name extends string = string> {
24
+ readonly "~effect-actions/Authentication/Security": Name;
25
+ readonly "~effect/http-api/HttpApiMiddleware": {
26
+ readonly provides: I;
27
+ readonly requires: never;
28
+ readonly error: never;
29
+ readonly clientError: never;
30
+ readonly requiredForClient: false;
31
+ };
32
+ }
33
+ type Response = Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, unknown>;
34
+ interface Runtime$1 {
35
+ /** The descriptor the provider was built from, the one a binding naming it must hold. */
36
+ readonly descriptor: unknown;
37
+ /** The native security middleware's implementation, keyed by its scheme. */
38
+ readonly middleware: Readonly<Record<string, (route: Response, options: {
39
+ readonly credential: Credential;
40
+ }) => Response>>;
41
+ /**
42
+ * A protected HTTP route as it leaves the layer's middleware, a step-up refusal it fails with
43
+ * answered with its challenge under Bearer; as it is under another scheme.
44
+ */
45
+ readonly stepUp: HttpApiMiddleware.HttpApiMiddleware<never, never, never>;
46
+ readonly http: <E, R>(route: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>, optional: boolean) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R | HttpRouter.Provided>;
47
+ /**
48
+ * A route of the host's own: the request is decoded here, the route gets the identity
49
+ * itself, and a refusal it fails with is answered as an action route's.
50
+ */
51
+ readonly route: <E, R>(route: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>) => Effect.Effect<HttpServerResponse.HttpServerResponse, Exclude<E, Refusal>, HttpRouter.Provided>;
52
+ }
53
+ /**
54
+ * What `Authentication.make` declares: its name, the identity it authenticates, its scheme,
55
+ * and what its verifier may fail with besides a refusal. The `~` keys are the library's own
56
+ * wiring, which no host composes.
57
+ */
58
+ export interface Descriptor<I, A, S extends Security, Name extends string = string, E extends Errors = readonly []> {
59
+ readonly name: Name;
60
+ readonly identity: Context.Key<I, A>;
61
+ readonly security: S;
62
+ /**
63
+ * What its verifier may fail with besides a refusal, as a list: every protected endpoint
64
+ * declares it, and every remote surface sends it with its status.
65
+ */
66
+ readonly error: E;
67
+ readonly "~provider": Context.Key<Provider<I, Name>, Runtime$1>;
68
+ readonly "~middleware": Context.Key<SecurityMiddleware<I, Name>, unknown>;
69
+ /** An MCP request's verified identity, which a protected tool's call promotes. */
70
+ readonly "~verified": Context.Key<never, unknown>;
71
+ }
72
+ export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
73
+ export type Identity<A extends Any$1> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
74
+ /**
75
+ * The identity a descriptor for `A` must authenticate: `unknown` for an erased action, whose
76
+ * key is known only at run time, where `assertAuthentication` checks it.
77
+ */
78
+ type Covered<A extends Any$1> = Exclude<A["caller"], typeof Anyone> extends Context.Key<infer I, unknown> ? I : never;
79
+ export type Required<A extends Any$1> = [Protected<A>] extends [never] ? {
80
+ readonly authentication?: Any;
81
+ } : {
82
+ readonly authentication: Descriptor<Covered<Protected<A>>, unknown, Security, string, Errors>;
83
+ };
84
+ export type ProviderOf<D> = D extends Descriptor<infer I, unknown, Security, infer Name, Errors> ? Provider<I, Name> : never;
85
+ /** All protected actions on a surface use the descriptor's single identity key. */
86
+ export type Matching<A extends Any$1, D> = unknown extends Covered<Protected<A>> ? unknown : [Exclude<Covered<Protected<A>>, D extends Descriptor<infer I, unknown, Security, string, Errors> ? I : never>] extends [never] ? unknown : {
87
+ readonly "Authentication descriptor does not cover every protected action": never;
88
+ };
89
+ /** Keep public requirements separate, without rescanning all handlers for every action. */
90
+ export type RemoteRequest<App, A extends Any$1> = ServedRequest<App, Extract<A, {
91
+ readonly caller: typeof Anyone;
92
+ }>> | Exclude<ServedRequest<App, Protected<A>>, Identity<Protected<A>>>;
93
+ /**
94
+ * What a call of `A` under the descriptor `D` may fail with from its verifier: the errors `D`
95
+ * declares, for a protected action; none for a public one, which nothing verifies.
96
+ */
97
+ export type VerifierError<A extends Any$1, D> = A["caller"] extends typeof Anyone ? never : D extends {
98
+ readonly error: infer E extends Errors;
99
+ } ? E[number] : never;
100
+ export type ServedProvider<App, A extends Any$1, D> = [Protected<Serving<App, A>>] extends [never] ? never : ProviderOf<D>;
101
+ //#endregion
102
+ export { Runtime$1 as Runtime };
@@ -0,0 +1,11 @@
1
+ import { Unauthenticated } from "./errors.mjs";
2
+ import { Anyone } from "./actions.mjs";
3
+ import { Effect, Option } from "effect";
4
+ //#region src/internal/authentication.ts
5
+ const assertAuthentication = (actions, auth) => {
6
+ for (const action of actions) if (action.caller !== Anyone && (auth === void 0 || action.caller.key !== auth.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor`);
7
+ };
8
+ /** Remote promotion happens at tool entry, before shared dispatch, never in local calls. */
9
+ 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()));
10
+ //#endregion
11
+ export { assertAuthentication, promote };
@@ -0,0 +1,12 @@
1
+ import "./implementation.mjs";
2
+ import { Action_d_exports, Any } from "../Action.mjs";
3
+ import { Effect, Schema } from "effect";
4
+ //#region src/internal/call.d.ts
5
+ /**
6
+ * A call of `A` answering `R`. It may leave its input out when `{}` is a valid encoded input,
7
+ * such as for an action declared without `input` or one whose every field has a decoding
8
+ * default, and then sends the input `{}` decodes to. Every client shares this rule:
9
+ * `ActionHttp.client`, `Testing.mcpClient` and `Action.client`.
10
+ */
11
+ export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
12
+ //#endregion
@@ -0,0 +1,12 @@
1
+ import { Effect, Schema } from "effect";
2
+ //#region src/internal/call.ts
3
+ /**
4
+ * The input a method of `action` sends for the arguments it was called with: a given
5
+ * argument as given, and none as the input `{}` decodes to, as a server decodes the `{}` it
6
+ * receives. `Call` allows none only when `{}` is a valid encoded input, which is what this
7
+ * decodes; decoding makes it one of the input's own values, such as an instance of an input
8
+ * class, or fills its defaults.
9
+ */
10
+ const inputOf = (action, args) => args.length === 0 ? Schema.decodeUnknownEffect(Schema.toCodecJson(action.input))({}) : Effect.succeed(args[0]);
11
+ //#endregion
12
+ export { inputOf };
@@ -1,29 +1,43 @@
1
- import "../Action.mjs";
2
- import { Effect, Schema } from "effect";
3
- import { Command } from "effect/cli";
1
+ import "./errors.mjs";
2
+ import { Action_d_exports, Any } from "../Action.mjs";
3
+ import { Effect, Runtime, Types } from "effect";
4
+ import { CliError, Command } from "effect/cli";
4
5
  //#region src/internal/cli.d.ts
5
- /** Options shared by the local and HTTP command projections. */
6
- interface CommonOptions<Output> {
7
- /** Override the command name. The action name is used by default. */
6
+ /**
7
+ * The fields of `A`'s input that may be positional: the named top-level fields of a struct
8
+ * or class input, as encoded. None for any other input, a record or a union included.
9
+ */
10
+ export type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
11
+ /** How one command is named, takes its input and prints its result. */
12
+ export interface Options<A extends Any> {
13
+ /** Override the command name. The action name, in kebab case, is used by default. */
8
14
  readonly name?: string;
9
15
  /** Human output. Adds a `--json` flag to the command that selects JSON instead. */
10
- readonly render?: (output: Output) => string;
11
- }
12
- /** The default projection accepts a whole canonical JSON input with `--input`. */
13
- export interface JsonOptions<Output> extends CommonOptions<Output> {
14
- readonly parameters?: never;
15
- readonly input?: never;
16
- }
17
- /** Supply native CLI parameters and map their parsed values to canonical action JSON. */
18
- export interface ParametersOptions<Output, Parameters extends Command.Command.Config> extends CommonOptions<Output> {
19
- /** Native Effect CLI flags and arguments. */
20
- readonly parameters: Parameters;
21
- /** Maps native parsed parameters to the action's canonical JSON input. */
22
- readonly input: (parsed: Command.Command.Config.InferValue<Parameters>) => Schema.Json;
16
+ readonly render?: (output: A["success"]["Type"]) => string;
17
+ /**
18
+ * Input fields taken as positional arguments instead of flags, in this order. An
19
+ * optional field is an optional argument, so it follows every required one.
20
+ */
21
+ readonly positional?: ReadonlyArray<Field<A>>;
22
+ /**
23
+ * A short name per input field's flag, such as `{ limit: "n" }` for `-n`, given as
24
+ * `Param.withAlias` takes it.
25
+ */
26
+ readonly aliases?: { readonly [K in Field<A>]?: string; };
23
27
  }
24
28
  /**
25
- * A command either accepts whole JSON input or has explicit native CLI parameters.
26
- * The latter intentionally has no implicit `--input` mode.
29
+ * What a command fails with when its action fails: Effect CLI's own error for a handler's
30
+ * failure, which `Command.run` prints on stderr and marks reported, so `runMain` does not
31
+ * print it again. Its `cause` is the action's failure. The process exits with the cause's
32
+ * `Runtime.errorExitCode`, 1 by default.
27
33
  */
28
- export type Options<Output, Parameters extends Command.Command.Config = never> = JsonOptions<Output> | ParametersOptions<Output, Parameters>;
34
+ export declare class UserError<out E = unknown> extends CliError.UserError {
35
+ readonly cause: E;
36
+ /**
37
+ * The action's failure, as Effect names an error's nested one, so the native
38
+ * `Effect.catchReason("UserError", "UserNotFound", f)` and `catchReasons` match it by tag.
39
+ */
40
+ get reason(): E;
41
+ get [Runtime.errorExitCode](): number;
42
+ }
29
43
  //#endregion