@drawbridge/drawbridge-utils 0.0.171 → 0.0.173

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.
@@ -527,6 +527,7 @@ var row = ({ connection: connection2, data: data2, manifest, row: described }) =
527
527
  };
528
528
  };
529
529
  var segmentRowWrites = ({ connection: connection2, data: data2, manifest, row: described, segment }) => {
530
+ if ((described == null ? void 0 : described.id) === void 0 || (described == null ? void 0 : described.id) === null) return [];
530
531
  const built = row({ connection: connection2, data: data2, manifest, row: described });
531
532
  return [
532
533
  // PUSH IF ABSENT. The $ne guard is what makes a second concurrent register
@@ -2076,16 +2077,16 @@ var deliveryWrite = ({ at, code: code2, notification, permanent, provider, reaso
2076
2077
  };
2077
2078
  };
2078
2079
  var summariseEvents = (events) => {
2079
- const list = Array.isArray(events) ? events : [];
2080
- if (!list.length) return { message: "SendGrid delivered an empty event batch.", skipped: true };
2080
+ const list2 = Array.isArray(events) ? events : [];
2081
+ if (!list2.length) return { message: "SendGrid delivered an empty event batch.", skipped: true };
2081
2082
  const counts = {};
2082
- for (const { event } of list) {
2083
+ for (const { event } of list2) {
2083
2084
  const name = event || "unknown";
2084
2085
  counts[name] = (counts[name] || 0) + 1;
2085
2086
  }
2086
2087
  const tally = Object.entries(counts).map(([name, count]) => count + " " + name).join(", ");
2087
- const refused = list.filter(({ event }) => REFUSALS.has(event)).slice(0, 5).map(({ email, reason, response, status }) => email + " \u2014 " + (reason || response || status || "no reason given"));
2088
- const writes = list.map((event) => {
2088
+ const refused = list2.filter(({ event }) => REFUSALS.has(event)).slice(0, 5).map(({ email, reason, response, status }) => email + " \u2014 " + (reason || response || status || "no reason given"));
2089
+ const writes = list2.map((event) => {
2089
2090
  var _a;
2090
2091
  return deliveryWrite({
2091
2092
  at: event.timestamp ? new Date(event.timestamp * 1e3) : void 0,
@@ -2105,7 +2106,7 @@ var summariseEvents = (events) => {
2105
2106
  });
2106
2107
  }).filter(Boolean);
2107
2108
  return {
2108
- message: [list.length + " SendGrid event(s): " + tally, ...refused].join(" | "),
2109
+ message: [list2.length + " SendGrid event(s): " + tally, ...refused].join(" | "),
2109
2110
  ...writes.length ? { writes } : { skipped: true }
2110
2111
  };
2111
2112
  };
@@ -2972,6 +2973,14 @@ var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
2972
2973
  <path d="M365.047 327.038H134.954V172.964H365.047L316.856 250.001L365.047 327.038Z" fill="#232121"/>
2973
2974
  </svg>`;
2974
2975
 
2976
+ // lib/connections/scopes.js
2977
+ var list = (value) => (Array.isArray(value) ? value.flatMap((entry) => String(entry).split(/\s+/)) : String(value ?? "").split(/\s+/)).map((entry) => entry.trim()).filter(Boolean);
2978
+ var missingScopes = ({ granted, required }) => {
2979
+ if (granted === null || granted === void 0 || granted === "") return null;
2980
+ const held = new Set(list(granted));
2981
+ return list(required).filter((scope) => !held.has(scope));
2982
+ };
2983
+
2975
2984
  // lib/connections/providers/klaviyo.js
2976
2985
  var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) => {
2977
2986
  const response = await fetcher("https://a.klaviyo.com/api" + path, {
@@ -2999,8 +3008,10 @@ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) =>
2999
3008
  }
3000
3009
  return response.status === 204 ? null : response.json();
3001
3010
  };
3002
- var segmentName = (title) => "Drawbridge: " + title;
3003
- var canManageSegments = (settings) => String((settings == null ? void 0 : settings.scope) || "").split(/\s+/).includes("segments:write");
3011
+ var segmentName = (title, id) => "Drawbridge: " + title + " (" + String(id).slice(-6) + ")";
3012
+ var SCOPES = "accounts:read lists:read lists:write profiles:read profiles:write segments:read segments:write";
3013
+ var missing = (settings) => missingScopes({ granted: settings == null ? void 0 : settings.scope, required: SCOPES });
3014
+ var canManageSegments = (settings) => !(missing(settings) || []).includes("segments:write");
3004
3015
  var klaviyo_default2 = {
3005
3016
  // OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
3006
3017
  // exchange without a code_verifier matching the challenge the consent
@@ -3044,7 +3055,7 @@ var klaviyo_default2 = {
3044
3055
  // Update and Delete Segment each list `segments:write`
3045
3056
  // (raw.githubusercontent.com/klaviyo/openapi/main/openapi/stable.json,
3046
3057
  // revision 2026-07-15, fetched 2026-09-11).
3047
- scopes: "accounts:read lists:read lists:write profiles:read profiles:write segments:read segments:write",
3058
+ scopes: SCOPES,
3048
3059
  // EVERY VENDOR URL, in one place. `revoke` used to be a literal inside
3049
3060
  // the disconnect hook — three vendor addresses, two of them declared,
3050
3061
  // which is exactly the kind of split that goes unnoticed.
@@ -3225,8 +3236,14 @@ var klaviyo_default2 = {
3225
3236
  });
3226
3237
  return { ok: Boolean(token) };
3227
3238
  },
3228
- // Klaviyo scopes are fixed at app level and re-consented, not drifted.
3229
- scopes: false,
3239
+ // THEY DO DRIFT, and the comment here used to say they could not. Klaviyo
3240
+ // scopes are fixed on the APP, so adding one re-consents every NEW grant
3241
+ // and leaves every EXISTING one exactly as narrow as it was — with no
3242
+ // error, no webhook, and nothing that notices. That is what left grants
3243
+ // authenticating perfectly while silently unable to manage a segment.
3244
+ //
3245
+ // The same slot Shopify answers, so one caller can ask any vendor.
3246
+ scopes: ({ scope }) => missingScopes({ granted: scope, required: SCOPES }),
3230
3247
  // KLAVIYO REQUIRES HTTP BASIC on the token endpoint and rejects the same
3231
3248
  // client_id/client_secret pair as body fields. Everything else about the
3232
3249
  // request is standard, so this is the shared implementation told the one
@@ -3247,9 +3264,9 @@ var klaviyo_default2 = {
3247
3264
  remove: false,
3248
3265
  sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
3249
3266
  var _a, _b, _c, _d, _e;
3250
- const list = settings == null ? void 0 : settings.list;
3267
+ const list2 = settings == null ? void 0 : settings.list;
3251
3268
  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 };
3269
+ if (!list2) return { message: "No Klaviyo list is chosen for this connection.", request: request2, skipped: true };
3253
3270
  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
3271
  if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3255
3272
  const person = (context == null ? void 0 : context.contact) || null;
@@ -3328,7 +3345,7 @@ var klaviyo_default2 = {
3328
3345
  }]
3329
3346
  }
3330
3347
  },
3331
- relationships: { list: { data: { id: list, type: "list" } } },
3348
+ relationships: { list: { data: { id: list2, type: "list" } } },
3332
3349
  type: "profile-subscription-bulk-create-job"
3333
3350
  }
3334
3351
  },
@@ -3374,7 +3391,7 @@ var klaviyo_default2 = {
3374
3391
  skipped: true
3375
3392
  };
3376
3393
  }
3377
- const name = segmentName(segment.title);
3394
+ const name = segmentName(segment.title, segment.id);
3378
3395
  const existing = segmentRowFor({ connection: connection2, segment });
3379
3396
  let id = null;
3380
3397
  if (existing == null ? void 0 : existing.id) {
@@ -3501,7 +3518,7 @@ var klaviyo_default2 = {
3501
3518
  // The one call below proves the minted token is HONOURED — mint and
3502
3519
  // acceptance are different facts, and /accounts is already the call
3503
3520
  // the connect flow makes (auth.connect), so it needs no new scope.
3504
- health: async ({ connection: connection2, token }, { fetcher, read } = {}) => {
3521
+ health: async ({ connection: connection2, settings, token }, { fetcher, read } = {}) => {
3505
3522
  const request2 = { connectionId: connection2.id };
3506
3523
  try {
3507
3524
  await api2("/accounts", { fetcher, token });
@@ -3553,8 +3570,8 @@ var klaviyo_default2 = {
3553
3570
  let pages = 0;
3554
3571
  while (next && audiences.length < limit && pages < 20) {
3555
3572
  const body = await api2(next, { fetcher, token });
3556
- for (const list of (body == null ? void 0 : body.data) || []) {
3557
- audiences.push({ id: list.id, title: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id });
3573
+ for (const list2 of (body == null ? void 0 : body.data) || []) {
3574
+ audiences.push({ id: list2.id, title: ((_a = list2 == null ? void 0 : list2.attributes) == null ? void 0 : _a.name) || list2.id });
3558
3575
  }
3559
3576
  const link = (_b = body == null ? void 0 : body.links) == null ? void 0 : _b.next;
3560
3577
  next = link ? String(link).replace(/^https:\/\/a\.klaviyo\.com\/api/, "") : null;
@@ -3720,13 +3737,14 @@ var klaviyo_default2 = {
3720
3737
  var _a;
3721
3738
  if (!["active", "pending"].includes(data2 == null ? void 0 : data2.status)) return [];
3722
3739
  return [
3723
- // A connection made before segments were requested is authenticated and
3724
- // cannot manage them, and no error surfaces anywhere else the register
3725
- // runs skip rather than fail.
3726
- ...canManageSegments(data2 == null ? void 0 : data2.settings) ? [] : [{
3727
- message: "Drawbridge now keeps a Klaviyo segment in step with each of your Drawbridge segments. Reconnect Klaviyo to allow it.",
3728
- title: "Reconnect Klaviyo"
3729
- }],
3740
+ // THE MISSING SEGMENT SCOPE IS NOT HERE ANY MORE. It is an ERROR entry,
3741
+ // written by the health check, because a task is quiet: it renders only in
3742
+ // the body of this connection's own page, so a merchant who never opens it
3743
+ // never learns that their segments stopped being published. An error entry
3744
+ // reaches the card and the organization checklist too.
3745
+ //
3746
+ // Deliberately not a status change — see the health hook for why that
3747
+ // would stop the contact sync that still works.
3730
3748
  ...((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [{
3731
3749
  message: "Choose which Klaviyo list your contacts should sync into. Until you do, nothing is being synced.",
3732
3750
  title: "Choose a list"
@@ -3781,7 +3799,10 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3781
3799
  };
3782
3800
  var subscriberHash = (email) => (0, import_node_crypto5.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
3783
3801
  var TAG_NAME_LIMIT = 100;
3784
- var tagName = (title) => ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT);
3802
+ var tagName = (title, id) => {
3803
+ const suffix = " (" + String(id).slice(-6) + ")";
3804
+ return ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT - suffix.length) + suffix;
3805
+ };
3785
3806
  var mailchimp_default2 = {
3786
3807
  // OAUTH 2, authorization code. Every url below is quoted from
3787
3808
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -3962,14 +3983,17 @@ var mailchimp_default2 = {
3962
3983
  }
3963
3984
  },
3964
3985
  {
3965
- $project: { _id: 0, title: 1 }
3986
+ // THE ID AS WELL AS THE TITLE, because the tag name carries it —
3987
+ // see tagName. Without it every name here would end in the
3988
+ // string 'undefined' and match nothing register wrote.
3989
+ $project: { _id: 0, id: 1, title: 1 }
3966
3990
  }
3967
3991
  ]
3968
3992
  });
3969
- const joined = new Set(segments.map((entry) => entry.title));
3970
- const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
3971
- name: tagName(title),
3972
- status: joined.has(title) ? "active" : "inactive"
3993
+ const joined = new Set(segments.map((entry) => entry.id));
3994
+ const tags = (owned || []).filter((entry) => entry.id && entry.title).map((entry) => ({
3995
+ name: tagName(entry.title, entry.id),
3996
+ status: joined.has(entry.id) ? "active" : "inactive"
3973
3997
  }));
3974
3998
  if (tags.length > 0) {
3975
3999
  await api3("/lists/" + audience + "/members/" + hash + "/tags", {
@@ -4021,7 +4045,7 @@ var mailchimp_default2 = {
4021
4045
  if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
4022
4046
  const segment = await currentSegment({ read, segment: context == null ? void 0 : context.segment });
4023
4047
  if (!(segment == null ? void 0 : segment.id) || segment.system) return { message: "That segment is not one this connection publishes.", skipped: true };
4024
- const name = tagName(segment.title);
4048
+ const name = tagName(segment.title, segment.id);
4025
4049
  const existing = segmentRowFor({ connection: connection2, segment });
4026
4050
  let id = null;
4027
4051
  if (existing == null ? void 0 : existing.id) {
@@ -4127,7 +4151,7 @@ var mailchimp_default2 = {
4127
4151
  "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
4128
4152
  { dc: settings == null ? void 0 : settings.dc, fetcher, token }
4129
4153
  );
4130
- const audiences = ((body == null ? void 0 : body.lists) || []).map((list) => ({ id: list.id, title: (list == null ? void 0 : list.name) || list.id }));
4154
+ const audiences = ((body == null ? void 0 : body.lists) || []).map((list2) => ({ id: list2.id, title: (list2 == null ? void 0 : list2.name) || list2.id }));
4131
4155
  const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
4132
4156
  const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
4133
4157
  const nextOffset = offset + count;
@@ -4593,10 +4617,6 @@ var shopify_default2 = {
4593
4617
  if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing.", request: request2, response: { skipped: true }, skipped: true };
4594
4618
  if (!(discount == null ? void 0 : discount.id)) return { message: "Discount is not configured on this step.", request: request2, response: { skipped: true }, skipped: true };
4595
4619
  const adminAccessToken = await adminToken();
4596
- if (!context.shopifyCustomerId) {
4597
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain: connection2.shop, email: context.email });
4598
- if (!(customer == null ? void 0 : customer.id)) return { message: "Shopify did not return a customer id \u2014 create/lookup failed.", request: request2, response: { skipped: true }, skipped: true };
4599
- }
4600
4620
  const discountCode = await shopify.admin.createDiscountCode({
4601
4621
  adminAccessToken,
4602
4622
  code: "DB-" + generateDiscountCode(),
package/dist/providers.js CHANGED
@@ -478,6 +478,7 @@ var row = ({ connection: connection2, data: data2, manifest, row: described }) =
478
478
  };
479
479
  };
480
480
  var segmentRowWrites = ({ connection: connection2, data: data2, manifest, row: described, segment }) => {
481
+ if ((described == null ? void 0 : described.id) === void 0 || (described == null ? void 0 : described.id) === null) return [];
481
482
  const built = row({ connection: connection2, data: data2, manifest, row: described });
482
483
  return [
483
484
  // PUSH IF ABSENT. The $ne guard is what makes a second concurrent register
@@ -2027,16 +2028,16 @@ var deliveryWrite = ({ at, code: code2, notification, permanent, provider, reaso
2027
2028
  };
2028
2029
  };
2029
2030
  var summariseEvents = (events) => {
2030
- const list = Array.isArray(events) ? events : [];
2031
- if (!list.length) return { message: "SendGrid delivered an empty event batch.", skipped: true };
2031
+ const list2 = Array.isArray(events) ? events : [];
2032
+ if (!list2.length) return { message: "SendGrid delivered an empty event batch.", skipped: true };
2032
2033
  const counts = {};
2033
- for (const { event } of list) {
2034
+ for (const { event } of list2) {
2034
2035
  const name = event || "unknown";
2035
2036
  counts[name] = (counts[name] || 0) + 1;
2036
2037
  }
2037
2038
  const tally = Object.entries(counts).map(([name, count]) => count + " " + name).join(", ");
2038
- const refused = list.filter(({ event }) => REFUSALS.has(event)).slice(0, 5).map(({ email, reason, response, status }) => email + " \u2014 " + (reason || response || status || "no reason given"));
2039
- const writes = list.map((event) => {
2039
+ const refused = list2.filter(({ event }) => REFUSALS.has(event)).slice(0, 5).map(({ email, reason, response, status }) => email + " \u2014 " + (reason || response || status || "no reason given"));
2040
+ const writes = list2.map((event) => {
2040
2041
  var _a;
2041
2042
  return deliveryWrite({
2042
2043
  at: event.timestamp ? new Date(event.timestamp * 1e3) : void 0,
@@ -2056,7 +2057,7 @@ var summariseEvents = (events) => {
2056
2057
  });
2057
2058
  }).filter(Boolean);
2058
2059
  return {
2059
- message: [list.length + " SendGrid event(s): " + tally, ...refused].join(" | "),
2060
+ message: [list2.length + " SendGrid event(s): " + tally, ...refused].join(" | "),
2060
2061
  ...writes.length ? { writes } : { skipped: true }
2061
2062
  };
2062
2063
  };
@@ -2923,6 +2924,14 @@ var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
2923
2924
  <path d="M365.047 327.038H134.954V172.964H365.047L316.856 250.001L365.047 327.038Z" fill="#232121"/>
2924
2925
  </svg>`;
2925
2926
 
2927
+ // lib/connections/scopes.js
2928
+ var list = (value) => (Array.isArray(value) ? value.flatMap((entry) => String(entry).split(/\s+/)) : String(value ?? "").split(/\s+/)).map((entry) => entry.trim()).filter(Boolean);
2929
+ var missingScopes = ({ granted, required }) => {
2930
+ if (granted === null || granted === void 0 || granted === "") return null;
2931
+ const held = new Set(list(granted));
2932
+ return list(required).filter((scope) => !held.has(scope));
2933
+ };
2934
+
2926
2935
  // lib/connections/providers/klaviyo.js
2927
2936
  var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) => {
2928
2937
  const response = await fetcher("https://a.klaviyo.com/api" + path, {
@@ -2950,8 +2959,10 @@ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) =>
2950
2959
  }
2951
2960
  return response.status === 204 ? null : response.json();
2952
2961
  };
2953
- var segmentName = (title) => "Drawbridge: " + title;
2954
- var canManageSegments = (settings) => String((settings == null ? void 0 : settings.scope) || "").split(/\s+/).includes("segments:write");
2962
+ var segmentName = (title, id) => "Drawbridge: " + title + " (" + String(id).slice(-6) + ")";
2963
+ var SCOPES = "accounts:read lists:read lists:write profiles:read profiles:write segments:read segments:write";
2964
+ var missing = (settings) => missingScopes({ granted: settings == null ? void 0 : settings.scope, required: SCOPES });
2965
+ var canManageSegments = (settings) => !(missing(settings) || []).includes("segments:write");
2955
2966
  var klaviyo_default2 = {
2956
2967
  // OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
2957
2968
  // exchange without a code_verifier matching the challenge the consent
@@ -2995,7 +3006,7 @@ var klaviyo_default2 = {
2995
3006
  // Update and Delete Segment each list `segments:write`
2996
3007
  // (raw.githubusercontent.com/klaviyo/openapi/main/openapi/stable.json,
2997
3008
  // revision 2026-07-15, fetched 2026-09-11).
2998
- scopes: "accounts:read lists:read lists:write profiles:read profiles:write segments:read segments:write",
3009
+ scopes: SCOPES,
2999
3010
  // EVERY VENDOR URL, in one place. `revoke` used to be a literal inside
3000
3011
  // the disconnect hook — three vendor addresses, two of them declared,
3001
3012
  // which is exactly the kind of split that goes unnoticed.
@@ -3176,8 +3187,14 @@ var klaviyo_default2 = {
3176
3187
  });
3177
3188
  return { ok: Boolean(token) };
3178
3189
  },
3179
- // Klaviyo scopes are fixed at app level and re-consented, not drifted.
3180
- scopes: false,
3190
+ // THEY DO DRIFT, and the comment here used to say they could not. Klaviyo
3191
+ // scopes are fixed on the APP, so adding one re-consents every NEW grant
3192
+ // and leaves every EXISTING one exactly as narrow as it was — with no
3193
+ // error, no webhook, and nothing that notices. That is what left grants
3194
+ // authenticating perfectly while silently unable to manage a segment.
3195
+ //
3196
+ // The same slot Shopify answers, so one caller can ask any vendor.
3197
+ scopes: ({ scope }) => missingScopes({ granted: scope, required: SCOPES }),
3181
3198
  // KLAVIYO REQUIRES HTTP BASIC on the token endpoint and rejects the same
3182
3199
  // client_id/client_secret pair as body fields. Everything else about the
3183
3200
  // request is standard, so this is the shared implementation told the one
@@ -3198,9 +3215,9 @@ var klaviyo_default2 = {
3198
3215
  remove: false,
3199
3216
  sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
3200
3217
  var _a, _b, _c, _d, _e;
3201
- const list = settings == null ? void 0 : settings.list;
3218
+ const list2 = settings == null ? void 0 : settings.list;
3202
3219
  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 };
3220
+ if (!list2) return { message: "No Klaviyo list is chosen for this connection.", request: request2, skipped: true };
3204
3221
  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
3222
  if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3206
3223
  const person = (context == null ? void 0 : context.contact) || null;
@@ -3279,7 +3296,7 @@ var klaviyo_default2 = {
3279
3296
  }]
3280
3297
  }
3281
3298
  },
3282
- relationships: { list: { data: { id: list, type: "list" } } },
3299
+ relationships: { list: { data: { id: list2, type: "list" } } },
3283
3300
  type: "profile-subscription-bulk-create-job"
3284
3301
  }
3285
3302
  },
