@gjermundgaraba/effect-actions 0.8.0 → 0.10.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 +57 -50
- package/dist/Action.d.mts +285 -79
- package/dist/Action.mjs +124 -23
- package/dist/ActionCli.d.mts +102 -31
- package/dist/ActionCli.mjs +114 -16
- package/dist/ActionHttp.d.mts +170 -51
- package/dist/ActionHttp.mjs +130 -79
- package/dist/ActionMcp.d.mts +71 -21
- package/dist/ActionMcp.mjs +108 -81
- package/dist/ActionToolkit.d.mts +56 -27
- package/dist/ActionToolkit.mjs +16 -5
- package/dist/Authentication.d.mts +129 -33
- package/dist/Authentication.mjs +231 -38
- package/dist/Testing.d.mts +82 -57
- package/dist/Testing.mjs +89 -79
- package/dist/internal/actions.d.mts +5 -25
- package/dist/internal/actions.mjs +75 -27
- package/dist/internal/authentication.d.mts +102 -0
- package/dist/internal/authentication.mjs +11 -0
- package/dist/internal/call.d.mts +12 -0
- package/dist/internal/call.mjs +12 -0
- package/dist/internal/cli.d.mts +36 -22
- package/dist/internal/cli.mjs +311 -33
- package/dist/internal/client.d.mts +43 -0
- package/dist/internal/client.mjs +25 -0
- package/dist/internal/console.mjs +77 -0
- package/dist/internal/declared.d.mts +13 -0
- package/dist/internal/declared.mjs +34 -0
- package/dist/internal/errors.d.mts +77 -0
- package/dist/internal/errors.mjs +79 -0
- package/dist/internal/implementation.d.mts +171 -41
- package/dist/internal/implementation.mjs +147 -29
- package/dist/internal/mcp.d.mts +11 -0
- package/dist/internal/mcp.mjs +52 -0
- package/dist/internal/memory.d.mts +6 -0
- package/dist/internal/memory.mjs +38 -0
- package/dist/internal/refusal.mjs +67 -0
- package/dist/internal/schema-errors.mjs +20 -0
- package/dist/internal/tools.mjs +27 -57
- package/docs/Action.md +401 -73
- package/docs/ActionCli.md +280 -51
- package/docs/ActionHttp.md +266 -82
- package/docs/ActionMcp.md +149 -104
- package/docs/ActionToolkit.md +97 -35
- package/docs/Authentication.md +369 -62
- package/docs/CONTEXT.md +23 -36
- package/docs/README.md +29 -73
- package/docs/Testing.md +154 -77
- package/docs/guarantees.md +81 -43
- package/docs/setup.md +90 -27
- package/package.json +9 -17
- package/dist/ActionCatalog.d.mts +0 -30
- package/dist/ActionCatalog.mjs +0 -39
- package/dist/ActionCliClient.d.mts +0 -48
- package/dist/ActionCliClient.mjs +0 -43
- package/dist/ActionGroup.d.mts +0 -44
- package/dist/ActionGroup.mjs +0 -35
- package/dist/ActionHttpClient.d.mts +0 -33
- package/dist/ActionHttpClient.mjs +0 -16
- package/dist/TestingClient.d.mts +0 -17
- package/dist/TestingClient.mjs +0 -22
- package/dist/internal/fetchClient.d.mts +0 -6
- package/dist/internal/fetchClient.mjs +0 -8
- package/dist/internal/tools.d.mts +0 -22
- package/docs/ActionCatalog.md +0 -54
- package/docs/ActionCliClient.md +0 -63
- package/docs/ActionGroup.md +0 -128
- package/docs/ActionHttpClient.md +0 -80
package/dist/ActionMcp.mjs
CHANGED
|
@@ -1,98 +1,125 @@
|
|
|
1
|
-
import "./internal/
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { Anyone } from "./internal/actions.mjs";
|
|
2
|
+
import { provideHandlers, select, toList } from "./internal/implementation.mjs";
|
|
3
|
+
import { assertAuthentication, promote } from "./internal/authentication.mjs";
|
|
4
|
+
import { httpProtocol } from "./internal/mcp.mjs";
|
|
5
|
+
import { recordStepUp } from "./internal/refusal.mjs";
|
|
6
|
+
import { logToStderr } from "./internal/console.mjs";
|
|
7
|
+
import { bindTools } from "./internal/tools.mjs";
|
|
8
|
+
import { Cause, Context, Effect, Exit, Fiber, JsonPointer, Layer, Predicate, Schema } from "effect";
|
|
9
|
+
import { HttpRouter, HttpServerRequest } from "effect/http";
|
|
10
|
+
import { McpProtocol, McpSchema, McpServer, Tool } from "effect/ai";
|
|
5
11
|
//#region src/ActionMcp.ts
|
|
6
12
|
/**
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
13
|
+
* The revisions served over stdio: 2026-07-28 and the stateful revisions hosts open stdio
|
|
14
|
+
* with, newest first; a host asking for another is offered 2025-11-25. A success is sent as it
|
|
15
|
+
* is on each. 2026-07-28 carries any success as `structuredContent`; the adapters of 2025-11-25
|
|
16
|
+
* and 2025-06-18 carry only an object there, and list only an object-rooted `outputSchema`. A
|
|
17
|
+
* success they do not structure is text alone, a string as itself.
|
|
9
18
|
*/
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
rest
|
|
24
|
-
};
|
|
19
|
+
const stdioProtocols = [
|
|
20
|
+
McpProtocol.v2026_07_28,
|
|
21
|
+
McpProtocol.v2025_11_25,
|
|
22
|
+
McpProtocol.v2025_06_18
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* MCP has a JSON-only wire contract. Success is the encoded success as structured content,
|
|
26
|
+
* and its JSON as text; declared failures are returned as JSON text. The native server
|
|
27
|
+
* refuses undeclared arguments and publishes closed input schemas.
|
|
28
|
+
*/
|
|
29
|
+
const projection = {
|
|
30
|
+
label: "MCP tool",
|
|
31
|
+
tool: (tool) => tool.annotate(Tool.Strict, true)
|
|
25
32
|
};
|
|
33
|
+
/** Whether a JSON Schema is one the native server takes for a tool's arguments. */
|
|
34
|
+
const isToolJson = Schema.is(McpSchema.ToolJson);
|
|
26
35
|
/**
|
|
27
|
-
* The
|
|
28
|
-
*
|
|
29
|
-
* structured content would not satisfy.
|
|
36
|
+
* The JSON Schema root of `schema` as the native server lists it for a tool, a `$ref` at its
|
|
37
|
+
* root resolved.
|
|
30
38
|
*/
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const required = (listed.required ?? []).filter((key) => key !== field);
|
|
36
|
-
return Effect.succeed({
|
|
37
|
-
...output,
|
|
38
|
-
properties: detach(listed.properties, field).rest,
|
|
39
|
-
...required.length === 0 ? {} : { required }
|
|
40
|
-
});
|
|
39
|
+
const rootOf = (schema) => {
|
|
40
|
+
const document = Schema.toJsonSchemaDocument(Schema.toCodecJson(schema));
|
|
41
|
+
const [scope, key] = Predicate.isString(document.schema.$ref) ? JsonPointer.parseUriFragment(document.schema.$ref) ?? [] : [];
|
|
42
|
+
return scope === "$defs" && key !== void 0 ? document.definitions[key] : document.schema;
|
|
41
43
|
};
|
|
42
44
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
45
|
+
* Refuse, when the server is made, the actions of `apps` whose input is not one object, as the
|
|
46
|
+
* root of a tool's arguments must be, naming every one, reading the JSON Schema the native
|
|
47
|
+
* server reads: not a union, an array, a scalar, nor `Schema.Struct({})`, which takes any value
|
|
48
|
+
* but `null`, where the native server dies when the layer builds, suggesting
|
|
49
|
+
* `Tool.EmptyParams`.
|
|
46
50
|
*/
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
...
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
51
|
+
const assertShapes = (apps) => {
|
|
52
|
+
const inputs = apps.flatMap((app) => app.actions).filter((action) => !isToolJson(rootOf(action.input))).map(({ name }) => name);
|
|
53
|
+
if (inputs.length > 0) throw new Error(`MCP tool input must be one object with keys, such as a struct: ${inputs.join(", ")}`);
|
|
54
|
+
};
|
|
55
|
+
const server = (apps, transport, features = Layer.empty, authentication) => {
|
|
56
|
+
const binding = bindTools(apps, authentication === void 0 ? projection : {
|
|
57
|
+
...projection,
|
|
58
|
+
handler: (run, action) => (input) => action.caller === Anyone ? run(input) : recordStepUp(promote(authentication, run(input)))
|
|
59
|
+
});
|
|
60
|
+
assertShapes(apps);
|
|
61
|
+
const names = new Set(apps.flatMap((app) => app.actions.map(({ name }) => name)));
|
|
62
|
+
const register = Effect.gen(function* () {
|
|
63
|
+
const registry = yield* McpServer.McpServer;
|
|
64
|
+
const claimed = registry.tools.flatMap(({ tool }) => names.has(tool.name) ? [tool.name] : []);
|
|
65
|
+
if (claimed.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate MCP tool: ${claimed.join(", ")}, claimed by an action and a native feature`));
|
|
66
|
+
const handlers = yield* Layer.build(binding.layer);
|
|
67
|
+
yield* McpServer.registerToolkit(binding.toolkit).pipe(Effect.setContext(Context.add(handlers, McpServer.McpServer, registry)));
|
|
62
68
|
});
|
|
69
|
+
return Layer.effectDiscard(register).pipe(Layer.provide(features), Layer.provide(transport), Layer.fresh, provideHandlers(apps));
|
|
63
70
|
};
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
/**
|
|
72
|
+
* What an anonymous caller of a mixed endpoint may send besides a public tool's call:
|
|
73
|
+
* discovering the server and its tools, which the contracts describe, and cancelling.
|
|
74
|
+
* Everything else, a native feature's listing, completion or subscription among them, and
|
|
75
|
+
* any method a later revision adds, authenticates.
|
|
76
|
+
*/
|
|
77
|
+
const anonymous = /* @__PURE__ */ new Set([
|
|
78
|
+
"server/discover",
|
|
79
|
+
"tools/list",
|
|
80
|
+
"notifications/cancelled"
|
|
81
|
+
]);
|
|
82
|
+
/**
|
|
83
|
+
* Whether a request to an endpoint serving `actions` must authenticate before the native
|
|
84
|
+
* server reads its body. An endpoint whose every tool is protected authenticates every
|
|
85
|
+
* request, so a host signs in when it connects. Otherwise the request's routing headers
|
|
86
|
+
* decide, which the native server refuses when they disagree with the body: discovery and a
|
|
87
|
+
* call of a public tool authenticate only a caller presenting credentials, and everything
|
|
88
|
+
* else authenticates, so native features are protected too. A request whose headers name
|
|
89
|
+
* no public tool as it is authenticates: it fails closed.
|
|
90
|
+
*/
|
|
91
|
+
const requiresAuthentication = (actions) => {
|
|
92
|
+
const open = new Set(actions.filter(({ caller }) => caller === Anyone).map(({ name }) => name));
|
|
93
|
+
return (headers) => {
|
|
94
|
+
if (open.size === 0) return true;
|
|
95
|
+
const method = headers["mcp-method"];
|
|
96
|
+
if (method === void 0) return true;
|
|
97
|
+
if (anonymous.has(method)) return false;
|
|
98
|
+
if (method !== "tools/call") return true;
|
|
99
|
+
const name = headers["mcp-name"];
|
|
100
|
+
return name === void 0 || !open.has(name);
|
|
101
|
+
};
|
|
83
102
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
function layerHttp(apps, { actions, features, path, authentication, ...options }) {
|
|
104
|
+
const selected = select(toList(apps), actions);
|
|
105
|
+
const served = selected.flatMap((app) => app.actions);
|
|
106
|
+
assertAuthentication(served, authentication);
|
|
107
|
+
const transport = server(selected, McpServer.layerHttp({
|
|
87
108
|
...options,
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
path: path ?? "/mcp",
|
|
110
|
+
protocols: [httpProtocol]
|
|
111
|
+
}), features, authentication);
|
|
112
|
+
if (authentication === void 0) return transport;
|
|
113
|
+
const required = requiresAuthentication(served);
|
|
114
|
+
const middleware = HttpRouter.middleware(Effect.map(authentication["~provider"], (runtime) => (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => runtime.http(route, !required(request.headers)))));
|
|
115
|
+
return transport.pipe(Layer.provide(middleware.layer));
|
|
90
116
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
117
|
+
function runStdio(apps, { actions, features, ...options }) {
|
|
118
|
+
const transport = server(select(toList(apps), actions), McpServer.layerStdio({
|
|
93
119
|
...options,
|
|
94
|
-
protocols
|
|
95
|
-
}));
|
|
120
|
+
protocols: stdioProtocols
|
|
121
|
+
}), features);
|
|
122
|
+
return Layer.launch(transport).pipe(logToStderr, Effect.forkChild, Effect.flatMap(Fiber.await), Effect.flatMap((exit) => Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause) ? Effect.failCause(exit.cause) : Effect.void));
|
|
96
123
|
}
|
|
97
124
|
//#endregion
|
|
98
|
-
export { layerHttp,
|
|
125
|
+
export { layerHttp, runStdio };
|
package/dist/ActionToolkit.d.mts
CHANGED
|
@@ -1,40 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Layer, Schema } from "effect";
|
|
1
|
+
import { BuiltIns } from "./internal/errors.mjs";
|
|
2
|
+
import { ActionOf, BuildError, BuildServices, Holding, Known, Member, Offered, OptionalUnless, RequestOf, SelectedOf, Served, Serving } from "./internal/implementation.mjs";
|
|
3
|
+
import { Action_d_exports, Any } from "./Action.mjs";
|
|
4
|
+
import { Effect, Layer, Schema } from "effect";
|
|
5
5
|
import { Tool, Toolkit } from "effect/ai";
|
|
6
6
|
//#region src/ActionToolkit.d.ts
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
readonly
|
|
12
|
-
} ? Tool.Tool<Name, {
|
|
13
|
-
readonly parameters: A["input"];
|
|
14
|
-
readonly success: A["success"];
|
|
15
|
-
readonly failure: Schema.Union<ReadonlyArray<A["errors"][number] | E>>;
|
|
7
|
+
/** A native tool named after its action, taking and giving JSON. */
|
|
8
|
+
type NativeTool<A extends Any, R> = Tool.Tool<A["name"], {
|
|
9
|
+
readonly parameters: Schema.toCodecJson<A["input"]>;
|
|
10
|
+
readonly success: Schema.toCodecJson<A["success"]>;
|
|
11
|
+
readonly failure: Schema.toCodecJson<Schema.Union<ReadonlyArray<A["error"][number] | BuiltIns>>>;
|
|
16
12
|
readonly failureMode: "return";
|
|
17
|
-
}, R
|
|
18
|
-
type HandlersOf<App extends AnyImplementation> = App extends Implementation<any, infer H, any, any> ? H : never;
|
|
13
|
+
}, R>;
|
|
19
14
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* The tool of each action of `App` among `Listed`. A tool needs what its handler and its
|
|
16
|
+
* implementation's authorization need.
|
|
22
17
|
*/
|
|
23
|
-
type
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
type ToolFor<App, Listed extends Any> = App extends unknown ? Serving<App, Listed> extends (infer A extends Any) ? A extends Any ? NativeTool<A, RequestOf<App, A>> : never : never : never;
|
|
19
|
+
/**
|
|
20
|
+
* Native tools bound to their action implementations: a native `Toolkit` and the layer of
|
|
21
|
+
* its handlers, for `LanguageModel`, `Toolkit.merge` or `handle`. Tools belong to their
|
|
22
|
+
* implementations: the `layer` of a `make` call serves the tools it selected in any `toolkit`
|
|
23
|
+
* of the same implementations, while two implementations never run each other's handlers,
|
|
24
|
+
* even with tools of one name.
|
|
25
|
+
*/
|
|
26
|
+
export interface Tools<T extends Record<string, Tool.Any>, E, R> {
|
|
27
|
+
/**
|
|
28
|
+
* The native toolkit: its `tools` are the definitions, by name, with schemas, hints and approval.
|
|
29
|
+
*/
|
|
30
|
+
readonly toolkit: Toolkit.Toolkit<T>;
|
|
29
31
|
/** Acquires handlers once in the layer scope; handler requirements remain at invocation. */
|
|
30
|
-
readonly layer: Layer.Layer<Tool.HandlersFor<
|
|
32
|
+
readonly layer: Layer.Layer<Tool.HandlersFor<T>, E, R>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One call of a tool, as `needsApproval` receives it: its action's name, the action, and
|
|
36
|
+
* the call's decoded input. Checking `name` narrows the other two to that action's.
|
|
37
|
+
*/
|
|
38
|
+
export type ToolCall<A extends Any = Any> = A extends Any ? {
|
|
39
|
+
readonly name: A["name"];
|
|
40
|
+
readonly action: A;
|
|
41
|
+
readonly input: A["input"]["Type"];
|
|
42
|
+
} : never;
|
|
43
|
+
/** How `make` projects its tools, the actions `A`. */
|
|
44
|
+
export interface Options<A extends Any> {
|
|
45
|
+
/**
|
|
46
|
+
* The actions that are tools, among the implementations' actions: `[GetUser, RenameUser]`.
|
|
47
|
+
* Each keeps its implementation's authorization and builder. Defaults to every action of them.
|
|
48
|
+
*/
|
|
49
|
+
readonly actions?: ReadonlyArray<A> | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a model's call needs approval before it runs: a boolean, or an Effect of one, for
|
|
52
|
+
* each call, with Effect's native approval context. `LanguageModel` asks for approval
|
|
53
|
+
* instead of calling the tool when it is `true`. The Effect runs in the caller's context and
|
|
54
|
+
* requires nothing, so it reads the caller with `Effect.serviceOption`. Defaults to none.
|
|
55
|
+
*/
|
|
56
|
+
readonly needsApproval?: (call: ToolCall<A>, context: Tool.NeedsApprovalContext) => boolean | Effect.Effect<boolean>;
|
|
31
57
|
}
|
|
32
58
|
/**
|
|
33
|
-
* Project implementations into Effect's native AI toolkit
|
|
59
|
+
* Project implementations into Effect's native AI toolkit: one tool per action, keyed by its
|
|
60
|
+
* name, of the listed `actions` or, without them, of every action of the implementations.
|
|
34
61
|
*
|
|
35
62
|
* Unlike MCP, calls return the action's native success/failure values directly.
|
|
36
63
|
* Build services are needed to construct `layer`; request services are needed
|
|
37
|
-
* when the resulting toolkit handles a call
|
|
64
|
+
* when the resulting toolkit handles a call, the identity an implementation's `authorize`
|
|
65
|
+
* reads included: the caller provides it. `needsApproval` marks the calls a model must have
|
|
66
|
+
* approved; it authorizes nothing, which stays `authorize`'s.
|
|
38
67
|
*/
|
|
39
|
-
export declare function make<const Apps extends
|
|
68
|
+
export declare function make<const Apps extends Served, const O extends Options<ActionOf<Member<Apps>>> = {}>(implementations: Apps, ...options: OptionalUnless<O, Options<ActionOf<Member<Apps>>> & O & NoInfer<Known<O, Options<Any>>>>): Tools<{ readonly [T in ToolFor<Member<Apps>, Offered<O, ActionOf<Member<Apps>>>> as T["name"]]: T; }, BuildError<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>, BuildServices<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>;
|
|
40
69
|
//#endregion
|
package/dist/ActionToolkit.mjs
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import "./internal/implementation.mjs";
|
|
1
|
+
import { Implementation, provideHandlers, select, toList } from "./internal/implementation.mjs";
|
|
2
2
|
import { bindTools } from "./internal/tools.mjs";
|
|
3
|
-
import "effect";
|
|
4
|
-
import "effect/ai";
|
|
5
3
|
//#region src/ActionToolkit.ts
|
|
6
|
-
function make(apps, options
|
|
7
|
-
|
|
4
|
+
function make(apps, options) {
|
|
5
|
+
const served = select(toList(apps), options?.actions);
|
|
6
|
+
const needsApproval = options?.needsApproval;
|
|
7
|
+
const { toolkit, layer } = bindTools(served, {
|
|
8
|
+
label: "tool",
|
|
9
|
+
tool: (tool, action, app) => Object.assign(needsApproval === void 0 ? tool : tool.setNeedsApproval((input, context) => needsApproval({
|
|
10
|
+
name: action.name,
|
|
11
|
+
action,
|
|
12
|
+
input
|
|
13
|
+
}, context)), { id: `${Implementation.runKey(app)}/${action.name}` })
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
toolkit,
|
|
17
|
+
layer: layer.pipe(provideHandlers(served))
|
|
18
|
+
};
|
|
8
19
|
}
|
|
9
20
|
//#endregion
|
|
10
21
|
export { make };
|
|
@@ -1,46 +1,142 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Refusal } from "./internal/errors.mjs";
|
|
2
|
+
import { Known, OptionalUnless } from "./internal/implementation.mjs";
|
|
3
|
+
import { Errors } from "./Action.mjs";
|
|
4
|
+
import { Any, Descriptor, Provider, Security } from "./internal/authentication.mjs";
|
|
5
|
+
import { ErrorsOf } from "./internal/declared.mjs";
|
|
6
|
+
import { Array, Context, Effect, Layer, Option, Redacted, Scope } from "effect";
|
|
2
7
|
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
3
|
-
import {
|
|
8
|
+
import { HttpApiSecurity } from "effect/http-api";
|
|
4
9
|
//#region src/Authentication.d.ts
|
|
5
10
|
/**
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* including private failures serialized by enclosing middleware.
|
|
11
|
+
* The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
|
|
12
|
+
* `HttpApiSecurity.bearer` reads it for routes and MCP, which a test holds equal. It is for a
|
|
13
|
+
* caller the router never routes, which holds the header and no request; a route of the host's
|
|
14
|
+
* own is authenticated by `protect`. The scheme is matched case-insensitively, as RFC 9110
|
|
15
|
+
* requires, and the token is `Redacted`, as Effect's own decoder gives it.
|
|
12
16
|
*/
|
|
13
|
-
export declare const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
error: never;
|
|
17
|
-
requires: Exclude<R, HttpRouter.Provided> | Exclude<Exclude<import("effect/Types").NoInfer<I>, I>, HttpRouter.Provided>;
|
|
18
|
-
layerError: never;
|
|
19
|
-
layerRequires: never;
|
|
20
|
-
}>;
|
|
21
|
-
/** RFC 9728 metadata to publish; the host is responsible for these being valid OAuth URLs. */
|
|
22
|
-
export interface ProtectedResourceOptions {
|
|
17
|
+
export declare const bearerTokenOf: (authorization: string | null | undefined) => Option.Option<Redacted.Redacted<string>>;
|
|
18
|
+
/** An OAuth protected resource (RFC 9728), as `layer` publishes it. */
|
|
19
|
+
export interface ProtectedResource {
|
|
23
20
|
/** Exact OAuth resource identifier; its path and query select the discovery path. */
|
|
24
21
|
readonly resource: string;
|
|
25
|
-
|
|
22
|
+
/** Where clients get tokens: nonempty. */
|
|
23
|
+
readonly authorizationServers: Array.NonEmptyReadonlyArray<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Every scope the resource accepts, each an OAuth scope token, which a client requests when a
|
|
26
|
+
* 401 names none.
|
|
27
|
+
*/
|
|
26
28
|
readonly scopesSupported?: ReadonlyArray<string>;
|
|
29
|
+
/**
|
|
30
|
+
* The scopes every 401 names, each an OAuth scope token: what a client requests when it
|
|
31
|
+
* authenticates, rather than every scope supported. A `Forbidden` naming scopes asks for
|
|
32
|
+
* more when a call needs them.
|
|
33
|
+
*/
|
|
34
|
+
readonly scopesRequired?: Array.NonEmptyReadonlyArray<string>;
|
|
27
35
|
readonly resourceName?: string;
|
|
28
36
|
}
|
|
29
|
-
/**
|
|
30
|
-
export interface
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
/** What `refusalResponse` answers about. */
|
|
38
|
+
export interface RefusalResponseOptions<D extends Any = Any> {
|
|
39
|
+
/**
|
|
40
|
+
* The descriptor refusing, whose scheme decides the challenge: Bearer's, below, when left
|
|
41
|
+
* out; another scheme's 401 names that scheme, and nothing steps up under it. An error it
|
|
42
|
+
* declares is sent as its endpoints declare it.
|
|
43
|
+
*/
|
|
44
|
+
readonly authentication?: D | undefined;
|
|
45
|
+
/** The OAuth protected resource refusing: the one given to `layer`. */
|
|
46
|
+
readonly protectedResource?: ProtectedResource | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The request's `Authorization` header, as the web `Headers.get` gives it, which decides whether a
|
|
49
|
+
* 401 names `invalid_token`.
|
|
50
|
+
*/
|
|
51
|
+
readonly authorization?: string | null | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** What the descriptor `D` declares its verifier fails with besides a refusal. */
|
|
54
|
+
type DeclaredOf<D> = D extends {
|
|
55
|
+
readonly error: infer E extends Errors;
|
|
56
|
+
} ? E[number]["Type"] : never;
|
|
57
|
+
/**
|
|
58
|
+
* The response `layer` answers a refusal with, for a caller the router never routes, such as
|
|
59
|
+
* a Node `upgrade` handler admitting a socket: its status, its JSON, `Cache-Control: no-store`
|
|
60
|
+
* and its challenge, an `Unauthenticated`'s `Bearer`, naming `invalid_token` when
|
|
61
|
+
* `authorization` presented a bearer token, or the `insufficient_scope` of a `Forbidden`
|
|
62
|
+
* naming scopes. Given the protected resource `layer` was, every challenge names its metadata
|
|
63
|
+
* URL and a 401's its `scopesRequired`. Under a descriptor of another scheme, it is the JSON
|
|
64
|
+
* and status alone, a 401 naming that scheme. An error `authentication` declares is sent as
|
|
65
|
+
* its endpoints declare it, its JSON with its status. `HttpServerResponse.toWeb` gives it as a
|
|
66
|
+
* web `Response`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const refusalResponse: <D extends Any = never>(error: Refusal | DeclaredOf<D>, options?: RefusalResponseOptions<D>) => HttpServerResponse.HttpServerResponse;
|
|
69
|
+
/** What `make` takes besides the name and the identity. */
|
|
70
|
+
export interface Options {
|
|
71
|
+
/** The one native scheme a caller proves the identity by: Bearer when left out. */
|
|
72
|
+
readonly security?: Security;
|
|
73
|
+
/**
|
|
74
|
+
* What the verifier may fail with besides a refusal, such as its provider being unreachable:
|
|
75
|
+
* one schema, or a list. Every protected endpoint declares it, so clients decode it.
|
|
76
|
+
*/
|
|
77
|
+
readonly error?: Errors | Errors[number];
|
|
78
|
+
}
|
|
79
|
+
/** The scheme options `O` give: Bearer too wherever they may leave it out, as at run time. */
|
|
80
|
+
type SecurityOf<O> = O extends {
|
|
81
|
+
readonly security: infer S extends Security;
|
|
82
|
+
} ? S : "security" extends keyof O ? NonNullable<O["security" & keyof O]> | HttpApiSecurity.Http : HttpApiSecurity.Http;
|
|
83
|
+
/**
|
|
84
|
+
* Browser-safe declaration shared by clients and remote surfaces: the `identity` it
|
|
85
|
+
* authenticates, by the one native scheme `security`, Bearer unless it names another, such
|
|
86
|
+
* as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`. The literal name
|
|
87
|
+
* identifies the provider, like a native Context.Key name, and the scheme's OpenAPI key, so it
|
|
88
|
+
* holds only letters, digits, `_`, `.` and `-`; reuse one name only for one declaration.
|
|
89
|
+
*/
|
|
90
|
+
export declare const make: <const Name extends string, I, A, const O extends Options = {}>(name: Name, identity: Context.Key<I, A>, ...options: OptionalUnless<O, O & NoInfer<Known<O, Options>>>) => Descriptor<I, A, SecurityOf<O>, Name, ErrorsOf<O>>;
|
|
91
|
+
/**
|
|
92
|
+
* A descriptor's verifier: the credential its scheme decodes, as Effect's own decoder gives
|
|
93
|
+
* it, to the identity, or a refusal, or an error the descriptor declares, `E`. Its services
|
|
94
|
+
* belong to the request.
|
|
95
|
+
*/
|
|
96
|
+
export type Verify<A, S extends Security, R, E = never> = (credential: HttpApiSecurity.HttpApiSecurity.Type<S>) => Effect.Effect<A, Refusal | E, R>;
|
|
97
|
+
export interface LayerOptions<EP = never, RP = never> {
|
|
98
|
+
readonly protectedResource?: ProtectedResource | Effect.Effect<ProtectedResource | undefined, EP, RP>;
|
|
35
99
|
}
|
|
36
100
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
101
|
+
* Only a Bearer scheme publishes an OAuth protected resource. The types tell only an `Http`
|
|
102
|
+
* scheme from the others, as its scheme is a `string`: `layer` refuses another `Http` one.
|
|
39
103
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
metadataUrl: string;
|
|
43
|
-
/** The `WWW-Authenticate` value; parameter values are quoted and escaped. */
|
|
44
|
-
challenge: (challenge?: BearerChallengeOptions) => string;
|
|
104
|
+
type ResourceOf<S> = [S] extends [HttpApiSecurity.Http] ? unknown : {
|
|
105
|
+
readonly protectedResource?: undefined;
|
|
45
106
|
};
|
|
46
|
-
|
|
107
|
+
/**
|
|
108
|
+
* The provider of `authentication`: `verify`, or an Effect building it once per layer graph, run on
|
|
109
|
+
* each remote request a protected action receives, with the credential the descriptor's
|
|
110
|
+
* scheme decodes. A Bearer scheme answers each 401 with its challenge, and may publish the
|
|
111
|
+
* OAuth protected resource `protectedResource`.
|
|
112
|
+
*/
|
|
113
|
+
export declare function layer<const Name extends string, I, A, const S extends Security, E extends Errors, R, EX = never, RX = never, EP = never, RP = never>(authentication: Descriptor<I, A, S, Name, E>, verify: Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>> | Effect.Effect<Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>>, EX, RX>, options?: LayerOptions<EP, RP> & ResourceOf<S>): Layer.Layer<Provider<I, Name>, EX | EP, HttpRouter.HttpRouter | Exclude<RX | RP, Scope.Scope> | HttpRouter.Request.From<"Requires", Exclude<R, HttpRouter.Provided>>>;
|
|
114
|
+
/** The identity `I`, or none where it is erased to `unknown`. */
|
|
115
|
+
type Provides<I> = unknown extends I ? never : I;
|
|
116
|
+
/**
|
|
117
|
+
* What `protect` returns for a descriptor named `Name` authenticating the identity `I`: router
|
|
118
|
+
* middleware providing it, answering a refusal its routes fail with, and requiring the
|
|
119
|
+
* descriptor's provider. Named so a package emitting declarations can export one.
|
|
120
|
+
*/
|
|
121
|
+
export type Protection<I, Name extends string> = HttpRouter.Middleware<{
|
|
122
|
+
provides: Provides<I>;
|
|
123
|
+
handles: Refusal;
|
|
124
|
+
error: never;
|
|
125
|
+
requires: never;
|
|
126
|
+
layerError: never;
|
|
127
|
+
layerRequires: Provider<I, Name>;
|
|
128
|
+
}>;
|
|
129
|
+
/**
|
|
130
|
+
* Native router middleware authenticating a route of the host's own, such as an export, a
|
|
131
|
+
* page frame or a WebSocket upgrade, with `authentication`'s provider, as its actions' routes
|
|
132
|
+
* are: the credential its scheme decodes, verified by the same verifier, and the identity
|
|
133
|
+
* provided to the route. It answers as an action route: a missing or invalid credential with
|
|
134
|
+
* the refusal, every response to the request it authenticates `no-store` unless it states its
|
|
135
|
+
* own caching, and a refusal the route fails with, such as a scope check's `Forbidden`, as its
|
|
136
|
+
* status, JSON and challenge, stepping up under Bearer. The layer requires the provider,
|
|
137
|
+
* `layer(authentication, ...)`. An erased descriptor, `Any`, provides no service the types can
|
|
138
|
+
* name: its identity is `unknown`, which would discharge every request service the route owes.
|
|
139
|
+
*/
|
|
140
|
+
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>;
|
|
141
|
+
//#endregion
|
|
142
|
+
export type { Any, Descriptor, Provider };
|