@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.
Files changed (144) hide show
  1. package/README.md +20 -47
  2. package/dist/Action.d.mts +96 -47
  3. package/dist/Action.mjs +11 -14
  4. package/dist/ActionCli.d.mts +81 -42
  5. package/dist/ActionCli.mjs +52 -33
  6. package/dist/ActionHttp.d.mts +84 -43
  7. package/dist/ActionHttp.mjs +2 -2
  8. package/dist/ActionMcp.d.mts +54 -27
  9. package/dist/ActionMcp.mjs +5 -5
  10. package/dist/ActionRpc.d.mts +48 -35
  11. package/dist/ActionRpc.mjs +1 -1
  12. package/dist/ActionToolkit.d.mts +32 -13
  13. package/dist/ActionToolkit.mjs +10 -7
  14. package/dist/Authentication.d.mts +92 -40
  15. package/dist/Authentication.mjs +37 -35
  16. package/dist/Testing.d.mts +22 -24
  17. package/dist/authentication/provider.d.mts +1 -4
  18. package/dist/authentication/provider.mjs +1 -1
  19. package/dist/authentication/refusal.mjs +2 -1
  20. package/dist/cli/commands.d.mts +34 -11
  21. package/dist/cli/commands.mjs +47 -22
  22. package/dist/contract/caller.mjs +1 -1
  23. package/dist/contract/declared.mjs +27 -14
  24. package/dist/contract/errors.d.mts +42 -17
  25. package/dist/contract/errors.mjs +42 -17
  26. package/dist/contract/hints.mjs +3 -2
  27. package/dist/contract/implementation.d.mts +5 -8
  28. package/dist/contract/implementation.mjs +29 -17
  29. package/dist/contract/media.d.mts +6 -2
  30. package/dist/contract/media.mjs +6 -2
  31. package/dist/contract/names.mjs +14 -12
  32. package/dist/contract/status.mjs +3 -10
  33. package/dist/http/client.d.mts +15 -4
  34. package/dist/http/client.mjs +1 -1
  35. package/dist/mcp/client.d.mts +21 -8
  36. package/dist/mcp/client.mjs +6 -8
  37. package/docs/Action-failures.md +6 -15
  38. package/docs/Action.md +159 -191
  39. package/docs/ActionCli-failures.md +12 -92
  40. package/docs/ActionCli.md +102 -156
  41. package/docs/ActionHttp-failures.md +14 -76
  42. package/docs/ActionHttp.md +150 -176
  43. package/docs/ActionMcp-failures.md +2 -22
  44. package/docs/ActionMcp.md +79 -170
  45. package/docs/ActionRpc-failures.md +3 -12
  46. package/docs/ActionRpc.md +183 -120
  47. package/docs/ActionToolkit-failures.md +3 -10
  48. package/docs/ActionToolkit.md +112 -109
  49. package/docs/Authentication-advanced.md +167 -108
  50. package/docs/Authentication-failures.md +11 -91
  51. package/docs/Authentication.md +114 -135
  52. package/docs/README.md +45 -30
  53. package/docs/Testing-failures.md +2 -6
  54. package/docs/Testing.md +126 -159
  55. package/docs/setup-failures.md +8 -32
  56. package/docs/setup.md +42 -37
  57. package/examples/README.md +6 -10
  58. package/examples/assistant/README.md +20 -38
  59. package/examples/assistant/src/agent.ts +2 -3
  60. package/examples/assistant/src/cli.ts +4 -9
  61. package/examples/assistant/src/commands.ts +6 -4
  62. package/examples/assistant/src/identity/authorize.ts +3 -3
  63. package/examples/assistant/src/tasks/Tasks.ts +1 -2
  64. package/examples/assistant/src/tasks/handlers.ts +3 -3
  65. package/examples/assistant/src/toolkit.ts +6 -8
  66. package/examples/assistant/test/commands.test.ts +16 -11
  67. package/examples/assistant/test/tasks/tasks.test.ts +0 -2
  68. package/examples/assistant/test/toolkit.test.ts +0 -1
  69. package/examples/kitchen-sink/README.md +30 -52
  70. package/examples/kitchen-sink/src/Actions.ts +2 -3
  71. package/examples/kitchen-sink/src/Api.ts +3 -4
  72. package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
  73. package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
  74. package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
  75. package/examples/kitchen-sink/src/agent.ts +1 -1
  76. package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
  77. package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
  78. package/examples/kitchen-sink/src/attachments/download.ts +5 -8
  79. package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
  80. package/examples/kitchen-sink/src/browser.ts +3 -4
  81. package/examples/kitchen-sink/src/cli.ts +6 -7
  82. package/examples/kitchen-sink/src/config.ts +2 -0
  83. package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
  84. package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
  85. package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
  86. package/examples/kitchen-sink/src/identity/quota.ts +1 -1
  87. package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
  88. package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
  89. package/examples/kitchen-sink/src/issues/guide.ts +2 -3
  90. package/examples/kitchen-sink/src/ops.ts +3 -6
  91. package/examples/kitchen-sink/src/request-policy.ts +3 -3
  92. package/examples/kitchen-sink/src/routes.ts +4 -6
  93. package/examples/kitchen-sink/src/seed.ts +0 -1
  94. package/examples/kitchen-sink/src/server.ts +11 -12
  95. package/examples/kitchen-sink/src/stdio.ts +3 -2
  96. package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
  97. package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
  98. package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
  99. package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
  100. package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
  101. package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
  102. package/examples/kitchen-sink/test/request-policy.test.ts +1 -1
  103. package/examples/kitchen-sink/test/routes.test.ts +3 -3
  104. package/examples/live-rpc/README.md +8 -49
  105. package/examples/live-rpc/src/browser.ts +8 -41
  106. package/examples/live-rpc/src/chat/Chat.ts +9 -3
  107. package/examples/live-rpc/src/chat/room.ts +1 -1
  108. package/examples/live-rpc/src/client.ts +2 -3
  109. package/examples/live-rpc/src/page.ts +34 -0
  110. package/examples/live-rpc/src/routes.ts +6 -7
  111. package/examples/live-rpc/src/server.ts +6 -4
  112. package/examples/live-rpc/test/browser.test.ts +6 -5
  113. package/examples/live-rpc/test/routes.test.ts +16 -15
  114. package/examples/live-rpc/test/server.test.ts +1 -1
  115. package/examples/mcp-tools/README.md +22 -45
  116. package/examples/mcp-tools/src/printers/Printers.ts +3 -7
  117. package/examples/mcp-tools/src/routes.ts +0 -7
  118. package/examples/mcp-tools/src/server.ts +1 -1
  119. package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
  120. package/examples/mcp-tools/test/routes.test.ts +2 -3
  121. package/examples/notes-service/README.md +9 -43
  122. package/examples/notes-service/src/cli.ts +0 -3
  123. package/examples/notes-service/src/commands.ts +7 -17
  124. package/examples/notes-service/src/identity/authorize.ts +2 -5
  125. package/examples/notes-service/src/identity/verifier.ts +3 -4
  126. package/examples/notes-service/src/notes/Notes.ts +2 -6
  127. package/examples/notes-service/src/notes/store.ts +5 -1
  128. package/examples/notes-service/src/routes.ts +6 -8
  129. package/examples/notes-service/src/server.ts +4 -3
  130. package/examples/notes-service/src/stdio.ts +4 -3
  131. package/examples/notes-service/test/commands.test.ts +1 -1
  132. package/examples/notes-service/test/notes/as.ts +6 -0
  133. package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
  134. package/examples/notes-service/test/notes/in-process.test.ts +50 -0
  135. package/examples/notes-service/test/notes/notes.test.ts +3 -42
  136. package/examples/notes-service/test/routes.test.ts +2 -2
  137. package/examples/quickstart/README.md +1 -1
  138. package/examples/quickstart/src/client.ts +1 -0
  139. package/examples/quickstart/src/server.ts +2 -4
  140. package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
  141. package/package.json +1 -1
  142. package/docs/Action-in-process.md +0 -93
  143. package/docs/CONTEXT.md +0 -46
  144. package/docs/guarantees.md +0 -44
