@drawbridge/drawbridge-utils 0.0.169 → 0.0.171

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.
@@ -774,10 +774,11 @@ var attentive_default2 = {
774
774
  sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
775
775
  var _a, _b, _c, _d;
776
776
  const segment = settings == null ? void 0 : settings.segment;
777
- if (!segment) return { message: "No Attentive segment is chosen for this connection.", skipped: true };
778
777
  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);
779
778
  const phone = toE164(((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.phone) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.phone));
780
- if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", skipped: true };
779
+ const request2 = { email: email || null, phone: phone || null, segment: (settings == null ? void 0 : settings.segment) || null };
780
+ if (!segment) return { message: "No Attentive segment is chosen for this connection.", request: request2, skipped: true };
781
+ if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", request: request2, skipped: true };
781
782
  const user = {
782
783
  ...email && { email },
783
784
  ...phone && { phone }
@@ -799,6 +800,7 @@ var attentive_default2 = {
799
800
  });
800
801
  return {
801
802
  message: "Attentive accepted an unsubscribe for this contact \u2014 they have opted out.",
803
+ request: request2,
802
804
  response: { accepted: true, unsubscribed: true }
803
805
  };
804
806
  }
@@ -839,6 +841,7 @@ var attentive_default2 = {
839
841
  // queued them — a merchant who reads "synced" and looks for the person
840
842
  // in Attentive a second later has been told the wrong thing.
841
843
  message: "Attentive accepted this contact for the segment. Attentive processes these asynchronously, so it appears there shortly.",
844
+ request: request2,
842
845
  response: {
843
846
  accepted: true,
844
847
  ...(membership == null ? void 0 : membership.batchJobId) && { batchJobId: membership.batchJobId }
@@ -2855,7 +2858,7 @@ var drawbridge_default2 = {
2855
2858
  text: { max: 2e3, type: "string" }
2856
2859
  },
2857
2860
  triggers: ["schedule.day", "schedule.week", "schedule.month"],
2858
- usage: { actions: 0 }
2861
+ usage: { actions: channels.email.actionsPerSend }
2859
2862
  }),
2860
2863
  // To organization MEMBERS. Never suppressed — an entrant's opt-out must
2861
2864
  // not silence an alert to staff — and not billed.
@@ -2880,10 +2883,14 @@ var drawbridge_default2 = {
2880
2883
  subject: { max: 150, type: "string" }
2881
2884
  },
2882
2885
  triggers: ["lead.insert"],
2883
- // Zero is a PRICE, and a deliberate one. Declared rather than omitted
2884
- // so "this is free" and "nobody decided" stay different statements;
2885
- // completeStep bills only when actions > 0.
2886
- usage: { actions: 0 }
2886
+ // EVERY STEP IN A BILLABLE WORKFLOW COUNTS, team mail included. This
2887
+ // leaves our SendGrid account and costs the same to send as any other
2888
+ // email, so it carries the same weight. Whether anything is charged at
2889
+ // all is the workflow's `billable` tag, not the step's audience — and
2890
+ // this step triggers on lead.insert, so a merchant notifying
2891
+ // themselves on every entry spends against their allowance twice per
2892
+ // lead, once for the entry and once for the notification.
2893
+ usage: { actions: channels.email.actionsPerSend }
2887
2894
  }),
2888
2895
  // To a LEAD. Suppression applies and the send is billed.
