@drawbridge/drawbridge-utils 0.0.154 → 0.0.156
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 +78 -16
- package/dist/connections/index.d.cts +156 -22
- package/dist/connections/index.d.ts +156 -22
- package/dist/connections/index.js +78 -16
- package/dist/email.cjs +14 -0
- package/dist/email.d.cts +53 -1
- package/dist/email.d.ts +53 -1
- package/dist/email.js +13 -0
- package/dist/providers.cjs +78 -16
- package/dist/providers.js +78 -16
- 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 },
|
|
@@ -4938,9 +4995,14 @@ var webhook_default = {
|
|
|
4938
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."
|
|
4939
4996
|
],
|
|
4940
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.
|
|
4941
5002
|
guide: [
|
|
4942
5003
|
"Press Connect. Drawbridge generates a signing secret and shows it here.",
|
|
4943
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.",
|
|
4944
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."
|
|
4945
5007
|
]
|
|
4946
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
|
|
|
@@ -1516,10 +1520,15 @@ const contacts = {
|
|
|
1516
1520
|
remove : async ( { email, id, token }, { fetcher } = {} ) => {
|
|
1517
1521
|
|
|
1518
1522
|
// NO TOKEN IS A NO-OP HERE, unlike sendgrid and twilio, and the
|
|
1519
|
-
// difference is what the credential is
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
+
// difference is what the credential is FOR: this is internal CRM
|
|
1524
|
+
// tooling no merchant sees, so a deployment with no portal is a
|
|
1525
|
+
// supported state rather than a missing credential.
|
|
1526
|
+
//
|
|
1527
|
+
// The manifest does mark hubspotToken `required`, and that is not a
|
|
1528
|
+
// contradiction — the flag feeds `isLive` and nothing else, so it
|
|
1529
|
+
// decides whether the admin card reads live. Not live is the honest
|
|
1530
|
+
// badge when nothing will sync. It gates no behaviour, and
|
|
1531
|
+
// saveProviderSettings never consults it, so the row still writes.
|
|
1523
1532
|
if( ! token ) return;
|
|
1524
1533
|
|
|
1525
1534
|
const contact = id || await lookup({ email, fetcher, token });
|
|
@@ -2834,10 +2843,15 @@ var klaviyo = {
|
|
|
2834
2843
|
|
|
2835
2844
|
// HOW TO CONNECT, in the merchant's words. Was `setup`, which nothing
|
|
2836
2845
|
// rendered — four useful instructions no component displayed.
|
|
2846
|
+
// NAMES THE LIST STEP, because `status` below gates on it: a grant with no
|
|
2847
|
+
// list chosen sits at Pending, and this told the merchant it would show
|
|
2848
|
+
// Active. The one required action was the one action the guide omitted, so
|
|
2849
|
+
// anybody following it landed on a connection that looked broken.
|
|
2837
2850
|
guide : [
|
|
2838
2851
|
'Press Connect. Drawbridge sends you to Klaviyo to approve access.',
|
|
2839
2852
|
'Sign in to Klaviyo if you are not already, and choose the account to connect.',
|
|
2840
|
-
'Approve the permissions Klaviyo lists. You
|
|
2853
|
+
'Approve the permissions Klaviyo lists. You come back here to pick the list your contacts should sync into.',
|
|
2854
|
+
'The connection shows Pending until you pick a list, then Active.',
|
|
2841
2855
|
'You can revoke access at any time from Klaviyo, under Integrations.'
|
|
2842
2856
|
]
|
|
2843
2857
|
|
|
@@ -2987,7 +3001,7 @@ var klaviyo = {
|
|
|
2987
3001
|
// different thing from deleting the profile.
|
|
2988
3002
|
remove : false,
|
|
2989
3003
|
|
|
2990
|
-
sync : async ( {
|
|
3004
|
+
sync : async ( { context, lead, segments, settings, suppressed, token }, { fetcher } = {} ) => {
|
|
2991
3005
|
|
|
2992
3006
|
const list = settings?.list;
|
|
2993
3007
|
|
|
@@ -3005,9 +3019,32 @@ var klaviyo = {
|
|
|
3005
3019
|
// contact knows money: order attribution is person-level and merges
|
|
3006
3020
|
// across every address a human used, so revenue and orders cannot
|
|
3007
3021
|
// exist on a single-address record.
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3022
|
+
//
|
|
3023
|
+
// READ OFF `context`, NOT the shell's `contact` prop. That prop is the
|
|
3024
|
+
// ADDRESS — a string — so `contact?.totals` was undefined on every run
|
|
3025
|
+
// this hook has ever made, and all five properties below shipped as 0
|
|
3026
|
+
// no matter what the person had entered, drawn or spent. Only a
|
|
3027
|
+
// segment.contact.add run carries the document (drawbridge-sync
|
|
3028
|
+
// lib/segment.js); lead.insert names a brand-new entrant who has no
|
|
3029
|
+
// history to send anyway.
|
|
3030
|
+
const person = context?.contact || null;
|
|
3031
|
+
const totals = person?.totals || {};
|
|
3032
|
+
|
|
3033
|
+
// UPSERT, NOT CREATE. /profiles/ is the plain create and answers a
|
|
3034
|
+
// conflict for a profile Klaviyo already holds — which is most of them,
|
|
3035
|
+
// because the whole point of the segment trigger is syncing people who
|
|
3036
|
+
// have been around long enough to earn a ranking. profile-import is
|
|
3037
|
+
// Klaviyo's documented create-or-update: 201 when it made one, 200 when
|
|
3038
|
+
// it updated one, and no conflict either way
|
|
3039
|
+
// (developers.klaviyo.com/en/reference/create_or_update_profile,
|
|
3040
|
+
// fetched 2026-09-09).
|
|
3041
|
+
//
|
|
3042
|
+
// IT MERGES, and that is why the blocks below are conditional rather
|
|
3043
|
+
// than defaulted. "Not including a field in your request will leave it
|
|
3044
|
+
// unchanged" — so omitting the totals on a lead.insert preserves
|
|
3045
|
+
// whatever the last segment run published, where sending zeros would
|
|
3046
|
+
// erase it. A property we cannot compute is one we must not write.
|
|
3047
|
+
const profile = await api$1( '/profile-import', {
|
|
3011
3048
|
fetcher,
|
|
3012
3049
|
method : 'POST',
|
|
3013
3050
|
payload : {
|
|
@@ -3016,16 +3053,36 @@ var klaviyo = {
|
|
|
3016
3053
|
email,
|
|
3017
3054
|
...( lead?.name && { first_name : String( lead.name ).trim().split( /\s+/ )[ 0 ] }),
|
|
3018
3055
|
properties : {
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3056
|
+
...( person && {
|
|
3057
|
+
drawbridge_campaigns : ( person.campaigns || [] ).length,
|
|
3058
|
+
drawbridge_draws : totals.draws || 0,
|
|
3059
|
+
drawbridge_entries : totals.entries || 0,
|
|
3060
|
+
drawbridge_orders : totals.orders || 0,
|
|
3061
|
+
// Campaign-attributed, NOT lifetime. A merchant running
|
|
3062
|
+
// Shopify already has lifetime revenue in Klaviyo through
|
|
3063
|
+
// Klaviyo's own integration; what only we can say is how
|
|
3064
|
+
// much a campaign drove. Named so the two cannot be
|
|
3065
|
+
// mistaken for one another in a segment builder.
|
|
3066
|
+
drawbridge_revenue : totals.gross || 0
|
|
3067
|
+
}),
|
|
3068
|
+
// THE DRAWBRIDGE SEGMENTS THEY ARE IN, as a list property the
|
|
3069
|
+
// merchant builds Klaviyo segments on top of. Klaviyo owns no
|
|
3070
|
+
// writable membership — its segments are computed from rules,
|
|
3071
|
+
// so a profile cannot be put in one — and this is the nearest
|
|
3072
|
+
// honest equivalent.
|
|
3073
|
+
//
|
|
3074
|
+
// WHOLESALE, WHICH IS WHAT MAKES IT SELF-HEALING. Drawbridge
|
|
3075
|
+
// segments are dynamic and nothing dispatches a leave event —
|
|
3076
|
+
// only segment.contact.add exists — so a property that was
|
|
3077
|
+
// only ever appended to would outlive the membership that
|
|
3078
|
+
// earned it. Rewriting the whole array each run means every
|
|
3079
|
+
// sync corrects whatever the last one got wrong, with no
|
|
3080
|
+
// removal path to build.
|
|
3081
|
+
//
|
|
3082
|
+
// `segments` is null when the run carried no contact document,
|
|
3083
|
+
// meaning nobody looked — different from [], which means they
|
|
3084
|
+
// are in none. Null omits the key and merge leaves it alone.
|
|
3085
|
+
...( segments && { drawbridge_segments : segments.map( ( entry ) => entry.title ).filter( Boolean ) })
|
|
3029
3086
|
}
|
|
3030
3087
|
},
|
|
3031
3088
|
type : 'profile'
|
|
@@ -3427,7 +3484,10 @@ const api = async ( path, { dc, fetcher = fetch, method = 'GET', payload, token
|
|
|
3427
3484
|
|
|
3428
3485
|
}
|
|
3429
3486
|
|
|
3430
|
-
|
|
3487
|
+
// 204 ON A TAG WRITE — nothing to parse, and asking an empty body for json
|
|
3488
|
+
// throws, which would report a change Mailchimp accepted as a failed step.
|
|
3489
|
+
// Klaviyo's file carries the same guard for the same reason.
|
|
3490
|
+
return response.status === 204 ? null : response.json();
|
|
3431
3491
|
|
|
3432
3492
|
};
|
|
3433
3493
|
|
|
@@ -3591,7 +3651,7 @@ var mailchimp = {
|
|
|
3591
3651
|
// PUT /lists/{list_id}/members/{subscriber_hash} — an UPSERT, which is
|
|
3592
3652
|
// why there is no create-or-update branch here. Quoted from Mailchimp's
|
|
3593
3653
|
// Marketing API reference for the list-members resource.
|
|
3594
|
-
sync : async ( { lead, settings, suppressed, token }, { fetcher } = {} ) => {
|
|
3654
|
+
sync : async ( { connection, lead, segments, settings, suppressed, token }, { fetcher, read } = {} ) => {
|
|
3595
3655
|
|
|
3596
3656
|
const audience = settings?.audience;
|
|
3597
3657
|
|
|
@@ -3648,6 +3708,75 @@ var mailchimp = {
|
|
|
3648
3708
|
token
|
|
3649
3709
|
});
|
|
3650
3710
|
|
|
3711
|
+
// THE DRAWBRIDGE SEGMENTS THIS PERSON IS IN, written as Mailchimp tags.
|
|
3712
|
+
//
|
|
3713
|
+
// TAGS, NOT MERGE FIELDS. A merge field has to exist on the audience
|
|
3714
|
+
// before anything can be written to it, and an unregistered one is
|
|
3715
|
+
// refused along with the whole member request — so carrying data that
|
|
3716
|
+
// way means registering fields first, which is lifecycle.register's job
|
|
3717
|
+
// and is not built. A tag needs no setup: "If a tag that does not exist
|
|
3718
|
+
// is passed in and set as 'active', a new tag will be created"
|
|
3719
|
+
// (mailchimp.com/developer/marketing/api/list-member-tags/add-or-remove-member-tags,
|
|
3720
|
+
// fetched 2026-09-09). Nothing for the merchant to prepare, so nothing
|
|
3721
|
+
// to explain in the guide.
|
|
3722
|
+
//
|
|
3723
|
+
// ACTIVE AND INACTIVE IN ONE CALL, which is what keeps this correct
|
|
3724
|
+
// over time. Drawbridge segments are dynamic, and nothing dispatches a
|
|
3725
|
+
// leave event — only segment.contact.add exists — so a tag that was
|
|
3726
|
+
// only ever added would outlive the membership behind it and the
|
|
3727
|
+
// merchant would target a condition that had stopped being true.
|
|
3728
|
+
// Sending the whole list every time, each tag active or inactive
|
|
3729
|
+
// against current membership, means each sync corrects the last.
|
|
3730
|
+
//
|
|
3731
|
+
// WRITTEN FOR A SUPPRESSED PERSON TOO. A tag is a label, not a
|
|
3732
|
+
// permission: the `status : 'unsubscribed'` above is what stops mail
|
|
3733
|
+
// reaching them, and letting their labels rot would leave the merchant
|
|
3734
|
+
// a wrong answer about who belongs to what.
|
|
3735
|
+
//
|
|
3736
|
+
// PREFIXED, so a Drawbridge tag cannot collide with one the merchant
|
|
3737
|
+
// keeps by hand, and so the inactive pass can only clear tags this
|
|
3738
|
+
// integration wrote.
|
|
3739
|
+
if( segments && read ){
|
|
3740
|
+
|
|
3741
|
+
const owned = await read.aggregate({
|
|
3742
|
+
collection : 'segment',
|
|
3743
|
+
pipeline : [
|
|
3744
|
+
{
|
|
3745
|
+
$match : {
|
|
3746
|
+
organization : connection?.organization,
|
|
3747
|
+
system : { $ne : true }
|
|
3748
|
+
}
|
|
3749
|
+
},
|
|
3750
|
+
{
|
|
3751
|
+
$project : { _id : 0, title : 1 }
|
|
3752
|
+
}
|
|
3753
|
+
]
|
|
3754
|
+
});
|
|
3755
|
+
|
|
3756
|
+
const joined = new Set( segments.map( ( entry ) => entry.title ) );
|
|
3757
|
+
|
|
3758
|
+
const tags = ( owned || [] )
|
|
3759
|
+
.map( ( entry ) => entry.title )
|
|
3760
|
+
.filter( Boolean )
|
|
3761
|
+
.map( ( title ) => ({
|
|
3762
|
+
name : 'Drawbridge: ' + title,
|
|
3763
|
+
status : joined.has( title ) ? 'active' : 'inactive'
|
|
3764
|
+
}) );
|
|
3765
|
+
|
|
3766
|
+
if( tags.length > 0 ){
|
|
3767
|
+
|
|
3768
|
+
await api( '/lists/' + audience + '/members/' + hash + '/tags', {
|
|
3769
|
+
dc : settings?.dc,
|
|
3770
|
+
fetcher,
|
|
3771
|
+
method : 'POST',
|
|
3772
|
+
payload : { tags },
|
|
3773
|
+
token
|
|
3774
|
+
});
|
|
3775
|
+
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3651
3780
|
return {
|
|
3652
3781
|
// Merged into `context` for later steps in this run.
|
|
3653
3782
|
context : { mailchimpMemberId : member?.id || hash },
|
|
@@ -5765,9 +5894,14 @@ var webhook = {
|
|
|
5765
5894
|
'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.'
|
|
5766
5895
|
],
|
|
5767
5896
|
excerpt : 'Sign outgoing webhook payloads with an HMAC secret to verify authenticity.',
|
|
5897
|
+
// SAYS WHERE THE URL GOES. Connecting only mints a secret, so a merchant
|
|
5898
|
+
// following this had nowhere to put the endpoint they came to configure —
|
|
5899
|
+
// the destination lives on the step (see steps.webhook.send.settings), and
|
|
5900
|
+
// the guide never said so.
|
|
5768
5901
|
guide : [
|
|
5769
5902
|
'Press Connect. Drawbridge generates a signing secret and shows it here.',
|
|
5770
5903
|
'Copy the secret into your own endpoint.',
|
|
5904
|
+
'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.',
|
|
5771
5905
|
'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.'
|
|
5772
5906
|
]
|
|
5773
5907
|
},
|