@oneshot-agent/sdk 0.33.0 → 0.35.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
@@ -55,8 +55,32 @@ var swap_1 = require("./swap");
55
55
  Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function () { return swap_1.getSwapQuote; } });
56
56
  Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
57
57
  __exportStar(require("./errors"), exports);
58
+ // Receipt verification (`canonicalizeReceipt`, `verifyReceipt`,
59
+ // `receiptFromWire`, and their supporting types `SignableReceipt`,
60
+ // `ReceiptSignatureFields`, `ReceiptJwk`, `ReceiptJwks`,
61
+ // `ReceiptVerificationResult`, `ReceiptVerificationFailureReason`) is
62
+ // intentionally NOT re-exported here, values OR types. `./receipt`
63
+ // statically imports `node:crypto`, and this file is the SDK's main entry —
64
+ // re-exporting its values would give every consumer (including browser/edge
65
+ // bundles, which this file otherwise avoids requiring Node builtins for;
66
+ // see the deliberate `btoa` fallback below) a hard Node-only dependency
67
+ // with no way to opt out. Import from the `@oneshot-agent/sdk/receipt`
68
+ // subpath instead (see package.json `exports` and the README's
69
+ // "Verifying Receipts" section).
70
+ //
71
+ // Types alone erase at compile time and carry no runtime import, so a
72
+ // type-only re-export here would be runtime-safe — but `SignableReceipt`
73
+ // declares `providerCost` (libs/agent-sdk/src/receipt.ts), the field whose
74
+ // removal from the public SDK surface `tests/unit/sdk-provider-cost-removed
75
+ // .test.ts` guards. Re-exporting it from the main entry would put it back
76
+ // on the surface that guard exists to keep clean, just via the type system
77
+ // instead of a runtime value. `providerCost` must stay inside
78
+ // `SignableReceipt` itself (it's part of what the signer actually signs —
79
+ // see `canonicalizeReceipt`), so the fix is not exposing that type here,
80
+ // not removing the field. Import `SignableReceipt` from the `./receipt`
81
+ // subpath directly if you need it.
58
82
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
59
- const SDK_VERSION = '0.33.0';
83
+ const SDK_VERSION = '0.35.0';
60
84
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
61
85
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
62
86
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
@@ -216,6 +240,7 @@ class OneShot {
216
240
  this._budgets = validateBudgetConfig(config.budgets);
217
241
  this._alertEmail = config.alerts?.email;
218
242
  this._defaultHeaders = { ...(config.defaultHeaders ?? {}) };
243
+ this._stagingKey = config.stagingKey;
219
244
  this.rpcProvider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl ?? RPC_URL);
