@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.
- package/dist/connections/index.cjs +465 -130
- package/dist/connections/index.d.cts +626 -141
- package/dist/connections/index.d.ts +626 -141
- package/dist/connections/index.js +458 -123
- package/dist/providers.cjs +470 -167
- package/dist/providers.d.cts +40 -60
- package/dist/providers.d.ts +40 -60
- package/dist/providers.js +463 -160
- package/package.json +1 -1
package/dist/providers.js
CHANGED
|
@@ -404,7 +404,47 @@ var attentive_default = `<svg width="500" height="500" viewBox="0 0 500 500" fil
|
|
|
404
404
|
<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"/>
|
|
405
405
|
</svg>`;
|
|
406
406
|
|
|
407
|
+
// lib/phone.js
|
|
408
|
+
import { AsYouType, parsePhoneNumberFromString, isValidPhoneNumber } from "libphonenumber-js";
|
|
409
|
+
var toE164 = (value, country) => {
|
|
410
|
+
if (!value) return null;
|
|
411
|
+
try {
|
|
412
|
+
const parsed = parsePhoneNumberFromString(String(value), country);
|
|
413
|
+
return parsed ? parsed.number : null;
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
var detectCountry = (value) => {
|
|
419
|
+
if (!value) return null;
|
|
420
|
+
try {
|
|
421
|
+
const parser = new AsYouType();
|
|
422
|
+
parser.input(String(value));
|
|
423
|
+
return parser.getCountry() || null;
|
|
424
|
+
} catch {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
407
429
|
// lib/connections/providers/attentive.js
|
|
430
|
+
var api = async (path, { fetcher = fetch, method = "GET", payload, token }) => {
|
|
431
|
+
const response = await fetcher("https://api.attentivemobile.com" + path, {
|
|
432
|
+
...payload && { body: JSON.stringify(payload) },
|
|
433
|
+
headers: {
|
|
434
|
+
authorization: "Bearer " + token,
|
|
435
|
+
...payload && { "content-type": "application/json" }
|
|
436
|
+
},
|
|
437
|
+
method,
|
|
438
|
+
signal: AbortSignal.timeout(15e3)
|
|
439
|
+
});
|
|
440
|
+
if (!response.ok) {
|
|
441
|
+
throw Object.assign(
|
|
442
|
+
new Error("Attentive refused the request (" + response.status + ")"),
|
|
443
|
+
{ status: response.status }
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return response.json().catch(() => null);
|
|
447
|
+
};
|
|
408
448
|
var attentive_default2 = {
|
|
409
449
|
auth: {
|
|
410
450
|
oauth: {
|
|
@@ -438,9 +478,10 @@ var attentive_default2 = {
|
|
|
438
478
|
// has to say so rather than let them believe otherwise.
|
|
439
479
|
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.",
|
|
440
480
|
description: [
|
|
441
|
-
"Attentive is where your SMS marketing lives, and this connection
|
|
481
|
+
"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.",
|
|
442
482
|
"You authorize Drawbridge from inside Attentive and can revoke that access there at any time. Drawbridge never sees or stores your Attentive password.",
|
|
443
|
-
"
|
|
483
|
+
"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.",
|
|
484
|
+
"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."
|
|
444
485
|
],
|
|
445
486
|
excerpt: "Sync your Drawbridge contacts into an Attentive segment.",
|
|
446
487
|
guide: [
|
|
@@ -461,6 +502,10 @@ var attentive_default2 = {
|
|
|
461
502
|
message: "Contacts your campaigns collect are synced into this segment.",
|
|
462
503
|
hook: "resources.audiences",
|
|
463
504
|
required: true
|
|
505
|
+
// CONSUMED BY THE MEMBERSHIP CALL, not by the subscribe. Attentive's
|
|
506
|
+
// /v1/subscriptions takes no segment id — subscription and segment
|
|
507
|
+
// membership are two operations here — so contacts.sync makes both calls
|
|
508
|
+
// and this value is the externalId the second one carries.
|
|
464
509
|
// No `search : false` here, and that is a first: /v2/segments takes a
|
|
465
510
|
// `name` filter (partial match, cited above), so this picker searches
|
|
466
511
|
// the ACCOUNT — Klaviyo and Mailchimp can only match the fetched page.
|
|
@@ -473,17 +518,25 @@ var attentive_default2 = {
|
|
|
473
518
|
// and contacts.sync are the first to flip.
|
|
474
519
|
hooks: {
|
|
475
520
|
auth: {
|
|
476
|
-
// The exchange already yields the tokens, and Attentive documents no
|
|
477
|
-
// account-identity endpoint to enrich them with — Klaviyo's connect
|
|
478
|
-
// reads the account name back; this has nothing cited to read. The
|
|
479
|
-
// callback stores the tokens and skips enrichment on `unimplemented`.
|
|
480
521
|
// FALSE, NOT {}. `{}` means "supported, implemented in the repo with the
|
|
481
|
-
// dependencies", and nothing anywhere implements either of these —
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
522
|
+
// dependencies", and nothing anywhere implements either of these — there
|
|
523
|
+
// is nothing for them to do. They document no revocation endpoint at all,
|
|
524
|
+
// so disconnect has nothing to call. Recorded as a decision rather than
|
|
525
|
+
// left as an unkept promise.
|
|
526
|
+
//
|
|
527
|
+
// STILL FALSE AFTER LOOKING AGAIN, and this is the reason written down so
|
|
528
|
+
// nobody re-derives it. Klaviyo's connect reads the account name back so
|
|
529
|
+
// the card is not blank; Attentive's card stays blank. There IS an
|
|
530
|
+
// endpoint — GET https://api.attentivemobile.com/v1/me, Bearer, described
|
|
531
|
+
// on docs.attentive.com/pages/authentication/ as returning "information
|
|
532
|
+
// specific to your company" — but its RESPONSE SCHEMA is published
|
|
533
|
+
// nowhere we can read: the docs show the curl and no body. Reading
|
|
534
|
+
// `body.name` would be a guess, and a guess here fails at the worst
|
|
535
|
+
// moment, in the callback, after the merchant has already consented.
|
|
536
|
+
//
|
|
537
|
+
// A live token settles it in one call, alongside the three registration
|
|
538
|
+
// checks in the header. Until then the honest state is a blank field, not
|
|
539
|
+
// a hopeful one.
|
|
487
540
|
connect: false,
|
|
488
541
|
disconnect: false,
|
|
489
542
|
probe: false,
|
|
@@ -505,7 +558,112 @@ var attentive_default2 = {
|
|
|
505
558
|
}
|
|
506
559
|
},
|
|
507
560
|
commerce: false,
|
|
508
|
-
|
|
561
|
+
// The verb the contacts.sync step points at.
|
|
562
|
+
contacts: {
|
|
563
|
+
// Not yet. Suppression syncs an opt-out as unsubscribed, which is a
|
|
564
|
+
// different thing from erasing the subscriber — Attentive's deletion sits
|
|
565
|
+
// behind their privacy-request API, which is a different grant.
|
|
566
|
+
remove: false,
|
|
567
|
+
// TWO CALLS, BECAUSE ATTENTIVE HAS TWO IDEAS.
|
|
568
|
+
//
|
|
569
|
+
// Subscribing and being in a segment are NOT the same operation here —
|
|
570
|
+
// unlike Klaviyo, where a subscription is created against the list itself.
|
|
571
|
+
// /v1/subscriptions takes no segment id at all, so the segment a merchant
|
|
572
|
+
// picked on this connection can only be honoured by the bulk segment
|
|
573
|
+
// membership API:
|
|
574
|
+
//
|
|
575
|
+
// subscribe POST /v1/subscriptions
|
|
576
|
+
// { user : { email, phone }, locale, subscriptionType } — the
|
|
577
|
+
// docs require EITHER signUpSourceId OR (locale +
|
|
578
|
+
// subscriptionType), and we hold no sign-up source. 202.
|
|
579
|
+
//
|
|
580
|
+
// membership POST /v2/bulk/segments/members
|
|
581
|
+
// { externalId, members : [ { email, phone } ] }, 1-10,000
|
|
582
|
+
// members, 202 with a batchJobId
|
|
583
|
+
// (docs.attentive.com/reference/postbulksegmentmembers).
|
|
584
|
+
//
|
|
585
|
+
// unsubscribe POST /v1/subscriptions/unsubscribe
|
|
586
|
+
// { user, subscriptions : [ { type, channel } ] }. 202.
|
|
587
|
+
//
|
|
588
|
+
// EVERY ONE OF THEM ANSWERS 202 ACCEPTED, which means Attentive took the
|
|
589
|
+
// job, not that it ran — the same distinction the Shopify usage charge
|
|
590
|
+
// makes between a 202 and a charge. The message below says accepted, and
|
|
591
|
+
// must keep saying accepted.
|
|
592
|
+
sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
|
|
593
|
+
var _a, _b, _c, _d;
|
|
594
|
+
const segment = settings == null ? void 0 : settings.segment;
|
|
595
|
+
if (!segment) return { message: "No Attentive segment is chosen for this connection.", skipped: true };
|
|
596
|
+
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);
|
|
597
|
+
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));
|
|
598
|
+
if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", skipped: true };
|
|
599
|
+
const user = {
|
|
600
|
+
...email && { email },
|
|
601
|
+
...phone && { phone }
|
|
602
|
+
};
|
|
603
|
+
if (suppressed) {
|
|
604
|
+
await api("/v1/subscriptions/unsubscribe", {
|
|
605
|
+
fetcher,
|
|
606
|
+
method: "POST",
|
|
607
|
+
payload: {
|
|
608
|
+
// One entry per channel we can actually name them by. MARKETING
|
|
609
|
+
// is the only type Drawbridge ever subscribed them to.
|
|
610
|
+
subscriptions: [
|
|
611
|
+
...phone ? [{ channel: "TEXT", type: "MARKETING" }] : [],
|
|
612
|
+
...email ? [{ channel: "EMAIL", type: "MARKETING" }] : []
|
|
613
|
+
],
|
|
614
|
+
user
|
|
615
|
+
},
|
|
616
|
+
token
|
|
617
|
+
});
|
|
618
|
+
return {
|
|
619
|
+
message: "Attentive accepted an unsubscribe for this contact \u2014 they have opted out.",
|
|
620
|
+
response: { accepted: true, unsubscribed: true }
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
await api("/v1/subscriptions", {
|
|
624
|
+
fetcher,
|
|
625
|
+
method: "POST",
|
|
626
|
+
payload: {
|
|
627
|
+
// LOCALE, because we hold no signUpSourceId and the docs require
|
|
628
|
+
// one or the other. The country is READ OFF the number when there
|
|
629
|
+
// is one — libphonenumber knows it from the calling code — rather
|
|
630
|
+
// than assumed; only the fallback pair below is a default, and it
|
|
631
|
+
// is the one value here that no vendor document dictates.
|
|
632
|
+
//
|
|
633
|
+
// ponytail: en/US default. A `signUpSourceId` field on the
|
|
634
|
+
// connection is the upgrade — Attentive's sign-up sources carry
|
|
635
|
+
// the consent language, which is a better answer than any locale
|
|
636
|
+
// we can infer — and it replaces this branch entirely.
|
|
637
|
+
locale: {
|
|
638
|
+
country: phone && detectCountry(phone) || "US",
|
|
639
|
+
language: "en"
|
|
640
|
+
},
|
|
641
|
+
subscriptionType: "MARKETING",
|
|
642
|
+
user
|
|
643
|
+
},
|
|
644
|
+
token
|
|
645
|
+
});
|
|
646
|
+
const membership = await api("/v2/bulk/segments/members", {
|
|
647
|
+
fetcher,
|
|
648
|
+
method: "POST",
|
|
649
|
+
payload: {
|
|
650
|
+
externalId: segment,
|
|
651
|
+
members: [user]
|
|
652
|
+
},
|
|
653
|
+
token
|
|
654
|
+
});
|
|
655
|
+
return {
|
|
656
|
+
// ACCEPTED, NOT LIVE. Both writes answered 202, which means Attentive
|
|
657
|
+
// queued them — a merchant who reads "synced" and looks for the person
|
|
658
|
+
// in Attentive a second later has been told the wrong thing.
|
|
659
|
+
message: "Attentive accepted this contact for the segment. Attentive processes these asynchronously, so it appears there shortly.",
|
|
660
|
+
response: {
|
|
661
|
+
accepted: true,
|
|
662
|
+
...(membership == null ? void 0 : membership.batchJobId) && { batchJobId: membership.batchJobId }
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
},
|
|
509
667
|
email: false,
|
|
510
668
|
inbound: false,
|
|
511
669
|
lifecycle: false,
|
|
@@ -518,26 +676,13 @@ var attentive_default2 = {
|
|
|
518
676
|
// show a picker quietly missing most of a real account. The response's
|
|
519
677
|
// only identifier is `externalId`, so an entry without one cannot be
|
|
520
678
|
// stored and is dropped.
|
|
521
|
-
audiences: async ({ cursor, limit = 100, search, token }, { fetcher
|
|
679
|
+
audiences: async ({ cursor, limit = 100, search, token }, { fetcher } = {}) => {
|
|
522
680
|
const query = new URLSearchParams({
|
|
523
681
|
limit: String(Math.min(limit, 1e3)),
|
|
524
682
|
...cursor && { cursor },
|
|
525
683
|
...(search == null ? void 0 : search.value) && { name: String(search.value).trim() }
|
|
526
684
|
});
|
|
527
|
-
const
|
|
528
|
-
"https://api.attentivemobile.com/v2/segments?" + query,
|
|
529
|
-
{
|
|
530
|
-
headers: { authorization: "Bearer " + token },
|
|
531
|
-
signal: AbortSignal.timeout(15e3)
|
|
532
|
-
}
|
|
533
|
-
);
|
|
534
|
-
if (!response.ok) {
|
|
535
|
-
throw Object.assign(
|
|
536
|
-
new Error("Attentive refused the request (" + response.status + ")"),
|
|
537
|
-
{ status: response.status }
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
const body = await response.json();
|
|
685
|
+
const body = await api("/v2/segments?" + query, { fetcher, token });
|
|
541
686
|
return {
|
|
542
687
|
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 })),
|
|
543
688
|
pageInfo: {
|
|
@@ -555,33 +700,71 @@ var attentive_default2 = {
|
|
|
555
700
|
webhook: false
|
|
556
701
|
},
|
|
557
702
|
icon: attentive_default,
|
|
703
|
+
// DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
|
|
704
|
+
// what an admin types on the provider screen, and the only declaration of it.
|
|
705
|
+
// It lives beside `requires`, which names the same variables: the manifest
|
|
706
|
+
// says what it needs and this says how someone supplies it, so a credential
|
|
707
|
+
// cannot be required by a vendor that offers nowhere to enter it.
|
|
708
|
+
//
|
|
709
|
+
// `redact` marks a secret — never returned by the api, and blank on save means
|
|
710
|
+
// keep the stored value. `required` drives the live check.
|
|
711
|
+
provider: {
|
|
712
|
+
fields: [
|
|
713
|
+
{ input: "text", key: "clientId", credential: "ATTENTIVE_OAUTH_CLIENT_ID", label: "Client ID", required: true },
|
|
714
|
+
{ input: "password", key: "clientSecret", credential: "ATTENTIVE_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
|
|
715
|
+
]
|
|
716
|
+
},
|
|
558
717
|
requires: [
|
|
559
718
|
"ATTENTIVE_OAUTH_CLIENT_ID",
|
|
560
719
|
"ATTENTIVE_OAUTH_CLIENT_SECRET"
|
|
561
720
|
],
|
|
562
721
|
slug: "attentive",
|
|
563
|
-
// A consent with no segment chosen is authenticated and inert — the sync
|
|
564
|
-
//
|
|
722
|
+
// A consent with no segment chosen is authenticated and inert — the sync needs
|
|
723
|
+
// somewhere to put people — so the card says Pending rather than Active over
|
|
724
|
+
// nothing.
|
|
565
725
|
status: (data2) => {
|
|
566
726
|
var _a;
|
|
567
727
|
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? data2.status : "pending";
|
|
568
728
|
},
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
729
|
+
steps: {
|
|
730
|
+
contacts: {
|
|
731
|
+
// A DECLARATION, not the work. The nesting IS the name: this is
|
|
732
|
+
// `step.contacts.sync`, the string a workflow document stores. Klaviyo and
|
|
733
|
+
// Mailchimp declare the same type — a step belongs to the capability, not
|
|
734
|
+
// to whoever implements it — and the connection on the step document is
|
|
735
|
+
// what says which vendor runs.
|
|
736
|
+
sync: ({ data: data2 }) => ({
|
|
737
|
+
hook: "contacts.sync",
|
|
738
|
+
// NO ACCOUNT NAME TO INTERPOLATE, unlike Klaviyo: auth.connect is false
|
|
739
|
+
// because Attentive publishes no account-identity response we can read
|
|
740
|
+
// (see its comment), and settings.segment is an opaque externalId no
|
|
741
|
+
// merchant would recognise in a builder label.
|
|
742
|
+
key: "Sync contact to Attentive",
|
|
743
|
+
queue: "connection",
|
|
744
|
+
// Nothing for a merchant to configure on the step itself — the segment
|
|
745
|
+
// is chosen once on the connection. Declared empty rather than omitted,
|
|
746
|
+
// so "this step takes no settings" and "nobody thought about settings"
|
|
747
|
+
// stay different statements.
|
|
748
|
+
settings: {},
|
|
749
|
+
// BOTH triggers, for the same reason as Klaviyo: lead.insert alone only
|
|
750
|
+
// ever fires for someone with no history yet, and crossing into a
|
|
751
|
+
// segment is the other moment a contact is worth pushing.
|
|
752
|
+
triggers: ["lead.insert", "segment.contact.add"],
|
|
753
|
+
usage: { actions: 1 }
|
|
754
|
+
})
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
// WHY, in the merchant's words, and what to do about it.
|
|
758
|
+
// ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
|
|
759
|
+
// is reconnecting — prompting "choose a segment" there asks the merchant to
|
|
760
|
+
// configure a grant that no longer exists. `pending` is exactly this task's
|
|
761
|
+
// moment: the grant is good and the segment is the missing half.
|
|
572
762
|
tasks: (data2) => {
|
|
573
763
|
var _a;
|
|
574
|
-
return [
|
|
575
|
-
...((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? [] : [
|
|
576
|
-
{
|
|
577
|
-
message: "Choose which Attentive segment your contacts should sync into. Until you do, nothing is being synced.",
|
|
578
|
-
title: "Choose a segment"
|
|
579
|
-
}
|
|
580
|
-
],
|
|
764
|
+
return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? [] : [
|
|
581
765
|
{
|
|
582
|
-
message: "
|
|
583
|
-
title: "
|
|
584
|
-
type: "warning"
|
|
766
|
+
message: "Choose which Attentive segment your contacts should sync into. Until you do, nothing is being synced.",
|
|
767
|
+
title: "Choose a segment"
|
|
585
768
|
}
|
|
586
769
|
];
|
|
587
770
|
},
|
|
@@ -783,7 +966,7 @@ var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fi
|
|
|
783
966
|
<rect width="500" height="500" fill="#BAEC5F"/>
|
|
784
967
|
<g clip-path="url(#clip0_2115_2832)">
|
|
785
968
|
<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"/>
|
|
786
|
-
<path d="M360.
|
|
969
|
+
<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"/>
|
|
787
970
|
</g>
|
|
788
971
|
<defs>
|
|
789
972
|
<clipPath id="clip0_2115_2832">
|
|
@@ -1822,6 +2005,34 @@ var drawbridge_default2 = {
|
|
|
1822
2005
|
icon: drawbridge_default,
|
|
1823
2006
|
// PRIVATE: never in the catalog, always available to the builder.
|
|
1824
2007
|
private: true,
|
|
2008
|
+
// THE PLATFORM'S OWN SENDING CREDENTIALS — SendGrid, Twilio, and the internal
|
|
2009
|
+
// HubSpot portal. No merchant ever sees these; they are what an admin types on
|
|
2010
|
+
// the provider screen so that Drawbridge itself can send.
|
|
2011
|
+
//
|
|
2012
|
+
// They belong on THIS manifest because this is the connection that sends: the
|
|
2013
|
+
// email, sms and segment hooks below are the only things that spend them, and
|
|
2014
|
+
// a private connection is still where a vendor fact lives.
|
|
2015
|
+
//
|
|
2016
|
+
// UNLIKE every public vendor, none of these appear in `requires` — see the
|
|
2017
|
+
// comment there. Availability and configuration are different questions, and a
|
|
2018
|
+
// missing CRM token must not take every base workflow step away.
|
|
2019
|
+
provider: {
|
|
2020
|
+
fields: [
|
|
2021
|
+
{ input: "email", key: "accountSender", credential: "SENDGRID_FROM_ADDRESS", label: "Account sender", message: "Verification codes and security alerts send from here.", required: true },
|
|
2022
|
+
{ input: "password", key: "apiKey", credential: "SENDGRID_API_KEY", label: "SendGrid API key", redact: true, required: true },
|
|
2023
|
+
// NOT required. The CRM sync is best-effort internal tooling and no-ops
|
|
2024
|
+
// without a token — requiring it would make the whole drawbridge provider
|
|
2025
|
+
// read not-live over something no merchant ever sees.
|
|
2026
|
+
{ 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 },
|
|
2027
|
+
// Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
|
|
2028
|
+
// either. Unset, it degrades to the account sender rather than
|
|
2029
|
+
// refusing to start.
|
|
2030
|
+
{ 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." },
|
|
2031
|
+
{ input: "text", key: "smsFrom", credential: "TWILIO_ACCOUNT_FROM", label: "SMS number", required: true },
|
|
2032
|
+
{ input: "password", key: "smsSid", credential: "TWILIO_ACCOUNT_SID", label: "Twilio account SID", redact: true, required: true },
|
|
2033
|
+
{ input: "password", key: "smsToken", credential: "TWILIO_AUTH_TOKEN", label: "Twilio auth token", redact: true, required: true }
|
|
2034
|
+
]
|
|
2035
|
+
},
|
|
1825
2036
|
// NOTHING, and HUBSPOT_ACCESS_TOKEN in particular must not be here.
|
|
1826
2037
|
//
|
|
1827
2038
|
// `requires` gates AVAILABILITY: a name in it that is unset removes the whole
|
|
@@ -1958,12 +2169,12 @@ var drawbridge_default2 = {
|
|
|
1958
2169
|
|
|
1959
2170
|
// lib/connections/icons/klaviyo.js
|
|
1960
2171
|
var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1961
|
-
<rect width="500" height="500" fill="
|
|
2172
|
+
<rect width="500" height="500" fill="#FF4B32"/>
|
|
1962
2173
|
<path d="M365.047 327.038H134.954V172.964H365.047L316.856 250.001L365.047 327.038Z" fill="#232121"/>
|
|
1963
2174
|
</svg>`;
|
|
1964
2175
|
|
|
1965
2176
|
// lib/connections/providers/klaviyo.js
|
|
1966
|
-
var
|
|
2177
|
+
var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) => {
|
|
1967
2178
|
const response = await fetcher("https://a.klaviyo.com/api" + path, {
|
|
1968
2179
|
...payload && { body: JSON.stringify(payload) },
|
|
1969
2180
|
headers: {
|
|
@@ -2142,7 +2353,7 @@ var klaviyo_default2 = {
|
|
|
2142
2353
|
// decided it, and asking again would be a question we can answer.
|
|
2143
2354
|
connect: async ({ tokens }, { fetcher } = {}) => {
|
|
2144
2355
|
var _a, _b, _c;
|
|
2145
|
-
const body = await
|
|
2356
|
+
const body = await api2("/accounts", { fetcher, token: tokens.accessToken });
|
|
2146
2357
|
const account = (_a = body == null ? void 0 : body.data) == null ? void 0 : _a[0];
|
|
2147
2358
|
return {
|
|
2148
2359
|
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,
|
|
@@ -2219,7 +2430,7 @@ var klaviyo_default2 = {
|
|
|
2219
2430
|
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);
|
|
2220
2431
|
if (!email) return { message: "That lead has no email address to sync.", skipped: true };
|
|
2221
2432
|
const totals = (contact == null ? void 0 : contact.totals) || {};
|
|
2222
|
-
const profile = await
|
|
2433
|
+
const profile = await api2("/profiles/", {
|
|
2223
2434
|
fetcher,
|
|
2224
2435
|
method: "POST",
|
|
2225
2436
|
payload: {
|
|
@@ -2247,7 +2458,7 @@ var klaviyo_default2 = {
|
|
|
2247
2458
|
});
|
|
2248
2459
|
const profileId = (_c = profile == null ? void 0 : profile.data) == null ? void 0 : _c.id;
|
|
2249
2460
|
if (!profileId) return { message: "Klaviyo returned no profile id.", skipped: true };
|
|
2250
|
-
await
|
|
2461
|
+
await api2("/profile-subscription-bulk-create-jobs/", {
|
|
2251
2462
|
fetcher,
|
|
2252
2463
|
method: "POST",
|
|
2253
2464
|
payload: {
|
|
@@ -2310,7 +2521,7 @@ var klaviyo_default2 = {
|
|
|
2310
2521
|
let next = cursor ? "/lists?page%5Bsize%5D=10&page%5Bcursor%5D=" + encodeURIComponent(cursor) : "/lists?page%5Bsize%5D=10";
|
|
2311
2522
|
let pages = 0;
|
|
2312
2523
|
while (next && audiences.length < limit && pages < 20) {
|
|
2313
|
-
const body = await
|
|
2524
|
+
const body = await api2(next, { fetcher, token });
|
|
2314
2525
|
for (const list of (body == null ? void 0 : body.data) || []) {
|
|
2315
2526
|
audiences.push({ id: list.id, title: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id });
|
|
2316
2527
|
}
|
|
@@ -2336,6 +2547,16 @@ var klaviyo_default2 = {
|
|
|
2336
2547
|
webhook: false
|
|
2337
2548
|
},
|
|
2338
2549
|
icon: klaviyo_default,
|
|
2550
|
+
// DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
|
|
2551
|
+
// what an admin types on the provider screen. Declared here rather than in a
|
|
2552
|
+
// table in lib/providers.js, so a vendor's credentials sit beside the
|
|
2553
|
+
// `requires` that names the same variables.
|
|
2554
|
+
provider: {
|
|
2555
|
+
fields: [
|
|
2556
|
+
{ input: "text", key: "clientId", credential: "KLAVIYO_OAUTH_CLIENT_ID", label: "Client ID", required: true },
|
|
2557
|
+
{ input: "password", key: "clientSecret", credential: "KLAVIYO_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
|
|
2558
|
+
]
|
|
2559
|
+
},
|
|
2339
2560
|
requires: [
|
|
2340
2561
|
"KLAVIYO_OAUTH_CLIENT_ID",
|
|
2341
2562
|
"KLAVIYO_OAUTH_CLIENT_SECRET"
|
|
@@ -2397,9 +2618,13 @@ var klaviyo_default2 = {
|
|
|
2397
2618
|
}
|
|
2398
2619
|
},
|
|
2399
2620
|
// WHY, in the merchant's words, and what to do about it.
|
|
2621
|
+
// ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
|
|
2622
|
+
// is reconnecting — prompting "choose a list" there asks the merchant to
|
|
2623
|
+
// configure a grant that no longer exists. `pending` is exactly this task's
|
|
2624
|
+
// moment: the grant is good and the list is the missing half.
|
|
2400
2625
|
tasks: (data2) => {
|
|
2401
2626
|
var _a;
|
|
2402
|
-
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [
|
|
2627
|
+
return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [
|
|
2403
2628
|
{
|
|
2404
2629
|
message: "Choose which Klaviyo list your contacts should sync into. Until you do, nothing is being synced.",
|
|
2405
2630
|
title: "Choose a list"
|
|
@@ -2409,6 +2634,9 @@ var klaviyo_default2 = {
|
|
|
2409
2634
|
title: "Klaviyo"
|
|
2410
2635
|
};
|
|
2411
2636
|
|
|
2637
|
+
// lib/connections/providers/mailchimp.js
|
|
2638
|
+
import { createHash as createHash2 } from "crypto";
|
|
2639
|
+
|
|
2412
2640
|
// lib/connections/icons/mailchimp.js
|
|
2413
2641
|
var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2414
2642
|
<rect width="500" height="500" fill="#FFE01B"/>
|
|
@@ -2421,6 +2649,25 @@ var base = (dc) => {
|
|
|
2421
2649
|
if (!dc) throw new Error("This Mailchimp connection has no data centre stored, so there is no host to call");
|
|
2422
2650
|
return "https://" + dc + ".api.mailchimp.com/3.0";
|
|
2423
2651
|
};
|
|
2652
|
+
var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token }) => {
|
|
2653
|
+
const response = await fetcher(base(dc) + path, {
|
|
2654
|
+
...payload && { body: JSON.stringify(payload) },
|
|
2655
|
+
headers: {
|
|
2656
|
+
authorization: "Bearer " + token,
|
|
2657
|
+
...payload && { "content-type": "application/json" }
|
|
2658
|
+
},
|
|
2659
|
+
method,
|
|
2660
|
+
signal: AbortSignal.timeout(15e3)
|
|
2661
|
+
});
|
|
2662
|
+
if (!response.ok) {
|
|
2663
|
+
throw Object.assign(
|
|
2664
|
+
new Error("Mailchimp refused the request (" + response.status + ")"),
|
|
2665
|
+
{ status: response.status }
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
return response.json();
|
|
2669
|
+
};
|
|
2670
|
+
var subscriberHash = (email) => createHash2("md5").update(String(email).trim().toLowerCase()).digest("hex");
|
|
2424
2671
|
var mailchimp_default2 = {
|
|
2425
2672
|
// OAUTH 2, authorization code. Every url below is quoted from
|
|
2426
2673
|
// mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
|
|
@@ -2462,8 +2709,10 @@ var mailchimp_default2 = {
|
|
|
2462
2709
|
content: {
|
|
2463
2710
|
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.",
|
|
2464
2711
|
description: [
|
|
2465
|
-
"Drawbridge no longer sends email through Mailchimp. Notification email now sends from Drawbridge itself, and verifying a domain under
|
|
2466
|
-
"
|
|
2712
|
+
"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.",
|
|
2713
|
+
"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.",
|
|
2714
|
+
"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.",
|
|
2715
|
+
"Someone who unsubscribed inside Mailchimp keeps that choice: a resync only sets the status of a subscriber Mailchimp has never seen before."
|
|
2467
2716
|
],
|
|
2468
2717
|
excerpt: "Sync your Drawbridge contacts into a Mailchimp audience.",
|
|
2469
2718
|
guide: [
|
|
@@ -2538,7 +2787,51 @@ var mailchimp_default2 = {
|
|
|
2538
2787
|
token: authToken
|
|
2539
2788
|
},
|
|
2540
2789
|
commerce: false,
|
|
2541
|
-
|
|
2790
|
+
// The verb the contacts.sync step points at.
|
|
2791
|
+
contacts: {
|
|
2792
|
+
// Not yet. Suppression syncs an opt-out as unsubscribed, which is a
|
|
2793
|
+
// different thing from deleting the member — and Mailchimp's own delete is
|
|
2794
|
+
// permanent, so the address can never be re-added.
|
|
2795
|
+
remove: false,
|
|
2796
|
+
// PUT /lists/{list_id}/members/{subscriber_hash} — an UPSERT, which is
|
|
2797
|
+
// why there is no create-or-update branch here. Quoted from Mailchimp's
|
|
2798
|
+
// Marketing API reference for the list-members resource.
|
|
2799
|
+
sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
|
|
2800
|
+
var _a, _b;
|
|
2801
|
+
const audience = settings == null ? void 0 : settings.audience;
|
|
2802
|
+
if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
|
|
2803
|
+
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);
|
|
2804
|
+
if (!email) return { message: "That lead has no email address to sync.", skipped: true };
|
|
2805
|
+
const hash = subscriberHash(email);
|
|
2806
|
+
const member = await api3("/lists/" + audience + "/members/" + hash, {
|
|
2807
|
+
dc: settings == null ? void 0 : settings.dc,
|
|
2808
|
+
fetcher,
|
|
2809
|
+
method: "PUT",
|
|
2810
|
+
payload: {
|
|
2811
|
+
email_address: email,
|
|
2812
|
+
// FNAME ONLY. Unlike Klaviyo, Mailchimp's custom fields are not
|
|
2813
|
+
// schemaless — a merge tag that does not exist on the audience is
|
|
2814
|
+
// refused, taking the whole request with it — and FNAME is one of
|
|
2815
|
+
// the two tags every audience is created with. The Drawbridge
|
|
2816
|
+
// totals Klaviyo receives cannot travel until something registers
|
|
2817
|
+
// merge fields on the chosen audience, which is lifecycle.register's
|
|
2818
|
+
// job and is not built.
|
|
2819
|
+
...(lead == null ? void 0 : lead.name) && { merge_fields: { FNAME: String(lead.name).trim().split(/\s+/)[0] } },
|
|
2820
|
+
...suppressed && { status: "unsubscribed" },
|
|
2821
|
+
status_if_new: suppressed ? "unsubscribed" : "subscribed"
|
|
2822
|
+
},
|
|
2823
|
+
token
|
|
2824
|
+
});
|
|
2825
|
+
return {
|
|
2826
|
+
// Merged into `context` for later steps in this run.
|
|
2827
|
+
context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
|
|
2828
|
+
message: suppressed ? "Synced to Mailchimp as unsubscribed \u2014 this contact has opted out." : "Synced to the Mailchimp audience.",
|
|
2829
|
+
// Recorded on the run for support to read back, not a write
|
|
2830
|
+
// instruction — the hook has already written what it needed to.
|
|
2831
|
+
response: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash }
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
},
|
|
2542
2835
|
// Drawbridge sends its own notification email and SMS, and owns its own
|
|
2543
2836
|
// segments — see the private `drawbridge` manifest. A vendor answering
|
|
2544
2837
|
// these would be a second sender, which is the arrangement the platform
|
|
@@ -2557,24 +2850,13 @@ var mailchimp_default2 = {
|
|
|
2557
2850
|
// successful — the same silent truncation Klaviyo has, at a different
|
|
2558
2851
|
// number. Paged against total_items so an account past a thousand still
|
|
2559
2852
|
// resolves.
|
|
2560
|
-
audiences: async ({ cursor, limit = 100, search, settings, token }, { fetcher
|
|
2561
|
-
const dc = settings == null ? void 0 : settings.dc;
|
|
2853
|
+
audiences: async ({ cursor, limit = 100, search, settings, token }, { fetcher } = {}) => {
|
|
2562
2854
|
const count = Math.min(limit, 1e3);
|
|
2563
2855
|
const offset = Number(cursor || 0);
|
|
2564
|
-
const
|
|
2565
|
-
|
|
2566
|
-
{
|
|
2567
|
-
headers: { authorization: "Bearer " + token },
|
|
2568
|
-
signal: AbortSignal.timeout(15e3)
|
|
2569
|
-
}
|
|
2856
|
+
const body = await api3(
|
|
2857
|
+
"/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
|
|
2858
|
+
{ dc: settings == null ? void 0 : settings.dc, fetcher, token }
|
|
2570
2859
|
);
|
|
2571
|
-
if (!response.ok) {
|
|
2572
|
-
throw Object.assign(
|
|
2573
|
-
new Error("Mailchimp refused the request (" + response.status + ")"),
|
|
2574
|
-
{ status: response.status }
|
|
2575
|
-
);
|
|
2576
|
-
}
|
|
2577
|
-
const body = await response.json();
|
|
2578
2860
|
const audiences = ((body == null ? void 0 : body.lists) || []).map((list) => ({ id: list.id, title: (list == null ? void 0 : list.name) || list.id }));
|
|
2579
2861
|
const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
|
|
2580
2862
|
const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
|
|
@@ -2596,6 +2878,15 @@ var mailchimp_default2 = {
|
|
|
2596
2878
|
webhook: false
|
|
2597
2879
|
},
|
|
2598
2880
|
icon: mailchimp_default,
|
|
2881
|
+
// DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
|
|
2882
|
+
// what an admin types on the provider screen, beside the `requires` naming the
|
|
2883
|
+
// same variables.
|
|
2884
|
+
provider: {
|
|
2885
|
+
fields: [
|
|
2886
|
+
{ input: "text", key: "clientId", credential: "MAILCHIMP_OAUTH_CLIENT_ID", label: "Client ID", required: true },
|
|
2887
|
+
{ input: "password", key: "clientSecret", credential: "MAILCHIMP_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
|
|
2888
|
+
]
|
|
2889
|
+
},
|
|
2599
2890
|
// The OAuth client this deployment registered. Without both, the vendor drops
|
|
2600
2891
|
// out of availableConnections rather than offering a Connect button that
|
|
2601
2892
|
// cannot complete.
|
|
@@ -2604,32 +2895,58 @@ var mailchimp_default2 = {
|
|
|
2604
2895
|
"MAILCHIMP_OAUTH_CLIENT_SECRET"
|
|
2605
2896
|
],
|
|
2606
2897
|
slug: "mailchimp",
|
|
2607
|
-
// A
|
|
2608
|
-
//
|
|
2609
|
-
//
|
|
2610
|
-
//
|
|
2611
|
-
//
|
|
2898
|
+
// A grant with no audience chosen is authenticated and useless — the sync has
|
|
2899
|
+
// nowhere to put anyone — so the card must say Pending rather than Active over
|
|
2900
|
+
// nothing. Mailchimp also needs its merge fields created on that audience
|
|
2901
|
+
// before any Drawbridge total can be written to a member — unlike Klaviyo, its
|
|
2902
|
+
// custom fields are not schemaless — so the audience must be picked before
|
|
2903
|
+
// lifecycle.register has anything to register against.
|
|
2612
2904
|
status: (data2) => {
|
|
2613
2905
|
var _a;
|
|
2614
2906
|
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? data2.status : "pending";
|
|
2615
2907
|
},
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2908
|
+
steps: {
|
|
2909
|
+
contacts: {
|
|
2910
|
+
// A DECLARATION, not the work. It names the hook that does the work, and
|
|
2911
|
+
// the nesting IS the name: this is `step.contacts.sync`, the string a
|
|
2912
|
+
// workflow document stores. Klaviyo and Attentive declare the same type —
|
|
2913
|
+
// a step belongs to the capability, not to whoever implements it — and the
|
|
2914
|
+
// connection on the step document is what says which vendor runs.
|
|
2915
|
+
sync: ({ data: data2 }) => ({
|
|
2916
|
+
hook: "contacts.sync",
|
|
2917
|
+
// NO ACCOUNT NAME TO INTERPOLATE, unlike Klaviyo. Mailchimp's
|
|
2918
|
+
// auth.connect deliberately stores only the data centre (a test pins
|
|
2919
|
+
// that), and settings.audience is an opaque list id no merchant would
|
|
2920
|
+
// recognise in a builder label — so the label names the vendor rather
|
|
2921
|
+
// than showing a string like a1b2c3d4e5.
|
|
2922
|
+
key: "Sync contact to Mailchimp",
|
|
2923
|
+
queue: "connection",
|
|
2924
|
+
// Nothing for a merchant to configure on the step itself — the audience
|
|
2925
|
+
// is chosen once on the connection. Declared empty rather than omitted,
|
|
2926
|
+
// so "this step takes no settings" and "nobody thought about settings"
|
|
2927
|
+
// stay different statements.
|
|
2928
|
+
settings: {},
|
|
2929
|
+
// BOTH triggers, for the same reason as Klaviyo: lead.insert alone only
|
|
2930
|
+
// ever fires for someone with no history yet, and crossing into a
|
|
2931
|
+
// segment is the other moment a contact is worth pushing.
|
|
2932
|
+
triggers: ["lead.insert", "segment.contact.add"],
|
|
2933
|
+
// One source for cost: what the builder discloses before a merchant
|
|
2934
|
+
// adds this step, and what is charged when it runs.
|
|
2935
|
+
usage: { actions: 1 }
|
|
2936
|
+
})
|
|
2937
|
+
}
|
|
2938
|
+
},
|
|
2939
|
+
// WHY, in the merchant's words, and what to do about it.
|
|
2940
|
+
// ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
|
|
2941
|
+
// is reconnecting — prompting "choose a audience" there asks the merchant to
|
|
2942
|
+
// configure a grant that no longer exists. `pending` is exactly this task's
|
|
2943
|
+
// moment: the grant is good and the audience is the missing half.
|
|
2620
2944
|
tasks: (data2) => {
|
|
2621
2945
|
var _a;
|
|
2622
|
-
return [
|
|
2623
|
-
...((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? [] : [
|
|
2624
|
-
{
|
|
2625
|
-
message: "Choose which Mailchimp audience your contacts should sync into. Until you do, nothing is being synced.",
|
|
2626
|
-
title: "Choose an audience"
|
|
2627
|
-
}
|
|
2628
|
-
],
|
|
2946
|
+
return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? [] : [
|
|
2629
2947
|
{
|
|
2630
|
-
message: "
|
|
2631
|
-
title: "
|
|
2632
|
-
type: "warning"
|
|
2948
|
+
message: "Choose which Mailchimp audience your contacts should sync into. Until you do, nothing is being synced.",
|
|
2949
|
+
title: "Choose an audience"
|
|
2633
2950
|
}
|
|
2634
2951
|
];
|
|
2635
2952
|
},
|
|
@@ -2642,10 +2959,9 @@ import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
|
2642
2959
|
|
|
2643
2960
|
// lib/connections/icons/shopify.js
|
|
2644
2961
|
var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2645
|
-
<rect width="500" height="500" fill="
|
|
2646
|
-
<path
|
|
2647
|
-
<path d="
|
|
2648
|
-
<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"/>
|
|
2962
|
+
<rect width="500" height="500" fill="#95C049"/>
|
|
2963
|
+
<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"/>
|
|
2964
|
+
<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"/>
|
|
2649
2965
|
</svg>`;
|
|
2650
2966
|
|
|
2651
2967
|
// lib/connections/inbound.js
|
|
@@ -2684,18 +3000,6 @@ var toCanonicalEmail = (value) => {
|
|
|
2684
3000
|
return local + "@" + domain;
|
|
2685
3001
|
};
|
|
2686
3002
|
|
|
2687
|
-
// lib/phone.js
|
|
2688
|
-
import { AsYouType, parsePhoneNumberFromString, isValidPhoneNumber } from "libphonenumber-js";
|
|
2689
|
-
var toE164 = (value, country) => {
|
|
2690
|
-
if (!value) return null;
|
|
2691
|
-
try {
|
|
2692
|
-
const parsed = parsePhoneNumberFromString(String(value), country);
|
|
2693
|
-
return parsed ? parsed.number : null;
|
|
2694
|
-
} catch {
|
|
2695
|
-
return null;
|
|
2696
|
-
}
|
|
2697
|
-
};
|
|
2698
|
-
|
|
2699
3003
|
// lib/connections/providers/shopify.js
|
|
2700
3004
|
var toLine = ({
|
|
2701
3005
|
price,
|
|
@@ -2795,7 +3099,7 @@ var shopify_default2 = {
|
|
|
2795
3099
|
// the App Store listing, and the dashboard must never imply a store can be
|
|
2796
3100
|
// linked from inside it.
|
|
2797
3101
|
redirect: {
|
|
2798
|
-
|
|
3102
|
+
credential: "SHOPIFY_APP_LISTING_URL",
|
|
2799
3103
|
title: "View on the Shopify App Store"
|
|
2800
3104
|
}
|
|
2801
3105
|
},
|
|
@@ -3548,6 +3852,19 @@ var shopify_default2 = {
|
|
|
3548
3852
|
const shop = (data2 == null ? void 0 : data2.shop) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.domain);
|
|
3549
3853
|
return shop ? "https://admin.shopify.com/store/" + String(shop).replace(".myshopify.com", "") + "/apps/" + (env == null ? void 0 : env.SHOPIFY_APP_HANDLE) : void 0;
|
|
3550
3854
|
},
|
|
3855
|
+
// DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
|
|
3856
|
+
// what an admin types on the provider screen. The four names below are exactly
|
|
3857
|
+
// what `requires` gates on, which is the point of declaring them together: a
|
|
3858
|
+
// name required by the manifest and enterable nowhere is a vendor that can
|
|
3859
|
+
// never go live from the admin screen.
|
|
3860
|
+
provider: {
|
|
3861
|
+
fields: [
|
|
3862
|
+
{ input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
|
|
3863
|
+
{ input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
|
|
3864
|
+
{ input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
|
|
3865
|
+
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true }
|
|
3866
|
+
]
|
|
3867
|
+
},
|
|
3551
3868
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
3552
3869
|
// exists and the app is fully configured. Requiring all four means it can
|
|
3553
3870
|
// never render half-configured — and absence of any one excludes the
|
|
@@ -3964,7 +4281,10 @@ var webhook_default = {
|
|
|
3964
4281
|
// pressing Connect will do; afterwards it states the verification the
|
|
3965
4282
|
// merchant's own endpoint has to perform, because a signed payload nobody
|
|
3966
4283
|
// checks is an unsigned payload.
|
|
3967
|
-
|
|
4284
|
+
// The connect prompt only where connecting is not already the card's whole
|
|
4285
|
+
// story: a disconnected or errored document renders a Connect action itself,
|
|
4286
|
+
// and a task repeating it talks over the button.
|
|
4287
|
+
tasks: ({ settings, status } = {}) => ["disconnected", "error"].includes(status) ? [] : (settings == null ? void 0 : settings.secret) ? [
|
|
3968
4288
|
{
|
|
3969
4289
|
message: "Compute HMAC-SHA256( secret, body ) and compare against the X-Drawbridge-Signature header to confirm each payload.",
|
|
3970
4290
|
title: "Requests must be verified",
|
|
@@ -3989,7 +4309,7 @@ var leaves = (node, path = []) => Object.entries(node || {}).flatMap(
|
|
|
3989
4309
|
([key, value]) => typeof value === "function" ? [[[...path, key].join("."), value]] : value && typeof value === "object" ? leaves(value, [...path, key]) : []
|
|
3990
4310
|
);
|
|
3991
4311
|
var build = (manifest) => {
|
|
3992
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
|
|
4312
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
|
|
3993
4313
|
if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
|
|
3994
4314
|
if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
|
|
3995
4315
|
if (!(manifest == null ? void 0 : manifest.private) && !(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
|
|
@@ -4019,6 +4339,17 @@ var build = (manifest) => {
|
|
|
4019
4339
|
}
|
|
4020
4340
|
}
|
|
4021
4341
|
}
|
|
4342
|
+
for (const field2 of ((_c = manifest.provider) == null ? void 0 : _c.fields) || []) {
|
|
4343
|
+
if (!(field2 == null ? void 0 : field2.key) || !(field2 == null ? void 0 : field2.label)) {
|
|
4344
|
+
throw new Error(manifest.slug + " declares a provider field with no key or label");
|
|
4345
|
+
}
|
|
4346
|
+
if (!INPUTS.includes(field2.input)) {
|
|
4347
|
+
throw new Error(manifest.slug + ".provider." + field2.key + " needs an input the admin form can render \u2014 one of " + INPUTS.join(", "));
|
|
4348
|
+
}
|
|
4349
|
+
if (field2.input === "password" && !field2.redact) {
|
|
4350
|
+
throw new Error(manifest.slug + ".provider." + field2.key + " is a password and must declare redact : true \u2014 the api would hand the value back");
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4022
4353
|
if (typeof (manifest == null ? void 0 : manifest.icon) !== "string" || !manifest.icon.includes("<svg")) {
|
|
4023
4354
|
throw new Error(manifest.slug + " needs an icon \u2014 the svg markup itself, not a path to one");
|
|
4024
4355
|
}
|
|
@@ -4031,23 +4362,23 @@ var build = (manifest) => {
|
|
|
4031
4362
|
if (!GROUPS.includes(manifest == null ? void 0 : manifest.group)) {
|
|
4032
4363
|
throw new Error(manifest.slug + " needs a group \u2014 one of " + GROUPS.join(", "));
|
|
4033
4364
|
}
|
|
4034
|
-
if ((
|
|
4365
|
+
if ((_d = manifest == null ? void 0 : manifest.connect) == null ? void 0 : _d.type) {
|
|
4035
4366
|
throw new Error(manifest.slug + " declares connect.type \u2014 that is auth.type now");
|
|
4036
4367
|
}
|
|
4037
|
-
if (!AUTH_TYPES.includes((
|
|
4368
|
+
if (!AUTH_TYPES.includes((_e = manifest == null ? void 0 : manifest.auth) == null ? void 0 : _e.type)) {
|
|
4038
4369
|
throw new Error(manifest.slug + " needs auth.type \u2014 one of " + AUTH_TYPES.join(", "));
|
|
4039
4370
|
}
|
|
4040
4371
|
if (manifest.auth.type === "oauth") {
|
|
4041
4372
|
for (const field2 of OAUTH_FIELDS) {
|
|
4042
|
-
if (!((
|
|
4373
|
+
if (!((_f = manifest.auth.oauth) == null ? void 0 : _f[field2])) {
|
|
4043
4374
|
throw new Error(manifest.slug + " is oauth and must declare auth.oauth." + field2);
|
|
4044
4375
|
}
|
|
4045
4376
|
}
|
|
4046
|
-
if (typeof ((
|
|
4377
|
+
if (typeof ((_h = (_g = manifest.hooks) == null ? void 0 : _g.auth) == null ? void 0 : _h.token) !== "function") {
|
|
4047
4378
|
throw new Error(manifest.slug + " is oauth and must implement hooks.auth.token \u2014 point it at authToken() or wrap it");
|
|
4048
4379
|
}
|
|
4049
4380
|
for (const url of OAUTH_URLS) {
|
|
4050
|
-
if (!((
|
|
4381
|
+
if (!((_j = (_i = manifest.auth.oauth) == null ? void 0 : _i.urls) == null ? void 0 : _j[url])) {
|
|
4051
4382
|
throw new Error(manifest.slug + " is oauth and must declare auth.oauth.urls." + url);
|
|
4052
4383
|
}
|
|
4053
4384
|
}
|
|
@@ -4057,16 +4388,16 @@ var build = (manifest) => {
|
|
|
4057
4388
|
);
|
|
4058
4389
|
}
|
|
4059
4390
|
}
|
|
4060
|
-
if (implemented(manifest.hooks, "inbound.event") && !((
|
|
4391
|
+
if (implemented(manifest.hooks, "inbound.event") && !((_l = (_k = manifest.inbound) == null ? void 0 : _k.headers) == null ? void 0 : _l.event)) {
|
|
4061
4392
|
throw new Error(manifest.slug + " implements inbound.event but declares no inbound.headers.event");
|
|
4062
4393
|
}
|
|
4063
|
-
if (implemented(manifest.hooks, "inbound.verify") && !((
|
|
4394
|
+
if (implemented(manifest.hooks, "inbound.verify") && !((_n = (_m = manifest.inbound) == null ? void 0 : _m.headers) == null ? void 0 : _n.signature)) {
|
|
4064
4395
|
throw new Error(manifest.slug + " implements inbound.verify but declares no inbound.headers.signature");
|
|
4065
4396
|
}
|
|
4066
4397
|
if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
|
|
4067
4398
|
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");
|
|
4068
4399
|
}
|
|
4069
|
-
if (!Array.isArray((
|
|
4400
|
+
if (!Array.isArray((_o = manifest == null ? void 0 : manifest.content) == null ? void 0 : _o.guide) || !manifest.content.guide.length) {
|
|
4070
4401
|
throw new Error(manifest.slug + " needs content.guide \u2014 an array of steps for its page");
|
|
4071
4402
|
}
|
|
4072
4403
|
if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
|
|
@@ -4078,8 +4409,8 @@ var build = (manifest) => {
|
|
|
4078
4409
|
}
|
|
4079
4410
|
for (const [domain, verbs] of Object.entries(HOOKS)) {
|
|
4080
4411
|
for (const verb of verbs) {
|
|
4081
|
-
const hook = (
|
|
4082
|
-
if (((
|
|
4412
|
+
const hook = (_q = (_p = manifest.hooks) == null ? void 0 : _p[domain]) == null ? void 0 : _q[verb];
|
|
4413
|
+
if (((_r = manifest.hooks) == null ? void 0 : _r[domain]) === false) continue;
|
|
4083
4414
|
if (hook !== false && !implemented({ [domain]: { [verb]: hook } }, domain + "." + verb)) {
|
|
4084
4415
|
throw new Error(manifest.slug + " must answer hooks." + domain + "." + verb + " \u2014 false, a function, or {} if another repo implements it");
|
|
4085
4416
|
}
|
|
@@ -4125,14 +4456,18 @@ var connections = Object.freeze({
|
|
|
4125
4456
|
webhook: build(webhook_default)
|
|
4126
4457
|
});
|
|
4127
4458
|
(() => {
|
|
4128
|
-
|
|
4459
|
+
var _a;
|
|
4460
|
+
const routes = {};
|
|
4129
4461
|
for (const [slug, manifest] of Object.entries(connections)) {
|
|
4130
|
-
for (const [name] of leaves(manifest.steps)) {
|
|
4462
|
+
for (const [name, step] of leaves(manifest.steps)) {
|
|
4131
4463
|
const type = "step." + name;
|
|
4132
|
-
|
|
4133
|
-
|
|
4464
|
+
const queue = (_a = step({})) == null ? void 0 : _a.queue;
|
|
4465
|
+
if (routes[type] && routes[type].queue !== queue) {
|
|
4466
|
+
throw new Error(
|
|
4467
|
+
"Step " + type + " routes to " + routes[type].queue + " for " + routes[type].slug + " and " + queue + " for " + slug + " \u2014 one of them would be enqueued nowhere"
|
|
4468
|
+
);
|
|
4134
4469
|
}
|
|
4135
|
-
|
|
4470
|
+
routes[type] = { queue, slug };
|
|
4136
4471
|
}
|
|
4137
4472
|
}
|
|
4138
4473
|
})();
|
|
@@ -4226,43 +4561,11 @@ var decrypt = (value) => {
|
|
|
4226
4561
|
};
|
|
4227
4562
|
|
|
4228
4563
|
// lib/providers.js
|
|
4229
|
-
var
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
{ input: "password", key: "clientSecret", env: "ATTENTIVE_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
|
|
4233
|
-
],
|
|
4234
|
-
drawbridge: [
|
|
4235
|
-
{ input: "email", key: "accountSender", env: "SENDGRID_FROM_ADDRESS", label: "Account sender", message: "Verification codes and security alerts send from here.", required: true },
|
|
4236
|
-
{ input: "password", key: "apiKey", env: "SENDGRID_API_KEY", label: "SendGrid API key", redact: true, required: true },
|
|
4237
|
-
// NOT required. The CRM sync is best-effort internal tooling and no-ops
|
|
4238
|
-
// without a token — requiring it would make the whole drawbridge provider
|
|
4239
|
-
// read not-live over something no merchant ever sees.
|
|
4240
|
-
{ 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 },
|
|
4241
|
-
// Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
|
|
4242
|
-
// either. Unset, it degrades to the account sender rather than
|
|
4243
|
-
// refusing to start.
|
|
4244
|
-
{ 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." },
|
|
4245
|
-
{ input: "text", key: "smsFrom", env: "TWILIO_ACCOUNT_FROM", label: "SMS number", required: true },
|
|
4246
|
-
{ input: "password", key: "smsSid", env: "TWILIO_ACCOUNT_SID", label: "Twilio account SID", redact: true, required: true },
|
|
4247
|
-
{ input: "password", key: "smsToken", env: "TWILIO_AUTH_TOKEN", label: "Twilio auth token", redact: true, required: true }
|
|
4248
|
-
],
|
|
4249
|
-
klaviyo: [
|
|
4250
|
-
{ input: "text", key: "clientId", env: "KLAVIYO_OAUTH_CLIENT_ID", label: "Client ID", required: true },
|
|
4251
|
-
{ input: "password", key: "clientSecret", env: "KLAVIYO_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
|
|
4252
|
-
],
|
|
4253
|
-
mailchimp: [
|
|
4254
|
-
{ input: "text", key: "clientId", env: "MAILCHIMP_OAUTH_CLIENT_ID", label: "Client ID", required: true },
|
|
4255
|
-
{ input: "password", key: "clientSecret", env: "MAILCHIMP_OAUTH_CLIENT_SECRET", label: "Client secret", redact: true, required: true }
|
|
4256
|
-
],
|
|
4257
|
-
shopify: [
|
|
4258
|
-
{ input: "text", key: "apiKey", env: "SHOPIFY_API_KEY", label: "API key", required: true },
|
|
4259
|
-
{ input: "password", key: "apiSecret", env: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
|
|
4260
|
-
{ input: "text", key: "appHandle", env: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
|
|
4261
|
-
{ input: "text", key: "listingUrl", env: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true }
|
|
4262
|
-
]
|
|
4564
|
+
var providerFields = (slug) => {
|
|
4565
|
+
var _a;
|
|
4566
|
+
return Object.hasOwn(connections, slug) ? ((_a = connections[slug].provider) == null ? void 0 : _a.fields) || [] : [];
|
|
4263
4567
|
};
|
|
4264
|
-
var
|
|
4265
|
-
var providerSlugs = () => Object.keys(FIELDS).sort();
|
|
4568
|
+
var providerSlugs = () => Object.keys(connections).filter((slug) => connections[slug].provider).sort();
|
|
4266
4569
|
var isLive = (slug, settings) => {
|
|
4267
4570
|
const fields2 = providerFields(slug);
|
|
4268
4571
|
if (!fields2.length) return false;
|
|
@@ -4319,7 +4622,7 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
|
|
|
4319
4622
|
return result;
|
|
4320
4623
|
};
|
|
4321
4624
|
var providerEnvNames = () => new Set(
|
|
4322
|
-
providerSlugs().flatMap((slug) => providerFields(slug)).map((field2) => field2.
|
|
4625
|
+
providerSlugs().flatMap((slug) => providerFields(slug)).map((field2) => field2.credential).filter(Boolean)
|
|
4323
4626
|
);
|
|
4324
4627
|
var providerCredentials = async ({ controller }) => {
|
|
4325
4628
|
const credentials2 = {};
|
|
@@ -4328,7 +4631,7 @@ var providerCredentials = async ({ controller }) => {
|
|
|
4328
4631
|
const settings = await providerSettings({ controller, slug });
|
|
4329
4632
|
for (const field2 of providerFields(slug)) {
|
|
4330
4633
|
const value = settings == null ? void 0 : settings[field2.key];
|
|
4331
|
-
if (field2.
|
|
4634
|
+
if (field2.credential && value) credentials2[field2.credential] = value;
|
|
4332
4635
|
}
|
|
4333
4636
|
} catch {
|
|
4334
4637
|
continue;
|