@oneshot-agent/sdk 0.46.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -37,6 +37,8 @@ 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 hunts_1 = require("./hunts");
41
+ __exportStar(require("./hunts"), exports);
40
42
  const physical_mail_1 = require("./physical-mail");
41
43
  __exportStar(require("./physical-mail"), exports);
42
44
  __exportStar(require("./approvals"), exports);
@@ -63,7 +65,7 @@ const approvals_1 = require("./approvals");
63
65
  // stay off this public surface (see tests/unit/sdk-provider-cost-removed.test.ts).
64
66
  // Import from the `@oneshot-agent/sdk/receipt` subpath instead.
65
67
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
66
- const SDK_VERSION = '0.46.0';
68
+ const SDK_VERSION = '0.48.0';
67
69
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
68
70
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
69
71
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
@@ -187,6 +189,7 @@ class OneShot {
187
189
  * For CDP wallets, use OneShot.create() instead.
188
190
  */
189
191
  constructor(config, walletProvider) {
192
+ this.hunts = new hunts_1.Hunts(this);
190
193
  this.physicalMail = new physical_mail_1.PhysicalMail(async (path, method, body, mime) => {
191
194
  const response = await fetch(`${this.baseUrl}/v1/tools/physical-mail${path}`, {
192
195
  method, headers: { ...await this.signedReadHeaders(method === 'GET' ? 'read' : 'write'), 'Content-Type': mime ?? 'application/json' },
@@ -208,6 +211,7 @@ class OneShot {
208
211
  /** ETH mode: serializes read → decide → swap → reserve per instance (also prevents concurrent swap nonce races). */
209
212
  this._usdcLock = Promise.resolve();
210
213
  this.baseUrl = config.baseUrl ?? BASE_URL;
214
+ this.huntUrl = config.huntUrl;
211
215
  this.debug = config.debug ?? false;
212
216
  this.logger = config.logger ?? console.log;
213
217
  this._currency = config.currency ?? 'USDC';
@@ -313,8 +317,24 @@ class OneShot {
313
317
  const fromAddress = useRotation
314
318
  ? undefined
315
319
  : `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
320
+ // Confirming a new-domain registration (#1070): the caller passes back the
321
+ // quote_id of the `confirmation_required` result it was shown. That quote
322
+ // is sent as-is — no second quote — so the server charges the stored total
323
+ // of the quote the user saw (x402-legacy validates the id and takes the
324
+ // amount from the stored quote). A bare `true` is not a confirmation: it
325
+ // names no amount, so it falls through to the quote-only path below. An
326
+ // expired or already-used quote is refused by the server, and the next
327
+ // unconfirmed call produces a fresh quote to show again.
328
+ const confirmedQuote = typeof options.confirmNewDomain === 'string' && options.confirmNewDomain.trim()
329
+ ? options.confirmNewDomain.trim() : undefined;
330
+ if (confirmedQuote) {
331
+ this.log(`Email: sending against confirmed quote ${confirmedQuote}`);
332
+ return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress), confirmedQuote);
333
+ }
316
334
  // `mailbox_provisioning_fee` (>0) means `from_address` is a new address that
317
335
  // provisions a mailbox on first send — a one-time fee folded into `total_cost`.
336
+ // `is_new` (true) means this quote would register a brand-new domain, which
337
+ // drives the confirm-before-charge gate below (#1070 round 2).
318
338
  const quote = await this.tool('email/quote', {
319
339
  ...(fromAddress ? { from_address: fromAddress } : {}),
320
340
  ...(options.to !== undefined ? { to_address: options.to } : {}),
@@ -334,7 +354,32 @@ class OneShot {
334
354
  // un-set so the server returned a 200 with the quote and the caller still
335
355
  // wants the local guard to apply).
336
356
  this.assertWithinMaxCost(quote.total_cost, options.maxCost);
337
- const resolvedFromAddress = fromAddress ?? quote.from_address;
357
+ // Registering a brand-new domain is charged only after its exact quote has
358
+ // been shown and explicitly authorized (#1070 round 2) — this call stops
359
+ // here, before /email/send, and charges nothing. Pass confirmNewDomain:
360
+ // true (with the same from_domain) to register the domain, pay the quoted
361
+ // total, and send the email in one follow-up call.
362
+ if (quote.is_new) {
363
+ const domainName = quote.domain ?? options.from_domain ?? '';
364
+ const breakdown = quote.registration_fee !== undefined && quote.service_fee !== undefined
365
+ ? ` (registration $${quote.registration_fee} + service fee $${quote.service_fee}` +
366
+ `${quote.mailbox_provisioning_fee ? ` + mailbox fee $${quote.mailbox_provisioning_fee}` : ''})`
367
+ : '';
368
+ return {
369
+ status: 'confirmation_required',
370
+ cost: parseFloat(quote.total_cost),
371
+ quote_id: quote.quote_id,
372
+ domain: { domain: domainName, status: 'quote_only', was_provisioned: false },
373
+ message: `Registering ${domainName} costs $${quote.total_cost} total${breakdown}. ` +
374
+ `Nothing has been charged yet. Show the user this exact amount; once they agree, call this again with the same ` +
375
+ `arguments and confirm_new_domain: '${quote.quote_id}' (SDK: confirmNewDomain) to register the domain, ` +
376
+ `charge exactly $${quote.total_cost}, and send this email. true only re-quotes; a wrong or expired quote id is refused without charge.`,
377
+ };
378
+ }
379
+ return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress ?? quote.from_address), quote.quote_id);
380
+ }
381
+ /** The /email/send body for `email()`: the same whether it follows a fresh quote or a confirmed one. */
382
+ emailSendPayload(options, resolvedFromAddress) {
338
383
  const payload = {
339
384
  // Server replays the locked address from the quote when from_address
340
385
  // is absent, but sending it anyway is harmless and forward-compatible
@@ -366,7 +411,11 @@ class OneShot {
366
411
  // Releases a send the action policy held (see ApprovalRequiredError).
367
412
  payload.approvalId = options.approvalId;
368
413
  }
369
- return this.executeToolRequest('/v1/tools/email/send', payload, quote.quote_id);
414
+ // Forward maxCost so the server-side X-Max-Cost-USDC header also guards
415
+ // the send, which matters on a confirmed send that skipped the quote.
416
+ if (options.maxCost !== undefined)
417
+ payload.maxCost = options.maxCost;
418
+ return payload;
370
419
  }
371
420
  /** List the caller's domain pool with warmup and rotation metadata. */
372
421
  async listDomains() {
@@ -780,6 +829,7 @@ class OneShot {
780
829
  payload,
781
830
  signal: options.signal,
782
831
  maxCost: options.maxCost,
832
+ approvalId: options.approvalId,
783
833
  // Build analysis can be slow server-side; bound the quote leg client-side.
784
834
  quoteTimeoutMs: 120000,
785
835
  expectMsg: 'Expected 402 for quote',
@@ -1065,6 +1115,35 @@ class OneShot {
1065
1115
  const { accountId, conversationId, text, ...rest } = options;
1066
1116
  return this.tool('linkedin/reply', { ...rest, account_id: accountId, conversation_id: conversationId, text, timeout: options.timeout ?? 180 });
1067
1117
  }
1118
+ /**
1119
+ * Message a first-degree connection: into the 1:1 conversation the account
1120
+ * already has with them, or a new one. Use it once an invitation is accepted,
1121
+ * or for a connection the account has never messaged. Needs the `reply`
1122
+ * grant; shares the reply daily cap and pacing. A failed send carries an
1123
+ * error code: `not_connected` (invite them first) and `rate_limited` mean
1124
+ * "defer", not "give up".
1125
+ */
1126
+ async linkedinMessage(options) {
1127
+ this.validate(options.accountId, 'accountId');
1128
+ this.validate(options.profile, 'profile');
1129
+ this.validate(options.text, 'text');
1130
+ const { accountId, profile, text, ...rest } = options;
1131
+ return this.tool('linkedin/message', { ...rest, account_id: accountId, profile, text, timeout: options.timeout ?? 180 });
1132
+ }
1133
+ /** One invitation sent through linkedinInvite: `accepted` (with `accepted_at`) as soon as the person is a connection. Free. */
1134
+ async linkedinInvitation(options) {
1135
+ this.validate(options.accountId, 'accountId');
1136
+ this.validate(options.invitationId, 'invitationId');
1137
+ return this.linkedinFree('GET', `/accounts/${encodeURIComponent(options.accountId)}/invitations/${encodeURIComponent(options.invitationId)}`, { what: 'invitation' });
1138
+ }
1139
+ /** Invitations sent through linkedinInvite, most recently changed first. Poll with `changedSince`. Free. */
1140
+ async linkedinInvitations(options) {
1141
+ this.validate(options.accountId, 'accountId');
1142
+ return this.linkedinFree('GET', `/accounts/${encodeURIComponent(options.accountId)}/invitations`, {
1143
+ what: 'invitations',
1144
+ query: { status: options.status, changed_since: options.changedSince, limit: options.limit?.toString() },
1145
+ });
1146
+ }
1068
1147
  /** View a profile through the connected account. `notify: true` is visible to the target and cannot be undone. */
1069
1148
  async linkedinViewProfile(options) {
1070
1149
  this.validate(options.accountId, 'accountId');
@@ -1351,6 +1430,8 @@ class OneShot {
1351
1430
  payload.schedule = options.schedule;
1352
1431
  if (options.approvals)
1353
1432
  payload.approvals = options.approvals;
1433
+ if (options.done_check)
1434
+ payload.done_check = options.done_check;
1354
1435
  // Compute returns the goal directly on 202 — no job polling, unlike the
1355
1436
  // other paid tools.
1356
1437
  const { execResp: createResp } = await this.runQuoteToPay({
@@ -1496,6 +1577,33 @@ class OneShot {
1496
1577
  const json = await response.json();
1497
1578
  return json.data;
1498
1579
  }
1580
+ /**
1581
+ * Answer whether a goal in `awaiting_confirmation` is done. A goal with no
1582
+ * done-check completes only when its owner confirms: the agent that ran it
1583
+ * cannot. `approved: false` sends it back with `response` as what is missing.
1584
+ *
1585
+ * @example
1586
+ * ```typescript
1587
+ * const goal = await agent.getComputeGoal(goalId);
1588
+ * if (goal.status === 'awaiting_confirmation') {
1589
+ * await agent.confirmComputeGoal(goalId, { approved: true });
1590
+ * }
1591
+ * ```
1592
+ */
1593
+ async confirmComputeGoal(goalId, input) {
1594
+ this.validate(goalId, 'goalId');
1595
+ const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1596
+ method: 'POST',
1597
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1598
+ body: JSON.stringify({ confirm_done: input.approved, ...(input.response ? { response: input.response } : {}) }),
1599
+ });
1600
+ if (response.status === 404)
1601
+ throw new errors_2.ToolError('Goal not found', 404, await response.text());
1602
+ if (!response.ok)
1603
+ throw new errors_2.ToolError('Failed to confirm compute goal', response.status, await response.text());
1604
+ const json = await response.json();
1605
+ return json.data;
1606
+ }
1499
1607
  /**
1500
1608
  * Pause a recurring compute goal
1501
1609
  *
@@ -1865,7 +1973,7 @@ class OneShot {
1865
1973
  * locally. A fresh nonce per call prevents replay. Signing failure falls back
1866
1974
  * to plain headers, with a one-time warning.
1867
1975
  */
1868
- async signedReadHeaders(scope = 'read') {
1976
+ async signedReadHeaders(scope = 'read', domainName = 'OneShot Agent Auth') {
1869
1977
  const base = this.headers();
1870
1978
  // The bearer token already binds the request to the agent (and is
1871
1979
  // revocable); the API accepts it in place of the signed proof.
@@ -1875,7 +1983,14 @@ class OneShot {
1875
1983
  const agent = this.provider.address;
1876
1984
  const issuedAt = Math.floor(Date.now() / 1000);
1877
1985
  const nonce = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
1878
- const signature = await this.provider.signTypedData({ name: 'OneShot Agent Auth', version: '1' }, {
1986
+ const signature = await this.provider.signTypedData(
1987
+ // `domainName` lets a different origin (e.g. Soul Hunt, which keeps
1988
+ // its own nonce store — see hunts.ts) use a DIFFERENT EIP-712 domain
1989
+ // than api-service's. EIP-712 mixes the domain into the signed hash,
1990
+ // so a proof is only valid for the one origin it was signed for and
1991
+ // can't be replayed across service boundaries (security finding
1992
+ // F-t_d4805d6f-3, round 2).
1993
+ { name: domainName, version: '1' }, {
1879
1994
  AgentReadAuth: [
1880
1995
  { name: 'agent', type: 'address' },
1881
1996
  { name: 'scope', type: 'string' },
@@ -2316,7 +2431,7 @@ class OneShot {
2316
2431
  }
2317
2432
  async executeToolRequest(endpoint, options, quoteId) {
2318
2433
  // LinkedIn writes act through a human's account and cannot be recalled: always keyed.
2319
- const reliable = /(?:^|\/)(enrich\/(profile|email)|verify\/email|linkedin\/(reply|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2434
+ const reliable = /(?:^|\/)(enrich\/(profile|email)|verify\/email|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2320
2435
  const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
2321
2436
  if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
2322
2437
  throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
@@ -3041,7 +3156,7 @@ class OneShot {
3041
3156
  }
3042
3157
  catch (err) {
3043
3158
  // Keyed writes may already be queued server-side after a transport failure: keep the reservation.
3044
- if (!/(enrich\/(profile|email)|verify\/email|physical-mail\/send|linkedin\/(reply|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3159
+ if (!/(enrich\/(profile|email)|verify\/email|physical-mail\/send|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3045
3160
  this.releaseUsdcReservation(signed.reservation);
3046
3161
  throw err;
3047
3162
  }