@@ -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 message is its JSON, which `Command.run`
173
- * prints on stderr and marks reported, so `runMain` does not print it again. The process exits with
174
- * the cause's `Runtime.errorExitCode`, 1 by default. A type only: after `Command.run`, match the
175
- * reason by its tag, `Effect.catchReason("UserError", "UserNotFound", f)`. A command of your own
176
- * fails with one through `toUserError`; any other failure is reported as a crash, `ERROR (#n)`.
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([...new Set(errors)]);
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
- if (positional.length > 0) throw new Error(`Positional arguments need named input fields: ${positional.join(", ")}`);
264
- if (Object.keys(aliases).length > 0) throw new Error(`Aliases need named input fields: ${Object.keys(aliases).join(", ")}`);
265
- if (stdin !== void 0) throw new Error(`Stdin needs named input fields: ${stdin}`);
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) => [`--${kebab(field)}`, `field ${field}`]),
280
- ...render === void 0 ? [] : [["--json", "render's --json"]],
281
- ...Object.entries(options?.aliases ?? {}).flatMap(([field, alias]) => Predicate.isString(alias) ? [[`--${alias.replace(/^-+/, "")}`, `alias of field ${field}`]] : [])
282
- ], ([flag]) => flag, ([, claimant]) => claimant);
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, action.description) : command.pipe(Command.withDescription(`${action.description}\n\n${stdinHelp}`), Command.withShortDescription(action.description));
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 };
@@ -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 projectedErrors = (action, surface = []) => [.../* @__PURE__ */ new Set([
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((error) => encodedTags(error.ast)));
23
- const assertOwnTags = (what, errors) => {
24
- const tag = errors.flatMap((error) => encodedTags(error.ast)).find((tag) => builtInTags.has(tag));
25
- if (tag !== void 0) throw new Error(`${what}: error _tag "${tag}" is built in, and declared on every surface`);
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, no built-in error in
29
- * `error`, and the descriptor every protected action names. Returns `error` as a list.
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 error = errorList(options.error);
34
- assertOwnTags(what, error);
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 { assertOwnTags, bindingErrors, bindingOf, errorList, projectedErrors };
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. HTTP answers every such
22
- * request with it, its message the schema's own description of what is wrong, and `issues`
23
- * the same description an issue at a time, for a caller that points at a field. A handler
24
- * failing with it for input that decodes but cannot be served may name its own.
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. Neither carries a value of the input: decoding
32
- * runs without `reportInput`, so no issue holds one to format. A symbol key is sent as its
33
- * string form, `Symbol(name)`, as JSON has no symbols, which tells it from a string key of
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
- /** The caller is not authenticated: authentication or an implementation's `authorize` refuses. */
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: an implementation's `authorize` refuses. `scopes`,
51
- * when given, are every OAuth scope the call needs: a refusal naming them is answered with
52
- * the `insufficient_scope` challenge an OAuth client re-authorizes on, so name them only where
53
- * re-authorizing can grant them. What it answers on each surface: `docs/guarantees.md` in this
54
- * package.
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. An error an action declares, such as a rate limit, is not a refusal. As a
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. No action or binding declares them itself.
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. */
@@ -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. HTTP answers every such
11
- * request with it, its message the schema's own description of what is wrong, and `issues`
12
- * the same description an issue at a time, for a caller that points at a field. A handler
13
- * failing with it for input that decodes but cannot be served may name its own.
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. Neither carries a value of the input: decoding
23
- * runs without `reportInput`, so no issue holds one to format. A symbol key is sent as its
24
- * string form, `Symbol(name)`, as JSON has no symbols, which tells it from a string key of
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
- /** The caller is not authenticated: authentication or an implementation's `authorize` refuses. */
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: an implementation's `authorize` refuses. `scopes`,
44
- * when given, are every OAuth scope the call needs: a refusal naming them is answered with
45
- * the `insufficient_scope` challenge an OAuth client re-authorizes on, so name them only where
46
- * re-authorizing can grant them. What it answers on each surface: `docs/guarantees.md` in this
47
- * package.
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. An error an action declares, such as a rate limit, is not a refusal. As a
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. No action or binding declares them itself.
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)`. */
@@ -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: a write destructive,
4
- * a read not, no tool idempotent and every tool open-world. A given value passes through, as MCP
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 per-request requirements, `EX` and `RX` the failures and services of
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. Public-only selections do not acquire
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 per-request requirements. */
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. Static, so it stays off the
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 per-request requirements, `EX` and `RX` the failures and services of
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. Public-only selections do not acquire
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. Static, so it stays off the
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: is effect-actions installed twice?");
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 assertHeld = (why, listed, held) => {
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
- throw new Error(`Listed in actions, but ${why}: ${missing.map(({ name }) => names.has(name) ? `${name} (another contract)` : name).join(", ")}`);
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("no implementation holds it", listed, apps.flatMap((app) => app.actions));
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("the binding does not hold it", listed, bound);
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.map(({ name }) => names.has(name) ? `${name} (another contract)` : name).join(", ") || "none"}`);
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 an array, or the whole
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;
@@ -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 an array, or the whole
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";