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