@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
@@ -0,0 +1,79 @@
1
+ import { Effect, Predicate, Schema, SchemaIssue } from "effect";
2
+ //#region src/internal/errors.ts
3
+ /** A message field that defaults, so `new Forbidden()` needs no argument. */
4
+ const message = (fallback) => Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(fallback)));
5
+ /** Each built-in error's HTTP status, by tag: its annotation, and what a refusal answers. */
6
+ const statuses = {
7
+ InvalidInput: 400,
8
+ Unauthenticated: 401,
9
+ Forbidden: 403
10
+ };
11
+ /**
12
+ * One thing wrong with an input: where, as the keys and indexes leading to it from the input's
13
+ * root, none for the input itself, and what the schema expects there.
14
+ */
15
+ const Issue = Schema.Struct({
16
+ path: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
17
+ message: Schema.String
18
+ });
19
+ const standardIssues = SchemaIssue.makeFormatterStandardSchemaV1();
20
+ /**
21
+ * The request's input does not decode against the action's input. HTTP answers every such
22
+ * request with it, its message the schema's own description of what is wrong, and `issues`
23
+ * the same description an issue at a time, for a caller that points at a field. A handler
24
+ * failing with it for input that decodes but cannot be served may name its own.
25
+ */
26
+ var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput", {
27
+ message: message("The input does not match the action's input."),
28
+ issues: Schema.Array(Issue).pipe(Schema.withConstructorDefault(Effect.succeed([])))
29
+ }, { httpApiStatus: statuses.InvalidInput }) {
30
+ /**
31
+ * The `InvalidInput` for input that did not decode, as every surface answers it: the
32
+ * schema's message, and each of its issues by path, for code that decodes input of its own,
33
+ * such as a header or a route parameter. Neither carries a value of the input: decoding
34
+ * runs without `reportInput`, so no issue holds one to format. A symbol key is sent as its
35
+ * string form, `Symbol(name)`, as JSON has no symbols, which tells it from a string key of
36
+ * that name.
37
+ */
38
+ static fromSchemaError = ({ issue, message }) => new InvalidInput({
39
+ message,
40
+ issues: standardIssues(issue).issues.map((found) => ({
41
+ path: (found.path ?? []).map((segment) => {
42
+ const key = Predicate.isPropertyKey(segment) ? segment : segment.key;
43
+ return Predicate.isSymbol(key) ? String(key) : key;
44
+ }),
45
+ message: found.message
46
+ }))
47
+ });
48
+ };
49
+ /** The caller is not authenticated: authentication or an implementation's `authorize` refuses. */
50
+ var Unauthenticated = class extends Schema.TaggedError()("Unauthenticated", { message: message("Authentication is required.") }, { httpApiStatus: statuses.Unauthenticated }) {};
51
+ /** An OAuth scope token (RFC 6749 §3.3): printable ASCII but space, `"` and `\`. */
52
+ const scopeToken = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
53
+ const ScopeToken = Schema.String.check(Schema.isPattern(scopeToken));
54
+ /**
55
+ * The caller may not run this action: an implementation's `authorize` refuses. `scopes`,
56
+ * when given, are every OAuth scope the call needs: a refusal naming them is answered with
57
+ * the `insufficient_scope` challenge an OAuth client re-authorizes on.
58
+ */
59
+ var Forbidden = class extends Schema.TaggedError()("Forbidden", {
60
+ message: message("Not allowed."),
61
+ scopes: Schema.optionalKey(Schema.NonEmptyArray(ScopeToken))
62
+ }, { httpApiStatus: statuses.Forbidden }) {};
63
+ /** The refusals, as schemas. */
64
+ const refusals = [Unauthenticated, Forbidden];
65
+ /**
66
+ * What authentication or an implementation's `authorize` refuses a caller with, instead of
67
+ * running a handler. An error an action declares, such as a rate limit, is not a refusal. As a
68
+ * schema, it tells a refusal apart: `Schema.is(Action.Refusal)(error)`.
69
+ */
70
+ const Refusal = Schema.Union(refusals);
71
+ /**
72
+ * The failures every endpoint and tool declares beyond its action's own, and any handler may
73
+ * fail with: bad input, and every refusal. No action or binding declares them itself.
74
+ */
75
+ const builtIns = [InvalidInput, ...refusals];
76
+ /** The built-in failures as one schema: `Schema.is(Action.BuiltIn)(error)`. */
77
+ const BuiltIn = Schema.Union(builtIns);
78
+ //#endregion
79
+ export { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated, builtIns, scopeToken, statuses };
@@ -1,49 +1,179 @@
1
+ import { Anyone } from "./actions.mjs";
2
+ import { Refusal } from "./errors.mjs";
1
3
  import { Action_d_exports, Any } from "../Action.mjs";
