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