@oneshot-agent/sdk 0.16.2 → 0.18.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
@@ -15,6 +15,9 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
19
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20
+ };
18
21
  var __importStar = (this && this.__importStar) || (function () {
19
22
  var ownKeys = function(o) {
20
23
  ownKeys = Object.getOwnPropertyNames || function (o) {
@@ -33,9 +36,10 @@ var __importStar = (this && this.__importStar) || (function () {
33
36
  };
34
37
  })();
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.OneShot = exports.EmergencyNumberError = exports.ContentBlockedError = exports.ValidationError = exports.JobTimeoutError = exports.JobError = exports.ToolError = exports.OneShotError = exports.executeSwap = exports.getSwapQuote = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
39
+ exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
37
40
  const ethers_1 = require("ethers");
38
41
  const ethers_2 = require("./providers/ethers");
42
+ const errors_1 = require("./errors");
39
43
  var ethers_3 = require("./providers/ethers");
40
44
  Object.defineProperty(exports, "EthersWalletProvider", { enumerable: true, get: function () { return ethers_3.EthersWalletProvider; } });
41
45
  var cdp_1 = require("./providers/cdp");
@@ -43,7 +47,8 @@ Object.defineProperty(exports, "CdpWalletProvider", { enumerable: true, get: fun
43
47
  var swap_1 = require("./swap");
44
48
  Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function () { return swap_1.getSwapQuote; } });
45
49
  Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
46
- const SDK_VERSION = '0.13.1';
50
+ __exportStar(require("./errors"), exports);
51
+ const SDK_VERSION = '0.16.2';
47
52
  // ============================================================================
48
53
  // Environment Configuration
49
54
  // ============================================================================
@@ -52,67 +57,12 @@ const RPC_URL = 'https://mainnet.base.org';
52
57
  const CHAIN_ID = 8453;
53
58
  const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
54
59
  // ============================================================================
55
- // Error Classes
60
+ // Public types — defined in ./types.ts. Re-exported below so existing
61
+ // consumer imports (`import { EmailToolOptions, ... } from '@oneshot-agent/sdk'`)
62
+ // keep resolving unchanged. The OneShot class below pulls the names it needs
63
+ // for internal use via the import block.
56
64
  // ============================================================================
57
- class OneShotError extends Error {
58
- constructor(message) {
59
- super(message);
60
- this.name = 'OneShotError';
61
- Object.setPrototypeOf(this, new.target.prototype);
62
- }
63
- }
64
- exports.OneShotError = OneShotError;
65
- class ToolError extends OneShotError {
66
- constructor(message, statusCode, responseBody) {
67
- super(message);
68
- this.statusCode = statusCode;
69
- this.responseBody = responseBody;
70
- this.name = 'ToolError';
71
- }
72
- }
73
- exports.ToolError = ToolError;
74
- class JobError extends OneShotError {
75
- constructor(message, jobId, jobError) {
76
- super(message);
77
- this.jobId = jobId;
78
- this.jobError = jobError;
79
- this.name = 'JobError';
80
- }
81
- }
82
- exports.JobError = JobError;
83
- class JobTimeoutError extends OneShotError {
84
- constructor(jobId, elapsedMs) {
85
- super(`Job ${jobId} timed out after ${elapsedMs / 1000}s`);
86
- this.jobId = jobId;
87
- this.elapsedMs = elapsedMs;
88
- this.name = 'JobTimeoutError';
89
- }
90
- }
91
- exports.JobTimeoutError = JobTimeoutError;
92
- class ValidationError extends OneShotError {
93
- constructor(message, field) {
94
- super(message);
95
- this.field = field;
96
- this.name = 'ValidationError';
97
- }
98
- }
99
- exports.ValidationError = ValidationError;
100
- class ContentBlockedError extends OneShotError {
101
- constructor(message, categories) {
102
- super(message);
103
- this.categories = categories;
104
- this.name = 'ContentBlockedError';
105
- }
106
- }
107
- exports.ContentBlockedError = ContentBlockedError;
108
- class EmergencyNumberError extends OneShotError {
109
- constructor(message, blockedNumber) {
110
- super(message);
111
- this.blockedNumber = blockedNumber;
112
- this.name = 'EmergencyNumberError';
113
- }
114
- }
115
- exports.EmergencyNumberError = EmergencyNumberError;
65
+ __exportStar(require("./types"), exports);
116
66
  // ============================================================================
117
67
  // OneShot SDK
118
68
  // ============================================================================
@@ -154,7 +104,7 @@ class OneShot {
154
104
  const walletProvider = new ethers_2.EthersWalletProvider(config.privateKey, rpcProvider);
155
105
  return new OneShot(config, walletProvider);
156
106
  }
157
- throw new ValidationError('Provide one of: privateKey, cdp, or walletProvider', 'config');
107
+ throw new errors_1.ValidationError('Provide one of: privateKey, cdp, or walletProvider', 'config');
158
108
  }
159
109
  /**
160
110
  * Sync constructor — works with privateKey (backwards compatible).
@@ -174,11 +124,11 @@ class OneShot {
174
124
  this.provider = new ethers_2.EthersWalletProvider(config.privateKey, this.rpcProvider);
175
125
  }
176
126
  else {
177
- throw new ValidationError('Provide privateKey or use OneShot.create() for CDP/custom wallets', 'config');
127
+ throw new errors_1.ValidationError('Provide privateKey or use OneShot.create() for CDP/custom wallets', 'config');
178
128
  }
179
129
  // Validate ETH mode requirements
180
130
  if (this._currency === 'ETH' && !this.provider.sendTransaction) {
181
- throw new ValidationError('ETH currency mode requires a wallet provider that supports sendTransaction', 'currency');
131
+ throw new errors_1.ValidationError('ETH currency mode requires a wallet provider that supports sendTransaction', 'currency');
182
132
  }
183
133
  if (this.debug) {
184
134
  this.log(`SDK initialized — chain=${CHAIN_ID} currency=${this._currency}`);
@@ -212,16 +162,41 @@ class OneShot {
212
162
  this.validate(options.to, 'to');
213
163
  this.validate(options.subject, 'subject');
214
164
  this.validate(options.body, 'body');
215
- const fromAddress = `${options.from_mailbox ?? 'agent'}@${options.from_domain ?? 'oneshotagent.com'}`;
165
+ // Rotation mode: when the caller passes neither from_domain nor
166
+ // from_mailbox, omit from_address entirely so the server picks from
167
+ // the agent's domain pool. The chosen address comes back on the
168
+ // quote response (`quote.from_address`) and we replay it on /send.
169
+ // When the caller pins either knob, we keep legacy behavior: build
170
+ // `${mailbox ?? 'agent'}@${domain ?? 'oneshotagent.com'}`.
171
+ const useRotation = !options.from_domain && !options.from_mailbox;
172
+ const fromAddress = useRotation
173
+ ? undefined
174
+ : `${options.from_mailbox ?? 'agent'}@${options.from_domain ?? 'oneshotagent.com'}`;
216
175
  const quote = await this.tool('email/quote', {
217
- from_address: fromAddress,
176
+ ...(fromAddress ? { from_address: fromAddress } : {}),
218
177
  to_address: options.to,
219
178
  subject: options.subject,
220
- body: options.body
179
+ body: options.body,
180
+ // Forward maxCost so the server-side X-Max-Cost-USDC header is set on
181
+ // the quote fetch (executeToolRequest destructures + threads it).
182
+ maxCost: options.maxCost,
221
183
  });
222
184
  this.log(`Email quote: $${quote.total_cost}`);
185
+ // Local fast-fail — matches the per-tool check on commerce/voice/sms/build/
186
+ // browser/compute. If the server-side header guard fired we'd never reach
187
+ // here; this catches the case where the server's enforcement is bypassed
188
+ // (e.g. a future API revision without the header check) or skipped (cap
189
+ // un-set so the server returned a 200 with the quote and the caller still
190
+ // wants the local guard to apply).
191
+ if (options.maxCost && parseFloat(quote.total_cost) > options.maxCost) {
192
+ throw new errors_1.OneShotError(`Quote $${quote.total_cost} exceeds maxCost $${options.maxCost}`);
193
+ }
194
+ const resolvedFromAddress = fromAddress ?? quote.from_address;
223
195
  const payload = {
224
- from_address: fromAddress,
196
+ // Server replays the locked address from the quote when from_address
197
+ // is absent, but sending it anyway is harmless and forward-compatible
198
+ // with future SDK versions that talk directly to /send without quoting.
199
+ ...(resolvedFromAddress ? { from_address: resolvedFromAddress } : {}),
225
200
  to_address: options.to,
226
201
  subject: options.subject,
227
202
  body: options.body,
@@ -237,6 +212,40 @@ class OneShot {
237
212
  }
238
213
  return this.executeToolRequest('/v1/tools/email/send', payload, quote.quote_id);
239
214
  }
215
+ /** List the caller's domain pool with warmup and rotation metadata. */
216
+ async listDomains() {
217
+ const response = await fetch(`${this.baseUrl}/v1/tools/email/domains`, {
218
+ headers: this.headers(),
219
+ });
220
+ if (!response.ok) {
221
+ throw new errors_1.ToolError('Failed to list domains', response.status, await response.text());
222
+ }
223
+ return response.json();
224
+ }
225
+ /** Take a domain out of rotation without releasing it. */
226
+ async pauseDomain(domain) {
227
+ this.validate(domain, 'domain');
228
+ const response = await fetch(`${this.baseUrl}/v1/tools/email/domains/${encodeURIComponent(domain)}/pause`, {
229
+ method: 'POST',
230
+ headers: this.headers(),
231
+ });
232
+ if (!response.ok) {
233
+ throw new errors_1.ToolError('Failed to pause domain', response.status, await response.text());
234
+ }
235
+ return response.json();
236
+ }
237
+ /** Put a paused domain back into rotation. */
238
+ async resumeDomain(domain) {
239
+ this.validate(domain, 'domain');
240
+ const response = await fetch(`${this.baseUrl}/v1/tools/email/domains/${encodeURIComponent(domain)}/resume`, {
241
+ method: 'POST',
242
+ headers: this.headers(),
243
+ });
244
+ if (!response.ok) {
245
+ throw new errors_1.ToolError('Failed to resume domain', response.status, await response.text());
246
+ }
247
+ return response.json();
248
+ }
240
249
  async research(options) {
241
250
  this.validate(options.topic, 'topic');
242
251
  return this.tool('research', { ...options });
@@ -246,14 +255,14 @@ class OneShot {
246
255
  }
247
256
  async enrichProfile(options) {
248
257
  if (!options.linkedin_url && !options.email && !options.name) {
249
- throw new ValidationError('At least one of linkedin_url, email, or name is required', 'identifier');
258
+ throw new errors_1.ValidationError('At least one of linkedin_url, email, or name is required', 'identifier');
250
259
  }
251
260
  return this.tool('enrich/profile', { ...options });
252
261
  }
253
262
  async findEmail(options) {
254
263
  this.validate(options.company_domain, 'company_domain');
255
264
  if (!options.full_name && !(options.first_name && options.last_name)) {
256
- throw new ValidationError('Either full_name or both first_name and last_name required', 'name');
265
+ throw new errors_1.ValidationError('Either full_name or both first_name and last_name required', 'name');
257
266
  }
258
267
  return this.tool('enrich/email', { ...options });
259
268
  }
@@ -263,13 +272,13 @@ class OneShot {
263
272
  }
264
273
  async deepResearchPerson(options) {
265
274
  if (!options.email && !options.social_media_url && !options.name) {
266
- throw new ValidationError('At least one of email, social_media_url, or name is required', 'identifier');
275
+ throw new errors_1.ValidationError('At least one of email, social_media_url, or name is required', 'identifier');
267
276
  }
268
277
  return this.tool('research/person', { ...options });
269
278
  }
270
279
  async socialProfiles(options) {
271
280
  if (!options.email && !options.social_media_url) {
272
- throw new ValidationError('At least one of email or social_media_url is required', 'identifier');
281
+ throw new errors_1.ValidationError('At least one of email or social_media_url is required', 'identifier');
273
282
  }
274
283
  return this.tool('research/social', { ...options });
275
284
  }
@@ -284,7 +293,7 @@ class OneShot {
284
293
  }
285
294
  async personInterests(options) {
286
295
  if (!options.email && !options.phone && !options.social_media_url) {
287
- throw new ValidationError('At least one of email, phone, or social_media_url is required', 'identifier');
296
+ throw new errors_1.ValidationError('At least one of email, phone, or social_media_url is required', 'identifier');
288
297
  }
289
298
  return this.tool('research/interests', { ...options });
290
299
  }
@@ -305,7 +314,7 @@ class OneShot {
305
314
  headers: this.headers()
306
315
  });
307
316
  if (!response.ok) {
308
- throw new ToolError('Failed to list inbox', response.status, await response.text());
317
+ throw new errors_1.ToolError('Failed to list inbox', response.status, await response.text());
309
318
  }
310
319
  return response.json();
311
320
  }
@@ -315,10 +324,10 @@ class OneShot {
315
324
  headers: this.headers()
316
325
  });