220
245
  if (config.accessToken) {
221
246
  // Option D: access-token session. Exclusive with every wallet option,
@@ -305,12 +330,19 @@ class OneShot {
305
330
  // from_mailbox, omit from_address entirely so the server picks from
306
331
  // the agent's domain pool. The chosen address comes back on the
307
332
  // quote response (`quote.from_address`) and we replay it on /send.
308
- // When the caller pins either knob, we keep legacy behavior: build
309
- // `${mailbox ?? 'agent'}@${domain ?? 'oneshotagent.com'}`.
333
+ // When the caller pins from_domain (with or without from_mailbox), we
334
+ // build `${mailbox ?? 'agent'}@${from_domain}`. Pinning from_mailbox
335
+ // alone has no domain to attach it to — `oneshotagent.com` is OneShot's
336
+ // own domain and no agent can send from it — so that combination is a
337
+ // client-side validation error rather than a guaranteed domain_not_owned
338
+ // rejection from the server.
339
+ if (options.from_mailbox && !options.from_domain) {
340
+ 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');
341
+ }
310
342
  const useRotation = !options.from_domain && !options.from_mailbox;
311
343
  const fromAddress = useRotation
312
344
  ? undefined
313
- : `${options.from_mailbox ?? 'agent'}@${options.from_domain ?? 'oneshotagent.com'}`;
345
+ : `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
314
346
  // `mailbox_provisioning_fee` (>0) means `from_address` is a new address that
315
347
  // provisions a mailbox on first send — a one-time fee folded into `total_cost`.
316
348
  const quote = await this.tool('email/quote', {
@@ -789,8 +821,8 @@ class OneShot {
789
821
  if (options.task.length < 10) {
790
822
  throw new errors_2.ValidationError('Task must be at least 10 characters', 'task');
791
823
  }
792
- if (options.max_steps !== undefined && (options.max_steps < 1 || options.max_steps > 100)) {
793
- throw new errors_2.ValidationError('max_steps must be between 1 and 100', 'max_steps');
824
+ if (options.max_steps !== undefined && (!Number.isInteger(options.max_steps) || options.max_steps < 25 || options.max_steps > 100)) {
825
+ throw new errors_2.ValidationError('max_steps must be between 25 and 100', 'max_steps');
794
826
  }
795
827
  const payload = {
796
828
  task: options.task,
@@ -805,8 +837,8 @@ class OneShot {
805
837
  payload.session_id = options.session_id;
806
838
  if (options.profile_id)
807
839
  payload.profile_id = options.profile_id;
808
- if (options.secrets)
809
- payload.secrets = options.secrets;
840
+ if (options.secrets !== undefined)
841
+ throw new errors_2.ValidationError('secrets is unsupported; import cookies or storage_state with createBrowserProfile', 'secrets');
810
842
  if (options.max_steps)
811
843
  payload.max_steps = options.max_steps;
812
844
  const { execResp } = await this.runQuoteToPay({
@@ -843,18 +875,42 @@ class OneShot {
843
875
  * console.log(profile.id); // Use this in browser({ profile_id: ... })
844
876
  * ```
845
877
  */
846
- async createBrowserProfile(name) {
878
+ async createBrowserProfile(name, options = {}) {
847
879
  this.validate(name, 'name');
880
+ if (options.cookies !== undefined && options.storage_state !== undefined)
881
+ throw new errors_2.ValidationError('Provide cookies or storage_state, not both', 'options');
848
882
  const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
849
883
  method: 'POST',
850
884
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
851
- body: JSON.stringify({ name }),
885
+ body: JSON.stringify({ name, ...options }),
886
+ signal: AbortSignal.timeout(150000),
852
887
  });
853
888
  if (!response.ok) {
854
889
  throw new errors_2.ToolError('Failed to create browser profile', response.status, await response.text());
855
890
  }
856
891
  return response.json();
857
892
  }
893
+ /** Open a profile login session; wait for idle before entering credentials in live_url. */
894
+ async startBrowserProfileSetup(profileId, startUrl) {
895
+ return this.browserProfileSetup(profileId, 'start', { start_url: startUrl });
896
+ }
897
+ async getBrowserProfileSetup(profileId) {
898
+ return this.browserProfileSetup(profileId, 'status');
899
+ }
900
+ /** Call after login and 2FA. Saves state and inspects cookies before site navigation. */
901
+ async finishBrowserProfileSetup(profileId) {
902
+ return this.browserProfileSetup(profileId, 'finish');
903
+ }
904
+ async browserProfileSetup(profileId, action, body = {}) {
905
+ this.validate(profileId, 'profileId');
906
+ const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles/${encodeURIComponent(profileId)}/setup/${action}`, {
907
+ method: 'POST', headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
908
+ body: JSON.stringify(body), signal: AbortSignal.timeout(150000),
909
+ });
910
+ if (!response.ok)
911
+ throw new errors_2.ToolError('Browser profile setup failed', response.status, '');
912
+ return response.json();
913
+ }
858
914
  /**
859
915
  * List all browser profiles
860
916
  *
@@ -894,6 +950,119 @@ class OneShot {
894
950
  throw new errors_2.ToolError('Failed to delete browser profile', response.status, await response.text());
895
951
  }
896
952
  }
953
+ // ── LinkedIn — messaging through a human-connected account (issue #756) ──
954
+ //
955
+ // The human connects THEIR OWN LinkedIn account through a hosted login link
956
+ // and grants specific actions. Connection/reads are free (signed proof);
957
+ // sync, reply, profile view and react are paid fixed-price tools. Every
958
+ // write is idempotency-keyed automatically — a message LinkedIn accepted
959
+ // cannot be recalled.
960
+ async linkedinFree(method, path, opts) {
961
+ const url = new URL(`${this.baseUrl}/v1/tools/linkedin${path}`);
962
+ for (const [k, v] of Object.entries(opts.query ?? {}))
963
+ if (v !== undefined)
964
+ url.searchParams.set(k, v);
965
+ const response = await fetch(url.toString(), {
966
+ method,
967
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders(opts.scope ?? 'read')) },
968
+ ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
969
+ signal: AbortSignal.timeout(60000),
970
+ });
971
+ if (!response.ok)
972
+ throw new errors_2.ToolError(`LinkedIn ${opts.what} failed`, response.status, await response.text());
973
+ return response.json();
974
+ }
975
+ /**
976
+ * Create a hosted LinkedIn login link. Show `url` to the human once; it
977
+ * expires in 30 minutes and must be opened top-level (never in an iframe).
978
+ * Poll `getLinkedInConnection(intent_id)` until `completed`.
979
+ */
980
+ async linkedinConnect(options) {
981
+ if (!Array.isArray(options.requestedActions) || options.requestedActions.length === 0) {
982
+ throw new errors_2.ValidationError('requestedActions must list at least one action (read, reply, view_profile, react)', 'requestedActions');
983
+ }
984
+ return this.linkedinFree('POST', '/connect', {
985
+ scope: 'write', what: 'connect',
986
+ body: { requested_actions: options.requestedActions, success_redirect_url: options.successRedirectUrl, failure_redirect_url: options.failureRedirectUrl },
987
+ });
988
+ }
989
+ async getLinkedInConnection(intentId) {
990
+ this.validate(intentId, 'intentId');
991
+ return this.linkedinFree('GET', `/connect/${encodeURIComponent(intentId)}`, { what: 'connection status' });
992
+ }
993
+ async listLinkedInAccounts(options = {}) {
994
+ return this.linkedinFree('GET', '/accounts', { what: 'accounts', query: { include_revoked: options.includeRevoked ? 'true' : undefined } });
995
+ }
996
+ async getLinkedInAccount(accountId) {
997
+ this.validate(accountId, 'accountId');
998
+ return this.linkedinFree('GET', `/accounts/${encodeURIComponent(accountId)}`, { what: 'account' });
999
+ }
1000
+ /** Reconnect an existing account (keeps ownership; may narrow, never widen, the grant). */
1001
+ async reconnectLinkedInAccount(accountId, options = {}) {
1002
+ this.validate(accountId, 'accountId');
1003
+ return this.linkedinFree('POST', `/accounts/${encodeURIComponent(accountId)}/reconnect`, {
1004
+ scope: 'write', what: 'reconnect',
1005
+ body: { requested_actions: options.requestedActions, success_redirect_url: options.successRedirectUrl, failure_redirect_url: options.failureRedirectUrl },
1006
+ });
1007
+ }
1008
+ /** End the grant, cancel queued writes, delete the upstream connection. */
1009
+ async revokeLinkedInAccount(accountId) {
1010
+ this.validate(accountId, 'accountId');
1011
+ return this.linkedinFree('DELETE', `/accounts/${encodeURIComponent(accountId)}`, { scope: 'write', what: 'revoke' });
1012
+ }
1013
+ async getLinkedInSync(accountId) {
1014
+ this.validate(accountId, 'accountId');
1015
+ return this.linkedinFree('GET', `/accounts/${encodeURIComponent(accountId)}/sync`, { what: 'sync status' });
1016
+ }
1017
+ async linkedinConversations(options) {
1018
+ this.validate(options.accountId, 'accountId');
1019
+ return this.linkedinFree('GET', `/accounts/${encodeURIComponent(options.accountId)}/conversations`, {
1020
+ what: 'conversations',
1021
+ query: { cursor: options.cursor, limit: options.limit?.toString(), since: options.since, unread: options.unread ? 'true' : undefined, archived: options.archived ? 'true' : undefined },
1022
+ });
1023
+ }
1024
+ async linkedinMessages(options) {
1025
+ this.validate(options.accountId, 'accountId');
1026
+ return this.linkedinFree('GET', `/accounts/${encodeURIComponent(options.accountId)}/messages`, {
1027
+ what: 'messages',
1028
+ query: {
1029
+ conversation_id: options.conversationId, direction: options.direction, since: options.since, changed_since: options.changedSince,
1030
+ cursor: options.cursor, limit: options.limit?.toString(), include_deleted: options.includeDeleted ? 'true' : undefined,
1031
+ },
1032
+ });
1033
+ }
1034
+ /**
1035
+ * One bounded, paid history-sync run (up to maxPages × 250 messages). A
1036
+ * run that hits the bound is paused with a cursor; the next `continue`
1037
+ * call resumes it. Check `coverage.complete` before buying another run.
1038
+ */
1039
+ async linkedinSync(options) {
1040
+ this.validate(options.accountId, 'accountId');
1041
+ const { accountId, mode, maxPages, ...rest } = options;
1042
+ return this.tool('linkedin/sync', { ...rest, account_id: accountId, ...(mode ? { mode } : {}), ...(maxPages ? { max_pages: maxPages } : {}), timeout: options.timeout ?? 600 });
1043
+ }
1044
+ /** Send a reply in an existing conversation through the connected account. Paced; per-account daily cap. */
1045
+ async linkedinReply(options) {
1046
+ this.validate(options.accountId, 'accountId');
1047
+ this.validate(options.conversationId, 'conversationId');
1048
+ this.validate(options.text, 'text');
1049
+ const { accountId, conversationId, text, ...rest } = options;
1050
+ return this.tool('linkedin/reply', { ...rest, account_id: accountId, conversation_id: conversationId, text, timeout: options.timeout ?? 180 });
1051
+ }
1052
+ /** View a profile through the connected account. `notify: true` is visible to the target and cannot be undone. */
1053
+ async linkedinViewProfile(options) {
1054
+ this.validate(options.accountId, 'accountId');
1055
+ this.validate(options.identifier, 'identifier');
1056
+ const { accountId, identifier, notify, ...rest } = options;
1057
+ return this.tool('linkedin/profile-view', { ...rest, account_id: accountId, identifier, notify: !!notify, timeout: options.timeout ?? 180 });
1058
+ }
1059
+ /** React to a post through the connected account. */
1060
+ async linkedinReact(options) {
1061
+ this.validate(options.accountId, 'accountId');
1062
+ this.validate(options.postId, 'postId');
1063
+ const { accountId, postId, reactionType, ...rest } = options;
1064
+ return this.tool('linkedin/react', { ...rest, account_id: accountId, post_id: postId, reaction_type: reactionType ?? 'like', timeout: options.timeout ?? 180 });
1065
+ }
897
1066
  /**
898
1067
  * Update an existing website build
899
1068
  *
@@ -1487,11 +1656,42 @@ class OneShot {
1487
1656
  headers() {
1488
1657
  return {
1489
1658
  ...this._defaultHeaders,
1659
+ ...(this._stagingKey ? { 'X-Staging-Key': this._stagingKey } : {}),
1490
1660
  'X-Agent-ID': this.provider.address,
1491
1661
  'X-OneShot-SDK-Version': SDK_VERSION,
1492
1662
  ...(this._accessToken ? { Authorization: `Bearer ${this._accessToken}` } : {}),
1493
1663
  };
1494
1664
  }
1665
+ /** Submit a wallet-proven application. Approval is performed by the staging operator. */
1666
+ async applyForStaging(contact, purpose) {
1667
+ return this.stagingOnboarding('POST', '/applications', { contact, purpose });
1668
+ }
1669
+ async getStagingApplication() {
1670
+ return this.stagingOnboarding('GET', '/applications/me');
1671
+ }
1672
+ /** Rotates the wallet's staging key and installs it on this SDK instance. */
1673
+ async obtainStagingKey() {
1674
+ const result = await this.stagingOnboarding('POST', '/credential', {});
1675
+ this._stagingKey = result.staging_key;
1676
+ return result.staging_key;
1677
+ }
1678
+ async stagingOnboarding(method, path, body) {
1679
+ if (this._accessToken)
1680
+ throw new Error('Staging onboarding requires a wallet signer');
1681
+ const digest = ethers_1.ethers.sha256(ethers_1.ethers.toUtf8Bytes(JSON.stringify(body ?? {}))).slice(2);
1682
+ const headers = await this.signedReadHeaders(`staging:${method}:${path}:${digest}`);
1683
+ if (!headers['x-agent-proof'])
1684
+ throw new Error('Could not sign staging application');
1685
+ const response = await fetch(`${this.baseUrl}/v1/staging${path}`, {
1686
+ method, headers: { ...headers, 'Content-Type': 'application/json' },
1687
+ signal: AbortSignal.timeout(10000),
1688
+ ...(body ? { body: JSON.stringify(body) } : {}),
1689
+ });
1690
+ const result = await response.json();
1691
+ if (!response.ok)
1692
+ throw new Error(result.error ?? `Staging request failed (${response.status})`);
1693
+ return result;
1694
+ }
1495
1695
  /** True for access-token sessions: credits-only, never signs. */
1496
1696
  get isAccessTokenSession() {
1497
1697
  return this._accessToken !== undefined;
@@ -1798,6 +1998,7 @@ class OneShot {
1798
1998
  response = await fetch(`${baseUrl}/v1/agents/me`, {
1799
1999
  headers: {
1800
2000
  ...(config.defaultHeaders ?? {}),
2001
+ ...(config.stagingKey ? { 'X-Staging-Key': config.stagingKey } : {}),
1801
2002
  Authorization: `Bearer ${config.accessToken}`,
1802
2003
  'X-OneShot-SDK-Version': SDK_VERSION,
1803
2004
  },
@@ -1913,7 +2114,8 @@ class OneShot {
1913
2114
  return this.readReliabilityJson('/v1/status', false, true);
1914
2115
  }
1915
2116
  async executeToolRequest(endpoint, options, quoteId) {
1916
- const reliable = /(?:^|\/)(enrich\/(profile|email)|verify\/email)$/.test(endpoint);
2117
+ // LinkedIn writes act through a human's account and cannot be recalled: always keyed.
2118
+ const reliable = /(?:^|\/)(enrich\/(profile|email)|verify\/email|linkedin\/(reply|profile-view|react))$/.test(endpoint);
1917
2119
  const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
1918
2120
  if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
1919
2121
  throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
@@ -2293,7 +2495,7 @@ class OneShot {
2293
2495
  settle(() => {
2294
2496
  cleanup();
2295
2497
  wait.via = 'ws';
2296
- reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code));
2498
+ reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code, msg.result));
2297
2499
  });
2298
2500
  }
2299
2501
  else {
@@ -2384,7 +2586,7 @@ class OneShot {
2384
2586
  if (job.status === 'failed') {
2385
2587
  if (wait)
2386
2588
  wait.via = 'http';
2387
- throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code);
2589
+ throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code, job.result);
2388
2590
  }
2389
2591
  emit?.(String(job.status));
2390
2592
  retries = 0;
@@ -2605,7 +2807,8 @@ class OneShot {
2605
2807
  resp = await this.makeRequest(endpoint, data, signed.auth, quoteId, signal, timeoutMs, extraHeaders);
2606
2808
  }
2607
2809
  catch (err) {
2608
- if (!/(enrich\/(profile|email)|verify\/email|physical-mail\/send)$/.test(endpoint))
2810
+ // Keyed writes may already be queued server-side after a transport failure: keep the reservation.
2811
+ if (!/(enrich\/(profile|email)|verify\/email|physical-mail\/send|linkedin\/(reply|profile-view|react))$/.test(endpoint))
2609
2812
  this.releaseUsdcReservation(signed.reservation);
2610
2813
  throw err;
2611
2814
  }