@zackbart/connecta 0.25.0 → 0.26.1

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 (100) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +387 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/artifacts/connector.d.ts +55 -0
  9. package/dist/artifacts/connector.js +724 -0
  10. package/dist/artifacts/document.js +80 -0
  11. package/dist/artifacts/guide.js +140 -0
  12. package/dist/artifacts/html-scan.js +329 -0
  13. package/dist/artifacts/json.js +68 -0
  14. package/dist/artifacts/kv-store.d.ts +20 -0
  15. package/dist/artifacts/kv-store.js +140 -0
  16. package/dist/artifacts/markdown.js +478 -0
  17. package/dist/artifacts/module.d.ts +37 -0
  18. package/dist/artifacts/module.js +60 -0
  19. package/dist/artifacts/operations.d.ts +195 -0
  20. package/dist/artifacts/operations.js +944 -0
  21. package/dist/artifacts/refresh.d.ts +53 -0
  22. package/dist/artifacts/refresh.js +235 -0
  23. package/dist/artifacts/routes.js +217 -0
  24. package/dist/artifacts/types.d.ts +214 -0
  25. package/dist/artifacts/types.js +22 -0
  26. package/dist/artifacts/validate.d.ts +62 -0
  27. package/dist/artifacts/validate.js +653 -0
  28. package/dist/artifacts.d.ts +6 -0
  29. package/dist/artifacts.js +9 -0
  30. package/dist/auth/downstream-oauth.js +397 -48
  31. package/dist/auth/oauth-start-reset.js +20 -0
  32. package/dist/branding.d.ts +87 -0
  33. package/dist/branding.js +170 -1
  34. package/dist/catalog-service.js +39 -29
  35. package/dist/catalog.js +42 -8
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connectors/remote-mcp.js +48 -9
  41. package/dist/errors.d.ts +16 -1
  42. package/dist/errors.js +28 -0
  43. package/dist/execute.js +431 -75
  44. package/dist/executor-admission.d.ts +11 -1
  45. package/dist/executor-admission.js +79 -14
  46. package/dist/executors/quickjs.js +4 -2
  47. package/dist/index.d.ts +62 -6
  48. package/dist/index.js +230 -6
  49. package/dist/invocation.js +56 -10
  50. package/dist/meta-tools.js +7 -2
  51. package/dist/module-contracts.d.ts +21 -1
  52. package/dist/oauth-callback-outcome.js +43 -18
  53. package/dist/operator-ui/generated.js +2 -2
  54. package/dist/operator-ui/model.d.ts +62 -5
  55. package/dist/operator-ui/view.js +372 -28
  56. package/dist/page-styles.js +7 -0
  57. package/dist/program-source.js +116 -0
  58. package/dist/registry.d.ts +42 -6
  59. package/dist/registry.js +164 -60
  60. package/dist/resumable.js +1291 -0
  61. package/dist/routes/activity.js +6 -69
  62. package/dist/routes/actor-labels.js +114 -0
  63. package/dist/routes/credentials.js +27 -5
  64. package/dist/routes/mcp.js +120 -59
  65. package/dist/routes/oauth-management.js +87 -20
  66. package/dist/routes/oauth.js +54 -87
  67. package/dist/routes/operator.js +1 -0
  68. package/dist/routes/shared.d.ts +23 -1
  69. package/dist/routes/shared.js +20 -2
  70. package/dist/routes/ui-data.js +2 -1
  71. package/dist/routes/ui.js +19 -4
  72. package/dist/run-journal.js +347 -0
  73. package/dist/runtime/shared-read.js +106 -0
  74. package/dist/server.js +30 -2
  75. package/dist/skills.js +30 -11
  76. package/dist/timeout.js +26 -0
  77. package/dist/tool-safety.d.ts +35 -0
  78. package/dist/tool-safety.js +31 -0
  79. package/dist/types.d.ts +23 -0
  80. package/dist/ui.d.ts +17 -5
  81. package/dist/ui.js +51 -68
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/documentation/architecture.md +214 -27
  85. package/documentation/auth.md +186 -2
  86. package/documentation/code-mode.md +236 -63
  87. package/documentation/meta-tools.md +55 -29
  88. package/ethos.md +12 -10
  89. package/examples/worker/AGENTS.md +6 -0
  90. package/examples/worker/README.md +174 -10
  91. package/examples/worker/src/d1-activity-row.ts +14 -2
  92. package/examples/worker/src/d1-activity.ts +4 -2
  93. package/examples/worker/src/index.ts +36 -1
  94. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  95. package/examples/worker/wrangler.jsonc +5 -1
  96. package/package.json +6 -2
  97. package/templates/node/AGENTS.md +9 -2
  98. package/templates/node/README.md +31 -3
  99. package/templates/node/package.json +1 -1
  100. package/templates/node/src/index.ts +27 -0
