@drawbridge/drawbridge-utils 0.0.135 → 0.0.137

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.
@@ -1051,6 +1051,74 @@ var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fi
1051
1051
  </defs>
1052
1052
  </svg>`;
1053
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
+
1054
1122
  // lib/features.js
1055
1123
  var page = {
1056
1124
  qrcode: {
@@ -1513,74 +1581,6 @@ var conversionRate = (subscription) => {
1513
1581
  return ((_a = resolvePlan(subscription)) == null ? void 0 : _a.conversion) ?? free.conversion;
1514
1582
  };
1515
1583
 
1516
- // lib/transactions.js
1517
- import { currentTraceId } from "@drawbridge/drawbridge-telemetry";
1518
-
1519
- // lib/billing.js
1520
- import { createLogger } from "@drawbridge/drawbridge-telemetry";
1521
- var logger = createLogger();
1522
- var MARKUP = 1.3;
1523
- var cost = {
1524
- // gemini-3.5-flash — verified against Google's pricing page 2026-07-09:
1525
- // $0.15 cached / $1.50 input / $9.00 output per 1M tokens (thinking billed at
1526
- // output). ~3.6x the retired 2.5-flash output rate.
1527
- "gemini-3.5-flash": {
1528
- cached: 15,
1529
- input: 150,
1530
- output: 900
1531
- },
1532
- // gemini-3.5-flash-lite — verified against Google's pricing page 2026-08-20:
1533
- // $0.03 cached / $0.30 input / $2.50 output per 1M tokens (thinking billed at
1534
- // output). A fifth of flash on input, ~a quarter on output. Growth's assistant
1535
- // ranks the feed on this tier — one call per page — so its rows were the
1536
- // unpriced ones until now.
1537
- "gemini-3.5-flash-lite": {
1538
- cached: 3,
1539
- input: 30,
1540
- output: 250
1541
- },
1542
- "gemini-2.5-flash": {
1543
- cached: 3,
1544
- input: 30,
1545
- output: 250
1546
- },
1547
- "gemini-2.5-flash-image": {
1548
- cached: 3,
1549
- input: 30,
1550
- output: 3e3
1551
- },
1552
- // gemini-3-pro-image-preview — verified against Google's pricing page
1553
- // 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
1554
- // tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
1555
- // 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
1556
- // that reproduces the per-image price from the tokens usageMetadata
1557
- // reports. Growth's hero generation runs this model today.
1558
- "gemini-3-pro-image-preview": {
1559
- cached: 20,
1560
- input: 200,
1561
- output: 12e3
1562
- }
1563
- };
1564
- var toolCost = {
1565
- search: 3.5
1566
- };
1567
- var toolPricing = Object.fromEntries(
1568
- Object.entries(toolCost).map(([tool, value]) => [
1569
- tool,
1570
- Math.ceil(value * MARKUP)
1571
- ])
1572
- );
1573
- var pricing = Object.fromEntries(
1574
- Object.entries(cost).map(([model, rates]) => [
1575
- model,
1576
- {
1577
- cached: Math.round(rates.cached * MARKUP),
1578
- input: Math.round(rates.input * MARKUP),
1579
- output: Math.round(rates.output * MARKUP)
1580
- }
1581
- ])
1582
- );
1583
-
1584
1584
  // lib/pricing.js
1585
1585
  var emailPlans = {
1586
1586
  essentials50k: {
@@ -1650,6 +1650,33 @@ var sending = {
1650
1650
  plan: emailPlan,
1651
1651
  plans: emailPlans,
1652
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
1653
1680
  }
1654
1681
  };
1655
1682
  var channels = {
@@ -1665,6 +1692,9 @@ var channels = {
1665
1692
  includedInAllowance: false
1666
1693
  }
1667
1694
  };
1695
+ var MINIMUM_ACTION_CENTS = Math.round(
1696
+ sending.sms.segmentCostCents / channels.sms.actionsPerSegment * MARKUP * 1e3
1697
+ ) / 1e3;
1668
1698
 
1669
1699
  // lib/connections/providers/drawbridge.js
1670
1700
  var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
@@ -1689,13 +1719,16 @@ var teamRecipients = async ({ memberIds = [], organization: organization2, read
1689
1719
  ]
1690
1720
  }) : [];
1691
1721
  const seen = /* @__PURE__ */ new Set();
1692
- return [owner, ...members].filter((member) => {
1693
- if (!(member == null ? void 0 : member.id) || !(member == null ? void 0 : member.email)) return false;
1694
- const address = member.email.toLowerCase();
1695
- if (seen.has(address)) return false;
1696
- seen.add(address);
1697
- return true;
1698
- });
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
+ };
1699
1732
  };
1700
1733
  var queueNotification = (data2) => ({ collection: "notification", data: data2, operation: "create" });
1701
1734
  var drawbridge_default2 = {
@@ -1771,7 +1804,7 @@ var drawbridge_default2 = {
1771
1804
  const count = Number((counted == null ? void 0 : counted.count) || 0);
1772
1805
  const request2 = { campaign, count, days };
1773
1806
  if (!count) return { message: "No new leads in the period \u2014 digest skipped.", request: request2, response: { skipped: true }, skipped: true };
1774
- const recipients = await teamRecipients({
1807
+ const { recipients } = await teamRecipients({
1775
1808
  memberIds: ((_d = step.settings) == null ? void 0 : _d.members) || [],
1776
1809
  organization: workflow.organization,
1777
1810
  read
@@ -1807,7 +1840,7 @@ var drawbridge_default2 = {
1807
1840
  var _a;
1808
1841
  const memberIds = ((_a = step.settings) == null ? void 0 : _a.members) || [];
1809
1842
  const request2 = { members: memberIds.length };
1810
- const recipients = await teamRecipients({ memberIds, organization: workflow.organization, read });
1843
+ const { org, recipients } = await teamRecipients({ memberIds, organization: workflow.organization, read });
1811
1844
  if (!recipients.length) {
1812
1845
  return {
1813
1846
  message: "No owner or accepted member with an email address \u2014 team notification skipped.",
@@ -1817,32 +1850,37 @@ var drawbridge_default2 = {
1817
1850
  };
1818
1851
  }
1819
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 : "");
1820
1860
  return {
1821
1861
  message: "Team notification queued for " + recipients.length + " recipient(s).",
1822
1862
  request: request2,
1823
1863
  response: { notified: recipients.length },
1824
- writes: recipients.map((member) => {
1825
- var _a2, _b;
1826
- return {
1827
- ...queueNotification({
1828
- audience: "member",
1829
- // Per workflow, recipient AND bucket, so one recipient's damper
1830
- // can never swallow another's mail and a later bucket is never
1831
- // mistaken for a duplicate of an earlier one.
1832
- key: "team.notify." + workflow.id + "." + member.id + "." + bucket,
1833
- message: interpolate((_a2 = step.settings) == null ? void 0 : _a2.message, context),
1834
- organization: workflow.organization,
1835
- send: { type: "email", email: member.email },
1836
- title: interpolate((_b = step.settings) == null ? void 0 : _b.subject, context),
1837
- workflow: workflow.id
1838
- }),
1839
- // E11000 IS THE DAMPER WORKING: this recipient has already been
1840
- // told within the bucket. Declared per write rather than assumed by
1841
- // the shell, because on every other write here a duplicate key is a
1842
- // real failure.
1843
- ignoreDuplicate: true
1844
- };
1845
- })
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
+ }))
1846
1884
  };
1847
1885
  },
1848
1886
  // Drawbridge sends lead-facing email itself — no merchant provider gates
@@ -2248,14 +2286,24 @@ var drawbridge_default2 = {
2248
2286
  }),
2249
2287
  // To organization MEMBERS. Never suppressed — an entrant's opt-out must
2250
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.
2251
2299
  notify: () => ({
2252
2300
  hook: "email.notify",
2253
2301
  key: "Email \u2014 Notification",
2254
2302
  queue: "notification",
2255
2303
  settings: {
2256
2304
  members: { of: "string", type: "array" },
2257
- message: { required: true, type: "string" },
2258
- subject: { required: true, type: "string" }
2305
+ message: { type: "string" },
2306
+ subject: { type: "string" }
2259
2307
  },
2260
2308
  triggers: ["lead.insert"],
2261
2309
  // Zero is a PRICE, and a deliberate one. Declared rather than omitted