@drawbridge/drawbridge-utils 0.0.134 → 0.0.136

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.
@@ -390,8 +390,14 @@ var authToken = async ({
390
390
  });
391
391
  const body = await response.json().catch(() => ({}));
392
392
  if (!response.ok) {
393
- throw new Error(
394
- renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
393
+ throw Object.assign(
394
+ new Error(
395
+ renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
396
+ ),
397
+ {
398
+ status: response.status,
399
+ ...renewing && { code: "grant_refused" }
400
+ }
395
401
  );
396
402
  }
397
403
  if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");
@@ -447,7 +453,10 @@ var accessToken = async ({
447
453
  }
448
454
  if (!force && !isStale(settings, now)) return settings.accessToken;
449
455
  if (!settings.refreshToken) {
450
- throw new Error("This connection has expired and cannot be renewed automatically. Reconnect it.");
456
+ throw Object.assign(
457
+ new Error("This connection has expired and cannot be renewed automatically. Reconnect it."),
458
+ { code: "grant_refused" }
459
+ );
451
460
  }
452
461
  const minted = await manifest.hooks.auth.token({
453
462
  clientId,
@@ -1042,6 +1051,74 @@ var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fi
1042
1051
  </defs>
1043
1052
  </svg>`;
1044
1053
 
1054
+ // lib/transactions.js
1055
+ import { currentTraceId } from "@drawbridge/drawbridge-telemetry";
1056
+
1057
+ // lib/billing.js
1058
+ import { createLogger } from "@drawbridge/drawbridge-telemetry";
1059
+ var logger = createLogger();
1060
+ var MARKUP = 1.3;
1061
+ var cost = {
1062
+ // gemini-3.5-flash — verified against Google's pricing page 2026-07-09:
1063
+ // $0.15 cached / $1.50 input / $9.00 output per 1M tokens (thinking billed at
1064
+ // output). ~3.6x the retired 2.5-flash output rate.
1065
+ "gemini-3.5-flash": {
1066
+ cached: 15,
1067
+ input: 150,
1068
+ output: 900
1069
+ },
1070
+ // gemini-3.5-flash-lite — verified against Google's pricing page 2026-08-20:
1071
+ // $0.03 cached / $0.30 input / $2.50 output per 1M tokens (thinking billed at
1072
+ // output). A fifth of flash on input, ~a quarter on output. Growth's assistant
1073
+ // ranks the feed on this tier — one call per page — so its rows were the
1074
+ // unpriced ones until now.
1075
+ "gemini-3.5-flash-lite": {
1076
+ cached: 3,
1077
+ input: 30,
1078
+ output: 250
1079
+ },
1080
+ "gemini-2.5-flash": {
1081
+ cached: 3,
1082
+ input: 30,
1083
+ output: 250
1084
+ },
1085
+ "gemini-2.5-flash-image": {
1086
+ cached: 3,
1087
+ input: 30,
1088
+ output: 3e3
1089
+ },
1090
+ // gemini-3-pro-image-preview — verified against Google's pricing page
1091
+ // 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
1092
+ // tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
1093
+ // 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
1094
+ // that reproduces the per-image price from the tokens usageMetadata
1095
+ // reports. Growth's hero generation runs this model today.
1096
+ "gemini-3-pro-image-preview": {
1097
+ cached: 20,
1098
+ input: 200,
1099
+ output: 12e3
1100
+ }
1101
+ };
1102
+ var toolCost = {
1103
+ search: 3.5
1104
+ };
1105
+ var toolPricing = Object.fromEntries(
1106
+ Object.entries(toolCost).map(([tool, value]) => [
1107
+ tool,
1108
+ Math.ceil(value * MARKUP)
1109
+ ])
1110
+ );
1111
+ var pricing = Object.fromEntries(
1112
+ Object.entries(cost).map(([model, rates]) => [
1113
+ model,
1114
+ {
1115
+ cached: Math.round(rates.cached * MARKUP),
1116
+ input: Math.round(rates.input * MARKUP),
1117
+ output: Math.round(rates.output * MARKUP)
1118
+ }
1119
+ ])
1120
+ );
1121
+
1045
1122
  // lib/features.js
1046
1123
  var page = {
1047
1124
  qrcode: {
@@ -1504,74 +1581,6 @@ var conversionRate = (subscription) => {
1504
1581
  return ((_a = resolvePlan(subscription)) == null ? void 0 : _a.conversion) ?? free.conversion;
1505
1582
  };
1506
1583
 
1507
- // lib/transactions.js
1508
- import { currentTraceId } from "@drawbridge/drawbridge-telemetry";
1509
-
1510
- // lib/billing.js
1511
- import { createLogger } from "@drawbridge/drawbridge-telemetry";
1512
- var logger = createLogger();
1513
- var MARKUP = 1.3;
1514
- var cost = {
1515
- // gemini-3.5-flash — verified against Google's pricing page 2026-07-09:
1516
- // $0.15 cached / $1.50 input / $9.00 output per 1M tokens (thinking billed at
1517
- // output). ~3.6x the retired 2.5-flash output rate.
1518
- "gemini-3.5-flash": {
1519
- cached: 15,
1520
- input: 150,
1521
- output: 900
1522
- },
1523
- // gemini-3.5-flash-lite — verified against Google's pricing page 2026-08-20:
1524
- // $0.03 cached / $0.30 input / $2.50 output per 1M tokens (thinking billed at
1525
- // output). A fifth of flash on input, ~a quarter on output. Growth's assistant
1526
- // ranks the feed on this tier — one call per page — so its rows were the
1527
- // unpriced ones until now.
1528
- "gemini-3.5-flash-lite": {
1529
- cached: 3,
1530
- input: 30,
1531
- output: 250
1532
- },
1533
- "gemini-2.5-flash": {
1534
- cached: 3,
1535
- input: 30,
1536
- output: 250
1537
- },
1538
- "gemini-2.5-flash-image": {
1539
- cached: 3,
1540
- input: 30,
1541
- output: 3e3
1542
- },
1543
- // gemini-3-pro-image-preview — verified against Google's pricing page
1544
- // 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
1545
- // tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
1546
- // 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
1547
- // that reproduces the per-image price from the tokens usageMetadata
1548
- // reports. Growth's hero generation runs this model today.
1549
- "gemini-3-pro-image-preview": {
1550
- cached: 20,
1551
- input: 200,
1552
- output: 12e3
1553
- }
1554
- };
1555
- var toolCost = {
1556
- search: 3.5
1557
- };
1558
- var toolPricing = Object.fromEntries(
1559
- Object.entries(toolCost).map(([tool, value]) => [
1560
- tool,
1561
- Math.ceil(value * MARKUP)
1562
- ])
1563
- );
1564
- var pricing = Object.fromEntries(
1565
- Object.entries(cost).map(([model, rates]) => [
1566
- model,
1567
- {
1568
- cached: Math.round(rates.cached * MARKUP),
1569
- input: Math.round(rates.input * MARKUP),
1570
- output: Math.round(rates.output * MARKUP)
1571
- }
1572
- ])
1573
- );
1574
-
1575
1584
  // lib/pricing.js
1576
1585
  var emailPlans = {
1577
1586
  essentials50k: {
@@ -1641,6 +1650,33 @@ var sending = {
1641
1650
  plan: emailPlan,
1642
1651
  plans: emailPlans,
1643
1652
  title: emailPlans[emailPlan].title
1653
+ },
1654
+ // What one outbound SMS SEGMENT costs us, against what channels.sms below
1655
+ // sells it for. Here for the same reason the email figures are: this file is
1656
+ // where a rate is found without knowing whose module owns it — and its
1657
+ // absence is why "does an SMS action cover its cost" could not be answered
1658
+ // from this repo at all.
1659
+ //
1660
+ // US toll-free outbound, read from Twilio's SMS pricing page 2026-09-03
1661
+ // (https://www.twilio.com/en-us/sms/pricing/us): $0.0083 per segment, plus a
1662
+ // carrier fee that varies by the DESTINATION carrier — $0.0035 AT&T, $0.0045
1663
+ // T-Mobile / Verizon / US Cellular, $0.004 everywhere else. CENTS per
1664
+ // segment, like every other rate in this file.
1665
+ //
1666
+ // WORST CASE, deliberately. A price floor computed from the average is a
1667
+ // floor that half of real traffic sits underneath.
1668
+ //
1669
+ // A segment is 153 GSM-7 characters once a message spans more than one — or
1670
+ // 67 if the body contains a single emoji or curly quote, which flips the
1671
+ // whole message to UCS-2. Twilio reports the count as `num_segments` on the
1672
+ // Message resource, which is what the send bills on.
1673
+ sms: {
1674
+ baseCents: 0.83,
1675
+ // The dearest carrier fee, not the mean: see WORST CASE above.
1676
+ carrierCents: 0.45,
1677
+ // Stated, not summed — 0.83 + 0.45 is 1.2800000000000002 in binary
1678
+ // floating point, and this number is compared against money.
1679
+ segmentCostCents: 1.28
1644
1680
  }
1645
1681
  };
1646
1682
  var channels = {
@@ -1656,6 +1692,9 @@ var channels = {
1656
1692
  includedInAllowance: false
1657
1693
  }
1658
1694
  };
1695
+ var MINIMUM_ACTION_CENTS = Math.round(
1696
+ sending.sms.segmentCostCents / channels.sms.actionsPerSegment * MARKUP * 1e3
1697
+ ) / 1e3;
1659
1698
 
1660
1699
  // lib/connections/providers/drawbridge.js
1661
1700
  var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
@@ -1680,13 +1719,16 @@ var teamRecipients = async ({ memberIds = [], organization: organization2, read
1680
1719
  ]
1681
1720
  }) : [];
1682
1721
  const seen = /* @__PURE__ */ new Set();
1683
- return [owner, ...members].filter((member) => {
1684
- if (!(member == null ? void 0 : member.id) || !(member == null ? void 0 : member.email)) return false;
1685
- const address = member.email.toLowerCase();
1686
- if (seen.has(address)) return false;
1687
- seen.add(address);
1688
- return true;
1689
- });
1722
+ return {
1723
+ org,
1724
+ recipients: [owner, ...members].filter((member) => {
1725
+ if (!(member == null ? void 0 : member.id) || !(member == null ? void 0 : member.email)) return false;
1726
+ const address = member.email.toLowerCase();
1727
+ if (seen.has(address)) return false;
1728
+ seen.add(address);
1729
+ return true;
1730
+ })
1731
+ };
1690
1732
  };
1691
1733
  var queueNotification = (data2) => ({ collection: "notification", data: data2, operation: "create" });
1692
1734
  var drawbridge_default2 = {
@@ -1762,7 +1804,7 @@ var drawbridge_default2 = {
1762
1804
  const count = Number((counted == null ? void 0 : counted.count) || 0);
1763
1805
  const request2 = { campaign, count, days };
1764
1806
  if (!count) return { message: "No new leads in the period \u2014 digest skipped.", request: request2, response: { skipped: true }, skipped: true };
1765
- const recipients = await teamRecipients({
1807
+ const { recipients } = await teamRecipients({
1766
1808
  memberIds: ((_d = step.settings) == null ? void 0 : _d.members) || [],
1767
1809
  organization: workflow.organization,
1768
1810
  read
@@ -1798,7 +1840,7 @@ var drawbridge_default2 = {
1798
1840
  var _a;
1799
1841
  const memberIds = ((_a = step.settings) == null ? void 0 : _a.members) || [];
1800
1842
  const request2 = { members: memberIds.length };
1801
- const recipients = await teamRecipients({ memberIds, organization: workflow.organization, read });
1843
+ const { org, recipients } = await teamRecipients({ memberIds, organization: workflow.organization, read });
1802
1844
  if (!recipients.length) {
1803
1845
  return {
1804
1846
  message: "No owner or accepted member with an email address \u2014 team notification skipped.",
@@ -1808,32 +1850,37 @@ var drawbridge_default2 = {
1808
1850
  };
1809
1851
  }
1810
1852
  const bucket = Math.floor(Date.now() / (15 * 60 * 1e3));
1853
+ const campaign = (context == null ? void 0 : context.campaign) ? await read.get({ collection: "campaign", query: { id: context.campaign } }) : null;
1854
+ const link = (context == null ? void 0 : context.campaign) && (context == null ? void 0 : context.lead) ? {
1855
+ href: "/organizations/" + workflow.organization + "/campaigns/" + context.campaign + "/leads/" + context.lead,
1856
+ text: "View lead"
1857
+ } : null;
1858
+ const body = ((context == null ? void 0 : context.email) || "Someone") + " just entered" + ((campaign == null ? void 0 : campaign.title) ? " " + campaign.title : "") + ((org == null ? void 0 : org.title) ? " at " + org.title : "") + ".";
1859
+ const subject = "New lead" + ((campaign == null ? void 0 : campaign.title) ? " \u2014 " + campaign.title : "");
1811
1860
  return {
1812
1861
  message: "Team notification queued for " + recipients.length + " recipient(s).",
1813
1862
  request: request2,
1814
1863
  response: { notified: recipients.length },
1815
- writes: recipients.map((member) => {
1816
- var _a2, _b;
1817
- return {
1818
- ...queueNotification({
1819
- audience: "member",
1820
- // Per workflow, recipient AND bucket, so one recipient's damper
1821
- // can never swallow another's mail and a later bucket is never
1822
- // mistaken for a duplicate of an earlier one.
1823
- key: "team.notify." + workflow.id + "." + member.id + "." + bucket,
1824
- message: interpolate((_a2 = step.settings) == null ? void 0 : _a2.message, context),
1825
- organization: workflow.organization,
1826
- send: { type: "email", email: member.email },
1827
- title: interpolate((_b = step.settings) == null ? void 0 : _b.subject, context),
1828
- workflow: workflow.id
1829
- }),
1830
- // E11000 IS THE DAMPER WORKING: this recipient has already been
1831
- // told within the bucket. Declared per write rather than assumed by
1832
- // the shell, because on every other write here a duplicate key is a
1833
- // real failure.
1834
- ignoreDuplicate: true
1835
- };
1836
- })
1864
+ writes: recipients.map((member) => ({
1865
+ ...queueNotification({
1866
+ audience: "member",
1867
+ // Per workflow, recipient AND bucket, so one recipient's damper
1868
+ // can never swallow another's mail and a later bucket is never
1869
+ // mistaken for a duplicate of an earlier one.
1870
+ key: "team.notify." + workflow.id + "." + member.id + "." + bucket,
1871
+ link,
1872
+ message: body,
1873
+ organization: workflow.organization,
1874
+ send: { type: "email", email: member.email },
1875
+ title: subject,
1876
+ workflow: workflow.id
1877
+ }),
1878
+ // E11000 IS THE DAMPER WORKING: this recipient has already been
1879
+ // told within the bucket. Declared per write rather than assumed by
1880
+ // the shell, because on every other write here a duplicate key is a
1881
+ // real failure.
1882
+ ignoreDuplicate: true
1883
+ }))
1837
1884
  };
1838
1885
  },
1839
1886
  // Drawbridge sends lead-facing email itself — no merchant provider gates
@@ -2239,14 +2286,24 @@ var drawbridge_default2 = {
2239
2286
  }),
2240
2287
  // To organization MEMBERS. Never suppressed — an entrant's opt-out must
2241
2288
  // not silence an alert to staff — and not billed.
2289
+ //
2290
+ // ITS COPY IS WRITTEN, NOT ASKED FOR. This says the same thing every
2291
+ // time — a lead arrived, here it is — so the merchant chooses who hears
2292
+ // it, not how it reads, and the hook composes the subject, the body and
2293
+ // the link to the lead.
2294
+ //
2295
+ // `message` and `subject` stay declared, optional and IGNORED: stored
2296
+ // workflows still carry them and the api validates settings with
2297
+ // noUnknown().strict(), so dropping the keys would 400 the next save of
2298
+ // a workflow built before this.
2242
2299
  notify: () => ({
2243
2300
  hook: "email.notify",
2244
2301
  key: "Email \u2014 Notification",
2245
2302
  queue: "notification",
2246
2303
  settings: {
2247
2304
  members: { of: "string", type: "array" },
2248
- message: { required: true, type: "string" },
2249
- subject: { required: true, type: "string" }
2305
+ message: { type: "string" },
2306
+ subject: { type: "string" }
2250
2307
  },
2251
2308
  triggers: ["lead.insert"],
2252
2309
  // Zero is a PRICE, and a deliberate one. Declared rather than omitted
@@ -2661,8 +2718,62 @@ var klaviyo_default2 = {
2661
2718
  sms: false,
2662
2719
  inbound: false,
2663
2720
  // Nothing to set up or tear down at the vendor: the grant is the whole
2664
- // integration, and revoking it is auth.disconnect's job.
2665
- lifecycle: false,
2721
+ // integration. What CAN rot is the grant itself, so health is the one
2722
+ // lifecycle hook Klaviyo carries.
2723
+ lifecycle: {
2724
+ // Nothing to set up, tear down, or re-register at the vendor — the
2725
+ // grant is the whole integration.
2726
+ cleanup: false,
2727
+ register: false,
2728
+ rehydrate: false,
2729
+ // KEEP THE GRANT PROVEN AND WARM. The shell mints a fresh access
2730
+ // token before every hook run — for Klaviyo that spends the refresh
2731
+ // token, which is the only question that reaches Klaviyo (see
2732
+ // auth.probe) — so this body running at all proves the grant still
2733
+ // rotates, and the daily run keeps it inside Klaviyo's 90-day idle
2734
+ // window. A dead grant never gets here: the shell's mint throws
2735
+ // `grant_refused` and the step runner errors the connection itself.
2736
+ //
2737
+ // The one call below proves the minted token is HONOURED — mint and
2738
+ // acceptance are different facts, and /accounts is already the call
2739
+ // the connect flow makes (auth.connect), so it needs no new scope.
2740
+ health: async ({ connection: connection2, token }, { fetcher, read } = {}) => {
2741
+ const request2 = { connectionId: connection2.id };
2742
+ try {
2743
+ await api2("/accounts", { fetcher, token });
2744
+ return {
2745
+ message: "Health check passed \u2014 token minted and accepted.",
2746
+ request: request2,
2747
+ response: { pingedAt: /* @__PURE__ */ new Date() }
2748
+ };
2749
+ } catch (error) {
2750
+ if ([401, 403].includes(error.status) && read) {
2751
+ const current = await read.get({ collection: "connection", query: { id: connection2.id } });
2752
+ if (current) {
2753
+ const others = (current.errors || []).filter((entry) => entry.source !== "oauth");
2754
+ error.writes = [{
2755
+ collection: "connection",
2756
+ data: {
2757
+ $set: {
2758
+ errors: [
2759
+ ...others,
2760
+ {
2761
+ message: "Klaviyo no longer accepts this connection. Reconnect Klaviyo to resume syncing.",
2762
+ source: "oauth"
2763
+ }
2764
+ ],
2765
+ status: "error"
2766
+ }
2767
+ },
2768
+ operation: "update",
2769
+ query: { id: connection2.id }
2770
+ }];
2771
+ }
2772
+ }
2773
+ throw error;
2774
+ }
2775
+ }
2776
+ },
2666
2777
  resources: {
2667
2778
  // The lists a merchant can sync into, for the picker on their
2668
2779
  // connection.
@@ -2738,6 +2849,19 @@ var klaviyo_default2 = {
2738
2849
  return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? data2.status : "pending";
2739
2850
  },
2740
2851
  steps: {
2852
+ connection: {
2853
+ // The daily grant check — same step type and workflow shape as
2854
+ // Shopify's, provisioned as a system workflow per connection.
2855
+ health: {
2856
+ check: () => ({
2857
+ description: "Keeps the Klaviyo grant working \u2014 spends the refresh token daily so a revoked or idle grant is reported instead of discovered by a failing sync.",
2858
+ hook: "lifecycle.health",
2859
+ key: "Klaviyo Connection Health",
2860
+ queue: "connection",
2861
+ system: true
2862
+ })
2863
+ }
2864
+ },
2741
2865
  contacts: {
2742
2866
  // A DECLARATION, not the work. It names the hook that does the work, and
2743
2867
  // says where that hook's values belong. Nested like the hooks, and the
@@ -94,8 +94,14 @@ var authToken = async ({
94
94
  });
95
95
  const body = await response.json().catch(() => ({}));
96
96
  if (!response.ok) {
97
- throw new Error(
98
- renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
97
+ throw Object.assign(
98
+ new Error(
99
+ renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
100
+ ),
101
+ {
102
+ status: response.status,
103
+ ...renewing && { code: "grant_refused" }
104
+ }
99
105
  );
100
106
  }
101
107
  if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");
@@ -173,9 +173,20 @@ const authToken = async ({
173
173
  // The two paths fail differently for the merchant: a refused exchange is
174
174
  // "try connecting again", a refused refresh is "reconnect this connection",
175
175
  // and collapsing them sends somebody to the wrong screen.
176
- throw new Error( renewing
177
- ? 'The vendor refused the refresh token (' + response.status + ') reconnect the connection'
178
- : 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' )
176
+ //
177
+ // `code` marks the refused REFRESH machine-readably — the health-check
178
+ // shell errors the connection on it rather than retrying a dead grant
179
+ // into the log forever. Only the renewing branch: a refused exchange is
180
+ // a connect-flow failure with a person watching.
181
+ throw Object.assign(
182
+ new Error( renewing
183
+ ? 'The vendor refused the refresh token (' + response.status + ') — reconnect the connection'
184
+ : 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' )
185
+ ),
186
+ {
187
+ status : response.status,
188
+ ...( renewing && { code : 'grant_refused' })
189
+ }
179
190
  );
180
191
 
181
192
  }
@@ -173,9 +173,20 @@ const authToken = async ({
173
173
  // The two paths fail differently for the merchant: a refused exchange is
174
174
  // "try connecting again", a refused refresh is "reconnect this connection",
175
175
  // and collapsing them sends somebody to the wrong screen.
176
- throw new Error( renewing
177
- ? 'The vendor refused the refresh token (' + response.status + ') reconnect the connection'
178
- : 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' )
176
+ //
177
+ // `code` marks the refused REFRESH machine-readably — the health-check
178
+ // shell errors the connection on it rather than retrying a dead grant
179
+ // into the log forever. Only the renewing branch: a refused exchange is
180
+ // a connect-flow failure with a person watching.
181
+ throw Object.assign(
182
+ new Error( renewing
183
+ ? 'The vendor refused the refresh token (' + response.status + ') — reconnect the connection'
184
+ : 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' )
185
+ ),
186
+ {
187
+ status : response.status,
188
+ ...( renewing && { code : 'grant_refused' })
189
+ }
179
190
  );
180
191
 
181
192
  }
@@ -69,8 +69,14 @@ var authToken = async ({
69
69
  });
70
70
  const body = await response.json().catch(() => ({}));
71
71
  if (!response.ok) {
72
- throw new Error(
73
- renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
72
+ throw Object.assign(
73
+ new Error(
74
+ renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
75
+ ),
76
+ {
77
+ status: response.status,
78
+ ...renewing && { code: "grant_refused" }
79
+ }
74
80
  );
75
81
  }
76
82
  if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");