@@ -3325,7 +3342,7 @@ var klaviyo_default2 = {
3325
3342
  skipped: true
3326
3343
  };
3327
3344
  }
3328
- const name = segmentName(segment.title);
3345
+ const name = segmentName(segment.title, segment.id);
3329
3346
  const existing = segmentRowFor({ connection: connection2, segment });
3330
3347
  let id = null;
3331
3348
  if (existing == null ? void 0 : existing.id) {
@@ -3452,7 +3469,7 @@ var klaviyo_default2 = {
3452
3469
  // The one call below proves the minted token is HONOURED — mint and
3453
3470
  // acceptance are different facts, and /accounts is already the call
3454
3471
  // the connect flow makes (auth.connect), so it needs no new scope.
3455
- health: async ({ connection: connection2, token }, { fetcher, read } = {}) => {
3472
+ health: async ({ connection: connection2, settings, token }, { fetcher, read } = {}) => {
3456
3473
  const request2 = { connectionId: connection2.id };
3457
3474
  try {
3458
3475
  await api2("/accounts", { fetcher, token });
@@ -3504,8 +3521,8 @@ var klaviyo_default2 = {
3504
3521
  let pages = 0;
3505
3522
  while (next && audiences.length < limit && pages < 20) {
3506
3523
  const body = await api2(next, { fetcher, token });
3507
- for (const list of (body == null ? void 0 : body.data) || []) {
3508
- audiences.push({ id: list.id, title: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id });
3524
+ for (const list2 of (body == null ? void 0 : body.data) || []) {
3525
+ audiences.push({ id: list2.id, title: ((_a = list2 == null ? void 0 : list2.attributes) == null ? void 0 : _a.name) || list2.id });
3509
3526
  }
3510
3527
  const link = (_b = body == null ? void 0 : body.links) == null ? void 0 : _b.next;
3511
3528
  next = link ? String(link).replace(/^https:\/\/a\.klaviyo\.com\/api/, "") : null;
@@ -3671,13 +3688,14 @@ var klaviyo_default2 = {
3671
3688
  var _a;
3672
3689
  if (!["active", "pending"].includes(data2 == null ? void 0 : data2.status)) return [];
3673
3690
  return [
3674
- // A connection made before segments were requested is authenticated and
3675
- // cannot manage them, and no error surfaces anywhere else the register
3676
- // runs skip rather than fail.
3677
- ...canManageSegments(data2 == null ? void 0 : data2.settings) ? [] : [{
3678
- message: "Drawbridge now keeps a Klaviyo segment in step with each of your Drawbridge segments. Reconnect Klaviyo to allow it.",
3679
- title: "Reconnect Klaviyo"
3680
- }],
3691
+ // THE MISSING SEGMENT SCOPE IS NOT HERE ANY MORE. It is an ERROR entry,
3692
+ // written by the health check, because a task is quiet: it renders only in
3693
+ // the body of this connection's own page, so a merchant who never opens it
3694
+ // never learns that their segments stopped being published. An error entry
3695
+ // reaches the card and the organization checklist too.
3696
+ //
3697
+ // Deliberately not a status change — see the health hook for why that
3698
+ // would stop the contact sync that still works.
3681
3699
  ...((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [{
3682
3700
  message: "Choose which Klaviyo list your contacts should sync into. Until you do, nothing is being synced.",
3683
3701
  title: "Choose a list"
@@ -3732,7 +3750,10 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3732
3750
  };
3733
3751
  var subscriberHash = (email) => createHash2("md5").update(String(email).trim().toLowerCase()).digest("hex");
3734
3752
  var TAG_NAME_LIMIT = 100;
3735
- var tagName = (title) => ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT);
3753
+ var tagName = (title, id) => {
3754
+ const suffix = " (" + String(id).slice(-6) + ")";
3755
+ return ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT - suffix.length) + suffix;
3756
+ };
3736
3757
  var mailchimp_default2 = {
3737
3758
  // OAUTH 2, authorization code. Every url below is quoted from
3738
3759
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -3913,14 +3934,17 @@ var mailchimp_default2 = {
3913
3934
  }
3914
3935
  },
3915
3936
  {
3916
- $project: { _id: 0, title: 1 }
3937
+ // THE ID AS WELL AS THE TITLE, because the tag name carries it —
3938
+ // see tagName. Without it every name here would end in the
3939
+ // string 'undefined' and match nothing register wrote.
3940
+ $project: { _id: 0, id: 1, title: 1 }
3917
3941
  }
3918
3942
  ]
3919
3943
  });
3920
- const joined = new Set(segments.map((entry) => entry.title));
3921
- const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
3922
- name: tagName(title),
3923
- status: joined.has(title) ? "active" : "inactive"
3944
+ const joined = new Set(segments.map((entry) => entry.id));
3945
+ const tags = (owned || []).filter((entry) => entry.id && entry.title).map((entry) => ({
3946
+ name: tagName(entry.title, entry.id),
3947
+ status: joined.has(entry.id) ? "active" : "inactive"
3924
3948
  }));
3925
3949
  if (tags.length > 0) {
3926
3950
  await api3("/lists/" + audience + "/members/" + hash + "/tags", {
@@ -3972,7 +3996,7 @@ var mailchimp_default2 = {
3972
3996
  if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
3973
3997
  const segment = await currentSegment({ read, segment: context == null ? void 0 : context.segment });
3974
3998
  if (!(segment == null ? void 0 : segment.id) || segment.system) return { message: "That segment is not one this connection publishes.", skipped: true };
3975
- const name = tagName(segment.title);
3999
+ const name = tagName(segment.title, segment.id);
3976
4000
  const existing = segmentRowFor({ connection: connection2, segment });
3977
4001
  let id = null;
3978
4002
  if (existing == null ? void 0 : existing.id) {
@@ -4078,7 +4102,7 @@ var mailchimp_default2 = {
4078
4102
  "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
4079
4103
  { dc: settings == null ? void 0 : settings.dc, fetcher, token }
4080
4104
  );
4081
- const audiences = ((body == null ? void 0 : body.lists) || []).map((list) => ({ id: list.id, title: (list == null ? void 0 : list.name) || list.id }));
4105
+ const audiences = ((body == null ? void 0 : body.lists) || []).map((list2) => ({ id: list2.id, title: (list2 == null ? void 0 : list2.name) || list2.id }));
4082
4106
  const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
4083
4107
  const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
4084
4108
  const nextOffset = offset + count;
@@ -4544,10 +4568,6 @@ var shopify_default2 = {
4544
4568
  if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing.", request: request2, response: { skipped: true }, skipped: true };
4545
4569
  if (!(discount == null ? void 0 : discount.id)) return { message: "Discount is not configured on this step.", request: request2, response: { skipped: true }, skipped: true };
4546
4570
  const adminAccessToken = await adminToken();
4547
- if (!context.shopifyCustomerId) {
4548
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain: connection2.shop, email: context.email });
4549
- if (!(customer == null ? void 0 : customer.id)) return { message: "Shopify did not return a customer id \u2014 create/lookup failed.", request: request2, response: { skipped: true }, skipped: true };
4550
- }
4551
4571
  const discountCode = await shopify.admin.createDiscountCode({
4552
4572
  adminAccessToken,
4553
4573
  code: "DB-" + generateDiscountCode(),
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "tinycolor2": "1.6.0"
14
14
  },
15
15
  "devDependencies": {
16
- "@drawbridge/drawbridge-agents": "0.1.47",
16
+ "@drawbridge/drawbridge-agents": "0.1.59",
17
17
  "@node-oauth/oauth2-server": "5.3.0",
18
18
  "tsup": "8.5.1",
19
19
  "typescript": "5.9.3",
@@ -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.171"
229
+ "version": "0.0.173"
230
230
  }