@gjermundgaraba/effect-actions 0.9.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.
Files changed (68) hide show
  1. package/README.md +57 -50
  2. package/dist/Action.d.mts +285 -79
  3. package/dist/Action.mjs +124 -23
  4. package/dist/ActionCli.d.mts +102 -31
  5. package/dist/ActionCli.mjs +114 -16
  6. package/dist/ActionHttp.d.mts +170 -51
  7. package/dist/ActionHttp.mjs +130 -79
  8. package/dist/ActionMcp.d.mts +71 -21
  9. package/dist/ActionMcp.mjs +108 -81
  10. package/dist/ActionToolkit.d.mts +56 -27
  11. package/dist/ActionToolkit.mjs +16 -5
  12. package/dist/Authentication.d.mts +129 -33
  13. package/dist/Authentication.mjs +231 -38
  14. package/dist/Testing.d.mts +82 -57
  15. package/dist/Testing.mjs +89 -79
  16. package/dist/internal/actions.d.mts +5 -25
  17. package/dist/internal/actions.mjs +75 -27
  18. package/dist/internal/authentication.d.mts +102 -0
  19. package/dist/internal/authentication.mjs +11 -0
  20. package/dist/internal/call.d.mts +12 -0
  21. package/dist/internal/call.mjs +12 -0
  22. package/dist/internal/cli.d.mts +36 -22
  23. package/dist/internal/cli.mjs +311 -33
  24. package/dist/internal/client.d.mts +43 -0
  25. package/dist/internal/client.mjs +25 -0
  26. package/dist/internal/console.mjs +77 -0
  27. package/dist/internal/declared.d.mts +13 -0
  28. package/dist/internal/declared.mjs +34 -0
  29. package/dist/internal/errors.d.mts +77 -0
  30. package/dist/internal/errors.mjs +79 -0
  31. package/dist/internal/implementation.d.mts +171 -41
  32. package/dist/internal/implementation.mjs +147 -29
  33. package/dist/internal/mcp.d.mts +11 -0
  34. package/dist/internal/mcp.mjs +52 -0
  35. package/dist/internal/memory.d.mts +6 -0
  36. package/dist/internal/memory.mjs +38 -0
  37. package/dist/internal/refusal.mjs +67 -0
  38. package/dist/internal/schema-errors.mjs +20 -0
  39. package/dist/internal/tools.mjs +27 -57
  40. package/docs/Action.md +401 -73
  41. package/docs/ActionCli.md +280 -51
  42. package/docs/ActionHttp.md +266 -82
  43. package/docs/ActionMcp.md +149 -104
  44. package/docs/ActionToolkit.md +97 -35
  45. package/docs/Authentication.md +369 -62
  46. package/docs/CONTEXT.md +23 -36
  47. package/docs/README.md +29 -73
  48. package/docs/Testing.md +154 -77
  49. package/docs/guarantees.md +81 -43
  50. package/docs/setup.md +90 -27
  51. package/package.json +7 -15
  52. package/dist/ActionCatalog.d.mts +0 -30
  53. package/dist/ActionCatalog.mjs +0 -39
  54. package/dist/ActionCliClient.d.mts +0 -48
  55. package/dist/ActionCliClient.mjs +0 -43
  56. package/dist/ActionGroup.d.mts +0 -44
  57. package/dist/ActionGroup.mjs +0 -35
  58. package/dist/ActionHttpClient.d.mts +0 -33
  59. package/dist/ActionHttpClient.mjs +0 -16
  60. package/dist/TestingClient.d.mts +0 -17
  61. package/dist/TestingClient.mjs +0 -22
  62. package/dist/internal/fetchClient.d.mts +0 -6
  63. package/dist/internal/fetchClient.mjs +0 -8
  64. package/dist/internal/tools.d.mts +0 -22
  65. package/docs/ActionCatalog.md +0 -54
  66. package/docs/ActionCliClient.md +0 -63
  67. package/docs/ActionGroup.md +0 -128
  68. package/docs/ActionHttpClient.md +0 -80
