@foir/sdk 0.30.0 → 0.31.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/CHANGELOG.md CHANGED
@@ -180,6 +180,20 @@ Changes on `dev` that have not been published to npm.
180
180
  stream nobody read, which threw. A page loaded again still picks the answer
181
181
  up from the stored run with `resume`.
182
182
 
183
+ ### Changed
184
+
185
+ - **Breaking: a prose entry's identity is `_id`.** `Passage._key` is now
186
+ `Passage._id`, a markDef is named by its `_id`, and the renderers key
187
+ entries by `_id` alone. Ask for `_id` where a query asked for `_key`. The id
188
+ is minted once and never changes, and is unique within its record; it is not
189
+ a record id.
190
+ - **Breaking: a reference is followed by `_targetId` alone.**
191
+ `Reference._targetNaturalKey` is gone: a reference names its target by id.
192
+ `Reference._id` is the entry's own id, never the target's.
193
+ - **`Image`, `Video`, `FileValue` and `GeoPoint` carry `_id`**: the value's own
194
+ id when it is an item of a list or an entry of a prose field, and null on a
195
+ plain field's value.
196
+
183
197
  ## [0.29.0] — 2026-10-21
184
198
 
185
199
  ### Added
package/README.md CHANGED
@@ -27,7 +27,7 @@ pnpm add yjs y-websocket # for @foir/sdk/realtime
27
27
  | `@foir/sdk/data` | Typed data client over `/graphql` — `createFoirClient`, per-key-shaped ops, `include`/`omit`, `fetchFoirManifest`; prints the document for you, so still no `graphql` | **none** |
28
28
  | `@foir/sdk/auth` | Framework-agnostic sign-in for your app's users: login/register/OTP/provider flows, token lifecycle (`SessionAdapter`), `createFoirSessionClient` (Foir-hosted cookie sessions), `authedRequest`, guards | `graphql` |
29
29
  | `@foir/sdk/react` | Drop-in React auth — `<FoirProvider>` (token or `hostedSession` mode), `useFoirSession`/`useFoirUser`/`useFoirAuth`, `<SignIn>`, `<OAuthButtons>`, `<UserButton>` | `react`, `graphql` |
30
- | `@foir/sdk/next` | Next.js App Router BFF auth — `createFoirCookieAdapter`, `getFoirSession`, `foirSignIn*`, `completeFoirProviderLogin`, `createFoirMiddleware` | `graphql` |
30
+ | `@foir/sdk/next` | Next.js App Router BFF auth: `createFoirCookieAdapter`, `getFoirSession`, `foirSignIn*`, `startFoirProviderSignIn`, `finishFoirProviderSignIn`, `createFoirMiddleware` | `graphql` |
31
31
  | `@foir/sdk/server` | Server-side helpers for extension/config API backends — `createServerClient`, `verifyWebhookSignature`, `verifyScopedToken`, callback client | — |
32
32
  | `@foir/sdk/live` | Live reads — the client half of Foir's invalidation graph: tag index, raw-payload store, `seq` ordering, record-change → tag mapping | **none** |
33
33
  | `@foir/sdk/live/react` | `<FoirLive>` — one socket, refcounted per model, wired to a query client you already have | `react` |
@@ -55,9 +55,10 @@
55
55
  * `foirChatMessages` reads a thread back as `useChat`'s messages.
56
56
  */
57
57
  import type { ChatTransport, FlexibleSchema, UIMessage, UIMessageChunk } from 'ai';
58
- import type { FoirAgentAttachmentUpload, FoirAgentRun, FoirAgentRunNode, FoirAgentThread, FoirCreateAgentAttachmentArgs, FoirStartAgentRunArgs } from '../data/builtins';
58
+ import type { FoirAgentAttachmentUpload, FoirAgentRun, FoirAgentHeldAct, FoirAgentRunNode, FoirAgentThread, FoirCreateAgentAttachmentArgs, FoirStartAgentRunArgs } from '../data/builtins';
59
59
  import type { FoirAgentRunEvent, FoirAgentRunSubscribeOptions } from '../data/subscriptions';
60
60
  import type { FoirSubscription } from '../data/ws-transport';
