@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.
@@ -477,7 +477,13 @@ var attentive_default2 = {
477
477
  // connection DOCUMENT carries its own `errors` array and is spread OVER the
478
478
  // resolved manifest downstream, so a top-level one would never render.
479
479
  content: {
480
- confirm: "Disconnecting revokes Drawbridge's access to your Attentive account. Your subscribers stay in both Attentive and Drawbridge \u2014 neither list is deleted.",
480
+ // SAYS WHAT ACTUALLY HAPPENS. This used to promise that disconnecting
481
+ // revokes Drawbridge's access, and it cannot: Attentive documents no
482
+ // revocation endpoint, and their authentication page states an access
483
+ // token "does not expire". So the grant survives a disconnect forever
484
+ // unless the merchant removes the integration at Attentive, and the copy
485
+ // has to say so rather than let them believe otherwise.
486
+ 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.",
481
487
  description: [
482
488
  "Attentive is where your SMS marketing lives, and this connection is becoming the way your Drawbridge contacts sync into an Attentive segment.",
483
489
  "You authorize Drawbridge from inside Attentive and can revoke that access there at any time. Drawbridge never sees or stores your Attentive password.",
@@ -518,8 +524,15 @@ var attentive_default2 = {
518
524
  // account-identity endpoint to enrich them with — Klaviyo's connect
519
525
  // reads the account name back; this has nothing cited to read. The
520
526
  // callback stores the tokens and skips enrichment on `unimplemented`.
521
- connect: {},
522
- disconnect: {},
527
+ // FALSE, NOT {}. `{}` means "supported, implemented in the repo with the
528
+ // dependencies", and nothing anywhere implements either of these —
529
+ // there is nothing for them to do. The exchange already yields the
530
+ // tokens and Attentive documents no account-identity endpoint to
531
+ // enrich them with, so connect has nothing to add; and they document
532
+ // no revocation endpoint at all, so disconnect has nothing to call.
533
+ // Recorded as a decision rather than left as an unkept promise.
534
+ connect: false,
535
+ disconnect: false,
523
536
  probe: false,
524
537
  scopes: false,
525
538
  // THE ONE THING WRAPPED, and it is about the response rather than the
@@ -622,6 +635,197 @@ var attentive_default2 = {
622
635
  title: "Attentive"
623
636
  };
624
637
 
638
+ // lib/http.js
639
+ var DEFAULT_TIMEOUT_MS = 15e3;
640
+ var request = async ({
641
+ body,
642
+ headers = {},
643
+ method = "GET",
644
+ query,
645
+ timeout = DEFAULT_TIMEOUT_MS,
646
+ type = "json",
647
+ url
648
+ }) => {
649
+ const fullUrl = new URL(url);
650
+ if (query) {
651
+ Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
652
+ }
653
+ ;
654
+ const isForm = type === "form";
655
+ const response = await fetch(fullUrl.toString(), {
656
+ method,
657
+ headers: {
658
+ "Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
659
+ ...headers
660
+ },
661
+ signal: AbortSignal.timeout(timeout),
662
+ ...body !== void 0 && {
663
+ body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
664
+ }
665
+ });
666
+ if (!response.ok) {
667
+ const text2 = await response.text().catch(() => "");
668
+ const error = new Error(text2 || response.statusText);
669
+ error.status = response.status;
670
+ throw error;
671
+ }
672
+ ;
673
+ const text = await response.text();
674
+ try {
675
+ return text ? JSON.parse(text) : null;
676
+ } catch {
677
+ return null;
678
+ }
679
+ };
680
+
681
+ // lib/hubspot.js
682
+ var HUBSPOT_BASE = "https://api.hubapi.com";
683
+ var hubspotRequest = ({ body, fetcher, method, path, query, token }) => {
684
+ return (fetcher || request)({
685
+ body,
686
+ headers: {
687
+ "Authorization": "Bearer " + (token || process.env.HUBSPOT_ACCESS_TOKEN)
688
+ },
689
+ method,
690
+ query,
691
+ url: HUBSPOT_BASE + path
692
+ });
693
+ };
694
+ var UTM_PROPERTIES = {
695
+ campaign: "utm_campaign",
696
+ content: "utm_content",
697
+ id: "utm_id",
698
+ medium: "utm_medium",
699
+ source: "utm_source",
700
+ term: "utm_term"
701
+ };
702
+ var CLICK_PROPERTIES = {
703
+ fbclid: "hs_facebook_click_id",
704
+ gclid: "hs_google_click_id",
705
+ liFatId: "hs_linkedin_click_id",
706
+ msclkid: "hs_bing_click_id",
707
+ ttclid: "hs_tiktok_click_id"
708
+ };
709
+ var DROPPABLE = new Set(Object.values(UTM_PROPERTIES));
710
+ var isUtmProperty = (key) => DROPPABLE.has(key);
711
+ var toProperties = ({ email, firstName, lastName, utm }) => {
712
+ var _a;
713
+ const properties = {};
714
+ if (email !== void 0) properties.email = email;
715
+ if (firstName !== void 0) properties.firstname = firstName;
716
+ if (lastName !== void 0) properties.lastname = lastName;
717
+ if (utm) {
718
+ for (const [key, property] of Object.entries(UTM_PROPERTIES)) {
719
+ if (utm[key]) properties[property] = utm[key];
720
+ }
721
+ ;
722
+ for (const [key, property] of Object.entries(CLICK_PROPERTIES)) {
723
+ if ((_a = utm.click) == null ? void 0 : _a[key]) properties[property] = utm.click[key];
724
+ }
725
+ ;
726
+ }
727
+ ;
728
+ return properties;
729
+ };
730
+ var send = async ({ doc, fetcher, method, path, token }) => {
731
+ const properties = toProperties(doc);
732
+ try {
733
+ return await hubspotRequest({
734
+ body: { properties },
735
+ fetcher,
736
+ method,
737
+ path,
738
+ token
739
+ });
740
+ } catch (error) {
741
+ const enriched = Object.keys(properties).some(isUtmProperty);
742
+ if ((error == null ? void 0 : error.status) !== 400 || !enriched) throw error;
743
+ return await hubspotRequest({
744
+ body: {
745
+ properties: Object.fromEntries(
746
+ Object.entries(properties).filter(([key]) => !isUtmProperty(key))
747
+ )
748
+ },
749
+ fetcher,
750
+ method,
751
+ path,
752
+ token
753
+ });
754
+ }
755
+ };
756
+ var lookup = async ({ email, fetcher, token }) => {
757
+ var _a, _b;
758
+ if (!token || !email) return;
759
+ try {
760
+ const body = await hubspotRequest({
761
+ body: {
762
+ filterGroups: [
763
+ {
764
+ filters: [
765
+ {
766
+ operator: "EQ",
767
+ propertyName: "email",
768
+ value: email
769
+ }
770
+ ]
771
+ }
772
+ ],
773
+ limit: 1,
774
+ properties: ["email"]
775
+ },
776
+ fetcher,
777
+ method: "POST",
778
+ path: "/crm/v3/objects/contacts/search",
779
+ token
780
+ });
781
+ return (_b = (_a = body == null ? void 0 : body.results) == null ? void 0 : _a[0]) == null ? void 0 : _b.id;
782
+ } catch (error) {
783
+ }
784
+ };
785
+ var contacts = {
786
+ // FORGET A CONTACT, by id or by email. Account deletion — the caller had
787
+ // to search then remove, which is one round trip it should not have to
788
+ // know about.
789
+ remove: async ({ email, fetcher, id, token }) => {
790
+ const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
791
+ if (!key) return;
792
+ const contact = id || await lookup({ email, fetcher, token: key });
793
+ if (!contact) return;
794
+ return hubspotRequest({
795
+ fetcher,
796
+ method: "DELETE",
797
+ path: "/crm/v3/objects/contacts/" + contact,
798
+ token: key
799
+ });
800
+ },
801
+ // Connect an account to its contact by email, creating it if absent, and
802
+ // return the contact id. Unlike SendGrid, HubSpot renames a contact's
803
+ // email in place, so an email change is a plain PATCH on the cached id —
804
+ // no delete-old-then-create-new.
805
+ //
806
+ // Prefer the cached hubspotId; fall back to a search; create last.
807
+ sync: async ({ doc, fetcher, token }) => {
808
+ var _a, _b;
809
+ const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
810
+ if (!key) return;
811
+ if (doc == null ? void 0 : doc.hubspotId) {
812
+ try {
813
+ return (_a = await send({ doc, fetcher, method: "PATCH", path: "/crm/v3/objects/contacts/" + doc.hubspotId, token: key })) == null ? void 0 : _a.id;
814
+ } catch (error) {
815
+ if ((error == null ? void 0 : error.status) !== 404) throw error;
816
+ }
817
+ }
818
+ const existing = await lookup({ email: doc == null ? void 0 : doc.email, fetcher, token: key });
819
+ return (_b = await send({
820
+ doc,
821
+ fetcher,
822
+ method: existing ? "PATCH" : "POST",
823
+ path: existing ? "/crm/v3/objects/contacts/" + existing : "/crm/v3/objects/contacts",
824
+ token: key
825
+ })) == null ? void 0 : _b.id;
826
+ }
827
+ };
828
+
625
829
  // lib/connections/icons/drawbridge.js
626
830
  var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
627
831
  <rect width="500" height="500" fill="#BAEC5F"/>
@@ -1259,7 +1463,15 @@ var drawbridge_default2 = {
1259
1463
  token: false
1260
1464
  },
1261
1465
  commerce: false,
1262
- contacts: { remove: false, sync: false },
1466
+ // DRAWBRIDGE'S OWN CRM. Not a merchant's — this keeps our HubSpot portal in
1467
+ // step with account signups, and drawbridge-sync's user stream calls it.
1468
+ //
1469
+ // A real implementation here rather than `{}` because the bodies are pure
1470
+ // HTTP against a token: no controller, no queue, nothing that would have to
1471
+ // live in the service. lib/hubspot.js holds them, beside lib/sendgrid.js
1472
+ // and lib/twilio.js, which are the same kind of thing — vendor clients for
1473
+ // accounts DRAWBRIDGE holds rather than ones a merchant connects.
1474
+ contacts,
1263
1475
  email: {
1264
1476
  digest: {},
1265
1477
  // To organization members. NEVER suppressed and never billed: an
@@ -1284,6 +1496,17 @@ var drawbridge_default2 = {
1284
1496
  icon: drawbridge_default,
1285
1497
  // PRIVATE: never in the catalog, always available to the builder.
1286
1498
  private: true,
1499
+ // NOTHING, and HUBSPOT_ACCESS_TOKEN in particular must not be here.
1500
+ //
1501
+ // `requires` gates AVAILABILITY: a name in it that is unset removes the whole
1502
+ // connection. This one is private and contributes every base workflow step —
1503
+ // email.send, sms.send, segment.sync — so gating it on a CRM token would take
1504
+ // all of them away from any deployment without a HubSpot portal, to protect a
1505
+ // sync that is best-effort and already no-ops without a token.
1506
+ //
1507
+ // The test 'a vendor is only available when its environment is configured'
1508
+ // caught exactly that: availableConnections({}) went from [ 'drawbridge' ] to
1509
+ // empty the moment this was added.
1287
1510
  requires: [],
1288
1511
  slug: "drawbridge",
1289
1512
  // Always on. There is no credential that could go bad and no configuration a
@@ -1407,251 +1630,6 @@ var drawbridge_default2 = {
1407
1630
  title: "Drawbridge"
1408
1631
  };
1409
1632
 
1410
- // lib/http.js
1411
- var DEFAULT_TIMEOUT_MS = 15e3;
1412
- var request = async ({
1413
- body,
1414
- headers = {},
1415
- method = "GET",
1416
- query,
1417
- timeout = DEFAULT_TIMEOUT_MS,
1418
- type = "json",
1419
- url
1420
- }) => {
1421
- const fullUrl = new URL(url);
1422
- if (query) {
1423
- Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
1424
- }
1425
- ;
1426
- const isForm = type === "form";
1427
- const response = await fetch(fullUrl.toString(), {
1428
- method,
1429
- headers: {
1430
- "Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
1431
- ...headers
1432
- },
1433
- signal: AbortSignal.timeout(timeout),
1434
- ...body !== void 0 && {
1435
- body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
1436
- }
1437
- });
1438
- if (!response.ok) {
1439
- const text2 = await response.text().catch(() => "");
1440
- const error = new Error(text2 || response.statusText);
1441
- error.status = response.status;
1442
- throw error;
1443
- }
1444
- ;
1445
- const text = await response.text();
1446
- try {
1447
- return text ? JSON.parse(text) : null;
1448
- } catch {
1449
- return null;
1450
- }
1451
- };
1452
-
1453
- // lib/connections/hubspot.js
1454
- var HUBSPOT_BASE = "https://api.hubapi.com";
1455
- var hubspotRequest = ({ body, fetcher, method, path, query, token }) => {
1456
- return (fetcher || request)({
1457
- body,
1458
- headers: {
1459
- "Authorization": "Bearer " + (token || process.env.HUBSPOT_ACCESS_TOKEN)
1460
- },
1461
- method,
1462
- query,
1463
- url: HUBSPOT_BASE + path
1464
- });
1465
- };
1466
- var UTM_PROPERTIES = {
1467
- campaign: "utm_campaign",
1468
- content: "utm_content",
1469
- id: "utm_id",
1470
- medium: "utm_medium",
1471
- source: "utm_source",
1472
- term: "utm_term"
1473
- };
1474
- var CLICK_PROPERTIES = {
1475
- fbclid: "hs_facebook_click_id",
1476
- gclid: "hs_google_click_id",
1477
- liFatId: "hs_linkedin_click_id",
1478
- msclkid: "hs_bing_click_id",
1479
- ttclid: "hs_tiktok_click_id"
1480
- };
1481
- var DROPPABLE = new Set(Object.values(UTM_PROPERTIES));
1482
- var isUtmProperty = (key) => DROPPABLE.has(key);
1483
- var toProperties = ({ email, firstName, lastName, utm }) => {
1484
- var _a;
1485
- const properties = {};
1486
- if (email !== void 0) properties.email = email;
1487
- if (firstName !== void 0) properties.firstname = firstName;
1488
- if (lastName !== void 0) properties.lastname = lastName;
1489
- if (utm) {
1490
- for (const [key, property] of Object.entries(UTM_PROPERTIES)) {
1491
- if (utm[key]) properties[property] = utm[key];
1492
- }
1493
- ;
1494
- for (const [key, property] of Object.entries(CLICK_PROPERTIES)) {
1495
- if ((_a = utm.click) == null ? void 0 : _a[key]) properties[property] = utm.click[key];
1496
- }
1497
- ;
1498
- }
1499
- ;
1500
- return properties;
1501
- };
1502
- var send = async ({ doc, fetcher, method, path, token }) => {
1503
- const properties = toProperties(doc);
1504
- try {
1505
- return await hubspotRequest({
1506
- body: { properties },
1507
- fetcher,
1508
- method,
1509
- path,
1510
- token
1511
- });
1512
- } catch (error) {
1513
- const enriched = Object.keys(properties).some(isUtmProperty);
1514
- if ((error == null ? void 0 : error.status) !== 400 || !enriched) throw error;
1515
- return await hubspotRequest({
1516
- body: {
1517
- properties: Object.fromEntries(
1518
- Object.entries(properties).filter(([key]) => !isUtmProperty(key))
1519
- )
1520
- },
1521
- fetcher,
1522
- method,
1523
- path,
1524
- token
1525
- });
1526
- }
1527
- };
1528
- var lookup = async ({ email, fetcher, token }) => {
1529
- var _a, _b;
1530
- if (!token || !email) return;
1531
- try {
1532
- const body = await hubspotRequest({
1533
- body: {
1534
- filterGroups: [
1535
- {
1536
- filters: [
1537
- {
1538
- operator: "EQ",
1539
- propertyName: "email",
1540
- value: email
1541
- }
1542
- ]
1543
- }
1544
- ],
1545
- limit: 1,
1546
- properties: ["email"]
1547
- },
1548
- fetcher,
1549
- method: "POST",
1550
- path: "/crm/v3/objects/contacts/search",
1551
- token
1552
- });
1553
- return (_b = (_a = body == null ? void 0 : body.results) == null ? void 0 : _a[0]) == null ? void 0 : _b.id;
1554
- } catch (error) {
1555
- }
1556
- };
1557
- var hubspot_default = {
1558
- auth: {
1559
- // A Private App token from our own portal. Nothing to connect, nothing to
1560
- // consent to, and no merchant involved.
1561
- type: "none"
1562
- },
1563
- content: {
1564
- confirm: "This connection is part of Drawbridge and cannot be disconnected.",
1565
- description: [
1566
- "Drawbridge keeps its own HubSpot portal in step with account signups, so the campaign a customer arrived on is on their contact record."
1567
- ],
1568
- excerpt: "Drawbridge's own CRM sync.",
1569
- guide: [
1570
- "Nothing to do. This is internal to Drawbridge."
1571
- ]
1572
- },
1573
- exclusive: false,
1574
- fields: [],
1575
- group: "contacts",
1576
- hooks: {
1577
- auth: {
1578
- connect: false,
1579
- disconnect: false,
1580
- probe: false,
1581
- scopes: false,
1582
- token: false
1583
- },
1584
- commerce: false,
1585
- contacts: {
1586
- // FORGET A CONTACT, by id or by email. Account deletion — the caller had
1587
- // to search then remove, which is one round trip it should not have to
1588
- // know about.
1589
- remove: async ({ email, fetcher, id, token }) => {
1590
- const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
1591
- if (!key) return;
1592
- const contact = id || await lookup({ email, fetcher, token: key });
1593
- if (!contact) return;
1594
- return hubspotRequest({
1595
- fetcher,
1596
- method: "DELETE",
1597
- path: "/crm/v3/objects/contacts/" + contact,
1598
- token: key
1599
- });
1600
- },
1601
- // Connect an account to its contact by email, creating it if absent, and
1602
- // return the contact id. Unlike SendGrid, HubSpot renames a contact's
1603
- // email in place, so an email change is a plain PATCH on the cached id —
1604
- // no delete-old-then-create-new.
1605
- //
1606
- // Prefer the cached hubspotId; fall back to a search; create last.
1607
- sync: async ({ doc, fetcher, token }) => {
1608
- var _a, _b;
1609
- const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
1610
- if (!key) return;
1611
- if (doc == null ? void 0 : doc.hubspotId) {
1612
- try {
1613
- return (_a = await send({ doc, fetcher, method: "PATCH", path: "/crm/v3/objects/contacts/" + doc.hubspotId, token: key })) == null ? void 0 : _a.id;
1614
- } catch (error) {
1615
- if ((error == null ? void 0 : error.status) !== 404) throw error;
1616
- }
1617
- }
1618
- const existing = await lookup({ email: doc == null ? void 0 : doc.email, fetcher, token: key });
1619
- return (_b = await send({
1620
- doc,
1621
- fetcher,
1622
- method: existing ? "PATCH" : "POST",
1623
- path: existing ? "/crm/v3/objects/contacts/" + existing : "/crm/v3/objects/contacts",
1624
- token: key
1625
- })) == null ? void 0 : _b.id;
1626
- }
1627
- },
1628
- email: false,
1629
- inbound: false,
1630
- lifecycle: false,
1631
- resources: {
1632
- audiences: false,
1633
- prices: false,
1634
- products: false,
1635
- promotions: false
1636
- },
1637
- segment: false,
1638
- sms: false,
1639
- webhook: false
1640
- },
1641
- icon: drawbridge_default,
1642
- // Borrowed: the Drawbridge mark, because this is ours and never rendered.
1643
- private: true,
1644
- // Absent the token the hooks no-op, so a deployment without a portal simply
1645
- // contributes nothing rather than failing.
1646
- requires: ["HUBSPOT_ACCESS_TOKEN"],
1647
- slug: "hubspot",
1648
- status: () => "active",
1649
- // No workflow steps. The hooks are called by the user stream, not the builder.
1650
- steps: {},
1651
- tasks: () => [],
1652
- title: "HubSpot"
1653
- };
1654
-
1655
1633
  // lib/connections/icons/klaviyo.js
1656
1634
  var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
1657
1635
  <rect width="500" height="500" fill="white"/>
@@ -2113,46 +2091,59 @@ var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fil
2113
2091
  </svg>`;
2114
2092
 
2115
2093
  // lib/connections/mailchimp.js
2116
- var base = (apiKey) => {
2117
- const dc = String(apiKey || "").split("-").pop();
2118
- if (!dc || dc === apiKey) throw new Error("That Mailchimp key carries no data centre suffix, so there is no host to call");
2094
+ var base = (dc) => {
2095
+ if (!dc) throw new Error("This Mailchimp connection has no data centre stored, so there is no host to call");
2119
2096
  return "https://" + dc + ".api.mailchimp.com/3.0";
2120
2097
  };
2121
2098
  var mailchimp_default2 = {
2122
- // Keys TODAY. Mailchimp integrations authenticate with OAuth 2 (authorization
2123
- // code) and that is where this goes, so the endpoints are recorded here
2124
- // rather than researched again later:
2099
+ // OAUTH 2, authorization code. Every url below is quoted from
2100
+ // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
2101
+ // than remembered.
2125
2102
  //
2126
- // authorize https://login.mailchimp.com/oauth2/authorize
2127
- // token https://login.mailchimp.com/oauth2/token
2128
- // metadata https://login.mailchimp.com/oauth2/metadata
2103
+ // THE METADATA CALL IS MAILCHIMP'S QUIRK and cannot be skipped: an access
2104
+ // token alone cannot call the Marketing API, because every account lives
2105
+ // behind a data-centre prefix (us1, us19...) that only GET
2106
+ // login.mailchimp.com/oauth2/metadata returns — and every subsequent request
2107
+ // needs it in the HOST. That is why auth.connect below is a real function:
2108
+ // the standard exchange does not know where to send anything afterwards.
2129
2109
  //
2130
- // The metadata call is Mailchimp's quirk and cannot be skipped: the access
2131
- // token alone is not enough to call the Marketing API, because every account
2132
- // lives behind a data-centre prefix (us1, us19...) that only that call
2133
- // returns and every subsequent request needs in its host. No PKCE.
2110
+ // No PKCE. No scopes the docs describe none. And no refresh token: "Mailchimp
2111
+ // Marketing access tokens do not expire, so you don't need to use a
2112
+ // refresh_token", so tokenSettings stores no expiry and isStale reads that as
2113
+ // nothing to refresh toward.
2134
2114
  //
2135
- // Switching is filling in auth.oauth and moving the apiKey field out. It is
2136
- // still a publish -- a manifest change always is -- but a value change
2137
- // rather than a shape one.
2115
+ // auth.token stays false: the exchange is POST form-encoded with grant_type,
2116
+ // client_id, client_secret, redirect_uri and code, which is exactly the
2117
+ // runner's default nothing to wrap.
2138
2118
  auth: {
2139
- type: "keys"
2119
+ oauth: {
2120
+ client: {
2121
+ id: "MAILCHIMP_OAUTH_CLIENT_ID",
2122
+ secret: "MAILCHIMP_OAUTH_CLIENT_SECRET"
2123
+ },
2124
+ urls: {
2125
+ authorize: "https://login.mailchimp.com/oauth2/authorize",
2126
+ redirect: "/api/connection/mailchimp/callback",
2127
+ token: "https://login.mailchimp.com/oauth2/token"
2128
+ }
2129
+ },
2130
+ type: "oauth"
2140
2131
  },
2141
2132
  // EVERYTHING A MERCHANT READS. `errors` belongs in here rather than at the
2142
2133
  // top level because the connection DOCUMENT carries its own `errors` array
2143
2134
  // and the document is spread OVER the resolved manifest downstream — a
2144
2135
  // top-level one would be replaced by that array and never render.
2145
2136
  content: {
2146
- confirm: "Disconnecting removes your stored Mailchimp key. Your contacts stay in both Drawbridge and Mailchimp \u2014 neither list is deleted.",
2137
+ 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.",
2147
2138
  description: [
2148
2139
  "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.",
2149
- "This connection is becoming the way your Drawbridge contacts sync into a Mailchimp audience. Audience syncing is not live yet, so a key stored here does nothing today."
2140
+ "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."
2150
2141
  ],
2151
2142
  excerpt: "Sync your Drawbridge contacts into a Mailchimp audience.",
2152
2143
  guide: [
2153
- "In Mailchimp, open Account & billing, then Extras, then API keys.",
2154
- "Create a key and copy it.",
2155
- "Paste it here. The key ends in a data-centre suffix like -us19, which tells Drawbridge which Mailchimp server your account is on."
2144
+ "Press Connect. Drawbridge sends you to Mailchimp to approve access.",
2145
+ "Sign in to Mailchimp if you are not already, and choose the account to connect.",
2146
+ "You come back here to pick the audience your contacts should sync into."
2156
2147
  ]
2157
2148
  },
2158
2149
  // Mailchimp and SendGrid shared a group while they were SENDERS, where an org
@@ -2162,15 +2153,6 @@ var mailchimp_default2 = {
2162
2153
  exclusive: false,
2163
2154
  feature: "organization:connection:mailchimp",
2164
2155
  fields: [
2165
- {
2166
- input: "password",
2167
- key: "apiKey",
2168
- label: "Mailchimp API key",
2169
- message: "Your Mailchimp API key",
2170
- 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",
2171
- redact: true,
2172
- required: true
2173
- },
2174
2156
  {
2175
2157
  input: "select",
2176
2158
  key: "audience",
@@ -2189,16 +2171,45 @@ var mailchimp_default2 = {
2189
2171
  // contacts.sync are the first to flip.
2190
2172
  hooks: {
2191
2173
  auth: {
2192
- // Implemented outside this package: storing a typed key needs no vendor
2193
- // call, so the api's own form handler does it.
2194
- connect: {},
2195
- disconnect: {},
2174
+ // WHERE THE ACCOUNT LIVES. Not enrichment without this the connection
2175
+ // is unusable, because the Marketing API host is per-account and only
2176
+ // this call knows it. The callback merges what this returns into the
2177
+ // stored settings, which is how `dc` reaches every later request.
2178
+ //
2179
+ // The header here is `OAuth <token>`, not Bearer — that is specific to
2180
+ // the metadata endpoint. Marketing API calls take Bearer; see the
2181
+ // audiences hook.
2182
+ connect: async ({ fetcher = fetch, tokens }) => {
2183
+ const response = await fetcher("https://login.mailchimp.com/oauth2/metadata", {
2184
+ headers: {
2185
+ authorization: "OAuth " + (tokens == null ? void 0 : tokens.accessToken)
2186
+ },
2187
+ signal: AbortSignal.timeout(15e3)
2188
+ });
2189
+ if (!response.ok) {
2190
+ throw Object.assign(
2191
+ new Error("Mailchimp would not say which data centre this account is on (" + response.status + ")"),
2192
+ { status: response.status }
2193
+ );
2194
+ }
2195
+ const body = await response.json();
2196
+ if (!(body == null ? void 0 : body.dc)) throw new Error("Mailchimp returned no data centre for this account");
2197
+ return { dc: body.dc };
2198
+ },
2199
+ // Nothing to call. A merchant revokes Drawbridge from Mailchimp's own
2200
+ // Authorized Apps page; the docs describe no revocation endpoint for us
2201
+ // to call on their behalf.
2202
+ disconnect: false,
2196
2203
  probe: false,
2197
2204
  scopes: false,
2198
- // Keys today. When Mailchimp's OAuth lands this becomes a wrapper that
2199
- // follows the exchange with /oauth2/metadata the data-centre call that
2200
- // is the whole reason its OAuth cannot be pure configuration.
2201
- token: false
2205
+ // The plain exchange. Mailchimp takes the client as FORM FIELDS
2206
+ // (grant_type, client_id, client_secret, redirect_uri, code), which is
2207
+ // the runner's default so no `basic : true` as Klaviyo needs.
2208
+ //
2209
+ // build() requires an oauth manifest to name this explicitly rather than
2210
+ // letting it default, which caught this file declaring `false` on the
2211
+ // first import after the conversion.
2212
+ token: authToken
2202
2213
  },
2203
2214
  commerce: false,
2204
2215
  contacts: { remove: false, sync: false },
@@ -2220,15 +2231,14 @@ var mailchimp_default2 = {
2220
2231
  // successful — the same silent truncation Klaviyo has, at a different
2221
2232
  // number. Paged against total_items so an account past a thousand still
2222
2233
  // resolves.
2223
- audiences: async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
2224
- const key = settings == null ? void 0 : settings.apiKey;
2234
+ audiences: async ({ cursor, fetcher = fetch, limit = 100, search, settings, token }) => {
2235
+ const dc = settings == null ? void 0 : settings.dc;
2225
2236
  const count = Math.min(limit, 1e3);
2226
2237
  const offset = Number(cursor || 0);
2227
2238
  const response = await fetcher(
2228
- base(key) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
2239
+ base(dc) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
2229
2240
  {
2230
- // Basic with any username Mailchimp reads only the password half.
2231
- headers: { authorization: "Basic " + Buffer.from("drawbridge:" + key).toString("base64") },
2241
+ headers: { authorization: "Bearer " + token },
2232
2242
  signal: AbortSignal.timeout(15e3)
2233
2243
  }
2234
2244
  );
@@ -2260,6 +2270,13 @@ var mailchimp_default2 = {
2260
2270
  webhook: false
2261
2271
  },
2262
2272
  icon: mailchimp_default,
2273
+ // The OAuth client this deployment registered. Without both, the vendor drops
2274
+ // out of availableConnections rather than offering a Connect button that
2275
+ // cannot complete.
2276
+ requires: [
2277
+ "MAILCHIMP_OAUTH_CLIENT_ID",
2278
+ "MAILCHIMP_OAUTH_CLIENT_SECRET"
2279
+ ],
2263
2280
  slug: "mailchimp",
2264
2281
  // A key with no audience chosen is authenticated and inert. Mailchimp also
2265
2282
  // needs its merge fields created on that audience before any Drawbridge total
@@ -2413,10 +2430,29 @@ var shopify_default2 = {
2413
2430
  // re-registers rather than answering "is this token still good", and
2414
2431
  // scope drift is its own hook because a token can be perfectly valid
2415
2432
  // while the grant is too narrow.
2416
- connect: {},
2417
- disconnect: {},
2433
+ //
2434
+ // connect and disconnect are FALSE rather than `{}`: there is nothing to
2435
+ // call on either side. The install already hands the callback everything
2436
+ // it stores, and a Shopify grant is withdrawn by UNINSTALLING the app in
2437
+ // Shopify admin — which Drawbridge learns about from the app_uninstalled
2438
+ // webhook rather than by asking. `{}` claimed a body implemented
2439
+ // elsewhere; none exists, and none could.
2440
+ connect: false,
2441
+ disconnect: false,
2418
2442
  probe: false,
2419
- scopes: {},
2443
+ // WHETHER THE GRANT IS STILL WIDE ENOUGH. A token can be perfectly valid
2444
+ // and still too narrow — a deploy that adds a scope leaves every existing
2445
+ // install short of it, and no webhook fires to say so.
2446
+ //
2447
+ // The comparison is the vendor's, so it belongs here. Reading WHICH
2448
+ // scopes a store granted is not: that lives in the `shop` collection and
2449
+ // needs a controller, which is precisely what a hook in a published
2450
+ // package must not be handed. The caller reads the grant and passes the
2451
+ // string; this answers what is missing from it.
2452
+ //
2453
+ // `shopify` is injected for the same reason it is everywhere else — this
2454
+ // package cannot import @drawbridge/shopify, which depends on it.
2455
+ scopes: ({ scope, shopify }) => scope ? shopify.oauth.missingScopes(scope) : null,
2420
2456
  // Shopify's install grant is exchanged inside its own app flow, not
2421
2457
  // through the shared OAuth runner.
2422
2458
  token: false
@@ -2879,10 +2915,12 @@ var webhook_default = {
2879
2915
  // connect generates a secret rather than proving a credential.
2880
2916
  hooks: {
2881
2917
  auth: {
2882
- // Minting and clearing a secret needs no vendor call, so the api's own
2883
- // handler does both.
2884
- connect: {},
2885
- disconnect: {},
2918
+ // FALSE, NOT {}. There is no vendor here at all connecting mints a
2919
+ // secret and disconnecting clears it, both done by the api's own
2920
+ // handler. `{}` would promise a body implemented elsewhere, and none
2921
+ // exists or could.
2922
+ connect: false,
2923
+ disconnect: false,
2886
2924
  probe: false,
2887
2925
  scopes: false,
2888
2926
  // Nothing to mint. Connecting generates a secret; there is no vendor.
@@ -3127,7 +3165,6 @@ var stepLabels = (catalog = connections) => Object.fromEntries(
3127
3165
  var connections = Object.freeze({
3128
3166
  attentive: build(attentive_default2),
3129
3167
  drawbridge: build(drawbridge_default2),
3130
- hubspot: build(hubspot_default),
3131
3168
  klaviyo: build(klaviyo_default2),
3132
3169
  mailchimp: build(mailchimp_default2),
3133
3170
  shopify: build(shopify_default2),