@drawbridge/drawbridge-utils 0.0.116 → 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.
@@ -566,6 +566,197 @@ var attentive_default2 = {
566
566
  title: "Attentive"
567
567
  };
568
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
+
569
760
  // lib/connections/icons/drawbridge.js
570
761
  var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
571
762
  <rect width="500" height="500" fill="#BAEC5F"/>
@@ -1203,7 +1394,15 @@ var drawbridge_default2 = {
1203
1394
  token: false
1204
1395
  },
1205
1396
  commerce: false,
1206
- contacts: { remove: false, sync: false },
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,
1207
1406
  email: {
1208
1407
  digest: {},
1209
1408
  // To organization members. NEVER suppressed and never billed: an
@@ -1228,6 +1427,17 @@ var drawbridge_default2 = {
1228
1427
  icon: drawbridge_default,
1229
1428
  // PRIVATE: never in the catalog, always available to the builder.
1230
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.
1231
1441
  requires: [],
1232
1442
  slug: "drawbridge",
1233
1443
  // Always on. There is no credential that could go bad and no configuration a
@@ -1351,251 +1561,6 @@ var drawbridge_default2 = {
1351
1561
  title: "Drawbridge"
1352
1562
  };
1353
1563
 
1354
- // lib/http.js
1355
- var DEFAULT_TIMEOUT_MS = 15e3;
1356
- var request = async ({
1357
- body,
1358
- headers = {},
1359
- method = "GET",
1360
- query,
1361
- timeout = DEFAULT_TIMEOUT_MS,
1362
- type = "json",
1363
- url
1364
- }) => {
1365
- const fullUrl = new URL(url);
1366
- if (query) {
1367
- Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
1368
- }
1369
- ;
1370
- const isForm = type === "form";
1371
- const response = await fetch(fullUrl.toString(), {
1372
- method,
1373
- headers: {
1374
- "Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
1375
- ...headers
1376
- },
1377
- signal: AbortSignal.timeout(timeout),
1378
- ...body !== void 0 && {
1379
- body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
1380
- }
1381
- });
1382
- if (!response.ok) {
1383
- const text2 = await response.text().catch(() => "");
1384
- const error = new Error(text2 || response.statusText);
1385
- error.status = response.status;
1386
- throw error;
1387
- }
1388
- ;
1389
- const text = await response.text();
1390
- try {
1391
- return text ? JSON.parse(text) : null;
1392
- } catch {
1393
- return null;
1394
- }
1395
- };
1396
-
1397
- // lib/connections/hubspot.js
1398
- var HUBSPOT_BASE = "https://api.hubapi.com";
1399
- var hubspotRequest = ({ body, fetcher, method, path, query, token }) => {
1400
- return (fetcher || request)({
1401
- body,
1402
- headers: {
1403
- "Authorization": "Bearer " + (token || process.env.HUBSPOT_ACCESS_TOKEN)
1404
- },
1405
- method,
1406
- query,
1407
- url: HUBSPOT_BASE + path
1408
- });
1409
- };
1410
- var UTM_PROPERTIES = {
1411
- campaign: "utm_campaign",
1412
- content: "utm_content",
1413
- id: "utm_id",
1414
- medium: "utm_medium",
1415
- source: "utm_source",
1416
- term: "utm_term"
1417
- };
1418
- var CLICK_PROPERTIES = {
1419
- fbclid: "hs_facebook_click_id",
1420
- gclid: "hs_google_click_id",
1421
- liFatId: "hs_linkedin_click_id",
1422
- msclkid: "hs_bing_click_id",
1423
- ttclid: "hs_tiktok_click_id"
1424
- };
1425
- var DROPPABLE = new Set(Object.values(UTM_PROPERTIES));
1426
- var isUtmProperty = (key) => DROPPABLE.has(key);
1427
- var toProperties = ({ email, firstName, lastName, utm }) => {
1428
- var _a;
1429
- const properties = {};
1430
- if (email !== void 0) properties.email = email;
1431
- if (firstName !== void 0) properties.firstname = firstName;
1432
- if (lastName !== void 0) properties.lastname = lastName;
1433
- if (utm) {
1434
- for (const [key, property] of Object.entries(UTM_PROPERTIES)) {
1435
- if (utm[key]) properties[property] = utm[key];
1436
- }
1437
- ;
1438
- for (const [key, property] of Object.entries(CLICK_PROPERTIES)) {
1439
- if ((_a = utm.click) == null ? void 0 : _a[key]) properties[property] = utm.click[key];
1440
- }
1441
- ;
1442
- }
1443
- ;
1444
- return properties;
1445
- };
1446
- var send = async ({ doc, fetcher, method, path, token }) => {
1447
- const properties = toProperties(doc);
1448
- try {
1449
- return await hubspotRequest({
1450
- body: { properties },
1451
- fetcher,
1452
- method,
1453
- path,
1454
- token
1455
- });
1456
- } catch (error) {
1457
- const enriched = Object.keys(properties).some(isUtmProperty);
1458
- if ((error == null ? void 0 : error.status) !== 400 || !enriched) throw error;
1459
- return await hubspotRequest({
1460
- body: {
1461
- properties: Object.fromEntries(
1462
- Object.entries(properties).filter(([key]) => !isUtmProperty(key))
1463
- )
1464
- },
1465
- fetcher,
1466
- method,
1467
- path,
1468
- token
1469
- });
1470
- }
1471
- };
1472
- var lookup = async ({ email, fetcher, token }) => {
1473
- var _a, _b;
1474
- if (!token || !email) return;
1475
- try {
1476
- const body = await hubspotRequest({
1477
- body: {
1478
- filterGroups: [
1479
- {
1480
- filters: [
1481
- {
1482
- operator: "EQ",
1483
- propertyName: "email",
1484
- value: email
1485
- }
1486
- ]
1487
- }
1488
- ],
1489
- limit: 1,
1490
- properties: ["email"]
1491
- },
1492
- fetcher,
1493
- method: "POST",
1494
- path: "/crm/v3/objects/contacts/search",
1495
- token
1496
- });
1497
- return (_b = (_a = body == null ? void 0 : body.results) == null ? void 0 : _a[0]) == null ? void 0 : _b.id;
1498
- } catch (error) {
1499
- }
1500
- };
1501
- var hubspot_default = {
1502
- auth: {
1503
- // A Private App token from our own portal. Nothing to connect, nothing to
1504
- // consent to, and no merchant involved.
1505
- type: "none"
1506
- },
1507
- content: {
1508
- confirm: "This connection is part of Drawbridge and cannot be disconnected.",
1509
- description: [
1510
- "Drawbridge keeps its own HubSpot portal in step with account signups, so the campaign a customer arrived on is on their contact record."
1511
- ],
1512
- excerpt: "Drawbridge's own CRM sync.",
1513
- guide: [
1514
- "Nothing to do. This is internal to Drawbridge."
1515
- ]
1516
- },
1517
- exclusive: false,
1518
- fields: [],
1519
- group: "contacts",
1520
- hooks: {
1521
- auth: {
1522
- connect: false,
1523
- disconnect: false,
1524
- probe: false,
1525
- scopes: false,
1526
- token: false
1527
- },
1528
- commerce: false,
1529
- contacts: {
1530
- // FORGET A CONTACT, by id or by email. Account deletion — the caller had
1531
- // to search then remove, which is one round trip it should not have to
1532
- // know about.
1533
- remove: async ({ email, fetcher, id, token }) => {
1534
- const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
1535
- if (!key) return;
1536
- const contact = id || await lookup({ email, fetcher, token: key });
1537
- if (!contact) return;
1538
- return hubspotRequest({
1539
- fetcher,
1540
- method: "DELETE",
1541
- path: "/crm/v3/objects/contacts/" + contact,
1542
- token: key
1543
- });
1544
- },
1545
- // Connect an account to its contact by email, creating it if absent, and
1546
- // return the contact id. Unlike SendGrid, HubSpot renames a contact's
1547
- // email in place, so an email change is a plain PATCH on the cached id —
1548
- // no delete-old-then-create-new.
1549
- //
1550
- // Prefer the cached hubspotId; fall back to a search; create last.
1551
- sync: async ({ doc, fetcher, token }) => {
1552
- var _a, _b;
1553
- const key = token || process.env.HUBSPOT_ACCESS_TOKEN;
1554
- if (!key) return;
1555
- if (doc == null ? void 0 : doc.hubspotId) {
1556
- try {
1557
- return (_a = await send({ doc, fetcher, method: "PATCH", path: "/crm/v3/objects/contacts/" + doc.hubspotId, token: key })) == null ? void 0 : _a.id;
1558
- } catch (error) {
1559
- if ((error == null ? void 0 : error.status) !== 404) throw error;
1560
- }
1561
- }
1562
- const existing = await lookup({ email: doc == null ? void 0 : doc.email, fetcher, token: key });
1563
- return (_b = await send({
1564
- doc,
1565
- fetcher,
1566
- method: existing ? "PATCH" : "POST",
1567
- path: existing ? "/crm/v3/objects/contacts/" + existing : "/crm/v3/objects/contacts",
1568
- token: key
1569
- })) == null ? void 0 : _b.id;
1570
- }
1571
- },
1572
- email: false,
1573
- inbound: false,
1574
- lifecycle: false,
1575
- resources: {
1576
- audiences: false,
1577
- prices: false,
1578
- products: false,
1579
- promotions: false
1580
- },
1581
- segment: false,
1582
- sms: false,
1583
- webhook: false
1584
- },
1585
- icon: drawbridge_default,
1586
- // Borrowed: the Drawbridge mark, because this is ours and never rendered.
1587
- private: true,
1588
- // Absent the token the hooks no-op, so a deployment without a portal simply
1589
- // contributes nothing rather than failing.
1590
- requires: ["HUBSPOT_ACCESS_TOKEN"],
1591
- slug: "hubspot",
1592
- status: () => "active",
1593
- // No workflow steps. The hooks are called by the user stream, not the builder.
1594
- steps: {},
1595
- tasks: () => [],
1596
- title: "HubSpot"
1597
- };
1598
-
1599
1564
  // lib/connections/icons/klaviyo.js
1600
1565
  var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
1601
1566
  <rect width="500" height="500" fill="white"/>
@@ -2057,46 +2022,59 @@ var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fil
2057
2022
  </svg>`;
2058
2023
 
2059
2024
  // lib/connections/mailchimp.js
2060
- var base = (apiKey) => {
2061
- const dc = String(apiKey || "").split("-").pop();
2062
- 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");
2063
2027
  return "https://" + dc + ".api.mailchimp.com/3.0";
2064
2028
  };
2065
2029
  var mailchimp_default2 = {
2066
- // Keys TODAY. Mailchimp integrations authenticate with OAuth 2 (authorization
2067
- // code) and that is where this goes, so the endpoints are recorded here
2068
- // rather than researched again later:
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.
2069
2033
  //
2070
- // authorize https://login.mailchimp.com/oauth2/authorize
2071
- // token https://login.mailchimp.com/oauth2/token
2072
- // metadata https://login.mailchimp.com/oauth2/metadata
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.
2073
2040
  //
2074
- // The metadata call is Mailchimp's quirk and cannot be skipped: the access
2075
- // token alone is not enough to call the Marketing API, because every account
2076
- // lives behind a data-centre prefix (us1, us19...) that only that call
2077
- // returns and every subsequent request needs in its host. No PKCE.
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.
2078
2045
  //
2079
- // Switching is filling in auth.oauth and moving the apiKey field out. It is
2080
- // still a publish -- a manifest change always is -- but a value change
2081
- // rather than a shape one.
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.
2082
2049
  auth: {
2083
- type: "keys"
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"
2084
2062
  },
2085
2063
  // EVERYTHING A MERCHANT READS. `errors` belongs in here rather than at the
2086
2064
  // top level because the connection DOCUMENT carries its own `errors` array
2087
2065
  // and the document is spread OVER the resolved manifest downstream — a
2088
2066
  // top-level one would be replaced by that array and never render.
2089
2067
  content: {
2090
- confirm: "Disconnecting removes your stored Mailchimp key. Your contacts stay in both Drawbridge and Mailchimp \u2014 neither list is deleted.",
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.",
2091
2069
  description: [
2092
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.",
2093
- "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."
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."
2094
2072
  ],
2095
2073
  excerpt: "Sync your Drawbridge contacts into a Mailchimp audience.",
2096
2074
  guide: [
2097
- "In Mailchimp, open Account & billing, then Extras, then API keys.",
2098
- "Create a key and copy it.",
2099
- "Paste it here. The key ends in a data-centre suffix like -us19, which tells Drawbridge which Mailchimp server your account is on."
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."
2100
2078
  ]
2101
2079
  },
2102
2080
  // Mailchimp and SendGrid shared a group while they were SENDERS, where an org
@@ -2106,15 +2084,6 @@ var mailchimp_default2 = {
2106
2084
  exclusive: false,
2107
2085
  feature: "organization:connection:mailchimp",
2108
2086
  fields: [
2109
- {
2110
- input: "password",
2111
- key: "apiKey",
2112
- label: "Mailchimp API key",
2113
- message: "Your Mailchimp API key",
2114
- 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",
2115
- redact: true,
2116
- required: true
2117
- },
2118
2087
  {
2119
2088
  input: "select",
2120
2089
  key: "audience",
@@ -2133,20 +2102,45 @@ var mailchimp_default2 = {
2133
2102
  // contacts.sync are the first to flip.
2134
2103
  hooks: {
2135
2104
  auth: {
2136
- // FALSE, NOT {}. `{}` promises an implementation living in the repo that
2137
- // holds the dependencies, and there is no implementation anywhere
2138
- // because there is nothing to implement: storing and clearing a typed
2139
- // key needs no vendor call, and the api's own form handler does it.
2140
- // Declaring `{}` made the coverage check chase a body that does not
2141
- // exist, and made a caller wait on an answer that never comes.
2142
- connect: false,
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.
2143
2133
  disconnect: false,
2144
2134
  probe: false,
2145
2135
  scopes: false,
2146
- // Keys today. When Mailchimp's OAuth lands this becomes a wrapper that
2147
- // follows the exchange with /oauth2/metadata the data-centre call that
2148
- // is the whole reason its OAuth cannot be pure configuration.
2149
- token: false
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
2150
2144
  },
2151
2145
  commerce: false,
2152
2146
  contacts: { remove: false, sync: false },
@@ -2168,15 +2162,14 @@ var mailchimp_default2 = {
2168
2162
  // successful — the same silent truncation Klaviyo has, at a different
2169
2163
  // number. Paged against total_items so an account past a thousand still
2170
2164
  // resolves.
2171
- audiences: async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
2172
- const key = settings == null ? void 0 : settings.apiKey;
2165
+ audiences: async ({ cursor, fetcher = fetch, limit = 100, search, settings, token }) => {
2166
+ const dc = settings == null ? void 0 : settings.dc;
2173
2167
  const count = Math.min(limit, 1e3);
2174
2168
  const offset = Number(cursor || 0);
2175
2169
  const response = await fetcher(
2176
- base(key) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
2170
+ base(dc) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
2177
2171
  {
2178
- // Basic with any username Mailchimp reads only the password half.
2179
- headers: { authorization: "Basic " + Buffer.from("drawbridge:" + key).toString("base64") },
2172
+ headers: { authorization: "Bearer " + token },
2180
2173
  signal: AbortSignal.timeout(15e3)
2181
2174
  }
2182
2175
  );
@@ -2208,6 +2201,13 @@ var mailchimp_default2 = {
2208
2201
  webhook: false
2209
2202
  },
2210
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
+ ],
2211
2211
  slug: "mailchimp",
2212
2212
  // A key with no audience chosen is authenticated and inert. Mailchimp also
2213
2213
  // needs its merge fields created on that audience before any Drawbridge total
@@ -3096,7 +3096,6 @@ var stepLabels = (catalog = connections) => Object.fromEntries(
3096
3096
  var connections = Object.freeze({
3097
3097
  attentive: build(attentive_default2),
3098
3098
  drawbridge: build(drawbridge_default2),
3099
- hubspot: build(hubspot_default),
3100
3099
  klaviyo: build(klaviyo_default2),
3101
3100
  mailchimp: build(mailchimp_default2),
3102
3101
  shopify: build(shopify_default2),