@oneshot-agent/sdk 0.59.0 → 0.61.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/dist/index.js CHANGED
@@ -37,6 +37,7 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.ReadOnlyWalletProvider = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
40
+ const untrusted_content_1 = require("./untrusted-content");
40
41
  const hunts_1 = require("./hunts");
41
42
  __exportStar(require("./solve"), exports);
42
43
  __exportStar(require("./predict"), exports);
@@ -61,13 +62,9 @@ Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function
61
62
  Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
62
63
  __exportStar(require("./errors"), exports);
63
64
  const approvals_1 = require("./approvals");
64
- // `./receipt` is NOT re-exported here, values or types: it statically imports
65
- // `node:crypto`, which would force a hard Node dependency on every consumer
66
- // including browser bundles; and its `SignableReceipt.providerCost` field must
67
- // stay off this public surface (see tests/unit/sdk-provider-cost-removed.test.ts).
68
- // Import from the `@oneshot-agent/sdk/receipt` subpath instead.
65
+ // Keep receipt on its subpath: node:crypto breaks browser bundles, and providerCost is not public.
69
66
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
70
- const SDK_VERSION = '0.59.0';
67
+ const SDK_VERSION = '0.61.0';
71
68
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
72
69
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
73
70
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
@@ -106,12 +103,7 @@ function chainIdFromNetwork(network) {
106
103
  return m ? Number(m[1]) : undefined;
107
104
  }
108
105
  const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
109
- // ETH-currency mode. A payment is an EIP-3009 authorization that the
110
- // facilitator settles AFTER the API has responded, so for seconds after a
111
- // successful call `balanceOf` still shows pre-payment funds. The SDK therefore
112
- // keeps a ledger of signed-but-unsettled authorizations and works from
113
- // `balanceOf − pending`, over-reserving (one early buffered swap) rather than
114
- // under-reserving (a failed settlement).
106
+ // Reserve signed-but-unsettled payments against balanceOf so delayed settlement cannot overspend USDC.
115
107
  /** One balanceOf read per paid call, deduped within roughly one Base block. */
116
108
  const USDC_BALANCE_CACHE_MS = 2000;
117
109
  /** How long a signed authorization stays subtracted from the on-chain balance (settlement lands well within this). */
@@ -139,10 +131,7 @@ __exportStar(require("./types"), exports);
139
131
  * await agent.email({ to: 'user@example.com', subject: 'Hi', body: 'Hello' });
140
132
  * ```
141
133
  */
142
- /**
143
- * Reject a budget config the server would reject, at construction rather than
144
- * on the first paid call — a typo'd cap must not become "no cap".
145
- */
134
+ /** Reject invalid budgets at construction so a typo cannot silently remove the cap. */
146
135
  function validateBudgetConfig(budgets) {
147
136
  if (!budgets)
148
137
  return undefined;
@@ -174,13 +163,13 @@ function validateBudgetConfig(budgets) {
174
163
  }
175
164
  /** The keys executeToolRequestImpl takes out of a request as SDK options, so they never reach the API body. */
176
165
  const PAID_ROUTE_OPTION_KEYS = ['timeout', 'totalTimeoutMs', 'onRequestCreated', 'onAccepted', 'signal', 'onStatusUpdate', 'wait', 'waitForPhones', 'phoneTimeoutSec', 'idempotencyKey', 'approvalId', 'maxCost'];
177
- /** ETH mode is unsupported on CDP wallets (#2153): the swap path needs a confirmed receipt CDP never returns. */
166
+ /** ETH mode is unsupported on CDP wallets: the swap path needs a confirmed receipt CDP never returns. */
178
167
  function cdpEthModeError() {
179
168
  return new errors_2.ValidationError("ETH currency mode is not supported with CDP wallets; fund the CDP wallet with USDC or use currency: 'USDC' (or a privateKey / custom walletProvider for ETH mode)", 'currency');
180
169
  }
181
170
  class OneShot {
182
171
  /**
183
- * Async factory — required for CDP wallets (account creation is async).
172
+ * Async factory, required for CDP wallets (account creation is async).
184
173
  * Also works with privateKey and custom walletProvider.
185
174
  *
186
175
  * @example
@@ -196,8 +185,7 @@ class OneShot {
196
185
  if (config.accessToken) {
197
186
  if (config.address)
198
187
  return new OneShot(config);
199
- // The API keys job status and the WebSocket on X-Agent-ID, so the
200
- // session needs its wallet address even though it never signs with it.
188
+ // Job status and WebSocket requests need the wallet address even when the session never signs.
201
189
  const address = await OneShot.resolveTokenAddress(config);
202
190
  return new OneShot({ ...config, address });
203
191
  }
@@ -205,7 +193,7 @@ class OneShot {
205
193
  return new OneShot(config, config.walletProvider);
206
194
  }
207
195
  if (config.cdp) {
208
- // Reject before the CDP SDK is imported or an account is created (#2153).
196
+ // Reject before importing the CDP SDK or creating an account.
209
197
  if ((config.currency ?? 'USDC') === 'ETH')
210
198
  throw cdpEthModeError();
211
199
  const { CdpWalletProvider } = await Promise.resolve().then(() => __importStar(require('./providers/cdp')));
@@ -221,7 +209,7 @@ class OneShot {
221
209
  throw new errors_2.ValidationError('Provide one of: privateKey, cdp, or walletProvider', 'config');
222
210
  }
223
211
  /**
224
- * Sync constructor — works with privateKey (backwards compatible).
212
+ * Sync constructor; works with privateKey.
225
213
  * For CDP wallets, use OneShot.create() instead.
226
214
  */
227
215
  constructor(config, walletProvider) {
@@ -260,9 +248,7 @@ class OneShot {
260
248
  this._stagingKey = config.stagingKey;
261
249
  this.rpcProvider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl ?? RPC_URL);
262
250
  if (config.accessToken) {
263
- // Option D: access-token session. Exclusive with every wallet option,
264
- // and budgets are read-only (a delegated credential that could raise its
265
- // own cap would make the cap meaningless) — set them from a wallet.
251
+ // Delegated tokens cannot change their own budgets; caps must be set from a wallet session.
266
252
  if (!/^oneshot_[0-9a-f]{64}$/.test(config.accessToken)) {
267
253
  throw new errors_2.ValidationError('accessToken must be oneshot_<64 lowercase hex characters>', 'accessToken');
268
254
  }
@@ -297,9 +283,7 @@ class OneShot {
297
283
  else {
298
284
  throw new errors_2.ValidationError('Provide privateKey or use OneShot.create() for CDP/custom wallets', 'config');
299
285
  }
300
- // CDP wallets cannot run ETH mode even though CdpWalletProvider exposes
301
- // sendTransaction: its wait() never observes a receipt, so the swap path
302
- // cannot confirm the ETH→USDC trade (#2153).
286
+ // CDP wait() never confirms a receipt, so ETH mode cannot confirm the swap.
303
287
  if (config.cdp && this._currency === 'ETH')
304
288
  throw cdpEthModeError();
305
289
  if (this._currency === 'ETH' && !this.provider.sendTransaction) {
@@ -342,16 +326,8 @@ class OneShot {
342
326
  this.validate(options.subject, 'subject');
343
327
  }
344
328
  this.validate(options.body, 'body');
345
- // Rotation mode: when the caller passes neither from_domain nor
346
- // from_mailbox, omit from_address entirely so the server picks from
347
- // the agent's domain pool. The chosen address comes back on the
348
- // quote response (`quote.from_address`) and we replay it on /send.
349
- // When the caller pins from_domain (with or without from_mailbox), we
350
- // build `${mailbox ?? 'agent'}@${from_domain}`. Pinning from_mailbox
351
- // alone has no domain to attach it to — `oneshotagent.com` is OneShot's
352
- // own domain and no agent can send from it — so that combination is a
353
- // client-side validation error rather than a guaranteed domain_not_owned
354
- // rejection from the server.
329
+ // Without a pinned sender, the quote locks an address from the agent's domain pool.
330
+ // A mailbox alone has no owned domain to send from.
355
331
  if (options.from_mailbox && !options.from_domain) {
356
332
  throw new errors_2.ValidationError('from_mailbox was pinned without from_domain. Pass from_domain (a domain you own) as well, or omit both to rotate across your warmed domains.', 'from_domain');
357
333
  }
@@ -359,24 +335,14 @@ class OneShot {
359
335
  const fromAddress = useRotation
360
336
  ? undefined
361
337
  : `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
362
- // Confirming a new-domain registration (#1070): the caller passes back the
363
- // quote_id of the `confirmation_required` result it was shown. That quote
364
- // is sent as-is — no second quote — so the server charges the stored total
365
- // of the quote the user saw (x402-legacy validates the id and takes the
366
- // amount from the stored quote). A bare `true` is not a confirmation: it
367
- // names no amount, so it falls through to the quote-only path below. An
368
- // expired or already-used quote is refused by the server, and the next
369
- // unconfirmed call produces a fresh quote to show again.
338
+ // Reuse the confirmed quote so the charge matches the amount shown; true alone confirms no amount.
370
339
  const confirmedQuote = typeof options.confirmNewDomain === 'string' && options.confirmNewDomain.trim()
