@drawbridge/drawbridge-utils 0.0.121 → 0.0.125

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.
@@ -448,7 +448,47 @@ var attentive_default = `<svg width="500" height="500" viewBox="0 0 500 500" fil
448
448
  <path d="M166.04 261.805C180.228 259.107 195.528 261.893 207.581 269.971C218.512 277.079 226.908 288.136 230.604 300.657C234.835 314.103 233.614 329.124 227.485 341.788C220.097 356.875 205.782 368.543 189.317 372.089C173.957 375.652 157.089 372.386 144.304 363.103C132.971 355.295 124.912 342.989 121.891 329.581C118.943 316.636 120.725 302.656 127.002 290.938C134.699 275.951 149.503 264.933 166.046 261.811" fill="#1E1C1C"/>
449
449
  </svg>`;
450
450
 
451
+ // lib/phone.js
452
+ var import_libphonenumber_js = require("libphonenumber-js");
453
+ var toE164 = (value, country) => {
454
+ if (!value) return null;
455
+ try {
456
+ const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(String(value), country);
457
+ return parsed ? parsed.number : null;
458
+ } catch {
459
+ return null;
460
+ }
461
+ };
462
+ var detectCountry = (value) => {
463
+ if (!value) return null;
464
+ try {
465
+ const parser = new import_libphonenumber_js.AsYouType();
466
+ parser.input(String(value));
467
+ return parser.getCountry() || null;
468
+ } catch {
469
+ return null;
470
+ }
471
+ };
472
+
451
473
  // lib/connections/providers/attentive.js
474
+ var api = async (path, { fetcher = fetch, method = "GET", payload, token }) => {
475
+ const response = await fetcher("https://api.attentivemobile.com" + path, {
476
+ ...payload && { body: JSON.stringify(payload) },
477
+ headers: {
478
+ authorization: "Bearer " + token,
479
+ ...payload && { "content-type": "application/json" }
480
+ },
481
+ method,
482
+ signal: AbortSignal.timeout(15e3)
483
+ });
484
+ if (!response.ok) {
485
+ throw Object.assign(
486
+ new Error("Attentive refused the request (" + response.status + ")"),
487
+ { status: response.status }
488
+ );
489
+ }
490
+ return response.json().catch(() => null);
491
+ };
452
492
  var attentive_default2 = {
453
493
  auth: {
454
494
  oauth: {
@@ -482,9 +522,10 @@ var attentive_default2 = {
482
522
  // has to say so rather than let them believe otherwise.
483
523
  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.",
484
524
  description: [
485
- "Attentive is where your SMS marketing lives, and this connection is becoming the way your Drawbridge contacts sync into an Attentive segment.",
525
+ "Attentive is where your SMS marketing lives, and this connection syncs the contacts your campaigns collect into an Attentive segment \u2014 subscribed for marketing and added to the segment you choose.",
486
526
  "You authorize Drawbridge from inside Attentive and can revoke that access there at any time. Drawbridge never sees or stores your Attentive password.",
487
- "Subscriber syncing is not live yet, so connecting today does nothing except choose the segment it will use when it ships."
527
+ "Anyone who has opted out in Drawbridge is sent to Attentive as an unsubscribe rather than omitted, so a person who asked not to be contacted stays suppressed in both systems instead of quietly reappearing.",
528
+ "Attentive accepts these updates and applies them in the background, so a contact appears in your segment shortly after the sync rather than the instant it runs."
488
529
  ],
489
530
  excerpt: "Sync your Drawbridge contacts into an Attentive segment.",
490
531
  guide: [
@@ -505,6 +546,10 @@ var attentive_default2 = {
505
546
  message: "Contacts your campaigns collect are synced into this segment.",
506
547
  hook: "resources.audiences",
507
548
  required: true
549
+ // CONSUMED BY THE MEMBERSHIP CALL, not by the subscribe. Attentive's
550
+ // /v1/subscriptions takes no segment id — subscription and segment
551
+ // membership are two operations here — so contacts.sync makes both calls
552
+ // and this value is the externalId the second one carries.
508
553
  // No `search : false` here, and that is a first: /v2/segments takes a
509
554
  // `name` filter (partial match, cited above), so this picker searches
510
555
  // the ACCOUNT — Klaviyo and Mailchimp can only match the fetched page.
@@ -517,17 +562,25 @@ var attentive_default2 = {
517
562
  // and contacts.sync are the first to flip.
518
563
  hooks: {
519
564
  auth: {
520
- // The exchange already yields the tokens, and Attentive documents no
521
- // account-identity endpoint to enrich them with — Klaviyo's connect
522
- // reads the account name back; this has nothing cited to read. The
523
- // callback stores the tokens and skips enrichment on `unimplemented`.
524
565
  // FALSE, NOT {}. `{}` means "supported, implemented in the repo with the
525
- // dependencies", and nothing anywhere implements either of these —
526
- // there is nothing for them to do. The exchange already yields the
527
- // tokens and Attentive documents no account-identity endpoint to
528
- // enrich them with, so connect has nothing to add; and they document
529
- // no revocation endpoint at all, so disconnect has nothing to call.
530
- // Recorded as a decision rather than left as an unkept promise.
566
+ // dependencies", and nothing anywhere implements either of these — there
567
+ // is nothing for them to do. They document no revocation endpoint at all,
568
+ // so disconnect has nothing to call. Recorded as a decision rather than
569
+ // left as an unkept promise.
570
+ //
571
+ // STILL FALSE AFTER LOOKING AGAIN, and this is the reason written down so
572
+ // nobody re-derives it. Klaviyo's connect reads the account name back so
573
+ // the card is not blank; Attentive's card stays blank. There IS an
574
+ // endpoint — GET https://api.attentivemobile.com/v1/me, Bearer, described
575
+ // on docs.attentive.com/pages/authentication/ as returning "information
576
+ // specific to your company" — but its RESPONSE SCHEMA is published
577
+ // nowhere we can read: the docs show the curl and no body. Reading
578
+ // `body.name` would be a guess, and a guess here fails at the worst
579
+ // moment, in the callback, after the merchant has already consented.
580
+ //
581
+ // A live token settles it in one call, alongside the three registration
582
+ // checks in the header. Until then the honest state is a blank field, not
583
+ // a hopeful one.
531
584
  connect: false,
532
585
  disconnect: false,
533
586
  probe: false,
@@ -549,7 +602,112 @@ var attentive_default2 = {
549
602
  }
550
603
  },
551
604
  commerce: false,
552
- contacts: { remove: false, sync: false },
605
+ // The verb the contacts.sync step points at.
606
+ contacts: {
607
+ // Not yet. Suppression syncs an opt-out as unsubscribed, which is a
608
+ // different thing from erasing the subscriber — Attentive's deletion sits
609
+ // behind their privacy-request API, which is a different grant.
610
+ remove: false,
611
+ // TWO CALLS, BECAUSE ATTENTIVE HAS TWO IDEAS.
612
+ //
613
+ // Subscribing and being in a segment are NOT the same operation here —
614
+ // unlike Klaviyo, where a subscription is created against the list itself.
615
+ // /v1/subscriptions takes no segment id at all, so the segment a merchant
616
+ // picked on this connection can only be honoured by the bulk segment
617
+ // membership API:
618
+ //
619
+ // subscribe POST /v1/subscriptions
620
+ // { user : { email, phone }, locale, subscriptionType } — the
621
+ // docs require EITHER signUpSourceId OR (locale +
622
+ // subscriptionType), and we hold no sign-up source. 202.
623
+ //
624
+ // membership POST /v2/bulk/segments/members
625
+ // { externalId, members : [ { email, phone } ] }, 1-10,000
626
+ // members, 202 with a batchJobId
627
+ // (docs.attentive.com/reference/postbulksegmentmembers).
628
+ //
629
+ // unsubscribe POST /v1/subscriptions/unsubscribe
630
+ // { user, subscriptions : [ { type, channel } ] }. 202.
631
+ //
632
+ // EVERY ONE OF THEM ANSWERS 202 ACCEPTED, which means Attentive took the
633
+ // job, not that it ran — the same distinction the Shopify usage charge
634
+ // makes between a 202 and a charge. The message below says accepted, and
635
+ // must keep saying accepted.
636
+ sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
637
+ var _a, _b, _c, _d;
638
+ const segment = settings == null ? void 0 : settings.segment;
639
+ if (!segment) return { message: "No Attentive segment is chosen for this connection.", skipped: true };
640
+ const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
641
+ const phone = toE164(((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.phone) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.phone));
642
+ if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", skipped: true };
643
+ const user = {
644
+ ...email && { email },
645
+ ...phone && { phone }
646
+ };
647
+ if (suppressed) {
648
+ await api("/v1/subscriptions/unsubscribe", {
649
+ fetcher,
650
+ method: "POST",
651
+ payload: {
652
+ // One entry per channel we can actually name them by. MARKETING
653
+ // is the only type Drawbridge ever subscribed them to.
654
+ subscriptions: [
655
+ ...phone ? [{ channel: "TEXT", type: "MARKETING" }] : [],
656
+ ...email ? [{ channel: "EMAIL", type: "MARKETING" }] : []
657
+ ],
658
+ user
659
+ },
660
+ token
661
+ });
662
+ return {
663
+ message: "Attentive accepted an unsubscribe for this contact \u2014 they have opted out.",
664
+ response: { accepted: true, unsubscribed: true }
665
+ };
666
+ }
667
+ await api("/v1/subscriptions", {
668
+ fetcher,
669
+ method: "POST",
670
+ payload: {
671
+ // LOCALE, because we hold no signUpSourceId and the docs require
672
+ // one or the other. The country is READ OFF the number when there
673
+ // is one — libphonenumber knows it from the calling code — rather
674
+ // than assumed; only the fallback pair below is a default, and it
675
+ // is the one value here that no vendor document dictates.
676
+ //
677
+ // ponytail: en/US default. A `signUpSourceId` field on the
678
+ // connection is the upgrade — Attentive's sign-up sources carry
679
+ // the consent language, which is a better answer than any locale
680
+ // we can infer — and it replaces this branch entirely.
681
+ locale: {
682
+ country: phone && detectCountry(phone) || "US",
683
+ language: "en"
684
+ },
685
+ subscriptionType: "MARKETING",
686
+ user
687
+ },
688
+ token
689
+ });
690
+ const membership = await api("/v2/bulk/segments/members", {
691
+ fetcher,
692
+ method: "POST",
693
+ payload: {
694
+ externalId: segment,
695
+ members: [user]
696
+ },
697
+ token
698
+ });
699
+ return {
700
+ // ACCEPTED, NOT LIVE. Both writes answered 202, which means Attentive
701
+ // queued them — a merchant who reads "synced" and looks for the person
702
+ // in Attentive a second later has been told the wrong thing.
703
+ message: "Attentive accepted this contact for the segment. Attentive processes these asynchronously, so it appears there shortly.",
704
+ response: {
705
+ accepted: true,
706
+ ...(membership == null ? void 0 : membership.batchJobId) && { batchJobId: membership.batchJobId }
707
+ }
708
+ };
709
+ }
710
+ },
553
711
  email: false,
554
712
  inbound: false,
555
713
  lifecycle: false,
@@ -562,26 +720,13 @@ var attentive_default2 = {
562
720
  // show a picker quietly missing most of a real account. The response's
563
721
  // only identifier is `externalId`, so an entry without one cannot be
564
722
  // stored and is dropped.
565
- audiences: async ({ cursor, limit = 100, search, token }, { fetcher = fetch } = {}) => {
723
+ audiences: async ({ cursor, limit = 100, search, token }, { fetcher } = {}) => {
566
724
  const query = new URLSearchParams({
567
725
  limit: String(Math.min(limit, 1e3)),
568
726
  ...cursor && { cursor },
569
727
  ...(search == null ? void 0 : search.value) && { name: String(search.value).trim() }
570
728
  });
571
- const response = await fetcher(
572
- "https://api.attentivemobile.com/v2/segments?" + query,
573
- {
574
- headers: { authorization: "Bearer " + token },
575
- signal: AbortSignal.timeout(15e3)
576
- }
577
- );
578
- if (!response.ok) {
579
- throw Object.assign(
580
- new Error("Attentive refused the request (" + response.status + ")"),
581
- { status: response.status }
582
- );
583
- }
584
- const body = await response.json();
729
+ const body = await api("/v2/segments?" + query, { fetcher, token });
585
730
  return {
586
731
  items: ((body == null ? void 0 : body.segments) || []).filter((segment) => segment == null ? void 0 : segment.externalId).map((segment) => ({ id: segment.externalId, title: (segment == null ? void 0 : segment.name) || segment.externalId })),
587
732
  pageInfo: {
@@ -599,33 +744,71 @@ var attentive_default2 = {
599
744
  webhook: false
600
745
  },
601
746
  icon: attentive_default,
747
+ // DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
748
+ // what an admin types on the provider screen, and the only declaration of it.
749
+ // It lives beside `requires`, which names the same variables: the manifest
750
+ // says what it needs and this says how someone supplies it, so a credential
751
+ // cannot be required by a vendor that offers nowhere to enter it.
752
+ //
753
+ // `redact` marks a secret — never returned by the api, and blank on save means
754
+ // keep the stored value. `required` drives the live check.
755
+ provider: {
756
+ fields: [
757
+ { input: "text", key: "clientId", credential: "ATTENTIVE_OAUTH_CLIENT_ID", label: "Client ID", required: true },
758
+ { input: "password", key: "clientSecret", credential: "ATTENTIVE_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
759
+ ]
760
+ },
602
761
  requires: [
603
762
  "ATTENTIVE_OAUTH_CLIENT_ID",
604
763
  "ATTENTIVE_OAUTH_CLIENT_SECRET"
605
764
  ],
606
765
  slug: "attentive",
607
- // A consent with no segment chosen is authenticated and inert — the sync,
608
- // when it ships, needs somewhere to put people.
766
+ // A consent with no segment chosen is authenticated and inert — the sync needs
767
+ // somewhere to put people so the card says Pending rather than Active over
768
+ // nothing.
609
769
  status: (data2) => {
610
770
  var _a;
611
771
  return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? data2.status : "pending";
612
772
  },
613
- // No steps: subscriber sync has not shipped, so this vendor contributes
614
- // nothing to a workflow yet. An empty steps object is the honest declaration.
615
- steps: {},
773
+ steps: {
774
+ contacts: {
775
+ // A DECLARATION, not the work. The nesting IS the name: this is
776
+ // `step.contacts.sync`, the string a workflow document stores. Klaviyo and
777
+ // Mailchimp declare the same type — a step belongs to the capability, not
778
+ // to whoever implements it — and the connection on the step document is
779
+ // what says which vendor runs.
780
+ sync: ({ data: data2 }) => ({
781
+ hook: "contacts.sync",
782
+ // NO ACCOUNT NAME TO INTERPOLATE, unlike Klaviyo: auth.connect is false
783
+ // because Attentive publishes no account-identity response we can read
784
+ // (see its comment), and settings.segment is an opaque externalId no
785
+ // merchant would recognise in a builder label.
786
+ key: "Sync contact to Attentive",
787
+ queue: "connection",
788
+ // Nothing for a merchant to configure on the step itself — the segment
789
+ // is chosen once on the connection. Declared empty rather than omitted,
790
+ // so "this step takes no settings" and "nobody thought about settings"
791
+ // stay different statements.
792
+ settings: {},
793
+ // BOTH triggers, for the same reason as Klaviyo: lead.insert alone only
794
+ // ever fires for someone with no history yet, and crossing into a
795
+ // segment is the other moment a contact is worth pushing.
796
+ triggers: ["lead.insert", "segment.contact.add"],
797
+ usage: { actions: 1 }
798
+ })
799
+ }
800
+ },
801
+ // WHY, in the merchant's words, and what to do about it.
802
+ // ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
803
+ // is reconnecting — prompting "choose a segment" there asks the merchant to
804
+ // configure a grant that no longer exists. `pending` is exactly this task's
805
+ // moment: the grant is good and the segment is the missing half.
616
806
  tasks: (data2) => {
617
807
  var _a;
618
- return [
619
- ...((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? [] : [
620
- {
621
- message: "Choose which Attentive segment your contacts should sync into. Until you do, nothing is being synced.",
622
- title: "Choose a segment"
623
- }
624
- ],
808
+ return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? [] : [
625
809
  {
626
- message: "Contact syncing to Attentive segments has not shipped yet. Nothing is being sent to Attentive right now.",
627
- title: "Subscriber sync not available yet",
628
- type: "warning"
810
+ message: "Choose which Attentive segment your contacts should sync into. Until you do, nothing is being synced.",
811
+ title: "Choose a segment"
629
812
  }
630
813
  ];
631
814
  },
@@ -827,7 +1010,7 @@ var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fi
827
1010
  <rect width="500" height="500" fill="#BAEC5F"/>
828
1011
  <g clip-path="url(#clip0_2115_2832)">
829
1012
  <path d="M140.224 127.586L174.803 188.73V311.176L140 372.32L176.084 392.031L216.111 321.753V178.278L176.341 108L140.224 127.586Z" fill="#0D1314"/>
830
- <path d="M360.001 127.523L323.693 108.282L284.948 178.498V321.596L322.923 391.749L359.393 372.79L326.224 311.52V188.73L360.001 127.523Z" fill="#0D1314"/>
1013
+ <path d="M360.002 127.523L323.694 108.282L284.949 178.498V321.596L322.924 391.749L359.394 372.79L326.225 311.52V188.73L360.002 127.523Z" fill="#0D1314"/>
831
1014
  </g>
832
1015
  <defs>
833
1016
  <clipPath id="clip0_2115_2832">
@@ -1866,6 +2049,34 @@ var drawbridge_default2 = {
1866
2049
  icon: drawbridge_default,
1867
2050
  // PRIVATE: never in the catalog, always available to the builder.
1868
2051
  private: true,
2052
+ // THE PLATFORM'S OWN SENDING CREDENTIALS — SendGrid, Twilio, and the internal
2053
+ // HubSpot portal. No merchant ever sees these; they are what an admin types on
2054
+ // the provider screen so that Drawbridge itself can send.
2055
+ //
2056
+ // They belong on THIS manifest because this is the connection that sends: the
2057
+ // email, sms and segment hooks below are the only things that spend them, and
2058
+ // a private connection is still where a vendor fact lives.
2059
+ //
2060
+ // UNLIKE every public vendor, none of these appear in `requires` — see the
2061
+ // comment there. Availability and configuration are different questions, and a
2062
+ // missing CRM token must not take every base workflow step away.
2063
+ provider: {
2064
+ fields: [
2065
+ { input: "email", key: "accountSender", credential: "SENDGRID_FROM_ADDRESS", label: "Account sender", message: "Verification codes and security alerts send from here.", required: true },
2066
+ { input: "password", key: "apiKey", credential: "SENDGRID_API_KEY", label: "SendGrid API key", redact: true, required: true },
2067
+ // NOT required. The CRM sync is best-effort internal tooling and no-ops
2068
+ // without a token — requiring it would make the whole drawbridge provider
2069
+ // read not-live over something no merchant ever sees.
2070
+ { input: "password", key: "hubspotToken", credential: "HUBSPOT_ACCESS_TOKEN", label: "HubSpot access token", message: "Drawbridge's own CRM portal. Internal \u2014 no merchant sees this.", redact: true },
2071
+ // Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
2072
+ // either. Unset, it degrades to the account sender rather than
2073
+ // refusing to start.
2074
+ { input: "email", key: "leadSender", credential: "SENDGRID_SEND_FROM_ADDRESS", label: "Lead sender", message: "The default for lead-facing mail when a merchant has not verified their own domain." },
2075
+ { input: "text", key: "smsFrom", credential: "TWILIO_ACCOUNT_FROM", label: "SMS number", required: true },
2076
+ { input: "password", key: "smsSid", credential: "TWILIO_ACCOUNT_SID", label: "Twilio account SID", redact: true, required: true },
2077
+ { input: "password", key: "smsToken", credential: "TWILIO_AUTH_TOKEN", label: "Twilio auth token", redact: true, required: true }
2078
+ ]
2079
+ },
1869
2080
  // NOTHING, and HUBSPOT_ACCESS_TOKEN in particular must not be here.
1870
2081
  //
1871
2082
  // `requires` gates AVAILABILITY: a name in it that is unset removes the whole
@@ -2002,12 +2213,12 @@ var drawbridge_default2 = {
2002
2213
 
2003
2214
  // lib/connections/icons/klaviyo.js
2004
2215
  var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2005
- <rect width="500" height="500" fill="white"/>
2216
+ <rect width="500" height="500" fill="#FF4B32"/>
2006
2217
  <path d="M365.047 327.038H134.954V172.964H365.047L316.856 250.001L365.047 327.038Z" fill="#232121"/>
2007
2218
  </svg>`;
2008
2219
 
2009
2220
  // lib/connections/providers/klaviyo.js
2010
- var api = async (path, { fetcher = fetch, method = "GET", payload, token }) => {
2221
+ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) => {
2011
2222
  const response = await fetcher("https://a.klaviyo.com/api" + path, {
2012
2223
  ...payload && { body: JSON.stringify(payload) },
2013
2224
  headers: {
@@ -2186,7 +2397,7 @@ var klaviyo_default2 = {
2186
2397
  // decided it, and asking again would be a question we can answer.
2187
2398
  connect: async ({ tokens }, { fetcher } = {}) => {
2188
2399
  var _a, _b, _c;
2189
- const body = await api("/accounts", { fetcher, token: tokens.accessToken });
2400
+ const body = await api2("/accounts", { fetcher, token: tokens.accessToken });
2190
2401
  const account = (_a = body == null ? void 0 : body.data) == null ? void 0 : _a[0];
2191
2402
  return {
2192
2403
  account: ((_c = (_b = account == null ? void 0 : account.attributes) == null ? void 0 : _b.contact_information) == null ? void 0 : _c.organization_name) || (account == null ? void 0 : account.id) || null,
@@ -2263,7 +2474,7 @@ var klaviyo_default2 = {
2263
2474
  const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
2264
2475
  if (!email) return { message: "That lead has no email address to sync.", skipped: true };
2265
2476
  const totals = (contact == null ? void 0 : contact.totals) || {};
2266
- const profile = await api("/profiles/", {
2477
+ const profile = await api2("/profiles/", {
2267
2478
  fetcher,
2268
2479
  method: "POST",
2269
2480
  payload: {
@@ -2291,7 +2502,7 @@ var klaviyo_default2 = {
2291
2502
  });
2292
2503
  const profileId = (_c = profile == null ? void 0 : profile.data) == null ? void 0 : _c.id;
2293
2504
  if (!profileId) return { message: "Klaviyo returned no profile id.", skipped: true };
2294
- await api("/profile-subscription-bulk-create-jobs/", {
2505
+ await api2("/profile-subscription-bulk-create-jobs/", {
2295
2506
  fetcher,
2296
2507
  method: "POST",
2297
2508
  payload: {
@@ -2354,7 +2565,7 @@ var klaviyo_default2 = {
2354
2565
  let next = cursor ? "/lists?page%5Bsize%5D=10&page%5Bcursor%5D=" + encodeURIComponent(cursor) : "/lists?page%5Bsize%5D=10";
2355
2566
  let pages = 0;
2356
2567
  while (next && audiences.length < limit && pages < 20) {
2357
- const body = await api(next, { fetcher, token });
2568
+ const body = await api2(next, { fetcher, token });
2358
2569
  for (const list of (body == null ? void 0 : body.data) || []) {
2359
2570
  audiences.push({ id: list.id, title: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id });
2360
2571
  }
@@ -2380,6 +2591,16 @@ var klaviyo_default2 = {
2380
2591
  webhook: false
2381
2592
  },
2382
2593
  icon: klaviyo_default,
2594
+ // DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
2595
+ // what an admin types on the provider screen. Declared here rather than in a
2596
+ // table in lib/providers.js, so a vendor's credentials sit beside the
2597
+ // `requires` that names the same variables.
2598
+ provider: {
2599
+ fields: [
2600
+ { input: "text", key: "clientId", credential: "KLAVIYO_OAUTH_CLIENT_ID", label: "Client ID", required: true },
2601
+ { input: "password", key: "clientSecret", credential: "KLAVIYO_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
2602
+ ]
2603
+ },
2383
2604
  requires: [
2384
2605
  "KLAVIYO_OAUTH_CLIENT_ID",
2385
2606
  "KLAVIYO_OAUTH_CLIENT_SECRET"
@@ -2441,9 +2662,13 @@ var klaviyo_default2 = {
2441
2662
  }
2442
2663
  },
2443
2664
  // WHY, in the merchant's words, and what to do about it.
2665
+ // ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
2666
+ // is reconnecting — prompting "choose a list" there asks the merchant to
2667
+ // configure a grant that no longer exists. `pending` is exactly this task's
2668
+ // moment: the grant is good and the list is the missing half.
2444
2669
  tasks: (data2) => {
2445
2670
  var _a;
2446
- return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [
2671
+ return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [
2447
2672
  {
2448
2673
  message: "Choose which Klaviyo list your contacts should sync into. Until you do, nothing is being synced.",
2449
2674
  title: "Choose a list"
@@ -2453,6 +2678,9 @@ var klaviyo_default2 = {
2453
2678
  title: "Klaviyo"
2454
2679
  };
2455
2680
 
2681
+ // lib/connections/providers/mailchimp.js
2682
+ var import_node_crypto2 = require("crypto");
2683
+
2456
2684
  // lib/connections/icons/mailchimp.js
2457
2685
  var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2458
2686
  <rect width="500" height="500" fill="#FFE01B"/>
@@ -2465,6 +2693,25 @@ var base = (dc) => {
2465
2693
  if (!dc) throw new Error("This Mailchimp connection has no data centre stored, so there is no host to call");
2466
2694
  return "https://" + dc + ".api.mailchimp.com/3.0";
2467
2695
  };
2696
+ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token }) => {
2697
+ const response = await fetcher(base(dc) + path, {
2698
+ ...payload && { body: JSON.stringify(payload) },
2699
+ headers: {
2700
+ authorization: "Bearer " + token,
2701
+ ...payload && { "content-type": "application/json" }
2702
+ },
2703
+ method,
2704
+ signal: AbortSignal.timeout(15e3)
2705
+ });
2706
+ if (!response.ok) {
2707
+ throw Object.assign(
2708
+ new Error("Mailchimp refused the request (" + response.status + ")"),
2709
+ { status: response.status }
2710
+ );
2711
+ }
2712
+ return response.json();
2713
+ };
2714
+ var subscriberHash = (email) => (0, import_node_crypto2.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
2468
2715
  var mailchimp_default2 = {
2469
2716
  // OAUTH 2, authorization code. Every url below is quoted from
2470
2717
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -2506,8 +2753,10 @@ var mailchimp_default2 = {
2506
2753
  content: {
2507
2754
  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.",
2508
2755
  description: [
2509
- "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.",
2510
- "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."
2756
+ "Drawbridge no longer sends email through Mailchimp. Notification email now sends from Drawbridge itself, and verifying a domain under Messaging in your organization settings puts your own brand in the from line.",
2757
+ "Connecting Mailchimp lets Drawbridge sync the contacts your campaigns collect into a Mailchimp audience, so the people who enter a giveaway can be marketed to alongside the rest of your list.",
2758
+ "Anyone who has opted out in Drawbridge is synced as unsubscribed rather than omitted, so a person who asked not to be contacted stays suppressed in both systems instead of quietly reappearing.",
2759
+ "Someone who unsubscribed inside Mailchimp keeps that choice: a resync only sets the status of a subscriber Mailchimp has never seen before."
2511
2760
  ],
2512
2761
  excerpt: "Sync your Drawbridge contacts into a Mailchimp audience.",
2513
2762
  guide: [
@@ -2582,7 +2831,51 @@ var mailchimp_default2 = {
2582
2831
  token: authToken
2583
2832
  },
2584
2833
  commerce: false,
2585
- contacts: { remove: false, sync: false },
2834
+ // The verb the contacts.sync step points at.
2835
+ contacts: {
2836
+ // Not yet. Suppression syncs an opt-out as unsubscribed, which is a
2837
+ // different thing from deleting the member — and Mailchimp's own delete is
2838
+ // permanent, so the address can never be re-added.
2839
+ remove: false,
2840
+ // PUT /lists/{list_id}/members/{subscriber_hash} — an UPSERT, which is
2841
+ // why there is no create-or-update branch here. Quoted from Mailchimp's
2842
+ // Marketing API reference for the list-members resource.
2843
+ sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
2844
+ var _a, _b;
2845
+ const audience = settings == null ? void 0 : settings.audience;
2846
+ if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
2847
+ const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
2848
+ if (!email) return { message: "That lead has no email address to sync.", skipped: true };
2849
+ const hash = subscriberHash(email);
2850
+ const member = await api3("/lists/" + audience + "/members/" + hash, {
2851
+ dc: settings == null ? void 0 : settings.dc,
2852
+ fetcher,
2853
+ method: "PUT",
2854
+ payload: {
2855
+ email_address: email,
2856
+ // FNAME ONLY. Unlike Klaviyo, Mailchimp's custom fields are not
2857
+ // schemaless — a merge tag that does not exist on the audience is
2858
+ // refused, taking the whole request with it — and FNAME is one of
2859
+ // the two tags every audience is created with. The Drawbridge
2860
+ // totals Klaviyo receives cannot travel until something registers
2861
+ // merge fields on the chosen audience, which is lifecycle.register's
2862
+ // job and is not built.
2863
+ ...(lead == null ? void 0 : lead.name) && { merge_fields: { FNAME: String(lead.name).trim().split(/\s+/)[0] } },
2864
+ ...suppressed && { status: "unsubscribed" },
2865
+ status_if_new: suppressed ? "unsubscribed" : "subscribed"
2866
+ },
2867
+ token
2868
+ });
2869
+ return {
2870
+ // Merged into `context` for later steps in this run.
2871
+ context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
2872
+ message: suppressed ? "Synced to Mailchimp as unsubscribed \u2014 this contact has opted out." : "Synced to the Mailchimp audience.",
2873
+ // Recorded on the run for support to read back, not a write
2874
+ // instruction — the hook has already written what it needed to.
2875
+ response: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash }
2876
+ };
2877
+ }
2878
+ },
2586
2879
  // Drawbridge sends its own notification email and SMS, and owns its own
2587
2880
  // segments — see the private `drawbridge` manifest. A vendor answering
2588
2881
  // these would be a second sender, which is the arrangement the platform
@@ -2601,24 +2894,13 @@ var mailchimp_default2 = {
2601
2894
  // successful — the same silent truncation Klaviyo has, at a different
2602
2895
  // number. Paged against total_items so an account past a thousand still
2603
2896
  // resolves.
2604
- audiences: async ({ cursor, limit = 100, search, settings, token }, { fetcher = fetch } = {}) => {
2605
- const dc = settings == null ? void 0 : settings.dc;
2897
+ audiences: async ({ cursor, limit = 100, search, settings, token }, { fetcher } = {}) => {
2606
2898
  const count = Math.min(limit, 1e3);
2607
2899
  const offset = Number(cursor || 0);
2608
- const response = await fetcher(
2609
- base(dc) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
2610
- {
2611
- headers: { authorization: "Bearer " + token },
2612
- signal: AbortSignal.timeout(15e3)
2613
- }
2900
+ const body = await api3(
2901
+ "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
2902
+ { dc: settings == null ? void 0 : settings.dc, fetcher, token }
2614
2903
  );
2615
- if (!response.ok) {
2616
- throw Object.assign(
2617
- new Error("Mailchimp refused the request (" + response.status + ")"),
2618
- { status: response.status }
2619
- );
2620
- }
2621
- const body = await response.json();
2622
2904
  const audiences = ((body == null ? void 0 : body.lists) || []).map((list) => ({ id: list.id, title: (list == null ? void 0 : list.name) || list.id }));
2623
2905
  const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
2624
2906
  const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
@@ -2640,6 +2922,15 @@ var mailchimp_default2 = {
2640
2922
  webhook: false
2641
2923
  },
2642
2924
  icon: mailchimp_default,
2925
+ // DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
2926
+ // what an admin types on the provider screen, beside the `requires` naming the
2927
+ // same variables.
2928
+ provider: {
2929
+ fields: [
2930
+ { input: "text", key: "clientId", credential: "MAILCHIMP_OAUTH_CLIENT_ID", label: "Client ID", required: true },
2931
+ { input: "password", key: "clientSecret", credential: "MAILCHIMP_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
2932
+ ]
2933
+ },
2643
2934
  // The OAuth client this deployment registered. Without both, the vendor drops
2644
2935
  // out of availableConnections rather than offering a Connect button that
2645
2936
  // cannot complete.
@@ -2648,32 +2939,58 @@ var mailchimp_default2 = {
2648
2939
  "MAILCHIMP_OAUTH_CLIENT_SECRET"
2649
2940
  ],
2650
2941
  slug: "mailchimp",
2651
- // A key with no audience chosen is authenticated and inert. Mailchimp also
2652
- // needs its merge fields created on that audience before any Drawbridge total
2653
- // can be written to a member unlike Klaviyo, its custom fields are not
2654
- // schemaless so the audience must be picked before lifecycle.register has
2655
- // anything to register against.
2942
+ // A grant with no audience chosen is authenticated and useless the sync has
2943
+ // nowhere to put anyone so the card must say Pending rather than Active over
2944
+ // nothing. Mailchimp also needs its merge fields created on that audience
2945
+ // before any Drawbridge total can be written to a member — unlike Klaviyo, its
2946
+ // custom fields are not schemaless — so the audience must be picked before
2947
+ // lifecycle.register has anything to register against.
2656
2948
  status: (data2) => {
2657
2949
  var _a;
2658
2950
  return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? data2.status : "pending";
2659
2951
  },
2660
- // No steps: audience sync has not shipped, so this vendor contributes nothing
2661
- // to a workflow yet. An empty steps object is the honest declaration — the
2662
- // catalog renders the connection, and no builder offers a step it cannot run.
2663
- steps: {},
2952
+ steps: {
2953
+ contacts: {
2954
+ // A DECLARATION, not the work. It names the hook that does the work, and
2955
+ // the nesting IS the name: this is `step.contacts.sync`, the string a
2956
+ // workflow document stores. Klaviyo and Attentive declare the same type —
2957
+ // a step belongs to the capability, not to whoever implements it — and the
2958
+ // connection on the step document is what says which vendor runs.
2959
+ sync: ({ data: data2 }) => ({
2960
+ hook: "contacts.sync",
2961
+ // NO ACCOUNT NAME TO INTERPOLATE, unlike Klaviyo. Mailchimp's
2962
+ // auth.connect deliberately stores only the data centre (a test pins
2963
+ // that), and settings.audience is an opaque list id no merchant would
2964
+ // recognise in a builder label — so the label names the vendor rather
2965
+ // than showing a string like a1b2c3d4e5.
2966
+ key: "Sync contact to Mailchimp",
2967
+ queue: "connection",
2968
+ // Nothing for a merchant to configure on the step itself — the audience
2969
+ // is chosen once on the connection. Declared empty rather than omitted,
2970
+ // so "this step takes no settings" and "nobody thought about settings"
2971
+ // stay different statements.
2972
+ settings: {},
2973
+ // BOTH triggers, for the same reason as Klaviyo: lead.insert alone only
2974
+ // ever fires for someone with no history yet, and crossing into a
2975
+ // segment is the other moment a contact is worth pushing.
2976
+ triggers: ["lead.insert", "segment.contact.add"],
2977
+ // One source for cost: what the builder discloses before a merchant
2978
+ // adds this step, and what is charged when it runs.
2979
+ usage: { actions: 1 }
2980
+ })
2981
+ }
2982
+ },
2983
+ // WHY, in the merchant's words, and what to do about it.
2984
+ // ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
2985
+ // is reconnecting — prompting "choose a audience" there asks the merchant to
2986
+ // configure a grant that no longer exists. `pending` is exactly this task's
2987
+ // moment: the grant is good and the audience is the missing half.
2664
2988
  tasks: (data2) => {
2665
2989
  var _a;
2666
- return [
2667
- ...((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? [] : [
2668
- {
2669
- message: "Choose which Mailchimp audience your contacts should sync into. Until you do, nothing is being synced.",
2670
- title: "Choose an audience"
2671
- }
2672
- ],
2990
+ return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? [] : [
2673
2991
  {
2674
- message: "Contact syncing to Mailchimp audiences has not shipped yet, and this connection no longer sends your email. Nothing is being sent to Mailchimp right now.",
2675
- title: "Audience sync not available yet",
2676
- type: "warning"
2992
+ message: "Choose which Mailchimp audience your contacts should sync into. Until you do, nothing is being synced.",
2993
+ title: "Choose an audience"
2677
2994
  }
2678
2995
  ];
2679
2996
  },
@@ -2681,19 +2998,18 @@ var mailchimp_default2 = {
2681
2998
  };
2682
2999
 
2683
3000
  // lib/connections/providers/shopify.js
2684
- var import_node_crypto3 = require("crypto");
3001
+ var import_node_crypto4 = require("crypto");
2685
3002
  var import_nanoid2 = require("nanoid");
2686
3003
 
2687
3004
  // lib/connections/icons/shopify.js
2688
3005
  var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2689
- <rect width="500" height="500" fill="white"/>
2690
- <path fill-rule="evenodd" clip-rule="evenodd" d="M309.524 407.192L308.799 128.423C306.921 126.545 303.258 127.112 301.827 127.531L292.29 130.487C291.113 126.613 289.585 122.854 287.726 119.258C280.959 106.337 271.069 99.5052 259.096 99.4866H259.059C258.259 99.4866 257.469 99.5609 256.67 99.626L256.577 99.6353C256.231 99.2089 255.871 98.7935 255.499 98.3897C250.293 92.8125 243.601 90.0889 235.588 90.3213C220.139 90.7675 204.755 101.941 192.271 121.786C183.487 135.757 176.823 153.298 174.917 166.878L144.493 176.313C135.542 179.129 135.263 179.408 134.082 187.858C133.199 194.253 109.766 375.69 109.766 375.69L306.159 409.692L309.524 407.192ZM245.181 103.065C242.569 101.346 239.511 100.546 235.885 100.621C212.033 101.308 191.23 138.611 185.923 163.467L208.771 156.384L212.851 155.119C215.845 139.336 223.355 122.957 233.181 112.416C236.616 108.639 240.671 105.477 245.172 103.065H245.181ZM224.145 151.615L256.94 141.446C257.042 132.894 256.112 120.252 251.836 111.329C247.282 113.207 243.452 116.497 240.7 119.444C233.329 127.373 227.315 139.466 224.155 151.615H224.145ZM267.211 138.267L282.455 133.536C280.02 125.616 274.238 112.342 262.517 110.111C266.161 119.527 267.099 130.431 267.211 138.267Z" fill="#95BF47"/>
2691
- <path d="M353.528 149.156C352.356 149.063 329.657 148.709 329.657 148.709C329.657 148.709 310.666 130.249 308.789 128.362C308.062 127.691 307.141 127.268 306.158 127.153V409.64L391.257 388.456C391.257 388.456 356.53 153.366 356.307 151.758C356.199 151.075 355.866 150.448 355.361 149.976C354.856 149.505 354.216 149.216 353.528 149.156Z" fill="#5E8E3E"/>
2692
- <path d="M258.993 193.019L249.103 230.052C249.103 230.052 238.079 225.023 225 225.85C205.833 227.059 205.628 239.171 205.824 242.21C206.865 258.756 250.376 262.381 252.821 301.171C254.745 331.688 236.656 352.574 210.592 354.21C179.313 356.19 162.089 337.711 162.089 337.711L168.716 309.472C168.716 309.472 186.052 322.569 199.921 321.686C208.993 321.119 212.228 313.738 211.903 308.514C210.536 286.921 175.102 288.185 172.862 252.695C170.985 222.811 190.57 192.554 233.803 189.821C250.46 188.762 258.993 193.028 258.993 193.028" fill="white"/>
3006
+ <rect width="500" height="500" fill="#95C049"/>
3007
+ <path d="M292.997 147.633C292.997 147.633 289.98 148.495 285.023 150.004C284.161 147.202 282.868 143.969 281.144 140.521C275.54 129.744 267.134 123.925 257.22 123.925C256.574 123.925 255.927 123.925 255.065 124.141C254.849 123.71 254.418 123.494 254.203 123.063C249.892 118.322 244.289 116.166 237.607 116.382C224.676 116.813 211.744 126.081 201.399 142.676C194.071 154.314 188.468 168.97 186.959 180.393C172.088 184.919 161.743 188.152 161.527 188.367C153.984 190.738 153.768 190.954 152.906 198.066C151.613 203.454 132 355.4 132 355.4L294.937 383.633V147.202C294.075 147.418 293.429 147.418 292.997 147.633ZM255.281 159.271C246.66 161.858 237.176 164.875 227.909 167.677C230.495 157.547 235.668 147.418 241.702 140.736C244.073 138.365 247.306 135.564 250.97 133.839C254.634 141.598 255.496 152.159 255.281 159.271ZM237.823 125.003C240.84 125.003 243.427 125.649 245.582 126.943C242.133 128.667 238.685 131.469 235.452 134.702C227.262 143.538 221.012 157.332 218.426 170.479C210.667 172.85 202.908 175.22 195.796 177.376C200.322 156.901 217.779 125.649 237.823 125.003ZM212.607 243.542C213.469 257.335 249.892 260.353 252.048 292.897C253.556 318.545 238.47 336.002 216.701 337.295C190.407 339.02 175.967 323.502 175.967 323.502L181.571 299.794C181.571 299.794 196.011 310.786 207.649 309.924C215.193 309.493 217.995 303.242 217.779 298.932C216.701 280.828 186.959 281.905 185.019 252.163C183.295 227.377 199.675 202.161 235.883 199.79C249.892 198.928 257.005 202.377 257.005 202.377L248.815 233.412C248.815 233.412 239.547 229.102 228.555 229.964C212.607 231.041 212.391 241.171 212.607 243.542ZM263.902 156.685C263.902 150.219 263.039 140.952 260.022 133.193C269.936 135.133 274.678 146.124 276.833 152.806C272.954 153.883 268.643 155.176 263.902 156.685Z" fill="white"/>
3008
+ <path d="M300.325 382.771L368 365.96C368 365.96 338.904 169.185 338.689 167.892C338.473 166.599 337.396 165.737 336.318 165.737C335.24 165.737 316.274 165.306 316.274 165.306C316.274 165.306 304.636 154.099 300.325 149.788V382.771Z" fill="white"/>
2693
3009
  </svg>`;
2694
3010
 
2695
3011
  // lib/connections/inbound.js
2696
- var import_node_crypto2 = require("crypto");
3012
+ var import_node_crypto3 = require("crypto");
2697
3013
  var verifySignature = ({ body, descriptor, headers, secret }) => {
2698
3014
  if (!secret) {
2699
3015
  throw Object.assign(new Error("Missing webhook secret: " + descriptor.signature.secret), { status: 500 });
@@ -2702,10 +3018,10 @@ var verifySignature = ({ body, descriptor, headers, secret }) => {
2702
3018
  if (!provided) {
2703
3019
  throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
2704
3020
  }
2705
- const digest = (0, import_node_crypto2.createHmac)(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
3021
+ const digest = (0, import_node_crypto3.createHmac)(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
2706
3022
  const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
2707
3023
  const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
2708
- if (digestBuffer.length !== providedBuffer.length || !(0, import_node_crypto2.timingSafeEqual)(digestBuffer, providedBuffer)) {
3024
+ if (digestBuffer.length !== providedBuffer.length || !(0, import_node_crypto3.timingSafeEqual)(digestBuffer, providedBuffer)) {
2709
3025
  throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
2710
3026
  }
2711
3027
  return JSON.parse(body.toString());
@@ -2728,18 +3044,6 @@ var toCanonicalEmail = (value) => {
2728
3044
  return local + "@" + domain;
2729
3045
  };
2730
3046
 
2731
- // lib/phone.js
2732
- var import_libphonenumber_js = require("libphonenumber-js");
2733
- var toE164 = (value, country) => {
2734
- if (!value) return null;
2735
- try {
2736
- const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(String(value), country);
2737
- return parsed ? parsed.number : null;
2738
- } catch {
2739
- return null;
2740
- }
2741
- };
2742
-
2743
3047
  // lib/connections/providers/shopify.js
2744
3048
  var toLine = ({
2745
3049
  price,
@@ -2839,7 +3143,7 @@ var shopify_default2 = {
2839
3143
  // the App Store listing, and the dashboard must never imply a store can be
2840
3144
  // linked from inside it.
2841
3145
  redirect: {
2842
- env: "SHOPIFY_APP_LISTING_URL",
3146
+ credential: "SHOPIFY_APP_LISTING_URL",
2843
3147
  title: "View on the Shopify App Store"
2844
3148
  }
2845
3149
  },
@@ -3456,7 +3760,7 @@ var shopify_default2 = {
3456
3760
  event: "shopify.register.webhooks"
3457
3761
  },
3458
3762
  name: "register",
3459
- options: { jobId: "connection.update.register." + workflow.connection + "." + (0, import_node_crypto3.randomUUID)() },
3763
+ options: { jobId: "connection.update.register." + workflow.connection + "." + (0, import_node_crypto4.randomUUID)() },
3460
3764
  queue: "connection"
3461
3765
  }],
3462
3766
  message: (scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.",
@@ -3592,6 +3896,19 @@ var shopify_default2 = {
3592
3896
  const shop = (data2 == null ? void 0 : data2.shop) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.domain);
3593
3897
  return shop ? "https://admin.shopify.com/store/" + String(shop).replace(".myshopify.com", "") + "/apps/" + (env == null ? void 0 : env.SHOPIFY_APP_HANDLE) : void 0;
3594
3898
  },
3899
+ // DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
3900
+ // what an admin types on the provider screen. The four names below are exactly
3901
+ // what `requires` gates on, which is the point of declaring them together: a
3902
+ // name required by the manifest and enterable nowhere is a vendor that can
3903
+ // never go live from the admin screen.
3904
+ provider: {
3905
+ fields: [
3906
+ { input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
3907
+ { input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
3908
+ { input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
3909
+ { input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true }
3910
+ ]
3911
+ },
3595
3912
  // A pre-launch integration: it only surfaces once the App Store listing
3596
3913
  // exists and the app is fully configured. Requiring all four means it can
3597
3914
  // never render half-configured — and absence of any one excludes the
@@ -3730,7 +4047,7 @@ var shopify_default2 = {
3730
4047
  };
3731
4048
 
3732
4049
  // lib/connections/providers/webhook.js
3733
- var import_node_crypto4 = __toESM(require("crypto"), 1);
4050
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
3734
4051
 
3735
4052
  // lib/safe-http.js
3736
4053
  var import_dns2 = __toESM(require("dns"), 1);
@@ -3962,7 +4279,7 @@ var webhook_default = {
3962
4279
  request2.body = body;
3963
4280
  const outgoing = { ...headers };
3964
4281
  if (settings == null ? void 0 : settings.secret) {
3965
- outgoing["X-Drawbridge-Signature"] = "sha256=" + import_node_crypto4.default.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
4282
+ outgoing["X-Drawbridge-Signature"] = "sha256=" + import_node_crypto5.default.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
3966
4283
  }
3967
4284
  const response = await send2({ body, headers: outgoing, method, url });
3968
4285
  return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
@@ -4008,7 +4325,10 @@ var webhook_default = {
4008
4325
  // pressing Connect will do; afterwards it states the verification the
4009
4326
  // merchant's own endpoint has to perform, because a signed payload nobody
4010
4327
  // checks is an unsigned payload.
4011
- tasks: ({ settings }) => (settings == null ? void 0 : settings.secret) ? [
4328
+ // The connect prompt only where connecting is not already the card's whole
4329
+ // story: a disconnected or errored document renders a Connect action itself,
4330
+ // and a task repeating it talks over the button.
4331
+ tasks: ({ settings, status } = {}) => ["disconnected", "error"].includes(status) ? [] : (settings == null ? void 0 : settings.secret) ? [
4012
4332
  {
4013
4333
  message: "Compute HMAC-SHA256( secret, body ) and compare against the X-Drawbridge-Signature header to confirm each payload.",
4014
4334
  title: "Requests must be verified",
@@ -4033,7 +4353,7 @@ var leaves = (node, path = []) => Object.entries(node || {}).flatMap(
4033
4353
  ([key, value]) => typeof value === "function" ? [[[...path, key].join("."), value]] : value && typeof value === "object" ? leaves(value, [...path, key]) : []
4034
4354
  );
4035
4355
  var build = (manifest) => {
4036
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
4356
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
4037
4357
  if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
4038
4358
  if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
4039
4359
  if (!(manifest == null ? void 0 : manifest.private) && !(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
@@ -4063,6 +4383,17 @@ var build = (manifest) => {
4063
4383
  }
4064
4384
  }
4065
4385
  }
4386
+ for (const field2 of ((_c = manifest.provider) == null ? void 0 : _c.fields) || []) {
4387
+ if (!(field2 == null ? void 0 : field2.key) || !(field2 == null ? void 0 : field2.label)) {
4388
+ throw new Error(manifest.slug + " declares a provider field with no key or label");
4389
+ }
4390
+ if (!INPUTS.includes(field2.input)) {
4391
+ throw new Error(manifest.slug + ".provider." + field2.key + " needs an input the admin form can render \u2014 one of " + INPUTS.join(", "));
4392
+ }
4393
+ if (field2.input === "password" && !field2.redact) {
4394
+ throw new Error(manifest.slug + ".provider." + field2.key + " is a password and must declare redact : true \u2014 the api would hand the value back");
4395
+ }
4396
+ }
4066
4397
  if (typeof (manifest == null ? void 0 : manifest.icon) !== "string" || !manifest.icon.includes("<svg")) {
4067
4398
  throw new Error(manifest.slug + " needs an icon \u2014 the svg markup itself, not a path to one");
4068
4399
  }
@@ -4075,23 +4406,23 @@ var build = (manifest) => {
4075
4406
  if (!GROUPS.includes(manifest == null ? void 0 : manifest.group)) {
4076
4407
  throw new Error(manifest.slug + " needs a group \u2014 one of " + GROUPS.join(", "));
4077
4408
  }
4078
- if ((_c = manifest == null ? void 0 : manifest.connect) == null ? void 0 : _c.type) {
4409
+ if ((_d = manifest == null ? void 0 : manifest.connect) == null ? void 0 : _d.type) {
4079
4410
  throw new Error(manifest.slug + " declares connect.type \u2014 that is auth.type now");
4080
4411
  }
4081
- if (!AUTH_TYPES.includes((_d = manifest == null ? void 0 : manifest.auth) == null ? void 0 : _d.type)) {
4412
+ if (!AUTH_TYPES.includes((_e = manifest == null ? void 0 : manifest.auth) == null ? void 0 : _e.type)) {
4082
4413
  throw new Error(manifest.slug + " needs auth.type \u2014 one of " + AUTH_TYPES.join(", "));
4083
4414
  }
4084
4415
  if (manifest.auth.type === "oauth") {
4085
4416
  for (const field2 of OAUTH_FIELDS) {
4086
- if (!((_e = manifest.auth.oauth) == null ? void 0 : _e[field2])) {
4417
+ if (!((_f = manifest.auth.oauth) == null ? void 0 : _f[field2])) {
4087
4418
  throw new Error(manifest.slug + " is oauth and must declare auth.oauth." + field2);
4088
4419
  }
4089
4420
  }
4090
- if (typeof ((_g = (_f = manifest.hooks) == null ? void 0 : _f.auth) == null ? void 0 : _g.token) !== "function") {
4421
+ if (typeof ((_h = (_g = manifest.hooks) == null ? void 0 : _g.auth) == null ? void 0 : _h.token) !== "function") {
4091
4422
  throw new Error(manifest.slug + " is oauth and must implement hooks.auth.token \u2014 point it at authToken() or wrap it");
4092
4423
  }
4093
4424
  for (const url of OAUTH_URLS) {
4094
- if (!((_i = (_h = manifest.auth.oauth) == null ? void 0 : _h.urls) == null ? void 0 : _i[url])) {
4425
+ if (!((_j = (_i = manifest.auth.oauth) == null ? void 0 : _i.urls) == null ? void 0 : _j[url])) {
4095
4426
  throw new Error(manifest.slug + " is oauth and must declare auth.oauth.urls." + url);
4096
4427
  }
4097
4428
  }
@@ -4101,16 +4432,16 @@ var build = (manifest) => {
4101
4432
  );
4102
4433
  }
4103
4434
  }
4104
- if (implemented(manifest.hooks, "inbound.event") && !((_k = (_j = manifest.inbound) == null ? void 0 : _j.headers) == null ? void 0 : _k.event)) {
4435
+ if (implemented(manifest.hooks, "inbound.event") && !((_l = (_k = manifest.inbound) == null ? void 0 : _k.headers) == null ? void 0 : _l.event)) {
4105
4436
  throw new Error(manifest.slug + " implements inbound.event but declares no inbound.headers.event");
4106
4437
  }
4107
- if (implemented(manifest.hooks, "inbound.verify") && !((_m = (_l = manifest.inbound) == null ? void 0 : _l.headers) == null ? void 0 : _m.signature)) {
4438
+ if (implemented(manifest.hooks, "inbound.verify") && !((_n = (_m = manifest.inbound) == null ? void 0 : _m.headers) == null ? void 0 : _n.signature)) {
4108
4439
  throw new Error(manifest.slug + " implements inbound.verify but declares no inbound.headers.signature");
4109
4440
  }
4110
4441
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
4111
4442
  throw new Error(manifest.slug + " must declare status( data ) \u2014 return null to accept the connection's own status, or { message, status } to override it");
4112
4443
  }
4113
- if (!Array.isArray((_n = manifest == null ? void 0 : manifest.content) == null ? void 0 : _n.guide) || !manifest.content.guide.length) {
4444
+ if (!Array.isArray((_o = manifest == null ? void 0 : manifest.content) == null ? void 0 : _o.guide) || !manifest.content.guide.length) {
4114
4445
  throw new Error(manifest.slug + " needs content.guide \u2014 an array of steps for its page");
4115
4446
  }
4116
4447
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
@@ -4122,8 +4453,8 @@ var build = (manifest) => {
4122
4453
  }
4123
4454
  for (const [domain, verbs] of Object.entries(HOOKS)) {
4124
4455
  for (const verb of verbs) {
4125
- const hook = (_p = (_o = manifest.hooks) == null ? void 0 : _o[domain]) == null ? void 0 : _p[verb];
4126
- if (((_q = manifest.hooks) == null ? void 0 : _q[domain]) === false) continue;
4456
+ const hook = (_q = (_p = manifest.hooks) == null ? void 0 : _p[domain]) == null ? void 0 : _q[verb];
4457
+ if (((_r = manifest.hooks) == null ? void 0 : _r[domain]) === false) continue;
4127
4458
  if (hook !== false && !implemented({ [domain]: { [verb]: hook } }, domain + "." + verb)) {
4128
4459
  throw new Error(manifest.slug + " must answer hooks." + domain + "." + verb + " \u2014 false, a function, or {} if another repo implements it");
4129
4460
  }
@@ -4169,14 +4500,18 @@ var connections = Object.freeze({
4169
4500
  webhook: build(webhook_default)
4170
4501
  });
4171
4502
  (() => {
4172
- const owners = {};
4503
+ var _a;
4504
+ const routes = {};
4173
4505
  for (const [slug, manifest] of Object.entries(connections)) {
4174
- for (const [name] of leaves(manifest.steps)) {
4506
+ for (const [name, step] of leaves(manifest.steps)) {
4175
4507
  const type = "step." + name;
4176
- if (owners[type]) {
4177
- throw new Error("Step " + type + " is declared by both " + owners[type] + " and " + slug);
4508
+ const queue = (_a = step({})) == null ? void 0 : _a.queue;
4509
+ if (routes[type] && routes[type].queue !== queue) {
4510
+ throw new Error(
4511
+ "Step " + type + " routes to " + routes[type].queue + " for " + routes[type].slug + " and " + queue + " for " + slug + " \u2014 one of them would be enqueued nowhere"
4512
+ );
4178
4513
  }
4179
- owners[type] = slug;
4514
+ routes[type] = { queue, slug };
4180
4515
  }
4181
4516
  }
4182
4517
  })();
@@ -4270,43 +4605,11 @@ var decrypt = (value) => {
4270
4605
  };
4271
4606
 
4272
4607
  // lib/providers.js
4273
- var FIELDS = {
4274
- attentive: [
4275
- { input: "text", key: "clientId", env: "ATTENTIVE_OAUTH_CLIENT_ID", label: "Client ID", required: true },
4276
- { input: "password", key: "clientSecret", env: "ATTENTIVE_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
4277
- ],
4278
- drawbridge: [
4279
- { input: "email", key: "accountSender", env: "SENDGRID_FROM_ADDRESS", label: "Account sender", message: "Verification codes and security alerts send from here.", required: true },
4280
- { input: "password", key: "apiKey", env: "SENDGRID_API_KEY", label: "SendGrid API key", redact: true, required: true },
4281
- // NOT required. The CRM sync is best-effort internal tooling and no-ops
4282
- // without a token — requiring it would make the whole drawbridge provider
4283
- // read not-live over something no merchant ever sees.
4284
- { input: "password", key: "hubspotToken", env: "HUBSPOT_ACCESS_TOKEN", label: "HubSpot access token", message: "Drawbridge's own CRM portal. Internal \u2014 no merchant sees this.", redact: true },
4285
- // Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
4286
- // either. Unset, it degrades to the account sender rather than
4287
- // refusing to start.
4288
- { input: "email", key: "leadSender", env: "SENDGRID_SEND_FROM_ADDRESS", label: "Lead sender", message: "The default for lead-facing mail when a merchant has not verified their own domain." },
4289
- { input: "text", key: "smsFrom", env: "TWILIO_ACCOUNT_FROM", label: "SMS number", required: true },
4290
- { input: "password", key: "smsSid", env: "TWILIO_ACCOUNT_SID", label: "Twilio account SID", redact: true, required: true },
4291
- { input: "password", key: "smsToken", env: "TWILIO_AUTH_TOKEN", label: "Twilio auth token", redact: true, required: true }
4292
- ],
4293
- klaviyo: [
4294
- { input: "text", key: "clientId", env: "KLAVIYO_OAUTH_CLIENT_ID", label: "Client ID", required: true },
4295
- { input: "password", key: "clientSecret", env: "KLAVIYO_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
4296
- ],
4297
- mailchimp: [
4298
- { input: "text", key: "clientId", env: "MAILCHIMP_OAUTH_CLIENT_ID", label: "Client ID", required: true },
4299
- { input: "password", key: "clientSecret", env: "MAILCHIMP_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
4300
- ],
4301
- shopify: [
4302
- { input: "text", key: "apiKey", env: "SHOPIFY_API_KEY", label: "API key", required: true },
4303
- { input: "password", key: "apiSecret", env: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
4304
- { input: "text", key: "appHandle", env: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
4305
- { input: "text", key: "listingUrl", env: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true }
4306
- ]
4608
+ var providerFields = (slug) => {
4609
+ var _a;
4610
+ return Object.hasOwn(connections, slug) ? ((_a = connections[slug].provider) == null ? void 0 : _a.fields) || [] : [];
4307
4611
  };
4308
- var providerFields = (slug) => Object.hasOwn(FIELDS, slug) ? FIELDS[slug] : [];
4309
- var providerSlugs = () => Object.keys(FIELDS).sort();
4612
+ var providerSlugs = () => Object.keys(connections).filter((slug) => connections[slug].provider).sort();
4310
4613
  var isLive = (slug, settings) => {
4311
4614
  const fields2 = providerFields(slug);
4312
4615
  if (!fields2.length) return false;
@@ -4363,7 +4666,7 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
4363
4666
  return result;
4364
4667
  };
4365
4668
  var providerEnvNames = () => new Set(
4366
- providerSlugs().flatMap((slug) => providerFields(slug)).map((field2) => field2.env).filter(Boolean)
4669
+ providerSlugs().flatMap((slug) => providerFields(slug)).map((field2) => field2.credential).filter(Boolean)
4367
4670
  );
4368
4671
  var providerCredentials = async ({ controller }) => {
4369
4672
  const credentials2 = {};
@@ -4372,7 +4675,7 @@ var providerCredentials = async ({ controller }) => {
4372
4675
  const settings = await providerSettings({ controller, slug });
4373
4676
  for (const field2 of providerFields(slug)) {
4374
4677
  const value = settings == null ? void 0 : settings[field2.key];
4375
- if (field2.env && value) credentials2[field2.env] = value;
4678
+ if (field2.credential && value) credentials2[field2.credential] = value;
4376
4679
  }
4377
4680
  } catch {
4378
4681
  continue;