@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.
package/dist/providers.js CHANGED
@@ -326,8 +326,14 @@ var authToken = async ({
326
326
  });
327
327
  const body = await response.json().catch(() => ({}));
328
328
  if (!response.ok) {
329
- throw new Error(
330
- 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 : "")
329
+ throw Object.assign(
330
+ new Error(
331
+ 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 : "")
332
+ ),
333
+ {
334
+ status: response.status,
335
+ ...renewing && { code: "grant_refused" }
336
+ }
331
337
  );
332
338
  }
333
339
  if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");
@@ -383,7 +389,10 @@ var accessToken = async ({
383
389
  }
384
390
  if (!force && !isStale(settings, now)) return settings.accessToken;
385
391
  if (!settings.refreshToken) {
386
- throw new Error("This connection has expired and cannot be renewed automatically. Reconnect it.");
392
+ throw Object.assign(
393
+ new Error("This connection has expired and cannot be renewed automatically. Reconnect it."),
394
+ { code: "grant_refused" }
395
+ );
387
396
  }
388
397
  const minted = await manifest.hooks.auth.token({
389
398
  clientId,
@@ -978,6 +987,74 @@ var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fi
978
987
  </defs>
979
988
  </svg>`;
980
989
 
990
+ // lib/transactions.js
991
+ import { currentTraceId } from "@drawbridge/drawbridge-telemetry";
992
+
993
+ // lib/billing.js
994
+ import { createLogger } from "@drawbridge/drawbridge-telemetry";
995
+ var logger = createLogger();
996
+ var MARKUP = 1.3;
997
+ var cost = {
998
+ // gemini-3.5-flash — verified against Google's pricing page 2026-07-09:
999
+ // $0.15 cached / $1.50 input / $9.00 output per 1M tokens (thinking billed at
1000
+ // output). ~3.6x the retired 2.5-flash output rate.
1001
+ "gemini-3.5-flash": {
1002
+ cached: 15,
1003
+ input: 150,
1004
+ output: 900
1005
+ },
1006
+ // gemini-3.5-flash-lite — verified against Google's pricing page 2026-08-20:
1007
+ // $0.03 cached / $0.30 input / $2.50 output per 1M tokens (thinking billed at
1008
+ // output). A fifth of flash on input, ~a quarter on output. Growth's assistant
1009
+ // ranks the feed on this tier — one call per page — so its rows were the
1010
+ // unpriced ones until now.
1011
+ "gemini-3.5-flash-lite": {
1012
+ cached: 3,
1013
+ input: 30,
1014
+ output: 250
1015
+ },
1016
+ "gemini-2.5-flash": {
1017
+ cached: 3,
1018
+ input: 30,
1019
+ output: 250
1020
+ },
1021
+ "gemini-2.5-flash-image": {
1022
+ cached: 3,
1023
+ input: 30,
1024
+ output: 3e3
1025
+ },
1026
+ // gemini-3-pro-image-preview — verified against Google's pricing page
1027
+ // 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
1028
+ // tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
1029
+ // 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
1030
+ // that reproduces the per-image price from the tokens usageMetadata
1031
+ // reports. Growth's hero generation runs this model today.
1032
+ "gemini-3-pro-image-preview": {
1033
+ cached: 20,
1034
+ input: 200,
1035
+ output: 12e3
1036
+ }
1037
+ };
1038
+ var toolCost = {
1039
+ search: 3.5
1040
+ };
1041
+ var toolPricing = Object.fromEntries(
1042
+ Object.entries(toolCost).map(([tool, value]) => [
1043
+ tool,
1044
+ Math.ceil(value * MARKUP)
1045
+ ])
1046
+ );
1047
+ var pricing = Object.fromEntries(
1048
+ Object.entries(cost).map(([model, rates]) => [
1049
+ model,
1050
+ {
1051
+ cached: Math.round(rates.cached * MARKUP),
1052
+ input: Math.round(rates.input * MARKUP),
1053
+ output: Math.round(rates.output * MARKUP)
1054
+ }
1055
+ ])
1056
+ );
1057
+
981
1058
  // lib/features.js
982
1059
  var page = {
983
1060
  qrcode: {
@@ -1440,74 +1517,6 @@ var conversionRate = (subscription) => {
1440
1517
  return ((_a = resolvePlan(subscription)) == null ? void 0 : _a.conversion) ?? free.conversion;
1441
1518
  };
1442
1519
 
1443
- // lib/transactions.js
1444
- import { currentTraceId } from "@drawbridge/drawbridge-telemetry";
1445
-
1446
- // lib/billing.js
1447
- import { createLogger } from "@drawbridge/drawbridge-telemetry";
1448
- var logger = createLogger();
1449
- var MARKUP = 1.3;
1450
- var cost = {
1451
- // gemini-3.5-flash — verified against Google's pricing page 2026-07-09:
1452
- // $0.15 cached / $1.50 input / $9.00 output per 1M tokens (thinking billed at
1453
- // output). ~3.6x the retired 2.5-flash output rate.
1454
- "gemini-3.5-flash": {
1455
- cached: 15,
1456
- input: 150,
1457
- output: 900
1458
- },
1459
- // gemini-3.5-flash-lite — verified against Google's pricing page 2026-08-20:
1460
- // $0.03 cached / $0.30 input / $2.50 output per 1M tokens (thinking billed at
1461
- // output). A fifth of flash on input, ~a quarter on output. Growth's assistant
1462
- // ranks the feed on this tier — one call per page — so its rows were the
1463
- // unpriced ones until now.
1464
- "gemini-3.5-flash-lite": {
1465
- cached: 3,
1466
- input: 30,
1467
- output: 250
1468
- },
1469
- "gemini-2.5-flash": {
1470
- cached: 3,
1471
- input: 30,
1472
- output: 250
1473
- },
1474
- "gemini-2.5-flash-image": {
1475
- cached: 3,
1476
- input: 30,
1477
- output: 3e3
1478
- },
1479
- // gemini-3-pro-image-preview — verified against Google's pricing page
1480
- // 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
1481
- // tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
1482
- // 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
1483
- // that reproduces the per-image price from the tokens usageMetadata
1484
- // reports. Growth's hero generation runs this model today.
1485
- "gemini-3-pro-image-preview": {
1486
- cached: 20,
1487
- input: 200,
1488
- output: 12e3
1489
- }
1490
- };
1491
- var toolCost = {
1492
- search: 3.5
1493
- };
1494
- var toolPricing = Object.fromEntries(
1495
- Object.entries(toolCost).map(([tool, value]) => [
1496
- tool,
1497
- Math.ceil(value * MARKUP)
1498
- ])
1499
- );
1500
- var pricing = Object.fromEntries(
1501
- Object.entries(cost).map(([model, rates]) => [
1502
- model,
1503
- {
1504
- cached: Math.round(rates.cached * MARKUP),
1505
- input: Math.round(rates.input * MARKUP),
1506
- output: Math.round(rates.output * MARKUP)
1507
- }
1508
- ])
1509
- );
1510
-
1511
1520
  // lib/pricing.js
1512
1521
  var emailPlans = {
1513
1522
  essentials50k: {
@@ -1577,6 +1586,33 @@ var sending = {
1577
1586
  plan: emailPlan,
1578
1587
  plans: emailPlans,
1579
1588
  title: emailPlans[emailPlan].title
1589
+ },
1590
+ // What one outbound SMS SEGMENT costs us, against what channels.sms below
1591
+ // sells it for. Here for the same reason the email figures are: this file is
1592
+ // where a rate is found without knowing whose module owns it — and its
1593
+ // absence is why "does an SMS action cover its cost" could not be answered
1594
+ // from this repo at all.
1595
+ //
1596
+ // US toll-free outbound, read from Twilio's SMS pricing page 2026-09-03
1597
+ // (https://www.twilio.com/en-us/sms/pricing/us): $0.0083 per segment, plus a
1598
+ // carrier fee that varies by the DESTINATION carrier — $0.0035 AT&T, $0.0045
1599
+ // T-Mobile / Verizon / US Cellular, $0.004 everywhere else. CENTS per
1600
+ // segment, like every other rate in this file.
1601
+ //
1602
+ // WORST CASE, deliberately. A price floor computed from the average is a
1603
+ // floor that half of real traffic sits underneath.
1604
+ //
1605
+ // A segment is 153 GSM-7 characters once a message spans more than one — or
1606
+ // 67 if the body contains a single emoji or curly quote, which flips the
1607
+ // whole message to UCS-2. Twilio reports the count as `num_segments` on the
1608
+ // Message resource, which is what the send bills on.
1609
+ sms: {
1610
+ baseCents: 0.83,
1611
+ // The dearest carrier fee, not the mean: see WORST CASE above.
1612
+ carrierCents: 0.45,
1613
+ // Stated, not summed — 0.83 + 0.45 is 1.2800000000000002 in binary
1614
+ // floating point, and this number is compared against money.
1615
+ segmentCostCents: 1.28
1580
1616
  }
1581
1617
  };
1582
1618
  var channels = {
@@ -1592,6 +1628,9 @@ var channels = {
1592
1628
  includedInAllowance: false
1593
1629
  }
1594
1630
  };
1631
+ var MINIMUM_ACTION_CENTS = Math.round(
1632
+ sending.sms.segmentCostCents / channels.sms.actionsPerSegment * MARKUP * 1e3
1633
+ ) / 1e3;
1595
1634
 
1596
1635
  // lib/connections/providers/drawbridge.js
1597
1636
  var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
@@ -1616,13 +1655,16 @@ var teamRecipients = async ({ memberIds = [], organization: organization2, read
1616
1655
  ]
1617
1656
  }) : [];
1618
1657
  const seen = /* @__PURE__ */ new Set();
1619
- return [owner, ...members].filter((member) => {
1620
- if (!(member == null ? void 0 : member.id) || !(member == null ? void 0 : member.email)) return false;
1621
- const address = member.email.toLowerCase();
1622
- if (seen.has(address)) return false;
1623
- seen.add(address);
1624
- return true;
1625
- });
1658
+ return {
1659
+ org,
1660
+ recipients: [owner, ...members].filter((member) => {
1661
+ if (!(member == null ? void 0 : member.id) || !(member == null ? void 0 : member.email)) return false;
1662
+ const address = member.email.toLowerCase();
1663
+ if (seen.has(address)) return false;
1664
+ seen.add(address);
1665
+ return true;
1666
+ })
1667
+ };
1626
1668
  };
1627
1669
  var queueNotification = (data2) => ({ collection: "notification", data: data2, operation: "create" });
1628
1670
  var drawbridge_default2 = {
@@ -1698,7 +1740,7 @@ var drawbridge_default2 = {
1698
1740
  const count = Number((counted == null ? void 0 : counted.count) || 0);
1699
1741
  const request2 = { campaign, count, days };
1700
1742
  if (!count) return { message: "No new leads in the period \u2014 digest skipped.", request: request2, response: { skipped: true }, skipped: true };
1701
- const recipients = await teamRecipients({
1743
+ const { recipients } = await teamRecipients({
1702
1744
  memberIds: ((_d = step.settings) == null ? void 0 : _d.members) || [],
1703
1745
  organization: workflow.organization,
1704
1746
  read
@@ -1734,7 +1776,7 @@ var drawbridge_default2 = {
1734
1776
  var _a;
1735
1777
  const memberIds = ((_a = step.settings) == null ? void 0 : _a.members) || [];
1736
1778
  const request2 = { members: memberIds.length };
1737
- const recipients = await teamRecipients({ memberIds, organization: workflow.organization, read });
1779
+ const { org, recipients } = await teamRecipients({ memberIds, organization: workflow.organization, read });
1738
1780
  if (!recipients.length) {
1739
1781
  return {
1740
1782
  message: "No owner or accepted member with an email address \u2014 team notification skipped.",
@@ -1744,32 +1786,37 @@ var drawbridge_default2 = {
1744
1786
  };
1745
1787
  }
1746
1788
  const bucket = Math.floor(Date.now() / (15 * 60 * 1e3));
1789
+ const campaign = (context == null ? void 0 : context.campaign) ? await read.get({ collection: "campaign", query: { id: context.campaign } }) : null;
1790
+ const link = (context == null ? void 0 : context.campaign) && (context == null ? void 0 : context.lead) ? {
1791
+ href: "/organizations/" + workflow.organization + "/campaigns/" + context.campaign + "/leads/" + context.lead,
1792
+ text: "View lead"
1793
+ } : null;
1794
+ 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 : "") + ".";
1795
+ const subject = "New lead" + ((campaign == null ? void 0 : campaign.title) ? " \u2014 " + campaign.title : "");
1747
1796
  return {
1748
1797
  message: "Team notification queued for " + recipients.length + " recipient(s).",
1749
1798
  request: request2,
1750
1799
  response: { notified: recipients.length },
1751
- writes: recipients.map((member) => {
1752
- var _a2, _b;
1753
- return {
1754
- ...queueNotification({
1755
- audience: "member",
1756
- // Per workflow, recipient AND bucket, so one recipient's damper
1757
- // can never swallow another's mail and a later bucket is never
1758
- // mistaken for a duplicate of an earlier one.
1759
- key: "team.notify." + workflow.id + "." + member.id + "." + bucket,
1760
- message: interpolate((_a2 = step.settings) == null ? void 0 : _a2.message, context),
1761
- organization: workflow.organization,
1762
- send: { type: "email", email: member.email },
1763
- title: interpolate((_b = step.settings) == null ? void 0 : _b.subject, context),
1764
- workflow: workflow.id
1765
- }),
1766
- // E11000 IS THE DAMPER WORKING: this recipient has already been
1767
- // told within the bucket. Declared per write rather than assumed by
1768
- // the shell, because on every other write here a duplicate key is a
1769
- // real failure.
1770
- ignoreDuplicate: true
1771
- };
1772
- })
1800
+ writes: recipients.map((member) => ({
1801
+ ...queueNotification({
1802
+ audience: "member",
1803
+ // Per workflow, recipient AND bucket, so one recipient's damper
1804
+ // can never swallow another's mail and a later bucket is never
1805
+ // mistaken for a duplicate of an earlier one.
1806
+ key: "team.notify." + workflow.id + "." + member.id + "." + bucket,
1807
+ link,
1808
+ message: body,
1809
+ organization: workflow.organization,
1810
+ send: { type: "email", email: member.email },
1811
+ title: subject,
1812
+ workflow: workflow.id
1813
+ }),
1814
+ // E11000 IS THE DAMPER WORKING: this recipient has already been
1815
+ // told within the bucket. Declared per write rather than assumed by
1816
+ // the shell, because on every other write here a duplicate key is a
1817
+ // real failure.
1818
+ ignoreDuplicate: true
1819
+ }))
1773
1820
  };
1774
1821
  },
1775
1822
  // Drawbridge sends lead-facing email itself — no merchant provider gates
@@ -2175,14 +2222,24 @@ var drawbridge_default2 = {
2175
2222
  }),
2176
2223
  // To organization MEMBERS. Never suppressed — an entrant's opt-out must
2177
2224
  // not silence an alert to staff — and not billed.
2225
+ //
2226
+ // ITS COPY IS WRITTEN, NOT ASKED FOR. This says the same thing every
2227
+ // time — a lead arrived, here it is — so the merchant chooses who hears
2228
+ // it, not how it reads, and the hook composes the subject, the body and
2229
+ // the link to the lead.
2230
+ //
2231
+ // `message` and `subject` stay declared, optional and IGNORED: stored
2232
+ // workflows still carry them and the api validates settings with
2233
+ // noUnknown().strict(), so dropping the keys would 400 the next save of
2234
+ // a workflow built before this.
2178
2235
  notify: () => ({
2179
2236
  hook: "email.notify",
2180
2237
  key: "Email \u2014 Notification",
2181
2238
  queue: "notification",
2182
2239
  settings: {
2183
2240
  members: { of: "string", type: "array" },
2184
- message: { required: true, type: "string" },
2185
- subject: { required: true, type: "string" }
2241
+ message: { type: "string" },
2242
+ subject: { type: "string" }
2186
2243
  },
2187
2244
  triggers: ["lead.insert"],
2188
2245
  // Zero is a PRICE, and a deliberate one. Declared rather than omitted
@@ -2597,8 +2654,62 @@ var klaviyo_default2 = {
2597
2654
  sms: false,
2598
2655
  inbound: false,
2599
2656
  // Nothing to set up or tear down at the vendor: the grant is the whole
2600
- // integration, and revoking it is auth.disconnect's job.
2601
- lifecycle: false,
2657
+ // integration. What CAN rot is the grant itself, so health is the one
2658
+ // lifecycle hook Klaviyo carries.
2659
+ lifecycle: {
2660
+ // Nothing to set up, tear down, or re-register at the vendor — the
2661
+ // grant is the whole integration.
2662
+ cleanup: false,
2663
+ register: false,
2664
+ rehydrate: false,
2665
+ // KEEP THE GRANT PROVEN AND WARM. The shell mints a fresh access
2666
+ // token before every hook run — for Klaviyo that spends the refresh
2667
+ // token, which is the only question that reaches Klaviyo (see
2668
+ // auth.probe) — so this body running at all proves the grant still
2669
+ // rotates, and the daily run keeps it inside Klaviyo's 90-day idle
2670
+ // window. A dead grant never gets here: the shell's mint throws
2671
+ // `grant_refused` and the step runner errors the connection itself.
2672
+ //
2673
+ // The one call below proves the minted token is HONOURED — mint and
2674
+ // acceptance are different facts, and /accounts is already the call
2675
+ // the connect flow makes (auth.connect), so it needs no new scope.
2676
+ health: async ({ connection: connection2, token }, { fetcher, read } = {}) => {
2677
+ const request2 = { connectionId: connection2.id };
2678
+ try {
2679
+ await api2("/accounts", { fetcher, token });
2680
+ return {
2681
+ message: "Health check passed \u2014 token minted and accepted.",
2682
+ request: request2,
2683
+ response: { pingedAt: /* @__PURE__ */ new Date() }
2684
+ };
2685
+ } catch (error) {
2686
+ if ([401, 403].includes(error.status) && read) {
2687
+ const current = await read.get({ collection: "connection", query: { id: connection2.id } });
2688
+ if (current) {
2689
+ const others = (current.errors || []).filter((entry) => entry.source !== "oauth");
2690
+ error.writes = [{
2691
+ collection: "connection",
2692
+ data: {
2693
+ $set: {
2694
+ errors: [
2695
+ ...others,
2696
+ {
2697
+ message: "Klaviyo no longer accepts this connection. Reconnect Klaviyo to resume syncing.",
2698
+ source: "oauth"
2699
+ }
2700
+ ],
2701
+ status: "error"
2702
+ }
2703
+ },
2704
+ operation: "update",
2705
+ query: { id: connection2.id }
2706
+ }];
2707
+ }
2708
+ }
2709
+ throw error;
2710
+ }
2711
+ }
2712
+ },
2602
2713
  resources: {
2603
2714
  // The lists a merchant can sync into, for the picker on their
2604
2715
  // connection.
@@ -2674,6 +2785,19 @@ var klaviyo_default2 = {
2674
2785
  return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? data2.status : "pending";
2675
2786
  },
2676
2787
  steps: {
2788
+ connection: {
2789
+ // The daily grant check — same step type and workflow shape as
2790
+ // Shopify's, provisioned as a system workflow per connection.
2791
+ health: {
2792
+ check: () => ({
2793
+ 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.",
2794
+ hook: "lifecycle.health",
2795
+ key: "Klaviyo Connection Health",
2796
+ queue: "connection",
2797
+ system: true
2798
+ })
2799
+ }
2800
+ },
2677
2801
  contacts: {
2678
2802
  // A DECLARATION, not the work. It names the hook that does the work, and
2679
2803
  // says where that hook's values belong. Nested like the hooks, and the
package/dist/twilio.cjs CHANGED
@@ -174,6 +174,57 @@ var twilio = {
174
174
  sid: result == null ? void 0 : result.sid
175
175
  };
176
176
  },
177
+ // THE NUMBER AS TWILIO HOLDS IT NOW. Same IncomingPhoneNumber resource the
178
+ // purchase and release above use (api.twilio.com 2010-04-01, GET
179
+ // /IncomingPhoneNumbers/{sid}.json) — a 404 means the number is no longer
180
+ // on the account, which the health sweep must distinguish from a transient,
181
+ // so it is answered as { gone : true } rather than thrown.
182
+ numberConfig: async ({ accountSid, authToken, request: request2 = request, sid }) => {
183
+ if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
184
+ if (!sid) throw new Error("Twilio config read needs the IncomingPhoneNumber sid");
185
+ let result;
186
+ try {
187
+ result = await request2({
188
+ method: "GET",
189
+ url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/IncomingPhoneNumbers/" + sid + ".json",
190
+ headers: {
191
+ "Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
192
+ }
193
+ });
194
+ } catch (error) {
195
+ if ((error == null ? void 0 : error.status) === 404) return { gone: true };
196
+ throw error;
197
+ }
198
+ return {
199
+ gone: false,
200
+ number: result == null ? void 0 : result.phone_number,
201
+ smsMethod: result == null ? void 0 : result.sms_method,
202
+ smsUrl: result == null ? void 0 : result.sms_url,
203
+ status: result == null ? void 0 : result.status
204
+ };
205
+ },
206
+ // RE-POINT AN OWNED NUMBER'S INBOUND WEBHOOK — POST on the same resource,
207
+ // the exact fields purchase sets. The health sweep's repair arm.
208
+ updateNumber: async ({ accountSid, authToken, request: request2 = request, sid, smsUrl }) => {
209
+ if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
210
+ if (!sid) throw new Error("Twilio update needs the IncomingPhoneNumber sid");
211
+ if (!smsUrl) throw new Error("Twilio update needs the smsUrl to point the number at");
212
+ const result = await request2({
213
+ method: "POST",
214
+ type: "form",
215
+ url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/IncomingPhoneNumbers/" + sid + ".json",
216
+ headers: {
217
+ "Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
218
+ },
219
+ body: {
220
+ SmsUrl: smsUrl,
221
+ SmsMethod: "POST"
222
+ }
223
+ });
224
+ return {
225
+ smsUrl: result == null ? void 0 : result.sms_url
226
+ };
227
+ },
177
228
  releaseNumber: async ({ accountSid, authToken, request: request2 = request, sid }) => {
178
229
  if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
179
230
  if (!sid) throw new Error("Twilio release needs the IncomingPhoneNumber sid");
package/dist/twilio.d.cts CHANGED
@@ -159,6 +159,73 @@ const twilio = {
159
159
 
160
160
  },
161
161
 
162
+ // THE NUMBER AS TWILIO HOLDS IT NOW. Same IncomingPhoneNumber resource the
163
+ // purchase and release above use (api.twilio.com 2010-04-01, GET
164
+ // /IncomingPhoneNumbers/{sid}.json) — a 404 means the number is no longer
165
+ // on the account, which the health sweep must distinguish from a transient,
166
+ // so it is answered as { gone : true } rather than thrown.
167
+ numberConfig : async ({ accountSid, authToken, request: request$1 = request, sid }) => {
168
+
169
+ if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
170
+ if( ! sid ) throw new Error( 'Twilio config read needs the IncomingPhoneNumber sid' );
171
+
172
+ let result;
173
+
174
+ try {
175
+
176
+ result = await request$1({
177
+ method : 'GET',
178
+ url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/IncomingPhoneNumbers/' + sid + '.json',
179
+ headers : {
180
+ 'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
181
+ }
182
+ });
183
+
184
+ } catch ( error ) {
185
+
186
+ if( error?.status === 404 ) return { gone : true };
187
+
188
+ throw error;
189
+
190
+ }
191
+
192
+ return {
193
+ gone : false,
194
+ number : result?.phone_number,
195
+ smsMethod : result?.sms_method,
196
+ smsUrl : result?.sms_url,
197
+ status : result?.status
198
+ };
199
+
200
+ },
201
+
202
+ // RE-POINT AN OWNED NUMBER'S INBOUND WEBHOOK — POST on the same resource,
203
+ // the exact fields purchase sets. The health sweep's repair arm.
204
+ updateNumber : async ({ accountSid, authToken, request: request$1 = request, sid, smsUrl }) => {
205
+
206
+ if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
207
+ if( ! sid ) throw new Error( 'Twilio update needs the IncomingPhoneNumber sid' );
208
+ if( ! smsUrl ) throw new Error( 'Twilio update needs the smsUrl to point the number at' );
209
+
210
+ const result = await request$1({
211
+ method : 'POST',
212
+ type : 'form',
213
+ url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/IncomingPhoneNumbers/' + sid + '.json',
214
+ headers : {
215
+ 'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
216
+ },
217
+ body : {
218
+ SmsUrl : smsUrl,
219
+ SmsMethod : 'POST'
220
+ }
221
+ });
222
+
223
+ return {
224
+ smsUrl : result?.sms_url
225
+ };
226
+
227
+ },
228
+
162
229
  releaseNumber : async ({ accountSid, authToken, request: request$1 = request, sid }) => {
163
230
 
164
231
  if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
package/dist/twilio.d.ts CHANGED
@@ -159,6 +159,73 @@ const twilio = {
159
159
 
160
160
  },
161
161
 
162
+ // THE NUMBER AS TWILIO HOLDS IT NOW. Same IncomingPhoneNumber resource the
163
+ // purchase and release above use (api.twilio.com 2010-04-01, GET
164
+ // /IncomingPhoneNumbers/{sid}.json) — a 404 means the number is no longer
165
+ // on the account, which the health sweep must distinguish from a transient,
166
+ // so it is answered as { gone : true } rather than thrown.
167
+ numberConfig : async ({ accountSid, authToken, request: request$1 = request, sid }) => {
168
+
169
+ if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
170
+ if( ! sid ) throw new Error( 'Twilio config read needs the IncomingPhoneNumber sid' );
171
+
172
+ let result;
173
+
174
+ try {
175
+
176
+ result = await request$1({
177
+ method : 'GET',
178
+ url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/IncomingPhoneNumbers/' + sid + '.json',
179
+ headers : {
180
+ 'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
181
+ }
182
+ });
183
+
184
+ } catch ( error ) {
185
+
186
+ if( error?.status === 404 ) return { gone : true };
187
+
188
+ throw error;
189
+
190
+ }
191
+
192
+ return {
193
+ gone : false,
194
+ number : result?.phone_number,
195
+ smsMethod : result?.sms_method,
196
+ smsUrl : result?.sms_url,
197
+ status : result?.status
198
+ };
199
+
200
+ },
201
+
202
+ // RE-POINT AN OWNED NUMBER'S INBOUND WEBHOOK — POST on the same resource,
203
+ // the exact fields purchase sets. The health sweep's repair arm.
204
+ updateNumber : async ({ accountSid, authToken, request: request$1 = request, sid, smsUrl }) => {
205
+
206
+ if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
207
+ if( ! sid ) throw new Error( 'Twilio update needs the IncomingPhoneNumber sid' );
208
+ if( ! smsUrl ) throw new Error( 'Twilio update needs the smsUrl to point the number at' );
209
+
210
+ const result = await request$1({
211
+ method : 'POST',
212
+ type : 'form',
213
+ url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/IncomingPhoneNumbers/' + sid + '.json',
214
+ headers : {
215
+ 'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
216
+ },
217
+ body : {
218
+ SmsUrl : smsUrl,
219
+ SmsMethod : 'POST'
220
+ }
221
+ });
222
+
223
+ return {
224
+ smsUrl : result?.sms_url
225
+ };
226
+
227
+ },
228
+
162
229
  releaseNumber : async ({ accountSid, authToken, request: request$1 = request, sid }) => {
163
230
 
164
231
  if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );