@gjermundgaraba/effect-actions 0.18.2 → 0.19.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 -47
- package/dist/Action.mjs +11 -14
- package/dist/ActionCli.d.mts +81 -42
- package/dist/ActionCli.mjs +52 -33
- package/dist/ActionHttp.d.mts +84 -43
- package/dist/ActionHttp.mjs +2 -2
- package/dist/ActionMcp.d.mts +54 -27
- package/dist/ActionMcp.mjs +5 -5
- package/dist/ActionRpc.d.mts +48 -35
- package/dist/ActionRpc.mjs +1 -1
- package/dist/ActionToolkit.d.mts +32 -13
- package/dist/ActionToolkit.mjs +10 -7
- package/dist/Authentication.d.mts +92 -40
- package/dist/Authentication.mjs +37 -35
- package/dist/Testing.d.mts +22 -24
- package/dist/authentication/provider.d.mts +1 -4
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -1
- package/dist/cli/commands.d.mts +34 -11
- package/dist/cli/commands.mjs +47 -22
- package/dist/contract/caller.mjs +1 -1
- package/dist/contract/declared.mjs +27 -14
- package/dist/contract/errors.d.mts +42 -17
- package/dist/contract/errors.mjs +42 -17
- package/dist/contract/hints.mjs +3 -2
- package/dist/contract/implementation.d.mts +5 -8
- package/dist/contract/implementation.mjs +29 -17
- package/dist/contract/media.d.mts +6 -2
- package/dist/contract/media.mjs +6 -2
- package/dist/contract/names.mjs +14 -12
- package/dist/contract/status.mjs +3 -10
- package/dist/http/client.d.mts +15 -4
- package/dist/http/client.mjs +1 -1
- package/dist/mcp/client.d.mts +21 -8
- package/dist/mcp/client.mjs +6 -8
- 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 +167 -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 -37
- 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 +3 -3
- 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 +1 -1
- 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 +1 -1
- 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,6 +1,7 @@
|
|
|
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";
|
|
@@ -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,7 @@ 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
327
|
//#endregion
|
|
303
328
|
export { UserError, command, failingAsDerived, kebab };
|
package/dist/contract/caller.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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
11
|
//#endregion
|
|
12
12
|
export { Anyone, assertAuthentication };
|
|
@@ -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
|
//#region 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,28 @@ 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
54
|
//#endregion
|
|
42
|
-
export {
|
|
55
|
+
export { assertDistinctTags, bindingErrors, bindingOf, errorList, errorSchemas, projectedErrors };
|
|
@@ -18,27 +18,45 @@ declare const InvalidInput_base: Schema.Class<InvalidInput, Schema.TaggedStruct<
|
|
|
18
18
|
}>>>;
|
|
19
19
|
}>, import("effect/Cause").YieldableError>;
|
|
20
20
|
/**
|
|
21
|
-
* The request's input does not decode against the action's input
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* The request's input does not decode against the action's input: its message is the schema's
|
|
22
|
+
* own description of what is wrong, and `issues` the same an issue at a time, for a caller that
|
|
23
|
+
* points at a field. A handler fails with it, naming `issues` of its own, for input that decodes
|
|
24
|
+
* but cannot be served, instead of declaring a second error for bad input.
|
|
25
|
+
*
|
|
26
|
+
* **Gotchas**
|
|
27
|
+
*
|
|
28
|
+
* - 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`.
|
|
29
|
+
* - Failed by a handler, it is answered as a declared error on every surface.
|
|
30
|
+
* - 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.
|
|
31
|
+
* - 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.
|
|
32
|
+
* - A check's message, or a schema's `message` annotation, is sent as written: never put the input in one.
|
|
25
33
|
*/
|
|
26
34
|
export declare class InvalidInput extends InvalidInput_base {
|
|
27
35
|
private readonly "~declared in the action's error, or built in";
|
|
28
36
|
/**
|
|
29
37
|
* The `InvalidInput` for input that did not decode, as every surface answers it: the
|
|
30
38
|
* 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.
|
|
39
|
+
* such as a header or a route parameter:
|
|
40
|
+
* `Schema.decodeUnknownEffect(schema)(value, { errors: "all" }).pipe(Effect.mapError(Action.InvalidInput.fromSchemaError))`.
|
|
41
|
+
* A symbol key is sent as its string form, `Symbol(name)`, as JSON has no symbols, which
|
|
42
|
+
* tells it from a string key of that name.
|
|
35
43
|
*/
|
|
36
44
|
static readonly fromSchemaError: ({ issue, message }: Schema.SchemaError) => InvalidInput;
|
|
37
45
|
}
|
|
38
46
|
declare const Unauthenticated_base: Schema.Class<Unauthenticated, Schema.TaggedStruct<"Unauthenticated", {
|
|
39
47
|
readonly message: Schema.withConstructorDefault<Schema.String>;
|
|
40
48
|
}>, import("effect/Cause").YieldableError>;
|
|
41
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The caller is not authenticated: authentication refuses the credential, or an implementation's
|
|
51
|
+
* `authorize` or a handler fails with it.
|
|
52
|
+
*
|
|
53
|
+
* **Gotchas**
|
|
54
|
+
*
|
|
55
|
+
* - 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.
|
|
56
|
+
* - 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.
|
|
57
|
+
* - Over RPC the typed failure, never a status; on a Toolkit and in a CLI as a declared error.
|
|
58
|
+
* - 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.
|
|
59
|
+
*/
|
|
42
60
|
export declare class Unauthenticated extends Unauthenticated_base {
|
|
43
61
|
private readonly "~declared in the action's error, or built in";
|
|
44
62
|
}
|
|
@@ -47,26 +65,33 @@ declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbi
|
|
|
47
65
|
readonly scopes: Schema.optionalKey<Schema.NonEmptyArray<Schema.String>>;
|
|
48
66
|
}>, import("effect/Cause").YieldableError>;
|
|
49
67
|
/**
|
|
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
|
-
*
|
|
68
|
+
* The caller may not run this action: authentication refuses the credential, an
|
|
69
|
+
* implementation's `authorize` refuses, or a handler fails with it. `scopes`, when given, are every OAuth scope the call needs: a refusal naming
|
|
70
|
+
* them is answered with the `insufficient_scope` challenge an OAuth client re-authorizes on,
|
|
71
|
+
* which only a server publishing discovery can follow up, so name them only where
|
|
72
|
+
* re-authorizing can grant them; with fixed tokens, such as API keys, leave them out
|
|
73
|
+
* (`scopes: []` is a type error).
|
|
74
|
+
*
|
|
75
|
+
* **Gotchas**
|
|
76
|
+
*
|
|
77
|
+
* - 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.
|
|
78
|
+
* - 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.
|
|
79
|
+
* - Over RPC the typed failure, never a status or a challenge; on a Toolkit and in a CLI as a declared error.
|
|
80
|
+
* - 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
81
|
*/
|
|
56
82
|
export declare class Forbidden extends Forbidden_base {
|
|
57
83
|
private readonly "~declared in the action's error, or built in";
|
|
58
84
|
}
|
|
59
85
|
/**
|
|
60
86
|
* 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)`.
|
|
87
|
+
* running a handler. As a schema, it tells a refusal apart: `Schema.is(Action.Refusal)(error)`.
|
|
63
88
|
*/
|
|
64
89
|
export declare const Refusal: Schema.Union<readonly [typeof Unauthenticated, typeof Forbidden]>;
|
|
65
90
|
/** A refusal: `Unauthenticated | Forbidden`. */
|
|
66
91
|
export type Refusal = (typeof Refusal)["Type"];
|
|
67
92
|
/**
|
|
68
93
|
* The failures every endpoint and tool declares beyond its action's own, and any handler may
|
|
69
|
-
* fail with: bad input, and every refusal.
|
|
94
|
+
* fail with: bad input, and every refusal. An action or a binding need not declare them; one listed is accepted.
|
|
70
95
|
*/
|
|
71
96
|
export declare const builtIns: readonly [typeof InvalidInput, typeof Unauthenticated, typeof Forbidden];
|
|
72
97
|
/** The built-in failures' schemas. */
|
package/dist/contract/errors.mjs
CHANGED
|
@@ -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,13 +79,12 @@ 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)`. */
|
package/dist/contract/hints.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
//#region 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 }) => ({
|
|
@@ -28,14 +28,12 @@ interface Memoized<S, E, R> {
|
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* 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
|
|
31
|
+
* produced, one builder serving every surface. The private fields make this class nominal: a structurally similar object, including one made by
|
|
33
32
|
* spreading an implementation, is not an implementation. Its type parameters may be written
|
|
34
33
|
* out, as a package emitting declarations prints them: `A` its actions, `RequestServices` each
|
|
35
|
-
* action name's handler's
|
|
34
|
+
* action name's handler's request-time requirements, `EX` and `RX` the failures and services of
|
|
36
35
|
* 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.
|
|
36
|
+
* failures and services of the authorizer's builder.
|
|
39
37
|
*/
|
|
40
38
|
export declare class Implementation<A extends Any, RequestServices extends {
|
|
41
39
|
readonly [name: string]: unknown;
|
|
@@ -43,7 +41,7 @@ export declare class Implementation<A extends Any, RequestServices extends {
|
|
|
43
41
|
#private;
|
|
44
42
|
/** The contracts this implementation answers. */
|
|
45
43
|
readonly actions: ReadonlyArray<A>;
|
|
46
|
-
/** Type-only: each action's handler's
|
|
44
|
+
/** Type-only: each action's handler's request-time requirements. */
|
|
47
45
|
readonly "~request": RequestServices;
|
|
48
46
|
/** Type-only: what building its handlers fails with. */
|
|
49
47
|
readonly "~buildError": EX;
|
|
@@ -72,8 +70,7 @@ export declare class Implementation<A extends Any, RequestServices extends {
|
|
|
72
70
|
/**
|
|
73
71
|
* The builders of `app`, its handlers' and its authorizer's, as a layer. Effect memoizes each
|
|
74
72
|
* 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.
|
|
73
|
+
* implementation sharing its builder, shares one run.
|
|
77
74
|
*/
|
|
78
75
|
static layerOf(app: AnyImplementation): Layer.Layer<never, unknown, unknown>;
|
|
79
76
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Anyone, assertAuthentication } from "./caller.mjs";
|
|
2
2
|
import { Unauthenticated } from "./errors.mjs";
|
|
3
|
-
import { assertDistinct, assertOnce } from "./names.mjs";
|
|
3
|
+
import { assertDistinct, assertOnce, heldOnce } from "./names.mjs";
|
|
4
4
|
import { projectedErrors } from "./declared.mjs";
|
|
5
5
|
import { Array, Cause, Context, Effect, Exit, Layer, Option, Schema } from "effect";
|
|
6
6
|
//#region src/contract/implementation.ts
|
|
@@ -14,14 +14,12 @@ const memoized = (build) => {
|
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
16
|
* Actions bound to their handlers and their authorizer: everything one `Action.implement` call
|
|
17
|
-
* produced, one builder serving every surface.
|
|
18
|
-
* The private fields make this class nominal: a structurally similar object, including one made by
|
|
17
|
+
* produced, one builder serving every surface. The private fields make this class nominal: a structurally similar object, including one made by
|
|
19
18
|
* spreading an implementation, is not an implementation. Its type parameters may be written
|
|
20
19
|
* out, as a package emitting declarations prints them: `A` its actions, `RequestServices` each
|
|
21
|
-
* action name's handler's
|
|
20
|
+
* action name's handler's request-time requirements, `EX` and `RX` the failures and services of
|
|
22
21
|
* its handlers' builder, `RA` what its authorizer reads per request, and `EAX` and `RAX` the
|
|
23
|
-
* failures and services of the authorizer's builder.
|
|
24
|
-
* authorization. A plain authorization callback builds nothing.
|
|
22
|
+
* failures and services of the authorizer's builder.
|
|
25
23
|
*/
|
|
26
24
|
var Implementation = class Implementation {
|
|
27
25
|
actions;
|
|
@@ -47,8 +45,7 @@ var Implementation = class Implementation {
|
|
|
47
45
|
/**
|
|
48
46
|
* The builders of `app`, its handlers' and its authorizer's, as a layer. Effect memoizes each
|
|
49
47
|
* by reference within one layer graph, so every surface serving `app` there, and every
|
|
50
|
-
* implementation sharing its builder, shares one run.
|
|
51
|
-
* public instance type.
|
|
48
|
+
* implementation sharing its builder, shares one run.
|
|
52
49
|
*/
|
|
53
50
|
static layerOf(app) {
|
|
54
51
|
const source = Implementation.own(app);
|
|
@@ -69,21 +66,36 @@ var Implementation = class Implementation {
|
|
|
69
66
|
}
|
|
70
67
|
/** `app`, if this copy of the module made it; another installed copy's cannot be read. */
|
|
71
68
|
static own(app) {
|
|
72
|
-
if (!(#handlers in app)) throw new Error("Not an implementation made by this Action.implement:
|
|
69
|
+
if (!(#handlers in app)) throw new Error("Not an implementation made by this Action.implement: pass the value implement returned, not a copy such as a spread; or effect-actions is installed twice");
|
|
73
70
|
return app;
|
|
74
71
|
}
|
|
75
72
|
};
|
|
76
73
|
const toList = (served) => Array.ensure(served);
|
|
77
|
-
const
|
|
74
|
+
const notHeld = {
|
|
75
|
+
byImplementation: {
|
|
76
|
+
why: "no implementation holds it",
|
|
77
|
+
fix: "pass the implementation holding it, or list the contract it implements",
|
|
78
|
+
holder: "they do not hold: list the one they hold"
|
|
79
|
+
},
|
|
80
|
+
byBinding: {
|
|
81
|
+
why: "the binding does not hold it",
|
|
82
|
+
fix: "add it to the binding's actions",
|
|
83
|
+
holder: "it does not hold: list the one it holds"
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/** The actions' names, joined; one whose name another listed contract shares is marked `(another contract)`. */
|
|
87
|
+
const markingOthers = (actions, names) => actions.map(({ name }) => names.has(name) ? `${name} (another contract)` : name).join(", ");
|
|
88
|
+
const assertHeld = ({ why, fix, holder }, listed, held) => {
|
|
78
89
|
const missing = listed.filter((action) => !held.includes(action));
|
|
79
90
|
if (missing.length > 0) {
|
|
80
91
|
const names = new Set(held.map(({ name }) => name));
|
|
81
|
-
|
|
92
|
+
const sameName = missing.some(({ name }) => names.has(name));
|
|
93
|
+
throw new Error(`Listed in actions, but ${why}: ${markingOthers(missing, names)} (${fix}${sameName ? `; one marked (another contract) is a contract of that name ${holder}` : ""})`);
|
|
82
94
|
}
|
|
83
95
|
};
|
|
84
96
|
const select = (apps, listed) => {
|
|
85
97
|
if (listed === void 0) return apps;
|
|
86
|
-
assertHeld(
|
|
98
|
+
assertHeld(notHeld.byImplementation, listed, apps.flatMap((app) => app.actions));
|
|
87
99
|
return apps.flatMap((app) => {
|
|
88
100
|
const own = app.actions.filter((action) => listed.includes(action));
|
|
89
101
|
if (own.length === 0) return [];
|
|
@@ -91,15 +103,15 @@ const select = (apps, listed) => {
|
|
|
91
103
|
});
|
|
92
104
|
};
|
|
93
105
|
const servedBy = (what, bound, apps, listed) => {
|
|
94
|
-
if (listed !== void 0) assertHeld(
|
|
106
|
+
if (listed !== void 0) assertHeld(notHeld.byBinding, listed, bound);
|
|
95
107
|
const held = apps.flatMap((app) => app.actions);
|
|
96
108
|
const served = select(apps, listed ?? bound.filter((action) => held.includes(action)));
|
|
97
109
|
const actions = served.flatMap((app) => app.actions);
|
|
98
110
|
if (actions.length === 0 && listed === void 0 && apps.length > 0) {
|
|
99
111
|
const names = new Set(bound.map(({ name }) => name));
|
|
100
|
-
throw new Error(`No action of these implementations is in this ${what}: ${held
|
|
112
|
+
throw new Error(`No action of these implementations is in this ${what}: ${markingOthers(held, names) || "none"} (pass an implementation of a bound action, or bind one of these)`);
|
|
101
113
|
}
|
|
102
|
-
assertOnce("served action", actions);
|
|
114
|
+
assertOnce("served action", actions, heldOnce);
|
|
103
115
|
return served;
|
|
104
116
|
};
|
|
105
117
|
/**
|
|
@@ -110,7 +122,7 @@ const servingOf = (what, binding, served, options) => {
|
|
|
110
122
|
const apps = servedBy(what, binding.actions, toList(served), options.actions);
|
|
111
123
|
const actions = apps.flatMap((app) => app.actions);
|
|
112
124
|
assertAuthentication(actions, binding.authentication);
|
|
113
|
-
assertDistinct("middleware", options.middleware ?? [], (key) => key.key);
|
|
125
|
+
assertDistinct("middleware", options.middleware ?? [], (key) => key.key, { fix: "list each middleware once" });
|
|
114
126
|
return {
|
|
115
127
|
apps,
|
|
116
128
|
actions,
|
|
@@ -157,4 +169,4 @@ const dispatch = (action, handle, before) => {
|
|
|
157
169
|
return (input) => Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(Effect.suspend(() => handle(input))), Effect.catchCause(declaredOrDefect))).pipe(Effect.annotateLogs(actionAttributes), Effect.withSpan(action.name, { attributes: actionAttributes }, { captureStackTrace: false }));
|
|
158
170
|
};
|
|
159
171
|
//#endregion
|
|
160
|
-
export { Implementation, acquire, assertHeld, builders, built, memoized, provideHandlers, select, servingOf, toList };
|
|
172
|
+
export { Implementation, acquire, assertHeld, builders, built, memoized, notHeld, provideHandlers, select, servingOf, toList };
|
|
@@ -2,9 +2,13 @@ import { Schema, SchemaAST } from "effect";
|
|
|
2
2
|
//#region src/contract/media.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* An image, a media field of a success: `{ data: Uint8Array, mimeType: string }`, `mimeType` such
|
|
5
|
-
* as `image/png`. A top-level field of a struct success, optional or
|
|
6
|
-
* success, one or an array: an MCP tool lifts it into an image block of its own, and every other
|
|
5
|
+
* as `image/png`. A top-level field of a struct success, one, optional, or a required array, or the
|
|
6
|
+
* whole success, one or an array: an MCP tool lifts it into an image block of its own, and every other
|
|
7
7
|
* surface sends it as JSON, the bytes in base64. `ActionMcp` refuses it anywhere else.
|
|
8
|
+
*
|
|
9
|
+
* **Gotchas**
|
|
10
|
+
*
|
|
11
|
+
* - A media field's own `description` reaches no model: describe the image in the action's `description`.
|
|
8
12
|
*/
|
|
9
13
|
export declare const Image: Schema.Struct<{
|
|
10
14
|
readonly data: Schema.Uint8Array;
|
package/dist/contract/media.mjs
CHANGED
|
@@ -7,9 +7,13 @@ const Media = Schema.Struct({
|
|
|
7
7
|
});
|
|
8
8
|
/**
|
|
9
9
|
* An image, a media field of a success: `{ data: Uint8Array, mimeType: string }`, `mimeType` such
|
|
10
|
-
* as `image/png`. A top-level field of a struct success, optional or
|
|
11
|
-
* success, one or an array: an MCP tool lifts it into an image block of its own, and every other
|
|
10
|
+
* as `image/png`. A top-level field of a struct success, one, optional, or a required array, or the
|
|
11
|
+
* whole success, one or an array: an MCP tool lifts it into an image block of its own, and every other
|
|
12
12
|
* surface sends it as JSON, the bytes in base64. `ActionMcp` refuses it anywhere else.
|
|
13
|
+
*
|
|
14
|
+
* **Gotchas**
|
|
15
|
+
*
|
|
16
|
+
* - A media field's own `description` reaches no model: describe the image in the action's `description`.
|
|
13
17
|
*/
|
|
14
18
|
const Image = Media.annotate({ [mediaKindAnnotation]: "image" });
|
|
15
19
|
const isMarked = (ast) => ast.annotations?.[mediaKindAnnotation] === "image";
|