@gjermundgaraba/effect-actions 0.14.0 → 0.16.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.
- package/dist/Action.d.mts +1 -1
- package/dist/ActionCli.d.mts +4 -6
- package/dist/ActionCli.mjs +11 -9
- package/dist/ActionHttp.mjs +18 -30
- package/dist/ActionMcp.d.mts +23 -7
- package/dist/ActionMcp.mjs +78 -90
- package/dist/ActionRpc.mjs +17 -29
- package/dist/ActionToolkit.mjs +26 -11
- package/dist/Authentication.mjs +1 -2
- package/dist/Testing.d.mts +6 -73
- package/dist/Testing.mjs +3 -102
- package/dist/authentication/provider.mjs +1 -6
- package/dist/authentication/refusal.mjs +14 -4
- package/dist/cli/commands.mjs +8 -8
- package/dist/contract/call.d.mts +2 -1
- package/dist/contract/call.mjs +14 -1
- package/dist/contract/errors.d.mts +2 -3
- package/dist/contract/errors.mjs +6 -12
- package/dist/contract/implementation.mjs +33 -5
- package/dist/contract/rules.d.mts +7 -0
- package/dist/contract/rules.mjs +50 -3
- package/dist/http/client.mjs +4 -7
- package/dist/mcp/client.d.mts +87 -0
- package/dist/mcp/client.mjs +122 -0
- package/dist/mcp/protocol.mjs +25 -37
- package/docs/Action.md +6 -5
- package/docs/ActionCli.md +4 -3
- package/docs/ActionMcp-failures.md +7 -2
- package/docs/ActionMcp.md +43 -46
- package/docs/ActionToolkit-failures.md +2 -1
- package/docs/ActionToolkit.md +2 -2
- package/docs/Authentication-failures.md +2 -2
- package/docs/Authentication.md +10 -5
- package/docs/CONTEXT.md +1 -1
- package/docs/Testing-failures.md +3 -3
- package/docs/Testing.md +16 -24
- package/docs/guarantees.md +15 -14
- package/examples/kitchen-sink/README.md +26 -26
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +3 -2
- package/examples/kitchen-sink/test/identity/identity.test.ts +13 -6
- package/examples/kitchen-sink/test/routes.test.ts +4 -3
- package/examples/live-rpc/README.md +7 -2
- package/examples/mcp-tools/test/routes.test.ts +7 -6
- package/examples/notes-service/README.md +13 -3
- package/examples/notes-service/src/notes/Notes.ts +4 -2
- package/examples/notes-service/src/routes.ts +24 -14
- package/examples/notes-service/src/worker.ts +20 -0
- package/examples/notes-service/test/commands.test.ts +2 -1
- package/examples/notes-service/test/routes.test.ts +6 -3
- package/examples/notes-service/test/worker.test.ts +34 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +2 -1
- package/package.json +1 -1
- package/dist/http/declared.mjs +0 -20
- package/dist/mcp/protocol.d.mts +0 -18
- package/dist/toolkit/tools.mjs +0 -29
package/dist/Testing.d.mts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import { BuiltIn } from "./contract/errors.mjs";
|
|
2
|
-
import { Call } from "./contract/call.mjs";
|
|
3
|
-
import { Action_d_exports, Any } from "./Action.mjs";
|
|
4
|
-
import { Params } from "./mcp/protocol.mjs";
|
|
5
1
|
import { Served } from "./testing/memory.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
import { HttpClient, HttpRouter } from "effect/http";
|
|
8
4
|
//#region src/testing/Testing.d.ts
|
|
9
5
|
/**
|
|
10
6
|
* For a web handler the test already owns; most tests want `layer(routes)`, below, with
|
|
11
|
-
* `ActionHttp.client` and `
|
|
12
|
-
* the network: a web handler the test serves, which other tests may share, such as
|
|
7
|
+
* `ActionHttp.client` and `ActionMcp.client`. The native `HttpClient`, answered by `handler`
|
|
8
|
+
* instead of the network: a web handler the test serves, which other tests may share, such as
|
|
13
9
|
* `HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices))).handler`,
|
|
14
10
|
* the routes' platform services provided, as `layer(routes)` provides them itself. It is the
|
|
15
11
|
* client `layer(routes)` gives, without building anything: the test owns the handler, and
|
|
@@ -18,7 +14,7 @@ import { Headers, HttpClient, HttpClientError, HttpClientRequest, HttpRouter } f
|
|
|
18
14
|
export declare function layer(handler: (request: Request) => Promise<Response>): Layer.Layer<HttpClient.HttpClient>;
|
|
19
15
|
/**
|
|
20
16
|
* The native `HttpClient`, answered in memory by `routes` instead of the network: provide
|
|
21
|
-
* it to `ActionHttp.client` and to `
|
|
17
|
+
* it to `ActionHttp.client` and to `ActionMcp.client`. The routes are built with this layer and
|
|
22
18
|
* released with its scope, without request logs: only the cause of a defect, of a result that
|
|
23
19
|
* does not encode or of a failure a route of your own leaves unhandled is logged, as an error,
|
|
24
20
|
* where a server's request logger would show it, on the process console even under `it.effect`
|
|
@@ -36,67 +32,4 @@ export declare function layer(handler: (request: Request) => Promise<Response>):
|
|
|
36
32
|
* A type error at `it.effect`, such as one naming `Scope`: `docs/Testing-failures.md`.
|
|
37
33
|
*/
|
|
38
34
|
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>>;
|
|
39
|
-
|
|
40
|
-
export interface McpClientOptions {
|
|
41
|
-
/**
|
|
42
|
-
* The endpoint, resolved by the `HttpClient`: relative under `layer`. Defaults to `/mcp`,
|
|
43
|
-
* the default `ActionMcp.layerHttp` path.
|
|
44
|
-
*/
|
|
45
|
-
readonly url?: string;
|
|
46
|
-
/**
|
|
47
|
-
* Wraps the native `HttpClient`, as `ActionHttp.client` takes it: a bearer token is
|
|
48
|
-
* `HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`.
|
|
49
|
-
*/
|
|
50
|
-
readonly transformClient?: (client: HttpClient.HttpClient) => HttpClient.HttpClient;
|
|
51
|
-
}
|
|
52
|
-
/** Where an `mcpRequest` goes, and with what headers. */
|
|
53
|
-
export interface McpRequestOptions {
|
|
54
|
-
/**
|
|
55
|
-
* The endpoint, resolved by the `HttpClient` that sends the request: relative under `layer`.
|
|
56
|
-
* Defaults to `/mcp`, as `mcpClient`'s does; converting to a web `Request` needs an absolute
|
|
57
|
-
* one.
|
|
58
|
-
*/
|
|
59
|
-
readonly url?: string;
|
|
60
|
-
readonly headers?: Headers.Input;
|
|
61
|
-
}
|
|
62
|
-
declare const McpCallError_base: Schema.Class<McpCallError, Schema.TaggedStruct<"McpCallError", {
|
|
63
|
-
readonly message: Schema.String;
|
|
64
|
-
}>, Cause.YieldableError>;
|
|
65
|
-
/**
|
|
66
|
-
* An answer a client method cannot read as the action's success or a declared error, such as
|
|
67
|
-
* the native server's message for invalid arguments or an unknown tool, or a result without
|
|
68
|
-
* its structured content. Its message holds the answer. Structured content that does not
|
|
69
|
-
* decode as the success is a `SchemaError` instead, and a request that fails an
|
|
70
|
-
* `HttpClientError`.
|
|
71
|
-
*/
|
|
72
|
-
export declare class McpCallError extends McpCallError_base {}
|
|
73
|
-
type CallError<A extends Any> = A["error"][number]["Type"] | BuiltIn | Schema.SchemaError | HttpClientError.HttpClientError | McpCallError;
|
|
74
|
-
/** Every action of `Actions` as `client.<action>(input)`, calling its tool. */
|
|
75
|
-
export type McpClient<Actions extends ReadonlyArray<Any>> = { readonly [A in Actions[number] as A["name"]]: Call<A, Effect.Effect<A["success"]["Type"], CallError<A>>>; };
|
|
76
|
-
/**
|
|
77
|
-
* A client of an MCP endpoint served by `ActionMcp.layerHttp`, one method per action calling
|
|
78
|
-
* its tool with one stateless request, as `ActionHttp.client` calls routes: the input is
|
|
79
|
-
* encoded with the action's schema, and the success decoded from its structured content, each
|
|
80
|
-
* media field from the blocks whose `_meta` names it, and a success that is media, or whose
|
|
81
|
-
* every field is, from its blocks alone. A declared error the tool returns, the action's own
|
|
82
|
-
* or a refusal, is its decoded value, and so is a refusal the endpoint's authentication
|
|
83
|
-
* answers with: a test telling a 401 or 403 from a tool result asserts the status of a request
|
|
84
|
-
* `mcpRequest` builds. The argument may be omitted when `{}` is a valid input. Requires the
|
|
85
|
-
* native `HttpClient`, such as the one `layer` provides.
|
|
86
|
-
*/
|
|
87
|
-
export declare function mcpClient<const Actions extends ReadonlyArray<Any>>(actions: Actions, options?: McpClientOptions): Effect.Effect<McpClient<Actions>, never, HttpClient.HttpClient>;
|
|
88
|
-
/**
|
|
89
|
-
* One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it,
|
|
90
|
-
* as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
|
|
91
|
-
* `MCP-Protocol-Version`, `MCP-Method`), `mcp-name` from
|
|
92
|
-
* `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
|
|
93
|
-
* `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
|
|
94
|
-
* version is always the request's own. The test sends it: `HttpClient.execute` answers the
|
|
95
|
-
* response as the endpoint sent it, whatever its status, and `HttpClientRequest.toWebResult`
|
|
96
|
-
* gives the web `Request` a web handler or `fetch` takes, once `url` is absolute. It is for a test
|
|
97
|
-
* asserting on what `mcpClient` decodes away, such as `tools/list`, a refusal's challenge, or
|
|
98
|
-
* a call its types would not send.
|
|
99
|
-
*/
|
|
100
|
-
export declare const mcpRequest: (method: string, params?: Params, { headers, url }?: McpRequestOptions) => HttpClientRequest.HttpClientRequest;
|
|
101
|
-
//#endregion
|
|
102
|
-
export type { Params as McpParams };
|
|
35
|
+
//#endregion
|
package/dist/Testing.mjs
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import { Refusal } from "./contract/errors.mjs";
|
|
2
|
-
import { assertOnce, projectedErrors } from "./contract/rules.mjs";
|
|
3
|
-
import { checked } from "./contract/call.mjs";
|
|
4
|
-
import { defaultPath, liftOf, statelessRequest } from "./mcp/protocol.mjs";
|
|
5
1
|
import { clientOf } from "./testing/memory.mjs";
|
|
6
|
-
import { Cause, Console, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path
|
|
7
|
-
import {
|
|
8
|
-
import { Etag, HttpClient, HttpClientRequest, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
|
|
9
|
-
import { Base64, Sse } from "effect/encoding";
|
|
2
|
+
import { Cause, Console, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path } from "effect";
|
|
3
|
+
import { Etag, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
|
|
10
4
|
//#region src/testing/Testing.ts
|
|
11
5
|
function layer(routes) {
|
|
12
6
|
if (!Layer.isLayer(routes)) return clientOf(routes);
|
|
@@ -19,98 +13,5 @@ function layer(routes) {
|
|
|
19
13
|
}));
|
|
20
14
|
}
|
|
21
15
|
const isReported = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) && !ErrorReporter.isIgnored(reason.error) || Cause.isDieReason(reason) && !ErrorReporter.isIgnored(reason.defect));
|
|
22
|
-
/**
|
|
23
|
-
* An answer a client method cannot read as the action's success or a declared error, such as
|
|
24
|
-
* the native server's message for invalid arguments or an unknown tool, or a result without
|
|
25
|
-
* its structured content. Its message holds the answer. Structured content that does not
|
|
26
|
-
* decode as the success is a `SchemaError` instead, and a request that fails an
|
|
27
|
-
* `HttpClientError`.
|
|
28
|
-
*/
|
|
29
|
-
var McpCallError = class extends Schema.TaggedError()("McpCallError", { message: Schema.String }) {};
|
|
30
|
-
const textOf = (block) => block?.type === "text" ? block.text : void 0;
|
|
31
|
-
const ToolCallReply = Schema.Union([Schema.Struct({ result: McpSchema.CallToolResult }), Schema.Struct({ error: Schema.Struct({
|
|
32
|
-
code: Schema.Finite,
|
|
33
|
-
message: Schema.String
|
|
34
|
-
}) })]);
|
|
35
|
-
const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolCallReply));
|
|
36
|
-
const replyOf = (response, text) => {
|
|
37
|
-
if (!(response.headers["content-type"] ?? "").startsWith("text/event-stream")) return decodeReply(text);
|
|
38
|
-
const data = [];
|
|
39
|
-
Sse.makeParser((event) => {
|
|
40
|
-
if (Predicate.isTagged(event, "Event")) data.push(event.data);
|
|
41
|
-
}).feed(text);
|
|
42
|
-
return Option.fromNullishOr(data.flatMap((message) => Option.toArray(decodeReply(message))).at(-1));
|
|
43
|
-
};
|
|
44
|
-
const mediaFromBlocks = ({ name, kind, many }, result) => {
|
|
45
|
-
const values = result.content.flatMap((block) => block.type === kind && block._meta?.["effect-actions/field"] === name ? [{
|
|
46
|
-
data: Base64.encode(block.data),
|
|
47
|
-
mimeType: block.mimeType
|
|
48
|
-
}] : []);
|
|
49
|
-
return many || values.length > 1 ? values : values[0];
|
|
50
|
-
};
|
|
51
|
-
const encodedSuccessOf = (lift, result) => {
|
|
52
|
-
const [whole] = lift.fields;
|
|
53
|
-
if (whole !== void 0 && whole.name === void 0) return mediaFromBlocks(whole, result);
|
|
54
|
-
const { structuredContent = {} } = result;
|
|
55
|
-
if (!Predicate.isObject(structuredContent)) return structuredContent;
|
|
56
|
-
const lifted = new Set(lift.fields.map(({ name }) => name));
|
|
57
|
-
return Object.fromEntries([...Object.entries(structuredContent).filter(([key]) => !lifted.has(key)), ...lift.fields.flatMap((field) => {
|
|
58
|
-
const value = mediaFromBlocks(field, result);
|
|
59
|
-
return field.name === void 0 || value === void 0 ? [] : [[field.name, value]];
|
|
60
|
-
})]);
|
|
61
|
-
};
|
|
62
|
-
const failWith = (errors, text, otherwise) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))))(text).pipe(Effect.mapError(() => otherwise), Effect.flatMap(Effect.fail));
|
|
63
|
-
const callTool = (client, url, action, lift, input) => {
|
|
64
|
-
const { name } = action;
|
|
65
|
-
const other = (answer) => new McpCallError({ message: `MCP tools/call "${name}" ${answer}` });
|
|
66
|
-
return Effect.gen(function* () {
|
|
67
|
-
const encoded = yield* Schema.encodeEffect(Schema.toCodecJson(action.input))(input);
|
|
68
|
-
const response = yield* client.execute(mcpRequest("tools/call", {
|
|
69
|
-
name,
|
|
70
|
-
arguments: encoded
|
|
71
|
-
}, { url }));
|
|
72
|
-
const text = yield* response.text;
|
|
73
|
-
if (response.status !== 200) return yield* failWith(Refusal.members, text, other(`answered ${response.status}: ${text}`));
|
|
74
|
-
const reply = replyOf(response, text);
|
|
75
|
-
if (Option.isNone(reply)) return yield* Effect.fail(other(`had no reply: ${text}`));
|
|
76
|
-
if ("error" in reply.value) {
|
|
77
|
-
const { code, message } = reply.value.error;
|
|
78
|
-
return yield* Effect.fail(other(`failed with ${code}: ${message}`));
|
|
79
|
-
}
|
|
80
|
-
const { result } = reply.value;
|
|
81
|
-
if (result.isError === true) {
|
|
82
|
-
const error = textOf(result.content.find((block) => block.type === "text")) ?? "";
|
|
83
|
-
return yield* failWith(projectedErrors(action), error, other(`returned an error: ${error}`));
|
|
84
|
-
}
|
|
85
|
-
if (result.structuredContent === void 0 && lift.rest !== void 0) return yield* Effect.fail(other(`returned no structured content: ${text}`));
|
|
86
|
-
return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(encodedSuccessOf(lift, result));
|
|
87
|
-
});
|
|
88
|
-
};
|
|
89
|
-
function mcpClient(actions, { url = defaultPath, transformClient = identity } = {}) {
|
|
90
|
-
assertOnce("action", actions);
|
|
91
|
-
return Effect.map(HttpClient.HttpClient, (native) => {
|
|
92
|
-
const client = transformClient(native);
|
|
93
|
-
return Object.fromEntries(actions.map((action) => {
|
|
94
|
-
const lift = liftOf(action.success);
|
|
95
|
-
return [action.name, checked(action, (input) => callTool(client, url, action, lift, input))];
|
|
96
|
-
}));
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it,
|
|
101
|
-
* as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
|
|
102
|
-
* `MCP-Protocol-Version`, `MCP-Method`), `mcp-name` from
|
|
103
|
-
* `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
|
|
104
|
-
* `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
|
|
105
|
-
* version is always the request's own. The test sends it: `HttpClient.execute` answers the
|
|
106
|
-
* response as the endpoint sent it, whatever its status, and `HttpClientRequest.toWebResult`
|
|
107
|
-
* gives the web `Request` a web handler or `fetch` takes, once `url` is absolute. It is for a test
|
|
108
|
-
* asserting on what `mcpClient` decodes away, such as `tools/list`, a refusal's challenge, or
|
|
109
|
-
* a call its types would not send.
|
|
110
|
-
*/
|
|
111
|
-
const mcpRequest = (method, params = {}, { headers = {}, url = defaultPath } = {}) => {
|
|
112
|
-
const { headers: routingHeaders, body } = statelessRequest(method, params);
|
|
113
|
-
return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routingHeaders), HttpClientRequest.bodyJsonUnsafe(body));
|
|
114
|
-
};
|
|
115
16
|
//#endregion
|
|
116
|
-
export {
|
|
17
|
+
export { layer };
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { Unauthenticated } from "../contract/errors.mjs";
|
|
2
|
-
import { Anyone } from "../contract/rules.mjs";
|
|
3
2
|
import { Effect, Option } from "effect";
|
|
4
3
|
//#region src/authentication/provider.ts
|
|
5
|
-
const withVerifierErrors = (action, errors, verifierErrors) => action.caller === Anyone || verifierErrors === void 0 ? errors : [...errors, ...verifierErrors];
|
|
6
|
-
const assertAuthentication = (actions, auth) => {
|
|
7
|
-
for (const action of actions) if (action.caller !== Anyone && (auth === void 0 || action.caller.key !== auth.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor`);
|
|
8
|
-
};
|
|
9
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 binding's descriptor is not its provider's; build both from one descriptor`)));
|
|
10
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()));
|
|
11
6
|
//#endregion
|
|
12
|
-
export {
|
|
7
|
+
export { promote, providerOf };
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import { Refusal,
|
|
1
|
+
import { Forbidden, Refusal, Unauthenticated } from "../contract/errors.mjs";
|
|
2
|
+
import { declared, httpApiStatusOf } from "../contract/rules.mjs";
|
|
2
3
|
import { Cause, Context, Effect, Exit, Option, Predicate, Ref, Schema } from "effect";
|
|
3
4
|
import { HttpServerResponse } from "effect/http";
|
|
4
5
|
//#region src/authentication/refusal.ts
|
|
6
|
+
const responseOf = (schema, error) => HttpServerResponse.jsonUnsafe(Schema.encodeUnknownSync(Schema.toCodecJson(schema))(error), { status: httpApiStatusOf(schema.ast) ?? 422 });
|
|
7
|
+
/** `error` answered as an endpoint declaring `errors` answers it, if one of them declares it. */
|
|
8
|
+
const declaredResponse = (errors, error) => {
|
|
9
|
+
const schema = errors.flatMap(declared).find((each) => Schema.is(each)(error));
|
|
10
|
+
return schema === void 0 ? void 0 : responseOf(schema, error);
|
|
11
|
+
};
|
|
5
12
|
const rfc6750ErrorDescriptionCharacters = /^[\x20\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
6
13
|
const challenge = (scheme, parameters) => {
|
|
7
14
|
const given = parameters.flatMap(([name, value]) => value === void 0 ? [] : [`${name}="${value.replace(/["\\]/g, "\\$&")}"`]);
|
|
8
15
|
return given.length === 0 ? scheme : `${scheme} ${given.join(", ")}`;
|
|
9
16
|
};
|
|
10
17
|
const bearer = (parameters) => challenge("Bearer", parameters);
|
|
11
|
-
const encode = Schema.encodeSync(Refusal);
|
|
12
18
|
const isRefusal = Schema.is(Refusal);
|
|
13
19
|
const insufficientScopeChallenge = (error, metadataUrl) => Predicate.isTagged(error, "Forbidden") && error.scopes !== void 0 ? bearer([
|
|
14
20
|
["error", "insufficient_scope"],
|
|
@@ -16,7 +22,11 @@ const insufficientScopeChallenge = (error, metadataUrl) => Predicate.isTagged(er
|
|
|
16
22
|
["resource_metadata", metadataUrl],
|
|
17
23
|
["error_description", rfc6750ErrorDescriptionCharacters.test(error.message) ? error.message : void 0]
|
|
18
24
|
]) : void 0;
|
|
19
|
-
const
|
|
25
|
+
const refusalSchemas = {
|
|
26
|
+
Unauthenticated,
|
|
27
|
+
Forbidden
|
|
28
|
+
};
|
|
29
|
+
const plain = (error) => responseOf(refusalSchemas[error._tag], error);
|
|
20
30
|
const answer = (error, metadataUrl) => {
|
|
21
31
|
const response = plain(error);
|
|
22
32
|
const scoped = insufficientScopeChallenge(error, metadataUrl);
|
|
@@ -33,4 +43,4 @@ const answerStepUp = (route, metadataUrl) => Effect.gen(function* () {
|
|
|
33
43
|
return Option.isSome(refused) && !diedOrInterruptedAfterRefusal ? answer(refused.value, metadataUrl) : yield* exit;
|
|
34
44
|
});
|
|
35
45
|
//#endregion
|
|
36
|
-
export { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain, recordStepUp };
|
|
46
|
+
export { answer, answerStepUp, bearer, challenge, declaredResponse, isRefusal, isStepUp, plain, recordStepUp };
|
package/dist/cli/commands.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { InvalidInput } from "../contract/errors.mjs";
|
|
2
|
-
import { assertDistinct, literalValues, members,
|
|
2
|
+
import { assertDistinct, literalValues, members, unsuspended } from "../contract/rules.mjs";
|
|
3
|
+
import { decodeInput } from "../contract/call.mjs";
|
|
3
4
|
import { withStderrConsole } from "../stdio/console.mjs";
|
|
4
|
-
import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST, Stdio, Stream
|
|
5
|
+
import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST, Stdio, Stream } from "effect";
|
|
5
6
|
import { CliError, Command, Flag, Param } from "effect/cli";
|
|
6
7
|
//#region src/cli/commands.ts
|
|
7
8
|
const kebab = (name) => {
|
|
@@ -204,8 +205,8 @@ const userErrorOf = (errors) => {
|
|
|
204
205
|
userMessage
|
|
205
206
|
}))));
|
|
206
207
|
};
|
|
207
|
-
const failingAsDerived = (actions,
|
|
208
|
-
const failure = userErrorOf([...new Set(actions.flatMap(
|
|
208
|
+
const failingAsDerived = (actions, errorsOf) => {
|
|
209
|
+
const failure = userErrorOf([...new Set(actions.flatMap(errorsOf))]);
|
|
209
210
|
return (self) => Effect.catchIf(self, (error) => !CliError.isCliError(error), failure);
|
|
210
211
|
};
|
|
211
212
|
const unreadableStdinField = (field, message) => new InvalidInput({
|
|
@@ -228,8 +229,7 @@ const stdinTextOf = (field) => Effect.gen(function* () {
|
|
|
228
229
|
const inputConfig = (action, positional, aliases, stdin) => {
|
|
229
230
|
const codec = Schema.toCodecJson(action.input);
|
|
230
231
|
const encoded = unsuspended(SchemaAST.toEncoded(codec.ast));
|
|
231
|
-
const
|
|
232
|
-
const decode = flow(decodeRefusingUndeclared, Effect.mapError(InvalidInput.fromSchemaError));
|
|
232
|
+
const decode = decodeInput(action);
|
|
233
233
|
if (SchemaAST.isObjects(encoded) && encoded.indexSignatures.every((signature) => SchemaAST.isNever(signature.type))) {
|
|
234
234
|
const params = fieldParams(encoded, unsuspended(action.input.ast), positional, aliases);
|
|
235
235
|
const fromStdin = Option.map(Option.fromUndefinedOr(stdin), (field) => {
|
|
@@ -269,11 +269,11 @@ const inputConfig = (action, positional, aliases, stdin) => {
|
|
|
269
269
|
stdinHelp: void 0
|
|
270
270
|
};
|
|
271
271
|
};
|
|
272
|
-
const command = (action, execute, options, errors
|
|
272
|
+
const command = (action, execute, options, errors) => {
|
|
273
273
|
const name = options?.name ?? kebab(action.name);
|
|
274
274
|
const render = options?.render;
|
|
275
275
|
const { flags, positional, decode, stdinHelp } = inputConfig(action, options?.positional ?? [], options?.aliases ?? {}, options?.stdin);
|
|
276
|
-
const failure = userErrorOf(
|
|
276
|
+
const failure = userErrorOf(errors);
|
|
277
277
|
assertDistinct("flag", [
|
|
278
278
|
...Object.keys(flags).map((field) => [`--${kebab(field)}`, `field ${field}`]),
|
|
279
279
|
...render === void 0 ? [] : [["--json", "render's --json"]],
|
package/dist/contract/call.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "./errors.mjs";
|
|
1
2
|
import "./implementation.mjs";
|
|
2
3
|
import { Action_d_exports, Any } from "../Action.mjs";
|
|
3
4
|
import { Effect, Schema } from "effect";
|
|
@@ -6,7 +7,7 @@ import { Effect, Schema } from "effect";
|
|
|
6
7
|
* A call of `A` answering `R`. It may leave its input out when `{}` is a valid encoded input,
|
|
7
8
|
* such as for an action declared without `input` or one whose every field has a decoding
|
|
8
9
|
* default, and then sends the input `{}` decodes to. Every client shares this rule:
|
|
9
|
-
* `ActionHttp.client`, `ActionRpc.client`, `
|
|
10
|
+
* `ActionHttp.client`, `ActionRpc.client`, `ActionMcp.client` and `Action.client`.
|
|
10
11
|
*/
|
|
11
12
|
export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
|
|
12
13
|
//#endregion
|
package/dist/contract/call.mjs
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { InvalidInput } from "./errors.mjs";
|
|
2
2
|
import { Effect, Schema } from "effect";
|
|
3
3
|
//#region src/contract/call.ts
|
|
4
|
+
/**
|
|
5
|
+
* How HTTP, RPC, MCP and a command decode a request's input: reporting every issue, and refusing a
|
|
6
|
+
* field the input does not declare.
|
|
7
|
+
*/
|
|
8
|
+
const inputParseOptions = {
|
|
9
|
+
errors: "all",
|
|
10
|
+
onExcessProperty: "error"
|
|
11
|
+
};
|
|
12
|
+
/** Decode `action`'s input from its JSON form by `inputParseOptions`, or fail with `InvalidInput`. */
|
|
13
|
+
const decodeInput = (action) => {
|
|
14
|
+
const decode = Schema.decodeUnknownEffect(Schema.toCodecJson(action.input), inputParseOptions);
|
|
15
|
+
return (encoded) => Effect.mapError(decode(encoded), InvalidInput.fromSchemaError);
|
|
16
|
+
};
|
|
4
17
|
const inputOf = (action, args) => args.length === 0 ? Schema.decodeUnknownEffect(Schema.toCodecJson(action.input))({}) : Effect.succeed(args[0]);
|
|
5
18
|
const checked = (action, send) => {
|
|
6
19
|
const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(action.input), { errors: "all" });
|
|
7
20
|
return (...args) => inputOf(action, args).pipe(Effect.tap(encode), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap(send));
|
|
8
21
|
};
|
|
9
22
|
//#endregion
|
|
10
|
-
export { checked, inputOf };
|
|
23
|
+
export { checked, decodeInput, inputOf, inputParseOptions };
|
|
@@ -50,9 +50,8 @@ declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbi
|
|
|
50
50
|
* The caller may not run this action: an implementation's `authorize` refuses. `scopes`,
|
|
51
51
|
* when given, are every OAuth scope the call needs: a refusal naming them is answered with
|
|
52
52
|
* the `insufficient_scope` challenge an OAuth client re-authorizes on, so name them only where
|
|
53
|
-
* re-authorizing can grant them.
|
|
54
|
-
*
|
|
55
|
-
* otherwise a tool result with `isError: true`, in a 200 response.
|
|
53
|
+
* re-authorizing can grant them. What it answers on each surface: `docs/guarantees.md` in this
|
|
54
|
+
* package.
|
|
56
55
|
*/
|
|
57
56
|
export declare class Forbidden extends Forbidden_base {
|
|
58
57
|
private readonly "~declared in the action's error, or built in";
|
package/dist/contract/errors.mjs
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { Effect, Predicate, Schema, SchemaIssue } from "effect";
|
|
2
2
|
//#region src/contract/errors.ts
|
|
3
3
|
const defaultedMessage = (fallback) => Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(fallback)));
|
|
4
|
-
const statuses = {
|
|
5
|
-
InvalidInput: 400,
|
|
6
|
-
Unauthenticated: 401,
|
|
7
|
-
Forbidden: 403
|
|
8
|
-
};
|
|
9
4
|
const Issue = Schema.Struct({
|
|
10
5
|
path: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
|
|
11
6
|
message: Schema.String
|
|
@@ -20,7 +15,7 @@ const standardIssues = SchemaIssue.makeFormatterStandardSchemaV1();
|
|
|
20
15
|
var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput", {
|
|
21
16
|
message: defaultedMessage("The input does not match the action's input."),
|
|
22
17
|
issues: Schema.Array(Issue).pipe(Schema.withConstructorDefault(Effect.succeed([])))
|
|
23
|
-
}, { httpApiStatus:
|
|
18
|
+
}, { httpApiStatus: 400 }) {
|
|
24
19
|
/**
|
|
25
20
|
* The `InvalidInput` for input that did not decode, as every surface answers it: the
|
|
26
21
|
* schema's message, and each of its issues by path, for code that decodes input of its own,
|
|
@@ -41,21 +36,20 @@ var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput
|
|
|
41
36
|
});
|
|
42
37
|
};
|
|
43
38
|
/** The caller is not authenticated: authentication or an implementation's `authorize` refuses. */
|
|
44
|
-
var Unauthenticated = class extends Schema.TaggedError()("Unauthenticated", { message: defaultedMessage("Authentication is required.") }, { httpApiStatus:
|
|
39
|
+
var Unauthenticated = class extends Schema.TaggedError()("Unauthenticated", { message: defaultedMessage("Authentication is required.") }, { httpApiStatus: 401 }) {};
|
|
45
40
|
const scopeToken = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
46
41
|
const ScopeToken = Schema.String.check(Schema.isPattern(scopeToken));
|
|
47
42
|
/**
|
|
48
43
|
* The caller may not run this action: an implementation's `authorize` refuses. `scopes`,
|
|
49
44
|
* when given, are every OAuth scope the call needs: a refusal naming them is answered with
|
|
50
45
|
* the `insufficient_scope` challenge an OAuth client re-authorizes on, so name them only where
|
|
51
|
-
* re-authorizing can grant them.
|
|
52
|
-
*
|
|
53
|
-
* otherwise a tool result with `isError: true`, in a 200 response.
|
|
46
|
+
* re-authorizing can grant them. What it answers on each surface: `docs/guarantees.md` in this
|
|
47
|
+
* package.
|
|
54
48
|
*/
|
|
55
49
|
var Forbidden = class extends Schema.TaggedError()("Forbidden", {
|
|
56
50
|
message: defaultedMessage("Not allowed."),
|
|
57
51
|
scopes: Schema.optionalKey(Schema.NonEmptyArray(ScopeToken))
|
|
58
|
-
}, { httpApiStatus:
|
|
52
|
+
}, { httpApiStatus: 403 }) {};
|
|
59
53
|
const refusals = [Unauthenticated, Forbidden];
|
|
60
54
|
/**
|
|
61
55
|
* What authentication or an implementation's `authorize` refuses a caller with, instead of
|
|
@@ -71,4 +65,4 @@ const builtIns = [InvalidInput, ...refusals];
|
|
|
71
65
|
/** The built-in failures as one schema: `Schema.is(Action.BuiltIn)(error)`. */
|
|
72
66
|
const BuiltIn = Schema.Union(builtIns);
|
|
73
67
|
//#endregion
|
|
74
|
-
export { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated, builtIns, scopeToken
|
|
68
|
+
export { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated, builtIns, scopeToken };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Unauthenticated } from "./errors.mjs";
|
|
2
|
-
import { Anyone, assertOnce } from "./rules.mjs";
|
|
3
|
-
import { Array, Context, Effect, Layer, Option } from "effect";
|
|
2
|
+
import { Anyone, assertAuthentication, assertDistinct, assertOnce, projectedErrors } from "./rules.mjs";
|
|
3
|
+
import { Array, Cause, Context, Effect, Exit, Layer, Option, Schema } from "effect";
|
|
4
4
|
//#region src/contract/implementation.ts
|
|
5
5
|
const uniqueKey = () => Math.random().toString(36).slice(2);
|
|
6
6
|
const memoized = (build) => {
|
|
@@ -62,7 +62,7 @@ var Implementation = class Implementation {
|
|
|
62
62
|
return Effect.gen(function* () {
|
|
63
63
|
const bound = yield* source.#handlers.key;
|
|
64
64
|
const before = actions.some((action) => action.caller !== Anyone) ? yield* source.#authorizer.key : () => Effect.void;
|
|
65
|
-
return
|
|
65
|
+
return actions.flatMap((action) => bound.flatMap(([held, handle]) => held === action ? [[action, dispatch(action, handle, before)]] : []));
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
/** `app`, if this copy of the module made it; another installed copy's cannot be read. */
|
|
@@ -100,6 +100,21 @@ const servedBy = (what, bound, apps, listed) => {
|
|
|
100
100
|
assertOnce("served action", actions);
|
|
101
101
|
return served;
|
|
102
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* What `ActionHttp.layer` and `ActionRpc.layer` serve, with the checks they share;
|
|
105
|
+
* `authentication` is the binding's where a served action is protected.
|
|
106
|
+
*/
|
|
107
|
+
const servingOf = (what, binding, served, options) => {
|
|
108
|
+
const apps = servedBy(what, binding.actions, toList(served), options.actions);
|
|
109
|
+
const actions = apps.flatMap((app) => app.actions);
|
|
110
|
+
assertAuthentication(actions, binding.authentication);
|
|
111
|
+
assertDistinct("middleware", options.middleware ?? [], (key) => key.key);
|
|
112
|
+
return {
|
|
113
|
+
apps,
|
|
114
|
+
actions,
|
|
115
|
+
authentication: actions.some((action) => action.caller !== Anyone) ? binding.authentication : void 0
|
|
116
|
+
};
|
|
117
|
+
};
|
|
103
118
|
const provideHandlers = (apps) => (layer) => {
|
|
104
119
|
const [first, ...rest] = apps.map((app) => Implementation.layerOf(app));
|
|
105
120
|
return first === void 0 ? layer : Layer.provide(layer, Layer.mergeAll(first, ...rest));
|
|
@@ -124,13 +139,26 @@ const dispatch = (action, handle, before) => {
|
|
|
124
139
|
};
|
|
125
140
|
const authentication = action.caller === Anyone ? Effect.void : Effect.flatMap(Effect.serviceOption(action.caller), (actor) => Option.isSome(actor) ? Effect.void : Effect.fail(new Unauthenticated()));
|
|
126
141
|
const authorization = action.caller === Anyone ? Effect.void : Effect.suspend(() => before(action));
|
|
142
|
+
const validate = Schema.decodeUnknownEffect(Schema.toType(Schema.Union(projectedErrors(action))));
|
|
143
|
+
const asDefects = (reason, rejected) => {
|
|
144
|
+
const trace = Context.makeUnsafe(reason.annotations);
|
|
145
|
+
return [Cause.makeDieReason(reason.error).annotate(trace), Cause.makeDieReason(Cause.squash(rejected)).annotate(trace)];
|
|
146
|
+
};
|
|
147
|
+
const declaredOrDefect = Effect.fnUntraced(function* (cause) {
|
|
148
|
+
const reasons = [];
|
|
149
|
+
for (const reason of cause.reasons) if (Cause.isFailReason(reason)) {
|
|
150
|
+
const validated = yield* Effect.exit(validate(reason.error));
|
|
151
|
+
reasons.push(...Exit.isSuccess(validated) ? [reason] : asDefects(reason, validated.cause));
|
|
152
|
+
} else reasons.push(reason);
|
|
153
|
+
return yield* Effect.failCause(Cause.fromReasons(reasons));
|
|
154
|
+
});
|
|
127
155
|
return (input) => {
|
|
128
156
|
const handled = Effect.withSpan(Effect.annotateLogs(Effect.suspend(() => handle(input)), actionAttributes), action.name, {
|
|
129
157
|
captureStackTrace: false,
|
|
130
158
|
attributes: actionAttributes
|
|
131
159
|
});
|
|
132
|
-
return Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(handled)));
|
|
160
|
+
return Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(handled), Effect.catchCause(declaredOrDefect)));
|
|
133
161
|
};
|
|
134
162
|
};
|
|
135
163
|
//#endregion
|
|
136
|
-
export { Implementation, acquire, assertHeld, builders, built, memoized, provideHandlers, select,
|
|
164
|
+
export { Implementation, acquire, assertHeld, builders, built, memoized, provideHandlers, select, servingOf, toList };
|
|
@@ -16,6 +16,13 @@ type Single<T> = true extends Types.IsUnion<T> ? never : T;
|
|
|
16
16
|
* `E` may be one of several.
|
|
17
17
|
*/
|
|
18
18
|
export type Certain<E extends Errors> = true extends Types.IsUnion<E> ? never : number extends E["length"] ? never : { readonly [K in keyof E]: Single<E[K]>; }[number];
|
|
19
|
+
/** The errors a binding declares for every call, and its protected actions' authentication's. */
|
|
20
|
+
export interface Declaring {
|
|
21
|
+
readonly error: Errors;
|
|
22
|
+
readonly authentication?: {
|
|
23
|
+
readonly error: Errors;
|
|
24
|
+
} | undefined;
|
|
25
|
+
}
|
|
19
26
|
/**
|
|
20
27
|
* The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
|
|
21
28
|
* authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
|
package/dist/contract/rules.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { builtIns
|
|
1
|
+
import { builtIns } from "./errors.mjs";
|
|
2
2
|
import { Predicate, Schema, SchemaAST } from "effect";
|
|
3
3
|
//#region src/contract/rules.ts
|
|
4
4
|
const errorList = (error) => error === void 0 ? [] : Schema.isSchema(error) ? [error] : error;
|
|
@@ -8,6 +8,14 @@ const projectedErrors = (action, surface = []) => [.../* @__PURE__ */ new Set([
|
|
|
8
8
|
...surface
|
|
9
9
|
])];
|
|
10
10
|
/**
|
|
11
|
+
* Every error a binding's call of `action` declares: the built-in ones, the action's, the
|
|
12
|
+
* binding's and, where `action` is protected, its authentication's.
|
|
13
|
+
*/
|
|
14
|
+
const bindingErrors = (action, binding) => projectedErrors(action, action.caller === Anyone || binding.authentication === void 0 ? binding.error : [...binding.error, ...binding.authentication.error]);
|
|
15
|
+
const assertAuthentication = (actions, authentication) => {
|
|
16
|
+
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`);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
11
19
|
* The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
|
|
12
20
|
* authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
|
|
13
21
|
* agree on a name.
|
|
@@ -45,9 +53,48 @@ const encodedTags = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member)
|
|
|
45
53
|
const tag = SchemaAST.isObjects(member) ? member.propertySignatures.find((property) => property.name === "_tag")?.type : void 0;
|
|
46
54
|
return tag === void 0 ? [] : members(tag).flatMap(literalValues).filter(Predicate.isString);
|
|
47
55
|
});
|
|
56
|
+
const builtInTags = new Set(builtIns.flatMap((error) => encodedTags(error.ast)));
|
|
48
57
|
const assertOwnTags = (what, errors) => {
|
|
49
|
-
const tag = errors.flatMap((error) => encodedTags(error.ast)).find((tag) =>
|
|
58
|
+
const tag = errors.flatMap((error) => encodedTags(error.ast)).find((tag) => builtInTags.has(tag));
|
|
50
59
|
if (tag !== void 0) throw new Error(`${what}: error _tag "${tag}" is built in, and declared on every surface`);
|
|
51
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* The checks `ActionHttp.make` and `ActionRpc.make` share: each action once, no built-in error in
|
|
63
|
+
* `error`, and the descriptor every protected action names. Returns `error` as a list.
|
|
64
|
+
*/
|
|
65
|
+
const bindingOf = (what, actions, options) => {
|
|
66
|
+
assertOnce("action", actions);
|
|
67
|
+
const error = errorList(options.error);
|
|
68
|
+
assertOwnTags(what, error);
|
|
69
|
+
assertAuthentication(actions, options.authentication);
|
|
70
|
+
return {
|
|
71
|
+
error,
|
|
72
|
+
authentication: options.authentication
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const httpApiStatusOf = SchemaAST.resolveAt("httpApiStatus");
|
|
76
|
+
const statusThroughSuspensions = (ast) => httpApiStatusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThroughSuspensions(ast.thunk()) : void 0);
|
|
77
|
+
/**
|
|
78
|
+
* `error` as an endpoint declares it: each member of a union without a status of its own apart,
|
|
79
|
+
* each with its status, 422 without one.
|
|
80
|
+
*/
|
|
81
|
+
const declared = (error) => {
|
|
82
|
+
const status = statusThroughSuspensions(error.ast);
|
|
83
|
+
if (status !== void 0) return [httpApiStatusOf(error.ast) === void 0 ? error.annotate({ httpApiStatus: status }) : error];
|
|
84
|
+
const resolved = unsuspended(error.ast);
|
|
85
|
+
if (SchemaAST.isUnion(resolved) && resolved.checks === void 0 && resolved.encoding === void 0) return resolved.types.flatMap((member) => declared(Schema.make(member)));
|
|
86
|
+
return [error.annotate({ httpApiStatus: 422 })];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* An action's MCP tool hints, MCP's defaults where its `mcp` leaves one out: a write destructive,
|
|
90
|
+
* a read not, no tool idempotent and every tool open-world. A given value passes through, as MCP
|
|
91
|
+
* calls `destructiveHint` and `idempotentHint` meaningless on a read-only tool.
|
|
92
|
+
*/
|
|
93
|
+
const hintsOf = ({ readOnly, mcp }) => ({
|
|
94
|
+
readOnlyHint: readOnly,
|
|
95
|
+
destructiveHint: mcp.destructiveHint ?? !readOnly,
|
|
96
|
+
idempotentHint: mcp.idempotentHint ?? false,
|
|
97
|
+
openWorldHint: mcp.openWorldHint ?? true
|
|
98
|
+
});
|
|
52
99
|
//#endregion
|
|
53
|
-
export { Anyone, assertDistinct, assertKnown, assertName, assertOnce, assertOwnTags, errorList, literalValues, members, projectedErrors, unsuspended };
|
|
100
|
+
export { Anyone, assertAuthentication, assertDistinct, assertKnown, assertName, assertOnce, assertOwnTags, bindingErrors, bindingOf, declared, errorList, hintsOf, httpApiStatusOf, literalValues, members, projectedErrors, unsuspended };
|
package/dist/http/client.mjs
CHANGED
|
@@ -2,16 +2,13 @@ import { checked } from "../contract/call.mjs";
|
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import { HttpApiClient } from "effect/http-api";
|
|
4
4
|
//#region src/http/client.ts
|
|
5
|
-
const assertInBinding = (actions, action) => {
|
|
6
|
-
if (!actions.includes(action)) throw new Error(`Action "${action.name}" is not in this HTTP binding`);
|
|
7
|
-
};
|
|
8
5
|
const methods = (http, options = {}) => Effect.map(HttpApiClient.make(http.api, options), (client) => {
|
|
9
6
|
const native = client;
|
|
10
|
-
return (action) => {
|
|
7
|
+
return Object.fromEntries(http.actions.map((action) => {
|
|
11
8
|
const method = native[action.name];
|
|
12
9
|
if (method === void 0) throw new Error(`No client method for ${action.name}`);
|
|
13
|
-
return checked(action, (payload) => method({ payload }));
|
|
14
|
-
};
|
|
10
|
+
return [action.name, checked(action, (payload) => method({ payload }))];
|
|
11
|
+
}));
|
|
15
12
|
});
|
|
16
13
|
//#endregion
|
|
17
|
-
export {
|
|
14
|
+
export { methods };
|