@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/connections/index.cjs +230 -106
- package/dist/connections/index.d.cts +167 -17
- package/dist/connections/index.d.ts +167 -17
- package/dist/connections/index.js +230 -106
- package/dist/connections/oauth.cjs +8 -2
- package/dist/connections/oauth.d.cts +14 -3
- package/dist/connections/oauth.d.ts +14 -3
- package/dist/connections/oauth.js +8 -2
- package/dist/pricing.cjs +727 -695
- package/dist/pricing.d.cts +53 -5
- package/dist/pricing.d.ts +53 -5
- package/dist/pricing.js +726 -695
- package/dist/providers.cjs +230 -106
- package/dist/providers.d.cts +4 -4
- package/dist/providers.d.ts +4 -4
- package/dist/providers.js +230 -106
- package/dist/twilio.cjs +51 -0
- package/dist/twilio.d.cts +67 -0
- package/dist/twilio.d.ts +67 -0
- package/dist/twilio.js +51 -0
- package/package.json +2 -2
|
@@ -390,8 +390,14 @@ var authToken = async ({
|
|
|
390
390
|
});
|
|
391
391
|
const body = await response.json().catch(() => ({}));
|
|
392
392
|
if (!response.ok) {
|
|
393
|
-
throw
|
|
394
|
-
|
|
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
|
|
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
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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: {
|
|
2249
|
-
subject: {
|
|
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
|
|
2665
|
-
lifecycle
|
|
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
|
|
98
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
73
|
-
|
|
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");
|