@oneshot-agent/sdk 0.32.1 → 0.34.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 +53 -9
- package/dist/errors.d.ts +16 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +23 -3
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +73 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -32
- package/dist/index.js.map +1 -1
- package/dist/providers/read-only.d.ts +14 -0
- package/dist/providers/read-only.d.ts.map +1 -0
- package/dist/providers/read-only.js +21 -0
- package/dist/providers/read-only.js.map +1 -0
- package/dist/types.d.ts +99 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,11 +36,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
|
|
39
|
+
exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.ReadOnlyWalletProvider = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
|
|
40
40
|
const physical_mail_1 = require("./physical-mail");
|
|
41
41
|
__exportStar(require("./physical-mail"), exports);
|
|
42
42
|
const deadline_1 = require("./deadline");
|
|
43
43
|
const errors_1 = require("./errors");
|
|
44
|
+
const read_only_1 = require("./providers/read-only");
|
|
44
45
|
const ethers_1 = require("ethers");
|
|
45
46
|
const ethers_2 = require("./providers/ethers");
|
|
46
47
|
const errors_2 = require("./errors");
|
|
@@ -48,12 +49,14 @@ var ethers_3 = require("./providers/ethers");
|
|
|
48
49
|
Object.defineProperty(exports, "EthersWalletProvider", { enumerable: true, get: function () { return ethers_3.EthersWalletProvider; } });
|
|
49
50
|
var cdp_1 = require("./providers/cdp");
|
|
50
51
|
Object.defineProperty(exports, "CdpWalletProvider", { enumerable: true, get: function () { return cdp_1.CdpWalletProvider; } });
|
|
52
|
+
var read_only_2 = require("./providers/read-only");
|
|
53
|
+
Object.defineProperty(exports, "ReadOnlyWalletProvider", { enumerable: true, get: function () { return read_only_2.ReadOnlyWalletProvider; } });
|
|
51
54
|
var swap_1 = require("./swap");
|
|
52
55
|
Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function () { return swap_1.getSwapQuote; } });
|
|
53
56
|
Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
|
|
54
57
|
__exportStar(require("./errors"), exports);
|
|
55
58
|
// Keep in sync with package.json `version`. Guarded by version.test.ts.
|
|
56
|
-
const SDK_VERSION = '0.
|
|
59
|
+
const SDK_VERSION = '0.34.0';
|
|
57
60
|
/** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
|
|
58
61
|
const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
|
|
59
62
|
/** HTTP poll cadence once the WebSocket has delivered for this request. */
|
|
@@ -160,6 +163,14 @@ class OneShot {
|
|
|
160
163
|
* ```
|
|
161
164
|
*/
|
|
162
165
|
static async create(config) {
|
|
166
|
+
if (config.accessToken) {
|
|
167
|
+
if (config.address)
|
|
168
|
+
return new OneShot(config);
|
|
169
|
+
// The API keys job status and the WebSocket on X-Agent-ID, so the
|
|
170
|
+
// session needs its wallet address even though it never signs with it.
|
|
171
|
+
const address = await OneShot.resolveTokenAddress(config);
|
|
172
|
+
return new OneShot({ ...config, address });
|
|
173
|
+
}
|
|
163
174
|
if (config.walletProvider) {
|
|
164
175
|
return new OneShot(config, config.walletProvider);
|
|
165
176
|
}
|
|
@@ -204,8 +215,39 @@ class OneShot {
|
|
|
204
215
|
this._swapBufferMultiplier = validateSwapBufferMultiplier(config.swapBufferMultiplier);
|
|
205
216
|
this._budgets = validateBudgetConfig(config.budgets);
|
|
206
217
|
this._alertEmail = config.alerts?.email;
|
|
218
|
+
this._defaultHeaders = { ...(config.defaultHeaders ?? {}) };
|
|
219
|
+
this._stagingKey = config.stagingKey;
|
|
207
220
|
this.rpcProvider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl ?? RPC_URL);
|
|
208
|
-
if (
|
|
221
|
+
if (config.accessToken) {
|
|
222
|
+
// Option D: access-token session. Exclusive with every wallet option,
|
|
223
|
+
// and budgets are read-only (a delegated credential that could raise its
|
|
224
|
+
// own cap would make the cap meaningless) — set them from a wallet.
|
|
225
|
+
if (!/^oneshot_[0-9a-f]{64}$/.test(config.accessToken)) {
|
|
226
|
+
throw new errors_2.ValidationError('accessToken must be oneshot_<64 lowercase hex characters>', 'accessToken');
|
|
227
|
+
}
|
|
228
|
+
if (config.privateKey || config.cdp || config.walletProvider || walletProvider) {
|
|
229
|
+
throw new errors_2.ValidationError('accessToken cannot be combined with privateKey, cdp, or walletProvider', 'accessToken');
|
|
230
|
+
}
|
|
231
|
+
if (config.budgets || config.alerts) {
|
|
232
|
+
throw new errors_2.ValidationError('budgets and alerts are read-only for access-token sessions; set them from a wallet session or the API', 'budgets');
|
|
233
|
+
}
|
|
234
|
+
if (this._currency === 'ETH') {
|
|
235
|
+
throw new errors_2.ValidationError('ETH currency mode is not available to access-token sessions (they pay from credits)', 'currency');
|
|
236
|
+
}
|
|
237
|
+
if (!config.address) {
|
|
238
|
+
throw new errors_2.ValidationError('address is required with accessToken in the constructor; use OneShot.create() to resolve it', 'address');
|
|
239
|
+
}
|
|
240
|
+
let address;
|
|
241
|
+
try {
|
|
242
|
+
address = ethers_1.ethers.getAddress(config.address);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
throw new errors_2.ValidationError('address must be a valid Ethereum address', 'address');
|
|
246
|
+
}
|
|
247
|
+
this._accessToken = config.accessToken;
|
|
248
|
+
this.provider = new read_only_1.ReadOnlyWalletProvider(address);
|
|
249
|
+
}
|
|
250
|
+
else if (walletProvider) {
|
|
209
251
|
this.provider = walletProvider;
|
|
210
252
|
}
|
|
211
253
|
else if (config.privateKey) {
|
|
@@ -264,12 +306,19 @@ class OneShot {
|
|
|
264
306
|
// from_mailbox, omit from_address entirely so the server picks from
|
|
265
307
|
// the agent's domain pool. The chosen address comes back on the
|
|
266
308
|
// quote response (`quote.from_address`) and we replay it on /send.
|
|
267
|
-
// When the caller pins
|
|
268
|
-
// `${mailbox ?? 'agent'}@${
|
|
309
|
+
// When the caller pins from_domain (with or without from_mailbox), we
|
|
310
|
+
// build `${mailbox ?? 'agent'}@${from_domain}`. Pinning from_mailbox
|
|
311
|
+
// alone has no domain to attach it to — `oneshotagent.com` is OneShot's
|
|
312
|
+
// own domain and no agent can send from it — so that combination is a
|
|
313
|
+
// client-side validation error rather than a guaranteed domain_not_owned
|
|
314
|
+
// rejection from the server.
|
|
315
|
+
if (options.from_mailbox && !options.from_domain) {
|
|
316
|
+
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');
|
|
317
|
+
}
|
|
269
318
|
const useRotation = !options.from_domain && !options.from_mailbox;
|
|
270
319
|
const fromAddress = useRotation
|
|
271
320
|
? undefined
|
|
272
|
-
: `${options.from_mailbox ?? 'agent'}@${options.from_domain
|
|
321
|
+
: `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
|
|
273
322
|
// `mailbox_provisioning_fee` (>0) means `from_address` is a new address that
|
|
274
323
|
// provisions a mailbox on first send — a one-time fee folded into `total_cost`.
|
|
275
324
|
const quote = await this.tool('email/quote', {
|
|
@@ -748,8 +797,8 @@ class OneShot {
|
|
|
748
797
|
if (options.task.length < 10) {
|
|
749
798
|
throw new errors_2.ValidationError('Task must be at least 10 characters', 'task');
|
|
750
799
|
}
|
|
751
|
-
if (options.max_steps !== undefined && (options.max_steps <
|
|
752
|
-
throw new errors_2.ValidationError('max_steps must be between
|
|
800
|
+
if (options.max_steps !== undefined && (!Number.isInteger(options.max_steps) || options.max_steps < 25 || options.max_steps > 100)) {
|
|
801
|
+
throw new errors_2.ValidationError('max_steps must be between 25 and 100', 'max_steps');
|
|
753
802
|
}
|
|
754
803
|
const payload = {
|
|
755
804
|
task: options.task,
|
|
@@ -764,8 +813,8 @@ class OneShot {
|
|
|
764
813
|
payload.session_id = options.session_id;
|
|
765
814
|
if (options.profile_id)
|
|
766
815
|
payload.profile_id = options.profile_id;
|
|
767
|
-
if (options.secrets)
|
|
768
|
-
|
|
816
|
+
if (options.secrets !== undefined)
|
|
817
|
+
throw new errors_2.ValidationError('secrets is unsupported; import cookies or storage_state with createBrowserProfile', 'secrets');
|
|
769
818
|
if (options.max_steps)
|
|
770
819
|
payload.max_steps = options.max_steps;
|
|
771
820
|
const { execResp } = await this.runQuoteToPay({
|
|
@@ -802,18 +851,42 @@ class OneShot {
|
|
|
802
851
|
* console.log(profile.id); // Use this in browser({ profile_id: ... })
|
|
803
852
|
* ```
|
|
804
853
|
*/
|
|
805
|
-
async createBrowserProfile(name) {
|
|
854
|
+
async createBrowserProfile(name, options = {}) {
|
|
806
855
|
this.validate(name, 'name');
|
|
856
|
+
if (options.cookies !== undefined && options.storage_state !== undefined)
|
|
857
|
+
throw new errors_2.ValidationError('Provide cookies or storage_state, not both', 'options');
|
|
807
858
|
const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
|
|
808
859
|
method: 'POST',
|
|
809
860
|
headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
|
|
810
|
-
body: JSON.stringify({ name }),
|
|
861
|
+
body: JSON.stringify({ name, ...options }),
|
|
862
|
+
signal: AbortSignal.timeout(150000),
|
|
811
863
|
});
|
|
812
864
|
if (!response.ok) {
|
|
813
865
|
throw new errors_2.ToolError('Failed to create browser profile', response.status, await response.text());
|
|
814
866
|
}
|
|
815
867
|
return response.json();
|
|
816
868
|
}
|
|
869
|
+
/** Open a profile login session; wait for idle before entering credentials in live_url. */
|
|
870
|
+
async startBrowserProfileSetup(profileId, startUrl) {
|
|
871
|
+
return this.browserProfileSetup(profileId, 'start', { start_url: startUrl });
|
|
872
|
+
}
|
|
873
|
+
async getBrowserProfileSetup(profileId) {
|
|
874
|
+
return this.browserProfileSetup(profileId, 'status');
|
|
875
|
+
}
|
|
876
|
+
/** Call after login and 2FA. Saves state and inspects cookies before site navigation. */
|
|
877
|
+
async finishBrowserProfileSetup(profileId) {
|
|
878
|
+
return this.browserProfileSetup(profileId, 'finish');
|
|
879
|
+
}
|
|
880
|
+
async browserProfileSetup(profileId, action, body = {}) {
|
|
881
|
+
this.validate(profileId, 'profileId');
|
|
882
|
+
const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles/${encodeURIComponent(profileId)}/setup/${action}`, {
|
|
883
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
|
|
884
|
+
body: JSON.stringify(body), signal: AbortSignal.timeout(150000),
|
|
885
|
+
});
|
|
886
|
+
if (!response.ok)
|
|
887
|
+
throw new errors_2.ToolError('Browser profile setup failed', response.status, '');
|
|
888
|
+
return response.json();
|
|
889
|
+
}
|
|
817
890
|
/**
|
|
818
891
|
* List all browser profiles
|
|
819
892
|
*
|
|
@@ -1246,6 +1319,13 @@ class OneShot {
|
|
|
1246
1319
|
const quoteData = await quoteResp.json();
|
|
1247
1320
|
this.log(`Compute fund quote: $${quoteData.payment_request.amount} to top up ${goalId}`);
|
|
1248
1321
|
this.assertWithinBudget(quoteData.payment_request.amount);
|
|
1322
|
+
if (this._accessToken) {
|
|
1323
|
+
const fundResp = await this.retryAsCreditsSession(quoteData, path, payload, quoteData.context.quote_id);
|
|
1324
|
+
if (!fundResp.ok)
|
|
1325
|
+
await this.failFromResponse('Failed to fund compute goal', fundResp);
|
|
1326
|
+
const json = await fundResp.json();
|
|
1327
|
+
return json.data;
|
|
1328
|
+
}
|
|
1249
1329
|
const paymentInfo = {
|
|
1250
1330
|
protocol: 'x402',
|
|
1251
1331
|
network: `eip155:${quoteData.payment_request.chain_id}`,
|
|
@@ -1438,10 +1518,47 @@ class OneShot {
|
|
|
1438
1518
|
}
|
|
1439
1519
|
headers() {
|
|
1440
1520
|
return {
|
|
1521
|
+
...this._defaultHeaders,
|
|
1522
|
+
...(this._stagingKey ? { 'X-Staging-Key': this._stagingKey } : {}),
|
|
1441
1523
|
'X-Agent-ID': this.provider.address,
|
|
1442
|
-
'X-OneShot-SDK-Version': SDK_VERSION
|
|
1524
|
+
'X-OneShot-SDK-Version': SDK_VERSION,
|
|
1525
|
+
...(this._accessToken ? { Authorization: `Bearer ${this._accessToken}` } : {}),
|
|
1443
1526
|
};
|
|
1444
1527
|
}
|
|
1528
|
+
/** Submit a wallet-proven application. Approval is performed by the staging operator. */
|
|
1529
|
+
async applyForStaging(contact, purpose) {
|
|
1530
|
+
return this.stagingOnboarding('POST', '/applications', { contact, purpose });
|
|
1531
|
+
}
|
|
1532
|
+
async getStagingApplication() {
|
|
1533
|
+
return this.stagingOnboarding('GET', '/applications/me');
|
|
1534
|
+
}
|
|
1535
|
+
/** Rotates the wallet's staging key and installs it on this SDK instance. */
|
|
1536
|
+
async obtainStagingKey() {
|
|
1537
|
+
const result = await this.stagingOnboarding('POST', '/credential', {});
|
|
1538
|
+
this._stagingKey = result.staging_key;
|
|
1539
|
+
return result.staging_key;
|
|
1540
|
+
}
|
|
1541
|
+
async stagingOnboarding(method, path, body) {
|
|
1542
|
+
if (this._accessToken)
|
|
1543
|
+
throw new Error('Staging onboarding requires a wallet signer');
|
|
1544
|
+
const digest = ethers_1.ethers.sha256(ethers_1.ethers.toUtf8Bytes(JSON.stringify(body ?? {}))).slice(2);
|
|
1545
|
+
const headers = await this.signedReadHeaders(`staging:${method}:${path}:${digest}`);
|
|
1546
|
+
if (!headers['x-agent-proof'])
|
|
1547
|
+
throw new Error('Could not sign staging application');
|
|
1548
|
+
const response = await fetch(`${this.baseUrl}/v1/staging${path}`, {
|
|
1549
|
+
method, headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1550
|
+
signal: AbortSignal.timeout(10000),
|
|
1551
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
1552
|
+
});
|
|
1553
|
+
const result = await response.json();
|
|
1554
|
+
if (!response.ok)
|
|
1555
|
+
throw new Error(result.error ?? `Staging request failed (${response.status})`);
|
|
1556
|
+
return result;
|
|
1557
|
+
}
|
|
1558
|
+
/** True for access-token sessions: credits-only, never signs. */
|
|
1559
|
+
get isAccessTokenSession() {
|
|
1560
|
+
return this._accessToken !== undefined;
|
|
1561
|
+
}
|
|
1445
1562
|
/** Auth headers plus Content-Type for JSON-body requests. */
|
|
1446
1563
|
jsonHeaders() {
|
|
1447
1564
|
return { 'Content-Type': 'application/json', ...this.headers() };
|
|
@@ -1457,6 +1574,10 @@ class OneShot {
|
|
|
1457
1574
|
*/
|
|
1458
1575
|
async signedReadHeaders(scope = 'read') {
|
|
1459
1576
|
const base = this.headers();
|
|
1577
|
+
// The bearer token already binds the request to the agent (and is
|
|
1578
|
+
// revocable); the API accepts it in place of the signed proof.
|
|
1579
|
+
if (this._accessToken)
|
|
1580
|
+
return base;
|
|
1460
1581
|
try {
|
|
1461
1582
|
const agent = this.provider.address;
|
|
1462
1583
|
const issuedAt = Math.floor(Date.now() / 1000);
|
|
@@ -1582,6 +1703,61 @@ class OneShot {
|
|
|
1582
1703
|
const body = await response.json();
|
|
1583
1704
|
return (body.data ?? body);
|
|
1584
1705
|
}
|
|
1706
|
+
// ---------------------------------------------------------------------------
|
|
1707
|
+
// Access tokens (wallet sessions only)
|
|
1708
|
+
// ---------------------------------------------------------------------------
|
|
1709
|
+
assertWalletSession(action) {
|
|
1710
|
+
if (this._accessToken) {
|
|
1711
|
+
throw new errors_2.ValidationError(`Access tokens cannot ${action}; use a wallet session`, 'accessToken');
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Mint an access token for this agent: a revocable, credits-only credential
|
|
1716
|
+
* for code that runs where the wallet key cannot be (hosted MCP clients,
|
|
1717
|
+
* cloud runners). The plaintext is returned once and never stored.
|
|
1718
|
+
*
|
|
1719
|
+
* @example
|
|
1720
|
+
* ```typescript
|
|
1721
|
+
* const { token } = await agent.createAccessToken({ name: 'grok-bot' });
|
|
1722
|
+
* // hand `token` to the hosted client; it uses OneShot.create({ accessToken: token })
|
|
1723
|
+
* ```
|
|
1724
|
+
*/
|
|
1725
|
+
async createAccessToken(opts = {}) {
|
|
1726
|
+
this.assertWalletSession('mint access tokens');
|
|
1727
|
+
const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens`, {
|
|
1728
|
+
method: 'POST',
|
|
1729
|
+
headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
|
|
1730
|
+
body: JSON.stringify(opts.name !== undefined ? { name: opts.name } : {}),
|
|
1731
|
+
});
|
|
1732
|
+
if (!response.ok)
|
|
1733
|
+
throw new errors_2.ToolError('Failed to create access token', response.status, await response.text());
|
|
1734
|
+
const body = await response.json();
|
|
1735
|
+
return body.data;
|
|
1736
|
+
}
|
|
1737
|
+
/** List this agent's access tokens (prefix, label, last use, spend today — never the token). */
|
|
1738
|
+
async listAccessTokens(opts = {}) {
|
|
1739
|
+
this.assertWalletSession('list access tokens');
|
|
1740
|
+
const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
|
|
1741
|
+
headers: await this.signedReadHeaders(),
|
|
1742
|
+
});
|
|
1743
|
+
if (!response.ok)
|
|
1744
|
+
throw new errors_2.ToolError('Failed to list access tokens', response.status, await response.text());
|
|
1745
|
+
const body = await response.json();
|
|
1746
|
+
return body.data.tokens;
|
|
1747
|
+
}
|
|
1748
|
+
/** Revoke an access token. Sessions using it fail with 401 from the next request. */
|
|
1749
|
+
async revokeAccessToken(id) {
|
|
1750
|
+
this.assertWalletSession('revoke access tokens');
|
|
1751
|
+
this.validate(id, 'id');
|
|
1752
|
+
const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens/${encodeURIComponent(id)}`, {
|
|
1753
|
+
method: 'DELETE',
|
|
1754
|
+
headers: await this.signedReadHeaders('write'),
|
|
1755
|
+
});
|
|
1756
|
+
if (!response.ok)
|
|
1757
|
+
throw new errors_2.ToolError('Failed to revoke access token', response.status, await response.text());
|
|
1758
|
+
const body = await response.json();
|
|
1759
|
+
return body.data;
|
|
1760
|
+
}
|
|
1585
1761
|
/** Local fast-fail guard: throw when a quote total exceeds the caller's cap. */
|
|
1586
1762
|
assertWithinMaxCost(total, maxCost) {
|
|
1587
1763
|
if (maxCost && parseFloat(total) > maxCost) {
|
|
@@ -1625,6 +1801,9 @@ class OneShot {
|
|
|
1625
1801
|
*/
|
|
1626
1802
|
async failFromResponse(message, response) {
|
|
1627
1803
|
const text = await response.text();
|
|
1804
|
+
const credits = response.status === 402 ? this.parseInsufficientCredits(text) : undefined;
|
|
1805
|
+
if (credits)
|
|
1806
|
+
throw credits;
|
|
1628
1807
|
const rejection = response.status === 402 ? this.parsePaymentRejection(text) : undefined;
|
|
1629
1808
|
if (rejection)
|
|
1630
1809
|
throw rejection;
|
|
@@ -1633,6 +1812,76 @@ class OneShot {
|
|
|
1633
1812
|
throw budget;
|
|
1634
1813
|
throw new errors_2.ToolError(message, response.status, text);
|
|
1635
1814
|
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Map a 402 `insufficient_credits` body (access-token sessions) onto a typed
|
|
1817
|
+
* error. Any other 402 falls through to the payment-rejection / quote parse.
|
|
1818
|
+
*/
|
|
1819
|
+
parseInsufficientCredits(text) {
|
|
1820
|
+
let body;
|
|
1821
|
+
try {
|
|
1822
|
+
body = typeof text === 'string' ? JSON.parse(text) : text;
|
|
1823
|
+
}
|
|
1824
|
+
catch {
|
|
1825
|
+
return undefined;
|
|
1826
|
+
}
|
|
1827
|
+
if (!body || body.error !== 'insufficient_credits')
|
|
1828
|
+
return undefined;
|
|
1829
|
+
const num = (v) => (typeof v === 'string' || typeof v === 'number') && Number.isFinite(parseFloat(String(v))) ? parseFloat(String(v)) : undefined;
|
|
1830
|
+
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));
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Access-token sessions never sign. When a paid call answers 402 the SDK
|
|
1834
|
+
* either got the server's `insufficient_credits` verdict (throw it) or a
|
|
1835
|
+
* quote (re-POST with `x-quote-id` and no payment: the server executes
|
|
1836
|
+
* when the quote's credits cover it, otherwise answers `insufficient_credits`
|
|
1837
|
+
* with the balance and shortfall, which failFromResponse turns into the
|
|
1838
|
+
* typed error). Server-decided on purpose — the SDK holds no ledger.
|
|
1839
|
+
*/
|
|
1840
|
+
async retryAsCreditsSession(body, endpoint, payload, quoteId, signal, timeoutMs, extraHeaders) {
|
|
1841
|
+
const credits = body ? this.parseInsufficientCredits(body) : undefined;
|
|
1842
|
+
if (credits)
|
|
1843
|
+
throw credits;
|
|
1844
|
+
const qid = quoteId ?? body?.context?.quote_id;
|
|
1845
|
+
if (!qid) {
|
|
1846
|
+
const amount = body?.payment_request?.amount;
|
|
1847
|
+
throw new errors_1.InsufficientCreditsError(`Credits do not cover ${amount ? `$${amount}` : 'this call'}; top up credits or use a wallet session`, amount ? parseFloat(amount) : undefined);
|
|
1848
|
+
}
|
|
1849
|
+
this.log(`Access-token session: retrying quote ${qid} against credits`);
|
|
1850
|
+
const retry = await this.makeRequest(endpoint, payload, undefined, qid, signal, timeoutMs, extraHeaders);
|
|
1851
|
+
if (retry.status === 402)
|
|
1852
|
+
await this.failFromResponse('Credits do not cover this call', retry);
|
|
1853
|
+
return retry;
|
|
1854
|
+
}
|
|
1855
|
+
/** GET /v1/agents/me with the token, for OneShot.create({ accessToken }) without an address. */
|
|
1856
|
+
static async resolveTokenAddress(config) {
|
|
1857
|
+
const TOKEN_RESOLVE_TIMEOUT_MS = 10000;
|
|
1858
|
+
const baseUrl = config.baseUrl ?? BASE_URL;
|
|
1859
|
+
let response;
|
|
1860
|
+
try {
|
|
1861
|
+
response = await fetch(`${baseUrl}/v1/agents/me`, {
|
|
1862
|
+
headers: {
|
|
1863
|
+
...(config.defaultHeaders ?? {}),
|
|
1864
|
+
...(config.stagingKey ? { 'X-Staging-Key': config.stagingKey } : {}),
|
|
1865
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
1866
|
+
'X-OneShot-SDK-Version': SDK_VERSION,
|
|
1867
|
+
},
|
|
1868
|
+
// Bounded: with no signal, Node's fetch can wait minutes on a hung
|
|
1869
|
+
// connection and OneShot.create() would hang with it.
|
|
1870
|
+
signal: AbortSignal.timeout(TOKEN_RESOLVE_TIMEOUT_MS),
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
catch (err) {
|
|
1874
|
+
throw new errors_2.ToolError(`Could not resolve the access token's agent (network): ${err}`, 0, String(err));
|
|
1875
|
+
}
|
|
1876
|
+
if (!response.ok) {
|
|
1877
|
+
throw new errors_2.ToolError('Could not resolve the access token\'s agent', response.status, await response.text());
|
|
1878
|
+
}
|
|
1879
|
+
const json = await response.json();
|
|
1880
|
+
const address = json.data?.wallet_address;
|
|
1881
|
+
if (!address)
|
|
1882
|
+
throw new errors_2.ToolError('GET /v1/agents/me returned no wallet_address', response.status, JSON.stringify(json));
|
|
1883
|
+
return address;
|
|
1884
|
+
}
|
|
1636
1885
|
/**
|
|
1637
1886
|
* Map a 403 `budget_exceeded` body onto a typed error, so callers can catch
|
|
1638
1887
|
* "my own budget stopped this" separately from an auth failure or a payment
|
|
@@ -1804,23 +2053,30 @@ class OneShot {
|
|
|
1804
2053
|
const { accepted, resource, extensions } = this.parsePaymentRequired(paymentRequiredHeader);
|
|
1805
2054
|
// Fallback: parse legacy body format for amount display
|
|
1806
2055
|
const data = await response.json();
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2056
|
+
if (this._accessToken) {
|
|
2057
|
+
// Credits-only session: never sign. Let the server settle it.
|
|
2058
|
+
this.checkAbortBeforePayment(signal);
|
|
2059
|
+
response = await this.retryAsCreditsSession(data, endpoint, payload, quoteId, signal, undefined, extraHeaders);
|
|
2060
|
+
}
|
|
2061
|
+
else {
|
|
2062
|
+
const paymentInfo = {
|
|
2063
|
+
protocol: 'x402',
|
|
2064
|
+
network: accepted.network,
|
|
2065
|
+
payTo: accepted.payTo,
|
|
2066
|
+
amount: this.chargeAmount(accepted, data.payment_request?.amount),
|
|
2067
|
+
currency: 'USD',
|
|
2068
|
+
facilitator_url: this.baseUrl,
|
|
2069
|
+
token: { address: accepted.asset, symbol: 'USDC', decimals: 6 }
|
|
2070
|
+
};
|
|
2071
|
+
this.log(`Payment required: ${paymentInfo.amount} USDC`);
|
|
2072
|
+
this.assertWithinBudget(paymentInfo.amount);
|
|
2073
|
+
this.checkAbortBeforePayment(signal);
|
|
2074
|
+
context.phase = 'payment';
|
|
2075
|
+
const signed = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
|
|
2076
|
+
this.checkAbortBeforePayment(signal);
|
|
2077
|
+
context.phase = 'submission';
|
|
2078
|
+
response = await this.makePaidRequest(signed, endpoint, payload, quoteId, signal, undefined, extraHeaders);
|
|
2079
|
+
}
|
|
1824
2080
|
}
|
|
1825
2081
|
if (!response.ok) {
|
|
1826
2082
|
await this.failFromResponse('Tool request failed', response);
|
|
@@ -1866,6 +2122,11 @@ class OneShot {
|
|
|
1866
2122
|
cfg.onQuote(quoteData.context);
|
|
1867
2123
|
this.assertWithinMaxCost(cfg.totalOf(quoteData.context), cfg.maxCost);
|
|
1868
2124
|
this.assertWithinBudget(cfg.totalOf(quoteData.context));
|
|
2125
|
+
if (this._accessToken) {
|
|
2126
|
+
this.checkAbortBeforePayment(cfg.signal);
|
|
2127
|
+
const execResp = await this.retryAsCreditsSession(quoteData, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs);
|
|
2128
|
+
return { context: quoteData.context, execResp };
|
|
2129
|
+
}
|
|
1869
2130
|
const paymentInfo = {
|
|
1870
2131
|
protocol: 'x402',
|
|
1871
2132
|
network: `eip155:${quoteData.payment_request.chain_id}`,
|
|
@@ -2096,7 +2357,7 @@ class OneShot {
|
|
|
2096
2357
|
settle(() => {
|
|
2097
2358
|
cleanup();
|
|
2098
2359
|
wait.via = 'ws';
|
|
2099
|
-
reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code));
|
|
2360
|
+
reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code, msg.result));
|
|
2100
2361
|
});
|
|
2101
2362
|
}
|
|
2102
2363
|
else {
|
|
@@ -2187,7 +2448,7 @@ class OneShot {
|
|
|
2187
2448
|
if (job.status === 'failed') {
|
|
2188
2449
|
if (wait)
|
|
2189
2450
|
wait.via = 'http';
|
|
2190
|
-
throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code);
|
|
2451
|
+
throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code, job.result);
|
|
2191
2452
|
}
|
|
2192
2453
|
emit?.(String(job.status));
|
|
2193
2454
|
retries = 0;
|
|
@@ -2487,6 +2748,11 @@ class OneShot {
|
|
|
2487
2748
|
},
|
|
2488
2749
|
} };
|
|
2489
2750
|
}
|
|
2751
|
+
// Belt and braces: every paid path branches to retryAsCreditsSession
|
|
2752
|
+
// before reaching here in access-token mode.
|
|
2753
|
+
if (this._accessToken) {
|
|
2754
|
+
throw new errors_1.InsufficientCreditsError(`Credits do not cover $${paymentInfo.amount}; top up credits or use a wallet session`, parseFloat(paymentInfo.amount));
|
|
2755
|
+
}
|
|
2490
2756
|
// If paying with ETH, make sure USDC covers the charge (swapping a buffer if not) and reserve it
|
|
2491
2757
|
const reservation = await this.ensureUsdcBalance(paymentInfo);
|
|
2492
2758
|
const now = Math.floor(Date.now() / 1000);
|