317
326
  if (response.status === 404) {
318
- throw new ToolError('Email not found', 404, 'Email not found');
327
+ throw new errors_1.ToolError('Email not found', 404, 'Email not found');
319
328
  }
320
329
  if (!response.ok) {
321
- throw new ToolError('Failed to get email', response.status, await response.text());
330
+ throw new errors_1.ToolError('Failed to get email', response.status, await response.text());
322
331
  }
323
332
  return response.json();
324
333
  }
@@ -333,14 +342,14 @@ class OneShot {
333
342
  variant_id: options.variant_id
334
343
  };
335
344
  // Commerce quotes can take up to 90s due to Rye API polling
336
- const quoteResp = await this.makeRequest('/v1/tools/commerce/buy', payload, undefined, undefined, options.signal, 120000);
345
+ const quoteResp = await this.makeRequest('/v1/tools/commerce/buy', payload, undefined, undefined, options.signal, 120000, this.maxCostHeader(options.maxCost));
337
346
  if (quoteResp.status !== 402) {
338
- throw new ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
347
+ throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
339
348
  }
340
349
  const quoteData = await quoteResp.json();
341
350
  this.log(`Commerce quote: $${quoteData.context.total} for "${quoteData.context.product_title}"`);
342
351
  if (options.maxCost && parseFloat(quoteData.context.total) > options.maxCost) {
343
- throw new OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
352
+ throw new errors_1.OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
344
353
  }
