@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.
@@ -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 are returned here and the connection shows Active.",
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 ({ contact, lead, settings, suppressed, token }, { fetcher } = {}) => {
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 totals = (contact == null ? void 0 : contact.totals) || {};
2709
- const profile = await api2("/profiles/", {
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
- drawbridge_campaigns: ((contact == null ? void 0 : contact.campaigns) || []).length,
2719
- drawbridge_draws: totals.draws || 0,
2720
- drawbridge_entries: totals.entries || 0,
2721
- drawbridge_orders: totals.orders || 0,
2722
- // Campaign-attributed, NOT lifetime. A merchant running
2723
- // Shopify already has lifetime revenue in Klaviyo through
2724
- // Klaviyo's own integration; what only we can say is how
2725
- // much a campaign drove. Named so the two cannot be
2726
- // mistaken for one another in a segment builder.
2727
- drawbridge_revenue: totals.gross || 0
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 },
@@ -4863,9 +4920,14 @@ var webhook_default = {
4863
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."
4864
4921
  ],
4865
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.
4866
4927
  guide: [
4867
4928
  "Press Connect. Drawbridge generates a signing secret and shows it here.",
4868
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.",
4869
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."
4870
4932
  ]
4871
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 are returned here and the connection shows Active.",
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 ({ contact, lead, settings, suppressed, token }, { fetcher } = {}) => {
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 totals = (contact == null ? void 0 : contact.totals) || {};
2663
- const profile = await api2("/profiles/", {
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
- drawbridge_campaigns: ((contact == null ? void 0 : contact.campaigns) || []).length,
2673
- drawbridge_draws: totals.draws || 0,
2674
- drawbridge_entries: totals.entries || 0,
2675
- drawbridge_orders: totals.orders || 0,
2676
- // Campaign-attributed, NOT lifetime. A merchant running
2677
- // Shopify already has lifetime revenue in Klaviyo through
2678
- // Klaviyo's own integration; what only we can say is how
2679
- // much a campaign drove. Named so the two cannot be
2680
- // mistaken for one another in a segment builder.
2681
- drawbridge_revenue: totals.gross || 0
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 },
@@ -4817,9 +4874,14 @@ var webhook_default = {
4817
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."
4818
4875
  ],
4819
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.
4820
4881
  guide: [
4821
4882
  "Press Connect. Drawbridge generates a signing secret and shows it here.",
4822
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.",
4823
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."
4824
4886
  ]
4825
4887
  },
package/package.json CHANGED
@@ -216,5 +216,5 @@
216
216
  "prepublishOnly": ". \"$HOME/.nvm/nvm.sh\" && nvm use && tsup && node --test"
217
217
  },
218
218
  "types": "dist/index.d.ts",
219
- "version": "0.0.154"
219
+ "version": "0.0.156"
220
220
  }