371
340
  ? options.confirmNewDomain.trim() : undefined;
372
341
  if (confirmedQuote) {
373
342
  this.log(`Email: sending against confirmed quote ${confirmedQuote}`);
374
343
  return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress), confirmedQuote);
375
344
  }
376
- // `mailbox_provisioning_fee` (>0) means `from_address` is a new address that
377
- // provisions a mailbox on first send — a one-time fee folded into `total_cost`.
378
- // `is_new` (true) means this quote would register a brand-new domain, which
379
- // drives the confirm-before-charge gate below (#1070 round 2).
345
+ // Mailbox provisioning and domain registration are distinct fees; only a new domain needs confirmation.
380
346
  const quote = await this.tool('email/quote', {
381
347
  ...(fromAddress ? { from_address: fromAddress } : {}),
382
348
  ...(options.to !== undefined ? { to_address: options.to } : {}),
@@ -384,23 +350,12 @@ class OneShot {
384
350
  ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
385
351
  ...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
386
352
  body: options.body,
387
- // Forward maxCost so the server-side X-Max-Cost-USDC header is set on
388
- // the quote fetch (executeToolRequest destructures + threads it).
389
353
  maxCost: options.maxCost,
390
354
  });
391
355
  this.log(`Email quote: $${quote.total_cost}`);
392
- // Local fast-fail — matches the per-tool check on commerce/voice/sms/build/
393
- // browser/compute. If the server-side header guard fired we'd never reach
394
- // here; this catches the case where the server's enforcement is bypassed
395
- // (e.g. a future API revision without the header check) or skipped (cap
396
- // un-set so the server returned a 200 with the quote and the caller still
397
- // wants the local guard to apply).
356
+ // Enforce the cap locally even if the server did not check the header.
398
357
  this.assertWithinMaxCost(quote.total_cost, options.maxCost);
399
- // Registering a brand-new domain is charged only after its exact quote has
400
- // been shown and explicitly authorized (#1070 round 2) — this call stops
401
- // here, before /email/send, and charges nothing. Pass confirmNewDomain:
402
- // true (with the same from_domain) to register the domain, pay the quoted
403
- // total, and send the email in one follow-up call.
358
+ // A new domain must be explicitly authorized against its exact quote before any charge or send.
404
359
  if (quote.is_new) {
405
360
  const domainName = quote.domain ?? options.from_domain ?? '';
406
361
  const breakdown = quote.registration_fee !== undefined && quote.service_fee !== undefined
@@ -423,16 +378,11 @@ class OneShot {
423
378
  /** The /email/send body for `email()`: the same whether it follows a fresh quote or a confirmed one. */
424
379
  emailSendPayload(options, resolvedFromAddress) {
425
380
  const payload = {
426
- // Server replays the locked address from the quote when from_address
427
- // is absent, but sending it anyway is harmless and forward-compatible
428
- // with future SDK versions that talk directly to /send without quoting.
429
381
  ...(resolvedFromAddress ? { from_address: resolvedFromAddress } : {}),
430
382
  ...(options.to !== undefined ? { to_address: options.to } : {}),
431
383
  ...(options.subject !== undefined ? { subject: options.subject } : {}),
432
384
  ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
433
- // Re-passed on /send so a new-domain mailbox quote still provisions a real
434
- // mailbox even though the domain row doesn't exist yet (server also falls back
435
- // to 'mailbox' when a provisioning fee was paid on the quote).
385
+ // Preserve mailbox provisioning when the new domain does not yet have a stored mode.
436
386
  ...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
437
387
  body: options.body,
438
388
  signal: options.signal,
@@ -446,15 +396,14 @@ class OneShot {
446
396
  payload.attachments = options.attachments;
447
397
  }
448
398
  if (options.idempotencyKey) {
449
- // Only the send call carries the key — the quote call has no side effects.
399
+ // Only sends need idempotency; quoting has no side effects.
450
400
  payload.idempotencyKey = options.idempotencyKey;
451
401
  }
452
402
  if (options.approvalId) {
453
403
  // Releases a send the action policy held (see ApprovalRequiredError).
454
404
  payload.approvalId = options.approvalId;
455
405
  }
456
- // Forward maxCost so the server-side X-Max-Cost-USDC header also guards
457
- // the send, which matters on a confirmed send that skipped the quote.
406
+ // Confirmed sends skip quoting, so the send must also carry the cap.
458
407
  if (options.maxCost !== undefined)
459
408
  payload.maxCost = options.maxCost;
460
409
  return payload;
@@ -515,7 +464,7 @@ class OneShot {
515
464
  const { includePhone, ...payload } = options;
516
465
  return this.tool('find/person', { ...payload, include_phone: includePhone, limit: options.limit ?? 100 });
517
466
  }
518
- /** At least one identity (#1899): name, email, phone or a profile URL under any of its names. */
467
+ /** At least one identity: name, email, phone or a profile URL under any of its names. */
519
468
  requirePersonIdentity(options) {
520
469
  const hasName = options.name || options.full_name || (options.first_name && options.last_name);
521
470
  if (!hasName && !options.email && !options.phone && !options.profile_url && !options.linkedin_url && !options.social_media_url) {
@@ -562,7 +511,7 @@ class OneShot {
562
511
  return this.tool('resolve/business', { ...options });
563
512
  }
564
513
  /**
565
- * Federal contract opportunities (SAM.gov) by NAICS code — Sources Sought
514
+ * Federal contract opportunities (SAM.gov) by NAICS code - Sources Sought
566
515
  * and Presolicitation notices with the contracting officer's published
567
516
  * contact. Flat price per search; zero notices is a completed result.
568
517
  * `data_as_of` on the result is the snapshot behind the rows (SAM.gov's
@@ -593,7 +542,7 @@ class OneShot {
593
542
  async personDossier(options) {
594
543
  this.requirePersonIdentity(options);
595
544
  const { includePhone, ...payload } = options;
596
- // The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run (#1913).
545
+ // The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run.
597
546
  return this.tool('lookup/person-dossier', { ...payload, include_phone: includePhone, timeout: options.timeout ?? 300 });
598
547
  }
599
548
  async personSocialAccounts(options) {
@@ -629,7 +578,7 @@ class OneShot {
629
578
  if (!response.ok) {
630
579
  throw new errors_2.ToolError('Failed to list inbox', response.status, await response.text(), response.headers.get('retry-after'));
631
580
  }
632
- return response.json();
581
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'inbox_list');
633
582
  }
634
583
  async inboxGet(emailId) {
635
584
  this.validate(emailId, 'emailId');
@@ -642,7 +591,7 @@ class OneShot {
642
591
  if (!response.ok) {
643
592
  throw new errors_2.ToolError('Failed to get email', response.status, await response.text(), response.headers.get('retry-after'));
644
593
  }
645
- return response.json();
594
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'inbox_get');
646
595
  }
647
596
  async commerceBuy(options) {
648
597
  this.validate(options.product_url, 'product_url');
@@ -754,9 +703,9 @@ class OneShot {
754
703
  if (options.wait !== false && result.request_id) {
755
704
  // Wait for the longest the call may run, plus two minutes to dial and settle.
756
705
  const timeout = options.timeout ?? (options.max_duration_minutes ?? 10) * 60 + 120;
757
- return this.pollJob(result.request_id, timeout, options.signal, options.onStatusUpdate);
706
+ return this.pollJob(result.request_id, timeout, options.signal, options.onStatusUpdate, undefined, 'voice');
758
707
  }
759
- return result;
708
+ return (0, untrusted_content_1.markUntrusted)(result, 'voice');
760
709
  }
761
710
  /**
762
711
  * Send an SMS message
@@ -947,8 +896,6 @@ class OneShot {
947
896
  payload.session_id = options.session_id;
948
897
  if (options.profile_id)
949
898
  payload.profile_id = options.profile_id;
950
- if (options.secrets !== undefined)
951
- throw new errors_2.ValidationError('secrets is unsupported; import cookies or storage_state with createBrowserProfile', 'secrets');
952
899
  if (options.max_steps)
953
900
  payload.max_steps = options.max_steps;
954
901
  const { execResp } = await this.runQuoteToPay({
@@ -972,9 +919,9 @@ class OneShot {
972
919
  const result = await execResp.json();
973
920
  this.log(`Browser task initiated: ${result.request_id}`);
974
921
  if (options.wait !== false && result.request_id) {
975
- return this.pollJob(result.request_id, options.timeout ?? 300, options.signal, options.onStatusUpdate);
922
+ return this.pollJob(result.request_id, options.timeout ?? 300, options.signal, options.onStatusUpdate, undefined, 'browser');
976
923
  }
977
- return result;
924
+ return (0, untrusted_content_1.markUntrusted)(result, 'browser');
978
925
  }
979
926
  /**
980
927
  * Create a persistent browser profile for reusing cookies/localStorage across sessions
@@ -1094,7 +1041,7 @@ class OneShot {
1094
1041
  throw new errors_1.LinkedInConnectRequiredError(body);
1095
1042
  throw new errors_2.ToolError(`LinkedIn ${opts.what} failed`, response.status, body, response.headers.get('retry-after'));
1096
1043
  }
1097
- return response.json();
1044
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), `/v1/tools/linkedin${path}`, method);
1098
1045
  }
1099
1046
  /**
1100
1047
  * Create a hosted LinkedIn login link. Show `url` to the human once; it
@@ -1222,7 +1169,6 @@ class OneShot {
1222
1169
  this.validate(options.identifier, 'identifier');
1223
1170
  const { accountId, identifier, notify, sections, ...rest } = options;
1224
1171
  const body = { ...rest, account_id: accountId, identifier, notify: !!notify, timeout: options.timeout ?? 180 };
1225
- // Left off the wire when empty, so a view without sections is the request it always was.
1226
1172
  if (sections?.length)
1227
1173
  body.sections = sections;
1228
1174
  const result = await this.tool('linkedin/profile-view', body);
@@ -1285,7 +1231,7 @@ class OneShot {
1285
1231
  if (!response.ok) {
1286
1232
  throw new errors_2.ToolError('Failed to list SMS inbox', response.status, await response.text(), response.headers.get('retry-after'));
1287
1233
  }
1288
- return response.json();
1234
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'sms_inbox_list');
1289
1235
  }
1290
1236
  /**
1291
1237
  * Get this agent a voice number without placing a call. Returns the existing
@@ -1368,7 +1314,7 @@ class OneShot {
1368
1314
  return this.signedJson('GET', `/v1/tools/voice/inbound/${encodeURIComponent(callId)}`, undefined, 'read', 'Failed to get inbound call');
1369
1315
  }
1370
1316
  /**
1371
- * Call a declared route of the API by method and path (#1580), the one generic call for a route that has no
1317
+ * Call a declared route of the API by method and path, the one generic call for a route that has no
1372
1318
  * typed method. Follows the Python client's `call_free_get/post/patch/put/delete`:
1373
1319
  *
1374
1320
  * - `path` is the route, with `{name}` (or `:name`) for a path parameter, filled from `params` and URL-encoded
@@ -1448,7 +1394,7 @@ class OneShot {
1448
1394
  if (response.status === 204 || !text)
1449
1395
  return { success: true };
1450
1396
  const json = JSON.parse(text);
1451
- return (json && typeof json === 'object' && 'data' in json ? json.data : json);
1397
+ return (0, untrusted_content_1.markUntrusted)(json && typeof json === 'object' && 'data' in json ? json.data : json, target, method);
1452
1398
  })();
1453
1399
  return await (0, deadline_1.abortable)(request, scope.signal);
1454
1400
  }
@@ -1470,7 +1416,7 @@ class OneShot {
1470
1416
  throw new errors_2.ToolError(failure, response.status, await response.text(), response.headers.get('retry-after'));
1471
1417
  }
1472
1418
  const json = await response.json();
1473
- return (json.data ?? json);
1419
+ return (0, untrusted_content_1.markUntrusted)(json.data ?? json, path, method);
1474
1420
  }
1475
1421
  /**
1476
1422
  * Get a specific inbound SMS message
@@ -1492,7 +1438,7 @@ class OneShot {
1492
1438
  if (!response.ok) {
1493
1439
  throw new errors_2.ToolError('Failed to get SMS message', response.status, await response.text(), response.headers.get('retry-after'));
1494
1440
  }
1495
- return response.json();
1441
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'sms_inbox_get');
1496
1442
  }
1497
1443
  /**
1498
1444
  * List notifications for the agent
@@ -1541,7 +1487,7 @@ class OneShot {
1541
1487
  }
1542
1488
  }
1543
1489
  /**
1544
- * The agent's connections (#2403): GitHub and business systems, each with `id`, `system`, `account_label` and
1490
+ * The agent's connections: GitHub and business systems, each with `id`, `system`, `account_label` and
1545
1491
  * `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement) takes the `id` of an active
1546
1492
  * one as `connection_id`; pick it by `system` and `account_label`, then call the read with the generic
1547
1493
  * {@link OneShot.route}.
@@ -1570,12 +1516,10 @@ class OneShot {
1570
1516
  return response.json();
1571
1517
  }
1572
1518
  async getBalance(tokenAddress) {
1573
- // Use unified API endpoint for USDC (default) balance
1574
1519
  if (!tokenAddress || tokenAddress === this.usdcAddress) {
1575
1520
  const unified = await this.getUnifiedBalance();
1576
1521
  return unified.on_chain_balance;
1577
1522
  }
1578
- // Custom token: keep existing RPC logic
1579
1523
  const contract = new ethers_1.ethers.Contract(tokenAddress, ['function balanceOf(address) view returns (uint256)', 'function decimals() view returns (uint8)'], this.rpcProvider);
1580
1524
  const [balance, decimals] = await Promise.all([
1581
1525
  contract.balanceOf(this.provider.address),
@@ -1584,7 +1528,7 @@ class OneShot {
1584
1528
  return ethers_1.ethers.formatUnits(balance, decimals);
1585
1529
  }
1586
1530
  /**
1587
- * Create a compute goal — the orchestrator will plan, execute, and iterate autonomously.
1531
+ * Create a compute goal - the orchestrator will plan, execute, and iterate autonomously.
1588
1532
  *
1589
1533
  * Uses the quote-then-pay flow: first call gets a 402 with a budget estimate,
1590
1534
  * second call (with payment) creates the goal.
@@ -1623,7 +1567,9 @@ class OneShot {
1623
1567
  payload.approvals = options.approvals;
1624
1568
  if (options.done_check)
1625
1569
  payload.done_check = options.done_check;
1626
- // Compute returns the goal directly on 202 — no job polling, unlike the
1570
+ if (options.egress_allowlist)
1571
+ payload.egress_allowlist = options.egress_allowlist;
1572
+ // Compute returns the goal directly on 202 - no job polling, unlike the
1627
1573
  // other paid tools.
1628
1574
  const { execResp: createResp } = await this.runQuoteToPay({
1629
1575
  endpoint: '/v1/compute',
@@ -1681,7 +1627,7 @@ class OneShot {
1681
1627
  * with JobError (goal ID, end_reason code, snapshot in partialResult).
1682
1628
  * Budget exhaustion ends this wait; after fundComputeGoal revives the goal,
1683
1629
  * call waitForComputeGoal again with the same ID to observe the new run.
1684
- * Paused/awaiting_confirmation and recurring goals keep waiting until the
1630
+ * Paused/awaiting_confirmation/awaiting_connection and recurring goals keep waiting until the
1685
1631
  * goal ends or the timeout (seconds, default 120) expires with JobTimeoutError.
1686
1632
  * Infinity disables the local deadline.
1687
1633
  * onStatusUpdate receives each distinct observed status before settlement.
@@ -1907,6 +1853,30 @@ class OneShot {
1907
1853
  const json = await response.json();
1908
1854
  return json.data;
1909
1855
  }
1856
+ /**
1857
+ * Dispute a goal that ended `impossible_unverified`: the loop claimed it
1858
+ * cannot be done with no proof code could check. The dispute is recorded
1859
+ * on the goal and its receipt; nothing is refunded.
1860
+ *
1861
+ * @example
1862
+ * ```typescript
1863
+ * await agent.disputeComputeGoal(goalId, { reason: 'The data is public.' });
1864
+ * ```
1865
+ */
1866
+ async disputeComputeGoal(goalId, input = {}) {
1867
+ this.validate(goalId, 'goalId');
1868
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1869
+ method: 'POST',
1870
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1871
+ body: JSON.stringify({ dispute: true, ...(input.reason ? { response: input.reason } : {}) }),
1872
+ });
1873
+ if (response.status === 404)
1874
+ throw new errors_2.ToolError('Goal not found', 404, await response.text(), response.headers.get('retry-after'));
1875
+ if (!response.ok)
1876
+ throw new errors_2.ToolError('Failed to dispute compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1877
+ const json = await response.json();
1878
+ return json.data;
1879
+ }
1910
1880
  /**
1911
1881
  * Pause a recurring compute goal
1912
1882
  *
@@ -2106,7 +2076,7 @@ class OneShot {
2106
2076
  * List receipts with optional filtering
2107
2077
  *
2108
2078
  * Paginate with `limit`/`offset`, or page through an explicit time window with
2109
- * `since`/`until` (ISO string or Date) — useful for reaching receipts older than
2079
+ * `since`/`until` (ISO string or Date) - useful for reaching receipts older than
2110
2080
  * the server's 100-row cap when tagging value weeks after the originating call.
2111
2081
  *
2112
2082
  * @example
@@ -2144,8 +2114,8 @@ class OneShot {
2144
2114
  * Three ways to address the value:
2145
2115
  * - `rcpt_…` receipt id (from `result.receipt_id` or `receiptsList`), or
2146
2116
  * - the `request_id` returned by the originating tool call (`result.request_id`,
2147
- * resolved server-side via `Receipt.job_id`) — annotate without a list lookup, or
2148
- * - a `goalId` correlation key — attributes a whole *cadence* outcome (the email,
2117
+ * resolved server-side via `Receipt.job_id`) - annotate without a list lookup, or
2118
+ * - a `goalId` correlation key - attributes a whole *cadence* outcome (the email,
2149
2119
  * the follow-ups, the find/verify/enrich calls that share `decisionContext.goalId`)
2150
2120
  * in one call. The value is recorded once in the outcome ledger, so it can't
2151
2121
  * double-count across the cadence's receipts. Read it back with `rocsByGoal()`.
@@ -2159,13 +2129,12 @@ class OneShot {
2159
2129
  * const r = await agent.personEmailStatus({ email: 'x@y.com' });
2160
2130
  * await agent.tagReceiptValue({ requestId: r.request_id }, { type: 'lead', amount: 1 });
2161
2131
  *
2162
- * // By cadence correlation key — one call attributes a closed deal to the whole sequence:
2132
+ * // By cadence correlation key - one call attributes a closed deal to the whole sequence:
2163
2133
  * await agent.tagReceiptValue({ goalId: 'goal_q2_acme' }, { type: 'revenue', amount: 5000, label: 'Closed deal' });
2164
2134
  * ```
2165
2135
  */
2166
2136
  async tagReceiptValue(ref, valueTag) {
2167
2137
  this.validate(valueTag.type, 'valueTag.type');
2168
- // Cadence-level: route to the outcome ledger by correlation key.
2169
2138
  if (typeof ref === 'object' && ref.goalId && !ref.receiptId && !ref.requestId) {
2170
2139
  const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/outcomes`, {
2171
2140
  method: 'POST',
@@ -2192,7 +2161,7 @@ class OneShot {
2192
2161
  }
2193
2162
  }
2194
2163
  /**
2195
- * Per-cadence RoCS rollup — spend (from receipts) vs. value (from outcomes)
2164
+ * Per-cadence RoCS rollup - spend (from receipts) vs. value (from outcomes)
2196
2165
  * grouped by `decisionContext.goalId`. Answers "what did this cadence cost vs.
2197
2166
  * earn" in one query. `value` counts judge-confirmed outcomes; `pending_value`
2198
2167
  * surfaces self-reported outcomes not yet confirmed.
@@ -2273,15 +2242,8 @@ class OneShot {
2273
2242
  jsonHeaders() {
2274
2243
  return { 'Content-Type': 'application/json', ...this.headers() };
2275
2244
  }
2276
- /**
2277
- * Auth headers plus a signed EIP-712 proof (`x-agent-proof`) for the read
2278
- * routes (inbox, sms inbox, notifications, balance). These identify the caller
2279
- * by wallet, and wallet addresses are public, so without a proof anyone could
2280
- * read another agent's data by supplying its address. The proof binds this
2281
- * request to the wallet the SDK controls; the server verifies the signature
2282
- * locally. A fresh nonce per call prevents replay. Signing failure falls back
2283
- * to plain headers, with a one-time warning.
2284
- */
2245
+ /** Signed, single-use proofs bind requests to the wallet; an address alone cannot authenticate reads.
2246
+ * Signing failure falls back to plain headers with a one-time warning. */
2285
2247
  async signedReadHeaders(scope = 'read', domainName = 'OneShot Agent Auth') {
2286
2248
  const base = this.headers();
2287
2249
  // The bearer token already binds the request to the agent (and is
@@ -2293,12 +2255,7 @@ class OneShot {
2293
2255
  const issuedAt = Math.floor(Date.now() / 1000) + (domainName === 'OneShot Agent Auth' ? this.proofClockOffsetSec : 0);
2294
2256
  const nonce = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
2295
2257
  const signature = await this.provider.signTypedData(
2296
- // `domainName` lets a different origin (e.g. Soul Hunt, which keeps
2297
- // its own nonce store — see hunts.ts) use a DIFFERENT EIP-712 domain
2298
- // than api-service's. EIP-712 mixes the domain into the signed hash,
2299
- // so a proof is only valid for the one origin it was signed for and
2300
- // can't be replayed across service boundaries (security finding
2301
- // F-t_d4805d6f-3, round 2).
2258
+ // Separate EIP-712 domains prevent proof replay across services with independent nonce stores.
2302
2259
  { name: domainName, version: '1' }, {
2303
2260
  AgentReadAuth: [
2304
2261
  { name: 'agent', type: 'address' },
@@ -2312,9 +2269,7 @@ class OneShot {
2312
2269
  return { ...base, 'x-agent-proof': proof };
2313
2270
  }
2314
2271
  catch (err) {
2315
- // Once per client, even without debug: when the API enforces proofs,
2316
- // these requests will be rejected, and a silent fallback would leave
2317
- // the caller with a 401 and no idea why.
2272
+ // Warn once even without debug so a failed proof explains the ensuing authentication failure.
2318
2273
  if (!this.readProofWarned) {
2319
2274
  this.readProofWarned = true;
2320
2275
  this.logger(`[OneShot] Warning: could not sign the x-agent-proof read proof, so requests go without it and will be rejected where the API enforces proofs. Pass a signer that supports signTypedData, or use an access token. Cause: ${err}`);
@@ -2325,14 +2280,15 @@ class OneShot {
2325
2280
  return base;
2326
2281
  }
2327
2282
  }
2328
- /** Retry a clock-window refusal once, before the API has dispatched the request.
2329
- * Only this API's proof clock changes; payments and other services keep their own clocks.
2330
- * Callers supply replayable JSON bodies and retain their original abort/deadline signal.
2331
- */
2283
+ /** Retry a clock-window refusal once using the API's server_time, or a spent nonce on GET/HEAD.
2284
+ * Never retry a spent nonce on a write: the first delivery may already have executed it.
2285
+ * Callers supply replayable JSON bodies and retain their original abort/deadline signal. */
2332
2286
  async fetchApi(url, init) {
2333
- const response = await fetch(url, init);
2334
- const headers = new Headers(init.headers);
2335
- const proof = headers.get('x-agent-proof');
2287
+ const proof = new Headers(init.headers).get('x-agent-proof');
2288
+ // Disable connection reuse where supported to prevent transport retries from replaying a single-use proof.
2289
+ // Browsers control their own pooling; only replayed reads can be safely retried there.
2290
+ const deliver = (headers) => fetch(url, proof ? { ...init, headers: OneShot.singleDelivery(headers), keepalive: false } : init);
2291
+ const response = await deliver(init.headers);
2336
2292
  if (response.status !== 401 || !proof || this._accessToken)
2337
2293
  return response;
2338
2294
  const origin = new URL(this.baseUrl).origin;
@@ -2342,30 +2298,50 @@ class OneShot {
2342
2298
  let serverTime;
2343
2299
  try {
2344
2300
  const body = await response.clone().json();
2345
- if (body?.error !== 'proof_required' ||
2346
- !['proof expired', 'proof issued in the future'].some(reason => body.message === `This endpoint requires a signed agent proof (x-agent-proof): ${reason}`) ||
2347
- typeof body.server_time !== 'number' || !Number.isSafeInteger(body.server_time) || body.server_time <= 0)
2301
+ if (body?.error !== 'proof_required' || typeof body.message !== 'string')
2302
+ return response;
2303
+ const prefix = 'This endpoint requires a signed agent proof (x-agent-proof): ';
2304
+ const reason = body.message.startsWith(prefix) ? body.message.slice(prefix.length) : '';
2305
+ const clockRefusal = reason === 'proof expired' || reason === 'proof issued in the future';
2306
+ const method = (init.method ?? 'GET').toUpperCase();
2307
+ const replayedRead = reason === 'proof nonce already used (replay)' && (method === 'GET' || method === 'HEAD');
2308
+ if (!clockRefusal && !replayedRead)
2348
2309
  return response;
2310
+ if (clockRefusal) {
2311
+ if (typeof body.server_time !== 'number' || !Number.isSafeInteger(body.server_time) || body.server_time <= 0)
2312
+ return response;
2313
+ serverTime = body.server_time;
2314
+ }
2349
2315
  const sent = JSON.parse(typeof Buffer !== 'undefined' ? Buffer.from(proof, 'base64').toString('utf-8') : atob(proof));
2350
2316
  if (typeof sent.scope !== 'string')
2351
2317
  return response;
2352
2318
  scope = sent.scope;
2353
- serverTime = body.server_time;
2354
2319
  }
2355
2320
  catch {
2356
2321
  return response;
2357
2322
  }
2358
2323
  init.signal?.throwIfAborted();
2359
- this.proofClockOffsetSec = serverTime - Math.floor(Date.now() / 1000);
2324
+ if (serverTime !== undefined)
2325
+ this.proofClockOffsetSec = serverTime - Math.floor(Date.now() / 1000);
2360
2326
  const refreshed = await this.signedReadHeaders(scope);
2361
2327
  init.signal?.throwIfAborted();
2362
2328
  if (!refreshed['x-agent-proof'])
2363
2329
  return response;
2364
- headers.set('x-agent-proof', refreshed['x-agent-proof']);
2365
- return fetch(url, { ...init, headers });
2330
+ const retryHeaders = new Headers(init.headers); // Keep the caller's headers intact.
2331
+ retryHeaders.set('x-agent-proof', refreshed['x-agent-proof']);
2332
+ return deliver(retryHeaders);
2333
+ }
2334
+ /** The caller's headers plus `Connection: close`, in the caller's own shape (a plain record stays a record).
2335
+ * Runtimes that treat `Connection` as a forbidden request header (browsers) drop it silently; nothing throws. */
2336
+ static singleDelivery(headers) {
2337
+ if (headers instanceof Headers || Array.isArray(headers)) {
2338
+ const copy = new Headers(headers);
2339
+ copy.set('connection', 'close');
2340
+ return copy;
2341
+ }
2342
+ return { ...Object.fromEntries(Object.entries(headers ?? {}).filter(([name]) => name.toLowerCase() !== 'connection')), Connection: 'close' };
2366
2343
  }
2367
- /** Build a query string, skipping null/undefined values. Pass falsy-but-valid
2368
- * values (0, '') as undefined at the call site to match prior `if (x)` guards. */
2344
+ /** Skip null/undefined query values; callers must explicitly omit other unwanted falsy values. */
2369
2345
  buildQuery(params) {
2370
2346
  const qs = new URLSearchParams();
2371
2347
  for (const [k, v] of Object.entries(params)) {
@@ -2374,20 +2350,8 @@ class OneShot {
2374
2350
  }
2375
2351
  return qs.toString();
2376
2352
  }
2377
- /**
2378
- * Push `config.budgets` to the server once, before the first paid call.
2379
- *
2380
- * Lazy rather than in the constructor because the PUT is signed (async) and
2381
- * the constructor is sync. Idempotent per instance via the cached promise,
2382
- * which is only kept once the sync SUCCEEDS.
2383
- *
2384
- * FAILS CLOSED. If the server can't confirm the budget — network error,
2385
- * 5xx, 429, or a rejected config — this throws BudgetSyncError and the paid
2386
- * call is not made. Proceeding would silently drop the guardrail the
2387
- * developer configured, which is exactly the "empty wallet at 3am" the
2388
- * budget exists to prevent. The marker is cleared so the next paid call
2389
- * retries.
2390
- */
2353
+ /** Sync budgets lazily because signing is async; concurrent calls share the cached promise.
2354
+ * Fail closed until the server confirms the budget, and clear failed syncs so the next call retries. */
2391
2355
  async ensureBudgetsSynced() {
2392
2356
  if (!this._budgets && !this._alertEmail)
2393
2357
  return;
@@ -2427,19 +2391,12 @@ class OneShot {
2427
2391
  await attempt;
2428
2392
  }
2429
2393
  catch (err) {
2430
- // Not synced: clear the marker so the next paid call retries, then
2431
- // refuse this one rather than run it unguarded.
2432
2394
  this._budgetSync = undefined;
2433
2395
  this.log(`${err.message} — paid call refused until the budget is confirmed`);
2434
2396
  throw err;
2435
2397
  }
2436
2398
  }
2437
- /**
2438
- * Local fast-fail on the per-transaction cap, so an oversized call fails
2439
- * without a network round-trip. The server enforces the same cap (and the
2440
- * daily one, which needs the ledger) regardless of which client calls —
2441
- * this is the same SDK-checks/server-enforces split as maxCost.
2442
- */
2399
+ /** Fail oversized calls locally before a round-trip; the server also enforces shared daily spend. */
2443
2400
  assertWithinBudget(total) {
2444
2401
  const cap = this._budgets?.perTransaction;
2445
2402
  if (!cap || cap <= 0)
@@ -2509,7 +2466,7 @@ class OneShot {
2509
2466
  const body = await response.json();
2510
2467
  return body.data;
2511
2468
  }
2512
- /** List this agent's access tokens (prefix, label, last use, spend today — never the token). */
2469
+ /** List this agent's access tokens (prefix, label, last use, spend today - never the token). */
2513
2470
  async listAccessTokens(opts = {}) {
2514
2471
  this.assertWalletSession('list access tokens');
2515
2472
  const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
@@ -2539,41 +2496,10 @@ class OneShot {
2539
2496
  throw new errors_2.OneShotError(`Quote $${total} exceeds maxCost $${maxCost}`);
2540
2497
  }
2541
2498
  }
2542
- /**
2543
- * Header that asks the API to reject the request when the computed quote
2544
- * exceeds the caller-supplied cap (commit 8f328a7). The SDK still does its
2545
- * own local `quote.total > maxCost` check after the 402 returns — this is
2546
- * a server-side enforcement layer so non-SDK callers (MCP server, custom
2547
- * integrations) can't ignore the cap.
2548
- */
2549
- /**
2550
- * The amount to sign, in decimal USDC.
2551
- *
2552
- * A 402 advertises its price twice: the x402 v2 `PAYMENT-REQUIRED` header
2553
- * (`accepts[0].amount`, atomic units) and the legacy JSON body
2554
- * (`payment_request.amount`, decimal). The header is authoritative — it is
2555
- * what the server rebuilds its requirement from and what
2556
- * `findMatchingRequirements` compares the signature against.
2557
- *
2558
- * Preferring the body is how quote-based routes (email/send, sms, voice,
2559
- * build, commerce/buy, compute) silently failed: the body carried a
2560
- * hardcoded "0.00", so the SDK signed a zero-cost authorization against a
2561
- * real charge and the server answered with a bodiless 402. It stayed hidden
2562
- * for as long as credits covered those calls, since the credit path returns
2563
- * 202 without any payment handshake.
2564
- *
2565
- * Falls back to the body when the header is missing or unparseable.
2566
- */
2567
- /**
2568
- * Throw the most specific error a failed response supports.
2569
- *
2570
- * A 402 arriving on the PAID retry means the facilitator refused the
2571
- * signature — not the ordinary "here is your quote" 402. The API names the
2572
- * cause in that body (`payment_verification_failed` + reason + expected vs
2573
- * received amount); surface it as a `PaymentError` so callers can branch on
2574
- * `err.reason` instead of string-matching. Anything else keeps the existing
2575
- * `ToolError` shape.
2576
- */
2499
+ /** Ask the API to enforce the caller's cap before payment, in addition to the local quote check. */
2500
+ /** Resolve decimal USDC from the authoritative PAYMENT-REQUIRED header (atomic units);
2501
+ * fall back to the legacy body only when the header is missing or unparseable. */
2502
+ /** A 402 on the paid retry is a payment rejection, not a quote; expose its cause as PaymentError. */
2577
2503
  async failFromResponse(message, response) {
2578
2504
  const text = await response.text();
2579
2505
  const credits = response.status === 402 ? this.parseInsufficientCredits(text) : undefined;
@@ -2607,14 +2533,7 @@ class OneShot {
2607
2533
  const num = (v) => (typeof v === 'string' || typeof v === 'number') && Number.isFinite(parseFloat(String(v))) ? parseFloat(String(v)) : undefined;
2608
2534
  return new errors_1.InsufficientCreditsError(body.message ?? 'Credit balance does not cover this call', num(body.required_usdc), num(body.credits_balance), num(body.shortfall_usdc));
2609
2535
  }
2610
- /**
2611
- * Access-token sessions never sign. When a paid call answers 402 the SDK
2612
- * either got the server's `insufficient_credits` verdict (throw it) or a
2613
- * quote (re-POST with `x-quote-id` and no payment: the server executes
2614
- * when the quote's credits cover it, otherwise answers `insufficient_credits`
2615
- * with the balance and shortfall, which failFromResponse turns into the
2616
- * typed error). Server-decided on purpose — the SDK holds no ledger.
2617
- */
2536
+ /** Token sessions never sign: retry the quote against credits and let the server ledger decide sufficiency. */
2618
2537
  async retryAsCreditsSession(body, endpoint, payload, quoteId, signal, timeoutMs, extraHeaders) {
2619
2538
  const credits = body ? this.parseInsufficientCredits(body) : undefined;
2620
2539
  if (credits)
@@ -2723,10 +2642,7 @@ class OneShot {
2723
2642
  return bodyAmount ?? '0';
2724
2643
  }
2725
2644
  const fromHeader = ethers_1.ethers.formatUnits(accepted.amount, 6);
2726
- // When the two agree (every fixed-price route), keep the body's string
2727
- // verbatim — downstream consumers such as the ETH auto-swap pass this
2728
- // value along, and there is no reason to reformat it. Only a genuine
2729
- // disagreement flips to the header.
2645
+ // Preserve the body's amount formatting when it agrees with the authoritative header.
2730
2646
  if (bodyAmount != null && parseFloat(bodyAmount) === parseFloat(fromHeader)) {
2731
2647
  return bodyAmount;
2732
2648
  }
@@ -2737,11 +2653,7 @@ class OneShot {
2737
2653
  return undefined;
2738
2654
  return { 'X-Max-Cost-USDC': maxCost.toString() };
2739
2655
  }
2740
- /**
2741
- * Idempotency-Key header, sent on both legs (pre-402 and paid retry) so
2742
- * the server can replay a cached result instead of double-charging and
2743
- * double-executing on a client retry.
2744
- */
2656
+ /** Send the idempotency key on both legs so retries cannot double-charge or repeat the action. */
2745
2657
  idempotencyHeader(idempotencyKey) {
2746
2658
  if (!idempotencyKey)
2747
2659
  return undefined;
@@ -2792,7 +2704,7 @@ class OneShot {
2792
2704
  throw new errors_2.OneShotError('Operation cancelled');
2793
2705
  if (key)
2794
2706
  options.onRequestCreated?.({ idempotencyKey: key });
2795
- return await (0, deadline_1.abortable)(this.executeToolRequestImpl(endpoint, { ...options, idempotencyKey: key, signal: scope.signal }, quoteId, context), scope.signal);
2707
+ return (0, untrusted_content_1.markUntrusted)(await (0, deadline_1.abortable)(this.executeToolRequestImpl(endpoint, { ...options, idempotencyKey: key, signal: scope.signal }, quoteId, context), scope.signal), endpoint);
2796
2708
  }
2797
2709
  catch (error) {
2798
2710
  if (scope.timedOut())
@@ -2815,7 +2727,7 @@ class OneShot {
2815
2727
  Object.assign(extraHeaders, this.approvalHeader(approvalId));
2816
2728
  if (payload.memo !== undefined) {
2817
2729
  if (typeof payload.memo !== 'string' || payload.memo.trim().length === 0) {
2818
- delete payload.memo; // Drop invalid memo silently
2730
+ delete payload.memo; // Invalid optional metadata must not block the call.
2819
2731
  }
2820
2732
  else if (payload.memo.length > 1000) {
2821
2733
  payload.memo = payload.memo.slice(0, 1000);
@@ -2839,8 +2751,7 @@ class OneShot {
2839
2751
  if (signal?.aborted) {
2840
2752
  throw new errors_2.OneShotError('Operation cancelled');
2841
2753
  }
2842
- // One-time push of config.budgets before the first paid call, so the
2843
- // server-side gate knows about them on this very request.
2754
+ // The server must know the configured budget before the first paid request.
2844
2755
  await this.ensureBudgetsSynced();
2845
2756
  if (signal?.aborted)
2846
2757
  throw new errors_2.OneShotError('Operation cancelled');
@@ -2850,7 +2761,6 @@ class OneShot {
2850
2761
  throw new errors_2.OneShotError('Operation cancelled');
2851
2762
  context.phase = 'submission';
2852
2763
  let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
2853
- // Handle 402 Payment Required
2854
2764
  if (response.status === 402 && /\/v1\/tools\/(solve|predict|find\/person|lookup\/person-(profile|dossier))$/.test(endpoint)) {
2855
2765
  ({ execResp: response } = await this.runQuoteToPay({
2856
2766
  endpoint, payload, signal, maxCost, quoteResponse: response, extraHeaders,
@@ -2860,10 +2770,8 @@ class OneShot {
2860
2770
  context.phase = 'submission';
2861
2771
  }
2862
2772
  else if (response.status === 402) {
2863
- // Parse x402 v2 PaymentRequired from PAYMENT-REQUIRED header
2864
2773
  const paymentRequiredHeader = response.headers.get('payment-required');
2865
2774
  const { accepted, resource, extensions } = this.parsePaymentRequired(paymentRequiredHeader);
2866
- // Fallback: parse legacy body format for amount display
2867
2775
  const data = await response.json();
2868
2776
  quoteId ?? (quoteId = data.context?.quote_id);
2869
2777
  if (this._accessToken) {
@@ -2903,7 +2811,6 @@ class OneShot {
2903
2811
  await this.failFromResponse('Tool request failed', response);
2904
2812
  }
2905
2813
  const result = await response.json();
2906
- // Handle async jobs
2907
2814
  if ((result.status === 'pending' || result.status === 'processing') && result.request_id) {
2908
2815
  context.requestId = String(result.request_id);
2909
2816
  context.receiptId = typeof result.receipt_id === 'string' ? result.receipt_id : undefined;
@@ -2913,7 +2820,7 @@ class OneShot {
2913
2820
  if (!wait) {
2914
2821
  return { ...result, idempotencyKey };
2915
2822
  }
2916
- const completed = await this.pollJob(result.request_id, timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined);
2823
+ const completed = await this.pollJob(result.request_id, timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined, endpoint);
2917
2824
  if (completed && typeof completed === 'object' && !Array.isArray(completed)) {
2918
2825
  return { ...completed, request_id: context.requestId, receipt_id: completed.receipt_id ?? context.receiptId, idempotencyKey };
2919
2826
  }
@@ -2921,13 +2828,7 @@ class OneShot {
2921
2828
  }
2922
2829
  return (result.data ?? result);
2923
2830
  }
2924
- /**
2925
- * Shared x402 quote-to-pay flow for paid tools. Fetches the 402 quote, runs
2926
- * any per-tool 400 handling, enforces maxCost, signs the payment auth, and
2927
- * POSTs the paid request. Returns the parsed quote context plus the raw paid
2928
- * Response — the caller owns the post-202 handling (poll a job vs. return the
2929
- * body directly), since that differs per tool.
2930
- */
2831
+ /** Shared quote-to-pay flow; callers own post-202 handling because some tools poll and others return directly. */
2931
2832
  async runQuoteToPay(cfg) {
2932
2833
  await this.ensureBudgetsSynced();
2933
2834
  const approval = { ...cfg.extraHeaders, ...this.approvalHeader(cfg.approvalId) };
@@ -2936,8 +2837,6 @@ class OneShot {
2936
2837
  await cfg.on400(quoteResp);
2937
2838
  }
2938
2839
  if (quoteResp.status !== 402) {
2939
- // Routes the budget gate on the quote leg (fixed-price ones) surface a
2940
- // 403 here; failFromResponse maps it to BudgetExceededError.
2941
2840
  await this.failFromResponse(cfg.expectMsg, quoteResp);
2942
2841
  }
2943
2842
  const quoteData = await quoteResp.json();
@@ -2986,14 +2885,20 @@ class OneShot {
2986
2885
  this.validate(requestId, 'requestId');
2987
2886
  return this.pollJob(requestId, options.timeout, options.signal, options.onStatusUpdate, options.waitForPhones ? { waitForPhones: true, phoneTimeoutSec: options.phoneTimeoutSec } : undefined);
2988
2887
  }
2989
- async pollJob(requestId, timeoutSec, signal, onStatusUpdate, phoneOpts) {
2990
- // HTTP polling is the source of truth; the WebSocket is an accelerator.
2991
- // Both run concurrently from the start and the first terminal outcome wins,
2992
- // so a broken push channel (Redis down, worker unconfigured, proxy that
2993
- // swallows frames) costs nothing beyond the poll cadence. Previously the
2994
- // client waited 60% of its timeout on the socket before polling at all —
2995
- // in prod that was ~72s of dead air on every call while the push never came.
2996
- // An already-aborted signal ends the wait before any proof is signed or sent.
2888
+ unwrapJobResult(job) {
2889
+ const result = job.result ?? job;
2890
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
2891
+ const value = { ...result };
2892
+ for (const field of ['cached', 'fetched_at']) {
2893
+ if (field in job)
2894
+ value[field] = job[field];
2895
+ }
2896
+ return value;
2897
+ }
2898
+ return result;
2899
+ }
2900
+ async pollJob(requestId, timeoutSec, signal, onStatusUpdate, phoneOpts, tool = '') {
2901
+ // Race HTTP with WebSocket push so a broken push channel cannot stall job completion.
2997
2902
  if (signal?.aborted)
2998
2903
  throw new errors_2.OneShotError('Operation cancelled');
2999
2904
  const inner = new AbortController();
@@ -3045,31 +2950,15 @@ class OneShot {
3045
2950
  signal?.removeEventListener('abort', onOuterAbort);
3046
2951
  }
3047
2952
  this.log(`Job ${requestId} ready after ${Date.now() - startedAt}ms via ${wait.via ?? 'unknown'}`);
3048
- // Optional second phase: keep polling for the async phone-reveal webhook.
3049
- // Only kicks in when the caller explicitly opts in AND the result still
3050
- // has phones_pending=true (set by the worker when the upstream enrichment
3051
- // has a pending async phone callback). This is opt-in so existing callers
3052
- // see no behavior change — the WebSocket/HTTP polls return as soon as the
3053
- // worker sets status=completed, with phones=null.
3054
- //
3055
- // Pass the existing result as the initial fallback: if the polling GETs
3056
- // hit a transient error before any successful refresh, we return what we
3057
- // already have (with phones_pending=true still set so consumers know).
2953
+ // Phone reveals are opt-in; retain the completed snapshot if every refresh fails.
3058
2954
  if (phoneOpts?.waitForPhones && this._isPhonesPending(result)) {
3059
2955
  result = await this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
3060
2956
  }
3061
2957
  if (phoneOpts?.waitForPhones)
3062
2958
  notify('completed');
3063
- return result;
2959
+ return (0, untrusted_content_1.markUntrusted)(result, tool || wait.resultTool || '');
3064
2960
  }
3065
- /**
3066
- * Detects whether a job result is still waiting for an async phone reveal.
3067
- * The worker sets `result.phones_pending=true` when the upstream enrichment
3068
- * has a pending async webhook and the webhook URL is configured; the webhook
3069
- * handler flips it to false (or removes it) once phones arrive.
3070
- *
3071
- * Accepts worker search rows, profile objects, dossier sections and legacy results.
3072
- */
2961
+ /** Detect pending phone reveals in search rows, profiles, dossier sections and legacy results. */
3073
2962
  _isPhonesPending(result) {
3074
2963
  if (!result || typeof result !== 'object')
3075
2964
  return false;
@@ -3085,19 +2974,10 @@ class OneShot {
3085
2974
  || (Array.isArray(body.results) && body.results.some(pending));
3086
2975
  });
3087
2976
  }
3088
- /**
3089
- * Slow poll loop that waits for async phone-reveal callbacks to arrive via
3090
- * the webhook handler (which UPDATEs jobs.result_data.result.phones). Polls
3091
- * GET /v1/requests/{id} every 5s — the upstream provider can take several
3092
- * minutes to deliver, so a tight loop just wastes API calls. Returns the
3093
- * latest snapshot whether or not phones arrived (consumer can re-check
3094
- * `phones_pending`).
3095
- */
2977
+ /** Poll phone reveals every 5s to avoid wasting calls during slow callbacks; return the latest snapshot on timeout. */
3096
2978
  async _pollForPhones(requestId, timeoutSec, signal, initialResult) {
3097
2979
  const deadline = Date.now() + timeoutSec * 1000;
3098
2980
  const interval = 5000;
3099
- // Seed with the snapshot the caller already has so a transient first-poll
3100
- // failure can still return the sync data (with phones_pending=true).
3101
2981
  let lastResult = initialResult;
3102
2982
  while (Date.now() < deadline) {
3103
2983
  if (signal?.aborted)
@@ -3108,14 +2988,13 @@ class OneShot {
3108
2988
  signal,
3109
2989
  });
3110
2990
  if (!resp.ok) {
3111
- // Soft-fail on transient errors during the phone wait — return
3112
- // whatever we last had so consumers don't lose the sync result.
2991
+ // Preserve the last successful snapshot across transient phone-wait errors.
3113
2992
  if (lastResult !== undefined)
3114
2993
  return lastResult;
3115
2994
  throw new errors_2.ToolError('Failed to check job status', resp.status, await resp.text(), resp.headers.get('retry-after'));
3116
2995
  }
3117
2996
  const job = await resp.json();
3118
- lastResult = (job.result ?? job);
2997
+ lastResult = this.unwrapJobResult(job);
3119
2998
  if (!this._isPhonesPending(lastResult)) {
3120
2999
  return lastResult;
3121
3000
  }
@@ -3131,38 +3010,17 @@ class OneShot {
3131
3010
  }
3132
3011
  await this.sleep(interval, signal);
3133
3012
  }
3134
- // Timeout — return the last snapshot. phones_pending will still be true
3135
- // so the consumer knows phones never arrived.
3136
3013
  return lastResult;
3137
3014
  }
3138
- /**
3139
- * WebSocket branch of a job wait. Resolves on a `completed` push for this
3140
- * request, rejects with `JobError` on `failed` and with `OneShotError` on
3141
- * abort. Every other failure (no WebSocket global, handshake refused, socket
3142
- * closed early) rejects with a plain `Error`, which `pollJob` treats as
3143
- * "no push available" rather than as an outcome. There is no timeout here:
3144
- * the HTTP branch owns the deadline and aborts this one when it settles.
3145
- */
3015
+ /** WebSocket transport failures leave HTTP polling in charge; job outcomes and cancellation settle the wait.
3016
+ * The HTTP branch owns the deadline and aborts this branch when it settles. */
3146
3017
  async waitViaWebSocket(requestId, signal, emit, wait) {
3147
- // Access-token sessions have no wallet signer to produce a signed proof,
3148
- // and the standard WebSocket constructor (browser and Node globals alike)
3149
- // cannot set a custom `Authorization` header on the handshake request —
3150
- // so there is no way to authenticate this upgrade without putting the
3151
- // reusable bearer token in the URL, which issue #971 round 1 (finding
3152
- // F-t_4d087f2a-5) forbids: a bearer secret must never ride in a URL that
3153
- // proxies, access logs, and telemetry can capture. These sessions skip
3154
- // the WebSocket accelerator entirely and rely on HTTP polling, which
3155
- // authenticates via the Authorization header on every request.
3018
+ // WebSocket cannot carry an Authorization header; keep reusable bearer tokens out of logged URLs.
3019
+ // Token sessions use authenticated HTTP polling instead.
3156
3020
  if (this.isAccessTokenSession) {
3157
3021
  throw new Error('WebSocket push is not available for access-token sessions; falling back to HTTP polling');
3158
3022
  }
3159
- // Build the signed, single-use read proof BEFORE opening the socket: the
3160
- // server authenticates the upgrade itself (not just the post-upgrade
3161
- // connection) the same as agentProofAuth does for every other free-read
3162
- // route (issue #971) — an unsigned ?wallet= alone is no longer
3163
- // sufficient. Unlike a bearer token, a proof is short-lived and single-use
3164
- // (5-minute TTL, Redis-enforced nonce), so passing it as a URL query
3165
- // param is an acceptable channel for it.
3023
+ // Authenticate the upgrade with a short-lived, single-use proof; a wallet address alone proves nothing.
3166
3024
  const authHeaders = await this.signedReadHeaders();
3167
3025
  const authParams = new URLSearchParams();
3168
3026
  if (authHeaders['x-agent-proof'])
@@ -3224,7 +3082,8 @@ class OneShot {
3224
3082
  settle(() => {
3225
3083
  cleanup();
3226
3084
  wait.via = 'ws';
3227
- const result = (msg.result ?? msg);
3085
+ wait.resultTool = typeof msg.result_tool === 'string' ? msg.result_tool : undefined;
3086
+ const result = this.unwrapJobResult(msg);
3228
3087
  if (msg.request_id && typeof result === 'object' && result !== null && !('request_id' in result)) {
3229
3088
  result.request_id = msg.request_id;
3230
3089
  }
@@ -3250,7 +3109,6 @@ class OneShot {
3250
3109
  });
3251
3110
  };
3252
3111
  ws.onclose = () => {
3253
- // Any close before we got a result means no push is coming.
3254
3112
  settle(() => {
3255
3113
  cleanup();
3256
3114
  reject(new Error('WebSocket closed before result'));
@@ -3298,7 +3156,7 @@ class OneShot {
3298
3156
  });
3299
3157
  if (!resp.ok) {
3300
3158
  const body = await resp.text();
3301
- // 5xx / 429 from the poll endpoint are transient — keep polling.
3159
+ // 5xx / 429 from the poll endpoint are transient - keep polling.
3302
3160
  // Any other non-2xx (401/403/404) is a real answer about this job.
3303
3161
  if (resp.status >= 500 || resp.status === 429) {
3304
3162
  throw new Error(`Poll returned ${resp.status}: ${body.slice(0, 200)}`);
@@ -3309,10 +3167,11 @@ class OneShot {
3309
3167
  emit?.(String(job.status));
3310
3168
  if (job.status === 'completed') {
3311
3169
  this.log('Job completed');
3312
- if (wait)
3170
+ if (wait) {
3313
3171
  wait.via = 'http';
3314
- const result = (job.result ?? job);
3315
- // Propagate request_id into the result so callers always have it
3172
+ wait.resultTool = typeof job.result_tool === 'string' ? job.result_tool : undefined;
3173
+ }
3174
+ const result = this.unwrapJobResult(job);
3316
3175
  if (job.request_id && typeof result === 'object' && result !== null && !('request_id' in result)) {
3317
3176
  result.request_id = job.request_id;
3318
3177
  }
@@ -3367,9 +3226,6 @@ class OneShot {
3367
3226
  }
3368
3227
  async makeRequest(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders) {
3369
3228
  let resp = await this.sendLeg(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders);
3370
- // The API refuses a request whose wallet proof it rejected, saying why (#1437).
3371
- // A stale clock window or a proof sent twice is cured by signing a new proof:
3372
- // send the leg again once. Nothing is paid in answer to this refusal.
3373
3229
  for (let attempt = 0; resp.status === 401; attempt++) {
3374
3230
  const refusal = await this.readInvalidProof(resp);
3375
3231
  if (!refusal)
@@ -3398,22 +3254,10 @@ class OneShot {
3398
3254
  }
3399
3255
  }
3400
3256
  async sendLeg(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders) {
3401
- // A leg with no payment attached proves the wallet with a signed agent
3402
- // proof: the API spends an agent's credits, and shows them on a quote, only
3403
- // for a caller that has proven it holds the wallet (a bare X-Agent-ID is a
3404
- // public address). A leg that carries a payment signature needs none. The
3405
- // endpoints with durable admission read a 'submit' proof; a caller that
3406
- // asked for a proof explicitly keeps the scope it always had.
3407
- //
3408
- // One payment leg does need a proof: when credits cover the whole cost the
3409
- // "payment" is the zero-cost authorization, an unsigned stub (signature
3410
- // '0x'). It proves nothing, and the API refuses to spend credits on it
3411
- // alone (401 proof_required).
3257
+ // Credit spending requires wallet proof unless a real payment signature already proves ownership.
3258
+ // Zero-cost authorizations are unsigned stubs, so those still need a proof.
3412
3259
  const unsignedStub = payment?.payload?.signature === '0x';
3413
- // Identity Verification (#2405) answers a retry from the start its key already made, before payment, and
3414
- // only for a caller that proves the wallet: its payment leg carries the agent proof too, however the key was
3415
- // given (an option, or the idempotency_key body field the generated MCP tool sends). The API refuses a
3416
- // payment signature alone there with agent_proof_required. Same list as oneshot-python.
3260
+ // Identity Verification requires wallet proof on the payment leg too, to recover keyed submissions.
3417
3261
  const provenPayment = /(?:^|\/)identity\/start$/.test(endpoint);
3418
3262
  const askedForProof = !!extraHeaders?.['x-agent-proof'];
3419
3263
  const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict)$/.test(endpoint)
@@ -3460,14 +3304,8 @@ class OneShot {
3460
3304
  throw new errors_2.OneShotError('Operation cancelled before payment');
3461
3305
  }
3462
3306
  }
3463
- /**
3464
- * ETH mode only. Make sure the wallet's *effective* USDC (on-chain balance
3465
- * minus payments signed but not yet settled) covers this charge, swapping
3466
- * ETH→USDC for a buffer of `swapBufferMultiplier` payments when it does not,
3467
- * then reserve the charge. Returns the reservation so the caller can release
3468
- * it if the payment never settles (request failed). Runs under a per-instance
3469
- * lock so concurrent calls never double-swap or race the wallet nonce.
3470
- */
3307
+ /** Reserve effective USDC (on-chain minus unsettled payments), swapping a buffer if needed.
3308
+ * Serialize reservations and swaps to avoid overspending and nonce races; release failed payments. */
3471
3309
  async ensureUsdcBalance(paymentInfo) {
3472
3310
  if (this._currency !== 'ETH')
3473
3311
  return undefined;
@@ -3548,18 +3386,10 @@ class OneShot {
3548
3386
  if (reservation)
3549
3387
  this._usdcPending.delete(reservation.id);
3550
3388
  }
3551
- /**
3552
- * How much USDC to buy: top up to `charge × swapBufferMultiplier`, counting
3553
- * whatever effective balance is already there. When the wallet provider
3554
- * exposes `getBalance` and its ETH cannot cover the buffered quote, fall
3555
- * back to the bare shortfall so an ETH-poor wallet can still make the one
3556
- * payment in front of it (a send-only provider always gets the buffer).
3557
- */
3389
+ /** Buy a buffer, or only the shortfall when getBalance shows insufficient ETH for the buffer. */
3558
3390
  async sizeSwap(charge, effective, chainId) {
3559
3391
  const shortfall = charge - effective;
3560
- // Integer math on a 10^6 scale (multiplier ≤ 1000, so ≤ 10^9 — always a
3561
- // finite, exact integer): keeps the multiplier's precision, no float→BigInt
3562
- // on an unbounded value.
3392
+ // Bounded fixed-point math preserves precision without converting an unbounded float to BigInt.
3563
3393
  const mScaled = BigInt(Math.round(this._swapBufferMultiplier * SWAP_MULTIPLIER_SCALE));
3564
3394
  const target = (charge * mScaled + BigInt(SWAP_MULTIPLIER_SCALE) - 1n) / BigInt(SWAP_MULTIPLIER_SCALE);
3565
3395
  let amount = target > effective ? target - effective : shortfall;
@@ -3573,7 +3403,7 @@ class OneShot {
3573
3403
  }
3574
3404
  }
3575
3405
  catch {
3576
- // Quoting failed — let executeSwap quote again and report properly.
3406
+ // Quoting failed - let executeSwap quote again and report properly.
3577
3407
  }
3578
3408
  }
3579
3409
  return amount;
@@ -3584,7 +3414,7 @@ class OneShot {
3584
3414
  const quote = await getSwapQuote(this.rpcProvider, usdcAmount, chainId, this._slippage);
3585
3415
  return quote?.amountInMax;
3586
3416
  }
3587
- /** Perform the on-chain swap. */
3417
+ /** Swap on the configured chain using the wallet provider. */
3588
3418
  async executeUsdcSwap(usdcAmount, chainId) {
3589
3419
  const { executeSwap } = await Promise.resolve().then(() => __importStar(require('./swap')));
3590
3420
  return executeSwap(this.provider, this.rpcProvider, usdcAmount, chainId, this._slippage);
@@ -3617,7 +3447,6 @@ class OneShot {
3617
3447
  ? Buffer.from(header, 'base64').toString()
3618
3448
  : atob(header);
3619
3449
  const parsed = JSON.parse(decoded);
3620
- // x402 v2: { x402Version: 2, accepts: [...], resource: {...}, extensions: {...} }
3621
3450
  if (parsed.accepts?.length > 0) {
3622
3451
  return {
3623
3452
  accepted: parsed.accepts[0],
@@ -3630,7 +3459,6 @@ class OneShot {
3630
3459
  this.log('Failed to parse PAYMENT-REQUIRED header, using defaults');
3631
3460
  }
3632
3461
  }
3633
- // Fallback: construct from known production values
3634
3462
  return {
3635
3463
  accepted: {
3636
3464
  scheme: 'exact',
@@ -3653,14 +3481,13 @@ class OneShot {
3653
3481
  if (header) {
3654
3482
  return this.parsePaymentRequired(header);
3655
3483
  }
3656
- // Probe: send quote ID without payment to get x402 middleware's 402
3657
3484
  // Carries X-Approval-Id: a policy-held call's probe must reach the 402,
3658
3485
  // not be held again.
3659
3486
  const probeResp = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
3660
3487
  return this.parsePaymentRequired(probeResp.headers.get('payment-required'));
3661
3488
  }
3662
3489
  async signPaymentAuthorization(paymentInfo, accepted, resource, extensions) {
3663
- // Credits may cover the full cost — send a zero-cost authorization so the server
3490
+ // Credits may cover the full cost - send a zero-cost authorization so the server
3664
3491
  // always receives a payment-signature header (avoids 402 from x402 SDK).
3665
3492
  if (parseFloat(paymentInfo.amount) === 0) {
3666
3493
  this.log('Credits cover full cost — sending zero-cost authorization');
@@ -3682,12 +3509,9 @@ class OneShot {
3682
3509
  },
3683
3510
  } };
3684
3511
  }
3685
- // Belt and braces: every paid path branches to retryAsCreditsSession
3686
- // before reaching here in access-token mode.
3687
3512
  if (this._accessToken) {
3688
3513
  throw new errors_1.InsufficientCreditsError(`Credits do not cover $${paymentInfo.amount}; top up credits or use a wallet session`, parseFloat(paymentInfo.amount));
3689
3514
  }
3690
- // If paying with ETH, make sure USDC covers the charge (swapping a buffer if not) and reserve it
3691
3515
  const reservation = await this.ensureUsdcBalance(paymentInfo);
3692
3516
  const now = Math.floor(Date.now() / 1000);
3693
3517
  const nonce = ethers_1.ethers.randomBytes(32);
@@ -3695,14 +3519,10 @@ class OneShot {
3695
3519
  const validAfter = now - 300; // Buffer for clock skew
3696
3520
  const validBefore = now + 3600;
3697
3521
  const nonceHex = ethers_1.ethers.hexlify(nonce);
3698
- // Use the EIP-712 domain from the server's payment requirements — name,
3699
- // version AND chain. The chain used to be the mainnet constant, which made
3700
- // every signature invalid against Base Sepolia (the domain separator
3701
- // includes chainId), so the SDK could never pay on staging.
3522
+ // Use the server's full EIP-712 domain, including chainId, so the signature matches its network.
3702
3523
  const domainName = accepted.extra?.name || 'USD Coin';
3703
3524
  const domainVersion = accepted.extra?.version || '2';
3704
3525
  const chainId = chainIdFromNetwork(accepted.network ?? paymentInfo.network) ?? CHAIN_ID;
3705
- // Sign EIP-3009 TransferWithAuthorization
3706
3526
  let signature;
3707
3527
  try {
3708
3528
  signature = await this.provider.signTypedData({
@@ -3732,7 +3552,6 @@ class OneShot {
3732
3552
  this.releaseUsdcReservation(reservation);
3733
3553
  throw err;
3734
3554
  }
3735
- // Return x402 PaymentPayload v2 format (including resource + extensions for Bazaar discovery)
3736
3555
  return { reservation, auth: {
3737
3556
  x402Version: 2,
3738
3557
  ...(resource ? { resource } : {}),