@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.
- 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 +7 -15
- 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/docs/Action.md
CHANGED
|
@@ -1,35 +1,83 @@
|
|
|
1
1
|
# Action
|
|
2
2
|
|
|
3
|
-
One contract: a name, schemas for input, success and declared errors,
|
|
4
|
-
|
|
3
|
+
One contract: a name, schemas for input, success and declared errors, whether it is read-only
|
|
4
|
+
(`readOnly`), who may call it (`caller`), and how its tool presents itself over MCP (`mcp`). A
|
|
5
|
+
contract holds no behavior;
|
|
6
|
+
`implement` binds handlers to contracts, with the authorization every surface runs before a
|
|
7
|
+
protected action's handler: whether this caller may call it. Every surface that runs handlers
|
|
8
|
+
takes an implementation or a list of them, and so does `client`, which calls them in process
|
|
9
|
+
with the methods `ActionHttp.client` has. The module also exports the built-in errors every
|
|
10
|
+
surface declares and any handler may fail with.
|
|
5
11
|
|
|
6
12
|
## API
|
|
7
13
|
|
|
8
14
|
Import `@gjermundgaraba/effect-actions/Action`.
|
|
9
15
|
|
|
10
|
-
| Export
|
|
11
|
-
|
|
|
12
|
-
| `make(name, options)`
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
16
|
+
| Export | Purpose |
|
|
17
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `make(name, options)` | Define a pure contract; literal names, `readOnly` and `caller` stay typed. |
|
|
19
|
+
| `byName(actions)` | A list of actions keyed by name, each its exact contract: `contracts.getUser`. |
|
|
20
|
+
| `implement(action, handler, { authorize })` | Bind one handler, and a protected action's authorizer; returns one `Implementation`. |
|
|
21
|
+
| `implement([actions], handlers, { authorize })` | Bind a record of handlers keyed by action name; returns one `Implementation` of all. |
|
|
22
|
+
| `implement(actions, builder, options?)` | Either form, with an Effect that builds the handler or record once per layer graph. |
|
|
23
|
+
| `allowAll` | The authorizer without an action-level rule: every authenticated caller may call. |
|
|
24
|
+
| `Anyone` | The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not. |
|
|
25
|
+
| `layer(implementations)` | Their builders as one layer: provided above every surface, each runs once for all. |
|
|
26
|
+
| `client(implementations, { actions? })` | An Effect of a caller running them in process: `client.<action>(input)`. |
|
|
27
|
+
| `InvalidInput`, `Unauthenticated`, `Forbidden` | Built-in errors: 400, 401, 403, body `{ _tag, message }`, `InvalidInput`'s with `issues`; `message` defaults. |
|
|
28
|
+
| `InvalidInput.fromSchemaError(error)` | The `InvalidInput` every surface answers a `Schema.SchemaError` with, `issues` given. |
|
|
29
|
+
| `Issue` | One of `InvalidInput`'s `issues`: a `path` into the input and a `message`. |
|
|
30
|
+
| `Refusal` | `Unauthenticated \| Forbidden`, schema and type: what authentication or an authorizer refuses with. |
|
|
31
|
+
| `BuiltIn` | `InvalidInput \| Refusal`, schema and type: what any handler may fail with. |
|
|
32
|
+
| `Errors` | A list of error schemas, as `error` holds it: `<E extends Action.Errors>`. |
|
|
33
|
+
| `Action`, `Any`, `Implementation` | Concrete and erased contracts, and bound implementations. |
|
|
34
|
+
| `Client<Apps>` | What `client` gives for the implementations `Apps`, one or a list. |
|
|
35
|
+
| `AnyImplementation`, `AnyImplementation<A>` | Any implementation, or any of actions `A`, erased: a generic helper's constraint. |
|
|
36
|
+
| `BuildServices`, `BuildError` | What their builders read and fail with at startup, which a surface serving them owes. |
|
|
37
|
+
| `implementation.actions` | Its exact contract values, as `Testing.mcpClient(users.actions)` takes them. |
|
|
38
|
+
| `Handler`, `Authorize` | Typed handlers, and authorizers `(action) => Effect<void, Refusal, R>`. |
|
|
39
|
+
| `Handlers` | A list's handlers keyed by name, for a builder written apart from `implement`. |
|
|
40
|
+
| `Options`, `ClientOptions`, `Mcp` | What `make` and `client` take, and `make`'s `mcp` options. |
|
|
15
41
|
|
|
16
|
-
| Option
|
|
17
|
-
|
|
|
18
|
-
| `description
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
42
|
+
| Option | Meaning |
|
|
43
|
+
| ------------- | ---------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `description` | Required description. |
|
|
45
|
+
| `readOnly` | Required: whether the action leaves its resource unchanged. |
|
|
46
|
+
| `caller` | Required: `Action.Anyone`, or the identity service a caller must have, such as `CurrentActor`. |
|
|
47
|
+
| `input` | Optional schema or fields; omitted or `{}`, an empty object. |
|
|
48
|
+
| `success` | Optional schema or fields; omission means `Schema.Void`. |
|
|
49
|
+
| `error` | Declared error codecs: one schema or a list; defaults to none. |
|
|
50
|
+
| `mcp` | The tool's MCP hints, in MCP's names, its `title` and `_meta`. |
|
|
22
51
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
`authorize` is required where the target holds a protected action, and refused where it holds
|
|
53
|
+
none. It receives the selected action and fails only with a `Refusal`; every surface runs it
|
|
54
|
+
for protected actions, after authentication and input decoding
|
|
55
|
+
([guarantees.md](guarantees.md#authorization)). It may instead be an Effect that builds it, as a
|
|
56
|
+
builder builds handlers. Authentication is the surface's: a remote one verifies the identity a
|
|
57
|
+
protected contract declares through the descriptor its binding or endpoint names
|
|
58
|
+
([Authentication.md](Authentication.md)); on a local one the host provides it.
|
|
59
|
+
|
|
60
|
+
`input` and `success` take a schema or plain fields: `{ id: Schema.String }` is
|
|
61
|
+
`Schema.Struct({ id: Schema.String })`. A tool is read-only exactly when its action is
|
|
62
|
+
(`readOnlyHint`). `mcp` takes MCP's own hints, `destructiveHint`, `idempotentHint` and
|
|
63
|
+
`openWorldHint`, each MCP's default when left out, but `destructiveHint`, which is `false` for a
|
|
64
|
+
read-only action. The tool is named after the action. Handlers receive decoded input and return
|
|
65
|
+
decoded success, failing only with declared errors and the built-in ones.
|
|
27
66
|
|
|
28
67
|
## Canonical
|
|
29
68
|
|
|
30
|
-
|
|
69
|
+
Contracts are pure values, safe to import in a browser. A protected contract names its identity,
|
|
70
|
+
a `Context.Service` declared without a verifier.
|
|
71
|
+
|
|
72
|
+
```ts example=contracts.ts
|
|
31
73
|
import { Schema } from "effect";
|
|
32
74
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
75
|
+
import { CurrentActor } from "./authorization.js";
|
|
76
|
+
|
|
77
|
+
export const User = Schema.Struct({
|
|
78
|
+
id: Schema.String,
|
|
79
|
+
name: Schema.String,
|
|
80
|
+
});
|
|
33
81
|
|
|
34
82
|
export class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
35
83
|
"UserNotFound",
|
|
@@ -37,85 +85,365 @@ export class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
|
37
85
|
{ httpApiStatus: 404 },
|
|
38
86
|
) {}
|
|
39
87
|
|
|
40
|
-
//
|
|
41
|
-
|
|
88
|
+
// Reachable without credentials: it must work before anyone has signed in.
|
|
89
|
+
export const Status = Action.make("status", {
|
|
90
|
+
description: "Report whether the service is up.",
|
|
91
|
+
success: { service: Schema.String, users: Schema.Finite },
|
|
92
|
+
readOnly: true,
|
|
93
|
+
caller: Action.Anyone,
|
|
94
|
+
});
|
|
95
|
+
|
|
42
96
|
export const GetUser = Action.make("getUser", {
|
|
43
97
|
description: "Look up a user in your tenant.",
|
|
44
|
-
input:
|
|
45
|
-
success:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
98
|
+
input: { id: Schema.String },
|
|
99
|
+
success: User,
|
|
100
|
+
error: UserNotFound,
|
|
101
|
+
readOnly: true,
|
|
102
|
+
caller: CurrentActor,
|
|
49
103
|
});
|
|
50
104
|
|
|
51
|
-
// A write. Every action says which it is; there is no default to fall back on.
|
|
52
105
|
export const RenameUser = Action.make("renameUser", {
|
|
53
106
|
description: "Rename a user in your tenant.",
|
|
54
|
-
input:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
107
|
+
input: {
|
|
108
|
+
id: Schema.String,
|
|
109
|
+
name: Schema.String.check(Schema.isMinLength(1)),
|
|
110
|
+
},
|
|
111
|
+
success: User,
|
|
112
|
+
error: UserNotFound,
|
|
113
|
+
readOnly: false,
|
|
114
|
+
caller: CurrentActor,
|
|
115
|
+
mcp: { destructiveHint: false },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// On either transport, input is { value: "21" }. The handler receives numeric 21.
|
|
119
|
+
export const Double = Action.make("double", {
|
|
120
|
+
description: "Double a finite number supplied as a string.",
|
|
121
|
+
input: { value: Schema.FiniteFromString },
|
|
122
|
+
success: Schema.Finite,
|
|
123
|
+
readOnly: true,
|
|
124
|
+
caller: CurrentActor,
|
|
59
125
|
});
|
|
60
126
|
|
|
61
|
-
//
|
|
127
|
+
// Identity comes from the host's authenticated request context, not action input.
|
|
62
128
|
export const WhoAmI = Action.make("whoAmI", {
|
|
63
129
|
description: "Inspect the authenticated actor.",
|
|
64
|
-
success:
|
|
65
|
-
|
|
130
|
+
success: { id: Schema.String, tenantId: Schema.String },
|
|
131
|
+
readOnly: true,
|
|
132
|
+
caller: CurrentActor,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const Change = Schema.Struct({
|
|
136
|
+
actorId: Schema.String,
|
|
137
|
+
userId: Schema.String,
|
|
138
|
+
name: Schema.String,
|
|
66
139
|
});
|
|
67
140
|
|
|
68
|
-
// A tool only: its group is left out of the HTTP binding (see ActionHttp.md).
|
|
69
141
|
export const ListChanges = Action.make("listChanges", {
|
|
70
|
-
description: "List renames in your tenant, oldest first.",
|
|
71
|
-
success:
|
|
72
|
-
|
|
73
|
-
|
|
142
|
+
description: "List the renames made in your tenant, oldest first.",
|
|
143
|
+
success: { changes: Schema.Array(Change) },
|
|
144
|
+
readOnly: true,
|
|
145
|
+
caller: CurrentActor,
|
|
74
146
|
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Identity and authorization
|
|
150
|
+
|
|
151
|
+
The module the contracts, the implementations, the authentication and every local caller import:
|
|
152
|
+
the identity, `CurrentActor`, which the protected contracts declare and authentication provides
|
|
153
|
+
per request, and `authorize`, which reads each action's `readOnly`. Its demo actors stand in for
|
|
154
|
+
the identities a token verifier returns.
|
|
75
155
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
156
|
+
```ts example=authorization.ts
|
|
157
|
+
import { Context, Effect } from "effect";
|
|
158
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
159
|
+
|
|
160
|
+
export type Permission = "users:read" | "users:write";
|
|
161
|
+
|
|
162
|
+
export interface Actor {
|
|
163
|
+
readonly id: string;
|
|
164
|
+
readonly tenantId: string;
|
|
165
|
+
readonly permissions: ReadonlyArray<Permission>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** DEMO ONLY: fixed credentials, not OAuth or a production token verifier. */
|
|
169
|
+
export const actors = {
|
|
170
|
+
alice: { id: "alice", tenantId: "acme", permissions: ["users:read", "users:write"] },
|
|
171
|
+
reader: { id: "reader", tenantId: "acme", permissions: ["users:read"] },
|
|
172
|
+
bob: { id: "bob", tenantId: "other", permissions: ["users:read", "users:write"] },
|
|
173
|
+
} as const satisfies Readonly<Record<string, Actor>>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The identity a protected contract declares, `caller: CurrentActor`: provided per request by
|
|
177
|
+
* authentication, and by the host on a local surface.
|
|
178
|
+
*/
|
|
179
|
+
export class CurrentActor extends Context.Service<CurrentActor, Actor>()("example/CurrentActor") {}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* One authorization rule for every surface, derived from each contract's own `readOnly`. An
|
|
183
|
+
* implementation of protected actions states it, `{ authorize }`, so every surface serving
|
|
184
|
+
* them runs it before each handler, once the caller is authenticated, and no handler contains
|
|
185
|
+
* authorization code. Its `Forbidden` is built in: every endpoint and tool declares it, and
|
|
186
|
+
* every client decodes it. Naming the missing scope makes it the `insufficient_scope`
|
|
187
|
+
* challenge an OAuth client steps up on.
|
|
188
|
+
*/
|
|
189
|
+
export const authorize = Effect.fn("authorize")(function* (action: Action.Any) {
|
|
190
|
+
const permission: Permission = action.readOnly ? "users:read" : "users:write";
|
|
191
|
+
const actor = yield* CurrentActor;
|
|
192
|
+
|
|
193
|
+
if (!actor.permissions.includes(permission)) {
|
|
194
|
+
return yield* new Action.Forbidden({
|
|
195
|
+
message: `Requires ${permission}.`,
|
|
196
|
+
scopes: [permission],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
84
199
|
});
|
|
200
|
+
```
|
|
85
201
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
202
|
+
### Implementations
|
|
203
|
+
|
|
204
|
+
Build-time services (`Users`) are yielded in a builder; request-time services
|
|
205
|
+
(`CurrentActor`) inside handlers. The implementations of protected actions state `authorize`,
|
|
206
|
+
which every surface runs before each of their handlers; the public `status` states none.
|
|
207
|
+
Every surface takes one implementation or a list: `double`, `userActions`,
|
|
208
|
+
`[status, userActions, double]`. Implementations group actions by domain, not by surface or by
|
|
209
|
+
who may call them: `userActions` holds `listChanges`, which HTTP does not serve because its
|
|
210
|
+
binding leaves it out, and one implementation may hold public and protected actions alike.
|
|
211
|
+
|
|
212
|
+
```ts example=handlers.ts
|
|
213
|
+
import { Effect } from "effect";
|
|
214
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
215
|
+
import { authorize, CurrentActor } from "./authorization.js";
|
|
216
|
+
import { Double, GetUser, ListChanges, RenameUser, Status, WhoAmI } from "./contracts.js";
|
|
217
|
+
import { Users } from "./users.js";
|
|
218
|
+
|
|
219
|
+
// A public contract: no authorization runs, and it owes nothing per request, on every
|
|
220
|
+
// surface.
|
|
221
|
+
export const status = Action.implement(
|
|
222
|
+
Status,
|
|
223
|
+
Effect.gen(function* () {
|
|
224
|
+
const users = yield* Users;
|
|
225
|
+
|
|
226
|
+
return () => Effect.map(users.count, (count) => ({ service: "effect-actions", users: count }));
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Capture Users at startup; resolve CurrentActor per request. Every surface authenticates
|
|
231
|
+
// the caller, then runs `authorize` before each handler, so it has already refused an actor
|
|
232
|
+
// without the permission the action needs. HTTP serves only the actions its binding holds:
|
|
233
|
+
// `listChanges`, which it leaves out, is a tool and a command, never a route.
|
|
234
|
+
export const userActions = Action.implement(
|
|
235
|
+
[GetUser, RenameUser, WhoAmI, ListChanges],
|
|
236
|
+
Effect.gen(function* () {
|
|
237
|
+
const users = yield* Users;
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
getUser: ({ id }) => Effect.flatMap(CurrentActor, (actor) => users.get(actor.tenantId, id)),
|
|
241
|
+
renameUser: ({ id, name }) =>
|
|
242
|
+
Effect.flatMap(CurrentActor, (actor) => users.rename(actor, id, name)),
|
|
243
|
+
whoAmI: () => Effect.map(CurrentActor, ({ id, tenantId }) => ({ id, tenantId })),
|
|
244
|
+
listChanges: () =>
|
|
245
|
+
Effect.gen(function* () {
|
|
246
|
+
const actor = yield* CurrentActor;
|
|
247
|
+
|
|
248
|
+
return { changes: yield* users.changes(actor.tenantId) };
|
|
249
|
+
}),
|
|
250
|
+
};
|
|
251
|
+
}),
|
|
252
|
+
{ authorize },
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Pure: no builder and no services, only the authorization.
|
|
256
|
+
export const double = Action.implement(Double, ({ value }) => Effect.succeed(value * 2), {
|
|
257
|
+
authorize,
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Built authorization
|
|
262
|
+
|
|
263
|
+
An authorizer that reads a startup service, such as a permission store, is built as handlers
|
|
264
|
+
are: an Effect yields the store once and returns the authorizer, which yields the caller on every
|
|
265
|
+
call. The host provides `Permissions.layerMemory` at startup, as it provides `Users`;
|
|
266
|
+
authentication still provides `CurrentActor` per request. Defined apart from `implement`, its
|
|
267
|
+
`action` is annotated; written in `implement`'s options, it would be inferred.
|
|
268
|
+
|
|
269
|
+
```ts example=authorization-built.ts
|
|
270
|
+
import { Context, Effect, Layer } from "effect";
|
|
271
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
272
|
+
import { actors, CurrentActor, type Permission } from "./authorization.js";
|
|
273
|
+
import { WhoAmI } from "./contracts.js";
|
|
274
|
+
|
|
275
|
+
/** DEMO ONLY: each actor's permissions, kept in a store rather than in the identity. */
|
|
276
|
+
export class Permissions extends Context.Service<
|
|
277
|
+
Permissions,
|
|
278
|
+
{ readonly of: (actorId: string) => Effect.Effect<ReadonlyArray<Permission>> }
|
|
279
|
+
>()("example/Permissions") {
|
|
280
|
+
static readonly layerMemory = Layer.succeed(Permissions, {
|
|
281
|
+
of: (actorId) =>
|
|
282
|
+
Effect.succeed(Object.values(actors).find(({ id }) => id === actorId)?.permissions ?? []),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The `authorize` rule, built as handlers are: the store is yielded once, a startup service
|
|
287
|
+
// provided next to `Users`, and the function it returns yields the actor on every call.
|
|
288
|
+
export const authorizeStored = Effect.gen(function* () {
|
|
289
|
+
const permissions = yield* Permissions;
|
|
290
|
+
|
|
291
|
+
return (action: Action.Any) =>
|
|
292
|
+
Effect.gen(function* () {
|
|
293
|
+
const permission: Permission = action.readOnly ? "users:read" : "users:write";
|
|
294
|
+
const actor = yield* CurrentActor;
|
|
295
|
+
|
|
296
|
+
if (!(yield* permissions.of(actor.id)).includes(permission)) {
|
|
297
|
+
return yield* new Action.Forbidden({
|
|
298
|
+
message: `Requires ${permission}.`,
|
|
299
|
+
scopes: [permission],
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Built once per layer graph for this implementation, and per invocation of a local command.
|
|
306
|
+
export const whoAmI = Action.implement(
|
|
307
|
+
WhoAmI,
|
|
308
|
+
() => Effect.map(CurrentActor, ({ id, tenantId }) => ({ id, tenantId })),
|
|
309
|
+
{ authorize: authorizeStored },
|
|
310
|
+
);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Client
|
|
314
|
+
|
|
315
|
+
Implementations called in process, by code: a test, a job, a command of your own. The client is
|
|
316
|
+
acquired once, where builders live, and each call is given its caller, as authentication gives
|
|
317
|
+
one per request, so one client serves several callers. Every implemented action has a method,
|
|
318
|
+
`listChanges` included, which no binding holds; `actions` lists fewer.
|
|
319
|
+
|
|
320
|
+
```ts example=in-process.ts
|
|
321
|
+
import { Effect } from "effect";
|
|
322
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
323
|
+
import { actors, CurrentActor } from "./authorization.js";
|
|
324
|
+
import { userActions } from "./handlers.js";
|
|
325
|
+
import { Users } from "./users.js";
|
|
326
|
+
|
|
327
|
+
// Each call names its caller, as authentication names one per request.
|
|
328
|
+
const asAlice = Effect.provideService(CurrentActor, actors.alice);
|
|
329
|
+
|
|
330
|
+
const asReader = Effect.provideService(CurrentActor, actors.reader);
|
|
331
|
+
|
|
332
|
+
const program = Effect.gen(function* () {
|
|
333
|
+
// Acquired once, as a layer is built: the builders run here, not per call.
|
|
334
|
+
const users = yield* Action.client(userActions);
|
|
335
|
+
|
|
336
|
+
// The methods of `ActionHttp.client(Http)`, with no transport between: each call decodes
|
|
337
|
+
// its input, runs `authorize`, then the handler, and checks the success or the failure.
|
|
338
|
+
const renamed = yield* users.renameUser({ id: "1", name: "Bea" }).pipe(asAlice);
|
|
339
|
+
const refused = yield* Effect.flip(users.renameUser({ id: "1", name: "Cy" }).pipe(asReader));
|
|
340
|
+
const { changes } = yield* users.listChanges().pipe(asReader); // not an HTTP route
|
|
341
|
+
|
|
342
|
+
return { renamed, refused, changes };
|
|
92
343
|
});
|
|
344
|
+
|
|
345
|
+
// The builders are released with the program's scope, before the services they captured.
|
|
346
|
+
console.log(
|
|
347
|
+
await Effect.runPromise(program.pipe(Effect.scoped, Effect.provide(Users.layerMemory))),
|
|
348
|
+
);
|
|
93
349
|
```
|
|
94
350
|
|
|
95
351
|
## Rules
|
|
96
352
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
- `
|
|
102
|
-
- `
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
- `
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
353
|
+
### Contracts
|
|
354
|
+
|
|
355
|
+
- Names are 1 to 128 characters of `[A-Za-z0-9_-]`. `then` is rejected: it would make a client thenable. The name is also the HTTP route segment, the client method and the MCP tool name.
|
|
356
|
+
- Omit `input`, or give `{}`, for a no-argument action. Its input is an empty object that accepts only `{}`: over HTTP, extra fields and an empty JSON body are a 400. It is MCP's object root. Its client method may be called without an argument.
|
|
357
|
+
- Omit `success` for an action that returns nothing. The default is `Schema.Void`: the client methods of `ActionHttp.client` and `Testing.mcpClient` return `void`, HTTP answers with no content, and a CLI command prints nothing by default or with `--json`; a custom `render` may print text. As for a function returning `void`, its handler may still return a value; the encoding drops it. Declare `success` to return data.
|
|
358
|
+
- `input` and `success` take a schema or fields. Fields become `Schema.Struct(fields)`, and no fields the empty object above; each field must be service-free, like every schema here. A schema is kept as given: a `Schema.Struct({})` accepts any value but `null`, as in Effect, and MCP refuses it as a tool's input, so write `{}` instead.
|
|
359
|
+
- `error` is one schema or a list of them, as `HttpApiEndpoint` takes it, default none: `error: UserNotFound`, `error: [UserNotFound, Conflict]`. The contract holds it as a list, `action.error`, one schema alone a list of one. Each keeps its own `httpApiStatus` annotation, and so does each member of a union without one of its own. An unannotated error is served as HTTP 422: an expected outcome, not Effect's default 500, which reads as a server fault.
|
|
360
|
+
- To share errors across actions, spread one constant array into each action's `error`. To share several options, such as a caller and its errors, spread one constant object into each action's options: with `const Owner = { caller: CurrentOwner, error: [NotFound, Conflict] } as const`, `Action.make("rename", { ...Owner, description, readOnly: false, input })` is typed as if written inline, and a handler failing with an error `Owner` does not list still fails to compile.
|
|
361
|
+
- `caller` is required: `Action.Anyone`, or the identity service a caller must have. An action whose `caller` is `Action.Anyone` is a public action, any other a protected one. There is no default, so an action nobody classified fails to compile rather than being served to anyone; `make` also throws without it, for a caller the compiler never sees.
|
|
362
|
+
- A protected contract imports its identity, so the module declaring it stays browser-safe: a `Context.Service` and its type, without the verifier ([setup.md](setup.md#browser)). Never a `Context.Reference`: its default is always present, so it would stand in for every caller who supplies none; the types refuse one, and `make` throws. A contract never imports a transport, its authentication descriptor included.
|
|
363
|
+
- One identity per action, and every protected action one remote surface serves declares the same one, its authentication descriptor's. A tenant or other request context is an ordinary request-time requirement of the handler, never a second identity.
|
|
364
|
+
- Every surface enforces `caller`, whatever the handler reads: a protected action is authenticated remotely and owes its identity per call locally, even where its handler and authorizer read none ([guarantees.md](guarantees.md#authorization)). A public action is neither authenticated nor authorized, and no surface provides it an identity: a public handler that reads one leaves its layer owing it ([Failure modes](#failure-modes)). An action whose answer depends on the caller is protected.
|
|
365
|
+
- `readOnly` is a boolean and is required: whether the action leaves its resource unchanged. `make` also checks it at runtime, so a caller the compiler never sees cannot define an action no rule classifies. It stays a literal on the action, so a type may select the read-only actions, `Extract<A, { readonly readOnly: true }>`.
|
|
366
|
+
- An implementation's `authorize` reads `readOnly` ([guarantees.md](guarantees.md#authorization)); the library itself authorizes nothing with it. Its only built-in uses are the tool's hints and the span/log annotation.
|
|
367
|
+
- `readOnly` is also the tool's read-only hint, which no option overrides, so authorization and what MCP clients are told cannot disagree. Derive authorization from `readOnly`, never from an `mcp` hint.
|
|
368
|
+
- A contract says nothing about where it is served. Each surface takes the list of actions it serves, from the implementations passed to it: HTTP its binding's, `ActionHttp.make(actions)`, and MCP, a Toolkit and `ActionCli.make` their `actions` option, or every action of the implementations without it. To keep an action off a surface, leave it out of that list, whatever implementation holds it. A listed action keeps its `caller`, its implementation's authorizer and its builder's one run.
|
|
369
|
+
- List the actions that are tools in one place, beside the contracts, `export const Tools = [GetUser, RenameUser] as const`, and give every MCP endpoint and Toolkit `actions: Tools`: an action added later is no tool until it is listed. List an action only where the model may hold what it takes and returns: a tool call passes both through the model's context, and records its input on a span ([guarantees.md](guarantees.md#observability)).
|
|
370
|
+
- `byName(actions)` keys a list by name: `const contracts = Action.byName(Actions)` gives `contracts.getUser`, its schemas for a test or a mock, `keyof typeof contracts` as the names, and an action's types by name, `(typeof contracts)[K]["input"]["Type"]`, which resolves under a generic `K`, where `Extract` over the list does not. The list is typed by its members ([guarantees.md](guarantees.md#names)). It throws `Duplicate action: <name>` for a name held twice.
|
|
371
|
+
- `make` accepts only the keys listed above, and `mcp` only `title`, `destructiveHint`, `idempotentHint`, `openWorldHint` and `_meta`: MCP's own names. An unknown key is a compile error, beside known ones too, and so is `readOnlyHint`. `title` is the tool's display name, MCP's `annotations.title`, and `_meta` its `_meta`, sent as given, such as an MCP App's UI resource: native `Tool.Title` and `Tool.Meta` on a Toolkit's tool. Neither has a default, and neither is sent when left out.
|
|
372
|
+
- An `undefined` option takes its default, as an omitted one does. One that may be either is typed as either: with `success: enabled ? Schema.String : undefined`, or the same through a conditional spread, the success is `string | void`. The same holds for `input` and `error`.
|
|
373
|
+
- Options annotated with a wide type, such as `Parameters<typeof Action.make>[1]`, are not checked. Their action's schemas are as wide as what may run, so its success is `unknown`. A constant object spread into the options, as above, keeps its exact types and is checked like inline options.
|
|
374
|
+
- MCP input must be one object with keys, an identified, recursive or suspended root included. Scalar, array or union input is fine for HTTP and for a native Toolkit, but `ActionMcp` refuses it when `layerHttp` or `runStdio` is called, naming the action ([ActionMcp.md](ActionMcp.md#rules)). Success and error schemas may be any shape.
|
|
375
|
+
- The contract keeps `mcp` as given, `action.mcp`, and defaults none of it. A hint left out is not annotated, so the native default, MCP's, stands: `destructiveHint` `true`, `idempotentHint` `false`, `openWorldHint` `true`; but a read-only action's `destructiveHint` is `false` unless it states one. A stated hint is sent as it is, a read-only action's included, which MCP reads only for a tool that is not read-only. `readOnlyHint` is always `readOnly`. Hints are metadata for the model. They do not enforce authorization, approval, or retries; a native Toolkit's approval is `ActionToolkit.make`'s `needsApproval` option.
|
|
376
|
+
- The built-in errors are declared everywhere ([guarantees.md](guarantees.md#wire-behavior)), and any handler may fail with them without listing them: `InvalidInput` for input that decodes but cannot be served, naming `issues` of its own as the library's do ([guarantees.md](guarantees.md#wire-behavior)), `new Action.InvalidInput({ message, issues: [{ path: ["lines"], message }] })`, so an application declares no second error for bad input; a refusal for a step-up. `make` refuses an `error` entry that encodes with a built-in tag, the built-in itself included, since every surface declares it already and a client could not tell a look-alike apart. Only those classes are built-in errors: a handler or an authorizer failing with an error of the same `_tag` and fields, such as Effect's `HttpApiError.Forbidden` or a `Forbidden` of your own, does not compile, since a built-in error's codec encodes its own class alone.
|
|
377
|
+
- Code that decodes input of its own, such as a header, a route parameter or a file's metadata, answers a failure as the surfaces do with `Action.InvalidInput.fromSchemaError(error)`: the schema's message and its `issues`, `Effect.mapError(Action.InvalidInput.fromSchemaError)` after a `Schema.decodeUnknownEffect`. To place the issues under a field of a larger input, decode the value as that field, `Schema.decodeUnknownEffect(Schema.Struct({ payload: Payload }))({ payload })`, so each issue's `path` starts at it.
|
|
378
|
+
- `Action.Refusal` and `Action.BuiltIn` are schemas as well as types, so code outside Effect tells a failure apart without listing the classes: `Schema.is(Action.Refusal)(error)` for a refusal before the handler (signing in again helps only an `Unauthenticated`, or a `Forbidden` naming `scopes` for a caller that can step up), and `Schema.Union([...Action.BuiltIn.members, NotFound])` for an action's declared and built-in errors; a remote client also sees its binding's `error` and its descriptor's. Neither belongs in an `error` option: `make` refuses a built-in tag there.
|
|
379
|
+
- Build a refusal with or without a message: `new Action.Forbidden()` sends `"Not allowed."`, `new Action.Forbidden({ message: "Requires users:write." })` sends that. A `Forbidden` may name the OAuth scopes the call lacks, `new Action.Forbidden({ scopes: ["users:write"] })`: a refused OAuth client then re-authorizes with them ([Authentication.md](Authentication.md#rules)).
|
|
110
380
|
- Schemas must be service-free. Put service access in the handler.
|
|
111
|
-
|
|
112
|
-
|
|
381
|
+
|
|
382
|
+
### Implementations
|
|
383
|
+
|
|
384
|
+
- `implement` returns one `Implementation` of everything it binds: `implement(action, handler, options)` one action, `implement([a, b], { a: ..., b: ... }, options)` every listed action. Either may take an Effect that builds the handler or the record instead. One implementation may hold public and protected actions. Every surface takes one implementation or a list: `[status, userActions, double]`. Which actions it exposes follows the [surface selection rules](guarantees.md#names).
|
|
385
|
+
- An implementation holding a protected action states who of the authenticated callers may call: `{ authorize }` is required, and leaving it out is a compile error. `Action.allowAll` lets every authenticated caller call; authentication still decides who that is. An authorizer that depends on the deployment is `enabled ? authorize : Action.allowAll`; its services are owed either way. Plain JavaScript has no types, so `implement` also checks at runtime that it got one.
|
|
386
|
+
- An implementation of public actions alone takes no options: `{ authorize }` there is a compile error, since nothing would run it.
|
|
387
|
+
- `authorize` runs only for protected actions, after authentication and input decoding, before the handler, on every surface ([guarantees.md](guarantees.md#authorization)). It fails only with a refusal: a limit, or any other failure a caller should see beside a refusal, is the handler's. Typed `Action.Authorize<Action.Any>`, an authorizer fits every implementation, as `Action.allowAll` does.
|
|
388
|
+
- `authorize` may be an Effect that builds it, as a builder builds handlers ([Built authorization](#built-authorization)). Its lifetime, and startup versus request-time services: [guarantees.md](guarantees.md#dependency-lifetimes). Guarding several implementations, it is built once for each: keep state they share in a service the Effect yields, or pass a service whose value is the authorizer itself, `{ authorize: Guard }`, which Effect builds once. `Effect.isEffect` tells the two forms apart, so an authorizer written with `Effect.fn` is a plain one.
|
|
389
|
+
- `authorize` sees `action` typed as the implementation's own actions when it is written in `implement`'s options, as a plain function, an `Effect.fn` or a built one. Defined apart, as a rule several implementations share is, nothing types it there: annotate it, `(action: Action.Any)`.
|
|
390
|
+
- A limit, such as a rate limit, is the handler's: the action declares its error in `error`, the builder yields the limiter, a startup service shared by every surface of the layer graph, and the handler fails with the error before its work, as with any declared error. Several actions sharing one limit spread one error array into their `error` and wrap their handlers in one function the builder defines. A limit counting every request, decoded or not, is router middleware over HTTP ([guarantees.md](guarantees.md#authorization)).
|
|
391
|
+
- A record has exactly one own-property function per action, keyed by its name. Missing and extra keys are compile errors, for a plain record and for a builder's. An inherited method does not count. Handlers are called without a receiver. Duplicate action names in one call throw at `implement`.
|
|
392
|
+
- A plain handler or record is checked at `implement`; a builder's record when it is built. A record that slips past the types (plain JavaScript, a cast) throws `Unknown handlers` for a key no action names, and `Missing handlers` for an action without a function. From a builder, the layer build dies with the same message. Nothing is served with a handler missing.
|
|
393
|
+
- Builder lifetime and build-time versus request-time services: [guarantees.md](guarantees.md#dependency-lifetimes). Authentication and authorization: [guarantees.md](guarantees.md#authorization). Surfaces keep the two kinds of services separate in their types, per action.
|
|
394
|
+
- A handler's input is typed from its action however it is written: an arrow, a function typed `Action.Handler`, or a generator, `Effect.fn(function* (input) { ... })` or `Effect.fnUntraced`, alone or in a record, a builder's included. The builder must itself be `implement`'s argument: through `.pipe(...)`, or a wrapper such as `Effect.withSpan(builder, name)`, an `Effect.fn` it returns takes an `any` input, so annotate the input there.
|
|
395
|
+
- Each handler already runs in a span named after its action, and every log line it writes is annotated with `action.name` and `action.read_only` ([guarantees.md](guarantees.md#observability)). Leave `Effect.fn` unnamed: don't wrap a handler in `Effect.fn(name)` or annotate its logs with the action's name yourself.
|
|
396
|
+
- A surface serves fewer of an implementation's actions through its own list ([Contracts](#contracts)), and `client` through `actions`: each listed action keeps its implementation's handlers, builder and authorizer. Selection never changes who may call. A caller trusted on one surface, such as an operator on an admin CLI, is an identity the host supplies there, which the same authorizer admits ([ActionCli.md](ActionCli.md#trusted-callers)). Another authorizer for the same handlers takes another `implement`, of the same builder Effect, which then runs once per implementation in each layer graph. A builder written apart from `implement` returns its record `satisfies Action.Handlers<typeof actions>`, so each handler is typed from its contract, as inside `implement`.
|
|
397
|
+
- A helper over implementations is generic, `<const Apps extends Action.AnyImplementation | ReadonlyArray<Action.AnyImplementation>>(apps: Apps)`, so each implementation's requirements reach the surface it passes them to, `ActionHttp.layer` included, whose binding decides what it serves. Beside the helper's own implementations, list or spread the parameter, `[app, double]` or `[...apps, double]`. `AnyImplementation` erases every channel to `unknown`: a value or a parameter typed with it owes `unknown`, which no surface can be given. An implementation's own type is inferred from `implement`. A package stating one in an interface of its own writes it out, `Action.Implementation<typeof Whoami, { readonly whoami: Principal }, never, Tenant>`: its actions, each action's per-request services by name, its builder's failures and services, then its authorizer's per-request services and its authorizer builder's failures and services, each `never` when left out. Exported from a package that emits declarations, the helper states its return type as the surface's own, `ReturnType<typeof Action.client<Apps>>`, which TypeScript can name where the inferred one cannot.
|
|
398
|
+
- `Implementation` is nominal. Spreading its properties does not produce an implementation. Surfaces match an implementation to an action by object identity, so implement the exact contract value a binding or a CLI selector receives.
|
|
399
|
+
- Test what an implementation does, its authorizer and its handlers, in process with `client`, each caller provided around its own calls ([Clients](#clients)); test what a surface adds, authentication, statuses and codecs, under `Testing.layer` ([Testing.md](Testing.md)). A builder's handlers exist only once it runs, so calling the function passed to `implement` works only for a plain handler, and skips authorization. Cover each surface the application exposes.
|
|
400
|
+
|
|
401
|
+
### Clients
|
|
402
|
+
|
|
403
|
+
- `client(implementations)` takes one implementation or a list, as a surface does, and gives one method per action, named after it and taking its input directly: the methods `ActionHttp.client` gives, so moving between an in-process and a remote caller changes the line acquiring it. The argument may be left out when `{}` is a valid encoded input, such as an input whose every field has a decoding default, and is then what `{}` decodes to; an input whose encoded form requires a field its decoded one lacks takes the argument. Every implemented action has a method, whether or not a surface serves it. An action name twice among the implementations throws `Duplicate action: <name>` where `client` is called.
|
|
404
|
+
- A call runs as a remote one does, through the dispatch every surface shares. Its input passes through its JSON codec, encoded to JSON text then decoded, so the handler receives what a remote handler decodes: an undeclared field dropped, as a client's encoding drops it, a class instance built anew, a decoding transformation such as a trim applied, `0` for `-0`. Authorization runs, then the handler, in its action's span, each call in a scope of its own ([guarantees.md](guarantees.md#dependency-lifetimes)). The success passes through its codec the same way, and a failure through the codec of every error the action declares, the built-in ones included, so the caller gets what a remote caller decodes: no undeclared field, a decoding transformation applied, `undefined` for a `Schema.Void` success. A failure keeps its trace: where the authorizer or the handler failed.
|
|
405
|
+
- A call fails with the action's declared errors and the built-in ones, as `ActionHttp.client` decodes them, and with nothing of a transport. Input that does not pass through its codec is `InvalidInput`, with the schema's message and `issues`, and authorization and the handler never run; a class input is passed as an instance, as over HTTP. A protected action's call without its identity fails with `Unauthenticated` before the authorizer runs. The authorizer's refusal is the call's failure. A success or a failure that does not pass, an error the action does not declare included, is a defect, as it is an empty 500 over HTTP: the handler broke its contract. The defect is its `SchemaError`, and for a failure, the failure itself after it.
|
|
406
|
+
- Each method owes, per call, what its handler and its implementation's authorizer read, and a protected action's identity whether or not they read it, and the caller provides them around the call, or around a program making several: `users.renameUser(input).pipe(Effect.provideService(CurrentActor, actor))`. A call reads its caller's context, never the one the client was acquired in: provided around the acquisition, a service reaches no call, so neither does a startup identity, and one client serves every caller.
|
|
407
|
+
- `actions` lists the actions it calls among the implementations', as a surface's `actions` does: an implementation holding none of them is left out, unbuilt, and a listed action none of them holds throws `Listed in actions, but no implementation holds it: <names>`. Where `actions` may be absent, an optional property or a union with options lacking it, the client offers methods only for the actions it may list, which are present either way, while it owes and builds what every action needs ([guarantees.md](guarantees.md#names)). An explicit type argument listing actions requires the options argument.
|
|
408
|
+
- Acquiring a client builds its implementations' builders and every authorizer an Effect builds, as a layer does: the acquisition owes their startup services and a `Scope`, and fails as they fail; no call does. Where it builds, and what it shares: [dependency lifetimes](guarantees.md#dependency-lifetimes). Acquire it where builders live, once: in a builder, in a layer providing it as a service of type `Action.Client<typeof users>`, or in a scoped program, such as a test, a job or a command. Never per request, in a handler.
|
|
113
409
|
|
|
114
410
|
## Failure modes
|
|
115
411
|
|
|
116
|
-
-
|
|
117
|
-
-
|
|
412
|
+
- `Invalid action name: <name>` thrown by `make`: the name is empty, longer than 128 characters, `then`, or has another character.
|
|
413
|
+
- `Invalid readOnly: <value>` thrown by `make`: `readOnly` is not a boolean, which only a caller the compiler never sees can pass.
|
|
414
|
+
- `Property 'caller' is missing` at `make`, or `Missing caller: declare Action.Anyone or an identity service key` thrown by `make`: the contract does not state who may call it. Give `caller: Action.Anyone`, or the identity service a caller must have. There is no default.
|
|
415
|
+
- `Type ... is not assignable to type '"An identity is a Context.Service, not a Context.Reference"'` at `make`, or `Invalid caller: an identity is a Context.Service, not a Context.Reference` thrown by `make`: `caller` is a `Context.Reference`, whose default would authenticate every caller. Declare the identity with `Context.Service`.
|
|
416
|
+
- `... is not assignable to parameter of type 'ProtectedActionsTakeAuthorize'` at `implement`, ending `Type 'typeof CurrentActor' is not assignable to type 'unique symbol'`: the target holds a protected action, and the implementation states no `authorize`. Pass `{ authorize }`, or `{ authorize: Action.allowAll }` where every authenticated caller may call.
|
|
417
|
+
- `No overload matches this call` at `implement`, ending `... is not assignable to type '"A public-only target takes no authorize"'`: an implementation of public actions alone states `authorize`, which nothing would run. Drop it; a public action is never authorized. If the action needs a caller, make its contract protected. Plain JavaScript passing one throws `A public-only target takes no authorize` at `implement`.
|
|
418
|
+
- `Protected actions require authorize, or Action.allowAll` thrown at `implement`: plain JavaScript left `authorize` out for a target holding a protected action. `Missing authorize: pass an authorization function, or Action.allowAll` thrown at `implement`, or the layer build dies with it: `authorize` is neither a function nor an Effect building one, or that Effect built something else.
|
|
419
|
+
- `Argument of type '... | undefined' is not assignable` at `implement`'s `authorize`: an authorizer that may be `undefined`, such as `enabled ? authorize : undefined`. Write `enabled ? authorize : Action.allowAll`.
|
|
420
|
+
- `Parameter 'action' implicitly has an 'any' type`, or an `Effect.fn` authorizer whose `action` is `any`, where it is defined apart from `implement`: nothing types it there. Annotate it, `(action: Action.Any)`.
|
|
421
|
+
- Type error `Effect<..., X, ...> is not assignable` at `implement`: the handler fails with an undeclared error `X`. Add it to the action's `error` or handle it.
|
|
422
|
+
- `Type 'X' is not assignable to type 'Refusal'` at `implement`, ending in a `_tag` mismatch with `"Unauthenticated"`: the authorizer fails with `X`, which is not a refusal. A limit or another failure callers should see is the handler's: declare `X` in the action's `error` and fail with it there.
|
|
423
|
+
- `Type 'CurrentActor' is not assignable to type 'never'`, or `Request<"Requires", CurrentActor>` still owed, for a layer serving only public actions: a public action's handler reads the identity, and no surface provides an identity to a public action. Make the action protected.
|
|
424
|
+
- `Property 'x' is missing in type` at `implement`: the record lacks a handler for action `x`.
|
|
425
|
+
- `Missing handlers: <names>` at `implement`, or when a builder's layer builds: the record has no own-property function for those actions. Add them to the record itself, not to a prototype.
|
|
426
|
+
- `Unknown handlers: <keys>` at `implement` or when a builder's layer builds, or a type error names a key: the record has a handler for an action not in this `implement` call. Remove it, or add its action to the list.
|
|
427
|
+
- `Listed in actions, but no implementation holds it: <names>` thrown by `client`: a listed action is none of the implementations'. Pass the implementation holding it, or check the contract values are the ones it implemented.
|
|
428
|
+
- `Duplicate action: <name>` at `implement`: the list names one action twice. Thrown by `client`: two of its implementations implement an action of that name. Separate contracts sharing a name are told apart by `actions`, listing one; two implementations of the same contract are not, since listing it keeps both: pass one implementation.
|
|
118
429
|
- Handler receives a string where a number was expected: the schema is `Schema.String`, not a transforming codec such as `Schema.FiniteFromString`.
|
|
119
|
-
- `Property '
|
|
120
|
-
-
|
|
121
|
-
-
|
|
430
|
+
- `Property 'readOnly' is missing` at `make`: every action declares whether it is read-only. There is no default.
|
|
431
|
+
- A service is resolved once and shared across requests when it should be per request: it was yielded in the builder, or in the Effect building the authorizer. Move the `yield*` into the handler or the authorizer. An identity yielded there is a startup requirement of the layer; never provide one at startup ([guarantees.md](guarantees.md#dependency-lifetimes)).
|
|
432
|
+
- `HttpRouter.Request<"Requires", X>` still owed, or `Type 'X' is not assignable to type 'never'` at `runMain`, although `X`, a store the authorizer reads, is provided at startup: the authorizer yields it on every call. Yield it in an Effect that builds the authorizer ([Built authorization](#built-authorization)).
|
|
433
|
+
- `unknown` among a surface's requirements, and a type error where it is served or run, such as `Argument of type 'Layer<never, unknown, unknown>' is not assignable` at `HttpRouter.toWebHandler` or `Type 'unknown' is not assignable to type 'never'` where it is launched or run: the implementations are typed `Action.AnyImplementation`, as a helper's parameter or a list's annotation. Make the helper generic over them, or drop the annotation.
|
|
434
|
+
- `Request<"Requires", unknown>` in a type error where an implementation is served, or `unknown` among the services a call or a run still requires: TypeScript inferred no requirements for a handler of a record. A data-first call whose type takes a parameter from no argument does it, `Effect.catchTag(effect, tag, f)` for one, where `Effect.catch`, `Effect.map` and `Effect.orDie` do not. Write that handler's Effect `effect.pipe(Effect.catchTag(tag, f))`, or as a generator.
|
|
435
|
+
- `'readOnlyHint' does not exist in type 'Mcp'` at `make`, or `Type 'true' is not assignable to type 'never'` on `readOnlyHint` beside another hint: a tool's read-only hint is its action's `readOnly`. Set `readOnly` instead.
|
|
436
|
+
- `Type '...' is not assignable to type 'never'` on a key at `make`, in `mcp` too: an option or hint it does not take, or a misspelled one, such as `destructive` for `destructiveHint`.
|
|
437
|
+
- `Type 'H' is not assignable to type 'H & ...'` at `make`, where `H` is a helper's type parameter, as in `<const H extends Action.Mcp>(mcp: H)`: `make` cannot read options a type parameter stands for, so it refuses them. An action's type carries no `mcp` types, so type the parameter `Action.Mcp`; the helper loses nothing. A constant object spread into the options is no type parameter: `make` reads it as written.
|
|
438
|
+
- Type error on `error` at `make`, `... is not assignable to type 'readonly never[]'` or naming the remaining errors: it lists a built-in error. Drop it; every surface declares it, and a handler may fail with it anyway.
|
|
439
|
+
- `make` throws `Action "<name>": error _tag "Forbidden" is built in, and declared on every surface`: an error in the action's `error`, or a member of a union there, encodes with the `_tag` of a built-in error, `InvalidInput`, `Unauthenticated` or `Forbidden`. Drop a built-in error from `error`, since a handler may fail with it anyway; rename an error of your own.
|
|
440
|
+
- `ReferenceError` thrown by `make`: an entry of `error` is a `Schema.suspend` whose thunk reads a `const` declared further down. `make` reads every error's `_tag` when it is called; declare the error first.
|
|
441
|
+
- `Type 'CurrentActor' is not assignable to type 'never'` where a program calling a client runs: a call of a protected action owes the caller, and nothing provides one around it. Provide it around the call, or around the calls it makes; provided around the acquisition, it reaches no call. Never provide it to a layer or a builder acquiring the client.
|
|
442
|
+
- A type error that a program still requires `Scope`: acquiring a client builds, as a layer does. Acquire it in a builder or a layer, or run the program in `Effect.scoped`.
|
|
443
|
+
- A builder runs on every request, or a route owes `HttpRouter.Request<"Requires", X>` for `X`, a service its builder yields: a handler acquires a client per request. Acquire it in the builder, and call it in the handler.
|
|
444
|
+
- A builder runs twice, or a job, an agent or a second server reads other state than the routes, such as an empty in-memory store: it is merged beside `HttpRouter.serve`, whose layer graph built the builder first. Put it inside the layer the server serves, or provide `Action.layer` and the services they share above both ([dependency lifetimes](guarantees.md#dependency-lifetimes)).
|
|
445
|
+
- Surfaces given startup services of their own, provided around each or to an `Action.layer` of each, all answer with one surface's: a builder runs once per layer graph, with the services of its first build. Wrap each surface in `Layer.fresh`, or give each an implementation of its own ([dependency lifetimes](guarantees.md#dependency-lifetimes)).
|
|
446
|
+
- `'<name>' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer` (TS7022) at `implement`, or, with the types bypassed, an acquisition that never completes: a builder acquires a client of its own implementation, directly or through another builder. Call the services both use instead.
|
|
447
|
+
- A call dies with a `SchemaError`: the handler's success, or what the handler fails with, does not pass through its codec, which an HTTP caller sees as an empty 500. A failure follows the `SchemaError` in the cause, as `Cause.pretty` shows. Return what the success schema accepts, and fail with an error the action declares, as its schema accepts it.
|
|
448
|
+
- `InvalidInput` with `Expected <Class>` from a call given a plain object: the input is a class, which encodes only its instances. Pass `new Class({ ... })`, or leave out an argument whose fields are all optional.
|
|
449
|
+
- `Not an implementation made by this Action.implement: is effect-actions installed twice?` thrown by a surface, `client` or `layer`, or by a local CLI command when it runs: another installed copy of the package made the implementation, and only that copy serves and calls it. Install one copy of the package; contracts and bindings still cross copies.
|