2
- import { Actions } from "./actions.mjs";
3
- import { Effect, Scope } from "effect";
4
+ import { Context, Effect, Layer, Scope } from "effect";
4
5
  //#region src/internal/implementation.d.ts
5
- /** Decoded values at the adapter dispatch boundary. */
6
+ /** Decoded values at the dispatch boundary every surface shares. */
6
7
  export type ErasedValue = Any["input"]["Type"];
7
- type ErasedHandler<R> = {
8
+ export type ErasedHandler<R> = {
8
9
  handle(input: ErasedValue): Effect.Effect<ErasedValue, ErasedValue, R>;
9
10
  }["handle"];
10
- /** An adapter's erased view of a record of handlers with request requirements `R`. */
11
- export type Handlers<R> = Readonly<Record<string, ErasedHandler<R>>>;
12
- /** Per-request requirements of the handler selected by an action name. */
13
- export type HandlerContext<H, Name extends keyof H> = H[Name] extends ((input: never) => Effect.Effect<infer _A, infer _E, infer R>) ? R : never;
14
- /** The union of per-request requirements declared by a handler record. */
15
- export type HandlersContext<H> = { readonly [K in keyof H]: HandlerContext<H, K>; }[keyof H];
16
- /**
17
- * An acquired handler record for a group.
18
- *
19
- * The private field makes this class nominal: a structurally similar object,
20
- * including one made by spreading an implementation, is not an implementation.
21
- */
22
- export declare class Implementation<G extends Actions, H, EX, RX> {
11
+ /** Authorization runs only for protected actions, after authentication and before the handler. */
12
+ export type Authorize<A extends Any, R = never> = (action: A) => Effect.Effect<void, Refusal, R>;
13
+ /** An implementation's authorizer, erased. */
14
+ export type ErasedAuthorize = (action: Any) => Effect.Effect<void, unknown, unknown>;
15
+ /** Each action of an implementation with its handler, as its builder made them. */
16
+ export type Bound = ReadonlyArray<readonly [Any, ErasedHandler<unknown>]>;
17
+ /**
18
+ * What a layer builds once per layer graph, under a key private to it: an implementation's
19
+ * handlers, or its authorizer. Effect memoizes a layer by reference within one layer graph, so
20
+ * everything in the graph holding this one shares one run.
21
+ */
22
+ export interface Memoized<S, E, R> {
23
+ readonly key: Context.Key<S, S>;
24
+ readonly layer: Layer.Layer<S, E, R>;
25
+ }
26
+ /**
27
+ * Actions bound to their handlers and their authorizer: everything one `Action.implement` call
28
+ * produced, one builder serving every surface.
29
+ * The private fields make this class nominal: a structurally similar object, including one made by
30
+ * spreading an implementation, is not an implementation. Its type parameters may be written
31
+ * out, as a package emitting declarations prints them: `A` its actions, `RequestServices` each
32
+ * action name's handler's per-request requirements, `EX` and `RX` the failures and services of
33
+ * its handlers' builder, `RA` what its authorizer reads per request, and `EAX` and `RAX` the
34
+ * failures and services of the authorizer's builder. Public-only selections do not acquire
35
+ * authorization. A plain authorization callback builds nothing.
36
+ */
37
+ export declare class Implementation<A extends Any, RequestServices extends {
38
+ readonly [name: string]: unknown;
39
+ }, EX, RX, RA = never, EAX = never, RAX = never> {
23
40
  #private;
24
- /** The group whose exact handler record was built. */
25
- readonly group: G;
26
- private constructor();
27
- /** Acquire the exact handler record in the adapter layer's scope. */
28
- get build(): Effect.Effect<H, EX, RX | Scope.Scope>;
29
- static make<G extends Actions, H, EX, RX>(group: G, build: Effect.Effect<H, EX, RX | Scope.Scope>): Implementation<G, H, EX, RX>;
41
+ /** The contracts this implementation answers. */
42
+ readonly actions: ReadonlyArray<A>;
43
+ /** Type-only: each action's handler's per-request requirements. */
44
+ readonly "~request": RequestServices;
45
+ /** Type-only: what building its handlers fails with. */
46
+ readonly "~buildError": EX;
47
+ /** Type-only: what building its handlers needs. */
48
+ readonly "~buildServices": RX;
49
+ /** Type-only: what its authorizer reads per request. */
50
+ readonly "~authorizeRequest": RA;
51
+ /** Type-only: what building its authorizer fails with. */
52
+ readonly "~authorizeBuildError": EAX;
53
+ /** Type-only: what building its authorizer needs. */
54
+ readonly "~authorizeBuildServices": RAX;
55
+ constructor(
56
+ /** The contracts this implementation answers. */
57
+ actions: ReadonlyArray<A>,
58
+ /** Builds each action paired with its handler: its own builder, or the one it shares. */
59
+ handlers: Memoized<Bound, EX, RX>,
60
+ /** Builds its authorizer. */
61
+ authorizer: Memoized<ErasedAuthorize, EAX, RAX>);
62
+ /** Select contracts while retaining their handlers, authorization and shared builder. */
63
+ static share(actions: ReadonlyArray<Any>, app: AnyImplementation): AnyImplementation;
64
+ /**
65
+ * The key of what runs a call of `app`'s actions: its handlers' memoized layer's, which each
66
+ * `implement` makes, so it is unique to the implementation. Every selection retains it.
67
+ */
68
+ static runKey(app: AnyImplementation): string;
69
+ /**
70
+ * The builders of `app`, its handlers' and its authorizer's, as a layer. Effect memoizes each
71
+ * by reference within one layer graph, so every surface serving `app` there, and every
72
+ * implementation sharing its builder, shares one run. Static, so it stays off the
73
+ * public instance type.
74
+ */
75
+ static layerOf(app: AnyImplementation): Layer.Layer<never, unknown, unknown>;
76
+ /**
77
+ * The actions of `app` with their handlers, behind its authorizer, from the context
78
+ * `layerOf(app)` provides.
79
+ */
80
+ static boundOf(app: AnyImplementation): Effect.Effect<Bound, never, unknown>;
81
+ /** `app`, if this copy of the module made it; another installed copy's cannot be read. */
82
+ private static own;
83
+ }
84
+ /** Any implementation, with its actions and channels erased. */
85
+ export type AnyImplementation<A extends Any = Any> = Implementation<A, {
86
+ readonly [name: string]: unknown;
87
+ }, unknown, unknown, unknown, unknown, unknown>;
88
+ /** What a surface serves: one implementation, or a list of them. */
89
+ export type Served = AnyImplementation | ReadonlyArray<AnyImplementation>;
90
+ /** The implementations `S` stands for. */
91
+ export type Member<S> = S extends ReadonlyArray<infer App> ? App : S;
92
+ /** Options that may list actions of `A`, the shape every selecting surface's options share. */
93
+ export interface Selection<A extends Any> {
94
+ readonly actions?: ReadonlyArray<A> | undefined;
30
95
  }
31
- /** Any nominal implementation of `G`, with its record and channels erased. */
32
- export type AnyImplementation<G extends Actions = Actions> = Implementation<G, any, any, any>;
33
- /** What hides an action from MCP and Toolkit types: an `mcp` type of exactly `false`. The one rule both use. */
34
- export type HiddenFromMcp = {
35
- readonly mcp: false;
36
- };
37
- /**
38
- * Names of the actions of `G` a surface serves: all of them, less those matching
39
- * `Hidden`. An action whose `mcp` may be `false` at runtime does not match, so it keeps
40
- * its requirements.
41
- */
42
- type ServedNames<G extends Actions, Hidden> = Exclude<G["actions"][number], Hidden>["name"];
43
- /** Per-request requirements of the handlers a surface can invoke, or of a union of implementations. */
44
- export type RequestContext<App, Hidden = never> = App extends Implementation<infer G, infer H, any, any> ? { readonly [K in Extract<ServedNames<G, Hidden>, keyof H>]: HandlerContext<H, K>; }[Extract<ServedNames<G, Hidden>, keyof H>] : never;
45
- /** Handler-acquisition failures of the implementations a surface acquires, or of a union of them. */
46
- export type BuildError<App, Hidden = never> = App extends Implementation<infer G, any, infer EX, any> ? [ServedNames<G, Hidden>] extends [never] ? never : EX : never;
47
- /** Handler-acquisition requirements of the implementations a surface acquires, or of a union of them. */
48
- export type BuildContext<App, Hidden = never> = App extends Implementation<infer G, any, any, infer RX> ? [ServedNames<G, Hidden>] extends [never] ? never : RX : never;
96
+ /**
97
+ * The actions options `O` select, of `All`: those its `actions` lists when it always lists
98
+ * them, and every one otherwise. Options whose `actions` may be absent, being optional or
99
+ * a union with options lacking it, select every action at run time, so they narrow nothing.
100
+ */
101
+ export type Selected<O, All extends Any> = O extends {
102
+ readonly actions: ReadonlyArray<infer A extends Any>;
103
+ } ? A : All;
104
+ /** The actions options `O` select of the implementations `Apps` stand for. */
105
+ export type SelectedOf<O, Apps extends Served> = Selected<O, ActionOf<Member<Apps>>>;
106
+ /**
107
+ * The actions a client of options `O` offers methods for, of `All`: those `actions` may
108
+ * list, wherever some member of `O` may list them, as each is present whether the list is
109
+ * given or not; every one where no member lists any.
110
+ */
111
+ export type Offered<O, All extends Any> = [Listing<O>] extends [never] ? All : Extract<All, Listing<O> extends ReadonlyArray<infer A extends Any> ? A : never>;
112
+ /** The lists of actions the members of `O` may give; none where no member gives one. */
113
+ type Listing<O> = O extends unknown ? NonNullable<O["actions" & keyof O]> : never;
114
+ /**
115
+ * No key of any member of `O` beyond those of `Keys`, so a misspelled option is refused
116
+ * rather than ignored: options inferred whole, as a type parameter, skip TypeScript's own
117
+ * check, and `keyof` a union holds only the keys every member has.
118
+ */
119
+ export type Known<O, Keys> = { readonly [K in Exclude<O extends unknown ? keyof O : never, keyof Keys>]?: never; };
120
+ /**
121
+ * The parameters of an optional options argument of type `O`: required where `O` names what
122
+ * only a given argument holds, such as listed actions, so explicit type arguments cannot
123
+ * narrow what an omitted argument leaves whole.
124
+ */
125
+ export type OptionalUnless<O, Given> = {} extends O ? [options?: Given] : [options: Given];
126
+ /** The actions of the implementations a surface serves. */
127
+ export type ActionOf<App> = App extends {
128
+ readonly actions: ReadonlyArray<infer A>;
129
+ } ? A : never;
130
+ /** Whether `A` may be one of `Listed`: either is assignable to the other. */
131
+ type Matches<A extends Any, Listed extends Any> = Listed extends Any ? [A] extends [Listed] ? true : [Listed] extends [A] ? true : never : never;
132
+ /**
133
+ * The actions of `App` a surface given the actions `Listed` may serve, as `select` selects
134
+ * them by identity: an action whose type either is assignable to, or is assigned by, one
135
+ * listed. So an implementation typed wider than its contract, or a list typed wider than its
136
+ * actions, an erased one included, still counts, and another contract of a listed name, which
137
+ * the surface leaves out beside an action it serves, does not.
138
+ */
139
+ export type Serving<App, Listed extends Any> = ActionOf<App> extends (infer A extends Any) ? A extends unknown ? [A] extends [Listed] ? A : string extends A["name"] ? A : [Extract<A["name"], Listed["name"]>] extends [never] ? never : [Matches<A, Listed>] extends [never] ? never : A : never : never;
140
+ /**
141
+ * What each implementation among `App` owes per request where a surface serving the actions
142
+ * `Listed` serves it: its authorizer's services, and the handlers' of the actions it serves.
143
+ */
144
+ export type ServedRequest<App, Listed extends Any> = App extends unknown ? RequestOf<App, Serving<App, Listed>> : never;
145
+ /** The implementations among `App` holding an action of `Listed`: those a surface builds. */
146
+ export type Holding<App, Listed extends Any> = App extends unknown ? [Serving<App, Listed>] extends [never] ? never : App : never;
147
+ /** The protected members of a contract union. */
148
+ export type Protected<A extends Any> = A extends unknown ? A["caller"] extends typeof Anyone ? never : A : never;
149
+ type ServiceOf<K> = K extends Context.Key<infer I, unknown> ? I : never;
150
+ /**
151
+ * The identity required by the contract, even when its handler never reads it. A `caller`
152
+ * narrowed to `Anyone`, such as `Action.Any & { caller: typeof Anyone }`'s, which TypeScript
153
+ * keeps as `typeof Anyone | (Key & typeof Anyone)`, requires none.
154
+ */
155
+ export type AuthenticationOf<A extends Any> = A["caller"] extends typeof Anyone ? never : ServiceOf<A["caller"]>;
156
+ /**
157
+ * Per-request requirements of `App`'s authorization and handler for each `A` it
158
+ * implements: those of every name `A`'s may be, so a union or an erased name owes each it may
159
+ * stand for.
160
+ */
161
+ export type RequestOf<App, A extends Any> = App extends {
162
+ readonly "~request": infer R;
163
+ readonly "~authorizeRequest": infer RA;
164
+ } ? A extends Any ? [A["name"] & keyof R] extends [never] ? never : R[A["name"] & keyof R] | (A["caller"] extends typeof Anyone ? never : RA) | AuthenticationOf<A> : never : never;
165
+ type OwnActions<App> = Extract<ActionOf<App>, Any>;
166
+ /** Builder failures include authorization only if selected protected actions need it. */
167
+ export type BuildError<App, Listed extends Any = OwnActions<App>> = App extends {
168
+ readonly "~buildError": infer EX;
169
+ readonly "~authorizeBuildError": infer EAX;
170
+ } ? EX | ([Protected<Serving<App, Listed>>] extends [never] ? never : EAX) : never;
171
+ /**
172
+ * What a surface serving the actions `Listed` reads at startup: the services the builders of
173
+ * their handlers and authorization read.
174
+ */
175
+ export type BuildServices<App, Listed extends Any = OwnActions<App>> = App extends {
176
+ readonly "~buildServices": infer RX;
177
+ readonly "~authorizeBuildServices": infer RAX;
178
+ } ? RX | ([Protected<Serving<App, Listed>>] extends [never] ? never : RAX) : never;
49
179
  //#endregion
@@ -1,49 +1,167 @@
1
- import { Effect, Predicate } from "effect";
1
+ import { Unauthenticated } from "./errors.mjs";
2
+ import { Anyone } from "./actions.mjs";
3
+ import { Array, Context, Effect, Layer, Option } from "effect";
2
4
  //#region src/internal/implementation.ts
3
5
  /**
4
- * An acquired handler record for a group.
5
- *
6
- * The private field makes this class nominal: a structurally similar object,
7
- * including one made by spreading an implementation, is not an implementation.
6
+ * A random key segment, unique to its caller even across installed copies of this module,
7
+ * where a counter of each copy would repeat another's.
8
+ */
9
+ const uniqueKey = () => Math.random().toString(36).slice(2);
10
+ /** What `build` makes, as a layer of its own. */
11
+ const memoized = (build) => {
12
+ const key = Context.Service(`effect-actions/Implementation/${uniqueKey()}`);
13
+ return {
14
+ key,
15
+ layer: Layer.effect(key, build)
16
+ };
17
+ };
18
+ /**
19
+ * Actions bound to their handlers and their authorizer: everything one `Action.implement` call
20
+ * produced, one builder serving every surface.
21
+ * The private fields make this class nominal: a structurally similar object, including one made by
22
+ * spreading an implementation, is not an implementation. Its type parameters may be written
23
+ * out, as a package emitting declarations prints them: `A` its actions, `RequestServices` each
24
+ * action name's handler's per-request requirements, `EX` and `RX` the failures and services of
25
+ * its handlers' builder, `RA` what its authorizer reads per request, and `EAX` and `RAX` the
26
+ * failures and services of the authorizer's builder. Public-only selections do not acquire
27
+ * authorization. A plain authorization callback builds nothing.
8
28
  */
9
29
  var Implementation = class Implementation {
10
- group;
11
- #build;
12
- constructor(group, build) {
13
- this.group = group;
14
- this.#build = build;
30
+ actions;
31
+ #handlers;
32
+ #authorizer;
33
+ constructor(actions, handlers, authorizer) {
34
+ this.actions = actions;
35
+ this.#handlers = handlers;
36
+ this.#authorizer = authorizer;
15
37
  }
16
- /** Acquire the exact handler record in the adapter layer's scope. */
17
- get build() {
18
- return this.#build;
38
+ /** Select contracts while retaining their handlers, authorization and shared builder. */
39
+ static share(actions, app) {
40
+ const source = Implementation.own(app);
41
+ return new Implementation(actions, source.#handlers, source.#authorizer);
19
42
  }
20
- static make(group, build) {
21
- return new Implementation(group, build);
43
+ /**
44
+ * The key of what runs a call of `app`'s actions: its handlers' memoized layer's, which each
45
+ * `implement` makes, so it is unique to the implementation. Every selection retains it.
46
+ */
47
+ static runKey(app) {
48
+ return Implementation.own(app).#handlers.key.key;
49
+ }
50
+ /**
51
+ * The builders of `app`, its handlers' and its authorizer's, as a layer. Effect memoizes each
52
+ * by reference within one layer graph, so every surface serving `app` there, and every
53
+ * implementation sharing its builder, shares one run. Static, so it stays off the
54
+ * public instance type.
55
+ */
56
+ static layerOf(app) {
57
+ const source = Implementation.own(app);
58
+ return app.actions.some((action) => action.caller !== Anyone) ? Layer.merge(source.#handlers.layer, source.#authorizer.layer) : source.#handlers.layer;
59
+ }
60
+ /**
61
+ * The actions of `app` with their handlers, behind its authorizer, from the context
62
+ * `layerOf(app)` provides.
63
+ */
64
+ static boundOf(app) {
65
+ const { actions } = app;
66
+ const source = Implementation.own(app);
67
+ return Effect.gen(function* () {
68
+ const bound = yield* source.#handlers.key;
69
+ const before = actions.some((action) => action.caller !== Anyone) ? yield* source.#authorizer.key : () => Effect.void;
70
+ return bound.filter(([action]) => actions.includes(action)).map(([action, handle]) => [action, dispatch(action, handle, before)]);
71
+ });
22
72
  }
73
+ /** `app`, if this copy of the module made it; another installed copy's cannot be read. */
74
+ static own(app) {
75
+ if (!(#handlers in app)) throw new Error("Not an implementation made by this Action.implement: is effect-actions installed twice?");
76
+ return app;
77
+ }
78
+ };
79
+ /** A surface's implementations as a list. */
80
+ const toList = (served) => Array.ensure(served);
81
+ /**
82
+ * Refuse an action of `listed` that `held` lacks, by identity, as a stale list, marking one
83
+ * of a held name as another contract's.
84
+ */
85
+ const assertHeld = (why, listed, held) => {
86
+ const missing = listed.filter((action) => !held.includes(action));
87
+ if (missing.length > 0) {
88
+ const names = new Set(held.map(({ name }) => name));
89
+ throw new Error(`Listed in actions, but ${why}: ${missing.map(({ name }) => names.has(name) ? `${name} (another contract)` : name).join(", ")}`);
90
+ }
91
+ };
92
+ /**
93
+ * Each of `apps` narrowed to the actions `listed` holds, matched by identity, behind its own
94
+ * authorization and sharing its builder; one holding none of them is
95
+ * left out, and not built. All of `apps` as they are when nothing is listed. A listed action
96
+ * none of them holds is refused, as a stale list or another contract of its name.
97
+ */
98
+ const select = (apps, listed) => {
99
+ if (listed === void 0) return apps;
100
+ assertHeld("no implementation holds it", listed, apps.flatMap((app) => app.actions));
101
+ return apps.flatMap((app) => {
102
+ const own = app.actions.filter((action) => listed.includes(action));
103
+ if (own.length === 0) return [];
104
+ return own.length === app.actions.length ? [app] : [Implementation.share(own, app)];
105
+ });
106
+ };
107
+ /**
108
+ * Provide `layer` the handlers of `apps`. Each implementation's layer is memoized, so its
109
+ * builder runs once per layer graph however many surfaces serve it.
110
+ */
111
+ const provideHandlers = (apps) => (layer) => {
112
+ const [first, ...rest] = apps.map((app) => Implementation.layerOf(app));
113
+ return first === void 0 ? layer : Layer.provide(layer, Layer.mergeAll(first, ...rest));
114
+ };
115
+ /**
116
+ * The builders of `apps` as one layer providing nothing: built above the surfaces, it runs
117
+ * each builder once for all of them, since every surface builds the same memoized layers.
118
+ */
119
+ const builders = (apps) => Layer.empty.pipe(provideHandlers(apps));
120
+ /**
121
+ * Every action `apps` serve with its handler, behind its authorizer, as `provideHandlers` built
122
+ * them.
123
+ */
124
+ const acquire = (apps) => Effect.map(Effect.forEach(apps, (app) => Implementation.boundOf(app)), (bound) => bound.flat());
125
+ /**
126
+ * Every action `apps` serve with its handler, behind its authorizer, their builders built as layers
127
+ * of the graph being built around the caller, into its memo map, as `HttpRouter` builds a
128
+ * middleware's dependencies: a builder acquiring them shares each builder's one run with the
129
+ * surfaces of its graph, whichever builds first. Outside any graph, a program under
130
+ * `Effect.provide` shares that layer's, and one under none builds into a map of its own. The
131
+ * caller's scope holds them; a surface serving them too keeps them until it is released.
132
+ */
133
+ const built = (apps) => {
134
+ const [first, ...rest] = apps.map((app) => Implementation.layerOf(app));
135
+ return Effect.gen(function* () {
136
+ if (first === void 0) return [];
137
+ const current = yield* Effect.serviceOption(Layer.CurrentMemoMap);
138
+ const memoMap = Option.getOrElse(current, Layer.makeMemoMapUnsafe);
139
+ const scope = yield* Effect.scope;
140
+ const context = yield* Layer.buildWithMemoMap(Layer.mergeAll(first, ...rest), memoMap, scope);
141
+ return yield* Effect.provideContext(acquire(apps), context);
142
+ });
23
143
  };
24
144
  /**
25
- * Select the handler of a concrete action, once, when an adapter binds its acquired
26
- * record; `implement` has already refused a record without it. The returned function
27
- * invokes it: the surface's pre-handler hook runs first, outside the action's span, so a
28
- * refusal is attributed to the surface rather than to a handler that never ran.
29
- * `R` remains in the returned effect so transport layers cannot erase required
30
- * per-request services while assembling routes.
145
+ * One action's handler behind its admission: a protected action's identity, then its
146
+ * `authorize`. These run outside the action's span, so what they fail with is attributed to
147
+ * the surface rather than to a handler that never ran. Each call has a scope of its own, on
148
+ * every surface: what `authorize` and the handler acquire is released when the call ends, the
149
+ * handler's first, so no call needs a `Scope` of its caller.
31
150
  */
32
- const dispatch = (group, action, handlers, before) => {
33
- const handle = Object.hasOwn(handlers, action.name) ? handlers[action.name] : void 0;
34
- if (!Predicate.isFunction(handle)) throw new Error(`No handler for ${group.name}.${action.name}`);
151
+ const dispatch = (action, handle, before) => {
35
152
  const attributes = {
36
- "action.group": group.name,
37
153
  "action.name": action.name,
38
- "action.access": action.access
154
+ "action.read_only": action.readOnly
39
155
  };
156
+ const authentication = action.caller === Anyone ? Effect.void : Effect.flatMap(Effect.serviceOption(action.caller), (actor) => Option.isSome(actor) ? Effect.void : Effect.fail(new Unauthenticated()));
157
+ const authorization = action.caller === Anyone ? Effect.void : Effect.suspend(() => before(action));
40
158
  return (input) => {
41
- const handled = Effect.withSpan(Effect.annotateLogs(Effect.suspend(() => handle(input)), attributes), `${group.name}.${action.name}`, {
159
+ const handled = Effect.withSpan(Effect.annotateLogs(Effect.suspend(() => handle(input)), attributes), action.name, {
42
160
  captureStackTrace: false,
43
161
  attributes
44
162
  });
45
- return before === void 0 ? handled : Effect.flatMap(before(action), () => handled);
163
+ return Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(handled)));
46
164
  };
47
165
  };
48
166
  //#endregion
49
- export { Implementation, dispatch };
167
+ export { Implementation, acquire, assertHeld, builders, built, memoized, provideHandlers, select, toList, uniqueKey };
@@ -0,0 +1,11 @@
1
+ import { Schema } from "effect";
2
+ import { McpProtocol } from "effect/ai";
3
+ //#region src/internal/mcp.d.ts
4
+ /** An MCP request's parameters: JSON, leaving out an `undefined` one, as a caller may. */
5
+ export interface Params {
6
+ readonly _meta?: {
7
+ readonly [key: string]: Schema.Json;
8
+ };
9
+ readonly [key: string]: Schema.Json | undefined;
10
+ }
11
+ //#endregion
@@ -0,0 +1,52 @@
1
+ import { Predicate } from "effect";
2
+ import { McpProtocol } from "effect/ai";
3
+ //#region src/internal/mcp.ts
4
+ /**
5
+ * The one protocol revision served over HTTP. 2026-07-28 is stateless: every request
6
+ * stands alone. The stateful revisions keep a session per `initialize`, which the native
7
+ * HTTP runtime never expires and which no identity owns. A mixed endpoint's gate relies on it
8
+ * too: it decides from the routing headers, which the native runtime checks against the body
9
+ * of a stateless request alone, so serving a stateful revision over HTTP would open the gate.
10
+ */
11
+ const httpProtocol = McpProtocol.v2026_07_28;
12
+ /** Where `ActionMcp.layerHttp` serves, and `Testing.mcpClient` calls, by default. */
13
+ const defaultPath = "/mcp";
14
+ /** The parameter a method's `mcp-name` header repeats, as the native runtime routes it. */
15
+ const routed = (method, params) => method === "resources/read" ? params.uri : params.name;
16
+ /**
17
+ * One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it:
18
+ * the headers routing it, which repeat what its body says, and its JSON-RPC body. The client
19
+ * metadata goes in `_meta`, under any given, such as a `progressToken`; the protocol version
20
+ * is always the request's own.
21
+ */
22
+ const statelessRequest = (method, params) => {
23
+ const { protocolVersion } = httpProtocol;
24
+ const name = routed(method, params);
25
+ return {
26
+ headers: {
27
+ accept: "application/json, text/event-stream",
28
+ "mcp-protocol-version": protocolVersion,
29
+ "mcp-method": method,
30
+ ...Predicate.isString(name) ? { "mcp-name": name } : {}
31
+ },
32
+ body: {
33
+ jsonrpc: "2.0",
34
+ id: 1,
35
+ method,
36
+ params: {
37
+ ...params,
38
+ _meta: {
39
+ "io.modelcontextprotocol/clientCapabilities": {},
40
+ "io.modelcontextprotocol/clientInfo": {
41
+ name: "effect-actions",
42
+ version: "0"
43
+ },
44
+ ...params._meta,
45
+ "io.modelcontextprotocol/protocolVersion": protocolVersion
46
+ }
47
+ }
48
+ }
49
+ };
50
+ };
51
+ //#endregion
52
+ export { defaultPath, httpProtocol, statelessRequest };
@@ -0,0 +1,6 @@
1
+ import { Layer } from "effect";
2
+ import { HttpClient, HttpRouter, HttpServer } from "effect/http";
3
+ //#region src/internal/memory.d.ts
4
+ /** 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
@@ -0,0 +1,38 @@
1
+ import { Effect, Layer, Predicate, Stream } from "effect";
2
+ import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/http";
3
+ //#region src/internal/memory.ts
4
+ /** Where relative request URLs resolve, so a client needs no `baseUrl`. */
5
+ const origin = "http://localhost/";
6
+ /**
7
+ * `request`, a streamed body turned into a web stream in the current context. The native client
8
+ * turns it in its send, under the in-memory `fetch`, where another client the stream calls
9
+ * would send to the routes too.
10
+ */
11
+ const pulled = (request) => {
12
+ const body = request.body;
13
+ if (!Predicate.isTagged(body, "Stream")) return Effect.succeed(request);
14
+ return Effect.map(Stream.toReadableStreamEffect(body.stream), (readable) => HttpClientRequest.setBody(request, HttpBody.raw(readable, {
15
+ contentType: body.contentType,
16
+ contentLength: body.contentLength
17
+ })).pipe(HttpClientRequest.updateHeaders(() => request.headers)));
18
+ };
19
+ /**
20
+ * The native `HttpClient`, sending every request to `handler` instead of the network. A
21
+ * relative URL resolves against `http://localhost/`, as a page there would resolve it, and a
22
+ * request carries the `Host` header of its URL, as one over the network does. The client is
23
+ * its own: the program's other clients keep their `fetch`, and get none of its requests.
24
+ */
25
+ const clientOf = (handler) => {
26
+ const fetch = (input, init) => {
27
+ const request = new Request(input, init);
28
+ if (!request.headers.has("host")) request.headers.set("host", new URL(request.url).host);
29
+ return handler(request);
30
+ };
31
+ return Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, (native) => HttpClient.makeWith((request) => Effect.flatMap(request, (mapped) => {
32
+ const resolved = URL.canParse(mapped.url) ? null : URL.parse(mapped.url, origin);
33
+ const sent = resolved === null ? mapped : HttpClientRequest.setUrl(mapped, resolved.href);
34
+ return Effect.flatMap(pulled(sent), (ready) => native.postprocess(Effect.succeed(ready)).pipe(Effect.provideService(FetchHttpClient.Fetch, fetch)));
35
+ }), native.preprocess))).pipe(Layer.provide(Layer.fresh(FetchHttpClient.layer)));
36
+ };
37
+ //#endregion
38
+ export { clientOf };