@oneshot-agent/sdk 0.62.0 → 0.64.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 +5 -8
- package/dist/approvals.d.ts +1 -1
- package/dist/approvals.d.ts.map +1 -1
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +46 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +197 -53
- package/dist/index.js.map +1 -1
- package/dist/predict-input.generated.d.ts +11 -0
- package/dist/predict-input.generated.d.ts.map +1 -1
- package/dist/predict.d.ts +40 -1
- package/dist/predict.d.ts.map +1 -1
- package/dist/recovery.d.ts +2 -2
- package/dist/recovery.d.ts.map +1 -1
- package/dist/types.d.ts +234 -67
- package/dist/types.d.ts.map +1 -1
- package/dist/untrusted-tools.generated.d.ts.map +1 -1
- package/dist/untrusted-tools.generated.js +67 -0
- package/dist/untrusted-tools.generated.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -65,7 +65,7 @@ __exportStar(require("./errors"), exports);
|
|
|
65
65
|
const approvals_1 = require("./approvals");
|
|
66
66
|
// Keep receipt on its subpath: node:crypto breaks browser bundles, and providerCost is not public.
|
|
67
67
|
// Keep in sync with package.json `version`. Guarded by version.test.ts.
|
|
68
|
-
const SDK_VERSION = '0.
|
|
68
|
+
const SDK_VERSION = '0.64.0';
|
|
69
69
|
/** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
|
|
70
70
|
const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
|
|
71
71
|
/** HTTP poll cadence once the WebSocket has delivered for this request. */
|
|
@@ -320,6 +320,39 @@ class OneShot {
|
|
|
320
320
|
async tool(toolName, options) {
|
|
321
321
|
return this.executeToolRequest(`/v1/tools/${toolName}`, options);
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Price account setup without a recipient or message; this does not buy or send. Choose the mode
|
|
325
|
+
* (`relay` by default, or `mailbox`) and the addresses (default `['agent']`). On a verified domain
|
|
326
|
+
* the agent already has, this prices only the named addresses it does not have yet.
|
|
327
|
+
*/
|
|
328
|
+
async quoteDomain(domain, options = {}) {
|
|
329
|
+
this.validate(domain, 'domain');
|
|
330
|
+
const { mailbox_mode, addresses, ...rest } = options;
|
|
331
|
+
if (mailbox_mode !== undefined && mailbox_mode !== 'relay' && mailbox_mode !== 'mailbox') {
|
|
332
|
+
throw new errors_2.ValidationError("mailbox_mode must be 'relay' or 'mailbox'", 'mailbox_mode');
|
|
333
|
+
}
|
|
334
|
+
if (addresses !== undefined && (!Array.isArray(addresses) || addresses.length < 1 || addresses.length > 10
|
|
335
|
+
|| addresses.some((a) => typeof a !== 'string' || !a.trim()))) {
|
|
336
|
+
throw new errors_2.ValidationError('addresses must name 1 to 10 local parts, such as ["sales"]', 'addresses');
|
|
337
|
+
}
|
|
338
|
+
const quote = await this.executeToolRequest('/v1/domains/quote', {
|
|
339
|
+
...rest, domain,
|
|
340
|
+
...(mailbox_mode ? { mailbox_mode } : {}),
|
|
341
|
+
...(addresses ? { addresses: addresses.map((a) => a.trim().toLowerCase()) } : {}),
|
|
342
|
+
});
|
|
343
|
+
if (quote.total_cost !== undefined)
|
|
344
|
+
this.assertWithinMaxCost(String(quote.total_cost), options.maxCost);
|
|
345
|
+
return quote;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Owner/API purchase of the exact setup quote. The mode and addresses are the quote's; the result
|
|
349
|
+
* names them. Registration, adding addresses and email are separate actions.
|
|
350
|
+
*/
|
|
351
|
+
async buyDomain(domain, quoteId, options = {}) {
|
|
352
|
+
this.validate(domain, 'domain');
|
|
353
|
+
this.validate(quoteId?.trim(), 'quoteId');
|
|
354
|
+
return this.executeToolRequest('/v1/domains/buy', { ...options, domain }, quoteId.trim());
|
|
355
|
+
}
|
|
323
356
|
async email(options) {
|
|
324
357
|
// to/subject are server-derived when replying to an inbound email.
|
|
325
358
|
if (!options.reply_to_email_id) {
|
|
@@ -332,59 +365,117 @@ class OneShot {
|
|
|
332
365
|
if (options.from_mailbox && !options.from_domain) {
|
|
333
366
|
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');
|
|
334
367
|
}
|
|
335
|
-
const useRotation = !options.from_domain && !options.from_mailbox;
|
|
336
|
-
const fromAddress = useRotation
|
|
337
|
-
? undefined
|
|
338
|
-
: `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
|
|
339
368
|
// Reuse the confirmed quote so the charge matches the amount shown; true alone confirms no amount.
|
|
340
369
|
const confirmedQuote = typeof options.confirmNewDomain === 'string' && options.confirmNewDomain.trim()
|
|
341
370
|
? options.confirmNewDomain.trim() : undefined;
|
|
342
371
|
if (confirmedQuote) {
|
|
343
|
-
this.
|
|
344
|
-
|
|
372
|
+
this.validate(options.from_domain, 'from_domain');
|
|
373
|
+
const setup = await this.buyDomain(options.from_domain, confirmedQuote, {
|
|
374
|
+
maxCost: options.maxCost, signal: options.signal, wait: false,
|
|
375
|
+
});
|
|
376
|
+
return { status: 'setup_pending', setup, request_id: setup.request_id, receipt_id: setup.receipt_id,
|
|
377
|
+
message: 'Setup purchased; no email was sent. Wait for it to finish, then call email without confirmNewDomain.' };
|
|
378
|
+
}
|
|
379
|
+
let fromAddress;
|
|
380
|
+
if (options.from_domain) {
|
|
381
|
+
const domainName = options.from_domain.trim().toLowerCase().replace(/\.$/, '');
|
|
382
|
+
// An unavailable ownership read leaves the email quote to decide whether setup is needed.
|
|
383
|
+
const owned = await this.listDomains().catch(() => undefined);
|
|
384
|
+
const entry = owned?.domains.find(d => d.domain.toLowerCase() === domainName);
|
|
385
|
+
// A new name reaches the setup quote without an email quote.
|
|
386
|
+
if (owned && !entry)
|
|
387
|
+
return this.emailDomainSetup(domainName, options);
|
|
388
|
+
fromAddress = options.from_mailbox
|
|
389
|
+
? `${options.from_mailbox}@${options.from_domain}`
|
|
390
|
+
: entry?.default_from ?? `agent@${options.from_domain}`;
|
|
391
|
+
}
|
|
392
|
+
let quote;
|
|
393
|
+
try {
|
|
394
|
+
quote = await this.tool('email/quote', {
|
|
395
|
+
...(fromAddress ? { from_address: fromAddress } : {}),
|
|
396
|
+
...(options.to !== undefined ? { to_address: options.to } : {}),
|
|
397
|
+
...(options.subject !== undefined ? { subject: options.subject } : {}),
|
|
398
|
+
...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
|
|
399
|
+
body: options.body,
|
|
400
|
+
maxCost: options.maxCost,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
const setup = await this.emailSetupFromRefusal(error, options);
|
|
405
|
+
if (setup)
|
|
406
|
+
return setup;
|
|
407
|
+
throw error;
|
|
345
408
|
}
|
|
346
|
-
// Mailbox provisioning and domain registration are distinct fees; only a new domain needs confirmation.
|
|
347
|
-
const quote = await this.tool('email/quote', {
|
|
348
|
-
...(fromAddress ? { from_address: fromAddress } : {}),
|
|
349
|
-
...(options.to !== undefined ? { to_address: options.to } : {}),
|
|
350
|
-
...(options.subject !== undefined ? { subject: options.subject } : {}),
|
|
351
|
-
...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
|
|
352
|
-
...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
|
|
353
|
-
body: options.body,
|
|
354
|
-
maxCost: options.maxCost,
|
|
355
|
-
});
|
|
356
409
|
this.log(`Email quote: $${quote.total_cost}`);
|
|
357
410
|
// Enforce the cap locally even if the server did not check the header.
|
|
358
411
|
this.assertWithinMaxCost(quote.total_cost, options.maxCost);
|
|
359
|
-
// A new domain must be explicitly authorized against its exact quote before any charge or send.
|
|
360
|
-
if (quote.is_new) {
|
|
361
|
-
const domainName = quote.domain ?? options.from_domain ?? '';
|
|
362
|
-
const breakdown = quote.registration_fee !== undefined && quote.service_fee !== undefined
|
|
363
|
-
? ` (registration $${quote.registration_fee} + service fee $${quote.service_fee}` +
|
|
364
|
-
`${quote.mailbox_provisioning_fee ? ` + mailbox fee $${quote.mailbox_provisioning_fee}` : ''})`
|
|
365
|
-
: '';
|
|
366
|
-
return {
|
|
367
|
-
status: 'confirmation_required',
|
|
368
|
-
cost: parseFloat(quote.total_cost),
|
|
369
|
-
quote_id: quote.quote_id,
|
|
370
|
-
domain: { domain: domainName, status: 'quote_only', was_provisioned: false },
|
|
371
|
-
message: `Registering ${domainName} costs $${quote.total_cost} total${breakdown}. ` +
|
|
372
|
-
`Nothing has been charged yet. Show the user this exact amount; once they agree, call this again with the same ` +
|
|
373
|
-
`arguments and confirm_new_domain: '${quote.quote_id}' (SDK: confirmNewDomain) to register the domain, ` +
|
|
374
|
-
`charge exactly $${quote.total_cost}, and send this email. true only re-quotes; a wrong or expired quote id is refused without charge.`,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
412
|
return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress ?? quote.from_address), quote.quote_id);
|
|
378
413
|
}
|
|
379
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* A send never sets up a domain or an address: the email quote refuses with `no_sending_domain` or
|
|
416
|
+
* `address_not_set_up` and names the setup. Route that to the setup quote, so the caller reviews its
|
|
417
|
+
* price, rather than surfacing a bare error. Any other error is the caller's.
|
|
418
|
+
*/
|
|
419
|
+
async emailSetupFromRefusal(error, options) {
|
|
420
|
+
if (!(error instanceof errors_2.ToolError) || error.statusCode !== 400)
|
|
421
|
+
return undefined;
|
|
422
|
+
let body;
|
|
423
|
+
try {
|
|
424
|
+
body = JSON.parse(error.responseBody);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
const requirements = Array.isArray(body.requirements) ? body.requirements : undefined;
|
|
430
|
+
if (body.error === 'address_not_set_up' && body.domain && body.from_address) {
|
|
431
|
+
const local = body.from_address.split('@')[0];
|
|
432
|
+
try {
|
|
433
|
+
return await this.emailDomainSetup(body.domain, options, [local]);
|
|
434
|
+
}
|
|
435
|
+
catch (quoteError) {
|
|
436
|
+
// The address is already being added by an earlier purchase.
|
|
437
|
+
if (quoteError instanceof errors_2.ToolError && quoteError.statusCode === 409 && /addresses_already_set_up/.test(quoteError.responseBody)) {
|
|
438
|
+
return { status: 'setup_pending', error: 'address_not_set_up', requirements,
|
|
439
|
+
message: `${body.from_address} is being set up; no email was sent. Send again once it is active in listDomains().` };
|
|
440
|
+
}
|
|
441
|
+
throw quoteError;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (body.error !== 'no_sending_domain')
|
|
445
|
+
return undefined;
|
|
446
|
+
const domain = body.domain ?? options.from_domain?.trim().toLowerCase();
|
|
447
|
+
if (body.reason === 'not_owned' && domain)
|
|
448
|
+
return this.emailDomainSetup(domain, options);
|
|
449
|
+
const pending = body.reason === 'not_verified';
|
|
450
|
+
return {
|
|
451
|
+
status: pending ? 'setup_pending' : 'setup_required', error: 'no_sending_domain', requirements,
|
|
452
|
+
message: body.message ?? 'This agent has no verified sending domain. Buy one with quoteDomain and buyDomain; no email was sent.',
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async emailDomainSetup(domainName, options, addresses) {
|
|
456
|
+
const named = addresses ?? (options.from_mailbox ? [options.from_mailbox] : undefined);
|
|
457
|
+
const setup = await this.quoteDomain(domainName, { maxCost: options.maxCost, signal: options.signal, ...(named ? { addresses: named } : {}) });
|
|
458
|
+
const addingAddresses = setup.setup === 'domain_addresses';
|
|
459
|
+
return {
|
|
460
|
+
status: setup.purchasable ? 'confirmation_required' : 'setup_unavailable',
|
|
461
|
+
cost: setup.total_cost,
|
|
462
|
+
quote_id: setup.quote_id,
|
|
463
|
+
setup,
|
|
464
|
+
domain: { domain: domainName, status: 'quote_only', was_provisioned: false },
|
|
465
|
+
message: !setup.purchasable
|
|
466
|
+
? 'This name cannot be purchased. Choose another name for account setup.'
|
|
467
|
+
: addingAddresses
|
|
468
|
+
? 'This address is not on the domain yet. No purchase or email send has occurred. The owner or API caller must review setup.total_cost, then buy this exact quote at POST /v1/domains/buy (SDK: confirmNewDomain). Send separately once the address is active.'
|
|
469
|
+
: 'No setup purchase or email send has occurred. The owner or API caller must review setup.total_cost and setup.billing, then buy this exact quote at POST /v1/domains/buy (SDK: confirmNewDomain). No email is included; send separately after verification.',
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
/** Keep setup-only fields out of the paid send body. */
|
|
380
473
|
emailSendPayload(options, resolvedFromAddress) {
|
|
381
474
|
const payload = {
|
|
382
475
|
...(resolvedFromAddress ? { from_address: resolvedFromAddress } : {}),
|
|
383
476
|
...(options.to !== undefined ? { to_address: options.to } : {}),
|
|
384
477
|
...(options.subject !== undefined ? { subject: options.subject } : {}),
|
|
385
478
|
...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
|
|
386
|
-
// Preserve mailbox provisioning when the new domain does not yet have a stored mode.
|
|
387
|
-
...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
|
|
388
479
|
body: options.body,
|
|
389
480
|
signal: options.signal,
|
|
390
481
|
onStatusUpdate: options.onStatusUpdate,
|
|
@@ -1140,8 +1231,8 @@ class OneShot {
|
|
|
1140
1231
|
* already has with them, or a new one. Use it once an invitation is accepted,
|
|
1141
1232
|
* or for a connection the account has never messaged. Needs the `reply`
|
|
1142
1233
|
* grant; shares the reply daily cap and pacing. A failed send carries an
|
|
1143
|
-
* error code: `
|
|
1144
|
-
*
|
|
1234
|
+
* error code: `recipient_not_connected` means invite them first, not give
|
|
1235
|
+
* up.
|
|
1145
1236
|
*/
|
|
1146
1237
|
async linkedinMessage(options) {
|
|
1147
1238
|
this.validate(options.accountId, 'accountId');
|
|
@@ -1489,7 +1580,7 @@ class OneShot {
|
|
|
1489
1580
|
}
|
|
1490
1581
|
/**
|
|
1491
1582
|
* The agent's connections: GitHub and business systems, each with `id`, `system`, `account_label` and
|
|
1492
|
-
* `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement, meetings) takes the `id` of an active
|
|
1583
|
+
* `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement, meetings, field service, legal matters) takes the `id` of an active
|
|
1493
1584
|
* one as `connection_id`; pick it by `system` and `account_label`, then call the read with the generic
|
|
1494
1585
|
* {@link OneShot.route}.
|
|
1495
1586
|
*
|
|
@@ -1561,6 +1652,9 @@ class OneShot {
|
|
|
1561
1652
|
payload.params = options.params;
|
|
1562
1653
|
if (options.budget_usdc)
|
|
1563
1654
|
payload.budget_usdc = options.budget_usdc;
|
|
1655
|
+
// Sent even at 0: the API refuses it, where dropping it would fall back to a limit in `scope`.
|
|
1656
|
+
if (options.purchase_limit_usdc !== undefined)
|
|
1657
|
+
payload.purchase_limit_usdc = options.purchase_limit_usdc;
|
|
1564
1658
|
if (options.deadline)
|
|
1565
1659
|
payload.deadline = options.deadline;
|
|
1566
1660
|
if (options.soul_slug)
|
|
@@ -1638,7 +1732,7 @@ class OneShot {
|
|
|
1638
1732
|
* with JobError (goal ID, end_reason code, snapshot in partialResult).
|
|
1639
1733
|
* Budget exhaustion waits in awaiting_budget for funding; after seven days it ends. After fundComputeGoal revives an ended goal,
|
|
1640
1734
|
* call waitForComputeGoal again with the same ID to observe the new run.
|
|
1641
|
-
* Paused/awaiting_confirmation/awaiting_connection/awaiting_budget/awaiting_tool and recurring goals keep waiting until the
|
|
1735
|
+
* Paused/awaiting_confirmation/awaiting_connection/awaiting_budget/awaiting_tool/awaiting_setup and recurring goals keep waiting until the
|
|
1642
1736
|
* goal ends or the timeout (seconds, default 120) expires with JobTimeoutError.
|
|
1643
1737
|
* Infinity disables the local deadline.
|
|
1644
1738
|
* onStatusUpdate receives each distinct observed status before settlement.
|
|
@@ -1696,7 +1790,8 @@ class OneShot {
|
|
|
1696
1790
|
}
|
|
1697
1791
|
}
|
|
1698
1792
|
/**
|
|
1699
|
-
* List tasks under a compute goal
|
|
1793
|
+
* List tasks under a compute goal, newest first: one page of `limit` tasks (default 100, at most 500).
|
|
1794
|
+
* Use `getComputeTasksPage` to read further pages.
|
|
1700
1795
|
*
|
|
1701
1796
|
* @example
|
|
1702
1797
|
* ```typescript
|
|
@@ -1706,16 +1801,40 @@ class OneShot {
|
|
|
1706
1801
|
* }
|
|
1707
1802
|
* ```
|
|
1708
1803
|
*/
|
|
1709
|
-
async getComputeTasks(goalId) {
|
|
1804
|
+
async getComputeTasks(goalId, options = {}) {
|
|
1805
|
+
return (await this.getComputeTasksPage(goalId, options)).tasks;
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* One page of a compute goal's tasks, newest first, with the cursor of the next page (null on the last).
|
|
1809
|
+
*
|
|
1810
|
+
* @example
|
|
1811
|
+
* ```typescript
|
|
1812
|
+
* let cursor: string | undefined;
|
|
1813
|
+
* do {
|
|
1814
|
+
* const page = await agent.getComputeTasksPage('goal_01HX...', { cursor });
|
|
1815
|
+
* for (const t of page.tasks) console.log(t.id, t.status);
|
|
1816
|
+
* cursor = page.next_cursor ?? undefined;
|
|
1817
|
+
* } while (cursor);
|
|
1818
|
+
* ```
|
|
1819
|
+
*/
|
|
1820
|
+
async getComputeTasksPage(goalId, options = {}) {
|
|
1710
1821
|
this.validate(goalId, 'goalId');
|
|
1711
|
-
const
|
|
1822
|
+
const query = new URLSearchParams();
|
|
1823
|
+
if (options.status)
|
|
1824
|
+
query.set('status', options.status);
|
|
1825
|
+
if (options.limit !== undefined)
|
|
1826
|
+
query.set('limit', String(options.limit));
|
|
1827
|
+
if (options.cursor)
|
|
1828
|
+
query.set('cursor', options.cursor);
|
|
1829
|
+
const qs = query.toString();
|
|
1830
|
+
const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/tasks${qs ? `?${qs}` : ''}`, {
|
|
1712
1831
|
headers: await this.signedReadHeaders()
|
|
1713
1832
|
});
|
|
1714
1833
|
if (!response.ok) {
|
|
1715
1834
|
throw new errors_2.ToolError('Failed to get compute tasks', response.status, await response.text(), response.headers.get('retry-after'));
|
|
1716
1835
|
}
|
|
1717
1836
|
const json = await response.json();
|
|
1718
|
-
return json.data;
|
|
1837
|
+
return { tasks: json.data, next_cursor: json.page?.next_cursor ?? null };
|
|
1719
1838
|
}
|
|
1720
1839
|
/**
|
|
1721
1840
|
* List what a compute goal produced: the files its sandbox tasks left and
|
|
@@ -1913,7 +2032,7 @@ class OneShot {
|
|
|
1913
2032
|
return json.data;
|
|
1914
2033
|
}
|
|
1915
2034
|
/**
|
|
1916
|
-
* Pause a
|
|
2035
|
+
* Pause a compute goal (one-off goals keep their work and continue where they were on resume)
|
|
1917
2036
|
*
|
|
1918
2037
|
* @example
|
|
1919
2038
|
* ```typescript
|
|
@@ -1934,7 +2053,7 @@ class OneShot {
|
|
|
1934
2053
|
return json.data;
|
|
1935
2054
|
}
|
|
1936
2055
|
/**
|
|
1937
|
-
* Resume a paused
|
|
2056
|
+
* Resume a paused compute goal
|
|
1938
2057
|
*
|
|
1939
2058
|
* @example
|
|
1940
2059
|
* ```typescript
|
|
@@ -2540,6 +2659,9 @@ class OneShot {
|
|
|
2540
2659
|
const credits = response.status === 402 ? this.parseInsufficientCredits(text) : undefined;
|
|
2541
2660
|
if (credits)
|
|
2542
2661
|
throw credits;
|
|
2662
|
+
const ambiguous = response.status === 500 ? this.parseSettlementAmbiguous(text) : undefined;
|
|
2663
|
+
if (ambiguous)
|
|
2664
|
+
throw ambiguous;
|
|
2543
2665
|
const rejection = [402, 500].includes(response.status) ? this.parsePaymentRejection(text) : undefined;
|
|
2544
2666
|
if (rejection)
|
|
2545
2667
|
throw rejection;
|
|
@@ -2648,6 +2770,20 @@ class OneShot {
|
|
|
2648
2770
|
return undefined;
|
|
2649
2771
|
}
|
|
2650
2772
|
}
|
|
2773
|
+
/** A 500 `settlement_ambiguous`: the payment may be spent, so the caller must not sign another for this call. */
|
|
2774
|
+
parseSettlementAmbiguous(text) {
|
|
2775
|
+
let body;
|
|
2776
|
+
try {
|
|
2777
|
+
body = JSON.parse(text);
|
|
2778
|
+
}
|
|
2779
|
+
catch {
|
|
2780
|
+
return undefined;
|
|
2781
|
+
}
|
|
2782
|
+
if (body?.error !== 'settlement_ambiguous')
|
|
2783
|
+
return undefined;
|
|
2784
|
+
const settlementId = typeof body.settlement_id === 'string' ? body.settlement_id : undefined;
|
|
2785
|
+
return new errors_1.SettlementAmbiguousError(`Payment may have gone through. Don't pay again; if it landed and the call did not run, it is credited to your account.${settlementId ? ` [settlement ${settlementId}]` : ''}`, settlementId);
|
|
2786
|
+
}
|
|
2651
2787
|
parsePaymentRejection(text) {
|
|
2652
2788
|
let data;
|
|
2653
2789
|
try {
|
|
@@ -2726,7 +2862,7 @@ class OneShot {
|
|
|
2726
2862
|
}
|
|
2727
2863
|
async executeToolRequest(endpoint, options, quoteId) {
|
|
2728
2864
|
// LinkedIn writes act through a human's account and cannot be recalled: always keyed.
|
|
2729
|
-
const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
|
|
2865
|
+
const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|lookup\/company-fund-holdings|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
|
|
2730
2866
|
const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
|
|
2731
2867
|
if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
|
|
2732
2868
|
throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
|
|
@@ -2839,7 +2975,7 @@ class OneShot {
|
|
|
2839
2975
|
if (duplicate?.error === 'duplicate_request' && typeof duplicate.details?.request_id === 'string') {
|
|
2840
2976
|
response = Response.json({ request_id: duplicate.details.request_id, receipt_id: duplicate.details.receipt_id,
|
|
2841
2977
|
status: 'processing', tool: 'linkedin', linkedin: { action_request_id: duplicate.details.action_request_id,
|
|
2842
|
-
invitation_id: duplicate.details.invitation_id } }, { status: 202 });
|
|
2978
|
+
invitation_id: duplicate.details.invitation_id, scheduled_for: null } }, { status: 202 });
|
|
2843
2979
|
}
|
|
2844
2980
|
}
|
|
2845
2981
|
if (!response.ok) {
|
|
@@ -3295,7 +3431,7 @@ class OneShot {
|
|
|
3295
3431
|
// Identity Verification requires wallet proof on the payment leg too, to recover keyed submissions.
|
|
3296
3432
|
const provenPayment = /(?:^|\/)identity\/start$/.test(endpoint);
|
|
3297
3433
|
const askedForProof = !!extraHeaders?.['x-agent-proof'];
|
|
3298
|
-
const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|physical-mail\/send|solve|predict)$/.test(endpoint)
|
|
3434
|
+
const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|lookup\/company-fund-holdings|physical-mail\/send|solve|predict)$/.test(endpoint)
|
|
3299
3435
|
? 'submit'
|
|
3300
3436
|
: askedForProof ? 'read' : 'write';
|
|
3301
3437
|
const headers = {
|
|
@@ -3466,14 +3602,22 @@ class OneShot {
|
|
|
3466
3602
|
}
|
|
3467
3603
|
catch (err) {
|
|
3468
3604
|
// Keyed writes may already be queued server-side after a transport failure: keep the reservation.
|
|
3469
|
-
if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|physical-mail\/send|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
|
|
3605
|
+
if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|lookup\/company-fund-holdings|physical-mail\/send|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
|
|
3470
3606
|
this.releaseUsdcReservation(signed.reservation);
|
|
3471
3607
|
throw err;
|
|
3472
3608
|
}
|
|
3473
|
-
|
|
3609
|
+
// A settlement_ambiguous payment may be spent: keep its reservation until it expires.
|
|
3610
|
+
if (!resp.ok && !(extraHeaders?.['Idempotency-Key'] && resp.status >= 500) && !(await this.isSettlementAmbiguous(resp))) {
|
|
3474
3611
|
this.releaseUsdcReservation(signed.reservation);
|
|
3612
|
+
}
|
|
3475
3613
|
return resp;
|
|
3476
3614
|
}
|
|
3615
|
+
async isSettlementAmbiguous(resp) {
|
|
3616
|
+
if (resp.status !== 500)
|
|
3617
|
+
return false;
|
|
3618
|
+
const text = await resp.clone().text().catch(() => '');
|
|
3619
|
+
return this.parseSettlementAmbiguous(text) !== undefined;
|
|
3620
|
+
}
|
|
3477
3621
|
/** Parse the PAYMENT-REQUIRED header from a 402 response into the accepted requirements and Bazaar metadata. */
|
|
3478
3622
|
parsePaymentRequired(header) {
|
|
3479
3623
|
if (header) {
|