61
+ import { type StepReply } from '../runs/step-reply';
61
62
  /** What the transport needs of a client: the agents namespace a key that runs an agent is given. */
62
63
  export interface FoirChatClient {
63
64
  ns: {
@@ -68,12 +69,7 @@ export interface FoirChatClient {
68
69
  id: string;
69
70
  approve: boolean;
70
71
  reason?: string;
71
- }): Promise<FoirAgentRunNode | null>;
72
- answerPageTool(args: {
73
- id: string;
74
- output: unknown;
75
- visitorToken?: string;
76
- }): Promise<FoirAgentRunNode | null>;
72
+ }): Promise<FoirAgentHeldAct | null>;
77
73
  agentThread(args: {
78
74
  id: string;
79
75
  visitorToken?: string;
@@ -133,12 +129,6 @@ export interface FoirChatTransportOptions {
133
129
  */
134
130
  pageTools?: Record<string, FoirPageTool>;
135
131
  }
136
- /**
137
- * One run's events as `useChat`'s chunks. A run outlives a stream: it stops
138
- * for a decision and carries on once it is made, so what the person has
139
- * already been shown (the steps, the last event) is kept per run, and each
140
- * stream only opens and closes its own text.
141
- */
142
132
  export declare class FoirRunChunks {
143
133
  private readonly runId;
144
134
  private readonly runs;
@@ -150,14 +140,19 @@ export declare class FoirRunChunks {
150
140
  private opened;
151
141
  /** The run stopped for this stream's followers: it ended, or holds an act. */
152
142
  stopped: boolean;
153
- /** Calls to the page's tools waiting for the app's `addToolOutput`. */
154
- readonly pageCalls: Set<string>;
143
+ /**
144
+ * Calls to the page's tools waiting for the app's `addToolOutput`, each with
145
+ * the reply the page answers it by.
146
+ */
147
+ readonly pageCalls: Map<string, StepReply | null>;
155
148
  /** Calls to the page's tools the transport runs and answers itself, until the run hears them. */
156
149
  readonly answering: Set<string>;
157
150
  /** Calls the transport is to run: taken with `takeCalls`. */
158
151
  private toRun;
159
152
  /** Acts held for a decision, by anyone. */
160
153
  private readonly held;
154
+ /** The messages whose text this run has shown, streamed or whole. */
155
+ private readonly shown;
161
156
  /** `runs` names the page's tools the transport runs itself. */
162
157
  constructor(runId: string, runs?: (tool: string) => boolean);
163
158
  /** The calls to the page's tools that arrived for the transport to run. */
package/dist/ai.js CHANGED
@@ -57,9 +57,162 @@ function randomSecret() {
57
57
  return out;
58
58
  }
59
59
 
60
+ // src/transport/foir-http.ts
61
+ var FoirRequestError = class extends Error {
62
+ /** HTTP status code, when the request failed at the HTTP layer. */
63
+ status;
64
+ /** GraphQL errors, when the request failed with a no-data error envelope. */
65
+ graphQLErrors;
66
+ /**
67
+ * The `data` the server did return, when a STRICT read refused a partial
68
+ * response. Present only on that path: without it, opting into strictness
69
+ * would trade a silent wrong answer for a thrown one that discards the answer,
70
+ * and a caller that wants to log what it nearly rendered could not.
71
+ */
72
+ partialData;
73
+ /**
74
+ * The selections the server stripped, when the refusal was a pruned document
75
+ * rather than a partial error. Same purpose as `partialData`: a caller that
76
+ * wants to log what it nearly rendered, or name the offending path in a
77
+ * developer-facing message, has it without re-parsing anything.
78
+ */
79
+ prunedSelections;
80
+ cause;
81
+ constructor(message, options) {
82
+ super(message);
83
+ this.name = "FoirRequestError";
84
+ this.cause = options?.cause;
85
+ this.status = options?.status;
86
+ this.graphQLErrors = options?.graphQLErrors;
87
+ this.partialData = options?.partialData;
88
+ this.prunedSelections = options?.prunedSelections;
89
+ }
90
+ /**
91
+ * The code of the first error, if the response carried one.
92
+ *
93
+ * A getter rather than a stored field so it stays correct for an error
94
+ * constructed anywhere, including by a caller in a test.
95
+ */
96
+ get code() {
97
+ return this.graphQLErrors?.[0]?.extensions?.code;
98
+ }
99
+ /** The correlation id, if the response carried one. */
100
+ get requestId() {
101
+ return this.graphQLErrors?.[0]?.extensions?.requestId;
102
+ }
103
+ /**
104
+ * Whether retrying this request could succeed.
105
+ *
106
+ * The 429 is the one that has to be in here and is easy to leave out: it sits
107
+ * among the 4xx statuses that are otherwise permanent, so a policy written as
108
+ * "retry 5xx, never retry 4xx" excludes exactly the failure that clears on its
109
+ * own. A storefront wrote that policy, its key saturated, and every read
110
+ * failed on the first attempt for an hour.
111
+ */
112
+ get retryable() {
113
+ if (this.status === void 0) {
114
+ if (this.graphQLErrors?.length || this.prunedSelections?.length) {
115
+ return this.code === "RATE_LIMITED" || this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
116
+ }
117
+ return true;
118
+ }
119
+ if (this.status === 429 || this.code === "RATE_LIMITED") return true;
120
+ if (this.status >= 500) return true;
121
+ return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
122
+ }
123
+ /**
124
+ * How long to wait before retrying, in milliseconds, when the server said.
125
+ * Undefined when it did not, which means the caller picks its own backoff.
126
+ */
127
+ get retryAfterMs() {
128
+ const seconds = this.graphQLErrors?.[0]?.extensions?.retryAfter;
129
+ return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
130
+ }
131
+ };
132
+
133
+ // src/transport/retry.ts
134
+ var DEFAULT_ATTEMPTS = 3;
135
+ var DEFAULT_BASE_DELAY_MS = 200;
136
+ var DEFAULT_MAX_DELAY_MS = 2e4;
137
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
138
+ function foirRetryDelayMs(error, attempt, policy = {}) {
139
+ if (!(error instanceof FoirRequestError) || !error.retryable) return null;
140
+ const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
141
+ const stated = error.retryAfterMs;
142
+ if (stated !== void 0) return Math.min(stated, max);
143
+ const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
144
+ return Math.min(base * 2 ** (attempt - 1), max);
145
+ }
146
+ async function withFoirRetry(fn, policy = {}) {
147
+ const attempts = Math.max(1, policy.attempts ?? DEFAULT_ATTEMPTS);
148
+ const sleep = policy.sleep ?? defaultSleep;
149
+ for (let attempt = 1; ; attempt += 1) {
150
+ try {
151
+ return await fn();
152
+ } catch (error) {
153
+ const delayMs = attempt >= attempts ? null : foirRetryDelayMs(error, attempt, policy);
154
+ if (delayMs === null) throw error;
155
+ policy.onRetry?.({ attempt, delayMs, error });
156
+ await sleep(delayMs);
157
+ }
158
+ }
159
+ }
160
+
161
+ // src/runs/step-reply.ts
162
+ async function replyToStep(reply, body, opts = {}) {
163
+ if (!reply?.token || !reply.signingKey || !reply.endpoint) {
164
+ throw new Error("replyToStep: the step was handed no reply, so it cannot answer");
165
+ }
166
+ return sendReply(reply.endpoint, reply.token, reply.signingKey, body, opts);
167
+ }
168
+ async function sendReply(endpoint, token, signingKey, body, opts = {}) {
169
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
170
+ if (!fetchImpl) throw new Error("replyToStep: no fetch available; pass opts.fetch on runtimes without a global fetch");
171
+ const payload = JSON.stringify(body);
172
+ const signature = await hmacSha256Hex(signingKey, payload);
173
+ return withFoirRetry(async () => {
174
+ let resp;
175
+ try {
176
+ resp = await fetchImpl(endpoint, {
177
+ method: "POST",
178
+ headers: {
179
+ "Content-Type": "application/json",
180
+ Authorization: `Bearer ${token}`,
181
+ "X-Foir-Signature": `sha256=${signature}`
182
+ },
183
+ body: payload,
184
+ ...opts.signal ? { signal: opts.signal } : {}
185
+ });
186
+ } catch (err) {
187
+ if (opts.signal?.aborted) throw err;
188
+ throw new FoirRequestError("the reply got no answer", { cause: err });
189
+ }
190
+ if (!resp.ok) {
191
+ throw new FoirRequestError(`the reply was refused with HTTP ${resp.status}: ${await resp.text()}`, { status: resp.status });
192
+ }
193
+ const out = await resp.json();
194
+ if (!out || typeof out.status !== "string") {
195
+ throw new FoirRequestError("the reply was answered with nothing", { status: resp.status });
196
+ }
197
+ return { status: out.status, cancelled: out.cancelled === true, applied: out.applied === true };
198
+ }, opts.retry);
199
+ }
200
+ async function hmacSha256Hex(secret, body) {
201
+ const enc = new TextEncoder();
202
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
203
+ const sig = await crypto.subtle.sign("HMAC", key, enc.encode(body));
204
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
205
+ }
206
+
60
207
  // src/ai/index.ts
61
208
  var TOOL_KINDS = /* @__PURE__ */ new Set(["VERB", "OPERATION", "MCP_TOOL", "AGENT", "PAGE_TOOL"]);
62
209
  var STOPS = /* @__PURE__ */ new Set(["COMPLETED", "FAILED", "SKIPPED", "CANCELLED", "HELD"]);
210
+ function keptText(parts) {
211
+ return parts.map((p) => {
212
+ const part = p;
213
+ return part && part.type === "text" && typeof part.text === "string" ? part.text : "";
214
+ }).join("");
215
+ }
63
216
  var FoirRunChunks = class {
64
217
  /** `runs` names the page's tools the transport runs itself. */
65
218
  constructor(runId, runs = () => false) {
@@ -76,14 +229,19 @@ var FoirRunChunks = class {
76
229
  opened = false;
77
230
  /** The run stopped for this stream's followers: it ended, or holds an act. */
78
231
  stopped = false;
79
- /** Calls to the page's tools waiting for the app's `addToolOutput`. */
80
- pageCalls = /* @__PURE__ */ new Set();
232
+ /**
233
+ * Calls to the page's tools waiting for the app's `addToolOutput`, each with
234
+ * the reply the page answers it by.
235
+ */
236
+ pageCalls = /* @__PURE__ */ new Map();
81
237
  /** Calls to the page's tools the transport runs and answers itself, until the run hears them. */
82
238
  answering = /* @__PURE__ */ new Set();
83
239
  /** Calls the transport is to run: taken with `takeCalls`. */
84
240
  toRun = [];
85
241
  /** Acts held for a decision, by anyone. */
86
242
  held = /* @__PURE__ */ new Set();
243
+ /** The messages whose text this run has shown, streamed or whole. */
244
+ shown = /* @__PURE__ */ new Set();
87
245
  /** The calls to the page's tools that arrived for the transport to run. */
88
246
  takeCalls() {
89
247
  const out = this.toRun;
@@ -118,6 +276,7 @@ var FoirRunChunks = class {
118
276
  switch (event.kind) {
119
277
  case "TEXT": {
120
278
  if (!event.text) break;
279
+ if (event.messageId) this.shown.add(event.messageId);
121
280
  this.open(out, fresh);
122
281
  this.closeReasoning(out);
123
282
  if (!this.textId) {
@@ -146,6 +305,19 @@ var FoirRunChunks = class {
146
305
  out.push(...this.toolChunks(node));
147
306
  break;
148
307
  }
308
+ case "MESSAGE": {
309
+ const message = event.message;
310
+ if (!message || message.role !== "ASSISTANT" || this.shown.has(message.id)) break;
311
+ const text = keptText(message.parts);
312
+ if (!text) break;
313
+ this.shown.add(message.id);
314
+ this.open(out, fresh);
315
+ this.closeText(out);
316
+ this.closeReasoning(out);
317
+ const id = `${this.runId}:m:${message.id}`;
318
+ out.push({ type: "text-start", id }, { type: "text-delta", id, delta: text }, { type: "text-end", id });
319
+ break;
320
+ }
149
321
  case "DATA": {
150
322
  if (!event.dataName) break;
151
323
  this.open(out, fresh);
@@ -161,7 +333,7 @@ var FoirRunChunks = class {
161
333
  if (event.status === "FAILED") {
162
334
  out.push({ type: "error", errorText: event.error || "The answer could not be finished." });
163
335
  }
164
- if (event.status === "SKIPPED" && event.answer && this.tools.size === 0) {
336
+ if (event.status === "SKIPPED" && event.answer && this.tools.size === 0 && this.shown.size === 0) {
165
337
  const id = `${this.runId}:refusal`;
166
338
  out.push({ type: "text-start", id }, { type: "text-delta", id, delta: event.answer }, { type: "text-end", id });
167
339
  }
@@ -228,7 +400,7 @@ var FoirRunChunks = class {
228
400
  this.answering.add(node.id);
229
401
  this.toRun.push(node);
230
402
  } else {
231
- this.pageCalls.add(node.id);
403
+ this.pageCalls.set(node.id, node.reply ?? null);
232
404
  }
233
405
  return out;
234
406
  }
@@ -348,7 +520,6 @@ function agentsOf(client, caller) {
348
520
  startAgentRun: refuse,
349
521
  onRun: refuse,
350
522
  decideAgentApproval: refuse,
351
- answerPageTool: refuse,
352
523
  agentThread: refuse,
353
524
  createAgentAttachment: refuse
354
525
  };
@@ -394,6 +565,10 @@ function foirChatTransport(options) {
394
565
  }
395
566
  return c;
396
567
  };
568
+ const answerPage = async (id, reply, output) => {
569
+ if (!reply) throw new Error(`The call ${id} came without its reply, so the page cannot answer it.`);
570
+ await replyToStep(reply, { result: output ?? null });
571
+ };
397
572
  const answerCall = async (node, signal) => {
398
573
  const tool = pageTools[node.key];
399
574
  let output;
@@ -404,7 +579,7 @@ function foirChatTransport(options) {
404
579
  } catch (err) {
405
580
  output = { error: err instanceof Error ? err.message : String(err) };
406
581
  }
407
- await agents.answerPageTool({ id: node.id, output, visitorToken: visitor });
582
+ await answerPage(node.id, node.reply, output);
408
583
  };
409
584
  const follow = (runId, fresh, signal) => {
410
585
  const chunks = chunksOf(runId);
@@ -456,7 +631,7 @@ function foirChatTransport(options) {
456
631
  async sendMessages({ chatId, messages, abortSignal }) {
457
632
  const decisions = decisionsIn(messages, decided);
458
633
  const waiting = /* @__PURE__ */ new Set();
459
- for (const c of runs.values()) for (const id2 of c.pageCalls) waiting.add(id2);
634
+ for (const c of runs.values()) for (const id2 of c.pageCalls.keys()) waiting.add(id2);
460
635
  const answers = pageAnswersIn(messages, waiting);
461
636
  if (decisions.length > 0 || answers.length > 0) {
462
637
  for (const d of decisions) {
@@ -464,7 +639,9 @@ function foirChatTransport(options) {
464
639
  decided.add(d.id);
465
640
  }
466
641
  for (const a of answers) {
467
- await agents.answerPageTool({ id: a.id, output: a.output, visitorToken: visitor });
642
+ let reply;
643
+ for (const c of runs.values()) reply ??= c.pageCalls.get(a.id);
644
+ await answerPage(a.id, reply, a.output);
468
645
  for (const c of runs.values()) c.pageCalls.delete(a.id);
469
646
  }
470
647
  let runId = lastRun.get(chatId);
@@ -462,11 +462,15 @@ export interface AppClient {
462
462
  deleted: boolean;
463
463
  }>;
464
464
  /**
465
- * Drop what the data API has cached for one record, by natural key or id,
466
- * so its next read resolves it again. False when Foir holds no such
467
- * record. For a source read live from its host.
465
+ * Drop what the data API has cached for one record, named by its id or
466
+ * by its natural key, so its next read resolves it again. False when Foir
467
+ * holds no such record. For a source read live from its host.
468
468
  */
469
- refresh(model: string, idOrKey: string): Promise<{
469
+ refresh(model: string, record: {
470
+ id: string;
471
+ } | {
472
+ naturalKey: string;
473
+ }): Promise<{
470
474
  refreshed: boolean;
471
475
  }>;
472
476
  /**
@@ -766,7 +770,7 @@ export interface AppStartedOperation {
766
770
  /** True when `concurrencyKey` matched a run in flight, so nothing was started. */
767
771
  joined: boolean;
768
772
  /** True when `idempotencyKey` matched a run in flight or completed, so nothing was started. */
769
- deduplicated: boolean;
773
+ replayed: boolean;
770
774
  }
771
775
  /** Build the client an installed app calls Foir with. See the module doc. */
772
776
  export declare function createAppClient(config: AppClientConfig): AppClient;
package/dist/app.js CHANGED
@@ -632,10 +632,16 @@ function createAppClient(config) {
632
632
  const deleted = await field(name, `mutation AppDeleteRecord($id: String!) { ${name}(id: $id) }`, { id });
633
633
  return { deleted: deleted === true };
634
634
  },
635
- async refresh(model, idOrKey) {
636
- if (idOrKey === "") refuse(`a record refresh on ${model} was given an empty name`);
635
+ async refresh(model, record) {
636
+ const byId = "id" in record;
637
+ const value = byId ? record.id : record.naturalKey;
638
+ if (value === "") refuse(`a record refresh on ${model} was given an empty name`);
637
639
  const name = `refresh${appModelTypeName(model)}`;
638
- const refreshed = await field(name, `mutation AppRefreshRecord($idOrKey: String!) { ${name}(idOrKey: $idOrKey) }`, { idOrKey });
640
+ const refreshed = await field(
641
+ name,
642
+ byId ? `mutation AppRefreshRecord($id: ID!) { ${name}(id: $id) }` : `mutation AppRefreshRecord($naturalKey: String!) { ${name}(naturalKey: $naturalKey) }`,
643
+ byId ? { id: value } : { naturalKey: value }
644
+ );
639
645
  if (typeof refreshed !== "boolean") return refuse(`${name} reported no error and did not say whether it refreshed the record`);
640
646
  return { refreshed };
641
647
  },
@@ -972,12 +978,12 @@ function createAppClient(config) {
972
978
  args.push("concurrencyKey: $concurrencyKey");
973
979
  variables.concurrencyKey = options.concurrencyKey;
974
980
  }
975
- const document = `mutation AppStartOperation(${params.join(", ")}) { startOperation(${args.join(", ")}) { executionId joined deduplicated } }`;
981
+ const document = `mutation AppStartOperation(${params.join(", ")}) { startOperation(${args.join(", ")}) { executionId joined replayed } }`;
976
982
  const keyed = Boolean(options.idempotencyKey || options.concurrencyKey);
977
983
  const data = keyed ? await run(document, variables) : await send(document, variables);
978
984
  const answer = data["startOperation"];
979
985
  if (!answer?.executionId) return refuse("startOperation reported no error and named no run");
980
- return { executionId: answer.executionId, joined: answer.joined === true, deduplicated: answer.deduplicated === true };
986
+ return { executionId: answer.executionId, joined: answer.joined === true, replayed: answer.replayed === true };
981
987
  }
982
988
  }
983
989
  };
@@ -1067,18 +1073,11 @@ function readCallback(value) {
1067
1073
  if (!isObject(value)) return void 0;
1068
1074
  const token = text(value.token);
1069
1075
  if (!token) return void 0;
1070
- const mutations = isObject(value.mutations) ? value.mutations : {};
1071
1076
  const signingKey = text(value.signingKey);
1072
1077
  return {
1073
- gqlEndpoint: text(value.gqlEndpoint) ?? "",
1078
+ endpoint: text(value.endpoint) ?? "",
1074
1079
  token,
1075
1080
  expiresAt: text(value.expiresAt) ?? "",
1076
- mutations: {
1077
- complete: text(mutations.complete) ?? "",
1078
- fail: text(mutations.fail) ?? "",
1079
- progress: text(mutations.progress) ?? "",
1080
- cancel: text(mutations.cancel) ?? ""
1081
- },
1082
1081
  ...Array.isArray(value.causationChain) ? { causationChain: value.causationChain.map(String) } : {},
1083
1082
  ...signingKey ? { signingKey } : {}
1084
1083
  };