@drawbridge/drawbridge-utils 0.0.115 → 0.0.117
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 +338 -301
- package/dist/connections/index.d.cts +390 -323
- package/dist/connections/index.d.ts +390 -323
- package/dist/connections/index.js +338 -301
- package/package.json +1 -1
|
@@ -408,7 +408,13 @@ var attentive_default2 = {
|
|
|
408
408
|
// connection DOCUMENT carries its own `errors` array and is spread OVER the
|
|
409
409
|
// resolved manifest downstream, so a top-level one would never render.
|
|
410
410
|
content: {
|
|
411
|
-
|
|
411
|
+
// SAYS WHAT ACTUALLY HAPPENS. This used to promise that disconnecting
|
|
412
|
+
// revokes Drawbridge's access, and it cannot: Attentive documents no
|
|
413
|
+
// revocation endpoint, and their authentication page states an access
|
|
414
|
+
// token "does not expire". So the grant survives a disconnect forever
|
|
415
|
+
// unless the merchant removes the integration at Attentive, and the copy
|
|
416
|
+
// has to say so rather than let them believe otherwise.
|
|
417
|
+
confirm: "Disconnecting removes Drawbridge's stored Attentive token. Attentive does not offer a way for us to revoke it, so remove the Drawbridge integration in Attentive as well if you want its access fully withdrawn. Your subscribers stay in both Attentive and Drawbridge \u2014 neither list is deleted.",
|
|
412
418
|
description: [
|
|
413
419
|
"Attentive is where your SMS marketing lives, and this connection is becoming the way your Drawbridge contacts sync into an Attentive segment.",
|
|
414
420
|
"You authorize Drawbridge from inside Attentive and can revoke that access there at any time. Drawbridge never sees or stores your Attentive password.",
|
|
@@ -449,8 +455,15 @@ var attentive_default2 = {
|
|
|
449
455
|
// account-identity endpoint to enrich them with — Klaviyo's connect
|
|
450
456
|
// reads the account name back; this has nothing cited to read. The
|
|
451
457
|
// callback stores the tokens and skips enrichment on `unimplemented`.
|
|
452
|
-
|
|
453
|
-
|
|
458
|
+
// FALSE, NOT {}. `{}` means "supported, implemented in the repo with the
|
|
459
|
+
// dependencies", and nothing anywhere implements either of these —
|
|
460
|
+
// there is nothing for them to do. The exchange already yields the
|
|
461
|
+
// tokens and Attentive documents no account-identity endpoint to
|
|
462
|
+
// enrich them with, so connect has nothing to add; and they document
|
|
463
|
+
// no revocation endpoint at all, so disconnect has nothing to call.
|
|
464
|
+
// Recorded as a decision rather than left as an unkept promise.
|
|
465
|
+
connect: false,
|
|
466
|
+
disconnect: false,
|
|
454
467
|
probe: false,
|
|
455
468
|
scopes: false,
|
|
456
469
|
// THE ONE THING WRAPPED, and it is about the response rather than the
|
|
@@ -553,6 +566,197 @@ var attentive_default2 = {
|
|
|
553
566
|
title: "Attentive"
|
|
554
567
|
};
|
|
555
568
|
|
|
569
|
+
// lib/http.js
|
|
570
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
571
|
+
var request = async ({
|
|
572
|
+
body,
|
|
573
|
+
headers = {},
|
|
574
|
+
method = "GET",
|
|
575
|
+
query,
|
|
576
|
+
timeout = DEFAULT_TIMEOUT_MS,
|
|
577
|
+
type = "json",
|
|
578
|
+
url
|
|
579
|
+
}) => {
|
|
580
|
+
const fullUrl = new URL(url);
|
|
581
|
+
if (query) {
|
|
582
|
+
Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
|
|
583
|
+
}
|
|
584
|
+
;
|
|
585
|
+
const isForm = type === "form";
|
|
586
|
+
const response = await fetch(fullUrl.toString(), {
|
|
587
|
+
method,
|
|
588
|
+
headers: {
|
|
589
|
+
"Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
|
|
590
|
+
...headers
|
|
591
|
+
},
|
|
592
|
+
signal: AbortSignal.timeout(timeout),
|
|
593
|
+
...body !== void 0 && {
|
|
594
|
+
body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
if (!response.ok) {
|
|
598
|
+
const text2 = await response.text().catch(() => "");
|
|
599
|
+
const error = new Error(text2 || response.statusText);
|
|
600
|
+
error.status = response.status;
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
;
|
|
604
|
+
const text = await response.text();
|
|
605
|
+
try {
|
|
606
|
+
return text ? JSON.parse(text) : null;
|
|
607
|
+
} catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// lib/hubspot.js
|
|
613
|
+
var HUBSPOT_BASE = "https://api.hubapi.com";
|
|
614
|
+
var hubspotRequest = ({ body, fetcher, method, path, query, token }) => {
|
|
615
|
+
return (fetcher || request)({
|
|
616
|
+
body,
|
|
617
|
+
headers: {
|
|
618
|
+
"Authorization": "Bearer " + (token || process.env.HUBSPOT_ACCESS_TOKEN)
|
|
619
|
+
},
|
|
620
|
+
method,
|
|
621
|
+
query,
|
|
622
|
+
url: HUBSPOT_BASE + path
|
|
623
|
+
});
|
|
624
|
+
};
|
|
625
|
+
var UTM_PROPERTIES = {
|
|
626
|
+
campaign: "utm_campaign",
|
|
627
|
+
content: "utm_content",
|
|
628
|
+
id: "utm_id",
|
|
629
|
+
medium: "utm_medium",
|
|
630
|
+
source: "utm_source",
|
|
631
|
+
term: "utm_term"
|
|
632
|
+
};
|
|
633
|
+
var CLICK_PROPERTIES = {
|
|
634
|
+
fbclid: "hs_facebook_click_id",
|
|
635
|
+
gclid: "hs_google_click_id",
|
|
636
|
+
liFatId: "hs_linkedin_click_id",
|
|
637
|
+
msclkid: "hs_bing_click_id",
|
|
638
|
+
ttclid: "hs_tiktok_click_id"
|
|
639
|
+
};
|
|
640
|
+
var DROPPABLE = new Set(Object.values(UTM_PROPERTIES));
|
|
641
|
+
var isUtmProperty = (key) => DROPPABLE.has(key);
|
|
642
|
+
var toProperties = ({ email, firstName, lastName, utm }) => {
|
|
643
|
+
var _a;
|
|
644
|
+
const properties = {};
|
|
645
|
+
if (email !== void 0) properties.email = email;
|
|
646
|
+
if (firstName !== void 0) properties.firstname = firstName;
|
|
647
|
+
if (lastName !== void 0) properties.lastname = lastName;
|
|
648
|
+
if (utm) {
|
|
649
|
+
for (const [key, property] of Object.entries(UTM_PROPERTIES)) {
|
|
650
|
+
if (utm[key]) properties[property] = utm[key];
|
|
651
|
+
}
|
|
652
|
+
;
|
|
653
|
+
for (const [key, property] of Object.entries(CLICK_PROPERTIES)) {
|
|
654
|
+
if ((_a = utm.click) == null ? void 0 : _a[key]) properties[property] = utm.click[key];
|
|
655
|
+
}
|
|
656
|
+
;
|
|
657
|
+
}
|
|
658
|
+
;
|
|
659
|
+
return properties;
|
|
660
|
+
};
|
|
661
|
+
var send = async ({ doc, fetcher, method, path, token }) => {
|
|
662
|
+
const properties = toProperties(doc);
|
|
663
|
+
try {
|
|
664
|
+
return await hubspotRequest({
|
|
665
|
+
body: { properties },
|
|
666
|
+
fetcher,
|
|
667
|
+
method,
|
|
668
|
+
path,
|
|
669
|
+
token
|
|
670
|
+
});
|
|
671
|
+
} catch (error) {
|
|
672
|
+
const enriched = Object.keys(properties).some(isUtmProperty);
|
|
673
|
+
if ((error == null ? void 0 : error.status) !== 400 || !enriched) throw error;
|
|
674
|
+
return await hubspotRequest({
|
|
675
|
+
body: {
|
|
676
|
+
properties: Object.fromEntries(
|
|
677
|
+
Object.entries(properties).filter(([key]) => !isUtmProperty(key))
|
|
678
|
+
)
|
|
679
|
+
},
|
|
680
|
+
fetcher,
|
|
681
|
+
method,
|
|
682
|
+
path,
|
|
683
|
+
token
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
var lookup = async ({ email, fetcher, token }) => {
|
|
688
|
+
var _a, _b;
|
|
689
|
+
if (!token || !email) return;
|
|
690
|
+
try {
|
|
691
|
+
const body = await hubspotRequest({
|
|
692
|
+
body: {
|
|
693
|
+
filterGroups: [
|
|
694
|
+
{
|
|
695
|
+
filters: [
|
|
696
|
+
{
|
|
697
|
+
operator: "EQ",
|
|
698
|
+
propertyName: "email",
|
|
699
|
+
value: email
|
|
700
|
+
}
|
|
701
|
+
]
|
|
702
|
+
}
|
|
703
|
+
],
|
|
704
|
+
limit: 1,
|
|
705
|
+
properties: ["email"]
|
|
706
|
+
},
|
|
707
|
+
fetcher,
|
|
708
|
+
method: "POST",
|
|
709
|
+
path: "/crm/v3/objects/contacts/search",
|
|
710
|
+
token
|
|
711
|
+
});
|
|
712
|
+
return (_b = (_a = body == null ? void 0 : body.results) == null ? void 0 : _a[0]) == null ? void 0 : _b.id;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
var contacts = {
|
|
717
|
+
// FORGET A CONTACT, by id or by email. Account deletion — the caller had
|
|
718
|
+
// to search then remove, which is one round trip it should not have to
|
|
719
|
+
// know about.
|
|
720
|
+
remove: async ({ email, fetcher, id, token }) => {
|
|
721
|
+
const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
|
|
722
|
+
if (!key) return;
|
|
723
|
+
const contact = id || await lookup({ email, fetcher, token: key });
|
|
724
|
+
if (!contact) return;
|
|
725
|
+
return hubspotRequest({
|
|
726
|
+
fetcher,
|
|
727
|
+
method: "DELETE",
|
|
728
|
+
path: "/crm/v3/objects/contacts/" + contact,
|
|
729
|
+
token: key
|
|
730
|
+
});
|
|
731
|
+
},
|
|
732
|
+
// Connect an account to its contact by email, creating it if absent, and
|
|
733
|
+
// return the contact id. Unlike SendGrid, HubSpot renames a contact's
|
|
734
|
+
// email in place, so an email change is a plain PATCH on the cached id —
|
|
735
|
+
// no delete-old-then-create-new.
|
|
736
|
+
//
|
|
737
|
+
// Prefer the cached hubspotId; fall back to a search; create last.
|
|
738
|
+
sync: async ({ doc, fetcher, token }) => {
|
|
739
|
+
var _a, _b;
|
|
740
|
+
const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
|
|
741
|
+
if (!key) return;
|
|
742
|
+
if (doc == null ? void 0 : doc.hubspotId) {
|
|
743
|
+
try {
|
|
744
|
+
return (_a = await send({ doc, fetcher, method: "PATCH", path: "/crm/v3/objects/contacts/" + doc.hubspotId, token: key })) == null ? void 0 : _a.id;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if ((error == null ? void 0 : error.status) !== 404) throw error;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const existing = await lookup({ email: doc == null ? void 0 : doc.email, fetcher, token: key });
|
|
750
|
+
return (_b = await send({
|
|
751
|
+
doc,
|
|
752
|
+
fetcher,
|
|
753
|
+
method: existing ? "PATCH" : "POST",
|
|
754
|
+
path: existing ? "/crm/v3/objects/contacts/" + existing : "/crm/v3/objects/contacts",
|
|
755
|
+
token: key
|
|
756
|
+
})) == null ? void 0 : _b.id;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
556
760
|
// lib/connections/icons/drawbridge.js
|
|
557
761
|
var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
558
762
|
<rect width="500" height="500" fill="#BAEC5F"/>
|
|
@@ -1190,7 +1394,15 @@ var drawbridge_default2 = {
|
|
|
1190
1394
|
token: false
|
|
1191
1395
|
},
|
|
1192
1396
|
commerce: false,
|
|
1193
|
-
|
|
1397
|
+
// DRAWBRIDGE'S OWN CRM. Not a merchant's — this keeps our HubSpot portal in
|
|
1398
|
+
// step with account signups, and drawbridge-sync's user stream calls it.
|
|
1399
|
+
//
|
|
1400
|
+
// A real implementation here rather than `{}` because the bodies are pure
|
|
1401
|
+
// HTTP against a token: no controller, no queue, nothing that would have to
|
|
1402
|
+
// live in the service. lib/hubspot.js holds them, beside lib/sendgrid.js
|
|
1403
|
+
// and lib/twilio.js, which are the same kind of thing — vendor clients for
|
|
1404
|
+
// accounts DRAWBRIDGE holds rather than ones a merchant connects.
|
|
1405
|
+
contacts,
|
|
1194
1406
|
email: {
|
|
1195
1407
|
digest: {},
|
|
1196
1408
|
// To organization members. NEVER suppressed and never billed: an
|
|
@@ -1215,6 +1427,17 @@ var drawbridge_default2 = {
|
|
|
1215
1427
|
icon: drawbridge_default,
|
|
1216
1428
|
// PRIVATE: never in the catalog, always available to the builder.
|
|
1217
1429
|
private: true,
|
|
1430
|
+
// NOTHING, and HUBSPOT_ACCESS_TOKEN in particular must not be here.
|
|
1431
|
+
//
|
|
1432
|
+
// `requires` gates AVAILABILITY: a name in it that is unset removes the whole
|
|
1433
|
+
// connection. This one is private and contributes every base workflow step —
|
|
1434
|
+
// email.send, sms.send, segment.sync — so gating it on a CRM token would take
|
|
1435
|
+
// all of them away from any deployment without a HubSpot portal, to protect a
|
|
1436
|
+
// sync that is best-effort and already no-ops without a token.
|
|
1437
|
+
//
|
|
1438
|
+
// The test 'a vendor is only available when its environment is configured'
|
|
1439
|
+
// caught exactly that: availableConnections({}) went from [ 'drawbridge' ] to
|
|
1440
|
+
// empty the moment this was added.
|
|
1218
1441
|
requires: [],
|
|
1219
1442
|
slug: "drawbridge",
|
|
1220
1443
|
// Always on. There is no credential that could go bad and no configuration a
|
|
@@ -1338,251 +1561,6 @@ var drawbridge_default2 = {
|
|
|
1338
1561
|
title: "Drawbridge"
|
|
1339
1562
|
};
|
|
1340
1563
|
|
|
1341
|
-
// lib/http.js
|
|
1342
|
-
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
1343
|
-
var request = async ({
|
|
1344
|
-
body,
|
|
1345
|
-
headers = {},
|
|
1346
|
-
method = "GET",
|
|
1347
|
-
query,
|
|
1348
|
-
timeout = DEFAULT_TIMEOUT_MS,
|
|
1349
|
-
type = "json",
|
|
1350
|
-
url
|
|
1351
|
-
}) => {
|
|
1352
|
-
const fullUrl = new URL(url);
|
|
1353
|
-
if (query) {
|
|
1354
|
-
Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
|
|
1355
|
-
}
|
|
1356
|
-
;
|
|
1357
|
-
const isForm = type === "form";
|
|
1358
|
-
const response = await fetch(fullUrl.toString(), {
|
|
1359
|
-
method,
|
|
1360
|
-
headers: {
|
|
1361
|
-
"Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
|
|
1362
|
-
...headers
|
|
1363
|
-
},
|
|
1364
|
-
signal: AbortSignal.timeout(timeout),
|
|
1365
|
-
...body !== void 0 && {
|
|
1366
|
-
body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
|
|
1367
|
-
}
|
|
1368
|
-
});
|
|
1369
|
-
if (!response.ok) {
|
|
1370
|
-
const text2 = await response.text().catch(() => "");
|
|
1371
|
-
const error = new Error(text2 || response.statusText);
|
|
1372
|
-
error.status = response.status;
|
|
1373
|
-
throw error;
|
|
1374
|
-
}
|
|
1375
|
-
;
|
|
1376
|
-
const text = await response.text();
|
|
1377
|
-
try {
|
|
1378
|
-
return text ? JSON.parse(text) : null;
|
|
1379
|
-
} catch {
|
|
1380
|
-
return null;
|
|
1381
|
-
}
|
|
1382
|
-
};
|
|
1383
|
-
|
|
1384
|
-
// lib/connections/hubspot.js
|
|
1385
|
-
var HUBSPOT_BASE = "https://api.hubapi.com";
|
|
1386
|
-
var hubspotRequest = ({ body, fetcher, method, path, query, token }) => {
|
|
1387
|
-
return (fetcher || request)({
|
|
1388
|
-
body,
|
|
1389
|
-
headers: {
|
|
1390
|
-
"Authorization": "Bearer " + (token || process.env.HUBSPOT_ACCESS_TOKEN)
|
|
1391
|
-
},
|
|
1392
|
-
method,
|
|
1393
|
-
query,
|
|
1394
|
-
url: HUBSPOT_BASE + path
|
|
1395
|
-
});
|
|
1396
|
-
};
|
|
1397
|
-
var UTM_PROPERTIES = {
|
|
1398
|
-
campaign: "utm_campaign",
|
|
1399
|
-
content: "utm_content",
|
|
1400
|
-
id: "utm_id",
|
|
1401
|
-
medium: "utm_medium",
|
|
1402
|
-
source: "utm_source",
|
|
1403
|
-
term: "utm_term"
|
|
1404
|
-
};
|
|
1405
|
-
var CLICK_PROPERTIES = {
|
|
1406
|
-
fbclid: "hs_facebook_click_id",
|
|
1407
|
-
gclid: "hs_google_click_id",
|
|
1408
|
-
liFatId: "hs_linkedin_click_id",
|
|
1409
|
-
msclkid: "hs_bing_click_id",
|
|
1410
|
-
ttclid: "hs_tiktok_click_id"
|
|
1411
|
-
};
|
|
1412
|
-
var DROPPABLE = new Set(Object.values(UTM_PROPERTIES));
|
|
1413
|
-
var isUtmProperty = (key) => DROPPABLE.has(key);
|
|
1414
|
-
var toProperties = ({ email, firstName, lastName, utm }) => {
|
|
1415
|
-
var _a;
|
|
1416
|
-
const properties = {};
|
|
1417
|
-
if (email !== void 0) properties.email = email;
|
|
1418
|
-
if (firstName !== void 0) properties.firstname = firstName;
|
|
1419
|
-
if (lastName !== void 0) properties.lastname = lastName;
|
|
1420
|
-
if (utm) {
|
|
1421
|
-
for (const [key, property] of Object.entries(UTM_PROPERTIES)) {
|
|
1422
|
-
if (utm[key]) properties[property] = utm[key];
|
|
1423
|
-
}
|
|
1424
|
-
;
|
|
1425
|
-
for (const [key, property] of Object.entries(CLICK_PROPERTIES)) {
|
|
1426
|
-
if ((_a = utm.click) == null ? void 0 : _a[key]) properties[property] = utm.click[key];
|
|
1427
|
-
}
|
|
1428
|
-
;
|
|
1429
|
-
}
|
|
1430
|
-
;
|
|
1431
|
-
return properties;
|
|
1432
|
-
};
|
|
1433
|
-
var send = async ({ doc, fetcher, method, path, token }) => {
|
|
1434
|
-
const properties = toProperties(doc);
|
|
1435
|
-
try {
|
|
1436
|
-
return await hubspotRequest({
|
|
1437
|
-
body: { properties },
|
|
1438
|
-
fetcher,
|
|
1439
|
-
method,
|
|
1440
|
-
path,
|
|
1441
|
-
token
|
|
1442
|
-
});
|
|
1443
|
-
} catch (error) {
|
|
1444
|
-
const enriched = Object.keys(properties).some(isUtmProperty);
|
|
1445
|
-
if ((error == null ? void 0 : error.status) !== 400 || !enriched) throw error;
|
|
1446
|
-
return await hubspotRequest({
|
|
1447
|
-
body: {
|
|
1448
|
-
properties: Object.fromEntries(
|
|
1449
|
-
Object.entries(properties).filter(([key]) => !isUtmProperty(key))
|
|
1450
|
-
)
|
|
1451
|
-
},
|
|
1452
|
-
fetcher,
|
|
1453
|
-
method,
|
|
1454
|
-
path,
|
|
1455
|
-
token
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
|
-
};
|
|
1459
|
-
var lookup = async ({ email, fetcher, token }) => {
|
|
1460
|
-
var _a, _b;
|
|
1461
|
-
if (!token || !email) return;
|
|
1462
|
-
try {
|
|
1463
|
-
const body = await hubspotRequest({
|
|
1464
|
-
body: {
|
|
1465
|
-
filterGroups: [
|
|
1466
|
-
{
|
|
1467
|
-
filters: [
|
|
1468
|
-
{
|
|
1469
|
-
operator: "EQ",
|
|
1470
|
-
propertyName: "email",
|
|
1471
|
-
value: email
|
|
1472
|
-
}
|
|
1473
|
-
]
|
|
1474
|
-
}
|
|
1475
|
-
],
|
|
1476
|
-
limit: 1,
|
|
1477
|
-
properties: ["email"]
|
|
1478
|
-
},
|
|
1479
|
-
fetcher,
|
|
1480
|
-
method: "POST",
|
|
1481
|
-
path: "/crm/v3/objects/contacts/search",
|
|
1482
|
-
token
|
|
1483
|
-
});
|
|
1484
|
-
return (_b = (_a = body == null ? void 0 : body.results) == null ? void 0 : _a[0]) == null ? void 0 : _b.id;
|
|
1485
|
-
} catch (error) {
|
|
1486
|
-
}
|
|
1487
|
-
};
|
|
1488
|
-
var hubspot_default = {
|
|
1489
|
-
auth: {
|
|
1490
|
-
// A Private App token from our own portal. Nothing to connect, nothing to
|
|
1491
|
-
// consent to, and no merchant involved.
|
|
1492
|
-
type: "none"
|
|
1493
|
-
},
|
|
1494
|
-
content: {
|
|
1495
|
-
confirm: "This connection is part of Drawbridge and cannot be disconnected.",
|
|
1496
|
-
description: [
|
|
1497
|
-
"Drawbridge keeps its own HubSpot portal in step with account signups, so the campaign a customer arrived on is on their contact record."
|
|
1498
|
-
],
|
|
1499
|
-
excerpt: "Drawbridge's own CRM sync.",
|
|
1500
|
-
guide: [
|
|
1501
|
-
"Nothing to do. This is internal to Drawbridge."
|
|
1502
|
-
]
|
|
1503
|
-
},
|
|
1504
|
-
exclusive: false,
|
|
1505
|
-
fields: [],
|
|
1506
|
-
group: "contacts",
|
|
1507
|
-
hooks: {
|
|
1508
|
-
auth: {
|
|
1509
|
-
connect: false,
|
|
1510
|
-
disconnect: false,
|
|
1511
|
-
probe: false,
|
|
1512
|
-
scopes: false,
|
|
1513
|
-
token: false
|
|
1514
|
-
},
|
|
1515
|
-
commerce: false,
|
|
1516
|
-
contacts: {
|
|
1517
|
-
// FORGET A CONTACT, by id or by email. Account deletion — the caller had
|
|
1518
|
-
// to search then remove, which is one round trip it should not have to
|
|
1519
|
-
// know about.
|
|
1520
|
-
remove: async ({ email, fetcher, id, token }) => {
|
|
1521
|
-
const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
|
|
1522
|
-
if (!key) return;
|
|
1523
|
-
const contact = id || await lookup({ email, fetcher, token: key });
|
|
1524
|
-
if (!contact) return;
|
|
1525
|
-
return hubspotRequest({
|
|
1526
|
-
fetcher,
|
|
1527
|
-
method: "DELETE",
|
|
1528
|
-
path: "/crm/v3/objects/contacts/" + contact,
|
|
1529
|
-
token: key
|
|
1530
|
-
});
|
|
1531
|
-
},
|
|
1532
|
-
// Connect an account to its contact by email, creating it if absent, and
|
|
1533
|
-
// return the contact id. Unlike SendGrid, HubSpot renames a contact's
|
|
1534
|
-
// email in place, so an email change is a plain PATCH on the cached id —
|
|
1535
|
-
// no delete-old-then-create-new.
|
|
1536
|
-
//
|
|
1537
|
-
// Prefer the cached hubspotId; fall back to a search; create last.
|
|
1538
|
-
sync: async ({ doc, fetcher, token }) => {
|
|
1539
|
-
var _a, _b;
|
|
1540
|
-
const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
|
|
1541
|
-
if (!key) return;
|
|
1542
|
-
if (doc == null ? void 0 : doc.hubspotId) {
|
|
1543
|
-
try {
|
|
1544
|
-
return (_a = await send({ doc, fetcher, method: "PATCH", path: "/crm/v3/objects/contacts/" + doc.hubspotId, token: key })) == null ? void 0 : _a.id;
|
|
1545
|
-
} catch (error) {
|
|
1546
|
-
if ((error == null ? void 0 : error.status) !== 404) throw error;
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
const existing = await lookup({ email: doc == null ? void 0 : doc.email, fetcher, token: key });
|
|
1550
|
-
return (_b = await send({
|
|
1551
|
-
doc,
|
|
1552
|
-
fetcher,
|
|
1553
|
-
method: existing ? "PATCH" : "POST",
|
|
1554
|
-
path: existing ? "/crm/v3/objects/contacts/" + existing : "/crm/v3/objects/contacts",
|
|
1555
|
-
token: key
|
|
1556
|
-
})) == null ? void 0 : _b.id;
|
|
1557
|
-
}
|
|
1558
|
-
},
|
|
1559
|
-
email: false,
|
|
1560
|
-
inbound: false,
|
|
1561
|
-
lifecycle: false,
|
|
1562
|
-
resources: {
|
|
1563
|
-
audiences: false,
|
|
1564
|
-
prices: false,
|
|
1565
|
-
products: false,
|
|
1566
|
-
promotions: false
|
|
1567
|
-
},
|
|
1568
|
-
segment: false,
|
|
1569
|
-
sms: false,
|
|
1570
|
-
webhook: false
|
|
1571
|
-
},
|
|
1572
|
-
icon: drawbridge_default,
|
|
1573
|
-
// Borrowed: the Drawbridge mark, because this is ours and never rendered.
|
|
1574
|
-
private: true,
|
|
1575
|
-
// Absent the token the hooks no-op, so a deployment without a portal simply
|
|
1576
|
-
// contributes nothing rather than failing.
|
|
1577
|
-
requires: ["HUBSPOT_ACCESS_TOKEN"],
|
|
1578
|
-
slug: "hubspot",
|
|
1579
|
-
status: () => "active",
|
|
1580
|
-
// No workflow steps. The hooks are called by the user stream, not the builder.
|
|
1581
|
-
steps: {},
|
|
1582
|
-
tasks: () => [],
|
|
1583
|
-
title: "HubSpot"
|
|
1584
|
-
};
|
|
1585
|
-
|
|
1586
1564
|
// lib/connections/icons/klaviyo.js
|
|
1587
1565
|
var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1588
1566
|
<rect width="500" height="500" fill="white"/>
|
|
@@ -2044,46 +2022,59 @@ var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fil
|
|
|
2044
2022
|
</svg>`;
|
|
2045
2023
|
|
|
2046
2024
|
// lib/connections/mailchimp.js
|
|
2047
|
-
var base = (
|
|
2048
|
-
|
|
2049
|
-
if (!dc || dc === apiKey) throw new Error("That Mailchimp key carries no data centre suffix, so there is no host to call");
|
|
2025
|
+
var base = (dc) => {
|
|
2026
|
+
if (!dc) throw new Error("This Mailchimp connection has no data centre stored, so there is no host to call");
|
|
2050
2027
|
return "https://" + dc + ".api.mailchimp.com/3.0";
|
|
2051
2028
|
};
|
|
2052
2029
|
var mailchimp_default2 = {
|
|
2053
|
-
//
|
|
2054
|
-
//
|
|
2055
|
-
//
|
|
2030
|
+
// OAUTH 2, authorization code. Every url below is quoted from
|
|
2031
|
+
// mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
|
|
2032
|
+
// than remembered.
|
|
2056
2033
|
//
|
|
2057
|
-
//
|
|
2058
|
-
//
|
|
2059
|
-
//
|
|
2034
|
+
// THE METADATA CALL IS MAILCHIMP'S QUIRK and cannot be skipped: an access
|
|
2035
|
+
// token alone cannot call the Marketing API, because every account lives
|
|
2036
|
+
// behind a data-centre prefix (us1, us19...) that only GET
|
|
2037
|
+
// login.mailchimp.com/oauth2/metadata returns — and every subsequent request
|
|
2038
|
+
// needs it in the HOST. That is why auth.connect below is a real function:
|
|
2039
|
+
// the standard exchange does not know where to send anything afterwards.
|
|
2060
2040
|
//
|
|
2061
|
-
//
|
|
2062
|
-
//
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2041
|
+
// No PKCE. No scopes — the docs describe none. And no refresh token: "Mailchimp
|
|
2042
|
+
// Marketing access tokens do not expire, so you don't need to use a
|
|
2043
|
+
// refresh_token", so tokenSettings stores no expiry and isStale reads that as
|
|
2044
|
+
// nothing to refresh toward.
|
|
2065
2045
|
//
|
|
2066
|
-
//
|
|
2067
|
-
//
|
|
2068
|
-
//
|
|
2046
|
+
// auth.token stays false: the exchange is POST form-encoded with grant_type,
|
|
2047
|
+
// client_id, client_secret, redirect_uri and code, which is exactly the
|
|
2048
|
+
// runner's default — nothing to wrap.
|
|
2069
2049
|
auth: {
|
|
2070
|
-
|
|
2050
|
+
oauth: {
|
|
2051
|
+
client: {
|
|
2052
|
+
id: "MAILCHIMP_OAUTH_CLIENT_ID",
|
|
2053
|
+
secret: "MAILCHIMP_OAUTH_CLIENT_SECRET"
|
|
2054
|
+
},
|
|
2055
|
+
urls: {
|
|
2056
|
+
authorize: "https://login.mailchimp.com/oauth2/authorize",
|
|
2057
|
+
redirect: "/api/connection/mailchimp/callback",
|
|
2058
|
+
token: "https://login.mailchimp.com/oauth2/token"
|
|
2059
|
+
}
|
|
2060
|
+
},
|
|
2061
|
+
type: "oauth"
|
|
2071
2062
|
},
|
|
2072
2063
|
// EVERYTHING A MERCHANT READS. `errors` belongs in here rather than at the
|
|
2073
2064
|
// top level because the connection DOCUMENT carries its own `errors` array
|
|
2074
2065
|
// and the document is spread OVER the resolved manifest downstream — a
|
|
2075
2066
|
// top-level one would be replaced by that array and never render.
|
|
2076
2067
|
content: {
|
|
2077
|
-
confirm: "Disconnecting removes
|
|
2068
|
+
confirm: "Disconnecting removes Drawbridge's stored Mailchimp access. You can also remove Drawbridge from the Authorized Apps page in your Mailchimp account. Your contacts stay in both Drawbridge and Mailchimp \u2014 neither list is deleted.",
|
|
2078
2069
|
description: [
|
|
2079
2070
|
"Drawbridge no longer sends email through Mailchimp. Notification email now sends from Drawbridge itself, and verifying a domain under Networking in your organization settings puts your own brand in the from line.",
|
|
2080
|
-
"This connection is becoming the way your Drawbridge contacts sync into a Mailchimp audience. Audience syncing is not live yet, so
|
|
2071
|
+
"This connection is becoming the way your Drawbridge contacts sync into a Mailchimp audience. Audience syncing is not live yet, so connecting today does nothing except choose the audience it will use when it ships."
|
|
2081
2072
|
],
|
|
2082
2073
|
excerpt: "Sync your Drawbridge contacts into a Mailchimp audience.",
|
|
2083
2074
|
guide: [
|
|
2084
|
-
"
|
|
2085
|
-
"
|
|
2086
|
-
"
|
|
2075
|
+
"Press Connect. Drawbridge sends you to Mailchimp to approve access.",
|
|
2076
|
+
"Sign in to Mailchimp if you are not already, and choose the account to connect.",
|
|
2077
|
+
"You come back here to pick the audience your contacts should sync into."
|
|
2087
2078
|
]
|
|
2088
2079
|
},
|
|
2089
2080
|
// Mailchimp and SendGrid shared a group while they were SENDERS, where an org
|
|
@@ -2093,15 +2084,6 @@ var mailchimp_default2 = {
|
|
|
2093
2084
|
exclusive: false,
|
|
2094
2085
|
feature: "organization:connection:mailchimp",
|
|
2095
2086
|
fields: [
|
|
2096
|
-
{
|
|
2097
|
-
input: "password",
|
|
2098
|
-
key: "apiKey",
|
|
2099
|
-
label: "Mailchimp API key",
|
|
2100
|
-
message: "Your Mailchimp API key",
|
|
2101
|
-
placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022-us1",
|
|
2102
|
-
redact: true,
|
|
2103
|
-
required: true
|
|
2104
|
-
},
|
|
2105
2087
|
{
|
|
2106
2088
|
input: "select",
|
|
2107
2089
|
key: "audience",
|
|
@@ -2120,16 +2102,45 @@ var mailchimp_default2 = {
|
|
|
2120
2102
|
// contacts.sync are the first to flip.
|
|
2121
2103
|
hooks: {
|
|
2122
2104
|
auth: {
|
|
2123
|
-
//
|
|
2124
|
-
//
|
|
2125
|
-
|
|
2126
|
-
|
|
2105
|
+
// WHERE THE ACCOUNT LIVES. Not enrichment — without this the connection
|
|
2106
|
+
// is unusable, because the Marketing API host is per-account and only
|
|
2107
|
+
// this call knows it. The callback merges what this returns into the
|
|
2108
|
+
// stored settings, which is how `dc` reaches every later request.
|
|
2109
|
+
//
|
|
2110
|
+
// The header here is `OAuth <token>`, not Bearer — that is specific to
|
|
2111
|
+
// the metadata endpoint. Marketing API calls take Bearer; see the
|
|
2112
|
+
// audiences hook.
|
|
2113
|
+
connect: async ({ fetcher = fetch, tokens }) => {
|
|
2114
|
+
const response = await fetcher("https://login.mailchimp.com/oauth2/metadata", {
|
|
2115
|
+
headers: {
|
|
2116
|
+
authorization: "OAuth " + (tokens == null ? void 0 : tokens.accessToken)
|
|
2117
|
+
},
|
|
2118
|
+
signal: AbortSignal.timeout(15e3)
|
|
2119
|
+
});
|
|
2120
|
+
if (!response.ok) {
|
|
2121
|
+
throw Object.assign(
|
|
2122
|
+
new Error("Mailchimp would not say which data centre this account is on (" + response.status + ")"),
|
|
2123
|
+
{ status: response.status }
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
const body = await response.json();
|
|
2127
|
+
if (!(body == null ? void 0 : body.dc)) throw new Error("Mailchimp returned no data centre for this account");
|
|
2128
|
+
return { dc: body.dc };
|
|
2129
|
+
},
|
|
2130
|
+
// Nothing to call. A merchant revokes Drawbridge from Mailchimp's own
|
|
2131
|
+
// Authorized Apps page; the docs describe no revocation endpoint for us
|
|
2132
|
+
// to call on their behalf.
|
|
2133
|
+
disconnect: false,
|
|
2127
2134
|
probe: false,
|
|
2128
2135
|
scopes: false,
|
|
2129
|
-
//
|
|
2130
|
-
//
|
|
2131
|
-
//
|
|
2132
|
-
|
|
2136
|
+
// The plain exchange. Mailchimp takes the client as FORM FIELDS
|
|
2137
|
+
// (grant_type, client_id, client_secret, redirect_uri, code), which is
|
|
2138
|
+
// the runner's default — so no `basic : true` as Klaviyo needs.
|
|
2139
|
+
//
|
|
2140
|
+
// build() requires an oauth manifest to name this explicitly rather than
|
|
2141
|
+
// letting it default, which caught this file declaring `false` on the
|
|
2142
|
+
// first import after the conversion.
|
|
2143
|
+
token: authToken
|
|
2133
2144
|
},
|
|
2134
2145
|
commerce: false,
|
|
2135
2146
|
contacts: { remove: false, sync: false },
|
|
@@ -2151,15 +2162,14 @@ var mailchimp_default2 = {
|
|
|
2151
2162
|
// successful — the same silent truncation Klaviyo has, at a different
|
|
2152
2163
|
// number. Paged against total_items so an account past a thousand still
|
|
2153
2164
|
// resolves.
|
|
2154
|
-
audiences: async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
|
|
2155
|
-
const
|
|
2165
|
+
audiences: async ({ cursor, fetcher = fetch, limit = 100, search, settings, token }) => {
|
|
2166
|
+
const dc = settings == null ? void 0 : settings.dc;
|
|
2156
2167
|
const count = Math.min(limit, 1e3);
|
|
2157
2168
|
const offset = Number(cursor || 0);
|
|
2158
2169
|
const response = await fetcher(
|
|
2159
|
-
base(
|
|
2170
|
+
base(dc) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
|
|
2160
2171
|
{
|
|
2161
|
-
|
|
2162
|
-
headers: { authorization: "Basic " + Buffer.from("drawbridge:" + key).toString("base64") },
|
|
2172
|
+
headers: { authorization: "Bearer " + token },
|
|
2163
2173
|
signal: AbortSignal.timeout(15e3)
|
|
2164
2174
|
}
|
|
2165
2175
|
);
|
|
@@ -2191,6 +2201,13 @@ var mailchimp_default2 = {
|
|
|
2191
2201
|
webhook: false
|
|
2192
2202
|
},
|
|
2193
2203
|
icon: mailchimp_default,
|
|
2204
|
+
// The OAuth client this deployment registered. Without both, the vendor drops
|
|
2205
|
+
// out of availableConnections rather than offering a Connect button that
|
|
2206
|
+
// cannot complete.
|
|
2207
|
+
requires: [
|
|
2208
|
+
"MAILCHIMP_OAUTH_CLIENT_ID",
|
|
2209
|
+
"MAILCHIMP_OAUTH_CLIENT_SECRET"
|
|
2210
|
+
],
|
|
2194
2211
|
slug: "mailchimp",
|
|
2195
2212
|
// A key with no audience chosen is authenticated and inert. Mailchimp also
|
|
2196
2213
|
// needs its merge fields created on that audience before any Drawbridge total
|
|
@@ -2344,10 +2361,29 @@ var shopify_default2 = {
|
|
|
2344
2361
|
// re-registers rather than answering "is this token still good", and
|
|
2345
2362
|
// scope drift is its own hook because a token can be perfectly valid
|
|
2346
2363
|
// while the grant is too narrow.
|
|
2347
|
-
|
|
2348
|
-
disconnect
|
|
2364
|
+
//
|
|
2365
|
+
// connect and disconnect are FALSE rather than `{}`: there is nothing to
|
|
2366
|
+
// call on either side. The install already hands the callback everything
|
|
2367
|
+
// it stores, and a Shopify grant is withdrawn by UNINSTALLING the app in
|
|
2368
|
+
// Shopify admin — which Drawbridge learns about from the app_uninstalled
|
|
2369
|
+
// webhook rather than by asking. `{}` claimed a body implemented
|
|
2370
|
+
// elsewhere; none exists, and none could.
|
|
2371
|
+
connect: false,
|
|
2372
|
+
disconnect: false,
|
|
2349
2373
|
probe: false,
|
|
2350
|
-
|
|
2374
|
+
// WHETHER THE GRANT IS STILL WIDE ENOUGH. A token can be perfectly valid
|
|
2375
|
+
// and still too narrow — a deploy that adds a scope leaves every existing
|
|
2376
|
+
// install short of it, and no webhook fires to say so.
|
|
2377
|
+
//
|
|
2378
|
+
// The comparison is the vendor's, so it belongs here. Reading WHICH
|
|
2379
|
+
// scopes a store granted is not: that lives in the `shop` collection and
|
|
2380
|
+
// needs a controller, which is precisely what a hook in a published
|
|
2381
|
+
// package must not be handed. The caller reads the grant and passes the
|
|
2382
|
+
// string; this answers what is missing from it.
|
|
2383
|
+
//
|
|
2384
|
+
// `shopify` is injected for the same reason it is everywhere else — this
|
|
2385
|
+
// package cannot import @drawbridge/shopify, which depends on it.
|
|
2386
|
+
scopes: ({ scope, shopify }) => scope ? shopify.oauth.missingScopes(scope) : null,
|
|
2351
2387
|
// Shopify's install grant is exchanged inside its own app flow, not
|
|
2352
2388
|
// through the shared OAuth runner.
|
|
2353
2389
|
token: false
|
|
@@ -2810,10 +2846,12 @@ var webhook_default = {
|
|
|
2810
2846
|
// connect generates a secret rather than proving a credential.
|
|
2811
2847
|
hooks: {
|
|
2812
2848
|
auth: {
|
|
2813
|
-
//
|
|
2814
|
-
//
|
|
2815
|
-
|
|
2816
|
-
|
|
2849
|
+
// FALSE, NOT {}. There is no vendor here at all — connecting mints a
|
|
2850
|
+
// secret and disconnecting clears it, both done by the api's own
|
|
2851
|
+
// handler. `{}` would promise a body implemented elsewhere, and none
|
|
2852
|
+
// exists or could.
|
|
2853
|
+
connect: false,
|
|
2854
|
+
disconnect: false,
|
|
2817
2855
|
probe: false,
|
|
2818
2856
|
scopes: false,
|
|
2819
2857
|
// Nothing to mint. Connecting generates a secret; there is no vendor.
|
|
@@ -3058,7 +3096,6 @@ var stepLabels = (catalog = connections) => Object.fromEntries(
|
|
|
3058
3096
|
var connections = Object.freeze({
|
|
3059
3097
|
attentive: build(attentive_default2),
|
|
3060
3098
|
drawbridge: build(drawbridge_default2),
|
|
3061
|
-
hubspot: build(hubspot_default),
|
|
3062
3099
|
klaviyo: build(klaviyo_default2),
|
|
3063
3100
|
mailchimp: build(mailchimp_default2),
|
|
3064
3101
|
shopify: build(shopify_default2),
|