@drawbridge/drawbridge-utils 0.0.110 → 0.0.112
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 +141 -9
- package/dist/connections/index.d.cts +207 -2
- package/dist/connections/index.d.ts +207 -2
- package/dist/connections/index.js +141 -9
- package/package.json +1 -1
|
@@ -126,7 +126,32 @@ var HOOKS = Object.freeze({
|
|
|
126
126
|
// Vendor data a campaign draws on. Named for what every store platform has,
|
|
127
127
|
// not for what Shopify calls it: Shopify says discounts, Stripe says coupons
|
|
128
128
|
// and promotion codes, BigCommerce says coupons and promotions.
|
|
129
|
+
//
|
|
130
|
+
// ONE SHAPE FOR ALL OF THEM — searchable and cursor-paged:
|
|
131
|
+
//
|
|
132
|
+
// ({ connection, cursor, limit, search, settings, token })
|
|
133
|
+
// -> { items : [ { id, title, ... } ], pageInfo : { endCursor, hasNextPage } }
|
|
134
|
+
//
|
|
135
|
+
// Lifted from what the Shopify product picker already does, rather than
|
|
136
|
+
// invented: that endpoint takes cursor/limit/search and returns items plus a
|
|
137
|
+
// pageInfo, and ListResource consumes exactly that. It was a good contract
|
|
138
|
+
// written once for one vendor; this makes it the contract.
|
|
139
|
+
//
|
|
140
|
+
// `title` is not arbitrary — ListResource searches `[ 'title' ]` by default,
|
|
141
|
+
// so normalising to { id, title } is what lets a picker work with no
|
|
142
|
+
// per-vendor configuration.
|
|
143
|
+
//
|
|
144
|
+
// HOW a vendor searches is its own business, which is the point of a hook.
|
|
145
|
+
// Shopify pushes the term into its GraphQL query, Klaviyo has a filter
|
|
146
|
+
// parameter, and Mailchimp's /lists has no name filter at all so its hook
|
|
147
|
+
// matches against what it fetched. The caller never learns which.
|
|
129
148
|
catalog: Object.freeze([
|
|
149
|
+
// The named groups a contact can be synced INTO. Klaviyo calls them lists,
|
|
150
|
+
// Mailchimp calls them audiences; `audiences` is the industry-generic term
|
|
151
|
+
// and belongs to neither vendor's API. Read at form time, so a merchant
|
|
152
|
+
// picks from what actually exists in their account rather than pasting an
|
|
153
|
+
// id from another tab.
|
|
154
|
+
"audiences",
|
|
130
155
|
"products",
|
|
131
156
|
// Shopify folds price into the product variant; Stripe makes Price a
|
|
132
157
|
// first-class object beside Product. Declared so a vendor that separates
|
|
@@ -402,6 +427,17 @@ var klaviyo_default2 = {
|
|
|
402
427
|
{
|
|
403
428
|
key: "account",
|
|
404
429
|
label: "Klaviyo account"
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
input: "select",
|
|
433
|
+
key: "list",
|
|
434
|
+
label: "Klaviyo list",
|
|
435
|
+
message: "Contacts your campaigns collect are synced into this list.",
|
|
436
|
+
required: true,
|
|
437
|
+
// The choices come from the merchant's own account, not from here — see
|
|
438
|
+
// catalog.audiences below. Static options would mean asking somebody to
|
|
439
|
+
// paste a list id copied out of another browser tab.
|
|
440
|
+
source: "catalog.audiences"
|
|
405
441
|
}
|
|
406
442
|
],
|
|
407
443
|
// The three auth hooks, all pure HTTP against Klaviyo — which is why they
|
|
@@ -427,6 +463,37 @@ var klaviyo_default2 = {
|
|
|
427
463
|
//
|
|
428
464
|
// Basic auth with our client, exactly like the token exchange — the token
|
|
429
465
|
// being revoked is the subject, not the credential.
|
|
466
|
+
// The lists a merchant can sync into, for the picker on their connection.
|
|
467
|
+
//
|
|
468
|
+
// PAGINATED DELIBERATELY. Klaviyo caps page[size] at 10 and defaults to it,
|
|
469
|
+
// so a single call quietly returns the first ten lists and an account with
|
|
470
|
+
// more would show a picker missing the one they wanted — with nothing to
|
|
471
|
+
// indicate anything was cut. Follows links.next, bounded so a runaway
|
|
472
|
+
// cursor cannot spin forever.
|
|
473
|
+
"catalog.audiences": async ({ cursor, fetcher, limit = 100, search, token }) => {
|
|
474
|
+
var _a, _b;
|
|
475
|
+
const audiences = [];
|
|
476
|
+
let next = cursor ? "/lists?page%5Bsize%5D=10&page%5Bcursor%5D=" + encodeURIComponent(cursor) : "/lists?page%5Bsize%5D=10";
|
|
477
|
+
let pages = 0;
|
|
478
|
+
while (next && audiences.length < limit && pages < 20) {
|
|
479
|
+
const body = await api(next, { fetcher, token });
|
|
480
|
+
for (const list of (body == null ? void 0 : body.data) || []) {
|
|
481
|
+
audiences.push({ id: list.id, title: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id });
|
|
482
|
+
}
|
|
483
|
+
const link = (_b = body == null ? void 0 : body.links) == null ? void 0 : _b.next;
|
|
484
|
+
next = link ? String(link).replace(/^https:\/\/a\.klaviyo\.com\/api/, "") : null;
|
|
485
|
+
pages = pages + 1;
|
|
486
|
+
}
|
|
487
|
+
const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
|
|
488
|
+
const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
|
|
489
|
+
return {
|
|
490
|
+
items,
|
|
491
|
+
pageInfo: {
|
|
492
|
+
endCursor: next,
|
|
493
|
+
hasNextPage: Boolean(next)
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
},
|
|
430
497
|
"auth.disconnect": async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
|
|
431
498
|
const token = (settings == null ? void 0 : settings.refreshToken) || (settings == null ? void 0 : settings.accessToken);
|
|
432
499
|
if (!token) return { revoked: false };
|
|
@@ -500,6 +567,7 @@ var klaviyo_default2 = {
|
|
|
500
567
|
"auth.probe": true,
|
|
501
568
|
// Klaviyo scopes are fixed at app level and re-consented, not drifted.
|
|
502
569
|
"auth.scopes": false,
|
|
570
|
+
"catalog.audiences": true,
|
|
503
571
|
"catalog.prices": false,
|
|
504
572
|
"catalog.products": false,
|
|
505
573
|
"catalog.promotions": false,
|
|
@@ -532,6 +600,11 @@ var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fil
|
|
|
532
600
|
</svg>`;
|
|
533
601
|
|
|
534
602
|
// lib/connections/mailchimp.js
|
|
603
|
+
var base = (apiKey) => {
|
|
604
|
+
const dc = String(apiKey || "").split("-").pop();
|
|
605
|
+
if (!dc || dc === apiKey) throw new Error("That Mailchimp key carries no data centre suffix, so there is no host to call");
|
|
606
|
+
return "https://" + dc + ".api.mailchimp.com/3.0";
|
|
607
|
+
};
|
|
535
608
|
var mailchimp_default2 = {
|
|
536
609
|
// Keys TODAY. Mailchimp integrations authenticate with OAuth 2 (authorization
|
|
537
610
|
// code) and that is where this goes, so the endpoints are recorded here
|
|
@@ -569,8 +642,56 @@ var mailchimp_default2 = {
|
|
|
569
642
|
placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022-us1",
|
|
570
643
|
redact: true,
|
|
571
644
|
required: true
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
input: "select",
|
|
648
|
+
key: "audience",
|
|
649
|
+
label: "Mailchimp audience",
|
|
650
|
+
message: "Contacts your campaigns collect are synced into this audience.",
|
|
651
|
+
required: true,
|
|
652
|
+
source: "catalog.audiences"
|
|
572
653
|
}
|
|
573
654
|
],
|
|
655
|
+
// The audiences a merchant can sync into, for the picker on their connection.
|
|
656
|
+
//
|
|
657
|
+
// count DEFAULTS TO 10 and maxes at 1000 (their own OpenAPI spec), so leaving
|
|
658
|
+
// it unset returns the first ten audiences and looks entirely successful —
|
|
659
|
+
// the same silent truncation Klaviyo has, at a different number. Paged
|
|
660
|
+
// against total_items so an account past a thousand still resolves.
|
|
661
|
+
hooks: {
|
|
662
|
+
"catalog.audiences": async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
|
|
663
|
+
const key = settings == null ? void 0 : settings.apiKey;
|
|
664
|
+
const count = Math.min(limit, 1e3);
|
|
665
|
+
const offset = Number(cursor || 0);
|
|
666
|
+
const response = await fetcher(
|
|
667
|
+
base(key) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
|
|
668
|
+
{
|
|
669
|
+
// Basic with any username — Mailchimp reads only the password half.
|
|
670
|
+
headers: { authorization: "Basic " + Buffer.from("drawbridge:" + key).toString("base64") },
|
|
671
|
+
signal: AbortSignal.timeout(15e3)
|
|
672
|
+
}
|
|
673
|
+
);
|
|
674
|
+
if (!response.ok) {
|
|
675
|
+
throw Object.assign(
|
|
676
|
+
new Error("Mailchimp refused the request (" + response.status + ")"),
|
|
677
|
+
{ status: response.status }
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const body = await response.json();
|
|
681
|
+
const audiences = ((body == null ? void 0 : body.lists) || []).map((list) => ({ id: list.id, title: (list == null ? void 0 : list.name) || list.id }));
|
|
682
|
+
const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
|
|
683
|
+
const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
|
|
684
|
+
const nextOffset = offset + count;
|
|
685
|
+
const more = nextOffset < Number((body == null ? void 0 : body.total_items) || 0);
|
|
686
|
+
return {
|
|
687
|
+
items,
|
|
688
|
+
pageInfo: {
|
|
689
|
+
endCursor: more ? String(nextOffset) : null,
|
|
690
|
+
hasNextPage: more
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
},
|
|
574
695
|
icon: mailchimp_default,
|
|
575
696
|
// A key with no audience chosen is authenticated and inert. Mailchimp also
|
|
576
697
|
// needs its merge fields created on that audience before any Drawbridge total
|
|
@@ -591,6 +712,7 @@ var mailchimp_default2 = {
|
|
|
591
712
|
"auth.disconnect": true,
|
|
592
713
|
"auth.probe": false,
|
|
593
714
|
"auth.scopes": false,
|
|
715
|
+
"catalog.audiences": true,
|
|
594
716
|
"catalog.prices": false,
|
|
595
717
|
"catalog.products": false,
|
|
596
718
|
"catalog.promotions": false,
|
|
@@ -765,6 +887,7 @@ var shopify_default2 = {
|
|
|
765
887
|
// Shopify has no separate price resource — a price belongs to a product
|
|
766
888
|
// variant and arrives with it, so there is nothing for prices to answer
|
|
767
889
|
// that products does not already.
|
|
890
|
+
"catalog.audiences": false,
|
|
768
891
|
"catalog.prices": false,
|
|
769
892
|
"catalog.products": true,
|
|
770
893
|
"catalog.promotions": true,
|
|
@@ -930,6 +1053,7 @@ var webhook_default = {
|
|
|
930
1053
|
"auth.disconnect": true,
|
|
931
1054
|
"auth.probe": false,
|
|
932
1055
|
"auth.scopes": false,
|
|
1056
|
+
"catalog.audiences": false,
|
|
933
1057
|
"catalog.prices": false,
|
|
934
1058
|
"catalog.products": false,
|
|
935
1059
|
"catalog.promotions": false,
|
|
@@ -981,7 +1105,7 @@ var webhook_default = {
|
|
|
981
1105
|
// lib/connections/index.js
|
|
982
1106
|
var QUEUES = ["connection", "notification", "segment", "webhook"];
|
|
983
1107
|
var build = (manifest) => {
|
|
984
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
1108
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
985
1109
|
if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
|
|
986
1110
|
if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
|
|
987
1111
|
if (!(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
|
|
@@ -996,8 +1120,16 @@ var build = (manifest) => {
|
|
|
996
1120
|
if (field.input && !INPUTS.includes(field.input)) {
|
|
997
1121
|
throw new Error(manifest.slug + "." + field.key + " declares an unknown input: " + field.input + " \u2014 one of " + INPUTS.join(", "));
|
|
998
1122
|
}
|
|
999
|
-
if (field.input === "select" && !(field.options || []).length) {
|
|
1000
|
-
throw new Error(manifest.slug + "." + field.key + " is a select and must declare options");
|
|
1123
|
+
if (field.input === "select" && !(field.options || []).length && !field.source) {
|
|
1124
|
+
throw new Error(manifest.slug + "." + field.key + " is a select and must declare options or a source");
|
|
1125
|
+
}
|
|
1126
|
+
if (field.source) {
|
|
1127
|
+
if (!HOOK_NAMES.includes(field.source)) {
|
|
1128
|
+
throw new Error(manifest.slug + "." + field.key + " sources options from an unknown hook: " + field.source);
|
|
1129
|
+
}
|
|
1130
|
+
if (((_a = manifest.supports) == null ? void 0 : _a[field.source]) !== true) {
|
|
1131
|
+
throw new Error(manifest.slug + "." + field.key + " sources options from " + field.source + ", which it declares unsupported");
|
|
1132
|
+
}
|
|
1001
1133
|
}
|
|
1002
1134
|
}
|
|
1003
1135
|
if (typeof (manifest == null ? void 0 : manifest.icon) !== "string" || !manifest.icon.includes("<svg")) {
|
|
@@ -1012,15 +1144,15 @@ var build = (manifest) => {
|
|
|
1012
1144
|
if (!CATEGORIES.includes(manifest == null ? void 0 : manifest.category)) {
|
|
1013
1145
|
throw new Error(manifest.slug + " needs a category \u2014 one of " + CATEGORIES.join(", "));
|
|
1014
1146
|
}
|
|
1015
|
-
if ((
|
|
1147
|
+
if ((_b = manifest == null ? void 0 : manifest.connect) == null ? void 0 : _b.type) {
|
|
1016
1148
|
throw new Error(manifest.slug + " declares connect.type \u2014 that is auth.type now");
|
|
1017
1149
|
}
|
|
1018
|
-
if (!AUTH_TYPES.includes((
|
|
1150
|
+
if (!AUTH_TYPES.includes((_c = manifest == null ? void 0 : manifest.auth) == null ? void 0 : _c.type)) {
|
|
1019
1151
|
throw new Error(manifest.slug + " needs auth.type \u2014 one of " + AUTH_TYPES.join(", "));
|
|
1020
1152
|
}
|
|
1021
1153
|
if (manifest.auth.type === "oauth") {
|
|
1022
1154
|
for (const field of OAUTH_FIELDS) {
|
|
1023
|
-
if (!((
|
|
1155
|
+
if (!((_d = manifest.auth.oauth) == null ? void 0 : _d[field])) {
|
|
1024
1156
|
throw new Error(manifest.slug + " is oauth and must declare auth.oauth." + field);
|
|
1025
1157
|
}
|
|
1026
1158
|
}
|
|
@@ -1030,10 +1162,10 @@ var build = (manifest) => {
|
|
|
1030
1162
|
);
|
|
1031
1163
|
}
|
|
1032
1164
|
}
|
|
1033
|
-
if (((
|
|
1165
|
+
if (((_e = manifest.supports) == null ? void 0 : _e["inbound.event"]) && !((_g = (_f = manifest.inbound) == null ? void 0 : _f.headers) == null ? void 0 : _g.event)) {
|
|
1034
1166
|
throw new Error(manifest.slug + " supports inbound.event but declares no inbound.headers.event");
|
|
1035
1167
|
}
|
|
1036
|
-
if (((
|
|
1168
|
+
if (((_h = manifest.supports) == null ? void 0 : _h["inbound.verify"]) && !((_j = (_i = manifest.inbound) == null ? void 0 : _i.headers) == null ? void 0 : _j.signature)) {
|
|
1037
1169
|
throw new Error(manifest.slug + " supports inbound.verify but declares no inbound.headers.signature");
|
|
1038
1170
|
}
|
|
1039
1171
|
if (typeof (manifest == null ? void 0 : manifest.incomplete) !== "function") {
|
|
@@ -1049,7 +1181,7 @@ var build = (manifest) => {
|
|
|
1049
1181
|
if (typeof hook !== "function") {
|
|
1050
1182
|
throw new Error(manifest.slug + " declares hook " + name + " but it is not a function");
|
|
1051
1183
|
}
|
|
1052
|
-
if (((
|
|
1184
|
+
if (((_k = manifest.supports) == null ? void 0 : _k[name]) !== true) {
|
|
1053
1185
|
throw new Error(manifest.slug + " implements " + name + " but declares supports[ '" + name + "' ] false");
|
|
1054
1186
|
}
|
|
1055
1187
|
}
|
|
@@ -97,7 +97,32 @@ const HOOKS = Object.freeze({
|
|
|
97
97
|
// Vendor data a campaign draws on. Named for what every store platform has,
|
|
98
98
|
// not for what Shopify calls it: Shopify says discounts, Stripe says coupons
|
|
99
99
|
// and promotion codes, BigCommerce says coupons and promotions.
|
|
100
|
+
//
|
|
101
|
+
// ONE SHAPE FOR ALL OF THEM — searchable and cursor-paged:
|
|
102
|
+
//
|
|
103
|
+
// ({ connection, cursor, limit, search, settings, token })
|
|
104
|
+
// -> { items : [ { id, title, ... } ], pageInfo : { endCursor, hasNextPage } }
|
|
105
|
+
//
|
|
106
|
+
// Lifted from what the Shopify product picker already does, rather than
|
|
107
|
+
// invented: that endpoint takes cursor/limit/search and returns items plus a
|
|
108
|
+
// pageInfo, and ListResource consumes exactly that. It was a good contract
|
|
109
|
+
// written once for one vendor; this makes it the contract.
|
|
110
|
+
//
|
|
111
|
+
// `title` is not arbitrary — ListResource searches `[ 'title' ]` by default,
|
|
112
|
+
// so normalising to { id, title } is what lets a picker work with no
|
|
113
|
+
// per-vendor configuration.
|
|
114
|
+
//
|
|
115
|
+
// HOW a vendor searches is its own business, which is the point of a hook.
|
|
116
|
+
// Shopify pushes the term into its GraphQL query, Klaviyo has a filter
|
|
117
|
+
// parameter, and Mailchimp's /lists has no name filter at all so its hook
|
|
118
|
+
// matches against what it fetched. The caller never learns which.
|
|
100
119
|
catalog : Object.freeze([
|
|
120
|
+
// The named groups a contact can be synced INTO. Klaviyo calls them lists,
|
|
121
|
+
// Mailchimp calls them audiences; `audiences` is the industry-generic term
|
|
122
|
+
// and belongs to neither vendor's API. Read at form time, so a merchant
|
|
123
|
+
// picks from what actually exists in their account rather than pasting an
|
|
124
|
+
// id from another tab.
|
|
125
|
+
'audiences',
|
|
101
126
|
'products',
|
|
102
127
|
// Shopify folds price into the product variant; Stripe makes Price a
|
|
103
128
|
// first-class object beside Product. Declared so a vendor that separates
|
|
@@ -421,6 +446,17 @@ var klaviyo = {
|
|
|
421
446
|
{
|
|
422
447
|
key : 'account',
|
|
423
448
|
label : 'Klaviyo account'
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
input : 'select',
|
|
452
|
+
key : 'list',
|
|
453
|
+
label : 'Klaviyo list',
|
|
454
|
+
message : 'Contacts your campaigns collect are synced into this list.',
|
|
455
|
+
required : true,
|
|
456
|
+
// The choices come from the merchant's own account, not from here — see
|
|
457
|
+
// catalog.audiences below. Static options would mean asking somebody to
|
|
458
|
+
// paste a list id copied out of another browser tab.
|
|
459
|
+
source : 'catalog.audiences'
|
|
424
460
|
}
|
|
425
461
|
],
|
|
426
462
|
// The three auth hooks, all pure HTTP against Klaviyo — which is why they
|
|
@@ -449,6 +485,65 @@ var klaviyo = {
|
|
|
449
485
|
//
|
|
450
486
|
// Basic auth with our client, exactly like the token exchange — the token
|
|
451
487
|
// being revoked is the subject, not the credential.
|
|
488
|
+
// The lists a merchant can sync into, for the picker on their connection.
|
|
489
|
+
//
|
|
490
|
+
// PAGINATED DELIBERATELY. Klaviyo caps page[size] at 10 and defaults to it,
|
|
491
|
+
// so a single call quietly returns the first ten lists and an account with
|
|
492
|
+
// more would show a picker missing the one they wanted — with nothing to
|
|
493
|
+
// indicate anything was cut. Follows links.next, bounded so a runaway
|
|
494
|
+
// cursor cannot spin forever.
|
|
495
|
+
'catalog.audiences' : async ({ cursor, fetcher, limit = 100, search, token }) => {
|
|
496
|
+
|
|
497
|
+
// Klaviyo pages by cursor and caps page[size] at 10, so `limit` is what
|
|
498
|
+
// the CALLER wants back rather than what one request can carry — the
|
|
499
|
+
// loop keeps pulling until it has that many or the vendor runs out.
|
|
500
|
+
const audiences = [];
|
|
501
|
+
|
|
502
|
+
let next = cursor
|
|
503
|
+
? '/lists?page%5Bsize%5D=10&page%5Bcursor%5D=' + encodeURIComponent( cursor )
|
|
504
|
+
: '/lists?page%5Bsize%5D=10';
|
|
505
|
+
|
|
506
|
+
let pages = 0;
|
|
507
|
+
|
|
508
|
+
while( next && audiences.length < limit && pages < 20 ){
|
|
509
|
+
|
|
510
|
+
const body = await api( next, { fetcher, token });
|
|
511
|
+
|
|
512
|
+
for( const list of ( body?.data || [] ) ){
|
|
513
|
+
|
|
514
|
+
audiences.push({ id : list.id, title : list?.attributes?.name || list.id });
|
|
515
|
+
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const link = body?.links?.next;
|
|
519
|
+
|
|
520
|
+
// Klaviyo returns an absolute url; only the part after /api travels on,
|
|
521
|
+
// because the shared caller prefixes its own base.
|
|
522
|
+
next = link ? String( link ).replace( /^https:\/\/a\.klaviyo\.com\/api/, '' ) : null;
|
|
523
|
+
|
|
524
|
+
pages = pages + 1;
|
|
525
|
+
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Klaviyo's list endpoint has no name filter, so a search term is applied
|
|
529
|
+
// to what came back. Honest about its own limits: with more lists than
|
|
530
|
+
// `limit`, a term matching only later ones finds nothing — which is why
|
|
531
|
+
// a genuinely large catalog wants server-side filtering, not this.
|
|
532
|
+
const term = String( search?.value || '' ).trim().toLowerCase();
|
|
533
|
+
|
|
534
|
+
const items = term
|
|
535
|
+
? audiences.filter( ( entry ) => entry.title.toLowerCase().includes( term ) )
|
|
536
|
+
: audiences;
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
items,
|
|
540
|
+
pageInfo : {
|
|
541
|
+
endCursor : next,
|
|
542
|
+
hasNextPage : Boolean( next )
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
},
|
|
452
547
|
'auth.disconnect' : async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
|
|
453
548
|
|
|
454
549
|
const token = settings?.refreshToken || settings?.accessToken;
|
|
@@ -533,6 +628,7 @@ var klaviyo = {
|
|
|
533
628
|
'auth.probe' : true,
|
|
534
629
|
// Klaviyo scopes are fixed at app level and re-consented, not drifted.
|
|
535
630
|
'auth.scopes' : false,
|
|
631
|
+
'catalog.audiences' : true,
|
|
536
632
|
'catalog.prices' : false,
|
|
537
633
|
'catalog.products' : false,
|
|
538
634
|
'catalog.promotions' : false,
|
|
@@ -569,6 +665,20 @@ var icon$2 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xm
|
|
|
569
665
|
<path d="M360.476 284.243C360.35 283.835 359.618 281.204 358.647 278.052L356.621 272.648C360.575 266.696 360.645 261.405 360.124 258.393C359.531 254.519 357.693 250.944 354.89 248.205C351.766 244.94 345.363 241.563 336.385 239.044L331.671 237.736C331.643 237.524 331.418 226.619 331.235 221.933C331.08 218.555 330.798 213.264 329.152 208.058C327.182 200.993 323.791 194.858 319.527 190.876C331.277 178.717 338.594 165.307 338.58 153.81C338.538 131.703 311.379 124.976 277.902 138.851L270.824 141.863C270.795 141.835 258.004 129.283 257.821 129.128C219.63 95.8334 100.327 228.476 138.49 260.687L146.835 267.737C144.581 273.775 143.781 280.259 144.499 286.664C145.414 295.543 149.973 304.029 157.375 310.6C164.411 316.82 173.684 320.788 182.648 320.774C197.494 354.998 231.408 375.965 271.175 377.161C313.842 378.427 349.641 358.403 364.67 322.435C365.641 319.916 369.806 308.546 369.806 298.513C369.792 288.409 364.093 284.229 360.476 284.243ZM185.913 311.149C184.613 311.381 183.293 311.48 181.973 311.445C169.083 311.079 155.166 299.483 153.787 285.735C152.253 270.537 160.02 258.829 173.783 256.071C175.415 255.72 177.414 255.537 179.552 255.635C187.264 256.085 198.606 261.996 201.209 278.784C203.517 293.63 199.858 308.785 185.913 311.149ZM171.545 246.953C163.179 248.499 155.744 253.244 150.817 260.18C148.045 257.873 142.909 253.426 142.008 251.681C134.635 237.693 150.043 210.478 160.823 195.111C187.405 157.145 229.086 128.424 248.393 133.603C251.517 134.503 261.902 146.563 261.902 146.563C261.902 146.563 242.623 157.244 224.724 172.16C200.646 190.735 182.423 217.697 171.545 246.953ZM306.792 305.464C306.937 305.403 307.057 305.295 307.134 305.157C307.211 305.019 307.239 304.86 307.214 304.704C307.205 304.61 307.178 304.519 307.133 304.436C307.088 304.353 307.027 304.28 306.954 304.221C306.88 304.162 306.796 304.118 306.705 304.092C306.614 304.066 306.519 304.059 306.426 304.071C306.426 304.071 286.246 307.054 267.179 300.089C269.247 293.348 274.792 295.754 283.137 296.444C296.108 297.209 309.116 295.802 321.623 292.279C330.25 289.788 341.592 284.905 350.401 277.953C353.384 284.497 354.425 291.674 354.425 291.674C354.425 291.674 356.719 291.265 358.647 292.447C360.476 293.573 361.799 295.895 360.898 301.89C359.027 313.119 354.271 322.224 346.235 330.611C341.236 336.036 335.277 340.492 328.659 343.754C324.983 345.691 321.151 347.32 317.205 348.623C286.964 358.487 256.006 347.638 246.029 324.321C245.224 322.535 244.556 320.691 244.03 318.804C239.781 303.438 243.383 285.032 254.655 273.408C255.372 272.676 256.09 271.804 256.09 270.706C256.09 269.806 255.499 268.835 255.007 268.131C251.066 262.418 237.374 252.666 240.132 233.795C242.088 220.23 253.951 210.689 265.012 211.252L267.826 211.421C272.611 211.702 276.79 212.307 280.73 212.49C287.344 212.758 293.268 211.801 300.304 205.947C302.683 203.949 304.582 202.246 307.791 201.711C308.128 201.627 308.973 201.359 310.647 201.416C312.365 201.485 314.032 202.015 315.474 202.949C321.103 206.693 321.905 215.783 322.214 222.439C322.383 226.225 322.848 235.414 322.988 238.031C323.354 244.054 324.944 244.912 328.125 245.954C329.94 246.573 331.615 246.995 334.077 247.713C341.521 249.781 345.968 251.934 348.754 254.65C350.198 256.049 351.13 257.893 351.4 259.885C352.315 266.316 346.432 274.252 330.925 281.457C313.954 289.324 293.367 291.322 279.154 289.732L274.173 289.169C262.774 287.649 256.315 302.34 263.14 312.402C267.545 318.889 279.52 323.11 291.523 323.11C319.006 323.139 340.142 311.402 348.023 301.242L348.642 300.356C349.008 299.765 348.712 299.469 348.22 299.779C341.817 304.169 313.279 321.619 282.771 316.384C282.771 316.384 279.056 315.765 275.678 314.442C273.005 313.429 267.362 310.811 266.686 305.042C291.27 312.683 306.792 305.478 306.792 305.464ZM220.671 194.971C230.127 184.051 241.765 174.538 252.206 169.219C252.558 169.022 252.938 169.43 252.741 169.754C251.46 172 250.476 174.403 249.814 176.902C249.73 177.282 250.138 177.592 250.461 177.353C256.963 172.934 268.248 168.192 278.155 167.601C278.251 167.584 278.351 167.601 278.436 167.65C278.521 167.698 278.586 167.775 278.621 167.866C278.656 167.957 278.658 168.058 278.627 168.151C278.596 168.244 278.533 168.323 278.451 168.375C276.809 169.634 275.342 171.105 274.088 172.751C273.891 173.032 274.074 173.44 274.426 173.44C281.378 173.483 291.186 175.903 297.56 179.491C297.982 179.745 297.673 180.575 297.209 180.462C287.527 178.253 271.724 176.564 255.288 180.575C240.597 184.149 229.396 189.666 221.248 195.618C220.826 195.899 220.333 195.351 220.671 194.971Z" fill="#231E15"/>
|
|
570
666
|
</svg>`;
|
|
571
667
|
|
|
668
|
+
// EVERY ACCOUNT LIVES BEHIND A DATA-CENTRE PREFIX, and the key carries it as a
|
|
669
|
+
// suffix — `...-us19` means us19.api.mailchimp.com. There is no fixed host to
|
|
670
|
+
// call, which is Mailchimp's oldest quirk and the reason its future OAuth flow
|
|
671
|
+
// needs a metadata call the other vendors do not.
|
|
672
|
+
const base = ( apiKey ) => {
|
|
673
|
+
|
|
674
|
+
const dc = String( apiKey || '' ).split( '-' ).pop();
|
|
675
|
+
|
|
676
|
+
if( ! dc || dc === apiKey ) throw new Error( 'That Mailchimp key carries no data centre suffix, so there is no host to call' );
|
|
677
|
+
|
|
678
|
+
return 'https://' + dc + '.api.mailchimp.com/3.0';
|
|
679
|
+
|
|
680
|
+
};
|
|
681
|
+
|
|
572
682
|
// Mailchimp — contact sync, not a sender.
|
|
573
683
|
//
|
|
574
684
|
// Deliberately no `group`. Mailchimp and SendGrid shared one while they were
|
|
@@ -612,8 +722,76 @@ var mailchimp = {
|
|
|
612
722
|
placeholder : '••••••••••••••••••••••••••••••••-us1',
|
|
613
723
|
redact : true,
|
|
614
724
|
required : true
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
input : 'select',
|
|
728
|
+
key : 'audience',
|
|
729
|
+
label : 'Mailchimp audience',
|
|
730
|
+
message : 'Contacts your campaigns collect are synced into this audience.',
|
|
731
|
+
required : true,
|
|
732
|
+
source : 'catalog.audiences'
|
|
615
733
|
}
|
|
616
734
|
],
|
|
735
|
+
// The audiences a merchant can sync into, for the picker on their connection.
|
|
736
|
+
//
|
|
737
|
+
// count DEFAULTS TO 10 and maxes at 1000 (their own OpenAPI spec), so leaving
|
|
738
|
+
// it unset returns the first ten audiences and looks entirely successful —
|
|
739
|
+
// the same silent truncation Klaviyo has, at a different number. Paged
|
|
740
|
+
// against total_items so an account past a thousand still resolves.
|
|
741
|
+
hooks : {
|
|
742
|
+
'catalog.audiences' : async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
|
|
743
|
+
|
|
744
|
+
const key = settings?.apiKey;
|
|
745
|
+
|
|
746
|
+
// count DEFAULTS TO 10 and maxes at 1000 in Mailchimp's own spec, so
|
|
747
|
+
// leaving it unset returns ten audiences and looks entirely successful.
|
|
748
|
+
const count = Math.min( limit, 1000 );
|
|
749
|
+
const offset = Number( cursor || 0 );
|
|
750
|
+
|
|
751
|
+
const response = await fetcher(
|
|
752
|
+
base( key ) + '/lists?count=' + count + '&offset=' + offset + '&fields=lists.id,lists.name,total_items',
|
|
753
|
+
{
|
|
754
|
+
// Basic with any username — Mailchimp reads only the password half.
|
|
755
|
+
headers : { authorization : 'Basic ' + Buffer.from( 'drawbridge:' + key ).toString( 'base64' ) },
|
|
756
|
+
signal : AbortSignal.timeout( 15000 )
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
if( ! response.ok ){
|
|
761
|
+
|
|
762
|
+
throw Object.assign(
|
|
763
|
+
new Error( 'Mailchimp refused the request (' + response.status + ')' ),
|
|
764
|
+
{ status : response.status }
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const body = await response.json();
|
|
770
|
+
|
|
771
|
+
const audiences = ( body?.lists || [] ).map( ( list ) => ({ id : list.id, title : list?.name || list.id }) );
|
|
772
|
+
|
|
773
|
+
// Mailchimp's /lists takes no name filter, so a term is matched against
|
|
774
|
+
// this page rather than the account. Stated plainly because it is a real
|
|
775
|
+
// limit: a term matching only an audience on a later page finds nothing.
|
|
776
|
+
const term = String( search?.value || '' ).trim().toLowerCase();
|
|
777
|
+
|
|
778
|
+
const items = term
|
|
779
|
+
? audiences.filter( ( entry ) => entry.title.toLowerCase().includes( term ) )
|
|
780
|
+
: audiences;
|
|
781
|
+
|
|
782
|
+
const nextOffset = offset + count;
|
|
783
|
+
const more = nextOffset < Number( body?.total_items || 0 );
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
items,
|
|
787
|
+
pageInfo : {
|
|
788
|
+
endCursor : more ? String( nextOffset ) : null,
|
|
789
|
+
hasNextPage : more
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
}
|
|
794
|
+
},
|
|
617
795
|
icon: icon$2,
|
|
618
796
|
// A key with no audience chosen is authenticated and inert. Mailchimp also
|
|
619
797
|
// needs its merge fields created on that audience before any Drawbridge total
|
|
@@ -634,6 +812,7 @@ var mailchimp = {
|
|
|
634
812
|
'auth.disconnect' : true,
|
|
635
813
|
'auth.probe' : false,
|
|
636
814
|
'auth.scopes' : false,
|
|
815
|
+
'catalog.audiences' : true,
|
|
637
816
|
'catalog.prices' : false,
|
|
638
817
|
'catalog.products' : false,
|
|
639
818
|
'catalog.promotions' : false,
|
|
@@ -875,6 +1054,7 @@ var shopify = {
|
|
|
875
1054
|
// Shopify has no separate price resource — a price belongs to a product
|
|
876
1055
|
// variant and arrives with it, so there is nothing for prices to answer
|
|
877
1056
|
// that products does not already.
|
|
1057
|
+
'catalog.audiences' : false,
|
|
878
1058
|
'catalog.prices' : false,
|
|
879
1059
|
'catalog.products' : true,
|
|
880
1060
|
'catalog.promotions' : true,
|
|
@@ -1047,6 +1227,7 @@ var webhook = {
|
|
|
1047
1227
|
'auth.disconnect' : true,
|
|
1048
1228
|
'auth.probe' : false,
|
|
1049
1229
|
'auth.scopes' : false,
|
|
1230
|
+
'catalog.audiences' : false,
|
|
1050
1231
|
'catalog.prices' : false,
|
|
1051
1232
|
'catalog.products' : false,
|
|
1052
1233
|
'catalog.promotions' : false,
|
|
@@ -1161,9 +1342,33 @@ const build = ( manifest ) => {
|
|
|
1161
1342
|
|
|
1162
1343
|
// A choice with nothing to choose from renders an empty dropdown, which
|
|
1163
1344
|
// looks like a loading state that never resolves.
|
|
1164
|
-
|
|
1345
|
+
//
|
|
1346
|
+
// `source` is the other way to have options: the vendor supplies them, per
|
|
1347
|
+
// merchant, at form time. A Klaviyo list cannot be a static list here —
|
|
1348
|
+
// it exists in their account, not in this package — and the alternative is
|
|
1349
|
+
// asking a merchant to paste an id from another browser tab.
|
|
1350
|
+
if( field.input === 'select' && ! ( field.options || [] ).length && ! field.source ){
|
|
1351
|
+
|
|
1352
|
+
throw new Error( manifest.slug + '.' + field.key + ' is a select and must declare options or a source' );
|
|
1353
|
+
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// A source names a hook, and the manifest must actually support it —
|
|
1357
|
+
// otherwise the form asks for choices from something that will answer
|
|
1358
|
+
// `unsupported`, and the merchant sees a dropdown that never fills.
|
|
1359
|
+
if( field.source ){
|
|
1360
|
+
|
|
1361
|
+
if( ! HOOK_NAMES.includes( field.source ) ){
|
|
1165
1362
|
|
|
1166
|
-
|
|
1363
|
+
throw new Error( manifest.slug + '.' + field.key + ' sources options from an unknown hook: ' + field.source );
|
|
1364
|
+
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if( manifest.supports?.[ field.source ] !== true ){
|
|
1368
|
+
|
|
1369
|
+
throw new Error( manifest.slug + '.' + field.key + ' sources options from ' + field.source + ', which it declares unsupported' );
|
|
1370
|
+
|
|
1371
|
+
}
|
|
1167
1372
|
|
|
1168
1373
|
}
|
|
1169
1374
|
|
|
@@ -97,7 +97,32 @@ const HOOKS = Object.freeze({
|
|
|
97
97
|
// Vendor data a campaign draws on. Named for what every store platform has,
|
|
98
98
|
// not for what Shopify calls it: Shopify says discounts, Stripe says coupons
|
|
99
99
|
// and promotion codes, BigCommerce says coupons and promotions.
|
|
100
|
+
//
|
|
101
|
+
// ONE SHAPE FOR ALL OF THEM — searchable and cursor-paged:
|
|
102
|
+
//
|
|
103
|
+
// ({ connection, cursor, limit, search, settings, token })
|
|
104
|
+
// -> { items : [ { id, title, ... } ], pageInfo : { endCursor, hasNextPage } }
|
|
105
|
+
//
|
|
106
|
+
// Lifted from what the Shopify product picker already does, rather than
|
|
107
|
+
// invented: that endpoint takes cursor/limit/search and returns items plus a
|
|
108
|
+
// pageInfo, and ListResource consumes exactly that. It was a good contract
|
|
109
|
+
// written once for one vendor; this makes it the contract.
|
|
110
|
+
//
|
|
111
|
+
// `title` is not arbitrary — ListResource searches `[ 'title' ]` by default,
|
|
112
|
+
// so normalising to { id, title } is what lets a picker work with no
|
|
113
|
+
// per-vendor configuration.
|
|
114
|
+
//
|
|
115
|
+
// HOW a vendor searches is its own business, which is the point of a hook.
|
|
116
|
+
// Shopify pushes the term into its GraphQL query, Klaviyo has a filter
|
|
117
|
+
// parameter, and Mailchimp's /lists has no name filter at all so its hook
|
|
118
|
+
// matches against what it fetched. The caller never learns which.
|
|
100
119
|
catalog : Object.freeze([
|
|
120
|
+
// The named groups a contact can be synced INTO. Klaviyo calls them lists,
|
|
121
|
+
// Mailchimp calls them audiences; `audiences` is the industry-generic term
|
|
122
|
+
// and belongs to neither vendor's API. Read at form time, so a merchant
|
|
123
|
+
// picks from what actually exists in their account rather than pasting an
|
|
124
|
+
// id from another tab.
|
|
125
|
+
'audiences',
|
|
101
126
|
'products',
|
|
102
127
|
// Shopify folds price into the product variant; Stripe makes Price a
|
|
103
128
|
// first-class object beside Product. Declared so a vendor that separates
|
|
@@ -421,6 +446,17 @@ var klaviyo = {
|
|
|
421
446
|
{
|
|
422
447
|
key : 'account',
|
|
423
448
|
label : 'Klaviyo account'
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
input : 'select',
|
|
452
|
+
key : 'list',
|
|
453
|
+
label : 'Klaviyo list',
|
|
454
|
+
message : 'Contacts your campaigns collect are synced into this list.',
|
|
455
|
+
required : true,
|
|
456
|
+
// The choices come from the merchant's own account, not from here — see
|
|
457
|
+
// catalog.audiences below. Static options would mean asking somebody to
|
|
458
|
+
// paste a list id copied out of another browser tab.
|
|
459
|
+
source : 'catalog.audiences'
|
|
424
460
|
}
|
|
425
461
|
],
|
|
426
462
|
// The three auth hooks, all pure HTTP against Klaviyo — which is why they
|
|
@@ -449,6 +485,65 @@ var klaviyo = {
|
|
|
449
485
|
//
|
|
450
486
|
// Basic auth with our client, exactly like the token exchange — the token
|
|
451
487
|
// being revoked is the subject, not the credential.
|
|
488
|
+
// The lists a merchant can sync into, for the picker on their connection.
|
|
489
|
+
//
|
|
490
|
+
// PAGINATED DELIBERATELY. Klaviyo caps page[size] at 10 and defaults to it,
|
|
491
|
+
// so a single call quietly returns the first ten lists and an account with
|
|
492
|
+
// more would show a picker missing the one they wanted — with nothing to
|
|
493
|
+
// indicate anything was cut. Follows links.next, bounded so a runaway
|
|
494
|
+
// cursor cannot spin forever.
|
|
495
|
+
'catalog.audiences' : async ({ cursor, fetcher, limit = 100, search, token }) => {
|
|
496
|
+
|
|
497
|
+
// Klaviyo pages by cursor and caps page[size] at 10, so `limit` is what
|
|
498
|
+
// the CALLER wants back rather than what one request can carry — the
|
|
499
|
+
// loop keeps pulling until it has that many or the vendor runs out.
|
|
500
|
+
const audiences = [];
|
|
501
|
+
|
|
502
|
+
let next = cursor
|
|
503
|
+
? '/lists?page%5Bsize%5D=10&page%5Bcursor%5D=' + encodeURIComponent( cursor )
|
|
504
|
+
: '/lists?page%5Bsize%5D=10';
|
|
505
|
+
|
|
506
|
+
let pages = 0;
|
|
507
|
+
|
|
508
|
+
while( next && audiences.length < limit && pages < 20 ){
|
|
509
|
+
|
|
510
|
+
const body = await api( next, { fetcher, token });
|
|
511
|
+
|
|
512
|
+
for( const list of ( body?.data || [] ) ){
|
|
513
|
+
|
|
514
|
+
audiences.push({ id : list.id, title : list?.attributes?.name || list.id });
|
|
515
|
+
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const link = body?.links?.next;
|
|
519
|
+
|
|
520
|
+
// Klaviyo returns an absolute url; only the part after /api travels on,
|
|
521
|
+
// because the shared caller prefixes its own base.
|
|
522
|
+
next = link ? String( link ).replace( /^https:\/\/a\.klaviyo\.com\/api/, '' ) : null;
|
|
523
|
+
|
|
524
|
+
pages = pages + 1;
|
|
525
|
+
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Klaviyo's list endpoint has no name filter, so a search term is applied
|
|
529
|
+
// to what came back. Honest about its own limits: with more lists than
|
|
530
|
+
// `limit`, a term matching only later ones finds nothing — which is why
|
|
531
|
+
// a genuinely large catalog wants server-side filtering, not this.
|
|
532
|
+
const term = String( search?.value || '' ).trim().toLowerCase();
|
|
533
|
+
|
|
534
|
+
const items = term
|
|
535
|
+
? audiences.filter( ( entry ) => entry.title.toLowerCase().includes( term ) )
|
|
536
|
+
: audiences;
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
items,
|
|
540
|
+
pageInfo : {
|
|
541
|
+
endCursor : next,
|
|
542
|
+
hasNextPage : Boolean( next )
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
},
|
|
452
547
|
'auth.disconnect' : async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
|
|
453
548
|
|
|
454
549
|
const token = settings?.refreshToken || settings?.accessToken;
|
|
@@ -533,6 +628,7 @@ var klaviyo = {
|
|
|
533
628
|
'auth.probe' : true,
|
|
534
629
|
// Klaviyo scopes are fixed at app level and re-consented, not drifted.
|
|
535
630
|
'auth.scopes' : false,
|
|
631
|
+
'catalog.audiences' : true,
|
|
536
632
|
'catalog.prices' : false,
|
|
537
633
|
'catalog.products' : false,
|
|
538
634
|
'catalog.promotions' : false,
|
|
@@ -569,6 +665,20 @@ var icon$2 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xm
|
|
|
569
665
|
<path d="M360.476 284.243C360.35 283.835 359.618 281.204 358.647 278.052L356.621 272.648C360.575 266.696 360.645 261.405 360.124 258.393C359.531 254.519 357.693 250.944 354.89 248.205C351.766 244.94 345.363 241.563 336.385 239.044L331.671 237.736C331.643 237.524 331.418 226.619 331.235 221.933C331.08 218.555 330.798 213.264 329.152 208.058C327.182 200.993 323.791 194.858 319.527 190.876C331.277 178.717 338.594 165.307 338.58 153.81C338.538 131.703 311.379 124.976 277.902 138.851L270.824 141.863C270.795 141.835 258.004 129.283 257.821 129.128C219.63 95.8334 100.327 228.476 138.49 260.687L146.835 267.737C144.581 273.775 143.781 280.259 144.499 286.664C145.414 295.543 149.973 304.029 157.375 310.6C164.411 316.82 173.684 320.788 182.648 320.774C197.494 354.998 231.408 375.965 271.175 377.161C313.842 378.427 349.641 358.403 364.67 322.435C365.641 319.916 369.806 308.546 369.806 298.513C369.792 288.409 364.093 284.229 360.476 284.243ZM185.913 311.149C184.613 311.381 183.293 311.48 181.973 311.445C169.083 311.079 155.166 299.483 153.787 285.735C152.253 270.537 160.02 258.829 173.783 256.071C175.415 255.72 177.414 255.537 179.552 255.635C187.264 256.085 198.606 261.996 201.209 278.784C203.517 293.63 199.858 308.785 185.913 311.149ZM171.545 246.953C163.179 248.499 155.744 253.244 150.817 260.18C148.045 257.873 142.909 253.426 142.008 251.681C134.635 237.693 150.043 210.478 160.823 195.111C187.405 157.145 229.086 128.424 248.393 133.603C251.517 134.503 261.902 146.563 261.902 146.563C261.902 146.563 242.623 157.244 224.724 172.16C200.646 190.735 182.423 217.697 171.545 246.953ZM306.792 305.464C306.937 305.403 307.057 305.295 307.134 305.157C307.211 305.019 307.239 304.86 307.214 304.704C307.205 304.61 307.178 304.519 307.133 304.436C307.088 304.353 307.027 304.28 306.954 304.221C306.88 304.162 306.796 304.118 306.705 304.092C306.614 304.066 306.519 304.059 306.426 304.071C306.426 304.071 286.246 307.054 267.179 300.089C269.247 293.348 274.792 295.754 283.137 296.444C296.108 297.209 309.116 295.802 321.623 292.279C330.25 289.788 341.592 284.905 350.401 277.953C353.384 284.497 354.425 291.674 354.425 291.674C354.425 291.674 356.719 291.265 358.647 292.447C360.476 293.573 361.799 295.895 360.898 301.89C359.027 313.119 354.271 322.224 346.235 330.611C341.236 336.036 335.277 340.492 328.659 343.754C324.983 345.691 321.151 347.32 317.205 348.623C286.964 358.487 256.006 347.638 246.029 324.321C245.224 322.535 244.556 320.691 244.03 318.804C239.781 303.438 243.383 285.032 254.655 273.408C255.372 272.676 256.09 271.804 256.09 270.706C256.09 269.806 255.499 268.835 255.007 268.131C251.066 262.418 237.374 252.666 240.132 233.795C242.088 220.23 253.951 210.689 265.012 211.252L267.826 211.421C272.611 211.702 276.79 212.307 280.73 212.49C287.344 212.758 293.268 211.801 300.304 205.947C302.683 203.949 304.582 202.246 307.791 201.711C308.128 201.627 308.973 201.359 310.647 201.416C312.365 201.485 314.032 202.015 315.474 202.949C321.103 206.693 321.905 215.783 322.214 222.439C322.383 226.225 322.848 235.414 322.988 238.031C323.354 244.054 324.944 244.912 328.125 245.954C329.94 246.573 331.615 246.995 334.077 247.713C341.521 249.781 345.968 251.934 348.754 254.65C350.198 256.049 351.13 257.893 351.4 259.885C352.315 266.316 346.432 274.252 330.925 281.457C313.954 289.324 293.367 291.322 279.154 289.732L274.173 289.169C262.774 287.649 256.315 302.34 263.14 312.402C267.545 318.889 279.52 323.11 291.523 323.11C319.006 323.139 340.142 311.402 348.023 301.242L348.642 300.356C349.008 299.765 348.712 299.469 348.22 299.779C341.817 304.169 313.279 321.619 282.771 316.384C282.771 316.384 279.056 315.765 275.678 314.442C273.005 313.429 267.362 310.811 266.686 305.042C291.27 312.683 306.792 305.478 306.792 305.464ZM220.671 194.971C230.127 184.051 241.765 174.538 252.206 169.219C252.558 169.022 252.938 169.43 252.741 169.754C251.46 172 250.476 174.403 249.814 176.902C249.73 177.282 250.138 177.592 250.461 177.353C256.963 172.934 268.248 168.192 278.155 167.601C278.251 167.584 278.351 167.601 278.436 167.65C278.521 167.698 278.586 167.775 278.621 167.866C278.656 167.957 278.658 168.058 278.627 168.151C278.596 168.244 278.533 168.323 278.451 168.375C276.809 169.634 275.342 171.105 274.088 172.751C273.891 173.032 274.074 173.44 274.426 173.44C281.378 173.483 291.186 175.903 297.56 179.491C297.982 179.745 297.673 180.575 297.209 180.462C287.527 178.253 271.724 176.564 255.288 180.575C240.597 184.149 229.396 189.666 221.248 195.618C220.826 195.899 220.333 195.351 220.671 194.971Z" fill="#231E15"/>
|
|
570
666
|
</svg>`;
|
|
571
667
|
|
|
668
|
+
// EVERY ACCOUNT LIVES BEHIND A DATA-CENTRE PREFIX, and the key carries it as a
|
|
669
|
+
// suffix — `...-us19` means us19.api.mailchimp.com. There is no fixed host to
|
|
670
|
+
// call, which is Mailchimp's oldest quirk and the reason its future OAuth flow
|
|
671
|
+
// needs a metadata call the other vendors do not.
|
|
672
|
+
const base = ( apiKey ) => {
|
|
673
|
+
|
|
674
|
+
const dc = String( apiKey || '' ).split( '-' ).pop();
|
|
675
|
+
|
|
676
|
+
if( ! dc || dc === apiKey ) throw new Error( 'That Mailchimp key carries no data centre suffix, so there is no host to call' );
|
|
677
|
+
|
|
678
|
+
return 'https://' + dc + '.api.mailchimp.com/3.0';
|
|
679
|
+
|
|
680
|
+
};
|
|
681
|
+
|
|
572
682
|
// Mailchimp — contact sync, not a sender.
|
|
573
683
|
//
|
|
574
684
|
// Deliberately no `group`. Mailchimp and SendGrid shared one while they were
|
|
@@ -612,8 +722,76 @@ var mailchimp = {
|
|
|
612
722
|
placeholder : '••••••••••••••••••••••••••••••••-us1',
|
|
613
723
|
redact : true,
|
|
614
724
|
required : true
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
input : 'select',
|
|
728
|
+
key : 'audience',
|
|
729
|
+
label : 'Mailchimp audience',
|
|
730
|
+
message : 'Contacts your campaigns collect are synced into this audience.',
|
|
731
|
+
required : true,
|
|
732
|
+
source : 'catalog.audiences'
|
|
615
733
|
}
|
|
616
734
|
],
|
|
735
|
+
// The audiences a merchant can sync into, for the picker on their connection.
|
|
736
|
+
//
|
|
737
|
+
// count DEFAULTS TO 10 and maxes at 1000 (their own OpenAPI spec), so leaving
|
|
738
|
+
// it unset returns the first ten audiences and looks entirely successful —
|
|
739
|
+
// the same silent truncation Klaviyo has, at a different number. Paged
|
|
740
|
+
// against total_items so an account past a thousand still resolves.
|
|
741
|
+
hooks : {
|
|
742
|
+
'catalog.audiences' : async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
|
|
743
|
+
|
|
744
|
+
const key = settings?.apiKey;
|
|
745
|
+
|
|
746
|
+
// count DEFAULTS TO 10 and maxes at 1000 in Mailchimp's own spec, so
|
|
747
|
+
// leaving it unset returns ten audiences and looks entirely successful.
|
|
748
|
+
const count = Math.min( limit, 1000 );
|
|
749
|
+
const offset = Number( cursor || 0 );
|
|
750
|
+
|
|
751
|
+
const response = await fetcher(
|
|
752
|
+
base( key ) + '/lists?count=' + count + '&offset=' + offset + '&fields=lists.id,lists.name,total_items',
|
|
753
|
+
{
|
|
754
|
+
// Basic with any username — Mailchimp reads only the password half.
|
|
755
|
+
headers : { authorization : 'Basic ' + Buffer.from( 'drawbridge:' + key ).toString( 'base64' ) },
|
|
756
|
+
signal : AbortSignal.timeout( 15000 )
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
if( ! response.ok ){
|
|
761
|
+
|
|
762
|
+
throw Object.assign(
|
|
763
|
+
new Error( 'Mailchimp refused the request (' + response.status + ')' ),
|
|
764
|
+
{ status : response.status }
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const body = await response.json();
|
|
770
|
+
|
|
771
|
+
const audiences = ( body?.lists || [] ).map( ( list ) => ({ id : list.id, title : list?.name || list.id }) );
|
|
772
|
+
|
|
773
|
+
// Mailchimp's /lists takes no name filter, so a term is matched against
|
|
774
|
+
// this page rather than the account. Stated plainly because it is a real
|
|
775
|
+
// limit: a term matching only an audience on a later page finds nothing.
|
|
776
|
+
const term = String( search?.value || '' ).trim().toLowerCase();
|
|
777
|
+
|
|
778
|
+
const items = term
|
|
779
|
+
? audiences.filter( ( entry ) => entry.title.toLowerCase().includes( term ) )
|
|
780
|
+
: audiences;
|
|
781
|
+
|
|
782
|
+
const nextOffset = offset + count;
|
|
783
|
+
const more = nextOffset < Number( body?.total_items || 0 );
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
items,
|
|
787
|
+
pageInfo : {
|
|
788
|
+
endCursor : more ? String( nextOffset ) : null,
|
|
789
|
+
hasNextPage : more
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
}
|
|
794
|
+
},
|
|
617
795
|
icon: icon$2,
|
|
618
796
|
// A key with no audience chosen is authenticated and inert. Mailchimp also
|
|
619
797
|
// needs its merge fields created on that audience before any Drawbridge total
|
|
@@ -634,6 +812,7 @@ var mailchimp = {
|
|
|
634
812
|
'auth.disconnect' : true,
|
|
635
813
|
'auth.probe' : false,
|
|
636
814
|
'auth.scopes' : false,
|
|
815
|
+
'catalog.audiences' : true,
|
|
637
816
|
'catalog.prices' : false,
|
|
638
817
|
'catalog.products' : false,
|
|
639
818
|
'catalog.promotions' : false,
|
|
@@ -875,6 +1054,7 @@ var shopify = {
|
|
|
875
1054
|
// Shopify has no separate price resource — a price belongs to a product
|
|
876
1055
|
// variant and arrives with it, so there is nothing for prices to answer
|
|
877
1056
|
// that products does not already.
|
|
1057
|
+
'catalog.audiences' : false,
|
|
878
1058
|
'catalog.prices' : false,
|
|
879
1059
|
'catalog.products' : true,
|
|
880
1060
|
'catalog.promotions' : true,
|
|
@@ -1047,6 +1227,7 @@ var webhook = {
|
|
|
1047
1227
|
'auth.disconnect' : true,
|
|
1048
1228
|
'auth.probe' : false,
|
|
1049
1229
|
'auth.scopes' : false,
|
|
1230
|
+
'catalog.audiences' : false,
|
|
1050
1231
|
'catalog.prices' : false,
|
|
1051
1232
|
'catalog.products' : false,
|
|
1052
1233
|
'catalog.promotions' : false,
|
|
@@ -1161,9 +1342,33 @@ const build = ( manifest ) => {
|
|
|
1161
1342
|
|
|
1162
1343
|
// A choice with nothing to choose from renders an empty dropdown, which
|
|
1163
1344
|
// looks like a loading state that never resolves.
|
|
1164
|
-
|
|
1345
|
+
//
|
|
1346
|
+
// `source` is the other way to have options: the vendor supplies them, per
|
|
1347
|
+
// merchant, at form time. A Klaviyo list cannot be a static list here —
|
|
1348
|
+
// it exists in their account, not in this package — and the alternative is
|
|
1349
|
+
// asking a merchant to paste an id from another browser tab.
|
|
1350
|
+
if( field.input === 'select' && ! ( field.options || [] ).length && ! field.source ){
|
|
1351
|
+
|
|
1352
|
+
throw new Error( manifest.slug + '.' + field.key + ' is a select and must declare options or a source' );
|
|
1353
|
+
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// A source names a hook, and the manifest must actually support it —
|
|
1357
|
+
// otherwise the form asks for choices from something that will answer
|
|
1358
|
+
// `unsupported`, and the merchant sees a dropdown that never fills.
|
|
1359
|
+
if( field.source ){
|
|
1360
|
+
|
|
1361
|
+
if( ! HOOK_NAMES.includes( field.source ) ){
|
|
1165
1362
|
|
|
1166
|
-
|
|
1363
|
+
throw new Error( manifest.slug + '.' + field.key + ' sources options from an unknown hook: ' + field.source );
|
|
1364
|
+
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if( manifest.supports?.[ field.source ] !== true ){
|
|
1368
|
+
|
|
1369
|
+
throw new Error( manifest.slug + '.' + field.key + ' sources options from ' + field.source + ', which it declares unsupported' );
|
|
1370
|
+
|
|
1371
|
+
}
|
|
1167
1372
|
|
|
1168
1373
|
}
|
|
1169
1374
|
|
|
@@ -73,7 +73,32 @@ var HOOKS = Object.freeze({
|
|
|
73
73
|
// Vendor data a campaign draws on. Named for what every store platform has,
|
|
74
74
|
// not for what Shopify calls it: Shopify says discounts, Stripe says coupons
|
|
75
75
|
// and promotion codes, BigCommerce says coupons and promotions.
|
|
76
|
+
//
|
|
77
|
+
// ONE SHAPE FOR ALL OF THEM — searchable and cursor-paged:
|
|
78
|
+
//
|
|
79
|
+
// ({ connection, cursor, limit, search, settings, token })
|
|
80
|
+
// -> { items : [ { id, title, ... } ], pageInfo : { endCursor, hasNextPage } }
|
|
81
|
+
//
|
|
82
|
+
// Lifted from what the Shopify product picker already does, rather than
|
|
83
|
+
// invented: that endpoint takes cursor/limit/search and returns items plus a
|
|
84
|
+
// pageInfo, and ListResource consumes exactly that. It was a good contract
|
|
85
|
+
// written once for one vendor; this makes it the contract.
|
|
86
|
+
//
|
|
87
|
+
// `title` is not arbitrary — ListResource searches `[ 'title' ]` by default,
|
|
88
|
+
// so normalising to { id, title } is what lets a picker work with no
|
|
89
|
+
// per-vendor configuration.
|
|
90
|
+
//
|
|
91
|
+
// HOW a vendor searches is its own business, which is the point of a hook.
|
|
92
|
+
// Shopify pushes the term into its GraphQL query, Klaviyo has a filter
|
|
93
|
+
// parameter, and Mailchimp's /lists has no name filter at all so its hook
|
|
94
|
+
// matches against what it fetched. The caller never learns which.
|
|
76
95
|
catalog: Object.freeze([
|
|
96
|
+
// The named groups a contact can be synced INTO. Klaviyo calls them lists,
|
|
97
|
+
// Mailchimp calls them audiences; `audiences` is the industry-generic term
|
|
98
|
+
// and belongs to neither vendor's API. Read at form time, so a merchant
|
|
99
|
+
// picks from what actually exists in their account rather than pasting an
|
|
100
|
+
// id from another tab.
|
|
101
|
+
"audiences",
|
|
77
102
|
"products",
|
|
78
103
|
// Shopify folds price into the product variant; Stripe makes Price a
|
|
79
104
|
// first-class object beside Product. Declared so a vendor that separates
|
|
@@ -349,6 +374,17 @@ var klaviyo_default2 = {
|
|
|
349
374
|
{
|
|
350
375
|
key: "account",
|
|
351
376
|
label: "Klaviyo account"
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
input: "select",
|
|
380
|
+
key: "list",
|
|
381
|
+
label: "Klaviyo list",
|
|
382
|
+
message: "Contacts your campaigns collect are synced into this list.",
|
|
383
|
+
required: true,
|
|
384
|
+
// The choices come from the merchant's own account, not from here — see
|
|
385
|
+
// catalog.audiences below. Static options would mean asking somebody to
|
|
386
|
+
// paste a list id copied out of another browser tab.
|
|
387
|
+
source: "catalog.audiences"
|
|
352
388
|
}
|
|
353
389
|
],
|
|
354
390
|
// The three auth hooks, all pure HTTP against Klaviyo — which is why they
|
|
@@ -374,6 +410,37 @@ var klaviyo_default2 = {
|
|
|
374
410
|
//
|
|
375
411
|
// Basic auth with our client, exactly like the token exchange — the token
|
|
376
412
|
// being revoked is the subject, not the credential.
|
|
413
|
+
// The lists a merchant can sync into, for the picker on their connection.
|
|
414
|
+
//
|
|
415
|
+
// PAGINATED DELIBERATELY. Klaviyo caps page[size] at 10 and defaults to it,
|
|
416
|
+
// so a single call quietly returns the first ten lists and an account with
|
|
417
|
+
// more would show a picker missing the one they wanted — with nothing to
|
|
418
|
+
// indicate anything was cut. Follows links.next, bounded so a runaway
|
|
419
|
+
// cursor cannot spin forever.
|
|
420
|
+
"catalog.audiences": async ({ cursor, fetcher, limit = 100, search, token }) => {
|
|
421
|
+
var _a, _b;
|
|
422
|
+
const audiences = [];
|
|
423
|
+
let next = cursor ? "/lists?page%5Bsize%5D=10&page%5Bcursor%5D=" + encodeURIComponent(cursor) : "/lists?page%5Bsize%5D=10";
|
|
424
|
+
let pages = 0;
|
|
425
|
+
while (next && audiences.length < limit && pages < 20) {
|
|
426
|
+
const body = await api(next, { fetcher, token });
|
|
427
|
+
for (const list of (body == null ? void 0 : body.data) || []) {
|
|
428
|
+
audiences.push({ id: list.id, title: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id });
|
|
429
|
+
}
|
|
430
|
+
const link = (_b = body == null ? void 0 : body.links) == null ? void 0 : _b.next;
|
|
431
|
+
next = link ? String(link).replace(/^https:\/\/a\.klaviyo\.com\/api/, "") : null;
|
|
432
|
+
pages = pages + 1;
|
|
433
|
+
}
|
|
434
|
+
const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
|
|
435
|
+
const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
|
|
436
|
+
return {
|
|
437
|
+
items,
|
|
438
|
+
pageInfo: {
|
|
439
|
+
endCursor: next,
|
|
440
|
+
hasNextPage: Boolean(next)
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
},
|
|
377
444
|
"auth.disconnect": async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
|
|
378
445
|
const token = (settings == null ? void 0 : settings.refreshToken) || (settings == null ? void 0 : settings.accessToken);
|
|
379
446
|
if (!token) return { revoked: false };
|
|
@@ -447,6 +514,7 @@ var klaviyo_default2 = {
|
|
|
447
514
|
"auth.probe": true,
|
|
448
515
|
// Klaviyo scopes are fixed at app level and re-consented, not drifted.
|
|
449
516
|
"auth.scopes": false,
|
|
517
|
+
"catalog.audiences": true,
|
|
450
518
|
"catalog.prices": false,
|
|
451
519
|
"catalog.products": false,
|
|
452
520
|
"catalog.promotions": false,
|
|
@@ -479,6 +547,11 @@ var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fil
|
|
|
479
547
|
</svg>`;
|
|
480
548
|
|
|
481
549
|
// lib/connections/mailchimp.js
|
|
550
|
+
var base = (apiKey) => {
|
|
551
|
+
const dc = String(apiKey || "").split("-").pop();
|
|
552
|
+
if (!dc || dc === apiKey) throw new Error("That Mailchimp key carries no data centre suffix, so there is no host to call");
|
|
553
|
+
return "https://" + dc + ".api.mailchimp.com/3.0";
|
|
554
|
+
};
|
|
482
555
|
var mailchimp_default2 = {
|
|
483
556
|
// Keys TODAY. Mailchimp integrations authenticate with OAuth 2 (authorization
|
|
484
557
|
// code) and that is where this goes, so the endpoints are recorded here
|
|
@@ -516,8 +589,56 @@ var mailchimp_default2 = {
|
|
|
516
589
|
placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022-us1",
|
|
517
590
|
redact: true,
|
|
518
591
|
required: true
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
input: "select",
|
|
595
|
+
key: "audience",
|
|
596
|
+
label: "Mailchimp audience",
|
|
597
|
+
message: "Contacts your campaigns collect are synced into this audience.",
|
|
598
|
+
required: true,
|
|
599
|
+
source: "catalog.audiences"
|
|
519
600
|
}
|
|
520
601
|
],
|
|
602
|
+
// The audiences a merchant can sync into, for the picker on their connection.
|
|
603
|
+
//
|
|
604
|
+
// count DEFAULTS TO 10 and maxes at 1000 (their own OpenAPI spec), so leaving
|
|
605
|
+
// it unset returns the first ten audiences and looks entirely successful —
|
|
606
|
+
// the same silent truncation Klaviyo has, at a different number. Paged
|
|
607
|
+
// against total_items so an account past a thousand still resolves.
|
|
608
|
+
hooks: {
|
|
609
|
+
"catalog.audiences": async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
|
|
610
|
+
const key = settings == null ? void 0 : settings.apiKey;
|
|
611
|
+
const count = Math.min(limit, 1e3);
|
|
612
|
+
const offset = Number(cursor || 0);
|
|
613
|
+
const response = await fetcher(
|
|
614
|
+
base(key) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
|
|
615
|
+
{
|
|
616
|
+
// Basic with any username — Mailchimp reads only the password half.
|
|
617
|
+
headers: { authorization: "Basic " + Buffer.from("drawbridge:" + key).toString("base64") },
|
|
618
|
+
signal: AbortSignal.timeout(15e3)
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
if (!response.ok) {
|
|
622
|
+
throw Object.assign(
|
|
623
|
+
new Error("Mailchimp refused the request (" + response.status + ")"),
|
|
624
|
+
{ status: response.status }
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
const body = await response.json();
|
|
628
|
+
const audiences = ((body == null ? void 0 : body.lists) || []).map((list) => ({ id: list.id, title: (list == null ? void 0 : list.name) || list.id }));
|
|
629
|
+
const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
|
|
630
|
+
const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
|
|
631
|
+
const nextOffset = offset + count;
|
|
632
|
+
const more = nextOffset < Number((body == null ? void 0 : body.total_items) || 0);
|
|
633
|
+
return {
|
|
634
|
+
items,
|
|
635
|
+
pageInfo: {
|
|
636
|
+
endCursor: more ? String(nextOffset) : null,
|
|
637
|
+
hasNextPage: more
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
},
|
|
521
642
|
icon: mailchimp_default,
|
|
522
643
|
// A key with no audience chosen is authenticated and inert. Mailchimp also
|
|
523
644
|
// needs its merge fields created on that audience before any Drawbridge total
|
|
@@ -538,6 +659,7 @@ var mailchimp_default2 = {
|
|
|
538
659
|
"auth.disconnect": true,
|
|
539
660
|
"auth.probe": false,
|
|
540
661
|
"auth.scopes": false,
|
|
662
|
+
"catalog.audiences": true,
|
|
541
663
|
"catalog.prices": false,
|
|
542
664
|
"catalog.products": false,
|
|
543
665
|
"catalog.promotions": false,
|
|
@@ -712,6 +834,7 @@ var shopify_default2 = {
|
|
|
712
834
|
// Shopify has no separate price resource — a price belongs to a product
|
|
713
835
|
// variant and arrives with it, so there is nothing for prices to answer
|
|
714
836
|
// that products does not already.
|
|
837
|
+
"catalog.audiences": false,
|
|
715
838
|
"catalog.prices": false,
|
|
716
839
|
"catalog.products": true,
|
|
717
840
|
"catalog.promotions": true,
|
|
@@ -877,6 +1000,7 @@ var webhook_default = {
|
|
|
877
1000
|
"auth.disconnect": true,
|
|
878
1001
|
"auth.probe": false,
|
|
879
1002
|
"auth.scopes": false,
|
|
1003
|
+
"catalog.audiences": false,
|
|
880
1004
|
"catalog.prices": false,
|
|
881
1005
|
"catalog.products": false,
|
|
882
1006
|
"catalog.promotions": false,
|
|
@@ -928,7 +1052,7 @@ var webhook_default = {
|
|
|
928
1052
|
// lib/connections/index.js
|
|
929
1053
|
var QUEUES = ["connection", "notification", "segment", "webhook"];
|
|
930
1054
|
var build = (manifest) => {
|
|
931
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
1055
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
932
1056
|
if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
|
|
933
1057
|
if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
|
|
934
1058
|
if (!(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
|
|
@@ -943,8 +1067,16 @@ var build = (manifest) => {
|
|
|
943
1067
|
if (field.input && !INPUTS.includes(field.input)) {
|
|
944
1068
|
throw new Error(manifest.slug + "." + field.key + " declares an unknown input: " + field.input + " \u2014 one of " + INPUTS.join(", "));
|
|
945
1069
|
}
|
|
946
|
-
if (field.input === "select" && !(field.options || []).length) {
|
|
947
|
-
throw new Error(manifest.slug + "." + field.key + " is a select and must declare options");
|
|
1070
|
+
if (field.input === "select" && !(field.options || []).length && !field.source) {
|
|
1071
|
+
throw new Error(manifest.slug + "." + field.key + " is a select and must declare options or a source");
|
|
1072
|
+
}
|
|
1073
|
+
if (field.source) {
|
|
1074
|
+
if (!HOOK_NAMES.includes(field.source)) {
|
|
1075
|
+
throw new Error(manifest.slug + "." + field.key + " sources options from an unknown hook: " + field.source);
|
|
1076
|
+
}
|
|
1077
|
+
if (((_a = manifest.supports) == null ? void 0 : _a[field.source]) !== true) {
|
|
1078
|
+
throw new Error(manifest.slug + "." + field.key + " sources options from " + field.source + ", which it declares unsupported");
|
|
1079
|
+
}
|
|
948
1080
|
}
|
|
949
1081
|
}
|
|
950
1082
|
if (typeof (manifest == null ? void 0 : manifest.icon) !== "string" || !manifest.icon.includes("<svg")) {
|
|
@@ -959,15 +1091,15 @@ var build = (manifest) => {
|
|
|
959
1091
|
if (!CATEGORIES.includes(manifest == null ? void 0 : manifest.category)) {
|
|
960
1092
|
throw new Error(manifest.slug + " needs a category \u2014 one of " + CATEGORIES.join(", "));
|
|
961
1093
|
}
|
|
962
|
-
if ((
|
|
1094
|
+
if ((_b = manifest == null ? void 0 : manifest.connect) == null ? void 0 : _b.type) {
|
|
963
1095
|
throw new Error(manifest.slug + " declares connect.type \u2014 that is auth.type now");
|
|
964
1096
|
}
|
|
965
|
-
if (!AUTH_TYPES.includes((
|
|
1097
|
+
if (!AUTH_TYPES.includes((_c = manifest == null ? void 0 : manifest.auth) == null ? void 0 : _c.type)) {
|
|
966
1098
|
throw new Error(manifest.slug + " needs auth.type \u2014 one of " + AUTH_TYPES.join(", "));
|
|
967
1099
|
}
|
|
968
1100
|
if (manifest.auth.type === "oauth") {
|
|
969
1101
|
for (const field of OAUTH_FIELDS) {
|
|
970
|
-
if (!((
|
|
1102
|
+
if (!((_d = manifest.auth.oauth) == null ? void 0 : _d[field])) {
|
|
971
1103
|
throw new Error(manifest.slug + " is oauth and must declare auth.oauth." + field);
|
|
972
1104
|
}
|
|
973
1105
|
}
|
|
@@ -977,10 +1109,10 @@ var build = (manifest) => {
|
|
|
977
1109
|
);
|
|
978
1110
|
}
|
|
979
1111
|
}
|
|
980
|
-
if (((
|
|
1112
|
+
if (((_e = manifest.supports) == null ? void 0 : _e["inbound.event"]) && !((_g = (_f = manifest.inbound) == null ? void 0 : _f.headers) == null ? void 0 : _g.event)) {
|
|
981
1113
|
throw new Error(manifest.slug + " supports inbound.event but declares no inbound.headers.event");
|
|
982
1114
|
}
|
|
983
|
-
if (((
|
|
1115
|
+
if (((_h = manifest.supports) == null ? void 0 : _h["inbound.verify"]) && !((_j = (_i = manifest.inbound) == null ? void 0 : _i.headers) == null ? void 0 : _j.signature)) {
|
|
984
1116
|
throw new Error(manifest.slug + " supports inbound.verify but declares no inbound.headers.signature");
|
|
985
1117
|
}
|
|
986
1118
|
if (typeof (manifest == null ? void 0 : manifest.incomplete) !== "function") {
|
|
@@ -996,7 +1128,7 @@ var build = (manifest) => {
|
|
|
996
1128
|
if (typeof hook !== "function") {
|
|
997
1129
|
throw new Error(manifest.slug + " declares hook " + name + " but it is not a function");
|
|
998
1130
|
}
|
|
999
|
-
if (((
|
|
1131
|
+
if (((_k = manifest.supports) == null ? void 0 : _k[name]) !== true) {
|
|
1000
1132
|
throw new Error(manifest.slug + " implements " + name + " but declares supports[ '" + name + "' ] false");
|
|
1001
1133
|
}
|
|
1002
1134
|
}
|
package/package.json
CHANGED