@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/README.md +15 -3
- package/dist/index.d.ts +74 -151
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +171 -352
- package/dist/index.js.map +1 -1
- package/dist/predict-input.generated.d.ts +3 -3
- package/dist/predict-input.generated.d.ts.map +1 -1
- package/dist/recovery.d.ts +13 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +3 -0
- package/dist/recovery.js.map +1 -0
- package/dist/solve-input.generated.d.ts +14 -14
- package/dist/solve-input.generated.d.ts.map +1 -1
- package/dist/types.d.ts +154 -87
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -4
- package/dist/types.js.map +1 -1
- package/dist/untrusted-content.d.ts +8 -0
- package/dist/untrusted-content.d.ts.map +1 -0
- package/dist/untrusted-content.js +26 -0
- package/dist/untrusted-content.js.map +1 -0
- package/dist/untrusted-tools.generated.d.ts +3 -0
- package/dist/untrusted-tools.generated.d.ts.map +1 -0
- package/dist/untrusted-tools.generated.js +309 -0
- package/dist/untrusted-tools.generated.js.map +1 -0
- package/package.json +1 -1
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
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
346
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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`)
|
|
2148
|
-
* - a `goalId` correlation key
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
2329
|
-
*
|
|
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
|
|
2334
|
-
|
|
2335
|
-
|
|
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
|
-
|
|
2347
|
-
|
|
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
|
-
|
|
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
|
|
2365
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
2544
|
-
*
|
|
2545
|
-
|
|
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
|
-
//
|
|
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; //
|
|
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
|
-
//
|
|
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
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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
|
-
//
|
|
3148
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3315
|
-
|
|
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
|
-
//
|
|
3402
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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 } : {}),
|