@drawbridge/drawbridge-utils 0.0.153 → 0.0.155
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 +81 -17
- package/dist/connections/index.d.cts +150 -19
- package/dist/connections/index.d.ts +150 -19
- package/dist/connections/index.js +81 -17
- package/dist/providers.cjs +81 -17
- package/dist/providers.js +81 -17
- package/package.json +1 -1
|
@@ -275,6 +275,7 @@ var HOOK_PROPS = Object.freeze([
|
|
|
275
275
|
"scope",
|
|
276
276
|
"search",
|
|
277
277
|
"secret",
|
|
278
|
+
"segments",
|
|
278
279
|
"settings",
|
|
279
280
|
"sort",
|
|
280
281
|
"step",
|
|
@@ -2668,10 +2669,15 @@ var klaviyo_default2 = {
|
|
|
2668
2669
|
excerpt: "Sync the contacts your campaigns collect into a Klaviyo list.",
|
|
2669
2670
|
// HOW TO CONNECT, in the merchant's words. Was `setup`, which nothing
|
|
2670
2671
|
// rendered — four useful instructions no component displayed.
|
|
2672
|
+
// NAMES THE LIST STEP, because `status` below gates on it: a grant with no
|
|
2673
|
+
// list chosen sits at Pending, and this told the merchant it would show
|
|
2674
|
+
// Active. The one required action was the one action the guide omitted, so
|
|
2675
|
+
// anybody following it landed on a connection that looked broken.
|
|
2671
2676
|
guide: [
|
|
2672
2677
|
"Press Connect. Drawbridge sends you to Klaviyo to approve access.",
|
|
2673
2678
|
"Sign in to Klaviyo if you are not already, and choose the account to connect.",
|
|
2674
|
-
"Approve the permissions Klaviyo lists. You
|
|
2679
|
+
"Approve the permissions Klaviyo lists. You come back here to pick the list your contacts should sync into.",
|
|
2680
|
+
"The connection shows Pending until you pick a list, then Active.",
|
|
2675
2681
|
"You can revoke access at any time from Klaviyo, under Integrations."
|
|
2676
2682
|
]
|
|
2677
2683
|
},
|
|
@@ -2793,14 +2799,15 @@ var klaviyo_default2 = {
|
|
|
2793
2799
|
// Not yet. Suppression syncs an opt-out as unsubscribed, which is a
|
|
2794
2800
|
// different thing from deleting the profile.
|
|
2795
2801
|
remove: false,
|
|
2796
|
-
sync: async ({
|
|
2802
|
+
sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
|
|
2797
2803
|
var _a, _b, _c;
|
|
2798
2804
|
const list = settings == null ? void 0 : settings.list;
|
|
2799
2805
|
if (!list) return { message: "No Klaviyo list is chosen for this connection.", skipped: true };
|
|
2800
2806
|
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);
|
|
2801
2807
|
if (!email) return { message: "That lead has no email address to sync.", skipped: true };
|
|
2802
|
-
const
|
|
2803
|
-
const
|
|
2808
|
+
const person = (context == null ? void 0 : context.contact) || null;
|
|
2809
|
+
const totals = (person == null ? void 0 : person.totals) || {};
|
|
2810
|
+
const profile = await api2("/profile-import", {
|
|
2804
2811
|
fetcher,
|
|
2805
2812
|
method: "POST",
|
|
2806
2813
|
payload: {
|
|
@@ -2809,16 +2816,36 @@ var klaviyo_default2 = {
|
|
|
2809
2816
|
email,
|
|
2810
2817
|
...(lead == null ? void 0 : lead.name) && { first_name: String(lead.name).trim().split(/\s+/)[0] },
|
|
2811
2818
|
properties: {
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2819
|
+
...person && {
|
|
2820
|
+
drawbridge_campaigns: (person.campaigns || []).length,
|
|
2821
|
+
drawbridge_draws: totals.draws || 0,
|
|
2822
|
+
drawbridge_entries: totals.entries || 0,
|
|
2823
|
+
drawbridge_orders: totals.orders || 0,
|
|
2824
|
+
// Campaign-attributed, NOT lifetime. A merchant running
|
|
2825
|
+
// Shopify already has lifetime revenue in Klaviyo through
|
|
2826
|
+
// Klaviyo's own integration; what only we can say is how
|
|
2827
|
+
// much a campaign drove. Named so the two cannot be
|
|
2828
|
+
// mistaken for one another in a segment builder.
|
|
2829
|
+
drawbridge_revenue: totals.gross || 0
|
|
2830
|
+
},
|
|
2831
|
+
// THE DRAWBRIDGE SEGMENTS THEY ARE IN, as a list property the
|
|
2832
|
+
// merchant builds Klaviyo segments on top of. Klaviyo owns no
|
|
2833
|
+
// writable membership — its segments are computed from rules,
|
|
2834
|
+
// so a profile cannot be put in one — and this is the nearest
|
|
2835
|
+
// honest equivalent.
|
|
2836
|
+
//
|
|
2837
|
+
// WHOLESALE, WHICH IS WHAT MAKES IT SELF-HEALING. Drawbridge
|
|
2838
|
+
// segments are dynamic and nothing dispatches a leave event —
|
|
2839
|
+
// only segment.contact.add exists — so a property that was
|
|
2840
|
+
// only ever appended to would outlive the membership that
|
|
2841
|
+
// earned it. Rewriting the whole array each run means every
|
|
2842
|
+
// sync corrects whatever the last one got wrong, with no
|
|
2843
|
+
// removal path to build.
|
|
2844
|
+
//
|
|
2845
|
+
// `segments` is null when the run carried no contact document,
|
|
2846
|
+
// meaning nobody looked — different from [], which means they
|
|
2847
|
+
// are in none. Null omits the key and merge leaves it alone.
|
|
2848
|
+
...segments && { drawbridge_segments: segments.map((entry) => entry.title).filter(Boolean) }
|
|
2822
2849
|
}
|
|
2823
2850
|
},
|
|
2824
2851
|
type: "profile"
|
|
@@ -3110,7 +3137,7 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
|
|
|
3110
3137
|
{ status: response.status }
|
|
3111
3138
|
);
|
|
3112
3139
|
}
|
|
3113
|
-
return response.json();
|
|
3140
|
+
return response.status === 204 ? null : response.json();
|
|
3114
3141
|
};
|
|
3115
3142
|
var subscriberHash = (email) => (0, import_node_crypto3.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
|
|
3116
3143
|
var mailchimp_default2 = {
|
|
@@ -3244,7 +3271,7 @@ var mailchimp_default2 = {
|
|
|
3244
3271
|
// PUT /lists/{list_id}/members/{subscriber_hash} — an UPSERT, which is
|
|
3245
3272
|
// why there is no create-or-update branch here. Quoted from Mailchimp's
|
|
3246
3273
|
// Marketing API reference for the list-members resource.
|
|
3247
|
-
sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
|
|
3274
|
+
sync: async ({ connection: connection2, lead, segments, settings, suppressed, token }, { fetcher, read } = {}) => {
|
|
3248
3275
|
var _a, _b;
|
|
3249
3276
|
const audience = settings == null ? void 0 : settings.audience;
|
|
3250
3277
|
if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
|
|
@@ -3270,6 +3297,36 @@ var mailchimp_default2 = {
|
|
|
3270
3297
|
},
|
|
3271
3298
|
token
|
|
3272
3299
|
});
|
|
3300
|
+
if (segments && read) {
|
|
3301
|
+
const owned = await read.aggregate({
|
|
3302
|
+
collection: "segment",
|
|
3303
|
+
pipeline: [
|
|
3304
|
+
{
|
|
3305
|
+
$match: {
|
|
3306
|
+
organization: connection2 == null ? void 0 : connection2.organization,
|
|
3307
|
+
system: { $ne: true }
|
|
3308
|
+
}
|
|
3309
|
+
},
|
|
3310
|
+
{
|
|
3311
|
+
$project: { _id: 0, title: 1 }
|
|
3312
|
+
}
|
|
3313
|
+
]
|
|
3314
|
+
});
|
|
3315
|
+
const joined = new Set(segments.map((entry) => entry.title));
|
|
3316
|
+
const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
|
|
3317
|
+
name: "Drawbridge: " + title,
|
|
3318
|
+
status: joined.has(title) ? "active" : "inactive"
|
|
3319
|
+
}));
|
|
3320
|
+
if (tags.length > 0) {
|
|
3321
|
+
await api3("/lists/" + audience + "/members/" + hash + "/tags", {
|
|
3322
|
+
dc: settings == null ? void 0 : settings.dc,
|
|
3323
|
+
fetcher,
|
|
3324
|
+
method: "POST",
|
|
3325
|
+
payload: { tags },
|
|
3326
|
+
token
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3273
3330
|
return {
|
|
3274
3331
|
// Merged into `context` for later steps in this run.
|
|
3275
3332
|
context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
|
|
@@ -4130,7 +4187,9 @@ var shopify_default2 = {
|
|
|
4130
4187
|
enqueues: [{
|
|
4131
4188
|
data: { product: productId, providerId, shop: connection2.shop },
|
|
4132
4189
|
name: "workflow",
|
|
4133
|
-
|
|
4190
|
+
// The numeric id, not the gid: BullMQ refuses a custom id with a
|
|
4191
|
+
// colon in it, and every gid carries two.
|
|
4192
|
+
options: { jobId: "product.workflow.shopify." + context.id + "." + Date.now() },
|
|
4134
4193
|
queue: "product.shopify"
|
|
4135
4194
|
}],
|
|
4136
4195
|
message: "Product sync queued from Shopify webhook.",
|
|
@@ -4936,9 +4995,14 @@ var webhook_default = {
|
|
|
4936
4995
|
"Generate a signing secret and Drawbridge signs every request with it. Your endpoint recomputes the signature to confirm each payload genuinely came from Drawbridge before acting on it."
|
|
4937
4996
|
],
|
|
4938
4997
|
excerpt: "Sign outgoing webhook payloads with an HMAC secret to verify authenticity.",
|
|
4998
|
+
// SAYS WHERE THE URL GOES. Connecting only mints a secret, so a merchant
|
|
4999
|
+
// following this had nowhere to put the endpoint they came to configure —
|
|
5000
|
+
// the destination lives on the step (see steps.webhook.send.settings), and
|
|
5001
|
+
// the guide never said so.
|
|
4939
5002
|
guide: [
|
|
4940
5003
|
"Press Connect. Drawbridge generates a signing secret and shows it here.",
|
|
4941
5004
|
"Copy the secret into your own endpoint.",
|
|
5005
|
+
"Add a Send webhook step to a workflow and put your endpoint URL on it. The URL belongs to the step rather than the connection, so one connection can serve several endpoints.",
|
|
4942
5006
|
"On each request, compute HMAC-SHA256 of the raw body using the secret and compare it against the X-Drawbridge-Signature header before acting on the payload."
|
|
4943
5007
|
]
|
|
4944
5008
|
},
|
|
@@ -334,6 +334,10 @@ const WRITE_OPERATIONS = Object.freeze([ 'create', 'delete', 'update' ]);
|
|
|
334
334
|
// payload the verified inbound body
|
|
335
335
|
// scope the granted scope string being judged
|
|
336
336
|
// secret the credential VALUE the caller resolved for verification
|
|
337
|
+
// segments the non-system segments this contact is in, as { id, title },
|
|
338
|
+
// resolved by the shell — null when the run carried no contact
|
|
339
|
+
// document, which means nobody looked, where [] means they are
|
|
340
|
+
// in none. A hook handed null leaves the vendor's copy alone
|
|
337
341
|
// settings the connection's decrypted settings
|
|
338
342
|
// step the step document
|
|
339
343
|
// suppressed the opt-out floor's answer for this lead, computed by the shell
|
|
@@ -383,7 +387,7 @@ const HOOK_PROPS = Object.freeze([
|
|
|
383
387
|
'channel', 'clientId', 'clientSecret', 'connection', 'contact', 'context',
|
|
384
388
|
'cursor', 'declaration', 'doc', 'email', 'event', 'headers', 'id', 'lead', 'limit',
|
|
385
389
|
'manifest', 'payload',
|
|
386
|
-
'scope', 'search', 'secret', 'settings', 'sort', 'step', 'suppressed',
|
|
390
|
+
'scope', 'search', 'secret', 'segments', 'settings', 'sort', 'step', 'suppressed',
|
|
387
391
|
'token', 'tokens', 'workflow'
|
|
388
392
|
]);
|
|
389
393
|
|
|
@@ -2834,10 +2838,15 @@ var klaviyo = {
|
|
|
2834
2838
|
|
|
2835
2839
|
// HOW TO CONNECT, in the merchant's words. Was `setup`, which nothing
|
|
2836
2840
|
// rendered — four useful instructions no component displayed.
|
|
2841
|
+
// NAMES THE LIST STEP, because `status` below gates on it: a grant with no
|
|
2842
|
+
// list chosen sits at Pending, and this told the merchant it would show
|
|
2843
|
+
// Active. The one required action was the one action the guide omitted, so
|
|
2844
|
+
// anybody following it landed on a connection that looked broken.
|
|
2837
2845
|
guide : [
|
|
2838
2846
|
'Press Connect. Drawbridge sends you to Klaviyo to approve access.',
|
|
2839
2847
|
'Sign in to Klaviyo if you are not already, and choose the account to connect.',
|
|
2840
|
-
'Approve the permissions Klaviyo lists. You
|
|
2848
|
+
'Approve the permissions Klaviyo lists. You come back here to pick the list your contacts should sync into.',
|
|
2849
|
+
'The connection shows Pending until you pick a list, then Active.',
|
|
2841
2850
|
'You can revoke access at any time from Klaviyo, under Integrations.'
|
|
2842
2851
|
]
|
|
2843
2852
|
|
|
@@ -2987,7 +2996,7 @@ var klaviyo = {
|
|
|
2987
2996
|
// different thing from deleting the profile.
|
|
2988
2997
|
remove : false,
|
|
2989
2998
|
|
|
2990
|
-
sync : async ( {
|
|
2999
|
+
sync : async ( { context, lead, segments, settings, suppressed, token }, { fetcher } = {} ) => {
|
|
2991
3000
|
|
|
2992
3001
|
const list = settings?.list;
|
|
2993
3002
|
|
|
@@ -3005,9 +3014,32 @@ var klaviyo = {
|
|
|
3005
3014
|
// contact knows money: order attribution is person-level and merges
|
|
3006
3015
|
// across every address a human used, so revenue and orders cannot
|
|
3007
3016
|
// exist on a single-address record.
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3017
|
+
//
|
|
3018
|
+
// READ OFF `context`, NOT the shell's `contact` prop. That prop is the
|
|
3019
|
+
// ADDRESS — a string — so `contact?.totals` was undefined on every run
|
|
3020
|
+
// this hook has ever made, and all five properties below shipped as 0
|
|
3021
|
+
// no matter what the person had entered, drawn or spent. Only a
|
|
3022
|
+
// segment.contact.add run carries the document (drawbridge-sync
|
|
3023
|
+
// lib/segment.js); lead.insert names a brand-new entrant who has no
|
|
3024
|
+
// history to send anyway.
|
|
3025
|
+
const person = context?.contact || null;
|
|
3026
|
+
const totals = person?.totals || {};
|
|
3027
|
+
|
|
3028
|
+
// UPSERT, NOT CREATE. /profiles/ is the plain create and answers a
|
|
3029
|
+
// conflict for a profile Klaviyo already holds — which is most of them,
|
|
3030
|
+
// because the whole point of the segment trigger is syncing people who
|
|
3031
|
+
// have been around long enough to earn a ranking. profile-import is
|
|
3032
|
+
// Klaviyo's documented create-or-update: 201 when it made one, 200 when
|
|
3033
|
+
// it updated one, and no conflict either way
|
|
3034
|
+
// (developers.klaviyo.com/en/reference/create_or_update_profile,
|
|
3035
|
+
// fetched 2026-09-09).
|
|
3036
|
+
//
|
|
3037
|
+
// IT MERGES, and that is why the blocks below are conditional rather
|
|
3038
|
+
// than defaulted. "Not including a field in your request will leave it
|
|
3039
|
+
// unchanged" — so omitting the totals on a lead.insert preserves
|
|
3040
|
+
// whatever the last segment run published, where sending zeros would
|
|
3041
|
+
// erase it. A property we cannot compute is one we must not write.
|
|
3042
|
+
const profile = await api$1( '/profile-import', {
|
|
3011
3043
|
fetcher,
|
|
3012
3044
|
method : 'POST',
|
|
3013
3045
|
payload : {
|
|
@@ -3016,16 +3048,36 @@ var klaviyo = {
|
|
|
3016
3048
|
email,
|
|
3017
3049
|
...( lead?.name && { first_name : String( lead.name ).trim().split( /\s+/ )[ 0 ] }),
|
|
3018
3050
|
properties : {
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3051
|
+
...( person && {
|
|
3052
|
+
drawbridge_campaigns : ( person.campaigns || [] ).length,
|
|
3053
|
+
drawbridge_draws : totals.draws || 0,
|
|
3054
|
+
drawbridge_entries : totals.entries || 0,
|
|
3055
|
+
drawbridge_orders : totals.orders || 0,
|
|
3056
|
+
// Campaign-attributed, NOT lifetime. A merchant running
|
|
3057
|
+
// Shopify already has lifetime revenue in Klaviyo through
|
|
3058
|
+
// Klaviyo's own integration; what only we can say is how
|
|
3059
|
+
// much a campaign drove. Named so the two cannot be
|
|
3060
|
+
// mistaken for one another in a segment builder.
|
|
3061
|
+
drawbridge_revenue : totals.gross || 0
|
|
3062
|
+
}),
|
|
3063
|
+
// THE DRAWBRIDGE SEGMENTS THEY ARE IN, as a list property the
|
|
3064
|
+
// merchant builds Klaviyo segments on top of. Klaviyo owns no
|
|
3065
|
+
// writable membership — its segments are computed from rules,
|
|
3066
|
+
// so a profile cannot be put in one — and this is the nearest
|
|
3067
|
+
// honest equivalent.
|
|
3068
|
+
//
|
|
3069
|
+
// WHOLESALE, WHICH IS WHAT MAKES IT SELF-HEALING. Drawbridge
|
|
3070
|
+
// segments are dynamic and nothing dispatches a leave event —
|
|
3071
|
+
// only segment.contact.add exists — so a property that was
|
|
3072
|
+
// only ever appended to would outlive the membership that
|
|
3073
|
+
// earned it. Rewriting the whole array each run means every
|
|
3074
|
+
// sync corrects whatever the last one got wrong, with no
|
|
3075
|
+
// removal path to build.
|
|
3076
|
+
//
|
|
3077
|
+
// `segments` is null when the run carried no contact document,
|
|
3078
|
+
// meaning nobody looked — different from [], which means they
|
|
3079
|
+
// are in none. Null omits the key and merge leaves it alone.
|
|
3080
|
+
...( segments && { drawbridge_segments : segments.map( ( entry ) => entry.title ).filter( Boolean ) })
|
|
3029
3081
|
}
|
|
3030
3082
|
},
|
|
3031
3083
|
type : 'profile'
|
|
@@ -3427,7 +3479,10 @@ const api = async ( path, { dc, fetcher = fetch, method = 'GET', payload, token
|
|
|
3427
3479
|
|
|
3428
3480
|
}
|
|
3429
3481
|
|
|
3430
|
-
|
|
3482
|
+
// 204 ON A TAG WRITE — nothing to parse, and asking an empty body for json
|
|
3483
|
+
// throws, which would report a change Mailchimp accepted as a failed step.
|
|
3484
|
+
// Klaviyo's file carries the same guard for the same reason.
|
|
3485
|
+
return response.status === 204 ? null : response.json();
|
|
3431
3486
|
|
|
3432
3487
|
};
|
|
3433
3488
|
|
|
@@ -3591,7 +3646,7 @@ var mailchimp = {
|
|
|
3591
3646
|
// PUT /lists/{list_id}/members/{subscriber_hash} — an UPSERT, which is
|
|
3592
3647
|
// why there is no create-or-update branch here. Quoted from Mailchimp's
|
|
3593
3648
|
// Marketing API reference for the list-members resource.
|
|
3594
|
-
sync : async ( { lead, settings, suppressed, token }, { fetcher } = {} ) => {
|
|
3649
|
+
sync : async ( { connection, lead, segments, settings, suppressed, token }, { fetcher, read } = {} ) => {
|
|
3595
3650
|
|
|
3596
3651
|
const audience = settings?.audience;
|
|
3597
3652
|
|
|
@@ -3648,6 +3703,75 @@ var mailchimp = {
|
|
|
3648
3703
|
token
|
|
3649
3704
|
});
|
|
3650
3705
|
|
|
3706
|
+
// THE DRAWBRIDGE SEGMENTS THIS PERSON IS IN, written as Mailchimp tags.
|
|
3707
|
+
//
|
|
3708
|
+
// TAGS, NOT MERGE FIELDS. A merge field has to exist on the audience
|
|
3709
|
+
// before anything can be written to it, and an unregistered one is
|
|
3710
|
+
// refused along with the whole member request — so carrying data that
|
|
3711
|
+
// way means registering fields first, which is lifecycle.register's job
|
|
3712
|
+
// and is not built. A tag needs no setup: "If a tag that does not exist
|
|
3713
|
+
// is passed in and set as 'active', a new tag will be created"
|
|
3714
|
+
// (mailchimp.com/developer/marketing/api/list-member-tags/add-or-remove-member-tags,
|
|
3715
|
+
// fetched 2026-09-09). Nothing for the merchant to prepare, so nothing
|
|
3716
|
+
// to explain in the guide.
|
|
3717
|
+
//
|
|
3718
|
+
// ACTIVE AND INACTIVE IN ONE CALL, which is what keeps this correct
|
|
3719
|
+
// over time. Drawbridge segments are dynamic, and nothing dispatches a
|
|
3720
|
+
// leave event — only segment.contact.add exists — so a tag that was
|
|
3721
|
+
// only ever added would outlive the membership behind it and the
|
|
3722
|
+
// merchant would target a condition that had stopped being true.
|
|
3723
|
+
// Sending the whole list every time, each tag active or inactive
|
|
3724
|
+
// against current membership, means each sync corrects the last.
|
|
3725
|
+
//
|
|
3726
|
+
// WRITTEN FOR A SUPPRESSED PERSON TOO. A tag is a label, not a
|
|
3727
|
+
// permission: the `status : 'unsubscribed'` above is what stops mail
|
|
3728
|
+
// reaching them, and letting their labels rot would leave the merchant
|
|
3729
|
+
// a wrong answer about who belongs to what.
|
|
3730
|
+
//
|
|
3731
|
+
// PREFIXED, so a Drawbridge tag cannot collide with one the merchant
|
|
3732
|
+
// keeps by hand, and so the inactive pass can only clear tags this
|
|
3733
|
+
// integration wrote.
|
|
3734
|
+
if( segments && read ){
|
|
3735
|
+
|
|
3736
|
+
const owned = await read.aggregate({
|
|
3737
|
+
collection : 'segment',
|
|
3738
|
+
pipeline : [
|
|
3739
|
+
{
|
|
3740
|
+
$match : {
|
|
3741
|
+
organization : connection?.organization,
|
|
3742
|
+
system : { $ne : true }
|
|
3743
|
+
}
|
|
3744
|
+
},
|
|
3745
|
+
{
|
|
3746
|
+
$project : { _id : 0, title : 1 }
|
|
3747
|
+
}
|
|
3748
|
+
]
|
|
3749
|
+
});
|
|
3750
|
+
|
|
3751
|
+
const joined = new Set( segments.map( ( entry ) => entry.title ) );
|
|
3752
|
+
|
|
3753
|
+
const tags = ( owned || [] )
|
|
3754
|
+
.map( ( entry ) => entry.title )
|
|
3755
|
+
.filter( Boolean )
|
|
3756
|
+
.map( ( title ) => ({
|
|
3757
|
+
name : 'Drawbridge: ' + title,
|
|
3758
|
+
status : joined.has( title ) ? 'active' : 'inactive'
|
|
3759
|
+
}) );
|
|
3760
|
+
|
|
3761
|
+
if( tags.length > 0 ){
|
|
3762
|
+
|
|
3763
|
+
await api( '/lists/' + audience + '/members/' + hash + '/tags', {
|
|
3764
|
+
dc : settings?.dc,
|
|
3765
|
+
fetcher,
|
|
3766
|
+
method : 'POST',
|
|
3767
|
+
payload : { tags },
|
|
3768
|
+
token
|
|
3769
|
+
});
|
|
3770
|
+
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3651
3775
|
return {
|
|
3652
3776
|
// Merged into `context` for later steps in this run.
|
|
3653
3777
|
context : { mailchimpMemberId : member?.id || hash },
|
|
@@ -4828,7 +4952,9 @@ var shopify = {
|
|
|
4828
4952
|
enqueues : [ {
|
|
4829
4953
|
data : { product : productId, providerId, shop : connection.shop },
|
|
4830
4954
|
name : 'workflow',
|
|
4831
|
-
|
|
4955
|
+
// The numeric id, not the gid: BullMQ refuses a custom id with a
|
|
4956
|
+
// colon in it, and every gid carries two.
|
|
4957
|
+
options : { jobId : 'product.workflow.shopify.' + context.id + '.' + Date.now() },
|
|
4832
4958
|
queue : 'product.shopify'
|
|
4833
4959
|
} ],
|
|
4834
4960
|
message : 'Product sync queued from Shopify webhook.',
|
|
@@ -5763,9 +5889,14 @@ var webhook = {
|
|
|
5763
5889
|
'Generate a signing secret and Drawbridge signs every request with it. Your endpoint recomputes the signature to confirm each payload genuinely came from Drawbridge before acting on it.'
|
|
5764
5890
|
],
|
|
5765
5891
|
excerpt : 'Sign outgoing webhook payloads with an HMAC secret to verify authenticity.',
|
|
5892
|
+
// SAYS WHERE THE URL GOES. Connecting only mints a secret, so a merchant
|
|
5893
|
+
// following this had nowhere to put the endpoint they came to configure —
|
|
5894
|
+
// the destination lives on the step (see steps.webhook.send.settings), and
|
|
5895
|
+
// the guide never said so.
|
|
5766
5896
|
guide : [
|
|
5767
5897
|
'Press Connect. Drawbridge generates a signing secret and shows it here.',
|
|
5768
5898
|
'Copy the secret into your own endpoint.',
|
|
5899
|
+
'Add a Send webhook step to a workflow and put your endpoint URL on it. The URL belongs to the step rather than the connection, so one connection can serve several endpoints.',
|
|
5769
5900
|
'On each request, compute HMAC-SHA256 of the raw body using the secret and compare it against the X-Drawbridge-Signature header before acting on the payload.'
|
|
5770
5901
|
]
|
|
5771
5902
|
},
|
|
@@ -334,6 +334,10 @@ const WRITE_OPERATIONS = Object.freeze([ 'create', 'delete', 'update' ]);
|
|
|
334
334
|
// payload the verified inbound body
|
|
335
335
|
// scope the granted scope string being judged
|
|
336
336
|
// secret the credential VALUE the caller resolved for verification
|
|
337
|
+
// segments the non-system segments this contact is in, as { id, title },
|
|
338
|
+
// resolved by the shell — null when the run carried no contact
|
|
339
|
+
// document, which means nobody looked, where [] means they are
|
|
340
|
+
// in none. A hook handed null leaves the vendor's copy alone
|
|
337
341
|
// settings the connection's decrypted settings
|
|
338
342
|
// step the step document
|
|
339
343
|
// suppressed the opt-out floor's answer for this lead, computed by the shell
|
|
@@ -383,7 +387,7 @@ const HOOK_PROPS = Object.freeze([
|
|
|
383
387
|
'channel', 'clientId', 'clientSecret', 'connection', 'contact', 'context',
|
|
384
388
|
'cursor', 'declaration', 'doc', 'email', 'event', 'headers', 'id', 'lead', 'limit',
|
|
385
389
|
'manifest', 'payload',
|
|
386
|
-
'scope', 'search', 'secret', 'settings', 'sort', 'step', 'suppressed',
|
|
390
|
+
'scope', 'search', 'secret', 'segments', 'settings', 'sort', 'step', 'suppressed',
|
|
387
391
|
'token', 'tokens', 'workflow'
|
|
388
392
|
]);
|
|
389
393
|
|
|
@@ -2834,10 +2838,15 @@ var klaviyo = {
|
|
|
2834
2838
|
|
|
2835
2839
|
// HOW TO CONNECT, in the merchant's words. Was `setup`, which nothing
|
|
2836
2840
|
// rendered — four useful instructions no component displayed.
|
|
2841
|
+
// NAMES THE LIST STEP, because `status` below gates on it: a grant with no
|
|
2842
|
+
// list chosen sits at Pending, and this told the merchant it would show
|
|
2843
|
+
// Active. The one required action was the one action the guide omitted, so
|
|
2844
|
+
// anybody following it landed on a connection that looked broken.
|
|
2837
2845
|
guide : [
|
|
2838
2846
|
'Press Connect. Drawbridge sends you to Klaviyo to approve access.',
|
|
2839
2847
|
'Sign in to Klaviyo if you are not already, and choose the account to connect.',
|
|
2840
|
-
'Approve the permissions Klaviyo lists. You
|
|
2848
|
+
'Approve the permissions Klaviyo lists. You come back here to pick the list your contacts should sync into.',
|
|
2849
|
+
'The connection shows Pending until you pick a list, then Active.',
|
|
2841
2850
|
'You can revoke access at any time from Klaviyo, under Integrations.'
|
|
2842
2851
|
]
|
|
2843
2852
|
|
|
@@ -2987,7 +2996,7 @@ var klaviyo = {
|
|
|
2987
2996
|
// different thing from deleting the profile.
|
|
2988
2997
|
remove : false,
|
|
2989
2998
|
|
|
2990
|
-
sync : async ( {
|
|
2999
|
+
sync : async ( { context, lead, segments, settings, suppressed, token }, { fetcher } = {} ) => {
|
|
2991
3000
|
|
|
2992
3001
|
const list = settings?.list;
|
|
2993
3002
|
|
|
@@ -3005,9 +3014,32 @@ var klaviyo = {
|
|
|
3005
3014
|
// contact knows money: order attribution is person-level and merges
|
|
3006
3015
|
// across every address a human used, so revenue and orders cannot
|
|
3007
3016
|
// exist on a single-address record.
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3017
|
+
//
|
|
3018
|
+
// READ OFF `context`, NOT the shell's `contact` prop. That prop is the
|
|
3019
|
+
// ADDRESS — a string — so `contact?.totals` was undefined on every run
|
|
3020
|
+
// this hook has ever made, and all five properties below shipped as 0
|
|
3021
|
+
// no matter what the person had entered, drawn or spent. Only a
|
|
3022
|
+
// segment.contact.add run carries the document (drawbridge-sync
|
|
3023
|
+
// lib/segment.js); lead.insert names a brand-new entrant who has no
|
|
3024
|
+
// history to send anyway.
|
|
3025
|
+
const person = context?.contact || null;
|
|
3026
|
+
const totals = person?.totals || {};
|
|
3027
|
+
|
|
3028
|
+
// UPSERT, NOT CREATE. /profiles/ is the plain create and answers a
|
|
3029
|
+
// conflict for a profile Klaviyo already holds — which is most of them,
|
|
3030
|
+
// because the whole point of the segment trigger is syncing people who
|
|
3031
|
+
// have been around long enough to earn a ranking. profile-import is
|
|
3032
|
+
// Klaviyo's documented create-or-update: 201 when it made one, 200 when
|
|
3033
|
+
// it updated one, and no conflict either way
|
|
3034
|
+
// (developers.klaviyo.com/en/reference/create_or_update_profile,
|
|
3035
|
+
// fetched 2026-09-09).
|
|
3036
|
+
//
|
|
3037
|
+
// IT MERGES, and that is why the blocks below are conditional rather
|
|
3038
|
+
// than defaulted. "Not including a field in your request will leave it
|
|
3039
|
+
// unchanged" — so omitting the totals on a lead.insert preserves
|
|
3040
|
+
// whatever the last segment run published, where sending zeros would
|
|
3041
|
+
// erase it. A property we cannot compute is one we must not write.
|
|
3042
|
+
const profile = await api$1( '/profile-import', {
|
|
3011
3043
|
fetcher,
|
|
3012
3044
|
method : 'POST',
|
|
3013
3045
|
payload : {
|
|
@@ -3016,16 +3048,36 @@ var klaviyo = {
|
|
|
3016
3048
|
email,
|
|
3017
3049
|
...( lead?.name && { first_name : String( lead.name ).trim().split( /\s+/ )[ 0 ] }),
|
|
3018
3050
|
properties : {
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3051
|
+
...( person && {
|
|
3052
|
+
drawbridge_campaigns : ( person.campaigns || [] ).length,
|
|
3053
|
+
drawbridge_draws : totals.draws || 0,
|
|
3054
|
+
drawbridge_entries : totals.entries || 0,
|
|
3055
|
+
drawbridge_orders : totals.orders || 0,
|
|
3056
|
+
// Campaign-attributed, NOT lifetime. A merchant running
|
|
3057
|
+
// Shopify already has lifetime revenue in Klaviyo through
|
|
3058
|
+
// Klaviyo's own integration; what only we can say is how
|
|
3059
|
+
// much a campaign drove. Named so the two cannot be
|
|
3060
|
+
// mistaken for one another in a segment builder.
|
|
3061
|
+
drawbridge_revenue : totals.gross || 0
|
|
3062
|
+
}),
|
|
3063
|
+
// THE DRAWBRIDGE SEGMENTS THEY ARE IN, as a list property the
|
|
3064
|
+
// merchant builds Klaviyo segments on top of. Klaviyo owns no
|
|
3065
|
+
// writable membership — its segments are computed from rules,
|
|
3066
|
+
// so a profile cannot be put in one — and this is the nearest
|
|
3067
|
+
// honest equivalent.
|
|
3068
|
+
//
|
|
3069
|
+
// WHOLESALE, WHICH IS WHAT MAKES IT SELF-HEALING. Drawbridge
|
|
3070
|
+
// segments are dynamic and nothing dispatches a leave event —
|
|
3071
|
+
// only segment.contact.add exists — so a property that was
|
|
3072
|
+
// only ever appended to would outlive the membership that
|
|
3073
|
+
// earned it. Rewriting the whole array each run means every
|
|
3074
|
+
// sync corrects whatever the last one got wrong, with no
|
|
3075
|
+
// removal path to build.
|
|
3076
|
+
//
|
|
3077
|
+
// `segments` is null when the run carried no contact document,
|
|
3078
|
+
// meaning nobody looked — different from [], which means they
|
|
3079
|
+
// are in none. Null omits the key and merge leaves it alone.
|
|
3080
|
+
...( segments && { drawbridge_segments : segments.map( ( entry ) => entry.title ).filter( Boolean ) })
|
|
3029
3081
|
}
|
|
3030
3082
|
},
|
|
3031
3083
|
type : 'profile'
|
|
@@ -3427,7 +3479,10 @@ const api = async ( path, { dc, fetcher = fetch, method = 'GET', payload, token
|
|
|
3427
3479
|
|
|
3428
3480
|
}
|
|
3429
3481
|
|
|
3430
|
-
|
|
3482
|
+
// 204 ON A TAG WRITE — nothing to parse, and asking an empty body for json
|
|
3483
|
+
// throws, which would report a change Mailchimp accepted as a failed step.
|
|
3484
|
+
// Klaviyo's file carries the same guard for the same reason.
|
|
3485
|
+
return response.status === 204 ? null : response.json();
|
|
3431
3486
|
|
|
3432
3487
|
};
|
|
3433
3488
|
|
|
@@ -3591,7 +3646,7 @@ var mailchimp = {
|
|
|
3591
3646
|
// PUT /lists/{list_id}/members/{subscriber_hash} — an UPSERT, which is
|
|
3592
3647
|
// why there is no create-or-update branch here. Quoted from Mailchimp's
|
|
3593
3648
|
// Marketing API reference for the list-members resource.
|
|
3594
|
-
sync : async ( { lead, settings, suppressed, token }, { fetcher } = {} ) => {
|
|
3649
|
+
sync : async ( { connection, lead, segments, settings, suppressed, token }, { fetcher, read } = {} ) => {
|
|
3595
3650
|
|
|
3596
3651
|
const audience = settings?.audience;
|
|
3597
3652
|
|
|
@@ -3648,6 +3703,75 @@ var mailchimp = {
|
|
|
3648
3703
|
token
|
|
3649
3704
|
});
|
|
3650
3705
|
|
|
3706
|
+
// THE DRAWBRIDGE SEGMENTS THIS PERSON IS IN, written as Mailchimp tags.
|
|
3707
|
+
//
|
|
3708
|
+
// TAGS, NOT MERGE FIELDS. A merge field has to exist on the audience
|
|
3709
|
+
// before anything can be written to it, and an unregistered one is
|
|
3710
|
+
// refused along with the whole member request — so carrying data that
|
|
3711
|
+
// way means registering fields first, which is lifecycle.register's job
|
|
3712
|
+
// and is not built. A tag needs no setup: "If a tag that does not exist
|
|
3713
|
+
// is passed in and set as 'active', a new tag will be created"
|
|
3714
|
+
// (mailchimp.com/developer/marketing/api/list-member-tags/add-or-remove-member-tags,
|
|
3715
|
+
// fetched 2026-09-09). Nothing for the merchant to prepare, so nothing
|
|
3716
|
+
// to explain in the guide.
|
|
3717
|
+
//
|
|
3718
|
+
// ACTIVE AND INACTIVE IN ONE CALL, which is what keeps this correct
|
|
3719
|
+
// over time. Drawbridge segments are dynamic, and nothing dispatches a
|
|
3720
|
+
// leave event — only segment.contact.add exists — so a tag that was
|
|
3721
|
+
// only ever added would outlive the membership behind it and the
|
|
3722
|
+
// merchant would target a condition that had stopped being true.
|
|
3723
|
+
// Sending the whole list every time, each tag active or inactive
|
|
3724
|
+
// against current membership, means each sync corrects the last.
|
|
3725
|
+
//
|
|
3726
|
+
// WRITTEN FOR A SUPPRESSED PERSON TOO. A tag is a label, not a
|
|
3727
|
+
// permission: the `status : 'unsubscribed'` above is what stops mail
|
|
3728
|
+
// reaching them, and letting their labels rot would leave the merchant
|
|
3729
|
+
// a wrong answer about who belongs to what.
|
|
3730
|
+
//
|
|
3731
|
+
// PREFIXED, so a Drawbridge tag cannot collide with one the merchant
|
|
3732
|
+
// keeps by hand, and so the inactive pass can only clear tags this
|
|
3733
|
+
// integration wrote.
|
|
3734
|
+
if( segments && read ){
|
|
3735
|
+
|
|
3736
|
+
const owned = await read.aggregate({
|
|
3737
|
+
collection : 'segment',
|
|
3738
|
+
pipeline : [
|
|
3739
|
+
{
|
|
3740
|
+
$match : {
|
|
3741
|
+
organization : connection?.organization,
|
|
3742
|
+
system : { $ne : true }
|
|
3743
|
+
}
|
|
3744
|
+
},
|
|
3745
|
+
{
|
|
3746
|
+
$project : { _id : 0, title : 1 }
|
|
3747
|
+
}
|
|
3748
|
+
]
|
|
3749
|
+
});
|
|
3750
|
+
|
|
3751
|
+
const joined = new Set( segments.map( ( entry ) => entry.title ) );
|
|
3752
|
+
|
|
3753
|
+
const tags = ( owned || [] )
|
|
3754
|
+
.map( ( entry ) => entry.title )
|
|
3755
|
+
.filter( Boolean )
|
|
3756
|
+
.map( ( title ) => ({
|
|
3757
|
+
name : 'Drawbridge: ' + title,
|
|
3758
|
+
status : joined.has( title ) ? 'active' : 'inactive'
|
|
3759
|
+
}) );
|
|
3760
|
+
|
|
3761
|
+
if( tags.length > 0 ){
|
|
3762
|
+
|
|
3763
|
+
await api( '/lists/' + audience + '/members/' + hash + '/tags', {
|
|
3764
|
+
dc : settings?.dc,
|
|
3765
|
+
fetcher,
|
|
3766
|
+
method : 'POST',
|
|
3767
|
+
payload : { tags },
|
|
3768
|
+
token
|
|
3769
|
+
});
|
|
3770
|
+
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3651
3775
|
return {
|
|
3652
3776
|
// Merged into `context` for later steps in this run.
|
|
3653
3777
|
context : { mailchimpMemberId : member?.id || hash },
|
|
@@ -4828,7 +4952,9 @@ var shopify = {
|
|
|
4828
4952
|
enqueues : [ {
|
|
4829
4953
|
data : { product : productId, providerId, shop : connection.shop },
|
|
4830
4954
|
name : 'workflow',
|
|
4831
|
-
|
|
4955
|
+
// The numeric id, not the gid: BullMQ refuses a custom id with a
|
|
4956
|
+
// colon in it, and every gid carries two.
|
|
4957
|
+
options : { jobId : 'product.workflow.shopify.' + context.id + '.' + Date.now() },
|
|
4832
4958
|
queue : 'product.shopify'
|
|
4833
4959
|
} ],
|
|
4834
4960
|
message : 'Product sync queued from Shopify webhook.',
|
|
@@ -5763,9 +5889,14 @@ var webhook = {
|
|
|
5763
5889
|
'Generate a signing secret and Drawbridge signs every request with it. Your endpoint recomputes the signature to confirm each payload genuinely came from Drawbridge before acting on it.'
|
|
5764
5890
|
],
|
|
5765
5891
|
excerpt : 'Sign outgoing webhook payloads with an HMAC secret to verify authenticity.',
|
|
5892
|
+
// SAYS WHERE THE URL GOES. Connecting only mints a secret, so a merchant
|
|
5893
|
+
// following this had nowhere to put the endpoint they came to configure —
|
|
5894
|
+
// the destination lives on the step (see steps.webhook.send.settings), and
|
|
5895
|
+
// the guide never said so.
|
|
5766
5896
|
guide : [
|
|
5767
5897
|
'Press Connect. Drawbridge generates a signing secret and shows it here.',
|
|
5768
5898
|
'Copy the secret into your own endpoint.',
|
|
5899
|
+
'Add a Send webhook step to a workflow and put your endpoint URL on it. The URL belongs to the step rather than the connection, so one connection can serve several endpoints.',
|
|
5769
5900
|
'On each request, compute HMAC-SHA256 of the raw body using the secret and compare it against the X-Drawbridge-Signature header before acting on the payload.'
|
|
5770
5901
|
]
|
|
5771
5902
|
},
|
|
@@ -199,6 +199,7 @@ var HOOK_PROPS = Object.freeze([
|
|
|
199
199
|
"scope",
|
|
200
200
|
"search",
|
|
201
201
|
"secret",
|
|
202
|
+
"segments",
|
|
202
203
|
"settings",
|
|
203
204
|
"sort",
|
|
204
205
|
"step",
|
|
@@ -2592,10 +2593,15 @@ var klaviyo_default2 = {
|
|
|
2592
2593
|
excerpt: "Sync the contacts your campaigns collect into a Klaviyo list.",
|
|
2593
2594
|
// HOW TO CONNECT, in the merchant's words. Was `setup`, which nothing
|
|
2594
2595
|
// rendered — four useful instructions no component displayed.
|
|
2596
|
+
// NAMES THE LIST STEP, because `status` below gates on it: a grant with no
|
|
2597
|
+
// list chosen sits at Pending, and this told the merchant it would show
|
|
2598
|
+
// Active. The one required action was the one action the guide omitted, so
|
|
2599
|
+
// anybody following it landed on a connection that looked broken.
|
|
2595
2600
|
guide: [
|
|
2596
2601
|
"Press Connect. Drawbridge sends you to Klaviyo to approve access.",
|
|
2597
2602
|
"Sign in to Klaviyo if you are not already, and choose the account to connect.",
|
|
2598
|
-
"Approve the permissions Klaviyo lists. You
|
|
2603
|
+
"Approve the permissions Klaviyo lists. You come back here to pick the list your contacts should sync into.",
|
|
2604
|
+
"The connection shows Pending until you pick a list, then Active.",
|
|
2599
2605
|
"You can revoke access at any time from Klaviyo, under Integrations."
|
|
2600
2606
|
]
|
|
2601
2607
|
},
|
|
@@ -2717,14 +2723,15 @@ var klaviyo_default2 = {
|
|
|
2717
2723
|
// Not yet. Suppression syncs an opt-out as unsubscribed, which is a
|
|
2718
2724
|
// different thing from deleting the profile.
|
|
2719
2725
|
remove: false,
|
|
2720
|
-
sync: async ({
|
|
2726
|
+
sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
|
|
2721
2727
|
var _a, _b, _c;
|
|
2722
2728
|
const list = settings == null ? void 0 : settings.list;
|
|
2723
2729
|
if (!list) return { message: "No Klaviyo list is chosen for this connection.", skipped: true };
|
|
2724
2730
|
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);
|
|
2725
2731
|
if (!email) return { message: "That lead has no email address to sync.", skipped: true };
|
|
2726
|
-
const
|
|
2727
|
-
const
|
|
2732
|
+
const person = (context == null ? void 0 : context.contact) || null;
|
|
2733
|
+
const totals = (person == null ? void 0 : person.totals) || {};
|
|
2734
|
+
const profile = await api2("/profile-import", {
|
|
2728
2735
|
fetcher,
|
|
2729
2736
|
method: "POST",
|
|
2730
2737
|
payload: {
|
|
@@ -2733,16 +2740,36 @@ var klaviyo_default2 = {
|
|
|
2733
2740
|
email,
|
|
2734
2741
|
...(lead == null ? void 0 : lead.name) && { first_name: String(lead.name).trim().split(/\s+/)[0] },
|
|
2735
2742
|
properties: {
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2743
|
+
...person && {
|
|
2744
|
+
drawbridge_campaigns: (person.campaigns || []).length,
|
|
2745
|
+
drawbridge_draws: totals.draws || 0,
|
|
2746
|
+
drawbridge_entries: totals.entries || 0,
|
|
2747
|
+
drawbridge_orders: totals.orders || 0,
|
|
2748
|
+
// Campaign-attributed, NOT lifetime. A merchant running
|
|
2749
|
+
// Shopify already has lifetime revenue in Klaviyo through
|
|
2750
|
+
// Klaviyo's own integration; what only we can say is how
|
|
2751
|
+
// much a campaign drove. Named so the two cannot be
|
|
2752
|
+
// mistaken for one another in a segment builder.
|
|
2753
|
+
drawbridge_revenue: totals.gross || 0
|
|
2754
|
+
},
|
|
2755
|
+
// THE DRAWBRIDGE SEGMENTS THEY ARE IN, as a list property the
|
|
2756
|
+
// merchant builds Klaviyo segments on top of. Klaviyo owns no
|
|
2757
|
+
// writable membership — its segments are computed from rules,
|
|
2758
|
+
// so a profile cannot be put in one — and this is the nearest
|
|
2759
|
+
// honest equivalent.
|
|
2760
|
+
//
|
|
2761
|
+
// WHOLESALE, WHICH IS WHAT MAKES IT SELF-HEALING. Drawbridge
|
|
2762
|
+
// segments are dynamic and nothing dispatches a leave event —
|
|
2763
|
+
// only segment.contact.add exists — so a property that was
|
|
2764
|
+
// only ever appended to would outlive the membership that
|
|
2765
|
+
// earned it. Rewriting the whole array each run means every
|
|
2766
|
+
// sync corrects whatever the last one got wrong, with no
|
|
2767
|
+
// removal path to build.
|
|
2768
|
+
//
|
|
2769
|
+
// `segments` is null when the run carried no contact document,
|
|
2770
|
+
// meaning nobody looked — different from [], which means they
|
|
2771
|
+
// are in none. Null omits the key and merge leaves it alone.
|
|
2772
|
+
...segments && { drawbridge_segments: segments.map((entry) => entry.title).filter(Boolean) }
|
|
2746
2773
|
}
|
|
2747
2774
|
},
|
|
2748
2775
|
type: "profile"
|
|
@@ -3034,7 +3061,7 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
|
|
|
3034
3061
|
{ status: response.status }
|
|
3035
3062
|
);
|
|
3036
3063
|
}
|
|
3037
|
-
return response.json();
|
|
3064
|
+
return response.status === 204 ? null : response.json();
|
|
3038
3065
|
};
|
|
3039
3066
|
var subscriberHash = (email) => createHash2("md5").update(String(email).trim().toLowerCase()).digest("hex");
|
|
3040
3067
|
var mailchimp_default2 = {
|
|
@@ -3168,7 +3195,7 @@ var mailchimp_default2 = {
|
|
|
3168
3195
|
// PUT /lists/{list_id}/members/{subscriber_hash} — an UPSERT, which is
|
|
3169
3196
|
// why there is no create-or-update branch here. Quoted from Mailchimp's
|
|
3170
3197
|
// Marketing API reference for the list-members resource.
|
|
3171
|
-
sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
|
|
3198
|
+
sync: async ({ connection: connection2, lead, segments, settings, suppressed, token }, { fetcher, read } = {}) => {
|
|
3172
3199
|
var _a, _b;
|
|
3173
3200
|
const audience = settings == null ? void 0 : settings.audience;
|
|
3174
3201
|
if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
|
|
@@ -3194,6 +3221,36 @@ var mailchimp_default2 = {
|
|
|
3194
3221
|
},
|
|
3195
3222
|
token
|
|
3196
3223
|
});
|
|
3224
|
+
if (segments && read) {
|
|
3225
|
+
const owned = await read.aggregate({
|
|
3226
|
+
collection: "segment",
|
|
3227
|
+
pipeline: [
|
|
3228
|
+
{
|
|
3229
|
+
$match: {
|
|
3230
|
+
organization: connection2 == null ? void 0 : connection2.organization,
|
|
3231
|
+
system: { $ne: true }
|
|
3232
|
+
}
|
|
3233
|
+
},
|
|
3234
|
+
{
|
|
3235
|
+
$project: { _id: 0, title: 1 }
|
|
3236
|
+
}
|
|
3237
|
+
]
|
|
3238
|
+
});
|
|
3239
|
+
const joined = new Set(segments.map((entry) => entry.title));
|
|
3240
|
+
const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
|
|
3241
|
+
name: "Drawbridge: " + title,
|
|
3242
|
+
status: joined.has(title) ? "active" : "inactive"
|
|
3243
|
+
}));
|
|
3244
|
+
if (tags.length > 0) {
|
|
3245
|
+
await api3("/lists/" + audience + "/members/" + hash + "/tags", {
|
|
3246
|
+
dc: settings == null ? void 0 : settings.dc,
|
|
3247
|
+
fetcher,
|
|
3248
|
+
method: "POST",
|
|
3249
|
+
payload: { tags },
|
|
3250
|
+
token
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3197
3254
|
return {
|
|
3198
3255
|
// Merged into `context` for later steps in this run.
|
|
3199
3256
|
context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
|
|
@@ -4054,7 +4111,9 @@ var shopify_default2 = {
|
|
|
4054
4111
|
enqueues: [{
|
|
4055
4112
|
data: { product: productId, providerId, shop: connection2.shop },
|
|
4056
4113
|
name: "workflow",
|
|
4057
|
-
|
|
4114
|
+
// The numeric id, not the gid: BullMQ refuses a custom id with a
|
|
4115
|
+
// colon in it, and every gid carries two.
|
|
4116
|
+
options: { jobId: "product.workflow.shopify." + context.id + "." + Date.now() },
|
|
4058
4117
|
queue: "product.shopify"
|
|
4059
4118
|
}],
|
|
4060
4119
|
message: "Product sync queued from Shopify webhook.",
|
|
@@ -4860,9 +4919,14 @@ var webhook_default = {
|
|
|
4860
4919
|
"Generate a signing secret and Drawbridge signs every request with it. Your endpoint recomputes the signature to confirm each payload genuinely came from Drawbridge before acting on it."
|
|
4861
4920
|
],
|
|
4862
4921
|
excerpt: "Sign outgoing webhook payloads with an HMAC secret to verify authenticity.",
|
|
4922
|
+
// SAYS WHERE THE URL GOES. Connecting only mints a secret, so a merchant
|
|
4923
|
+
// following this had nowhere to put the endpoint they came to configure —
|
|
4924
|
+
// the destination lives on the step (see steps.webhook.send.settings), and
|
|
4925
|
+
// the guide never said so.
|
|
4863
4926
|
guide: [
|
|
4864
4927
|
"Press Connect. Drawbridge generates a signing secret and shows it here.",
|
|
4865
4928
|
"Copy the secret into your own endpoint.",
|
|
4929
|
+
"Add a Send webhook step to a workflow and put your endpoint URL on it. The URL belongs to the step rather than the connection, so one connection can serve several endpoints.",
|
|
4866
4930
|
"On each request, compute HMAC-SHA256 of the raw body using the secret and compare it against the X-Drawbridge-Signature header before acting on the payload."
|
|
4867
4931
|
]
|
|
4868
4932
|
},
|
package/dist/providers.cjs
CHANGED
|
@@ -245,6 +245,7 @@ var HOOK_PROPS = Object.freeze([
|
|
|
245
245
|
"scope",
|
|
246
246
|
"search",
|
|
247
247
|
"secret",
|
|
248
|
+
"segments",
|
|
248
249
|
"settings",
|
|
249
250
|
"sort",
|
|
250
251
|
"step",
|
|
@@ -2574,10 +2575,15 @@ var klaviyo_default2 = {
|
|
|
2574
2575
|
excerpt: "Sync the contacts your campaigns collect into a Klaviyo list.",
|
|
2575
2576
|
// HOW TO CONNECT, in the merchant's words. Was `setup`, which nothing
|
|
2576
2577
|
// rendered — four useful instructions no component displayed.
|
|
2578
|
+
// NAMES THE LIST STEP, because `status` below gates on it: a grant with no
|
|
2579
|
+
// list chosen sits at Pending, and this told the merchant it would show
|
|
2580
|
+
// Active. The one required action was the one action the guide omitted, so
|
|
2581
|
+
// anybody following it landed on a connection that looked broken.
|
|
2577
2582
|
guide: [
|
|
2578
2583
|
"Press Connect. Drawbridge sends you to Klaviyo to approve access.",
|
|
2579
2584
|
"Sign in to Klaviyo if you are not already, and choose the account to connect.",
|
|
2580
|
-
"Approve the permissions Klaviyo lists. You
|
|
2585
|
+
"Approve the permissions Klaviyo lists. You come back here to pick the list your contacts should sync into.",
|
|
2586
|
+
"The connection shows Pending until you pick a list, then Active.",
|
|
2581
2587
|
"You can revoke access at any time from Klaviyo, under Integrations."
|
|
2582
2588
|
]
|
|
2583
2589
|
},
|
|
@@ -2699,14 +2705,15 @@ var klaviyo_default2 = {
|
|
|
2699
2705
|
// Not yet. Suppression syncs an opt-out as unsubscribed, which is a
|
|
2700
2706
|
// different thing from deleting the profile.
|
|
2701
2707
|
remove: false,
|
|
2702
|
-
sync: async ({
|
|
2708
|
+
sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
|
|
2703
2709
|
var _a, _b, _c;
|
|
2704
2710
|
const list = settings == null ? void 0 : settings.list;
|
|
2705
2711
|
if (!list) return { message: "No Klaviyo list is chosen for this connection.", skipped: true };
|
|
2706
2712
|
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);
|
|
2707
2713
|
if (!email) return { message: "That lead has no email address to sync.", skipped: true };
|
|
2708
|
-
const
|
|
2709
|
-
const
|
|
2714
|
+
const person = (context == null ? void 0 : context.contact) || null;
|
|
2715
|
+
const totals = (person == null ? void 0 : person.totals) || {};
|
|
2716
|
+
const profile = await api2("/profile-import", {
|
|
2710
2717
|
fetcher,
|
|
2711
2718
|
method: "POST",
|
|
2712
2719
|
payload: {
|
|
@@ -2715,16 +2722,36 @@ var klaviyo_default2 = {
|
|
|
2715
2722
|
email,
|
|
2716
2723
|
...(lead == null ? void 0 : lead.name) && { first_name: String(lead.name).trim().split(/\s+/)[0] },
|
|
2717
2724
|
properties: {
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2725
|
+
...person && {
|
|
2726
|
+
drawbridge_campaigns: (person.campaigns || []).length,
|
|
2727
|
+
drawbridge_draws: totals.draws || 0,
|
|
2728
|
+
drawbridge_entries: totals.entries || 0,
|
|
2729
|
+
drawbridge_orders: totals.orders || 0,
|
|
2730
|
+
// Campaign-attributed, NOT lifetime. A merchant running
|
|
2731
|
+
// Shopify already has lifetime revenue in Klaviyo through
|
|
2732
|
+
// Klaviyo's own integration; what only we can say is how
|
|
2733
|
+
// much a campaign drove. Named so the two cannot be
|
|
2734
|
+
// mistaken for one another in a segment builder.
|
|
2735
|
+
drawbridge_revenue: totals.gross || 0
|
|
2736
|
+
},
|
|
2737
|
+
// THE DRAWBRIDGE SEGMENTS THEY ARE IN, as a list property the
|
|
2738
|
+
// merchant builds Klaviyo segments on top of. Klaviyo owns no
|
|
2739
|
+
// writable membership — its segments are computed from rules,
|
|
2740
|
+
// so a profile cannot be put in one — and this is the nearest
|
|
2741
|
+
// honest equivalent.
|
|
2742
|
+
//
|
|
2743
|
+
// WHOLESALE, WHICH IS WHAT MAKES IT SELF-HEALING. Drawbridge
|
|
2744
|
+
// segments are dynamic and nothing dispatches a leave event —
|
|
2745
|
+
// only segment.contact.add exists — so a property that was
|
|
2746
|
+
// only ever appended to would outlive the membership that
|
|
2747
|
+
// earned it. Rewriting the whole array each run means every
|
|
2748
|
+
// sync corrects whatever the last one got wrong, with no
|
|
2749
|
+
// removal path to build.
|
|
2750
|
+
//
|
|
2751
|
+
// `segments` is null when the run carried no contact document,
|
|
2752
|
+
// meaning nobody looked — different from [], which means they
|
|
2753
|
+
// are in none. Null omits the key and merge leaves it alone.
|
|
2754
|
+
...segments && { drawbridge_segments: segments.map((entry) => entry.title).filter(Boolean) }
|
|
2728
2755
|
}
|
|
2729
2756
|
},
|
|
2730
2757
|
type: "profile"
|
|
@@ -3016,7 +3043,7 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
|
|
|
3016
3043
|
{ status: response.status }
|
|
3017
3044
|
);
|
|
3018
3045
|
}
|
|
3019
|
-
return response.json();
|
|
3046
|
+
return response.status === 204 ? null : response.json();
|
|
3020
3047
|
};
|
|
3021
3048
|
var subscriberHash = (email) => (0, import_node_crypto3.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
|
|
3022
3049
|
var mailchimp_default2 = {
|
|
@@ -3150,7 +3177,7 @@ var mailchimp_default2 = {
|
|
|
3150
3177
|
// PUT /lists/{list_id}/members/{subscriber_hash} — an UPSERT, which is
|
|
3151
3178
|
// why there is no create-or-update branch here. Quoted from Mailchimp's
|
|
3152
3179
|
// Marketing API reference for the list-members resource.
|
|
3153
|
-
sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
|
|
3180
|
+
sync: async ({ connection: connection2, lead, segments, settings, suppressed, token }, { fetcher, read } = {}) => {
|
|
3154
3181
|
var _a, _b;
|
|
3155
3182
|
const audience = settings == null ? void 0 : settings.audience;
|
|
3156
3183
|
if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
|
|
@@ -3176,6 +3203,36 @@ var mailchimp_default2 = {
|
|
|
3176
3203
|
},
|
|
3177
3204
|
token
|
|
3178
3205
|
});
|
|
3206
|
+
if (segments && read) {
|
|
3207
|
+
const owned = await read.aggregate({
|
|
3208
|
+
collection: "segment",
|
|
3209
|
+
pipeline: [
|
|
3210
|
+
{
|
|
3211
|
+
$match: {
|
|
3212
|
+
organization: connection2 == null ? void 0 : connection2.organization,
|
|
3213
|
+
system: { $ne: true }
|
|
3214
|
+
}
|
|
3215
|
+
},
|
|
3216
|
+
{
|
|
3217
|
+
$project: { _id: 0, title: 1 }
|
|
3218
|
+
}
|
|
3219
|
+
]
|
|
3220
|
+
});
|
|
3221
|
+
const joined = new Set(segments.map((entry) => entry.title));
|
|
3222
|
+
const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
|
|
3223
|
+
name: "Drawbridge: " + title,
|
|
3224
|
+
status: joined.has(title) ? "active" : "inactive"
|
|
3225
|
+
}));
|
|
3226
|
+
if (tags.length > 0) {
|
|
3227
|
+
await api3("/lists/" + audience + "/members/" + hash + "/tags", {
|
|
3228
|
+
dc: settings == null ? void 0 : settings.dc,
|
|
3229
|
+
fetcher,
|
|
3230
|
+
method: "POST",
|
|
3231
|
+
payload: { tags },
|
|
3232
|
+
token
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3179
3236
|
return {
|
|
3180
3237
|
// Merged into `context` for later steps in this run.
|
|
3181
3238
|
context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
|
|
@@ -4055,7 +4112,9 @@ var shopify_default2 = {
|
|
|
4055
4112
|
enqueues: [{
|
|
4056
4113
|
data: { product: productId, providerId, shop: connection2.shop },
|
|
4057
4114
|
name: "workflow",
|
|
4058
|
-
|
|
4115
|
+
// The numeric id, not the gid: BullMQ refuses a custom id with a
|
|
4116
|
+
// colon in it, and every gid carries two.
|
|
4117
|
+
options: { jobId: "product.workflow.shopify." + context.id + "." + Date.now() },
|
|
4059
4118
|
queue: "product.shopify"
|
|
4060
4119
|
}],
|
|
4061
4120
|
message: "Product sync queued from Shopify webhook.",
|
|
@@ -4861,9 +4920,14 @@ var webhook_default = {
|
|
|
4861
4920
|
"Generate a signing secret and Drawbridge signs every request with it. Your endpoint recomputes the signature to confirm each payload genuinely came from Drawbridge before acting on it."
|
|
4862
4921
|
],
|
|
4863
4922
|
excerpt: "Sign outgoing webhook payloads with an HMAC secret to verify authenticity.",
|
|
4923
|
+
// SAYS WHERE THE URL GOES. Connecting only mints a secret, so a merchant
|
|
4924
|
+
// following this had nowhere to put the endpoint they came to configure —
|
|
4925
|
+
// the destination lives on the step (see steps.webhook.send.settings), and
|
|
4926
|
+
// the guide never said so.
|
|
4864
4927
|
guide: [
|
|
4865
4928
|
"Press Connect. Drawbridge generates a signing secret and shows it here.",
|
|
4866
4929
|
"Copy the secret into your own endpoint.",
|
|
4930
|
+
"Add a Send webhook step to a workflow and put your endpoint URL on it. The URL belongs to the step rather than the connection, so one connection can serve several endpoints.",
|
|
4867
4931
|
"On each request, compute HMAC-SHA256 of the raw body using the secret and compare it against the X-Drawbridge-Signature header before acting on the payload."
|
|
4868
4932
|
]
|
|
4869
4933
|
},
|
package/dist/providers.js
CHANGED
|
@@ -199,6 +199,7 @@ var HOOK_PROPS = Object.freeze([
|
|
|
199
199
|
"scope",
|
|
200
200
|
"search",
|
|
201
201
|
"secret",
|
|
202
|
+
"segments",
|
|
202
203
|
"settings",
|
|
203
204
|
"sort",
|
|
204
205
|
"step",
|
|
@@ -2528,10 +2529,15 @@ var klaviyo_default2 = {
|
|
|
2528
2529
|
excerpt: "Sync the contacts your campaigns collect into a Klaviyo list.",
|
|
2529
2530
|
// HOW TO CONNECT, in the merchant's words. Was `setup`, which nothing
|
|
2530
2531
|
// rendered — four useful instructions no component displayed.
|
|
2532
|
+
// NAMES THE LIST STEP, because `status` below gates on it: a grant with no
|
|
2533
|
+
// list chosen sits at Pending, and this told the merchant it would show
|
|
2534
|
+
// Active. The one required action was the one action the guide omitted, so
|
|
2535
|
+
// anybody following it landed on a connection that looked broken.
|
|
2531
2536
|
guide: [
|
|
2532
2537
|
"Press Connect. Drawbridge sends you to Klaviyo to approve access.",
|
|
2533
2538
|
"Sign in to Klaviyo if you are not already, and choose the account to connect.",
|
|
2534
|
-
"Approve the permissions Klaviyo lists. You
|
|
2539
|
+
"Approve the permissions Klaviyo lists. You come back here to pick the list your contacts should sync into.",
|
|
2540
|
+
"The connection shows Pending until you pick a list, then Active.",
|
|
2535
2541
|
"You can revoke access at any time from Klaviyo, under Integrations."
|
|
2536
2542
|
]
|
|
2537
2543
|
},
|
|
@@ -2653,14 +2659,15 @@ var klaviyo_default2 = {
|
|
|
2653
2659
|
// Not yet. Suppression syncs an opt-out as unsubscribed, which is a
|
|
2654
2660
|
// different thing from deleting the profile.
|
|
2655
2661
|
remove: false,
|
|
2656
|
-
sync: async ({
|
|
2662
|
+
sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
|
|
2657
2663
|
var _a, _b, _c;
|
|
2658
2664
|
const list = settings == null ? void 0 : settings.list;
|
|
2659
2665
|
if (!list) return { message: "No Klaviyo list is chosen for this connection.", skipped: true };
|
|
2660
2666
|
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);
|
|
2661
2667
|
if (!email) return { message: "That lead has no email address to sync.", skipped: true };
|
|
2662
|
-
const
|
|
2663
|
-
const
|
|
2668
|
+
const person = (context == null ? void 0 : context.contact) || null;
|
|
2669
|
+
const totals = (person == null ? void 0 : person.totals) || {};
|
|
2670
|
+
const profile = await api2("/profile-import", {
|
|
2664
2671
|
fetcher,
|
|
2665
2672
|
method: "POST",
|
|
2666
2673
|
payload: {
|
|
@@ -2669,16 +2676,36 @@ var klaviyo_default2 = {
|
|
|
2669
2676
|
email,
|
|
2670
2677
|
...(lead == null ? void 0 : lead.name) && { first_name: String(lead.name).trim().split(/\s+/)[0] },
|
|
2671
2678
|
properties: {
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2679
|
+
...person && {
|
|
2680
|
+
drawbridge_campaigns: (person.campaigns || []).length,
|
|
2681
|
+
drawbridge_draws: totals.draws || 0,
|
|
2682
|
+
drawbridge_entries: totals.entries || 0,
|
|
2683
|
+
drawbridge_orders: totals.orders || 0,
|
|
2684
|
+
// Campaign-attributed, NOT lifetime. A merchant running
|
|
2685
|
+
// Shopify already has lifetime revenue in Klaviyo through
|
|
2686
|
+
// Klaviyo's own integration; what only we can say is how
|
|
2687
|
+
// much a campaign drove. Named so the two cannot be
|
|
2688
|
+
// mistaken for one another in a segment builder.
|
|
2689
|
+
drawbridge_revenue: totals.gross || 0
|
|
2690
|
+
},
|
|
2691
|
+
// THE DRAWBRIDGE SEGMENTS THEY ARE IN, as a list property the
|
|
2692
|
+
// merchant builds Klaviyo segments on top of. Klaviyo owns no
|
|
2693
|
+
// writable membership — its segments are computed from rules,
|
|
2694
|
+
// so a profile cannot be put in one — and this is the nearest
|
|
2695
|
+
// honest equivalent.
|
|
2696
|
+
//
|
|
2697
|
+
// WHOLESALE, WHICH IS WHAT MAKES IT SELF-HEALING. Drawbridge
|
|
2698
|
+
// segments are dynamic and nothing dispatches a leave event —
|
|
2699
|
+
// only segment.contact.add exists — so a property that was
|
|
2700
|
+
// only ever appended to would outlive the membership that
|
|
2701
|
+
// earned it. Rewriting the whole array each run means every
|
|
2702
|
+
// sync corrects whatever the last one got wrong, with no
|
|
2703
|
+
// removal path to build.
|
|
2704
|
+
//
|
|
2705
|
+
// `segments` is null when the run carried no contact document,
|
|
2706
|
+
// meaning nobody looked — different from [], which means they
|
|
2707
|
+
// are in none. Null omits the key and merge leaves it alone.
|
|
2708
|
+
...segments && { drawbridge_segments: segments.map((entry) => entry.title).filter(Boolean) }
|
|
2682
2709
|
}
|
|
2683
2710
|
},
|
|
2684
2711
|
type: "profile"
|
|
@@ -2970,7 +2997,7 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
|
|
|
2970
2997
|
{ status: response.status }
|
|
2971
2998
|
);
|
|
2972
2999
|
}
|
|
2973
|
-
return response.json();
|
|
3000
|
+
return response.status === 204 ? null : response.json();
|
|
2974
3001
|
};
|
|
2975
3002
|
var subscriberHash = (email) => createHash2("md5").update(String(email).trim().toLowerCase()).digest("hex");
|
|
2976
3003
|
var mailchimp_default2 = {
|
|
@@ -3104,7 +3131,7 @@ var mailchimp_default2 = {
|
|
|
3104
3131
|
// PUT /lists/{list_id}/members/{subscriber_hash} — an UPSERT, which is
|
|
3105
3132
|
// why there is no create-or-update branch here. Quoted from Mailchimp's
|
|
3106
3133
|
// Marketing API reference for the list-members resource.
|
|
3107
|
-
sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
|
|
3134
|
+
sync: async ({ connection: connection2, lead, segments, settings, suppressed, token }, { fetcher, read } = {}) => {
|
|
3108
3135
|
var _a, _b;
|
|
3109
3136
|
const audience = settings == null ? void 0 : settings.audience;
|
|
3110
3137
|
if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
|
|
@@ -3130,6 +3157,36 @@ var mailchimp_default2 = {
|
|
|
3130
3157
|
},
|
|
3131
3158
|
token
|
|
3132
3159
|
});
|
|
3160
|
+
if (segments && read) {
|
|
3161
|
+
const owned = await read.aggregate({
|
|
3162
|
+
collection: "segment",
|
|
3163
|
+
pipeline: [
|
|
3164
|
+
{
|
|
3165
|
+
$match: {
|
|
3166
|
+
organization: connection2 == null ? void 0 : connection2.organization,
|
|
3167
|
+
system: { $ne: true }
|
|
3168
|
+
}
|
|
3169
|
+
},
|
|
3170
|
+
{
|
|
3171
|
+
$project: { _id: 0, title: 1 }
|
|
3172
|
+
}
|
|
3173
|
+
]
|
|
3174
|
+
});
|
|
3175
|
+
const joined = new Set(segments.map((entry) => entry.title));
|
|
3176
|
+
const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
|
|
3177
|
+
name: "Drawbridge: " + title,
|
|
3178
|
+
status: joined.has(title) ? "active" : "inactive"
|
|
3179
|
+
}));
|
|
3180
|
+
if (tags.length > 0) {
|
|
3181
|
+
await api3("/lists/" + audience + "/members/" + hash + "/tags", {
|
|
3182
|
+
dc: settings == null ? void 0 : settings.dc,
|
|
3183
|
+
fetcher,
|
|
3184
|
+
method: "POST",
|
|
3185
|
+
payload: { tags },
|
|
3186
|
+
token
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3133
3190
|
return {
|
|
3134
3191
|
// Merged into `context` for later steps in this run.
|
|
3135
3192
|
context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
|
|
@@ -4009,7 +4066,9 @@ var shopify_default2 = {
|
|
|
4009
4066
|
enqueues: [{
|
|
4010
4067
|
data: { product: productId, providerId, shop: connection2.shop },
|
|
4011
4068
|
name: "workflow",
|
|
4012
|
-
|
|
4069
|
+
// The numeric id, not the gid: BullMQ refuses a custom id with a
|
|
4070
|
+
// colon in it, and every gid carries two.
|
|
4071
|
+
options: { jobId: "product.workflow.shopify." + context.id + "." + Date.now() },
|
|
4013
4072
|
queue: "product.shopify"
|
|
4014
4073
|
}],
|
|
4015
4074
|
message: "Product sync queued from Shopify webhook.",
|
|
@@ -4815,9 +4874,14 @@ var webhook_default = {
|
|
|
4815
4874
|
"Generate a signing secret and Drawbridge signs every request with it. Your endpoint recomputes the signature to confirm each payload genuinely came from Drawbridge before acting on it."
|
|
4816
4875
|
],
|
|
4817
4876
|
excerpt: "Sign outgoing webhook payloads with an HMAC secret to verify authenticity.",
|
|
4877
|
+
// SAYS WHERE THE URL GOES. Connecting only mints a secret, so a merchant
|
|
4878
|
+
// following this had nowhere to put the endpoint they came to configure —
|
|
4879
|
+
// the destination lives on the step (see steps.webhook.send.settings), and
|
|
4880
|
+
// the guide never said so.
|
|
4818
4881
|
guide: [
|
|
4819
4882
|
"Press Connect. Drawbridge generates a signing secret and shows it here.",
|
|
4820
4883
|
"Copy the secret into your own endpoint.",
|
|
4884
|
+
"Add a Send webhook step to a workflow and put your endpoint URL on it. The URL belongs to the step rather than the connection, so one connection can serve several endpoints.",
|
|
4821
4885
|
"On each request, compute HMAC-SHA256 of the raw body using the secret and compare it against the X-Drawbridge-Signature header before acting on the payload."
|
|
4822
4886
|
]
|
|
4823
4887
|
},
|
package/package.json
CHANGED