@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/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.62.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.log(`Email: sending against confirmed quote ${confirmedQuote}`);
344
- return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress), confirmedQuote);
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
- /** The /email/send body for `email()`: the same whether it follows a fresh quote or a confirmed one. */
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: `not_connected` (invite them first) and `rate_limited` mean
1144
- * "defer", not "give up".
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 response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/tasks`, {
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 recurring compute goal
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 recurring compute goal
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
- if (!resp.ok && !(extraHeaders?.['Idempotency-Key'] && resp.status >= 500))
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) {