345
354
  const paymentInfo = {
346
355
  protocol: 'x402',
@@ -356,7 +365,7 @@ class OneShot {
356
365
  const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
357
366
  const buyResp = await this.makeRequest('/v1/tools/commerce/buy', payload, auth, quoteData.context.quote_id, options.signal, 60000);
358
367
  if (buyResp.status !== 202) {
359
- throw new ToolError('Commerce buy failed', buyResp.status, await buyResp.text());
368
+ throw new errors_1.ToolError('Commerce buy failed', buyResp.status, await buyResp.text());
360
369
  }
361
370
  const result = await buyResp.json();
362
371
  this.log(`Order submitted: ${result.request_id}`);
@@ -395,13 +404,13 @@ class OneShot {
395
404
  this.validate(options.target_number, 'target_number');
396
405
  // Check for empty arrays
397
406
  if (Array.isArray(options.target_number) && options.target_number.length === 0) {
398
- throw new ValidationError('target_number array cannot be empty', 'target_number');
407
+ throw new errors_1.ValidationError('target_number array cannot be empty', 'target_number');
399
408
  }
400
409
  if (options.objective.length < 10) {
401
- throw new ValidationError('Objective must be at least 10 characters', 'objective');
410
+ throw new errors_1.ValidationError('Objective must be at least 10 characters', 'objective');
402
411
  }
403
412
  if (options.max_duration_minutes !== undefined && (options.max_duration_minutes < 1 || options.max_duration_minutes > 30)) {
404
- throw new ValidationError('max_duration_minutes must be between 1 and 30', 'max_duration_minutes');
413
+ throw new errors_1.ValidationError('max_duration_minutes must be between 1 and 30', 'max_duration_minutes');
405
414
  }
406
415
  const payload = {
407
416
  objective: options.objective,
@@ -417,26 +426,25 @@ class OneShot {
417
426
  if (options.max_duration_minutes)
418
427
  payload.max_duration_minutes = options.max_duration_minutes;
419
428
  // Voice uses quote-to-pay flow (402 -> payment -> 202)
420
- const quoteResp = await this.makeRequest('/v1/tools/voice/call', payload, undefined, undefined, options.signal);
429
+ const quoteResp = await this.makeRequest('/v1/tools/voice/call', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
421
430
  // Handle error responses before expecting 402
422
431
  if (quoteResp.status === 400) {
423
432
  const errorData = await quoteResp.json();
424
433
  if (errorData.error === 'content_blocked') {
425
- throw new ContentBlockedError(errorData.message, errorData.categories || []);
434
+ throw new errors_1.ContentBlockedError(errorData.message, errorData.categories || []);
426
435
  }
427
436
  if (errorData.error === 'emergency_number_blocked') {
428
- throw new EmergencyNumberError(errorData.message, errorData.blocked_number || '');
437
+ throw new errors_1.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
429
438
  }
430
- throw new ValidationError(errorData.message || 'Invalid request', 'request');
439
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
431
440
  }
432
441
  if (quoteResp.status !== 402) {
433
- throw new ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
442
+ throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
434
443
  }
435
444
  const quoteData = await quoteResp.json();
436
445
  this.log(`Voice quote: $${quoteData.context.total} for ${quoteData.context.estimated_duration_minutes}min call`);
437
- this.log(`Objective summary: ${quoteData.context.objective_summary}`);
438
446
  if (options.maxCost && parseFloat(quoteData.context.total) > options.maxCost) {
439
- throw new OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
447
+ throw new errors_1.OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
440
448
  }
441
449
  const paymentInfo = {
442
450
  protocol: 'x402',
@@ -452,7 +460,7 @@ class OneShot {
452
460
  const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
453
461
  const callResp = await this.makeRequest('/v1/tools/voice/call', payload, auth, quoteData.context.quote_id, options.signal);
454
462
  if (callResp.status !== 202) {
455
- throw new ToolError('Voice call initiation failed', callResp.status, await callResp.text());
463
+ throw new errors_1.ToolError('Voice call initiation failed', callResp.status, await callResp.text());
456
464
  }
457
465
  const result = await callResp.json();
458
466
  this.log(`Call initiated: ${result.request_id}`);
@@ -478,17 +486,17 @@ class OneShot {
478
486
  this.validate(options.to_number, 'to_number');
479
487
  // Check for empty arrays
480
488
  if (Array.isArray(options.to_number) && options.to_number.length === 0) {
481
- throw new ValidationError('to_number array cannot be empty', 'to_number');
489
+ throw new errors_1.ValidationError('to_number array cannot be empty', 'to_number');
482
490
  }
483
491
  if (options.message.length < 1) {
484
- throw new ValidationError('Message is required', 'message');
492
+ throw new errors_1.ValidationError('Message is required', 'message');
485
493
  }
486
494
  if (options.message.length > 1600) {
487
- throw new ValidationError('Message must be 1600 characters or less', 'message');
495
+ throw new errors_1.ValidationError('Message must be 1600 characters or less', 'message');
488
496
  }
489
497
  const recipientCount = Array.isArray(options.to_number) ? options.to_number.length : 1;
490
498
  if (recipientCount > 10) {
491
- throw new ValidationError('Maximum 10 recipients allowed', 'to_number');
499
+ throw new errors_1.ValidationError('Maximum 10 recipients allowed', 'to_number');
492
500
  }
493
501
  const payload = {
494
502
  message: options.message,
@@ -498,25 +506,25 @@ class OneShot {
498
506
  wait: options.wait
499
507
  };
500
508
  // SMS uses quote-to-pay flow (402 -> payment -> 202)
501
- const quoteResp = await this.makeRequest('/v1/tools/sms/send', payload, undefined, undefined, options.signal);
509
+ const quoteResp = await this.makeRequest('/v1/tools/sms/send', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
502
510
  // Handle error responses before expecting 402
503
511
  if (quoteResp.status === 400) {
504
512
  const errorData = await quoteResp.json();
505
513
  if (errorData.error === 'content_blocked') {
506
- throw new ContentBlockedError(errorData.message, errorData.categories || []);
514
+ throw new errors_1.ContentBlockedError(errorData.message, errorData.categories || []);
507
515
  }
508
516
  if (errorData.error === 'emergency_number_blocked') {
509
- throw new EmergencyNumberError(errorData.message, errorData.blocked_number || '');
517
+ throw new errors_1.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
510
518
  }
511
- throw new ValidationError(errorData.message || 'Invalid request', 'request');
519
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
512
520
  }
513
521
  if (quoteResp.status !== 402) {
514
- throw new ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
522
+ throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
515
523
  }
516
524
  const quoteData = await quoteResp.json();
517
525
  this.log(`SMS quote: $${quoteData.context.total} for ${quoteData.context.segment_count} segment(s) to ${recipientCount} recipient(s)`);
518
526
  if (options.maxCost && parseFloat(quoteData.context.total) > options.maxCost) {
519
- throw new OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
527
+ throw new errors_1.OneShotError(`Quote $${quoteData.context.total} exceeds maxCost $${options.maxCost}`);
520
528
  }
521
529
  const paymentInfo = {
522
530
  protocol: 'x402',
@@ -532,7 +540,7 @@ class OneShot {
532
540
  const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
533
541
  const sendResp = await this.makeRequest('/v1/tools/sms/send', payload, auth, quoteData.context.quote_id, options.signal);
534
542
  if (sendResp.status !== 202) {
535
- throw new ToolError('SMS send failed', sendResp.status, await sendResp.text());
543
+ throw new errors_1.ToolError('SMS send failed', sendResp.status, await sendResp.text());
536
544
  }
537
545
  const result = await sendResp.json();
538
546
  this.log(`SMS queued: ${result.request_id}`);
@@ -562,7 +570,7 @@ class OneShot {
562
570
  this.validate(options.product?.name, 'product.name');
563
571
  this.validate(options.product?.description, 'product.description');
564
572
  if (options.product.description.length < 10) {
565
- throw new ValidationError('Product description must be at least 10 characters', 'product.description');
573
+ throw new errors_1.ValidationError('Product description must be at least 10 characters', 'product.description');
566
574
  }
567
575
  const payload = {
568
576
  type: options.type ?? 'saas',
@@ -586,19 +594,19 @@ class OneShot {
586
594
  if (options.build_id)
587
595
  payload.build_id = options.build_id;
588
596
  // Build uses quote-to-pay flow (402 -> payment -> 202)
589
- const quoteResp = await this.makeRequest('/v1/tools/build', payload, undefined, undefined, options.signal);
597
+ const quoteResp = await this.makeRequest('/v1/tools/build', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
590
598
  if (quoteResp.status === 400) {
591
599
  const errorData = await quoteResp.json();
592
- throw new ValidationError(errorData.message || 'Invalid request', 'request');
600
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
593
601
  }
594
602
  if (quoteResp.status !== 402) {
595
- throw new ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
603
+ throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
596
604
  }
597
605
  const quoteData = await quoteResp.json();
598
606
  this.log(`Build quote: $${quoteData.context.pricing.total} for "${quoteData.context.product_name}"`);
599
607
  this.log(`Type: ${quoteData.context.analysis.inferred_type}, Sections: ${quoteData.context.analysis.estimated_sections}`);
600
608
  if (options.maxCost && parseFloat(quoteData.context.pricing.total) > options.maxCost) {
601
- throw new OneShotError(`Quote $${quoteData.context.pricing.total} exceeds maxCost $${options.maxCost}`);
609
+ throw new errors_1.OneShotError(`Quote $${quoteData.context.pricing.total} exceeds maxCost $${options.maxCost}`);
602
610
  }
603
611
  const paymentInfo = {
604
612
  protocol: 'x402',
@@ -614,7 +622,7 @@ class OneShot {
614
622
  const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
615
623
  const buildResp = await this.makeRequest('/v1/tools/build', payload, auth, quoteData.context.quote_id, options.signal);
616
624
  if (buildResp.status !== 202) {
617
- throw new ToolError('Build initiation failed', buildResp.status, await buildResp.text());
625
+ throw new errors_1.ToolError('Build initiation failed', buildResp.status, await buildResp.text());
618
626
  }
619
627
  const result = await buildResp.json();
620
628
  this.log(`Build initiated: ${result.request_id}`);
@@ -638,10 +646,10 @@ class OneShot {
638
646
  async browser(options) {
639
647
  this.validate(options.task, 'task');
640
648
  if (options.task.length < 10) {
641
- throw new ValidationError('Task must be at least 10 characters', 'task');
649
+ throw new errors_1.ValidationError('Task must be at least 10 characters', 'task');
642
650
  }
643
651
  if (options.max_steps !== undefined && (options.max_steps < 1 || options.max_steps > 100)) {
644
- throw new ValidationError('max_steps must be between 1 and 100', 'max_steps');
652
+ throw new errors_1.ValidationError('max_steps must be between 1 and 100', 'max_steps');
645
653
  }
646
654
  const payload = {
647
655
  task: options.task,
@@ -664,18 +672,18 @@ class OneShot {
664
672
  if (options.max_steps)
665
673
  payload.max_steps = options.max_steps;
666
674
  // Browser uses quote-to-pay flow (402 -> payment -> 202)
667
- const quoteResp = await this.makeRequest('/v1/tools/browser', payload, undefined, undefined, options.signal);
675
+ const quoteResp = await this.makeRequest('/v1/tools/browser', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
668
676
  if (quoteResp.status === 400) {
669
677
  const errorData = await quoteResp.json();
670
- throw new ValidationError(errorData.message || 'Invalid request', 'request');
678
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
671
679
  }
672
680
  if (quoteResp.status !== 402) {
673
- throw new ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
681
+ throw new errors_1.ToolError('Expected 402 for quote', quoteResp.status, await quoteResp.text());
674
682
  }
675
683
  const quoteData = await quoteResp.json();
676
684
  this.log(`Browser quote: $${quoteData.context.estimated_cost} for ~${quoteData.context.estimated_steps} steps`);
677
685
  if (options.maxCost && parseFloat(quoteData.context.estimated_cost) > options.maxCost) {
678
- throw new OneShotError(`Quote $${quoteData.context.estimated_cost} exceeds maxCost $${options.maxCost}`);
686
+ throw new errors_1.OneShotError(`Quote $${quoteData.context.estimated_cost} exceeds maxCost $${options.maxCost}`);
679
687
  }
680
688
  const paymentInfo = {
681
689
  protocol: 'x402',
@@ -691,7 +699,7 @@ class OneShot {
691
699
  const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
692
700
  const execResp = await this.makeRequest('/v1/tools/browser', payload, auth, quoteData.context.quote_id, options.signal);
693
701
  if (execResp.status !== 202) {
694
- throw new ToolError('Browser task initiation failed', execResp.status, await execResp.text());
702
+ throw new errors_1.ToolError('Browser task initiation failed', execResp.status, await execResp.text());
695
703
  }
696
704
  const result = await execResp.json();
697
705
  this.log(`Browser task initiated: ${result.request_id}`);
@@ -717,7 +725,7 @@ class OneShot {
717
725
  body: JSON.stringify({ name }),
718
726
  });
719
727
  if (!response.ok) {
720
- throw new ToolError('Failed to create browser profile', response.status, await response.text());
728
+ throw new errors_1.ToolError('Failed to create browser profile', response.status, await response.text());
721
729
  }
722
730
  return response.json();
723
731
  }
@@ -737,7 +745,7 @@ class OneShot {
737
745
  headers: this.headers(),
738
746
  });
739
747
  if (!response.ok) {
740
- throw new ToolError('Failed to list browser profiles', response.status, await response.text());
748
+ throw new errors_1.ToolError('Failed to list browser profiles', response.status, await response.text());
741
749
  }
742
750
  const data = await response.json();
743
751
  return data.profiles;
@@ -757,7 +765,7 @@ class OneShot {
757
765
  headers: this.headers(),
758
766
  });
759
767
  if (!response.ok) {
760
- throw new ToolError('Failed to delete browser profile', response.status, await response.text());
768
+ throw new errors_1.ToolError('Failed to delete browser profile', response.status, await response.text());
761
769
  }
762
770
  }
763
771
  /**
@@ -806,7 +814,7 @@ class OneShot {
806
814
  headers: this.headers()
807
815
  });
808
816
  if (!response.ok) {
809
- throw new ToolError('Failed to list SMS inbox', response.status, await response.text());
817
+ throw new errors_1.ToolError('Failed to list SMS inbox', response.status, await response.text());
810
818
  }
811
819
  return response.json();
812
820
  }
@@ -825,10 +833,10 @@ class OneShot {
825
833
  headers: this.headers()
826
834
  });
827
835
  if (response.status === 404) {
828
- throw new ToolError('SMS message not found', 404, 'Message not found');
836
+ throw new errors_1.ToolError('SMS message not found', 404, 'Message not found');
829
837
  }
830
838
  if (!response.ok) {
831
- throw new ToolError('Failed to get SMS message', response.status, await response.text());
839
+ throw new errors_1.ToolError('Failed to get SMS message', response.status, await response.text());
832
840
  }
833
841
  return response.json();
834
842
  }
@@ -855,7 +863,7 @@ class OneShot {
855
863
  headers: this.headers()
856
864
  });
857
865
  if (!response.ok) {
858
- throw new ToolError('Failed to list notifications', response.status, await response.text());
866
+ throw new errors_1.ToolError('Failed to list notifications', response.status, await response.text());
859
867
  }
860
868
  return response.json();
861
869
  }
@@ -874,10 +882,10 @@ class OneShot {
874
882
  headers: this.headers()
875
883
  });
876
884
  if (response.status === 404) {
877
- throw new ToolError('Notification not found', 404, 'Notification not found');
885
+ throw new errors_1.ToolError('Notification not found', 404, 'Notification not found');
878
886
  }
879
887
  if (!response.ok) {
880
- throw new ToolError('Failed to mark notification as read', response.status, await response.text());
888
+ throw new errors_1.ToolError('Failed to mark notification as read', response.status, await response.text());
881
889
  }
882
890
  }
883
891
  async getUnifiedBalance() {
@@ -885,7 +893,7 @@ class OneShot {
885
893
  headers: this.headers()
886
894
  });
887
895
  if (!response.ok) {
888
- throw new ToolError('Failed to fetch balance', response.status, await response.text());
896
+ throw new errors_1.ToolError('Failed to fetch balance', response.status, await response.text());
889
897
  }
890
898
  return response.json();
891
899
  }
@@ -943,21 +951,21 @@ class OneShot {
943
951
  if (options.schedule)
944
952
  payload.schedule = options.schedule;
945
953
  // First call: get quote (402)
946
- const quoteResp = await this.makeRequest('/v1/compute', payload, undefined, undefined, options.signal);
954
+ const quoteResp = await this.makeRequest('/v1/compute', payload, undefined, undefined, options.signal, undefined, this.maxCostHeader(options.maxCost));
947
955
  if (quoteResp.status === 400) {
948
956
  const errorData = await quoteResp.json();
949
957
  if (errorData.error === 'content_blocked') {
950
- throw new ContentBlockedError(errorData.message, []);
958
+ throw new errors_1.ContentBlockedError(errorData.message, []);
951
959
  }
952
- throw new ValidationError(errorData.message || 'Invalid request', 'request');
960
+ throw new errors_1.ValidationError(errorData.message || 'Invalid request', 'request');
953
961
  }
954
962
  if (quoteResp.status !== 402) {
955
- throw new ToolError('Expected 402 for compute quote', quoteResp.status, await quoteResp.text());
963
+ throw new errors_1.ToolError('Expected 402 for compute quote', quoteResp.status, await quoteResp.text());
956
964
  }
957
965
  const quoteData = await quoteResp.json();
958
966
  this.log(`Compute quote: $${quoteData.context.total_budget} — ${quoteData.context.objective_summary}`);
959
967
  if (options.maxCost && parseFloat(quoteData.context.total_budget) > options.maxCost) {
960
- throw new OneShotError(`Quote $${quoteData.context.total_budget} exceeds maxCost $${options.maxCost}`);
968
+ throw new errors_1.OneShotError(`Quote $${quoteData.context.total_budget} exceeds maxCost $${options.maxCost}`);
961
969
  }
962
970
  const paymentInfo = {
963
971
  protocol: 'x402',
@@ -973,7 +981,7 @@ class OneShot {
973
981
  const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
974
982
  const createResp = await this.makeRequest('/v1/compute', payload, auth, quoteData.context.quote_id, options.signal);
975
983
  if (createResp.status !== 202) {
976
- throw new ToolError('Compute goal creation failed', createResp.status, await createResp.text());
984
+ throw new errors_1.ToolError('Compute goal creation failed', createResp.status, await createResp.text());
977
985
  }
978
986
  return createResp.json();
979
987
  }
@@ -992,10 +1000,10 @@ class OneShot {
992
1000
  headers: this.headers()
993
1001
  });
994
1002
  if (response.status === 404) {
995
- throw new ToolError('Goal not found', 404, 'Goal not found');
1003
+ throw new errors_1.ToolError('Goal not found', 404, 'Goal not found');
996
1004
  }
997
1005
  if (!response.ok) {
998
- throw new ToolError('Failed to get compute goal', response.status, await response.text());
1006
+ throw new errors_1.ToolError('Failed to get compute goal', response.status, await response.text());
999
1007
  }
1000
1008
  const json = await response.json();
1001
1009
  return json.data;
@@ -1017,7 +1025,7 @@ class OneShot {
1017
1025
  headers: this.headers()
1018
1026
  });
1019
1027
  if (!response.ok) {
1020
- throw new ToolError('Failed to get compute tasks', response.status, await response.text());
1028
+ throw new errors_1.ToolError('Failed to get compute tasks', response.status, await response.text());
1021
1029
  }
1022
1030
  const json = await response.json();
1023
1031
  return json.data;
@@ -1037,10 +1045,10 @@ class OneShot {
1037
1045
  headers: this.headers()
1038
1046
  });
1039
1047
  if (response.status === 404) {
1040
- throw new ToolError('Budget not found', 404, 'Budget not found for this goal');
1048
+ throw new errors_1.ToolError('Budget not found', 404, 'Budget not found for this goal');
1041
1049
  }
1042
1050
  if (!response.ok) {
1043
- throw new ToolError('Failed to get compute budget', response.status, await response.text());
1051
+ throw new errors_1.ToolError('Failed to get compute budget', response.status, await response.text());
1044
1052
  }
1045
1053
  const json = await response.json();
1046
1054
  return json.data;
@@ -1062,10 +1070,10 @@ class OneShot {
1062
1070
  body: JSON.stringify({ reason })
1063
1071
  });
1064
1072
  if (response.status === 404) {
1065
- throw new ToolError('Goal not found', 404, 'Goal not found');
1073
+ throw new errors_1.ToolError('Goal not found', 404, 'Goal not found');
1066
1074
  }
1067
1075
  if (!response.ok) {
1068
- throw new ToolError('Failed to cancel compute goal', response.status, await response.text());
1076
+ throw new errors_1.ToolError('Failed to cancel compute goal', response.status, await response.text());
1069
1077
  }
1070
1078
  const json = await response.json();
1071
1079
  return json.data;
@@ -1091,10 +1099,10 @@ class OneShot {
1091
1099
  body: JSON.stringify(input)
1092
1100
  });
1093
1101
  if (response.status === 404) {
1094
- throw new ToolError('Goal or task not found', 404, await response.text());
1102
+ throw new errors_1.ToolError('Goal or task not found', 404, await response.text());
1095
1103
  }
1096
1104
  if (!response.ok) {
1097
- throw new ToolError('Failed to respond to compute task', response.status, await response.text());
1105
+ throw new errors_1.ToolError('Failed to respond to compute task', response.status, await response.text());
1098
1106
  }
1099
1107
  const json = await response.json();
1100
1108
  return json.data;
@@ -1115,7 +1123,7 @@ class OneShot {
1115
1123
  body: JSON.stringify({ reason })
1116
1124
  });
1117
1125
  if (!response.ok) {
1118
- throw new ToolError('Failed to pause compute goal', response.status, await response.text());
1126
+ throw new errors_1.ToolError('Failed to pause compute goal', response.status, await response.text());
1119
1127
  }
1120
1128
  const json = await response.json();
1121
1129
  return json.data;
@@ -1137,13 +1145,16 @@ class OneShot {
1137
1145
  body: JSON.stringify({})
1138
1146
  });
1139
1147
  if (!response.ok) {
1140
- throw new ToolError('Failed to resume compute goal', response.status, await response.text());
1148
+ throw new errors_1.ToolError('Failed to resume compute goal', response.status, await response.text());
1141
1149
  }
1142
1150
  const json = await response.json();
1143
1151
  return json.data;
1144
1152
  }
1145
1153
  /**
1146
- * Top up budget for a recurring compute goal
1154
+ * Top up budget for a recurring compute goal.
1155
+ *
1156
+ * Uses the quote-then-pay flow: the first call gets a 402 with the top-up price,
1157
+ * the second call (with payment) credits the budget. The amount IS the price.
1147
1158
  *
1148
1159
  * @example
1149
1160
  * ```typescript
@@ -1154,17 +1165,33 @@ class OneShot {
1154
1165
  async fundComputeGoal(goalId, amount) {
1155
1166
  this.validate(goalId, 'goalId');
1156
1167
  if (!amount || amount <= 0) {
1157
- throw new ValidationError('amount must be a positive number', 'amount');
1168
+ throw new errors_1.ValidationError('amount must be a positive number', 'amount');
1158
1169
  }
1159
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/fund`, {
1160
- method: 'POST',
1161
- headers: { 'Content-Type': 'application/json', ...this.headers() },
1162
- body: JSON.stringify({ amount })
1163
- });
1164
- if (!response.ok) {
1165
- throw new ToolError('Failed to fund compute goal', response.status, await response.text());
1170
+ const path = `/v1/compute/${goalId}/fund`;
1171
+ const payload = { amount };
1172
+ // First call: get quote (402)
1173
+ const quoteResp = await this.makeRequest(path, payload);
1174
+ if (quoteResp.status !== 402) {
1175
+ throw new errors_1.ToolError('Expected 402 for compute fund quote', quoteResp.status, await quoteResp.text());
1166
1176
  }
1167
- const json = await response.json();
1177
+ const quoteData = await quoteResp.json();
1178
+ this.log(`Compute fund quote: $${quoteData.payment_request.amount} to top up ${goalId}`);
1179
+ const paymentInfo = {
1180
+ protocol: 'x402',
1181
+ network: `eip155:${quoteData.payment_request.chain_id}`,
1182
+ payTo: quoteData.payment_request.recipient,
1183
+ amount: quoteData.payment_request.amount,
1184
+ currency: 'USD',
1185
+ facilitator_url: this.baseUrl,
1186
+ token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
1187
+ };
1188
+ const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, path, payload, quoteData.context.quote_id);
1189
+ const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
1190
+ const fundResp = await this.makeRequest(path, payload, auth, quoteData.context.quote_id);
1191
+ if (!fundResp.ok) {
1192
+ throw new errors_1.ToolError('Failed to fund compute goal', fundResp.status, await fundResp.text());
1193
+ }
1194
+ const json = await fundResp.json();
1168
1195
  return json.data;
1169
1196
  }
1170
1197
  // ---------------------------------------------------------------------------
@@ -1191,7 +1218,7 @@ class OneShot {
1191
1218
  headers: this.headers()
1192
1219
  });
1193
1220
  if (!response.ok) {
1194
- throw new ToolError('Failed to get spend breakdown', response.status, await response.text());
1221
+ throw new errors_1.ToolError('Failed to get spend breakdown', response.status, await response.text());
1195
1222
  }
1196
1223
  return response.json();
1197
1224
  }
@@ -1213,7 +1240,7 @@ class OneShot {
1213
1240
  headers: this.headers()
1214
1241
  });
1215
1242
  if (!response.ok) {
1216
- throw new ToolError('Failed to get RoCS', response.status, await response.text());
1243
+ throw new errors_1.ToolError('Failed to get RoCS', response.status, await response.text());
1217
1244
  }
1218
1245
  return response.json();
1219
1246
  }
@@ -1241,7 +1268,7 @@ class OneShot {
1241
1268
  headers: this.headers()
1242
1269
  });
1243
1270
  if (!response.ok) {
1244
- throw new ToolError('Failed to list receipts', response.status, await response.text());
1271
+ throw new errors_1.ToolError('Failed to list receipts', response.status, await response.text());
1245
1272
  }
1246
1273
  return response.json();
1247
1274
  }
@@ -1265,10 +1292,10 @@ class OneShot {
1265
1292
  body: JSON.stringify(valueTag)
1266
1293
  });
1267
1294
  if (response.status === 404) {
1268
- throw new ToolError('Receipt not found', 404, 'Receipt not found or not owned by this agent');
1295
+ throw new errors_1.ToolError('Receipt not found', 404, 'Receipt not found or not owned by this agent');
1269
1296
  }
1270
1297
  if (!response.ok) {
1271
- throw new ToolError('Failed to tag receipt value', response.status, await response.text());
1298
+ throw new errors_1.ToolError('Failed to tag receipt value', response.status, await response.text());
1272
1299
  }
1273
1300
  }
1274
1301
  // ---------------------------------------------------------------------------
@@ -1280,7 +1307,7 @@ class OneShot {
1280
1307
  }
1281
1308
  validate(value, field) {
1282
1309
  if (!value)
1283
- throw new ValidationError(`${field} is required`, field);
1310
+ throw new errors_1.ValidationError(`${field} is required`, field);
1284
1311
  }
1285
1312
  headers() {
1286
1313
  return {
@@ -1288,8 +1315,21 @@ class OneShot {
1288
1315
  'X-OneShot-SDK-Version': SDK_VERSION
1289
1316
  };
1290
1317
  }
1318
+ /**
1319
+ * Header that asks the API to reject the request when the computed quote
1320
+ * exceeds the caller-supplied cap (commit 8f328a7). The SDK still does its
1321
+ * own local `quote.total > maxCost` check after the 402 returns — this is
1322
+ * a server-side enforcement layer so non-SDK callers (MCP server, custom
1323
+ * integrations) can't ignore the cap.
1324
+ */
1325
+ maxCostHeader(maxCost) {
1326
+ if (!maxCost || maxCost <= 0)
1327
+ return undefined;
1328
+ return { 'X-Max-Cost-USDC': maxCost.toString() };
1329
+ }
1291
1330
  async executeToolRequest(endpoint, options, quoteId) {
1292
- const { signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, ...payload } = options;
1331
+ const { signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, maxCost, ...payload } = options;
1332
+ const extraHeaders = this.maxCostHeader(maxCost);
1293
1333
  // Validate memo
1294
1334
  if (payload.memo !== undefined) {
1295
1335
  if (typeof payload.memo !== 'string' || payload.memo.trim().length === 0) {
@@ -1316,9 +1356,9 @@ class OneShot {
1316
1356
  }
1317
1357
  }
1318
1358
  if (signal?.aborted) {
1319
- throw new OneShotError('Operation cancelled');
1359
+ throw new errors_1.OneShotError('Operation cancelled');
1320
1360
  }
1321
- let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal);
1361
+ let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
1322
1362
  // Handle 402 Payment Required
1323
1363
  if (response.status === 402) {
1324
1364
  // Parse x402 v2 PaymentRequired from PAYMENT-REQUIRED header
@@ -1338,10 +1378,10 @@ class OneShot {
1338
1378
  this.log(`Payment required: ${paymentInfo.amount} USDC`);
1339
1379
  this.checkAbortBeforePayment(signal);
1340
1380
  const auth = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
1341
- response = await this.makeRequest(endpoint, payload, auth, quoteId, signal);
1381
+ response = await this.makeRequest(endpoint, payload, auth, quoteId, signal, undefined, extraHeaders);
1342
1382
  }
1343
1383
  if (!response.ok) {
1344
- throw new ToolError('Tool request failed', response.status, await response.text());
1384
+ throw new errors_1.ToolError('Tool request failed', response.status, await response.text());
1345
1385
  }
1346
1386
  const result = await response.json();
1347
1387
  // Handle async jobs
@@ -1364,12 +1404,12 @@ class OneShot {
1364
1404
  this.log('WebSocket unavailable, falling back to HTTP polling');
1365
1405
  result = await this.pollJobHttp(requestId, timeoutSec, signal, onStatusUpdate);
1366
1406
  }
1367
- // Optional second phase: keep polling for Apollo phone-reveal callbacks.
1407
+ // Optional second phase: keep polling for the async phone-reveal webhook.
1368
1408
  // Only kicks in when the caller explicitly opts in AND the result still
1369
- // has phones_pending=true (set by the worker when Apollo enrichment is
1370
- // pending an async webhook). This is opt-in so existing callers see no
1371
- // behavior change — the WebSocket/HTTP polls return as soon as the worker
1372
- // sets status=completed, with phones=null.
1409
+ // has phones_pending=true (set by the worker when the upstream enrichment
1410
+ // has a pending async phone callback). This is opt-in so existing callers
1411
+ // see no behavior change — the WebSocket/HTTP polls return as soon as the
1412
+ // worker sets status=completed, with phones=null.
1373
1413
  //
1374
1414
  // Pass the existing result as the initial fallback: if the polling GETs
1375
1415
  // hit a transient error before any successful refresh, we return what we
@@ -1380,9 +1420,9 @@ class OneShot {
1380
1420
  return result;
1381
1421
  }
1382
1422
  /**
1383
- * Detects whether a job result is still waiting for Apollo's async phone
1384
- * reveal. The worker sets `result.phones_pending=true` when an enrichment
1385
- * was Apollo-sourced and Apollo's webhook URL is configured. The webhook
1423
+ * Detects whether a job result is still waiting for an async phone reveal.
1424
+ * The worker sets `result.phones_pending=true` when the upstream enrichment
1425
+ * has a pending async webhook and the webhook URL is configured; the webhook
1386
1426
  * handler flips it to false (or removes it) once phones arrive.
1387
1427
  *
1388
1428
  * Tolerates two shapes: the unwrapped result `{phones_pending}` and the
@@ -1400,11 +1440,12 @@ class OneShot {
1400
1440
  return false;
1401
1441
  }
1402
1442
  /**
1403
- * Slow poll loop that waits for Apollo phone-reveal callbacks to arrive
1404
- * via the webhook handler (which UPDATEs jobs.result_data.result.phones).
1405
- * Polls GET /v1/requests/{id} every 5s — Apollo says "several minutes" so
1406
- * a tight loop wastes API calls. Returns the latest snapshot whether or
1407
- * not phones arrived (consumer can re-check `phones_pending`).
1443
+ * Slow poll loop that waits for async phone-reveal callbacks to arrive via
1444
+ * the webhook handler (which UPDATEs jobs.result_data.result.phones). Polls
1445
+ * GET /v1/requests/{id} every 5s — the upstream provider can take several
1446
+ * minutes to deliver, so a tight loop just wastes API calls. Returns the
1447
+ * latest snapshot whether or not phones arrived (consumer can re-check
1448
+ * `phones_pending`).
1408
1449
  */
1409
1450
  async _pollForPhones(requestId, timeoutSec, signal, initialResult) {
1410
1451
  const deadline = Date.now() + timeoutSec * 1000;
@@ -1414,7 +1455,7 @@ class OneShot {
1414
1455
  let lastResult = initialResult;
1415
1456
  while (Date.now() < deadline) {
1416
1457
  if (signal?.aborted)
1417
- throw new OneShotError('Operation cancelled');
1458
+ throw new errors_1.OneShotError('Operation cancelled');
1418
1459
  try {
1419
1460
  const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
1420
1461
  headers: this.headers(),
@@ -1425,7 +1466,7 @@ class OneShot {
1425
1466
  // whatever we last had so consumers don't lose the sync result.
1426
1467
  if (lastResult !== undefined)
1427
1468
  return lastResult;
1428
- throw new ToolError('Failed to check job status', resp.status, await resp.text());
1469
+ throw new errors_1.ToolError('Failed to check job status', resp.status, await resp.text());
1429
1470
  }
1430
1471
  const job = await resp.json();
1431
1472
  lastResult = (job.result ?? job);
@@ -1434,7 +1475,7 @@ class OneShot {
1434
1475
  }
1435
1476
  }
1436
1477
  catch (err) {
1437
- if (err instanceof OneShotError)
1478
+ if (err instanceof errors_1.OneShotError)
1438
1479
  throw err;
1439
1480
  if (lastResult !== undefined)
1440
1481
  return lastResult;
@@ -1497,7 +1538,7 @@ class OneShot {
1497
1538
  signal.addEventListener('abort', () => {
1498
1539
  settle(() => {
1499
1540
  cleanup();
1500
- reject(new OneShotError('Operation cancelled'));
1541
+ reject(new errors_1.OneShotError('Operation cancelled'));
1501
1542
  });
1502
1543
  }, { once: true });
1503
1544
  }
@@ -1538,7 +1579,7 @@ class OneShot {
1538
1579
  else if (msg.status === 'failed') {
1539
1580
  settle(() => {
1540
1581
  cleanup();
1541
- reject(new JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown')));
1582
+ reject(new errors_1.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown')));
1542
1583
  });
1543
1584
  }
1544
1585
  else {
@@ -1574,14 +1615,14 @@ class OneShot {
1574
1615
  const maxRetries = 3;
1575
1616
  while (Date.now() - startTime < maxWaitMs) {
1576
1617
  if (signal?.aborted)
1577
- throw new OneShotError('Operation cancelled');
1618
+ throw new errors_1.OneShotError('Operation cancelled');
1578
1619
  try {
1579
1620
  const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
1580
1621
  headers: this.headers(),
1581
1622
  signal
1582
1623
  });
1583
1624
  if (!resp.ok) {
1584
- throw new ToolError('Failed to check job status', resp.status, await resp.text());
1625
+ throw new errors_1.ToolError('Failed to check job status', resp.status, await resp.text());
1585
1626
  }
1586
1627
  const job = await resp.json();
1587
1628
  if (job.status === 'completed') {
@@ -1594,42 +1635,43 @@ class OneShot {
1594
1635
  return result;
1595
1636
  }
1596
1637
  if (job.status === 'failed') {
1597
- throw new JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'));
1638
+ throw new errors_1.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'));
1598
1639
  }
1599
1640
  onStatusUpdate?.(job.status, requestId);
1600
1641
  retries = 0;
1601
1642
  await this.sleep(pollInterval, signal);
1602
1643
  }
1603
1644
  catch (err) {
1604
- if (err instanceof OneShotError)
1645
+ if (err instanceof errors_1.OneShotError)
1605
1646
  throw err;
1606
1647
  if (++retries > maxRetries) {
1607
- throw new OneShotError(`Polling failed after ${maxRetries} retries: ${err}`);
1648
+ throw new errors_1.OneShotError(`Polling failed after ${maxRetries} retries: ${err}`);
1608
1649
  }
1609
1650
  const backoff = pollInterval * Math.pow(2, retries - 1);
1610
1651
  this.log(`Retry ${retries}/${maxRetries} in ${backoff}ms`);
1611
1652
  await this.sleep(backoff, signal);
1612
1653
  }
1613
1654
  }
1614
- throw new JobTimeoutError(requestId, Date.now() - startTime);
1655
+ throw new errors_1.JobTimeoutError(requestId, Date.now() - startTime);
1615
1656
  }
1616
1657
  sleep(ms, signal) {
1617
1658
  return new Promise((resolve, reject) => {
1618
1659
  if (signal?.aborted) {
1619
- return reject(new OneShotError('Operation cancelled'));
1660
+ return reject(new errors_1.OneShotError('Operation cancelled'));
1620
1661
  }
1621
1662
  const timer = setTimeout(resolve, ms);
1622
1663
  const onAbort = () => {
1623
1664
  clearTimeout(timer);
1624
- reject(new OneShotError('Operation cancelled'));
1665
+ reject(new errors_1.OneShotError('Operation cancelled'));
1625
1666
  };
1626
1667
  signal?.addEventListener('abort', onAbort, { once: true });
1627
1668
  });
1628
1669
  }
1629
- async makeRequest(endpoint, data, payment, quoteId, signal, timeoutMs) {
1670
+ async makeRequest(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders) {
1630
1671
  const headers = {
1631
1672
  'Content-Type': 'application/json',
1632
- ...this.headers()
1673
+ ...this.headers(),
1674
+ ...(extraHeaders ?? {})
1633
1675
  };
1634
1676
  if (payment) {
1635
1677
  const paymentJson = JSON.stringify(payment);
@@ -1663,7 +1705,7 @@ class OneShot {
1663
1705
  }
1664
1706
  checkAbortBeforePayment(signal) {
1665
1707
  if (signal?.aborted) {
1666
- throw new OneShotError('Operation cancelled before payment');
1708
+ throw new errors_1.OneShotError('Operation cancelled before payment');
1667
1709
  }
1668
1710
  }
1669
1711
  /**