2889
2896
  send: () => ({
@@ -3239,13 +3246,15 @@ var klaviyo_default2 = {
3239
3246
  // different thing from deleting the profile.
3240
3247
  remove: false,
3241
3248
  sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
3242
- var _a, _b, _c;
3249
+ var _a, _b, _c, _d, _e;
3243
3250
  const list = settings == null ? void 0 : settings.list;
3244
- if (!list) return { message: "No Klaviyo list is chosen for this connection.", skipped: true };
3245
- 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);
3246
- if (!email) return { message: "That lead has no email address to sync.", skipped: true };
3251
+ const request2 = { 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) || null, list: (settings == null ? void 0 : settings.list) || null };
3252
+ if (!list) return { message: "No Klaviyo list is chosen for this connection.", request: request2, skipped: true };
3253
+ const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
3254
+ if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3247
3255
  const person = (context == null ? void 0 : context.contact) || null;
3248
3256
  const totals = (person == null ? void 0 : person.totals) || {};
3257
+ const count = (value) => typeof value === "number" ? value : (value == null ? void 0 : value.total) || 0;
3249
3258
  const profile = await api2("/profile-import", {
3250
3259
  fetcher,
3251
3260
  method: "POST",
@@ -3259,13 +3268,13 @@ var klaviyo_default2 = {
3259
3268
  drawbridge_campaigns: (person.campaigns || []).length,
3260
3269
  drawbridge_draws: totals.draws || 0,
3261
3270
  drawbridge_entries: totals.entries || 0,
3262
- drawbridge_orders: totals.orders || 0,
3271
+ drawbridge_orders: count(totals.orders),
3263
3272
  // Campaign-attributed, NOT lifetime. A merchant running
3264
3273
  // Shopify already has lifetime revenue in Klaviyo through
3265
3274
  // Klaviyo's own integration; what only we can say is how
3266
3275
  // much a campaign drove. Named so the two cannot be
3267
3276
  // mistaken for one another in a segment builder.
3268
- drawbridge_revenue: totals.gross || 0
3277
+ drawbridge_revenue: count(totals.gross)
3269
3278
  },
3270
3279
  // THE DRAWBRIDGE SEGMENTS THEY ARE IN, as a list property the
3271
3280
  // merchant builds Klaviyo segments on top of. Klaviyo owns no
@@ -3299,8 +3308,8 @@ var klaviyo_default2 = {
3299
3308
  },
3300
3309
  token
3301
3310
  });
3302
- const profileId = (_c = profile == null ? void 0 : profile.data) == null ? void 0 : _c.id;
3303
- if (!profileId) return { message: "Klaviyo returned no profile id.", skipped: true };
3311
+ const profileId = (_e = profile == null ? void 0 : profile.data) == null ? void 0 : _e.id;
3312
+ if (!profileId) return { message: "Klaviyo returned no profile id.", request: request2, skipped: true };
3304
3313
  await api2("/profile-subscription-bulk-create-jobs/", {
3305
3314
  fetcher,
3306
3315
  method: "POST",
@@ -3329,6 +3338,7 @@ var klaviyo_default2 = {
3329
3338
  // Merged into `context` for later steps in this run.
3330
3339
  context: { klaviyoProfileId: profileId },
3331
3340
  message: suppressed ? "Synced to Klaviyo as unsubscribed \u2014 this contact has opted out." : "Synced to the Klaviyo list.",
3341
+ request: request2,
3332
3342
  // Recorded on the run for support to read back, not a write
3333
3343
  // instruction — the hook has already written what it needed to.
3334
3344
  response: { klaviyoProfileId: profileId }
@@ -3770,7 +3780,8 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3770
3780
  return response.status === 204 ? null : response.json();
3771
3781
  };
3772
3782
  var subscriberHash = (email) => (0, import_node_crypto5.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
3773
- var tagName = (title) => "Drawbridge: " + title;
3783
+ var TAG_NAME_LIMIT = 100;
3784
+ var tagName = (title) => ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT);
3774
3785
  var mailchimp_default2 = {
3775
3786
  // OAUTH 2, authorization code. Every url below is quoted from
3776
3787
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -3908,15 +3919,16 @@ var mailchimp_default2 = {
3908
3919
  // why there is no create-or-update branch here. Quoted from Mailchimp's
3909
3920
  // Marketing API reference for the list-members resource.
3910
3921
  sync: async ({ connection: connection2, lead, segments, settings, suppressed, token }, { fetcher, read } = {}) => {
3911
- var _a, _b, _c;
3922
+ var _a, _b, _c, _d, _e;
3912
3923
  const audience = settings == null ? void 0 : settings.audience;
3913
- if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
3914
- 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);
3915
- if (!email) return { message: "That lead has no email address to sync.", skipped: true };
3924
+ const request2 = { audience: (settings == null ? void 0 : settings.audience) || null, 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) || null };
3925
+ if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", request: request2, skipped: true };
3926
+ const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
3927
+ if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3916
3928
  const hash = subscriberHash(email);
3917
3929
  const [firstName, ...restOfName] = String((lead == null ? void 0 : lead.name) || "").trim().split(/\s+/).filter(Boolean);
3918
3930
  const lastName = restOfName.join(" ");
3919
- const phone = ((_c = lead == null ? void 0 : lead.phone) == null ? void 0 : _c.number) || null;
3931
+ const phone = ((_e = lead == null ? void 0 : lead.phone) == null ? void 0 : _e.number) || null;
3920
3932
  const mergeFields = {
3921
3933
  ...firstName && { FNAME: firstName },
3922
3934
  ...lastName && { LNAME: lastName },
@@ -3973,6 +3985,7 @@ var mailchimp_default2 = {
3973
3985
  // Merged into `context` for later steps in this run.
3974
3986
  context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
3975
3987
  message: suppressed ? "Synced to Mailchimp as unsubscribed \u2014 this contact has opted out." : "Synced to the Mailchimp audience.",
3988
+ request: request2,
3976
3989
  // Recorded on the run for support to read back, not a write
3977
3990
  // instruction — the hook has already written what it needed to.
3978
3991
  response: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash }
@@ -3995,10 +4008,12 @@ var mailchimp_default2 = {
3995
4008
  // on attaching people to it by name. Both address the same object. The
3996
4009
  // segment schema says it outright: "The type of segment. Static segments
3997
4010
  // are now known as tags"
3998
- // (api.mailchimp.com/schema/3.0/Swagger.json, fetched 2026-09-11).
4011
+ // (api.mailchimp.com/schema/3.0/Definitions/Lists/Segments/Response.json,
4012
+ // fetched 2026-09-12 — the root Swagger.json carries no prose, only $refs
4013
+ // into fragment files like this one).
3999
4014
  //
4000
- // IDEMPOTENT ON EVERY PATH: called on create, on rename, on the boot
4001
- // sweep and on backfill, it converges. That is what lets one hook serve
4015
+ // IDEMPOTENT ON EVERY PATH: called on create, on rename, on a connection
4016
+ // finishing its configuration, and on the backfill migration, it converges. That is what lets one hook serve
4002
4017
  // all four without a create-vs-update branch anywhere else.
4003
4018
  register: async ({ connection: connection2, context, manifest, settings, token, workflow }, { fetcher, read } = {}) => {
4004
4019
  var _a;
@@ -5025,7 +5040,7 @@ var shopify_default2 = {
5025
5040
  //
5026
5041
  // The shell has already refused a missing or inactive Shopify connection,
5027
5042
  // so what is left is the two things only this hook can know are wrong.
5028
- product: async ({ connection: connection2, context, workflow }, { mintId } = {}) => {
5043
+ product: async ({ connection: connection2, context, workflow }, { mintId, read } = {}) => {
5029
5044
  const request2 = {
5030
5045
  numericId: (context == null ? void 0 : context.id) || null,
5031
5046
  organizationId: workflow.organization,
@@ -5034,7 +5049,13 @@ var shopify_default2 = {
5034
5049
  if (!(context == null ? void 0 : context.id)) return { message: "Skipped \u2014 product webhook payload had no id.", request: request2, response: { skipped: true }, skipped: true };
5035
5050
  if (!connection2.shop) return { message: "Skipped \u2014 Shopify connection is missing shop domain.", request: request2, response: { skipped: true }, skipped: true };
5036
5051
  const providerId = "gid://shopify/Product/" + context.id;
5037
- const productId = mintId();
5052
+ const query = {
5053
+ "provider.id": providerId,
5054
+ "provider.slug": "shopify",
5055
+ "source.domain": connection2.shop
5056
+ };
5057
+ const existing = (read == null ? void 0 : read.get) ? await read.get({ collection: "product", query }) : null;
5058
+ const productId = (existing == null ? void 0 : existing.id) || mintId();
5038
5059
  return {
5039
5060
  enqueues: [{
5040
5061
  data: { product: productId, providerId, shop: connection2.shop },
@@ -5064,11 +5085,7 @@ var shopify_default2 = {
5064
5085
  },
5065
5086
  operation: "update",
5066
5087
  options: { upsert: true },
5067
- query: {
5068
- "provider.id": providerId,
5069
- "provider.slug": "shopify",
5070
- "source.domain": connection2.shop
5071
- }
5088
+ query
5072
5089
  }]
5073
5090
  };
5074
5091
  }
@@ -5338,7 +5355,7 @@ var shopify_default2 = {
5338
5355
  options: { jobId: "connection.update.register." + workflow.connection + "." + (0, import_node_crypto6.randomUUID)() },
5339
5356
  queue: "connection"
5340
5357
  }],
5341
- message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
5358
+ message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, scopes reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, scopes reconciled." : "Health check passed \u2014 ping ok, scopes reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
5342
5359
  request: request2,
5343
5360
  response: {
5344
5361
  billingProbeFailed,
package/dist/providers.js CHANGED
@@ -725,10 +725,11 @@ var attentive_default2 = {
725
725
  sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
726
726
  var _a, _b, _c, _d;
727
727
  const segment = settings == null ? void 0 : settings.segment;
728
- if (!segment) return { message: "No Attentive segment is chosen for this connection.", skipped: true };
729
728
  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);
730
729
  const phone = toE164(((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.phone) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.phone));
731
- if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", skipped: true };
730
+ const request2 = { email: email || null, phone: phone || null, segment: (settings == null ? void 0 : settings.segment) || null };
731
+ if (!segment) return { message: "No Attentive segment is chosen for this connection.", request: request2, skipped: true };
732
+ if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", request: request2, skipped: true };
732
733
  const user = {
733
734
  ...email && { email },
734
735
  ...phone && { phone }
@@ -750,6 +751,7 @@ var attentive_default2 = {
750
751
  });
751
752
  return {
752
753
  message: "Attentive accepted an unsubscribe for this contact \u2014 they have opted out.",
754
+ request: request2,
753
755
  response: { accepted: true, unsubscribed: true }
754
756
  };
755
757
  }
@@ -790,6 +792,7 @@ var attentive_default2 = {
790
792
  // queued them — a merchant who reads "synced" and looks for the person
791
793
  // in Attentive a second later has been told the wrong thing.
792
794
  message: "Attentive accepted this contact for the segment. Attentive processes these asynchronously, so it appears there shortly.",
795
+ request: request2,
793
796
  response: {
794
797
  accepted: true,
795
798
  ...(membership == null ? void 0 : membership.batchJobId) && { batchJobId: membership.batchJobId }
@@ -2806,7 +2809,7 @@ var drawbridge_default2 = {
2806
2809
  text: { max: 2e3, type: "string" }
2807
2810
  },
2808
2811
  triggers: ["schedule.day", "schedule.week", "schedule.month"],
2809
- usage: { actions: 0 }
2812
+ usage: { actions: channels.email.actionsPerSend }
2810
2813
  }),
2811
2814
  // To organization MEMBERS. Never suppressed — an entrant's opt-out must
2812
2815
  // not silence an alert to staff — and not billed.
@@ -2831,10 +2834,14 @@ var drawbridge_default2 = {
2831
2834
  subject: { max: 150, type: "string" }
2832
2835
  },
2833
2836
  triggers: ["lead.insert"],
2834
- // Zero is a PRICE, and a deliberate one. Declared rather than omitted
2835
- // so "this is free" and "nobody decided" stay different statements;
2836
- // completeStep bills only when actions > 0.
2837
- usage: { actions: 0 }
2837
+ // EVERY STEP IN A BILLABLE WORKFLOW COUNTS, team mail included. This
2838
+ // leaves our SendGrid account and costs the same to send as any other
2839
+ // email, so it carries the same weight. Whether anything is charged at
2840
+ // all is the workflow's `billable` tag, not the step's audience — and
2841
+ // this step triggers on lead.insert, so a merchant notifying
2842
+ // themselves on every entry spends against their allowance twice per
2843
+ // lead, once for the entry and once for the notification.
2844
+ usage: { actions: channels.email.actionsPerSend }
2838
2845
  }),
2839
2846
  // To a LEAD. Suppression applies and the send is billed.
2840
2847
  send: () => ({
@@ -3190,13 +3197,15 @@ var klaviyo_default2 = {
3190
3197
  // different thing from deleting the profile.
3191
3198
  remove: false,
3192
3199
  sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
3193
- var _a, _b, _c;
3200
+ var _a, _b, _c, _d, _e;
3194
3201
  const list = settings == null ? void 0 : settings.list;
3195
- if (!list) return { message: "No Klaviyo list is chosen for this connection.", skipped: true };
3196
- 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);
3197
- if (!email) return { message: "That lead has no email address to sync.", skipped: true };
3202
+ const request2 = { 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) || null, list: (settings == null ? void 0 : settings.list) || null };
3203
+ if (!list) return { message: "No Klaviyo list is chosen for this connection.", request: request2, skipped: true };
3204
+ const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
3205
+ if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3198
3206
  const person = (context == null ? void 0 : context.contact) || null;
3199
3207
  const totals = (person == null ? void 0 : person.totals) || {};
3208
+ const count = (value) => typeof value === "number" ? value : (value == null ? void 0 : value.total) || 0;
3200
3209
  const profile = await api2("/profile-import", {
3201
3210
  fetcher,
3202
3211
  method: "POST",
@@ -3210,13 +3219,13 @@ var klaviyo_default2 = {
3210
3219
  drawbridge_campaigns: (person.campaigns || []).length,
3211
3220
  drawbridge_draws: totals.draws || 0,
3212
3221
  drawbridge_entries: totals.entries || 0,
3213
- drawbridge_orders: totals.orders || 0,
3222
+ drawbridge_orders: count(totals.orders),
3214
3223
  // Campaign-attributed, NOT lifetime. A merchant running
3215
3224
  // Shopify already has lifetime revenue in Klaviyo through
3216
3225
  // Klaviyo's own integration; what only we can say is how
3217
3226
  // much a campaign drove. Named so the two cannot be
3218
3227
  // mistaken for one another in a segment builder.
3219
- drawbridge_revenue: totals.gross || 0
3228
+ drawbridge_revenue: count(totals.gross)
3220
3229
  },
3221
3230
  // THE DRAWBRIDGE SEGMENTS THEY ARE IN, as a list property the
3222
3231
  // merchant builds Klaviyo segments on top of. Klaviyo owns no
@@ -3250,8 +3259,8 @@ var klaviyo_default2 = {
3250
3259
  },
3251
3260
  token
3252
3261
  });
3253
- const profileId = (_c = profile == null ? void 0 : profile.data) == null ? void 0 : _c.id;
3254
- if (!profileId) return { message: "Klaviyo returned no profile id.", skipped: true };
3262
+ const profileId = (_e = profile == null ? void 0 : profile.data) == null ? void 0 : _e.id;
3263
+ if (!profileId) return { message: "Klaviyo returned no profile id.", request: request2, skipped: true };
3255
3264
  await api2("/profile-subscription-bulk-create-jobs/", {
3256
3265
  fetcher,
3257
3266
  method: "POST",
@@ -3280,6 +3289,7 @@ var klaviyo_default2 = {
3280
3289
  // Merged into `context` for later steps in this run.
3281
3290
  context: { klaviyoProfileId: profileId },
3282
3291
  message: suppressed ? "Synced to Klaviyo as unsubscribed \u2014 this contact has opted out." : "Synced to the Klaviyo list.",
3292
+ request: request2,
3283
3293
  // Recorded on the run for support to read back, not a write
3284
3294
  // instruction — the hook has already written what it needed to.
3285
3295
  response: { klaviyoProfileId: profileId }
@@ -3721,7 +3731,8 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3721
3731
  return response.status === 204 ? null : response.json();
3722
3732
  };
3723
3733
  var subscriberHash = (email) => createHash2("md5").update(String(email).trim().toLowerCase()).digest("hex");
3724
- var tagName = (title) => "Drawbridge: " + title;
3734
+ var TAG_NAME_LIMIT = 100;
3735
+ var tagName = (title) => ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT);
3725
3736
  var mailchimp_default2 = {
3726
3737
  // OAUTH 2, authorization code. Every url below is quoted from
3727
3738
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -3859,15 +3870,16 @@ var mailchimp_default2 = {
3859
3870
  // why there is no create-or-update branch here. Quoted from Mailchimp's
3860
3871
  // Marketing API reference for the list-members resource.
3861
3872
  sync: async ({ connection: connection2, lead, segments, settings, suppressed, token }, { fetcher, read } = {}) => {
3862
- var _a, _b, _c;
3873
+ var _a, _b, _c, _d, _e;
3863
3874
  const audience = settings == null ? void 0 : settings.audience;
3864
- if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
3865
- 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);
3866
- if (!email) return { message: "That lead has no email address to sync.", skipped: true };
3875
+ const request2 = { audience: (settings == null ? void 0 : settings.audience) || null, 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) || null };
3876
+ if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", request: request2, skipped: true };
3877
+ const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
3878
+ if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3867
3879
  const hash = subscriberHash(email);
3868
3880
  const [firstName, ...restOfName] = String((lead == null ? void 0 : lead.name) || "").trim().split(/\s+/).filter(Boolean);
3869
3881
  const lastName = restOfName.join(" ");
3870
- const phone = ((_c = lead == null ? void 0 : lead.phone) == null ? void 0 : _c.number) || null;
3882
+ const phone = ((_e = lead == null ? void 0 : lead.phone) == null ? void 0 : _e.number) || null;
3871
3883
  const mergeFields = {
3872
3884
  ...firstName && { FNAME: firstName },
3873
3885
  ...lastName && { LNAME: lastName },
@@ -3924,6 +3936,7 @@ var mailchimp_default2 = {
3924
3936
  // Merged into `context` for later steps in this run.
3925
3937
  context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
3926
3938
  message: suppressed ? "Synced to Mailchimp as unsubscribed \u2014 this contact has opted out." : "Synced to the Mailchimp audience.",
3939
+ request: request2,
3927
3940
  // Recorded on the run for support to read back, not a write
3928
3941
  // instruction — the hook has already written what it needed to.
3929
3942
  response: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash }
@@ -3946,10 +3959,12 @@ var mailchimp_default2 = {
3946
3959
  // on attaching people to it by name. Both address the same object. The
3947
3960
  // segment schema says it outright: "The type of segment. Static segments
3948
3961
  // are now known as tags"
3949
- // (api.mailchimp.com/schema/3.0/Swagger.json, fetched 2026-09-11).
3962
+ // (api.mailchimp.com/schema/3.0/Definitions/Lists/Segments/Response.json,
3963
+ // fetched 2026-09-12 — the root Swagger.json carries no prose, only $refs
3964
+ // into fragment files like this one).
3950
3965
  //
3951
- // IDEMPOTENT ON EVERY PATH: called on create, on rename, on the boot
3952
- // sweep and on backfill, it converges. That is what lets one hook serve
3966
+ // IDEMPOTENT ON EVERY PATH: called on create, on rename, on a connection
3967
+ // finishing its configuration, and on the backfill migration, it converges. That is what lets one hook serve
3953
3968
  // all four without a create-vs-update branch anywhere else.
3954
3969
  register: async ({ connection: connection2, context, manifest, settings, token, workflow }, { fetcher, read } = {}) => {
3955
3970
  var _a;
@@ -4976,7 +4991,7 @@ var shopify_default2 = {
4976
4991
  //
4977
4992
  // The shell has already refused a missing or inactive Shopify connection,
4978
4993
  // so what is left is the two things only this hook can know are wrong.
4979
- product: async ({ connection: connection2, context, workflow }, { mintId } = {}) => {
4994
+ product: async ({ connection: connection2, context, workflow }, { mintId, read } = {}) => {
4980
4995
  const request2 = {
4981
4996
  numericId: (context == null ? void 0 : context.id) || null,
4982
4997
  organizationId: workflow.organization,
@@ -4985,7 +5000,13 @@ var shopify_default2 = {
4985
5000
  if (!(context == null ? void 0 : context.id)) return { message: "Skipped \u2014 product webhook payload had no id.", request: request2, response: { skipped: true }, skipped: true };
4986
5001
  if (!connection2.shop) return { message: "Skipped \u2014 Shopify connection is missing shop domain.", request: request2, response: { skipped: true }, skipped: true };
4987
5002
  const providerId = "gid://shopify/Product/" + context.id;
4988
- const productId = mintId();
5003
+ const query = {
5004
+ "provider.id": providerId,
5005
+ "provider.slug": "shopify",
5006
+ "source.domain": connection2.shop
5007
+ };
5008
+ const existing = (read == null ? void 0 : read.get) ? await read.get({ collection: "product", query }) : null;
5009
+ const productId = (existing == null ? void 0 : existing.id) || mintId();
4989
5010
  return {
4990
5011
  enqueues: [{
4991
5012
  data: { product: productId, providerId, shop: connection2.shop },
@@ -5015,11 +5036,7 @@ var shopify_default2 = {
5015
5036
  },
5016
5037
  operation: "update",
5017
5038
  options: { upsert: true },
5018
- query: {
5019
- "provider.id": providerId,
5020
- "provider.slug": "shopify",
5021
- "source.domain": connection2.shop
5022
- }
5039
+ query
5023
5040
  }]
5024
5041
  };
5025
5042
  }
@@ -5289,7 +5306,7 @@ var shopify_default2 = {
5289
5306
  options: { jobId: "connection.update.register." + workflow.connection + "." + randomUUID2() },
5290
5307
  queue: "connection"
5291
5308
  }],
5292
- message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
5309
+ message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, scopes reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, scopes reconciled." : "Health check passed \u2014 ping ok, scopes reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
5293
5310
  request: request2,
5294
5311
  response: {
5295
5312
  billingProbeFailed,
package/package.json CHANGED
@@ -226,5 +226,5 @@
226
226
  "prepublishOnly": ". \"$HOME/.nvm/nvm.sh\" && nvm use && tsup && node --test"
227
227
  },
228
228
  "types": "dist/index.d.ts",
229
- "version": "0.0.169"
229
+ "version": "0.0.171"
230
230
  }