@@ -1,98 +1,125 @@
1
- import "./internal/implementation.mjs";
2
- import { TextField, bindTools } from "./internal/tools.mjs";
3
- import { Context, Effect, Layer, Option, Predicate, Schema } from "effect";
4
- import { McpProtocol, McpSchema, McpServer } from "effect/ai";
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 one protocol revision served. 2026-07-28 is stateless over HTTP: no
8
- * initialize handshake and no session, so every request stands alone.
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 protocols = [McpProtocol.v2026_07_28];
11
- /** The parts of a listed output schema a text field is removed from. */
12
- const decodeOutput = Schema.decodeUnknownOption(Schema.Struct({
13
- properties: Schema.Record(Schema.String, Schema.Json),
14
- required: Schema.optionalKey(Schema.Array(Schema.String))
15
- }));
16
- /** A JSON value that is an object: neither null, a scalar nor an array. */
17
- const isJsonObject = (value) => Predicate.isObject(value);
18
- /** Split a JSON object into one field and the rest. */
19
- const detach = (value, field) => {
20
- const { [field]: detached, ...rest } = value;
21
- return {
22
- detached,
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 listed output schema without the text field. Only a top-level property can be
28
- * detached; anything else fails the layer build rather than publish a schema the
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 outputWithout = (tool, field) => {
32
- const { properties: _, required: __, ...output } = tool.outputSchema ?? {};
33
- const listed = Option.getOrUndefined(decodeOutput(tool.outputSchema));
34
- if (listed === void 0 || !Object.hasOwn(listed.properties, field)) return Effect.die(`MCP tool '${tool.name}' cannot send '${field}' as text: it is not a top-level property of its success`);
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
- * A successful result with a string text field sent once, raw, before the structured
44
- * rest and its JSON copy. Any other result, such as a failure or a success without
45
- * the field, is the native one.
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 textResult = (result, field) => {
48
- const structured = result.structuredContent;
49
- if (result.isError === true || !isJsonObject(structured)) return result;
50
- const { detached, rest } = detach(structured, field);
51
- if (!Predicate.isString(detached)) return result;
52
- return new McpSchema.CallToolResult({
53
- ...result,
54
- structuredContent: rest,
55
- content: [{
56
- type: "text",
57
- text: detached
58
- }, {
59
- type: "text",
60
- text: JSON.stringify(rest)
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
- /** A native registration whose success sends `field` as text. */
65
- const withText = (registration, field) => Effect.map(outputWithout(registration.tool, field), (outputSchema) => ({
66
- ...registration,
67
- tool: new McpSchema.Tool({
68
- ...registration.tool,
69
- outputSchema
70
- }),
71
- handle: (payload) => Effect.map(registration.handle(payload), (result) => Predicate.isTagged(result, "InputRequired") ? result : textResult(result, field))
72
- }));
73
- const registration = (apps, options) => {
74
- const binding = bindTools(apps, "mcp", options);
75
- const registered = McpServer.registerToolkit(binding.toolkit).pipe(Effect.updateService(McpServer.McpServer, (server) => ({
76
- ...server,
77
- addTool: (tool) => {
78
- const field = Context.get(tool.annotations, TextField);
79
- return field === void 0 ? server.addTool(tool) : Effect.flatMap(withText(tool, field), server.addTool);
80
- }
81
- })));
82
- return Layer.effectDiscard(registered).pipe(Layer.provide(binding.layer));
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
- const server = (apps, options, transport) => registration(apps, options).pipe(Layer.provide(transport), Layer.fresh);
85
- function layerHttp(apps, options) {
86
- return server(apps, options, McpServer.layerHttp({
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
- protocols
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 layerStdio(apps, options) {
92
- return server(apps, options, McpServer.layerStdio({
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, layerStdio };
125
+ export { layerHttp, runStdio };
@@ -1,40 +1,69 @@
1
- import { Action_d_exports, Any, Codec } from "./Action.mjs";
2
- import { AnyImplementation, BuildContext, BuildError, HandlerContext, HiddenFromMcp, Implementation } from "./internal/implementation.mjs";
3
- import { SurfaceOptions } from "./internal/tools.mjs";
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
- /** What the in-process caller binds around the implementations it projects. */
8
- export type Options<Errors extends ReadonlyArray<Codec> = [], R = never> = SurfaceOptions<Errors, R>;
9
- /** A native tool corresponding to an action MCP may serve, named by its tool metadata. */
10
- type NativeTool<A extends Any, E extends Codec, R> = Extract<A["mcp"], object> extends {
11
- readonly name: infer Name extends string;
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> : never;
18
- type HandlersOf<App extends AnyImplementation> = App extends Implementation<any, infer H, any, any> ? H : never;
13
+ }, R>;
19
14
  /**
20
- * A tool needs what its handler needs, plus what the binding's `before` hook needs. The
21
- * rule is `ActionMcp`'s: only an action certainly hidden from MCP has no tool.
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 ToolForAction<App extends AnyImplementation, A extends Any, E extends Codec, RB> = A extends HiddenFromMcp ? never : NativeTool<A, E, HandlerContext<HandlersOf<App>, Extract<A["name"], keyof HandlersOf<App>>> | RB>;
24
- type ToolsFor<App extends AnyImplementation, E extends Codec, RB> = App extends AnyImplementation ? ToolForAction<App, App["group"]["actions"][number], E, RB> : never;
25
- type ToolkitTools<Apps extends ReadonlyArray<AnyImplementation>, E extends Codec, RB> = { readonly [T in ToolsFor<Apps[number], E, RB> as T["name"]]: T; };
26
- /** Native tools and the layer that binds their action implementations. */
27
- export interface Binding<Tools extends Record<string, Tool.Any>, E = never, R = never> {
28
- readonly toolkit: Toolkit.Toolkit<Tools>;
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<Tools>, E, R>;
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 ReadonlyArray<AnyImplementation>, const Errors extends ReadonlyArray<Codec> = [], RB = never>(apps: readonly [...Apps], options?: Options<Errors, RB>): Binding<ToolkitTools<Apps, Errors[number], RB>, BuildError<Apps[number], HiddenFromMcp>, BuildContext<Apps[number], HiddenFromMcp>>;
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
@@ -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
- return bindTools(apps, "native", options);
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 { Context, Effect } from "effect";
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 { NonEmptyReadonlyArray } from "effect/Array";
8
+ import { HttpApiSecurity } from "effect/http-api";
4
9
  //#region src/Authentication.d.ts
5
10
  /**
6
- * Authenticate each request and provide its identity to the downstream handler.
7
- * `authenticate` fails with the response to send instead, so the host owns its
8
- * status, body and challenge headers. Dependencies remain native router request
9
- * requirements. Acquired resources live until the request scope closes, including
10
- * while the handler is running. Every response is marked `Cache-Control: no-store`,
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 middleware: <I, A, R>(service: Context.Key<I, A>, authenticate: Effect.Effect<NoInfer<A>, HttpServerResponse.HttpServerResponse, R>) => HttpRouter.Middleware<{
14
- provides: I;
15
- handles: never;
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
- readonly authorizationServers: NonEmptyReadonlyArray<string>;
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
- /** Parameters of one `WWW-Authenticate: Bearer` challenge, RFC 6750 §3. */
30
- export interface BearerChallengeOptions {
31
- readonly error?: "invalid_token" | "insufficient_scope";
32
- readonly errorDescription?: string;
33
- /** Space-separated scopes needed for this request, not all supported scopes. */
34
- readonly scope?: string;
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
- * Publish RFC 9728 discovery independently from the authenticated routes.
38
- * This supplies metadata and challenges; the host still verifies access tokens.
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
- export declare const protectedResource: (options: ProtectedResourceOptions) => {
41
- layer: import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter>;
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
- //#endregion
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 };