@@ -12,38 +12,69 @@ const hasControlCharacter = (value) => [...value].some((ch) => {
12
12
  * Normalize a grant list. A bare connector id grants every tool on that
13
13
  * connector; a `connector.tool` address grants one tool. Grants are additive,
14
14
  * so a bare id beside addresses for the same connector means the whole
15
- * connector. Anything else — an unknown shape, an empty tool name, a
16
- * non-string — throws, and the caller decides whether that is a construction
17
- * failure or a 403: a grant that cannot be parsed must never fail open.
15
+ * connector. Identity resolution can opt into `{ tool, requireReadOnly: true }`
16
+ * for a fixed address that also needs a current explicit read-only annotation;
17
+ * pool declarations remain strings. Anything else throws, and the caller
18
+ * decides whether that is a construction failure or a 403: a grant that
19
+ * cannot be parsed must never fail open.
18
20
  */
19
- export function parseConnectorAccess(value) {
21
+ export function parseConnectorAccess(value, options = {}) {
20
22
  if (value === "all")
21
23
  return { connectorIds: "all" };
22
24
  if (!Array.isArray(value))
23
25
  throw new Error("invalid connector permission");
24
26
  const whole = new Set();
25
- const partial = new Map();
27
+ const plain = new Map();
28
+ const guarded = new Map();
26
29
  for (const entry of value) {
27
- if (typeof entry !== "string")
28
- throw new Error("invalid connector permission");
29
- if (CONNECTOR_ID_RE.test(entry)) {
30
+ if (typeof entry === "string" && CONNECTOR_ID_RE.test(entry)) {
30
31
  whole.add(entry);
31
32
  continue;
32
33
  }
33
- if (!TOOL_ADDRESS_RE.test(entry) || hasControlCharacter(entry))
34
+ const isGuarded = typeof entry !== "string";
35
+ if (isGuarded && (!options.allowReadOnly || entry === null ||
36
+ typeof entry !== "object" || Array.isArray(entry) ||
37
+ !Object.hasOwn(entry, "tool") || !Object.hasOwn(entry, "requireReadOnly") ||
38
+ Object.keys(entry).length !== 2 || entry.requireReadOnly !== true)) {
39
+ throw new Error("invalid connector permission");
40
+ }
41
+ const address = isGuarded ? entry.tool : entry;
42
+ if (typeof address !== "string" || !TOOL_ADDRESS_RE.test(address) || hasControlCharacter(address)) {
34
43
  throw new Error("invalid connector permission");
35
- const dot = entry.indexOf(".");
36
- const connectorId = entry.slice(0, dot);
37
- const tools = partial.get(connectorId) ?? new Set();
38
- tools.add(entry.slice(dot + 1));
39
- partial.set(connectorId, tools);
44
+ }
45
+ const dot = address.indexOf(".");
46
+ const connectorId = address.slice(0, dot);
47
+ const target = isGuarded ? guarded : plain;
48
+ const tools = target.get(connectorId) ?? new Set();
49
+ tools.add(address.slice(dot + 1));
50
+ target.set(connectorId, tools);
51
+ }
52
+ for (const id of whole) {
53
+ plain.delete(id);
54
+ guarded.delete(id);
55
+ }
56
+ for (const [id, names] of plain) {
57
+ const restricted = guarded.get(id);
58
+ if (!restricted)
59
+ continue;
60
+ for (const name of names)
61
+ restricted.delete(name);
62
+ if (restricted.size === 0)
63
+ guarded.delete(id);
64
+ }
65
+ const partial = new Map(plain);
66
+ for (const [id, names] of guarded) {
67
+ const combined = partial.get(id) ?? new Set();
68
+ for (const name of names)
69
+ combined.add(name);
70
+ partial.set(id, combined);
40
71
  }
41
- for (const id of whole)
42
- partial.delete(id);
43
72
  const connectorIds = [...new Set([...whole, ...partial.keys()])];
44
- return partial.size > 0
45
- ? { connectorIds, toolAccess: partial }
46
- : { connectorIds };
73
+ return {
74
+ connectorIds,
75
+ ...(partial.size > 0 ? { toolAccess: partial } : {}),
76
+ ...(guarded.size > 0 ? { guardedToolAccess: guarded } : {}),
77
+ };
47
78
  }
48
79
  /**
49
80
  * The view a pool endpoint serves: the pool's grants, never wider than the
@@ -59,6 +90,7 @@ export function intersectAccess(ceiling, pool) {
59
90
  : new Set(ceiling.connectorIds);
60
91
  const connectorIds = [];
61
92
  const toolAccess = new Map();
93
+ const guardedToolAccess = new Map();
62
94
  for (const id of pool.connectorIds) {
63
95
  if (allowedIds && !allowedIds.has(id))
64
96
  continue;
@@ -73,7 +105,20 @@ export function intersectAccess(ceiling, pool) {
73
105
  else if (fromPool ?? fromCeiling) {
74
106
  toolAccess.set(id, (fromPool ?? fromCeiling));
75
107
  }
108
+ const guarded = ceiling.guardedToolAccess?.get(id);
109
+ if (guarded) {
110
+ const allowed = toolAccess.get(id);
111
+ const retained = allowed
112
+ ? new Set([...guarded].filter((name) => allowed.has(name)))
113
+ : guarded;
114
+ if (retained.size > 0)
115
+ guardedToolAccess.set(id, retained);
116
+ }
76
117
  connectorIds.push(id);
77
118
  }
78
- return toolAccess.size > 0 ? { connectorIds, toolAccess } : { connectorIds };
119
+ return {
120
+ connectorIds,
121
+ ...(toolAccess.size > 0 ? { toolAccess } : {}),
122
+ ...(guardedToolAccess.size > 0 ? { guardedToolAccess } : {}),
123
+ };
79
124
  }
@@ -0,0 +1,8 @@
1
+ import type { AuthenticatedIdentity, ConnectorContext } from "./types.js";
2
+ export interface ConnectorCaller {
3
+ identity: AuthenticatedIdentity;
4
+ /** The `/mcp/<pool>` the call arrived on, when it was a pool. */
5
+ pool?: string;
6
+ }
7
+ export declare function attachCaller(ctx: ConnectorContext, caller: ConnectorCaller | undefined): ConnectorContext;
8
+ export declare function callerOf(ctx: ConnectorContext): ConnectorCaller | undefined;
@@ -0,0 +1,19 @@
1
+ // Who is calling, for the connectors connecta ships and nothing else.
2
+ //
3
+ // `ConnectorContext` deliberately carries no identity: a deployment's own
4
+ // connector acts for the deployment, and one that could read its caller would
5
+ // invite per-user behavior the access rules never saw. The built-in artifacts
6
+ // connector is the exception — every version it writes records who wrote it —
7
+ // so the identity rides beside the context, the way the OAuth sealer does,
8
+ // where only in-repo code can read it. Core sets it from the authorization
9
+ // that admitted the request, on the scoped registry view that request
10
+ // receives; no argument, header, or program can name it.
11
+ const callers = new WeakMap();
12
+ export function attachCaller(ctx, caller) {
13
+ if (caller)
14
+ callers.set(ctx, caller);
15
+ return ctx;
16
+ }
17
+ export function callerOf(ctx) {
18
+ return callers.get(ctx);
19
+ }
@@ -1,6 +1,7 @@
1
1
  import { Client, ProtocolError, SdkHttpError, isInputRequiredResult, specTypeSchemas, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
2
2
  import { Deferred, Duration, Effect, Exit, Scope } from "effect";
3
3
  import { KvOAuthProvider, OAuthRefreshCoordinator, } from "../auth/downstream-oauth.js";
4
+ import { trackOAuthStartReset } from "../auth/oauth-start-reset.js";
4
5
  import { MAX_CATALOG_TOOLS } from "../catalog-limits.js";
5
6
  import { boundedEchoText, ConnectorCallError, msg, unavailableCallError, } from "../errors.js";
6
7
  import { CONNECTA_VERSION } from "../version.js";
@@ -587,7 +588,20 @@ export function remoteMcp(id, opts) {
587
588
  const newProvider = (ctx, state) => {
588
589
  if (state?.provider)
589
590
  return state.provider;
590
- const provider = new KvOAuthProvider(id, ctx.storage, `${ctx.baseUrl}/oauth/callback/${id}`, refreshCoordinator, ctx.allowAuthorization === true, oauthSealerFor(ctx));
591
+ const provider = new KvOAuthProvider(id, ctx.storage, `${ctx.baseUrl}/oauth/callback/${id}`, refreshCoordinator, ctx.allowAuthorization === true, oauthSealerFor(ctx), ctx.signal, JSON.stringify({
592
+ url: new URL(opts.url).href,
593
+ redirectUri: `${ctx.baseUrl}/oauth/callback/${id}`,
594
+ clientMetadata: {
595
+ redirect_uris: [`${ctx.baseUrl}/oauth/callback/${id}`],
596
+ client_name: "connecta",
597
+ grant_types: ["authorization_code", "refresh_token"],
598
+ response_types: ["code"],
599
+ token_endpoint_auth_method: "none",
600
+ },
601
+ authScope: opts.authScope ?? "shared",
602
+ versionNegotiation: opts.versionNegotiation ?? "auto",
603
+ redirects: opts.redirects ?? "none",
604
+ }), (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset));
591
605
  if (state)
592
606
  state.provider = provider;
593
607
  return provider;
@@ -775,6 +789,7 @@ export function remoteMcp(id, opts) {
775
789
  // SDK v2 selects its validator by runtime export condition: AJV on
776
790
  // Node and @cfworker/json-schema under workerd. The Workers-safe path
777
791
  // no longer needs Connecta-specific wiring.
792
+ const handshakeAbort = new AbortController();
778
793
  const c = new Client({ name: "connecta", version: CONNECTA_VERSION }, {
779
794
  versionNegotiation: {
780
795
  mode: opts.versionNegotiation ?? "auto",
@@ -783,6 +798,19 @@ export function remoteMcp(id, opts) {
783
798
  // below as one structured, non-retryable connector failure.
784
799
  inputRequired: { autoFulfill: false },
785
800
  });
801
+ // @modelcontextprotocol/client 2.0.0 can attempt the legacy initialized
802
+ // notification after we close its transport. Its rejected Promise is
803
+ // unhandled under workerd even though connect() has a caller. This
804
+ // connection is already abandoned, so skip only that final handshake
805
+ // notification after our abort. Remove when the SDK owns this race:
806
+ // https://github.com/modelcontextprotocol/typescript-sdk/issues/2864
807
+ const notify = c.notification.bind(c);
808
+ c.notification = (message, options) => {
809
+ if (handshakeAbort.signal.aborted && message.method === "notifications/initialized") {
810
+ return Promise.resolve();
811
+ }
812
+ return notify(message, options);
813
+ };
786
814
  const t = buildTransport(ctx, provider, credentialFramed);
787
815
  if (!owned()) {
788
816
  detach(closeConnection(null, t, ctx.logger));
@@ -794,11 +822,11 @@ export function remoteMcp(id, opts) {
794
822
  // what aborts a connect still in flight.
795
823
  const lease = Scope.forkUnsafe(state.scope);
796
824
  const held = { client: null };
797
- yield* Scope.addFinalizer(lease, Effect.suspend(() => closeConnection(held.client, t, ctx.logger)));
825
+ yield* Scope.addFinalizer(lease, Effect.sync(() => handshakeAbort.abort()).pipe(Effect.andThen(Effect.suspend(() => closeConnection(held.client, t, ctx.logger)))));
798
826
  state.lease = lease;
799
827
  state.transport = t;
800
828
  return yield* Effect.gen(function* () {
801
- yield* promised(() => c.connect(t));
829
+ yield* promised(() => c.connect(t, { signal: handshakeAbort.signal }));
802
830
  // A probe deadline can end its scope while connect is still in flight.
803
831
  // The transport is closed immediately by closeScope; if connect wins
804
832
  // that race anyway, close the resulting client rather than resurrecting
@@ -857,13 +885,14 @@ export function remoteMcp(id, opts) {
857
885
  }));
858
886
  });
859
887
  };
860
- const disconnectAuthorization = async (ctx, state, operatorDisconnected = false) => {
888
+ const disconnectAuthorization = async (ctx, state, operatorDisconnected = false, preserveClient = false) => {
861
889
  const provider = newProvider(ctx, state);
862
890
  // Publish the replacement epoch before waiting on or closing any
863
891
  // request-local transport. A hung connect therefore cannot delay the
864
892
  // fence, and every late OAuth write stays in the older namespace.
893
+ const reset = provider.resetAuthorization(operatorDisconnected, preserveClient);
865
894
  try {
866
- await provider.resetAuthorization(operatorDisconnected);
895
+ await reset;
867
896
  }
868
897
  finally {
869
898
  // Abandon any connect in flight and close whichever half of the
@@ -1181,22 +1210,32 @@ export function remoteMcp(id, opts) {
1181
1210
  const state = stateFor(ctx);
1182
1211
  state.provider = null;
1183
1212
  const p = newProvider(ctx, state);
1184
- if (startOpts?.force || (await p.operatorDisconnected())) {
1185
- await disconnectAuthorization(ctx, state);
1213
+ if (ctx.signal?.aborted)
1214
+ throw ctx.signal.reason;
1215
+ const disconnected = startOpts?.force ? false : await p.operatorDisconnected();
1216
+ if (ctx.signal?.aborted)
1217
+ throw ctx.signal.reason;
1218
+ if (startOpts?.force || disconnected) {
1219
+ await disconnectAuthorization(ctx, state, false, startOpts?.force === true);
1186
1220
  }
1187
1221
  else {
1188
1222
  // A consent URL already outstanding? Re-issue it rather than re-running
1189
1223
  // the SDK flow, which would overwrite the PKCE verifier and invalidate
1190
- // the URL the operator may be mid-consent on.
1191
- const pending = await p.pendingAuthorizationUrl();
1224
+ // the URL the operator may be mid-consent on. Only a recent one: an
1225
+ // old or untimed URL falls through to a fresh flow in this same
1226
+ // epoch, which keeps the stored client registration and discovery.
1227
+ const pending = await p.reusablePendingAuthorizationUrl();
1192
1228
  if (pending) {
1193
1229
  return {
1194
1230
  state: "auth_required",
1195
1231
  authorizationUrl: pending,
1232
+ authorizationReused: true,
1196
1233
  message: "Authorization required — open the URL to connect.",
1197
1234
  };
1198
1235
  }
1199
1236
  }
1237
+ if (ctx.signal?.aborted)
1238
+ throw ctx.signal.reason;
1200
1239
  try {
1201
1240
  await ensureConnected(ctx, state);
1202
1241
  return {
package/dist/errors.d.ts CHANGED
@@ -15,7 +15,14 @@ export type ConnectorCallErrorCode = "timeout" | "auth_required" | "rate_limited
15
15
  * code stays `connector_call_failed` or `auth_required` with a message that
16
16
  * states the ambiguity (H11).
17
17
  */
18
- | "not_found" | "input_required_unsupported" | "connector_call_failed";
18
+ | "not_found"
19
+ /**
20
+ * The write named a base version or revision that is no longer current:
21
+ * someone else changed the thing first. The caller re-reads, reapplies its
22
+ * change, and retries with the base `current` reports — repeating the same
23
+ * call can never succeed, and waiting will not help either.
24
+ */
25
+ | "conflict" | "input_required_unsupported" | "connector_call_failed";
19
26
  /** One bounded, payload-free explanation of an input-schema mismatch. */
20
27
  export interface ArgumentValidationIssue {
21
28
  /** JSON Pointer into the submitted arguments; "/" means the root value. */
@@ -88,12 +95,18 @@ export declare class ConnectorCallError extends Error {
88
95
  readonly validation: ArgumentValidationDetails | undefined;
89
96
  /** Sanitized transport diagnostics for `unavailable` only. */
90
97
  readonly details: UnavailableDetails | undefined;
98
+ /**
99
+ * For `conflict` only: where each thing the write touched stands now, e.g.
100
+ * `{ revision: 12, view: 7 }`. Bounded to 20 whole-number entries.
101
+ */
102
+ readonly current: Readonly<Record<string, number>> | undefined;
91
103
  constructor(code: ConnectorCallErrorCode, message: string, opts?: {
92
104
  retryable?: boolean;
93
105
  retryAfterMs?: number;
94
106
  cause?: unknown;
95
107
  validation?: ArgumentValidationDetails;
96
108
  details?: UnavailableDetails;
109
+ current?: Readonly<Record<string, number>>;
97
110
  });
98
111
  }
99
112
  /** The `error` object surfaced in value-mode call results and rejected promises. */
@@ -107,6 +120,8 @@ export interface CallErrorDetails {
107
120
  retryAfterMs?: number;
108
121
  /** Bounded input-schema findings; paths and expectations, never values. */
109
122
  validation?: ArgumentValidationDetails;
123
+ /** On a `conflict`: where each thing the write touched stands now. */
124
+ current?: Readonly<Record<string, number>>;
110
125
  /** Connector whose failed operation needs recovery. */
111
126
  connector?: string;
112
127
  /** Canonical downstream address the agent may retry after recovery. */
package/dist/errors.js CHANGED
@@ -162,9 +162,30 @@ const RETRYABLE_BY_CODE = {
162
162
  auth_required: false,
163
163
  invalid_args: false,
164
164
  not_found: false,
165
+ conflict: false,
165
166
  input_required_unsupported: false,
166
167
  connector_call_failed: false,
167
168
  };
169
+ /** Entries a conflict's `current` map may carry. */
170
+ const MAX_CURRENT_ENTRIES = 20;
171
+ const MAX_CURRENT_KEY_CHARS = 128;
172
+ /**
173
+ * Where each thing a conflicting write touched stands now: short names to
174
+ * whole non-negative numbers, at most 20 of them. Anything else is dropped
175
+ * whole rather than clipped, because a truncated key names a different thing.
176
+ */
177
+ function boundedCurrent(current) {
178
+ if (!current || typeof current !== "object")
179
+ return undefined;
180
+ const entries = Object.entries(current).filter(([key, value]) => key.length > 0 &&
181
+ key.length <= MAX_CURRENT_KEY_CHARS &&
182
+ key !== "__proto__" &&
183
+ Number.isSafeInteger(value) &&
184
+ value >= 0);
185
+ if (entries.length === 0)
186
+ return undefined;
187
+ return Object.freeze(Object.fromEntries(entries.slice(0, MAX_CURRENT_ENTRIES)));
188
+ }
168
189
  /** Non-negative integer milliseconds, or undefined for anything else. */
169
190
  function normalizeRetryAfterMs(value) {
170
191
  if (value === undefined)
@@ -201,6 +222,11 @@ export class ConnectorCallError extends Error {
201
222
  validation;
202
223
  /** Sanitized transport diagnostics for `unavailable` only. */
203
224
  details;
225
+ /**
226
+ * For `conflict` only: where each thing the write touched stands now, e.g.
227
+ * `{ revision: 12, view: 7 }`. Bounded to 20 whole-number entries.
228
+ */
229
+ current;
204
230
  constructor(code, message, opts = {}) {
205
231
  super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);
206
232
  this.name = "ConnectorCallError";
@@ -211,6 +237,7 @@ export class ConnectorCallError extends Error {
211
237
  code === "unavailable" ? sanitizedUnavailableDetails(opts.details) : undefined;
212
238
  this.validation =
213
239
  code === "invalid_args" ? boundedValidation(opts.validation) : undefined;
240
+ this.current = code === "conflict" ? boundedCurrent(opts.current) : undefined;
214
241
  }
215
242
  }
216
243
  /**
@@ -262,6 +289,7 @@ export function classifyCallError(err, fallbackCode = "connector_call_failed") {
262
289
  : {}),
263
290
  ...(err.validation ? { validation: err.validation } : {}),
264
291
  ...(err.details ? { details: err.details } : {}),
292
+ ...(err.current ? { current: err.current } : {}),
265
293
  };
266
294
  }
267
295
  // An aborted fetch rejects with a DOMException named "AbortError" whose