@drawbridge/drawbridge-utils 0.0.135 → 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
@@ -987,6 +987,74 @@ var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fi
987
987
  </defs>
988
988
  </svg>`;
989
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
+
990
1058
  // lib/features.js
991
1059
  var page = {
992
1060
  qrcode: {
@@ -1449,74 +1517,6 @@ var conversionRate = (subscription) => {
1449
1517
  return ((_a = resolvePlan(subscription)) == null ? void 0 : _a.conversion) ?? free.conversion;
1450
1518
  };
1451
1519
 
1452
- // lib/transactions.js
1453
- import { currentTraceId } from "@drawbridge/drawbridge-telemetry";
1454
-
1455
- // lib/billing.js
1456
- import { createLogger } from "@drawbridge/drawbridge-telemetry";
1457
- var logger = createLogger();
1458
- var MARKUP = 1.3;
1459
- var cost = {
1460
- // gemini-3.5-flash — verified against Google's pricing page 2026-07-09:
1461
- // $0.15 cached / $1.50 input / $9.00 output per 1M tokens (thinking billed at
1462
- // output). ~3.6x the retired 2.5-flash output rate.
1463
- "gemini-3.5-flash": {
1464
- cached: 15,
1465
- input: 150,
1466
- output: 900
1467
- },
1468
- // gemini-3.5-flash-lite — verified against Google's pricing page 2026-08-20:
1469
- // $0.03 cached / $0.30 input / $2.50 output per 1M tokens (thinking billed at
1470
- // output). A fifth of flash on input, ~a quarter on output. Growth's assistant
1471
- // ranks the feed on this tier — one call per page — so its rows were the
1472
- // unpriced ones until now.
1473
- "gemini-3.5-flash-lite": {
1474
- cached: 3,
1475
- input: 30,
1476
- output: 250
1477
- },
1478
- "gemini-2.5-flash": {
1479
- cached: 3,
1480
- input: 30,
1481
- output: 250
1482
- },
1483
- "gemini-2.5-flash-image": {
1484
- cached: 3,
1485
- input: 30,
1486
- output: 3e3
1487
- },
1488
- // gemini-3-pro-image-preview — verified against Google's pricing page
1489
- // 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
1490
- // tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
1491
- // 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
1492
- // that reproduces the per-image price from the tokens usageMetadata
1493
- // reports. Growth's hero generation runs this model today.
1494
- "gemini-3-pro-image-preview": {
1495
- cached: 20,
1496
- input: 200,
1497
- output: 12e3
1498
- }
1499
- };
1500
- var toolCost = {
1501
- search: 3.5
1502
- };
1503
- var toolPricing = Object.fromEntries(
1504
- Object.entries(toolCost).map(([tool, value]) => [
1505
- tool,
1506
- Math.ceil(value * MARKUP)
1507
- ])
1508
- );
1509
- var pricing = Object.fromEntries(
1510
- Object.entries(cost).map(([model, rates]) => [
1511
- model,
1512
- {
1513
- cached: Math.round(rates.cached * MARKUP),
1514
- input: Math.round(rates.input * MARKUP),
1515
- output: Math.round(rates.output * MARKUP)
1516
- }
1517
- ])
1518
- );
1519
-
1520
1520
  // lib/pricing.js
1521
1521
  var emailPlans = {
1522
1522
  essentials50k: {
@@ -1586,6 +1586,33 @@ var sending = {
1586
1586
  plan: emailPlan,
1587
1587
  plans: emailPlans,
1588
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
1589
1616
  }
1590
1617
  };
1591
1618
  var channels = {
@@ -1601,6 +1628,9 @@ var channels = {
1601
1628
  includedInAllowance: false
1602
1629
  }
1603
1630
  };
1631
+ var MINIMUM_ACTION_CENTS = Math.round(
1632
+ sending.sms.segmentCostCents / channels.sms.actionsPerSegment * MARKUP * 1e3
1633
+ ) / 1e3;
1604
1634
 
1605
1635
  // lib/connections/providers/drawbridge.js
1606
1636
  var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
@@ -1625,13 +1655,16 @@ var teamRecipients = async ({ memberIds = [], organization: organization2, read
1625
1655
  ]
1626
1656
  }) : [];
1627
1657
  const seen = /* @__PURE__ */ new Set();
1628
- return [owner, ...members].filter((member) => {
1629
- if (!(member == null ? void 0 : member.id) || !(member == null ? void 0 : member.email)) return false;
1630
- const address = member.email.toLowerCase();
1631
- if (seen.has(address)) return false;
1632
- seen.add(address);
1633
- return true;
1634
- });
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
+ };
1635
1668
  };
1636
1669
  var queueNotification = (data2) => ({ collection: "notification", data: data2, operation: "create" });
1637
1670
  var drawbridge_default2 = {
@@ -1707,7 +1740,7 @@ var drawbridge_default2 = {
1707
1740
  const count = Number((counted == null ? void 0 : counted.count) || 0);
1708
1741
  const request2 = { campaign, count, days };
1709
1742
  if (!count) return { message: "No new leads in the period \u2014 digest skipped.", request: request2, response: { skipped: true }, skipped: true };
1710
- const recipients = await teamRecipients({
1743
+ const { recipients } = await teamRecipients({
1711
1744
  memberIds: ((_d = step.settings) == null ? void 0 : _d.members) || [],
1712
1745
  organization: workflow.organization,
1713
1746
  read
@@ -1743,7 +1776,7 @@ var drawbridge_default2 = {
1743
1776
  var _a;
1744
1777
  const memberIds = ((_a = step.settings) == null ? void 0 : _a.members) || [];
1745
1778
  const request2 = { members: memberIds.length };
1746
- const recipients = await teamRecipients({ memberIds, organization: workflow.organization, read });
1779
+ const { org, recipients } = await teamRecipients({ memberIds, organization: workflow.organization, read });
1747
1780
  if (!recipients.length) {
1748
1781
  return {
1749
1782
  message: "No owner or accepted member with an email address \u2014 team notification skipped.",
@@ -1753,32 +1786,37 @@ var drawbridge_default2 = {
1753
1786
  };
1754
1787
  }
1755
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 : "");
1756
1796
  return {
1757
1797
  message: "Team notification queued for " + recipients.length + " recipient(s).",
1758
1798
  request: request2,
1759
1799
  response: { notified: recipients.length },
1760
- writes: recipients.map((member) => {
1761
- var _a2, _b;
1762
- return {
1763
- ...queueNotification({
1764
- audience: "member",
1765
- // Per workflow, recipient AND bucket, so one recipient's damper
1766
- // can never swallow another's mail and a later bucket is never
1767
- // mistaken for a duplicate of an earlier one.
1768
- key: "team.notify." + workflow.id + "." + member.id + "." + bucket,
1769
- message: interpolate((_a2 = step.settings) == null ? void 0 : _a2.message, context),
1770
- organization: workflow.organization,
1771
- send: { type: "email", email: member.email },
1772
- title: interpolate((_b = step.settings) == null ? void 0 : _b.subject, context),
1773
- workflow: workflow.id
1774
- }),
1775
- // E11000 IS THE DAMPER WORKING: this recipient has already been
1776
- // told within the bucket. Declared per write rather than assumed by
1777
- // the shell, because on every other write here a duplicate key is a
1778
- // real failure.
1779
- ignoreDuplicate: true
1780
- };
1781
- })
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
+ }))
1782
1820
  };
1783
1821
  },
1784
1822
  // Drawbridge sends lead-facing email itself — no merchant provider gates
@@ -2184,14 +2222,24 @@ var drawbridge_default2 = {
2184
2222
  }),
2185
2223
  // To organization MEMBERS. Never suppressed — an entrant's opt-out must
2186
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.
2187
2235
  notify: () => ({
2188
2236
  hook: "email.notify",
2189
2237
  key: "Email \u2014 Notification",
2190
2238
  queue: "notification",
2191
2239
  settings: {
2192
2240
  members: { of: "string", type: "array" },
2193
- message: { required: true, type: "string" },
2194
- subject: { required: true, type: "string" }
2241
+ message: { type: "string" },
2242
+ subject: { type: "string" }
2195
2243
  },
2196
2244
  triggers: ["lead.insert"],
2197
2245
  // Zero is a PRICE, and a deliberate one. Declared rather than omitted
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "tinycolor2": "1.6.0"
14
14
  },
15
15
  "devDependencies": {
16
- "@drawbridge/drawbridge-agents": "0.1.40",
16
+ "@drawbridge/drawbridge-agents": "0.1.47",
17
17
  "@node-oauth/oauth2-server": "5.3.0",
18
18
  "tsup": "8.5.1",
19
19
  "typescript": "5.9.3",
@@ -215,5 +215,5 @@
215
215
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
216
216
  },
217
217
  "types": "dist/index.d.ts",
218
- "version": "0.0.135"
218
+ "version": "0.0.136"
219
219
  }