@gjermundgaraba/effect-actions 0.18.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -47
- package/dist/Action.d.mts +96 -49
- package/dist/Action.mjs +12 -16
- package/dist/ActionCli.d.mts +82 -44
- package/dist/ActionCli.mjs +54 -35
- package/dist/ActionHttp.d.mts +84 -45
- package/dist/ActionHttp.mjs +3 -4
- package/dist/ActionMcp.d.mts +54 -29
- package/dist/ActionMcp.mjs +33 -15
- package/dist/ActionRpc.d.mts +48 -37
- package/dist/ActionRpc.mjs +3 -4
- package/dist/ActionToolkit.d.mts +32 -15
- package/dist/ActionToolkit.mjs +11 -9
- package/dist/Authentication.d.mts +92 -42
- package/dist/Authentication.mjs +38 -37
- package/dist/Testing.d.mts +23 -27
- package/dist/Testing.mjs +1 -2
- package/dist/authentication/provider.d.mts +1 -6
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -3
- package/dist/cli/commands.d.mts +34 -13
- package/dist/cli/commands.mjs +48 -24
- package/dist/contract/ast.mjs +1 -2
- package/dist/contract/call.d.mts +1 -3
- package/dist/contract/call.mjs +1 -2
- package/dist/contract/caller.d.mts +1 -3
- package/dist/contract/caller.mjs +2 -3
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +28 -16
- package/dist/contract/errors.d.mts +42 -19
- package/dist/contract/errors.mjs +43 -19
- package/dist/contract/hints.mjs +4 -4
- package/dist/contract/implementation.d.mts +7 -14
- package/dist/contract/implementation.mjs +31 -19
- package/dist/contract/media.d.mts +7 -5
- package/dist/contract/media.mjs +7 -4
- package/dist/contract/names.mjs +15 -14
- package/dist/contract/status.mjs +4 -12
- package/dist/http/client.d.mts +15 -6
- package/dist/http/client.mjs +2 -3
- package/dist/http/schema-errors.mjs +1 -2
- package/dist/mcp/client.d.mts +21 -10
- package/dist/mcp/client.mjs +8 -11
- package/dist/mcp/protocol.mjs +1 -2
- package/dist/stdio/console.mjs +1 -2
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +1 -2
- package/docs/Action-failures.md +6 -15
- package/docs/Action.md +159 -191
- package/docs/ActionCli-failures.md +12 -92
- package/docs/ActionCli.md +102 -156
- package/docs/ActionHttp-failures.md +14 -76
- package/docs/ActionHttp.md +150 -176
- package/docs/ActionMcp-failures.md +2 -22
- package/docs/ActionMcp.md +79 -170
- package/docs/ActionRpc-failures.md +3 -12
- package/docs/ActionRpc.md +183 -120
- package/docs/ActionToolkit-failures.md +3 -10
- package/docs/ActionToolkit.md +112 -109
- package/docs/Authentication-advanced.md +171 -108
- package/docs/Authentication-failures.md +11 -91
- package/docs/Authentication.md +114 -135
- package/docs/README.md +45 -30
- package/docs/Testing-failures.md +2 -6
- package/docs/Testing.md +126 -159
- package/docs/setup-failures.md +8 -32
- package/docs/setup.md +42 -38
- package/examples/README.md +6 -10
- package/examples/assistant/README.md +20 -38
- package/examples/assistant/src/agent.ts +2 -3
- package/examples/assistant/src/cli.ts +4 -9
- package/examples/assistant/src/commands.ts +6 -4
- package/examples/assistant/src/identity/authorize.ts +3 -3
- package/examples/assistant/src/tasks/Tasks.ts +1 -2
- package/examples/assistant/src/tasks/handlers.ts +3 -3
- package/examples/assistant/src/toolkit.ts +6 -8
- package/examples/assistant/test/commands.test.ts +16 -11
- package/examples/assistant/test/tasks/tasks.test.ts +0 -2
- package/examples/assistant/test/toolkit.test.ts +0 -1
- package/examples/kitchen-sink/README.md +30 -52
- package/examples/kitchen-sink/src/Actions.ts +2 -3
- package/examples/kitchen-sink/src/Api.ts +3 -4
- package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
- package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
- package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
- package/examples/kitchen-sink/src/agent.ts +1 -1
- package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
- package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
- package/examples/kitchen-sink/src/attachments/download.ts +5 -8
- package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
- package/examples/kitchen-sink/src/browser.ts +3 -4
- package/examples/kitchen-sink/src/cli.ts +6 -7
- package/examples/kitchen-sink/src/config.ts +2 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
- package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
- package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
- package/examples/kitchen-sink/src/identity/quota.ts +1 -1
- package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
- package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
- package/examples/kitchen-sink/src/issues/guide.ts +2 -3
- package/examples/kitchen-sink/src/ops.ts +3 -6
- package/examples/kitchen-sink/src/request-policy.ts +8 -4
- package/examples/kitchen-sink/src/routes.ts +4 -6
- package/examples/kitchen-sink/src/seed.ts +0 -1
- package/examples/kitchen-sink/src/server.ts +11 -12
- package/examples/kitchen-sink/src/stdio.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
- package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
- package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
- package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
- package/examples/kitchen-sink/test/request-policy.test.ts +39 -3
- package/examples/kitchen-sink/test/routes.test.ts +3 -3
- package/examples/live-rpc/README.md +8 -49
- package/examples/live-rpc/src/browser.ts +8 -41
- package/examples/live-rpc/src/chat/Chat.ts +9 -3
- package/examples/live-rpc/src/chat/room.ts +1 -1
- package/examples/live-rpc/src/client.ts +2 -3
- package/examples/live-rpc/src/page.ts +34 -0
- package/examples/live-rpc/src/routes.ts +6 -7
- package/examples/live-rpc/src/server.ts +6 -4
- package/examples/live-rpc/test/browser.test.ts +6 -5
- package/examples/live-rpc/test/routes.test.ts +16 -15
- package/examples/live-rpc/test/server.test.ts +1 -1
- package/examples/mcp-tools/README.md +22 -45
- package/examples/mcp-tools/src/printers/Printers.ts +3 -7
- package/examples/mcp-tools/src/routes.ts +0 -7
- package/examples/mcp-tools/src/server.ts +1 -1
- package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
- package/examples/mcp-tools/test/routes.test.ts +2 -3
- package/examples/notes-service/README.md +9 -43
- package/examples/notes-service/src/cli.ts +0 -3
- package/examples/notes-service/src/commands.ts +7 -17
- package/examples/notes-service/src/identity/authorize.ts +2 -5
- package/examples/notes-service/src/identity/verifier.ts +3 -4
- package/examples/notes-service/src/notes/Notes.ts +2 -6
- package/examples/notes-service/src/notes/store.ts +5 -1
- package/examples/notes-service/src/routes.ts +6 -8
- package/examples/notes-service/src/server.ts +4 -3
- package/examples/notes-service/src/stdio.ts +4 -3
- package/examples/notes-service/test/commands.test.ts +1 -1
- package/examples/notes-service/test/notes/as.ts +6 -0
- package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
- package/examples/notes-service/test/notes/in-process.test.ts +50 -0
- package/examples/notes-service/test/notes/notes.test.ts +3 -42
- package/examples/notes-service/test/routes.test.ts +2 -2
- package/examples/quickstart/README.md +1 -1
- package/examples/quickstart/src/client.ts +1 -0
- package/examples/quickstart/src/server.ts +2 -4
- package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
- package/package.json +12 -16
- package/docs/Action-in-process.md +0 -93
- package/docs/CONTEXT.md +0 -46
- package/docs/guarantees.md +0 -44
package/dist/cli/commands.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { literalValues, members, unsuspended } from "../contract/ast.mjs";
|
|
2
2
|
import { InvalidInput } from "../contract/errors.mjs";
|
|
3
3
|
import { assertDistinct } from "../contract/names.mjs";
|
|
4
|
+
import { errorSchemas } from "../contract/declared.mjs";
|
|
4
5
|
import { decodeInput } from "../contract/call.mjs";
|
|
5
6
|
import { withStderrConsole } from "../stdio/console.mjs";
|
|
6
7
|
import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST, Stdio, Stream } from "effect";
|
|
7
8
|
import { CliError, Command, Flag, Param } from "effect/cli";
|
|
8
|
-
|
|
9
|
+
// src/cli/commands.ts
|
|
9
10
|
const kebab = (name) => {
|
|
10
11
|
const kebabbed = name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
11
12
|
return kebabbed === "" ? name : kebabbed;
|
|
@@ -152,28 +153,30 @@ const fieldParams = (encoded, input, positional, aliases) => {
|
|
|
152
153
|
});
|
|
153
154
|
};
|
|
154
155
|
const positionalOrder = (params, positional) => {
|
|
155
|
-
assertDistinct("positional argument", positional, (field) => field);
|
|
156
|
+
assertDistinct("positional argument", positional, (field) => field, { fix: "list each field once" });
|
|
156
157
|
const ordered = positional.map((field) => {
|
|
157
158
|
const found = params.find((param) => param.field === field);
|
|
158
|
-
if (found === void 0) throw new Error(`Not an input field: ${field}`);
|
|
159
|
+
if (found === void 0) throw new Error(`Not an input field: ${field} (positional names fields of the action's input)`);
|
|
159
160
|
return found;
|
|
160
161
|
});
|
|
161
162
|
const firstOptional = ordered.findIndex((param) => param.optional);
|
|
162
163
|
const misplaced = ordered.slice(firstOptional === -1 ? ordered.length : firstOptional).find((param) => !param.optional);
|
|
163
|
-
if (misplaced !== void 0) throw new Error(`Required positional argument after an optional one: ${misplaced.field}`);
|
|
164
|
+
if (misplaced !== void 0) throw new Error(`Required positional argument after an optional one: ${misplaced.field} (list every required field first)`);
|
|
164
165
|
const repeatedBeforeLast = ordered.slice(0, -1).find((param) => param.repeats);
|
|
165
|
-
if (repeatedBeforeLast !== void 0) throw new Error(`Repeated positional argument before another one: ${repeatedBeforeLast.field}`);
|
|
166
|
+
if (repeatedBeforeLast !== void 0) throw new Error(`Repeated positional argument before another one: ${repeatedBeforeLast.field} (list an array field last)`);
|
|
166
167
|
return ordered;
|
|
167
168
|
};
|
|
168
169
|
const wholeInputFlag = (encoded) => Flag.String("input").pipe(Flag.withSchema(jsonOfAcceptedKindOrText(encoded)), Flag.withDescription("Whole action input as JSON; omitted, {}"), Flag.optional);
|
|
169
170
|
const printedOutput = (action, value, render) => Effect.map(Effect.orDie(Schema.encodeEffect(Schema.toCodecJson(action.success))(value)), (encoded) => render === void 0 && SchemaAST.isVoid(action.success.ast) ? Option.none() : Option.some(render === void 0 ? JSON.stringify(encoded, null, 2) : render(value)));
|
|
170
171
|
/**
|
|
171
172
|
* What a command fails with when its action fails: Effect CLI's `UserError`, typed with the
|
|
172
|
-
* action's failure as its `cause` and `reason`, and whose
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
173
|
+
* action's failure as its `cause` and `reason`, and whose `userMessage` is its JSON, or for a
|
|
174
|
+
* failure no schema encodes, as `command` says. A type only.
|
|
175
|
+
*
|
|
176
|
+
* **Gotchas**
|
|
177
|
+
*
|
|
178
|
+
* - `Command.run` has printed it before a recovery runs, so a recovery decides what follows, such as the exit code, not the text: for text of your own, provide `CliOutput.layer(Object.assign({}, CliOutput.defaultFormatter(), { formatError }))`, or pass `renderErrors: false` to `Command.run` and print every failure, the parser's too.
|
|
179
|
+
* - A run also fails with Effect CLI's own errors, such as a flag that does not parse, and only a `UserError` has a `userMessage`: match `Effect.catchTag("UserError", …)` before reading one.
|
|
177
180
|
*/
|
|
178
181
|
var UserError = class extends CliError.UserError {
|
|
179
182
|
/**
|
|
@@ -207,7 +210,7 @@ const userErrorOf = (errors) => {
|
|
|
207
210
|
}))));
|
|
208
211
|
};
|
|
209
212
|
const failingAsDerived = (errors) => {
|
|
210
|
-
const failure = userErrorOf(
|
|
213
|
+
const failure = userErrorOf(errorSchemas(errors));
|
|
211
214
|
return (self) => Effect.catchIf(self, (error) => !CliError.isCliError(error), failure);
|
|
212
215
|
};
|
|
213
216
|
const unreadableStdinField = (field, message) => new InvalidInput({
|
|
@@ -223,7 +226,7 @@ const fieldValueFromText = (field) => {
|
|
|
223
226
|
};
|
|
224
227
|
const stdinTextOf = (field) => Effect.gen(function* () {
|
|
225
228
|
const stdio = yield* Effect.serviceOption(Stdio.Stdio);
|
|
226
|
-
if (Option.isNone(stdio)) return yield* Effect.die(/* @__PURE__ */ new Error("Stdio is not provided"));
|
|
229
|
+
if (Option.isNone(stdio)) return yield* Effect.die(/* @__PURE__ */ new Error("Stdio is not provided (provide NodeServices.layer, or Stdio.layerTest in a test)"));
|
|
227
230
|
if (yield* stdio.value.stdinIsTerminal) return yield* unreadableStdinField(field, `${field} is read from stdin, which is a terminal: pipe it in`);
|
|
228
231
|
return (yield* stdio.value.stdin.pipe(Stream.decodeText(), Stream.mkString, Effect.mapError((error) => unreadableStdinField(field, `stdin cannot be read: ${error.message}`)))).replace(/[\r\n]+$/, "");
|
|
229
232
|
});
|
|
@@ -235,8 +238,8 @@ const inputConfig = (action, positional, aliases, stdin) => {
|
|
|
235
238
|
const params = fieldParams(encoded, unsuspended(action.input.ast), positional, aliases);
|
|
236
239
|
const fromStdin = Option.map(Option.fromUndefinedOr(stdin), (field) => {
|
|
237
240
|
const property = encoded.propertySignatures.find(({ name }) => name === field);
|
|
238
|
-
if (property === void 0 || SchemaAST.isOptional(property.type)) throw new Error(`Not a required input field: ${field}`);
|
|
239
|
-
if (positional.includes(field)) throw new Error(`Both positional and stdin: ${field}`);
|
|
241
|
+
if (property === void 0 || SchemaAST.isOptional(property.type)) throw new Error(`Not a required input field: ${field} (stdin names a required field of the action's input)`);
|
|
242
|
+
if (positional.includes(field)) throw new Error(`Both positional and stdin: ${field} (take it from one of them)`);
|
|
240
243
|
const description = params.find((param) => param.field === field)?.description;
|
|
241
244
|
return {
|
|
242
245
|
field,
|
|
@@ -246,7 +249,7 @@ const inputConfig = (action, positional, aliases, stdin) => {
|
|
|
246
249
|
});
|
|
247
250
|
const flagged = new Set(params.map(({ field }) => field).filter((field) => !positional.includes(field) && field !== stdin));
|
|
248
251
|
const stray = Object.keys(aliases).find((field) => !flagged.has(field));
|
|
249
|
-
if (stray !== void 0) throw new Error(`Not a flag's input field: ${stray}`);
|
|
252
|
+
if (stray !== void 0) throw new Error(`Not a flag's input field: ${stray} (aliases name fields read from flags, neither positional nor stdin)`);
|
|
250
253
|
return {
|
|
251
254
|
flags: Object.fromEntries(params.flatMap(({ field, param }) => flagged.has(field) ? [[field, param]] : [])),
|
|
252
255
|
positional: positionalOrder(params, positional),
|
|
@@ -260,9 +263,10 @@ const inputConfig = (action, positional, aliases, stdin) => {
|
|
|
260
263
|
stdinHelp: Option.getOrUndefined(Option.map(fromStdin, ({ help }) => help))
|
|
261
264
|
};
|
|
262
265
|
}
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
if (
|
|
266
|
+
const asWhole = "(the input is not a struct, so it is one --input flag: drop the option)";
|
|
267
|
+
if (positional.length > 0) throw new Error(`Positional arguments need named input fields: ${positional.join(", ")} ${asWhole}`);
|
|
268
|
+
if (Object.keys(aliases).length > 0) throw new Error(`Aliases need named input fields: ${Object.keys(aliases).join(", ")} ${asWhole}`);
|
|
269
|
+
if (stdin !== void 0) throw new Error(`Stdin needs named input fields: ${stdin} ${asWhole}`);
|
|
266
270
|
return {
|
|
267
271
|
flags: { input: wholeInputFlag(encoded) },
|
|
268
272
|
positional: [],
|
|
@@ -270,16 +274,37 @@ const inputConfig = (action, positional, aliases, stdin) => {
|
|
|
270
274
|
stdinHelp: void 0
|
|
271
275
|
};
|
|
272
276
|
};
|
|
277
|
+
const aliasFlag = (alias) => alias.length === 1 ? `-${alias}` : `--${alias}`;
|
|
273
278
|
const command = (action, execute, options, errors) => {
|
|
274
279
|
const name = options?.name ?? kebab(action.name);
|
|
280
|
+
const description = options?.description ?? action.description;
|
|
275
281
|
const render = options?.render;
|
|
276
282
|
const { flags, positional, decode, stdinHelp } = inputConfig(action, options?.positional ?? [], options?.aliases ?? {}, options?.stdin);
|
|
277
283
|
const failing = failingAsDerived(errors);
|
|
278
284
|
assertDistinct("flag", [
|
|
279
|
-
...Object.keys(flags).map((field) => [
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
...Object.keys(flags).map((field) => [
|
|
286
|
+
kebab(field),
|
|
287
|
+
`--${kebab(field)}`,
|
|
288
|
+
`field ${field}`
|
|
289
|
+
]),
|
|
290
|
+
...render === void 0 ? [] : [[
|
|
291
|
+
"json",
|
|
292
|
+
"--json",
|
|
293
|
+
"render's --json"
|
|
294
|
+
]],
|
|
295
|
+
...Object.entries(options?.aliases ?? {}).flatMap(([field, alias]) => {
|
|
296
|
+
const bare = Predicate.isString(alias) ? alias.replace(/^-+/, "") : void 0;
|
|
297
|
+
return bare === void 0 ? [] : [[
|
|
298
|
+
bare,
|
|
299
|
+
aliasFlag(bare),
|
|
300
|
+
`alias of field ${field}`
|
|
301
|
+
]];
|
|
302
|
+
})
|
|
303
|
+
], ([key]) => key, {
|
|
304
|
+
labelOf: ([, label]) => label,
|
|
305
|
+
claimantOf: ([, , claimant]) => claimant,
|
|
306
|
+
fix: render === void 0 ? "rename the field or the alias" : "rename the field or the alias, or drop `render`"
|
|
307
|
+
});
|
|
283
308
|
const args = positional.map(({ param }) => param);
|
|
284
309
|
const parsedFields = (input, values) => ({
|
|
285
310
|
...input,
|
|
@@ -297,7 +322,6 @@ const command = (action, execute, options, errors) => {
|
|
|
297
322
|
args,
|
|
298
323
|
json: renderedCommandJsonFlag
|
|
299
324
|
}, ({ input, args: values, json }) => run(parsedFields(input, values), json ? void 0 : render));
|
|
300
|
-
return stdinHelp === void 0 ? Command.withDescription(command,
|
|
325
|
+
return stdinHelp === void 0 ? Command.withDescription(command, description) : command.pipe(Command.withDescription(`${description}\n\n${stdinHelp}`), Command.withShortDescription(description));
|
|
301
326
|
};
|
|
302
|
-
//#endregion
|
|
303
327
|
export { UserError, command, failingAsDerived, kebab };
|
package/dist/contract/ast.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { SchemaAST } from "effect";
|
|
2
|
-
|
|
2
|
+
// src/contract/ast.ts
|
|
3
3
|
const unsuspended = (ast) => SchemaAST.isSuspend(ast) ? unsuspended(ast.thunk()) : ast;
|
|
4
4
|
const members = (ast) => {
|
|
5
5
|
const resolved = unsuspended(ast);
|
|
6
6
|
return SchemaAST.isUnion(resolved) ? resolved.types.flatMap(members) : [resolved];
|
|
7
7
|
};
|
|
8
8
|
const literalValues = (ast) => SchemaAST.isLiteral(ast) ? [ast.literal] : SchemaAST.isEnum(ast) ? ast.enums.map(([, value]) => value) : [void 0];
|
|
9
|
-
//#endregion
|
|
10
9
|
export { literalValues, members, unsuspended };
|
package/dist/contract/call.d.mts
CHANGED
|
@@ -2,12 +2,10 @@ import "./errors.mjs";
|
|
|
2
2
|
import "./implementation.mjs";
|
|
3
3
|
import { Any } from "../Action.mjs";
|
|
4
4
|
import { Effect, Schema } from "effect";
|
|
5
|
-
//#region src/contract/call.d.ts
|
|
6
5
|
/**
|
|
7
6
|
* A call of `A` answering `R`. It may leave its input out when `{}` is a valid encoded input,
|
|
8
7
|
* such as for an action declared without `input` or one whose every field has a decoding
|
|
9
8
|
* default, and then sends the input `{}` decodes to. Every client shares this rule:
|
|
10
9
|
* `ActionHttp.client`, `ActionRpc.client`, `ActionMcp.client` and `Action.client`.
|
|
11
10
|
*/
|
|
12
|
-
export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
|
|
13
|
-
//#endregion
|
|
11
|
+
export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
|
package/dist/contract/call.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InvalidInput } from "./errors.mjs";
|
|
2
2
|
import { Effect, Schema } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/contract/call.ts
|
|
4
4
|
/**
|
|
5
5
|
* How HTTP, RPC, MCP and a command decode a request's input: reporting every issue, and refusing a
|
|
6
6
|
* field the input does not declare.
|
|
@@ -19,5 +19,4 @@ const checked = (action, send) => {
|
|
|
19
19
|
const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(action.input), { errors: "all" });
|
|
20
20
|
return (...args) => inputOf(action, args).pipe(Effect.tap(encode), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap(send));
|
|
21
21
|
};
|
|
22
|
-
//#endregion
|
|
23
22
|
export { checked, decodeInput, inputOf, inputParseOptions };
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import "../Action.mjs";
|
|
2
|
-
//#region src/contract/caller.d.ts
|
|
3
2
|
/**
|
|
4
3
|
* The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
|
|
5
4
|
* authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
|
|
6
5
|
* agree on a name.
|
|
7
6
|
*/
|
|
8
|
-
export declare const Anyone: unique symbol;
|
|
9
|
-
//#endregion
|
|
7
|
+
export declare const Anyone: unique symbol;
|
package/dist/contract/caller.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/contract/caller.ts
|
|
2
2
|
/**
|
|
3
3
|
* The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
|
|
4
4
|
* authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
const Anyone = Symbol.for("@gjermundgaraba/effect-actions/Anyone");
|
|
8
8
|
const assertAuthentication = (actions, authentication) => {
|
|
9
|
-
for (const action of actions) if (action.caller !== Anyone && (authentication === void 0 || action.caller.key !== authentication.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor`);
|
|
9
|
+
for (const action of actions) if (action.caller !== Anyone && (authentication === void 0 || action.caller.key !== authentication.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor (give it the authentication whose identity the action's caller names)`);
|
|
10
10
|
};
|
|
11
|
-
//#endregion
|
|
12
11
|
export { Anyone, assertAuthentication };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import "./caller.mjs";
|
|
2
2
|
import { Errors } from "../Action.mjs";
|
|
3
3
|
import { Types } from "effect";
|
|
4
|
-
//#region src/contract/declared.d.ts
|
|
5
4
|
type ListOf<G> = G extends Errors ? G : readonly [G];
|
|
6
5
|
/**
|
|
7
6
|
* The errors options `O` declare, as at run time: those `error` always gives, or, where it may
|
|
@@ -24,5 +23,4 @@ export interface Declaring {
|
|
|
24
23
|
readonly error: Errors;
|
|
25
24
|
} | undefined;
|
|
26
25
|
}
|
|
27
|
-
//#endregion
|
|
28
26
|
export {};
|
|
@@ -2,14 +2,23 @@ import { literalValues, members } from "./ast.mjs";
|
|
|
2
2
|
import { Anyone, assertAuthentication } from "./caller.mjs";
|
|
3
3
|
import { builtIns } from "./errors.mjs";
|
|
4
4
|
import { assertOnce } from "./names.mjs";
|
|
5
|
+
import { httpApiStatusOf } from "./status.mjs";
|
|
5
6
|
import { Predicate, Schema, SchemaAST } from "effect";
|
|
6
|
-
|
|
7
|
+
// src/contract/declared.ts
|
|
7
8
|
const errorList = (error) => error === void 0 ? [] : Schema.isSchema(error) ? [error] : error;
|
|
8
|
-
const
|
|
9
|
+
const ownsNothing = (ast) => httpApiStatusOf(ast) === void 0 && ast.checks === void 0 && ast.encoding === void 0;
|
|
10
|
+
const schemasOf = (ast) => !ownsNothing(ast) ? [ast] : SchemaAST.isSuspend(ast) ? schemasOf(ast.thunk()) : SchemaAST.isUnion(ast) ? ast.types.flatMap(schemasOf) : [ast];
|
|
11
|
+
/** `errors` as the schemas a call decodes a failure with, in order, each once. */
|
|
12
|
+
const errorSchemas = (errors) => {
|
|
13
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
14
|
+
for (const error of errors) for (const ast of schemasOf(error.ast)) if (!schemas.has(ast)) schemas.set(ast, ast === error.ast ? error : Schema.make(ast));
|
|
15
|
+
return [...schemas.values()];
|
|
16
|
+
};
|
|
17
|
+
const projectedErrors = (action, surface = []) => errorSchemas([
|
|
9
18
|
...builtIns,
|
|
10
19
|
...action.error,
|
|
11
20
|
...surface
|
|
12
|
-
])
|
|
21
|
+
]);
|
|
13
22
|
/**
|
|
14
23
|
* Every error a binding's call of `action` declares: the built-in ones, the action's, the
|
|
15
24
|
* binding's and, where `action` is protected, its authentication's.
|
|
@@ -19,24 +28,27 @@ const encodedTags = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member)
|
|
|
19
28
|
const tag = SchemaAST.isObjects(member) ? member.propertySignatures.find((property) => property.name === "_tag")?.type : void 0;
|
|
20
29
|
return tag === void 0 ? [] : members(tag).flatMap(literalValues).filter(Predicate.isString);
|
|
21
30
|
});
|
|
22
|
-
const builtInTags = new Set(builtIns.flatMap((
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
const builtInTags = new Set(builtIns.flatMap((schema) => encodedTags(schema.ast)));
|
|
32
|
+
/** Throws where two of the schemas in `errors` encode with one `_tag`. */
|
|
33
|
+
const assertDistinctTags = (what, errors) => {
|
|
34
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35
|
+
for (const schema of errorSchemas(errors)) for (const tag of new Set(encodedTags(schema.ast))) {
|
|
36
|
+
if (seen.has(tag)) throw new Error(`${what}: error _tag "${tag}" names two error schemas, which a client cannot tell apart (reuse one schema value, or merge them into one whose fields tell them apart${builtInTags.has(tag) ? "; for a built-in's tag, fail with the built-in itself" : ""})`);
|
|
37
|
+
seen.add(tag);
|
|
38
|
+
}
|
|
26
39
|
};
|
|
27
40
|
/**
|
|
28
|
-
* The checks `ActionHttp.make` and `ActionRpc.make` share: each action once,
|
|
29
|
-
*
|
|
41
|
+
* The checks `ActionHttp.make` and `ActionRpc.make` share: each action once, the descriptor every
|
|
42
|
+
* protected action names, and each call's errors distinct by `_tag`. Returns the binding's `error`, as a list, and `authentication`.
|
|
30
43
|
*/
|
|
31
44
|
const bindingOf = (what, actions, options) => {
|
|
32
45
|
assertOnce("action", actions);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
assertAuthentication(actions, options.authentication);
|
|
36
|
-
return {
|
|
37
|
-
error,
|
|
46
|
+
const bound = {
|
|
47
|
+
error: errorList(options.error),
|
|
38
48
|
authentication: options.authentication
|
|
39
49
|
};
|
|
50
|
+
assertAuthentication(actions, bound.authentication);
|
|
51
|
+
for (const action of actions) assertDistinctTags(`${what} of "${action.name}"`, bindingErrors(action, bound));
|
|
52
|
+
return bound;
|
|
40
53
|
};
|
|
41
|
-
|
|
42
|
-
export { assertOwnTags, bindingErrors, bindingOf, errorList, projectedErrors };
|
|
54
|
+
export { assertDistinctTags, bindingErrors, bindingOf, errorList, errorSchemas, projectedErrors };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
//#region src/contract/errors.d.ts
|
|
3
2
|
declare const Issue: Schema.Struct<{
|
|
4
3
|
readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
|
|
5
4
|
readonly message: Schema.String;
|
|
@@ -18,27 +17,45 @@ declare const InvalidInput_base: Schema.Class<InvalidInput, Schema.TaggedStruct<
|
|
|
18
17
|
}>>>;
|
|
19
18
|
}>, import("effect/Cause").YieldableError>;
|
|
20
19
|
/**
|
|
21
|
-
* The request's input does not decode against the action's input
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* The request's input does not decode against the action's input: its message is the schema's
|
|
21
|
+
* own description of what is wrong, and `issues` the same an issue at a time, for a caller that
|
|
22
|
+
* points at a field. A handler fails with it, naming `issues` of its own, for input that decodes
|
|
23
|
+
* but cannot be served, instead of declaring a second error for bad input.
|
|
24
|
+
*
|
|
25
|
+
* **Gotchas**
|
|
26
|
+
*
|
|
27
|
+
* - Raised by the library for input that does not decode, before `authorize` and the handler; what each surface answers is on `ActionHttp.layer`, `ActionMcp.layerHttp` and `runStdio`, `ActionRpc.layer`, `ActionToolkit.make` and `ActionCli.command`.
|
|
28
|
+
* - Failed by a handler, it is answered as a declared error on every surface.
|
|
29
|
+
* - What receives encoded input, a route, an MCP tool, an rpc, a CLI flag or `--input`, refuses an undeclared field with it, nested ones too, naming its path; a typed client drops such a field instead, as its JSDoc says.
|
|
30
|
+
* - Its message and `issues` name paths and what the schema expects, never a field's value; a path names every key it passes through, a record's keys included, so never key a record by a secret such as an email or a token.
|
|
31
|
+
* - A check's message, or a schema's `message` annotation, is sent as written: never put the input in one.
|
|
25
32
|
*/
|
|
26
33
|
export declare class InvalidInput extends InvalidInput_base {
|
|
27
34
|
private readonly "~declared in the action's error, or built in";
|
|
28
35
|
/**
|
|
29
36
|
* The `InvalidInput` for input that did not decode, as every surface answers it: the
|
|
30
37
|
* schema's message, and each of its issues by path, for code that decodes input of its own,
|
|
31
|
-
* such as a header or a route parameter
|
|
32
|
-
*
|
|
33
|
-
* string form, `Symbol(name)`, as JSON has no symbols, which
|
|
34
|
-
* that name.
|
|
38
|
+
* such as a header or a route parameter:
|
|
39
|
+
* `Schema.decodeUnknownEffect(schema)(value, { errors: "all" }).pipe(Effect.mapError(Action.InvalidInput.fromSchemaError))`.
|
|
40
|
+
* A symbol key is sent as its string form, `Symbol(name)`, as JSON has no symbols, which
|
|
41
|
+
* tells it from a string key of that name.
|
|
35
42
|
*/
|
|
36
43
|
static readonly fromSchemaError: ({ issue, message }: Schema.SchemaError) => InvalidInput;
|
|
37
44
|
}
|
|
38
45
|
declare const Unauthenticated_base: Schema.Class<Unauthenticated, Schema.TaggedStruct<"Unauthenticated", {
|
|
39
46
|
readonly message: Schema.withConstructorDefault<Schema.String>;
|
|
40
47
|
}>, import("effect/Cause").YieldableError>;
|
|
41
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The caller is not authenticated: authentication refuses the credential, or an implementation's
|
|
50
|
+
* `authorize` or a handler fails with it.
|
|
51
|
+
*
|
|
52
|
+
* **Gotchas**
|
|
53
|
+
*
|
|
54
|
+
* - Over HTTP its JSON with a 401: from authentication before the body is read, from a protected action's `authorize` or handler after the input is decoded, each with the scheme's challenge, none for an API key.
|
|
55
|
+
* - Over MCP: from authentication, the same HTTP 401 in place of a JSON-RPC answer, before the arguments are decoded; from a protected tool's `authorize` or handler, the HTTP 401 with the challenge on an endpoint under Bearer, otherwise an `isError: true` tool result in a 200.
|
|
56
|
+
* - Over RPC the typed failure, never a status; on a Toolkit and in a CLI as a declared error.
|
|
57
|
+
* - From another service's client, such as `ActionHttp.client`, one passed on tells a caller whose token is valid to sign in again: map it to an error the action declares, or to a defect.
|
|
58
|
+
*/
|
|
42
59
|
export declare class Unauthenticated extends Unauthenticated_base {
|
|
43
60
|
private readonly "~declared in the action's error, or built in";
|
|
44
61
|
}
|
|
@@ -47,26 +64,33 @@ declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbi
|
|
|
47
64
|
readonly scopes: Schema.optionalKey<Schema.NonEmptyArray<Schema.String>>;
|
|
48
65
|
}>, import("effect/Cause").YieldableError>;
|
|
49
66
|
/**
|
|
50
|
-
* The caller may not run this action:
|
|
51
|
-
* when given, are every OAuth scope the call needs: a refusal naming
|
|
52
|
-
* the `insufficient_scope` challenge an OAuth client re-authorizes on,
|
|
53
|
-
*
|
|
54
|
-
*
|
|
67
|
+
* The caller may not run this action: authentication refuses the credential, an
|
|
68
|
+
* implementation's `authorize` refuses, or a handler fails with it. `scopes`, when given, are every OAuth scope the call needs: a refusal naming
|
|
69
|
+
* them is answered with the `insufficient_scope` challenge an OAuth client re-authorizes on,
|
|
70
|
+
* which only a server publishing discovery can follow up, so name them only where
|
|
71
|
+
* re-authorizing can grant them; with fixed tokens, such as API keys, leave them out
|
|
72
|
+
* (`scopes: []` is a type error).
|
|
73
|
+
*
|
|
74
|
+
* **Gotchas**
|
|
75
|
+
*
|
|
76
|
+
* - Over HTTP its JSON with a 403: from authentication before the body is read, from `authorize` or a handler after the input is decoded; on a protected route under Bearer, one naming `scopes` also carries the challenge.
|
|
77
|
+
* - Over MCP, from authentication the HTTP 403 before the arguments are decoded; from `authorize` or a handler an `isError: true` tool result holding its JSON as text, in a **200**, not a 403; only on an endpoint under Bearer is a protected tool's refusal naming `scopes` the HTTP 403 with the challenge instead.
|
|
78
|
+
* - Over RPC the typed failure, never a status or a challenge; on a Toolkit and in a CLI as a declared error.
|
|
79
|
+
* - From another service's client, such as `ActionHttp.client`, one passed on becomes this server's own refusal: map it to an error the action declares, or to a defect.
|
|
55
80
|
*/
|
|
56
81
|
export declare class Forbidden extends Forbidden_base {
|
|
57
82
|
private readonly "~declared in the action's error, or built in";
|
|
58
83
|
}
|
|
59
84
|
/**
|
|
60
85
|
* What authentication or an implementation's `authorize` refuses a caller with, instead of
|
|
61
|
-
* running a handler.
|
|
62
|
-
* schema, it tells a refusal apart: `Schema.is(Action.Refusal)(error)`.
|
|
86
|
+
* running a handler. As a schema, it tells a refusal apart: `Schema.is(Action.Refusal)(error)`.
|
|
63
87
|
*/
|
|
64
88
|
export declare const Refusal: Schema.Union<readonly [typeof Unauthenticated, typeof Forbidden]>;
|
|
65
89
|
/** A refusal: `Unauthenticated | Forbidden`. */
|
|
66
90
|
export type Refusal = (typeof Refusal)["Type"];
|
|
67
91
|
/**
|
|
68
92
|
* The failures every endpoint and tool declares beyond its action's own, and any handler may
|
|
69
|
-
* fail with: bad input, and every refusal.
|
|
93
|
+
* fail with: bad input, and every refusal. An action or a binding need not declare them; one listed is accepted.
|
|
70
94
|
*/
|
|
71
95
|
export declare const builtIns: readonly [typeof InvalidInput, typeof Unauthenticated, typeof Forbidden];
|
|
72
96
|
/** The built-in failures' schemas. */
|
|
@@ -75,5 +99,4 @@ export type BuiltIns = (typeof builtIns)[number];
|
|
|
75
99
|
export declare const BuiltIn: Schema.Union<readonly [typeof InvalidInput, typeof Unauthenticated, typeof Forbidden]>;
|
|
76
100
|
/** A built-in failure: `InvalidInput | Refusal`. */
|
|
77
101
|
export type BuiltIn = (typeof BuiltIn)["Type"];
|
|
78
|
-
//#endregion
|
|
79
102
|
export { Issue };
|
package/dist/contract/errors.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Predicate, Schema, SchemaIssue } from "effect";
|
|
2
|
-
|
|
2
|
+
// src/contract/errors.ts
|
|
3
3
|
const defaultedMessage = (fallback) => Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(fallback)));
|
|
4
4
|
const Issue = Schema.Struct({
|
|
5
5
|
path: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
|
|
@@ -7,10 +7,18 @@ const Issue = Schema.Struct({
|
|
|
7
7
|
});
|
|
8
8
|
const standardIssues = SchemaIssue.makeFormatterStandardSchemaV1();
|
|
9
9
|
/**
|
|
10
|
-
* The request's input does not decode against the action's input
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The request's input does not decode against the action's input: its message is the schema's
|
|
11
|
+
* own description of what is wrong, and `issues` the same an issue at a time, for a caller that
|
|
12
|
+
* points at a field. A handler fails with it, naming `issues` of its own, for input that decodes
|
|
13
|
+
* but cannot be served, instead of declaring a second error for bad input.
|
|
14
|
+
*
|
|
15
|
+
* **Gotchas**
|
|
16
|
+
*
|
|
17
|
+
* - Raised by the library for input that does not decode, before `authorize` and the handler; what each surface answers is on `ActionHttp.layer`, `ActionMcp.layerHttp` and `runStdio`, `ActionRpc.layer`, `ActionToolkit.make` and `ActionCli.command`.
|
|
18
|
+
* - Failed by a handler, it is answered as a declared error on every surface.
|
|
19
|
+
* - What receives encoded input, a route, an MCP tool, an rpc, a CLI flag or `--input`, refuses an undeclared field with it, nested ones too, naming its path; a typed client drops such a field instead, as its JSDoc says.
|
|
20
|
+
* - Its message and `issues` name paths and what the schema expects, never a field's value; a path names every key it passes through, a record's keys included, so never key a record by a secret such as an email or a token.
|
|
21
|
+
* - A check's message, or a schema's `message` annotation, is sent as written: never put the input in one.
|
|
14
22
|
*/
|
|
15
23
|
var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput", {
|
|
16
24
|
message: defaultedMessage("The input does not match the action's input."),
|
|
@@ -19,10 +27,10 @@ var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput
|
|
|
19
27
|
/**
|
|
20
28
|
* The `InvalidInput` for input that did not decode, as every surface answers it: the
|
|
21
29
|
* schema's message, and each of its issues by path, for code that decodes input of its own,
|
|
22
|
-
* such as a header or a route parameter
|
|
23
|
-
*
|
|
24
|
-
* string form, `Symbol(name)`, as JSON has no symbols, which
|
|
25
|
-
* that name.
|
|
30
|
+
* such as a header or a route parameter:
|
|
31
|
+
* `Schema.decodeUnknownEffect(schema)(value, { errors: "all" }).pipe(Effect.mapError(Action.InvalidInput.fromSchemaError))`.
|
|
32
|
+
* A symbol key is sent as its string form, `Symbol(name)`, as JSON has no symbols, which
|
|
33
|
+
* tells it from a string key of that name.
|
|
26
34
|
*/
|
|
27
35
|
static fromSchemaError = ({ issue, message }) => new InvalidInput({
|
|
28
36
|
message,
|
|
@@ -35,16 +43,34 @@ var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput
|
|
|
35
43
|
}))
|
|
36
44
|
});
|
|
37
45
|
};
|
|
38
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* The caller is not authenticated: authentication refuses the credential, or an implementation's
|
|
48
|
+
* `authorize` or a handler fails with it.
|
|
49
|
+
*
|
|
50
|
+
* **Gotchas**
|
|
51
|
+
*
|
|
52
|
+
* - Over HTTP its JSON with a 401: from authentication before the body is read, from a protected action's `authorize` or handler after the input is decoded, each with the scheme's challenge, none for an API key.
|
|
53
|
+
* - Over MCP: from authentication, the same HTTP 401 in place of a JSON-RPC answer, before the arguments are decoded; from a protected tool's `authorize` or handler, the HTTP 401 with the challenge on an endpoint under Bearer, otherwise an `isError: true` tool result in a 200.
|
|
54
|
+
* - Over RPC the typed failure, never a status; on a Toolkit and in a CLI as a declared error.
|
|
55
|
+
* - From another service's client, such as `ActionHttp.client`, one passed on tells a caller whose token is valid to sign in again: map it to an error the action declares, or to a defect.
|
|
56
|
+
*/
|
|
39
57
|
var Unauthenticated = class extends Schema.TaggedError()("Unauthenticated", { message: defaultedMessage("Authentication is required.") }, { httpApiStatus: 401 }) {};
|
|
40
58
|
const scopeToken = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
41
59
|
const ScopeToken = Schema.String.check(Schema.isPattern(scopeToken));
|
|
42
60
|
/**
|
|
43
|
-
* The caller may not run this action:
|
|
44
|
-
* when given, are every OAuth scope the call needs: a refusal naming
|
|
45
|
-
* the `insufficient_scope` challenge an OAuth client re-authorizes on,
|
|
46
|
-
*
|
|
47
|
-
*
|
|
61
|
+
* The caller may not run this action: authentication refuses the credential, an
|
|
62
|
+
* implementation's `authorize` refuses, or a handler fails with it. `scopes`, when given, are every OAuth scope the call needs: a refusal naming
|
|
63
|
+
* them is answered with the `insufficient_scope` challenge an OAuth client re-authorizes on,
|
|
64
|
+
* which only a server publishing discovery can follow up, so name them only where
|
|
65
|
+
* re-authorizing can grant them; with fixed tokens, such as API keys, leave them out
|
|
66
|
+
* (`scopes: []` is a type error).
|
|
67
|
+
*
|
|
68
|
+
* **Gotchas**
|
|
69
|
+
*
|
|
70
|
+
* - Over HTTP its JSON with a 403: from authentication before the body is read, from `authorize` or a handler after the input is decoded; on a protected route under Bearer, one naming `scopes` also carries the challenge.
|
|
71
|
+
* - Over MCP, from authentication the HTTP 403 before the arguments are decoded; from `authorize` or a handler an `isError: true` tool result holding its JSON as text, in a **200**, not a 403; only on an endpoint under Bearer is a protected tool's refusal naming `scopes` the HTTP 403 with the challenge instead.
|
|
72
|
+
* - Over RPC the typed failure, never a status or a challenge; on a Toolkit and in a CLI as a declared error.
|
|
73
|
+
* - From another service's client, such as `ActionHttp.client`, one passed on becomes this server's own refusal: map it to an error the action declares, or to a defect.
|
|
48
74
|
*/
|
|
49
75
|
var Forbidden = class extends Schema.TaggedError()("Forbidden", {
|
|
50
76
|
message: defaultedMessage("Not allowed."),
|
|
@@ -53,16 +79,14 @@ var Forbidden = class extends Schema.TaggedError()("Forbidden", {
|
|
|
53
79
|
const refusals = [Unauthenticated, Forbidden];
|
|
54
80
|
/**
|
|
55
81
|
* What authentication or an implementation's `authorize` refuses a caller with, instead of
|
|
56
|
-
* running a handler.
|
|
57
|
-
* schema, it tells a refusal apart: `Schema.is(Action.Refusal)(error)`.
|
|
82
|
+
* running a handler. As a schema, it tells a refusal apart: `Schema.is(Action.Refusal)(error)`.
|
|
58
83
|
*/
|
|
59
84
|
const Refusal = Schema.Union(refusals);
|
|
60
85
|
/**
|
|
61
86
|
* The failures every endpoint and tool declares beyond its action's own, and any handler may
|
|
62
|
-
* fail with: bad input, and every refusal.
|
|
87
|
+
* fail with: bad input, and every refusal. An action or a binding need not declare them; one listed is accepted.
|
|
63
88
|
*/
|
|
64
89
|
const builtIns = [InvalidInput, ...refusals];
|
|
65
90
|
/** The built-in failures as one schema: `Schema.is(Action.BuiltIn)(error)`. */
|
|
66
91
|
const BuiltIn = Schema.Union(builtIns);
|
|
67
|
-
//#endregion
|
|
68
92
|
export { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated, builtIns, scopeToken };
|
package/dist/contract/hints.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// src/contract/hints.ts
|
|
2
2
|
/**
|
|
3
|
-
* An action's MCP tool hints, MCP's defaults where its `mcp` leaves one out
|
|
4
|
-
*
|
|
3
|
+
* An action's MCP tool hints, MCP's defaults where its `mcp` leaves one out, except that a read
|
|
4
|
+
* is not destructive where MCP's default would say it is: no tool idempotent and every tool
|
|
5
|
+
* open-world. A given value passes through, as MCP
|
|
5
6
|
* calls `destructiveHint` and `idempotentHint` meaningless on a read-only tool.
|
|
6
7
|
*/
|
|
7
8
|
const hintsOf = ({ readOnly, mcp }) => ({
|
|
@@ -10,5 +11,4 @@ const hintsOf = ({ readOnly, mcp }) => ({
|
|
|
10
11
|
idempotentHint: mcp.idempotentHint ?? false,
|
|
11
12
|
openWorldHint: mcp.openWorldHint ?? true
|
|
12
13
|
});
|
|
13
|
-
//#endregion
|
|
14
14
|
export { hintsOf };
|
|
@@ -2,7 +2,6 @@ import { Anyone } from "./caller.mjs";
|
|
|
2
2
|
import { Refusal } from "./errors.mjs";
|
|
3
3
|
import { Any } from "../Action.mjs";
|
|
4
4
|
import { Context, Effect, Layer, Scope } from "effect";
|
|
5
|
-
//#region src/contract/implementation.d.ts
|
|
6
5
|
/** Decoded values at the dispatch boundary every surface shares. */
|
|
7
6
|
export type ErasedValue = Any["input"]["Type"];
|
|
8
7
|
/**
|
|
@@ -28,22 +27,18 @@ interface Memoized<S, E, R> {
|
|
|
28
27
|
}
|
|
29
28
|
/**
|
|
30
29
|
* Actions bound to their handlers and their authorizer: everything one `Action.implement` call
|
|
31
|
-
* produced, one builder serving every surface.
|
|
32
|
-
* The private fields make this class nominal: a structurally similar object, including one made by
|
|
30
|
+
* produced, one builder serving every surface. The private fields make this class nominal: a structurally similar object, including one made by
|
|
33
31
|
* spreading an implementation, is not an implementation. Its type parameters may be written
|
|
34
32
|
* out, as a package emitting declarations prints them: `A` its actions, `RequestServices` each
|
|
35
|
-
* action name's handler's
|
|
33
|
+
* action name's handler's request-time requirements, `EX` and `RX` the failures and services of
|
|
36
34
|
* its handlers' builder, `RA` what its authorizer reads per request, and `EAX` and `RAX` the
|
|
37
|
-
* failures and services of the authorizer's builder.
|
|
38
|
-
* authorization. A plain authorization callback builds nothing.
|
|
35
|
+
* failures and services of the authorizer's builder.
|
|
39
36
|
*/
|
|
40
37
|
export declare class Implementation<A extends Any, RequestServices extends {
|
|
41
38
|
readonly [name: string]: unknown;
|
|
42
39
|
}, EX, RX, RA = never, EAX = never, RAX = never> {
|
|
43
40
|
#private;
|
|
44
|
-
/**
|
|
45
|
-
readonly actions: ReadonlyArray<A>;
|
|
46
|
-
/** Type-only: each action's handler's per-request requirements. */
|
|
41
|
+
/** Type-only: each action's handler's request-time requirements. */
|
|
47
42
|
readonly "~request": RequestServices;
|
|
48
43
|
/** Type-only: what building its handlers fails with. */
|
|
49
44
|
readonly "~buildError": EX;
|
|
@@ -55,9 +50,9 @@ export declare class Implementation<A extends Any, RequestServices extends {
|
|
|
55
50
|
readonly "~authorizeBuildError": EAX;
|
|
56
51
|
/** Type-only: what building its authorizer needs. */
|
|
57
52
|
readonly "~authorizeBuildServices": RAX;
|
|
58
|
-
constructor(
|
|
59
53
|
/** The contracts this implementation answers. */
|
|
60
|
-
actions: ReadonlyArray<A
|
|
54
|
+
readonly actions: ReadonlyArray<A>;
|
|
55
|
+
constructor(actions: ReadonlyArray<A>,
|
|
61
56
|
/** Builds each action paired with its handler: its own builder, or the one it shares. */
|
|
62
57
|
handlers: Memoized<Bound, EX, RX>,
|
|
63
58
|
/** Builds its authorizer. */
|
|
@@ -72,8 +67,7 @@ export declare class Implementation<A extends Any, RequestServices extends {
|
|
|
72
67
|
/**
|
|
73
68
|
* The builders of `app`, its handlers' and its authorizer's, as a layer. Effect memoizes each
|
|
74
69
|
* by reference within one layer graph, so every surface serving `app` there, and every
|
|
75
|
-
* implementation sharing its builder, shares one run.
|
|
76
|
-
* public instance type.
|
|
70
|
+
* implementation sharing its builder, shares one run.
|
|
77
71
|
*/
|
|
78
72
|
static layerOf(app: AnyImplementation): Layer.Layer<never, unknown, unknown>;
|
|
79
73
|
/**
|
|
@@ -192,5 +186,4 @@ export type BuildServices<App, Listed extends Any = OwnActions<App>> = App exten
|
|
|
192
186
|
readonly "~buildServices": infer RX;
|
|
193
187
|
readonly "~authorizeBuildServices": infer RAX;
|
|
194
188
|
} ? RX | ([Protected<Serving<App, Listed>>] extends [never] ? never : RAX) : never;
|
|
195
|
-
//#endregion
|
|
196
189
|
export {};
|