@drawbridge/drawbridge-utils 0.0.162 → 0.0.164

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.
@@ -914,10 +914,15 @@ var attentive = {
914
914
  'Attentive accepts these updates and applies them in the background, so a contact appears in your segment shortly after the sync rather than the instant it runs.'
915
915
  ],
916
916
  excerpt : 'Sync your Drawbridge contacts into an Attentive segment.',
917
+ // NAMES THE SEGMENT STEP, because `status` below gates on it: a grant with
918
+ // no segment chosen sits at Pending, and a guide that stops at the consent
919
+ // leaves a merchant looking at a connection they think is broken. Same
920
+ // omission Klaviyo's guide already carries a note about.
917
921
  guide : [
918
922
  'Press Connect. Drawbridge sends you to Attentive to approve access.',
919
923
  'Sign in to Attentive if you are not already, and authorize the permissions listed.',
920
- 'You are returned here choose which Attentive segment your contacts should sync into.'
924
+ 'You come back here to choose which Attentive segment your contacts should sync into.',
925
+ 'The connection shows Pending until you choose a segment, then Active.'
921
926
  ]
922
927
  },
923
928
  // A contact destination, like Klaviyo and Mailchimp — a merchant could
@@ -1239,6 +1244,10 @@ var attentive = {
1239
1244
 
1240
1245
  queue : 'connection',
1241
1246
 
1247
+ // Attentive applies the update in the background and returns no id we
1248
+ // could hand on, so there is nothing for a later step to interpolate.
1249
+ returns : [],
1250
+
1242
1251
  // Nothing for a merchant to configure on the step itself — the segment
1243
1252
  // is chosen once on the connection. Declared empty rather than omitted,
1244
1253
  // so "this step takes no settings" and "nobody thought about settings"
@@ -2236,6 +2245,11 @@ var drawbridge = {
2236
2245
  message : interpolate( step.settings?.message, values ),
2237
2246
  organization : workflow.organization,
2238
2247
  send : { type : 'email', email : member.email },
2248
+ // The editor's plain rendering, interpolated the same way so
2249
+ // {{count}} resolves in the text part too. Spread rather than
2250
+ // written as null: a step saved before `text` existed has none,
2251
+ // and the send path strips the markup itself for those.
2252
+ ...( step.settings?.text && { text : interpolate( step.settings.text, values ) }),
2239
2253
  title : interpolate( step.settings?.subject, values ),
2240
2254
  workflow : workflow.id
2241
2255
  }) )
@@ -2423,6 +2437,10 @@ var drawbridge = {
2423
2437
  message : interpolate( step.settings?.message, context ),
2424
2438
  organization : workflow.organization,
2425
2439
  send : { type : 'email', email : to },
2440
+ // The editor's plain rendering, so the text/plain part of this
2441
+ // email is what the merchant wrote rather than a regex's guess at
2442
+ // it. See the digest above for why it is spread, not nulled.
2443
+ ...( step.settings?.text && { text : interpolate( step.settings.text, context ) }),
2426
2444
  title : interpolate( step.settings?.subject, context ),
2427
2445
  workflow : workflow.id
2428
2446
  })
@@ -2978,6 +2996,11 @@ var drawbridge = {
2978
2996
  hook : 'email.digest',
2979
2997
  key : 'Email — Digest',
2980
2998
  queue : 'notification',
2999
+ // Nothing for a later step to interpolate. Declared empty rather than
3000
+ // omitted, so "this step leaves nothing behind" and "nobody thought
3001
+ // about it" stay different statements — the second is what emptied the
3002
+ // builder's Variables menu for every step in the product.
3003
+ returns : [],
2981
3004
  settings : {
2982
3005
  // The organization OWNER is always a recipient, resolved by the
2983
3006
  // hook, so this is additional recipients rather than the list. It
@@ -2986,7 +3009,22 @@ var drawbridge = {
2986
3009
  // pick and could never save the step.
2987
3010
  members : { of : 'string', type : 'array' },
2988
3011
  message : { max : 2000, required : true, type : 'string' },
2989
- subject : { max : 150, required : true, type : 'string' }
3012
+ // THE LAST SPAM CHECK, stored beside the copy it measured. Shape is
3013
+ // pinned no further than the pair that proves the match, which is the
3014
+ // only part the api reads; the rest is SpamAssassin's own output and
3015
+ // is stored as it arrives. Same declaration the campaign's draw
3016
+ // notification validates against.
3017
+ //
3018
+ // Undeclared, this was silently dropped at submit — the settings
3019
+ // validator is noUnknown().strict(), so the dashboard's step form
3020
+ // mapped the key away rather than 400 — and every open of a workflow
3021
+ // step re-ran a check against a free service to be told what the last
3022
+ // open had already been told.
3023
+ score : { shape : { message : { type : 'string' }, subject : { type : 'string' } }, type : 'object' },
3024
+ subject : { max : 150, required : true, type : 'string' },
3025
+ // The editor's plain rendering of `message`, written beside it rather
3026
+ // than reconstructed by running a regex over the markup at send time.
3027
+ text : { max : 2000, type : 'string' }
2990
3028
  },
2991
3029
  triggers : [ 'schedule.day', 'schedule.week', 'schedule.month' ],
2992
3030
  usage : { actions : 0 }
@@ -3008,6 +3046,7 @@ var drawbridge = {
3008
3046
  hook : 'email.notify',
3009
3047
  key : 'Email — Notification',
3010
3048
  queue : 'notification',
3049
+ returns : [],
3011
3050
  settings : {
3012
3051
  members : { of : 'string', type : 'array' },
3013
3052
  message : { max : 2000, type : 'string' },
@@ -3025,9 +3064,14 @@ var drawbridge = {
3025
3064
  hook : 'email.send',
3026
3065
  key : 'Email — Send email',
3027
3066
  queue : 'notification',
3067
+ returns : [],
3028
3068
  settings : {
3029
3069
  message : { max : 2000, required : true, type : 'string' },
3030
- subject : { max : 150, required : true, type : 'string' }
3070
+ // See email.digest above the last spam check, and the editor's
3071
+ // plain rendering of the message.
3072
+ score : { shape : { message : { type : 'string' }, subject : { type : 'string' } }, type : 'object' },
3073
+ subject : { max : 150, required : true, type : 'string' },
3074
+ text : { max : 2000, type : 'string' }
3031
3075
  },
3032
3076
  triggers : [ 'lead.insert' ],
3033
3077
  // ONE SOURCE FOR THE PRICE. lib/pricing.js is the index of every
@@ -3813,6 +3857,14 @@ var klaviyo = {
3813
3857
 
3814
3858
  queue : 'connection',
3815
3859
 
3860
+ // The key the hook puts in `context`, which the runner merges into the
3861
+ // run so a later step can interpolate {{klaviyoProfileId}}. Declared
3862
+ // beside the hook that writes it — see the note on Shopify's
3863
+ // commerce.code, which is where this came back from.
3864
+ returns : [
3865
+ { key : 'klaviyoProfileId', label : 'Klaviyo Profile ID' }
3866
+ ],
3867
+
3816
3868
  // Nothing for a merchant to configure on the step itself — the list
3817
3869
  // is chosen once on the connection. Declared empty rather than
3818
3870
  // omitted, so "this step takes no settings" and "nobody thought about
@@ -3978,10 +4030,15 @@ var mailchimp = {
3978
4030
  'Someone who unsubscribed inside Mailchimp keeps that choice: a resync only sets the status of a subscriber Mailchimp has never seen before.'
3979
4031
  ],
3980
4032
  excerpt : 'Sync your Drawbridge contacts into a Mailchimp audience.',
4033
+ // NAMES THE AUDIENCE STEP, because `status` below gates on it: a grant with
4034
+ // no audience chosen sits at Pending, and a guide that stops at the consent
4035
+ // leaves a merchant looking at a connection they think is broken.
3981
4036
  guide : [
3982
4037
  'Press Connect. Drawbridge sends you to Mailchimp to approve access.',
3983
4038
  'Sign in to Mailchimp if you are not already, and choose the account to connect.',
3984
- 'You come back here to pick the audience your contacts should sync into.'
4039
+ 'You come back here to pick the audience your contacts should sync into.',
4040
+ 'The connection shows Pending until you pick an audience, then Active.',
4041
+ 'You can remove Drawbridge at any time from the Authorized Apps page in your Mailchimp account.'
3985
4042
  ]
3986
4043
  },
3987
4044
  // Mailchimp and SendGrid shared a group while they were SENDERS, where an org
@@ -4332,6 +4389,12 @@ var mailchimp = {
4332
4389
 
4333
4390
  queue : 'connection',
4334
4391
 
4392
+ // The key the hook puts in `context`, for a later step to interpolate
4393
+ // as {{mailchimpMemberId}}. Same reasoning as Klaviyo's.
4394
+ returns : [
4395
+ { key : 'mailchimpMemberId', label : 'Mailchimp Member ID' }
4396
+ ],
4397
+
4335
4398
  // Nothing for a merchant to configure on the step itself — the audience
4336
4399
  // is chosen once on the connection. Declared empty rather than omitted,
4337
4400
  // so "this step takes no settings" and "nobody thought about settings"
@@ -6096,6 +6159,24 @@ var shopify = {
6096
6159
  hook : 'commerce.code',
6097
6160
  key : 'Issue a discount code',
6098
6161
  queue : 'connection',
6162
+ // WHAT THIS STEP LEAVES BEHIND FOR THE ONES AFTER IT, and the only
6163
+ // reason the builder can offer a Variables menu.
6164
+ //
6165
+ // These are the exact keys the hook puts in `context` — the runner
6166
+ // merges that into the run's context, and a later step interpolates
6167
+ // {{shopifyDiscountCode}} out of it. Declared beside the hook that
6168
+ // writes them so the two cannot drift; there is nowhere else that
6169
+ // knows both.
6170
+ //
6171
+ // They existed as a hand-written list in the api's workflow catalog
6172
+ // until it was derived from these manifests, and the derivation
6173
+ // hardcoded `returns : []` for every step. Nothing has offered a
6174
+ // variable since — an email step after a discount step had no way to
6175
+ // name the code it was supposed to send.
6176
+ returns : [
6177
+ { key : 'shopifyDiscountCode', label : 'Shopify Discount Code' },
6178
+ { key : 'shopifyDiscountId', label : 'Shopify Discount ID' }
6179
+ ],
6099
6180
  settings : {
6100
6181
  discount : {
6101
6182
  required : true,
@@ -6115,6 +6196,12 @@ var shopify = {
6115
6196
  hook : 'commerce.customer',
6116
6197
  key : 'Create customer',
6117
6198
  queue : 'connection',
6199
+ // Written on every path the hook can take — created, reused from an
6200
+ // earlier step, or reused from the lead — so a later step can rely on
6201
+ // it being there whenever this one succeeded.
6202
+ returns : [
6203
+ { key : 'shopifyCustomerId', label : 'Shopify Customer ID' }
6204
+ ],
6118
6205
  settings : {},
6119
6206
  triggers : [ 'lead.insert' ],
6120
6207
  usage : { actions : 1 }
@@ -6425,6 +6512,13 @@ var webhook = {
6425
6512
  hook : 'webhook.send',
6426
6513
  key : 'Send webhook',
6427
6514
  queue : 'webhook',
6515
+ // The receiver's response is buffered, not merged into the run — a
6516
+ // receiver owes us a status, not a document — so there is nothing here
6517
+ // for a later step to interpolate. Declared empty rather than omitted:
6518
+ // "leaves nothing behind" and "nobody thought about it" are different
6519
+ // statements, and the second one is what emptied the builder's
6520
+ // Variables menu for every step in the product.
6521
+ returns : [],
6428
6522
  settings : {
6429
6523
  url : { format : 'url', required : true, type : 'string' }
6430
6524
  },
@@ -7406,6 +7500,12 @@ const publicConnectionKeys = Object.freeze([
7406
7500
  // excerpt, guide, and any vendor redirect copy.
7407
7501
  'content',
7408
7502
  'createdAt',
7503
+ // THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
7504
+ // paused — kept, configured, and not run — as distinct from a vendor the
7505
+ // platform switched off (which the api overlays as an error). Absent means
7506
+ // on; the dashboard renders Paused from it and the sync gates refuse a paused
7507
+ // connection the way they refuse a missing one.
7508
+ 'enabled',
7409
7509
  // The connection DOCUMENT's own errors array — scope-drift entries written by
7410
7510
  // drawbridge-sync. NOT the manifest's error copy, which is content.errors:
7411
7511
  // the document is spread OVER the resolved manifest downstream, so the two
@@ -558,10 +558,15 @@ var attentive_default2 = {
558
558
  "Attentive accepts these updates and applies them in the background, so a contact appears in your segment shortly after the sync rather than the instant it runs."
559
559
  ],
560
560
  excerpt: "Sync your Drawbridge contacts into an Attentive segment.",
561
+ // NAMES THE SEGMENT STEP, because `status` below gates on it: a grant with
562
+ // no segment chosen sits at Pending, and a guide that stops at the consent
563
+ // leaves a merchant looking at a connection they think is broken. Same
564
+ // omission Klaviyo's guide already carries a note about.
561
565
  guide: [
562
566
  "Press Connect. Drawbridge sends you to Attentive to approve access.",
563
567
  "Sign in to Attentive if you are not already, and authorize the permissions listed.",
564
- "You are returned here \u2014 choose which Attentive segment your contacts should sync into."
568
+ "You come back here to choose which Attentive segment your contacts should sync into.",
569
+ "The connection shows Pending until you choose a segment, then Active."
565
570
  ]
566
571
  },
567
572
  // A contact destination, like Klaviyo and Mailchimp — a merchant could
@@ -824,6 +829,9 @@ var attentive_default2 = {
824
829
  // merchant would recognise in a builder label.
825
830
  key: "Sync contact to Attentive",
826
831
  queue: "connection",
832
+ // Attentive applies the update in the background and returns no id we
833
+ // could hand on, so there is nothing for a later step to interpolate.
834
+ returns: [],
827
835
  // Nothing for a merchant to configure on the step itself — the segment
828
836
  // is chosen once on the connection. Declared empty rather than omitted,
829
837
  // so "this step takes no settings" and "nobody thought about settings"
@@ -2051,13 +2059,18 @@ var drawbridge_default2 = {
2051
2059
  request: request2,
2052
2060
  response: { count, notified: recipients.length },
2053
2061
  writes: recipients.map((member) => {
2054
- var _a2, _b2;
2062
+ var _a2, _b2, _c2;
2055
2063
  return queueNotification({
2056
2064
  audience: "member",
2057
2065
  message: interpolate((_a2 = step.settings) == null ? void 0 : _a2.message, values),
2058
2066
  organization: workflow.organization,
2059
2067
  send: { type: "email", email: member.email },
2060
- title: interpolate((_b2 = step.settings) == null ? void 0 : _b2.subject, values),
2068
+ // The editor's plain rendering, interpolated the same way so
2069
+ // {{count}} resolves in the text part too. Spread rather than
2070
+ // written as null: a step saved before `text` existed has none,
2071
+ // and the send path strips the markup itself for those.
2072
+ ...((_b2 = step.settings) == null ? void 0 : _b2.text) && { text: interpolate(step.settings.text, values) },
2073
+ title: interpolate((_c2 = step.settings) == null ? void 0 : _c2.subject, values),
2061
2074
  workflow: workflow.id
2062
2075
  });
2063
2076
  })
@@ -2126,7 +2139,7 @@ var drawbridge_default2 = {
2126
2139
  // unsubscribe token and the CAN-SPAM footer. The step's job is to say who
2127
2140
  // and what, correctly, and to refuse early when it must not send at all.
2128
2141
  send: async ({ context, step, workflow }, { canSend, read } = {}) => {
2129
- var _a, _b;
2142
+ var _a, _b, _c;
2130
2143
  const to = context == null ? void 0 : context.email;
2131
2144
  if (!to) throw new Error("No email address on context (context.email is required)");
2132
2145
  const request2 = { to };
@@ -2163,7 +2176,11 @@ var drawbridge_default2 = {
2163
2176
  message: interpolate((_a = step.settings) == null ? void 0 : _a.message, context),
2164
2177
  organization: workflow.organization,
2165
2178
  send: { type: "email", email: to },
2166
- title: interpolate((_b = step.settings) == null ? void 0 : _b.subject, context),
2179
+ // The editor's plain rendering, so the text/plain part of this
2180
+ // email is what the merchant wrote rather than a regex's guess at
2181
+ // it. See the digest above for why it is spread, not nulled.
2182
+ ...((_b = step.settings) == null ? void 0 : _b.text) && { text: interpolate(step.settings.text, context) },
2183
+ title: interpolate((_c = step.settings) == null ? void 0 : _c.subject, context),
2167
2184
  workflow: workflow.id
2168
2185
  })
2169
2186
  ]
@@ -2583,6 +2600,11 @@ var drawbridge_default2 = {
2583
2600
  hook: "email.digest",
2584
2601
  key: "Email \u2014 Digest",
2585
2602
  queue: "notification",
2603
+ // Nothing for a later step to interpolate. Declared empty rather than
2604
+ // omitted, so "this step leaves nothing behind" and "nobody thought
2605
+ // about it" stay different statements — the second is what emptied the
2606
+ // builder's Variables menu for every step in the product.
2607
+ returns: [],
2586
2608
  settings: {
2587
2609
  // The organization OWNER is always a recipient, resolved by the
2588
2610
  // hook, so this is additional recipients rather than the list. It
@@ -2591,7 +2613,22 @@ var drawbridge_default2 = {
2591
2613
  // pick and could never save the step.
2592
2614
  members: { of: "string", type: "array" },
2593
2615
  message: { max: 2e3, required: true, type: "string" },
2594
- subject: { max: 150, required: true, type: "string" }
2616
+ // THE LAST SPAM CHECK, stored beside the copy it measured. Shape is
2617
+ // pinned no further than the pair that proves the match, which is the
2618
+ // only part the api reads; the rest is SpamAssassin's own output and
2619
+ // is stored as it arrives. Same declaration the campaign's draw
2620
+ // notification validates against.
2621
+ //
2622
+ // Undeclared, this was silently dropped at submit — the settings
2623
+ // validator is noUnknown().strict(), so the dashboard's step form
2624
+ // mapped the key away rather than 400 — and every open of a workflow
2625
+ // step re-ran a check against a free service to be told what the last
2626
+ // open had already been told.
2627
+ score: { shape: { message: { type: "string" }, subject: { type: "string" } }, type: "object" },
2628
+ subject: { max: 150, required: true, type: "string" },
2629
+ // The editor's plain rendering of `message`, written beside it rather
2630
+ // than reconstructed by running a regex over the markup at send time.
2631
+ text: { max: 2e3, type: "string" }
2595
2632
  },
2596
2633
  triggers: ["schedule.day", "schedule.week", "schedule.month"],
2597
2634
  usage: { actions: 0 }
@@ -2612,6 +2649,7 @@ var drawbridge_default2 = {
2612
2649
  hook: "email.notify",
2613
2650
  key: "Email \u2014 Notification",
2614
2651
  queue: "notification",
2652
+ returns: [],
2615
2653
  settings: {
2616
2654
  members: { of: "string", type: "array" },
2617
2655
  message: { max: 2e3, type: "string" },
@@ -2628,9 +2666,14 @@ var drawbridge_default2 = {
2628
2666
  hook: "email.send",
2629
2667
  key: "Email \u2014 Send email",
2630
2668
  queue: "notification",
2669
+ returns: [],
2631
2670
  settings: {
2632
2671
  message: { max: 2e3, required: true, type: "string" },
2633
- subject: { max: 150, required: true, type: "string" }
2672
+ // See email.digest above the last spam check, and the editor's
2673
+ // plain rendering of the message.
2674
+ score: { shape: { message: { type: "string" }, subject: { type: "string" } }, type: "object" },
2675
+ subject: { max: 150, required: true, type: "string" },
2676
+ text: { max: 2e3, type: "string" }
2634
2677
  },
2635
2678
  triggers: ["lead.insert"],
2636
2679
  // ONE SOURCE FOR THE PRICE. lib/pricing.js is the index of every
@@ -3229,6 +3272,13 @@ var klaviyo_default2 = {
3229
3272
  // than a label that could be any of their Klaviyo accounts.
3230
3273
  key: "Sync contact to " + (((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.account) || "Klaviyo"),
3231
3274
  queue: "connection",
3275
+ // The key the hook puts in `context`, which the runner merges into the
3276
+ // run so a later step can interpolate {{klaviyoProfileId}}. Declared
3277
+ // beside the hook that writes it — see the note on Shopify's
3278
+ // commerce.code, which is where this came back from.
3279
+ returns: [
3280
+ { key: "klaviyoProfileId", label: "Klaviyo Profile ID" }
3281
+ ],
3232
3282
  // Nothing for a merchant to configure on the step itself — the list
3233
3283
  // is chosen once on the connection. Declared empty rather than
3234
3284
  // omitted, so "this step takes no settings" and "nobody thought about
@@ -3344,10 +3394,15 @@ var mailchimp_default2 = {
3344
3394
  "Someone who unsubscribed inside Mailchimp keeps that choice: a resync only sets the status of a subscriber Mailchimp has never seen before."
3345
3395
  ],
3346
3396
  excerpt: "Sync your Drawbridge contacts into a Mailchimp audience.",
3397
+ // NAMES THE AUDIENCE STEP, because `status` below gates on it: a grant with
3398
+ // no audience chosen sits at Pending, and a guide that stops at the consent
3399
+ // leaves a merchant looking at a connection they think is broken.
3347
3400
  guide: [
3348
3401
  "Press Connect. Drawbridge sends you to Mailchimp to approve access.",
3349
3402
  "Sign in to Mailchimp if you are not already, and choose the account to connect.",
3350
- "You come back here to pick the audience your contacts should sync into."
3403
+ "You come back here to pick the audience your contacts should sync into.",
3404
+ "The connection shows Pending until you pick an audience, then Active.",
3405
+ "You can remove Drawbridge at any time from the Authorized Apps page in your Mailchimp account."
3351
3406
  ]
3352
3407
  },
3353
3408
  // Mailchimp and SendGrid shared a group while they were SENDERS, where an org
@@ -3589,6 +3644,11 @@ var mailchimp_default2 = {
3589
3644
  // than showing a string like a1b2c3d4e5.
3590
3645
  key: "Sync contact to Mailchimp",
3591
3646
  queue: "connection",
3647
+ // The key the hook puts in `context`, for a later step to interpolate
3648
+ // as {{mailchimpMemberId}}. Same reasoning as Klaviyo's.
3649
+ returns: [
3650
+ { key: "mailchimpMemberId", label: "Mailchimp Member ID" }
3651
+ ],
3592
3652
  // Nothing for a merchant to configure on the step itself — the audience
3593
3653
  // is chosen once on the connection. Declared empty rather than omitted,
3594
3654
  // so "this step takes no settings" and "nobody thought about settings"
@@ -4850,6 +4910,24 @@ var shopify_default2 = {
4850
4910
  hook: "commerce.code",
4851
4911
  key: "Issue a discount code",
4852
4912
  queue: "connection",
4913
+ // WHAT THIS STEP LEAVES BEHIND FOR THE ONES AFTER IT, and the only
4914
+ // reason the builder can offer a Variables menu.
4915
+ //
4916
+ // These are the exact keys the hook puts in `context` — the runner
4917
+ // merges that into the run's context, and a later step interpolates
4918
+ // {{shopifyDiscountCode}} out of it. Declared beside the hook that
4919
+ // writes them so the two cannot drift; there is nowhere else that
4920
+ // knows both.
4921
+ //
4922
+ // They existed as a hand-written list in the api's workflow catalog
4923
+ // until it was derived from these manifests, and the derivation
4924
+ // hardcoded `returns : []` for every step. Nothing has offered a
4925
+ // variable since — an email step after a discount step had no way to
4926
+ // name the code it was supposed to send.
4927
+ returns: [
4928
+ { key: "shopifyDiscountCode", label: "Shopify Discount Code" },
4929
+ { key: "shopifyDiscountId", label: "Shopify Discount ID" }
4930
+ ],
4853
4931
  settings: {
4854
4932
  discount: {
4855
4933
  required: true,
@@ -4868,6 +4946,12 @@ var shopify_default2 = {
4868
4946
  hook: "commerce.customer",
4869
4947
  key: "Create customer",
4870
4948
  queue: "connection",
4949
+ // Written on every path the hook can take — created, reused from an
4950
+ // earlier step, or reused from the lead — so a later step can rely on
4951
+ // it being there whenever this one succeeded.
4952
+ returns: [
4953
+ { key: "shopifyCustomerId", label: "Shopify Customer ID" }
4954
+ ],
4871
4955
  settings: {},
4872
4956
  triggers: ["lead.insert"],
4873
4957
  usage: { actions: 1 }
@@ -5265,6 +5349,13 @@ var webhook_default = {
5265
5349
  hook: "webhook.send",
5266
5350
  key: "Send webhook",
5267
5351
  queue: "webhook",
5352
+ // The receiver's response is buffered, not merged into the run — a
5353
+ // receiver owes us a status, not a document — so there is nothing here
5354
+ // for a later step to interpolate. Declared empty rather than omitted:
5355
+ // "leaves nothing behind" and "nobody thought about it" are different
5356
+ // statements, and the second one is what emptied the builder's
5357
+ // Variables menu for every step in the product.
5358
+ returns: [],
5268
5359
  settings: {
5269
5360
  url: { format: "url", required: true, type: "string" }
5270
5361
  },
@@ -5641,6 +5732,12 @@ var publicConnectionKeys = Object.freeze([
5641
5732
  // excerpt, guide, and any vendor redirect copy.
5642
5733
  "content",
5643
5734
  "createdAt",
5735
+ // THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
5736
+ // paused — kept, configured, and not run — as distinct from a vendor the
5737
+ // platform switched off (which the api overlays as an error). Absent means
5738
+ // on; the dashboard renders Paused from it and the sync gates refuse a paused
5739
+ // connection the way they refuse a missing one.
5740
+ "enabled",
5644
5741
  // The connection DOCUMENT's own errors array — scope-drift entries written by
5645
5742
  // drawbridge-sync. NOT the manifest's error copy, which is content.errors:
5646
5743
  // the document is spread OVER the resolved manifest downstream, so the two
package/dist/html.cjs CHANGED
@@ -19,11 +19,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // lib/html.js
20
20
  var html_exports = {};
21
21
  __export(html_exports, {
22
- escapeHtml: () => escapeHtml
22
+ escapeHtml: () => escapeHtml,
23
+ plainText: () => plainText
23
24
  });
24
25
  module.exports = __toCommonJS(html_exports);
25
26
  var escapeHtml = (value) => String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
27
+ var plainText = (value) => String(value ?? "").replace(/<[^>]+>/g, "").trim();
26
28
  // Annotate the CommonJS export names for ESM import in node:
27
29
  0 && (module.exports = {
28
- escapeHtml
30
+ escapeHtml,
31
+ plainText
29
32
  });
package/dist/html.d.cts CHANGED
@@ -29,4 +29,23 @@ const escapeHtml = ( value ) => String( value ?? '' )
29
29
  .replace( /"/g, '&quot;' )
30
30
  .replace( /'/g, '&#39;' );
31
31
 
32
- export { escapeHtml };
32
+ // THE OTHER DIRECTION: markup out, words kept. For the places that need a plain
33
+ // rendering of editor output and were not handed one — the spam check scoring a
34
+ // message as text/plain, and the send paths' fallback for notifications written
35
+ // before `text` was stored beside `message`.
36
+ //
37
+ // A REGEX, AND KNOWINGLY A GUESS. It drops tags and nothing else: entities stay
38
+ // as typed and block boundaries collapse to nothing, so "<p>Hi</p><p>there</p>"
39
+ // reads "Hithere". Tiptap's getText() is the answer where an editor is in hand,
40
+ // which is why the dashboard stores what the editor says rather than calling
41
+ // this at send time. This is for the copies that exist without it.
42
+ //
43
+ // Same three-copy story as escapeHtml above: scripts.js, the draw route and the
44
+ // spam check route each carried this line inline. The spam check's was missing
45
+ // entirely — it scored the raw markup as text/plain, so a merchant's <p> tags
46
+ // were measured as words while the migration scored the same copy stripped.
47
+ const plainText = ( value ) => String( value ?? '' )
48
+ .replace( /<[^>]+>/g, '' )
49
+ .trim();
50
+
51
+ export { escapeHtml, plainText };
package/dist/html.d.ts CHANGED
@@ -29,4 +29,23 @@ const escapeHtml = ( value ) => String( value ?? '' )
29
29
  .replace( /"/g, '&quot;' )
30
30
  .replace( /'/g, '&#39;' );
31
31
 
32
- export { escapeHtml };
32
+ // THE OTHER DIRECTION: markup out, words kept. For the places that need a plain
33
+ // rendering of editor output and were not handed one — the spam check scoring a
34
+ // message as text/plain, and the send paths' fallback for notifications written
35
+ // before `text` was stored beside `message`.
36
+ //
37
+ // A REGEX, AND KNOWINGLY A GUESS. It drops tags and nothing else: entities stay
38
+ // as typed and block boundaries collapse to nothing, so "<p>Hi</p><p>there</p>"
39
+ // reads "Hithere". Tiptap's getText() is the answer where an editor is in hand,
40
+ // which is why the dashboard stores what the editor says rather than calling
41
+ // this at send time. This is for the copies that exist without it.
42
+ //
43
+ // Same three-copy story as escapeHtml above: scripts.js, the draw route and the
44
+ // spam check route each carried this line inline. The spam check's was missing
45
+ // entirely — it scored the raw markup as text/plain, so a merchant's <p> tags
46
+ // were measured as words while the migration scored the same copy stripped.
47
+ const plainText = ( value ) => String( value ?? '' )
48
+ .replace( /<[^>]+>/g, '' )
49
+ .trim();
50
+
51
+ export { escapeHtml, plainText };
package/dist/html.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // lib/html.js
2
2
  var escapeHtml = (value) => String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3
+ var plainText = (value) => String(value ?? "").replace(/<[^>]+>/g, "").trim();
3
4
  export {
4
- escapeHtml
5
+ escapeHtml,
6
+ plainText
5
7
  };
package/dist/index.cjs CHANGED
@@ -160,7 +160,40 @@ var constants_default = {
160
160
  notifications: {
161
161
  draw: {
162
162
  subject: "Your prize is ready to claim",
163
- message: "You were selected in our giveaway. Follow the link to confirm your details and claim your prize."
163
+ message: "You were selected in our giveaway. Follow the link to confirm your details and claim your prize.",
164
+ // THE DEFAULT COPY'S SPAM SCORE, SHIPPED WITH THE COPY.
165
+ //
166
+ // A campaign is seeded from this object, so a score written here is a
167
+ // score every new campaign has from the moment it exists — which is
168
+ // what lets the dashboard's card open with an answer and ask the
169
+ // checker only when a merchant changes the words. Without it the copy
170
+ // arrived unmeasured and the panel had to go and find out, spending a
171
+ // request on a free service to be told what is measured right here.
172
+ //
173
+ // Verbatim what the checker returned for these two strings on
174
+ // 2026-09-10, so a campaign created today and one moved by
175
+ // backfillDrawNotifications carry the same object rather than two
176
+ // shapes of the same fact.
177
+ //
178
+ // The 0.5 is OURS, not the copy's: SpamAssassin charges the sending
179
+ // domain for its missing vowels on every message it will ever score.
180
+ // That is why `subtotal` — the merchant's own share — is what the
181
+ // dashboard reads and what it scores out of five.
182
+ score: {
183
+ rules: [
184
+ { description: "From: domain has series of non-vowel letters", score: 0.5 },
185
+ { description: "Informational: message was not relayed via SMTP", score: 0 },
186
+ { description: "Informational: message has no Received headers", score: 0 }
187
+ ],
188
+ subtotal: 0,
189
+ threshold: 5,
190
+ total: 0.5
191
+ },
192
+ // The plain rendering that rides alongside `message`. Identical to it
193
+ // here because the default carries no markup, and stored rather than
194
+ // derived because the draw notification leaves as email OR as SMS and
195
+ // twilio.sms drops its body in verbatim.
196
+ text: "You were selected in our giveaway. Follow the link to confirm your details and claim your prize."
164
197
  }
165
198
  },
166
199
  settings: {
package/dist/index.d.cts CHANGED
@@ -85,7 +85,40 @@ var constantsData = {
85
85
  notifications : {
86
86
  draw : {
87
87
  subject : 'Your prize is ready to claim',
88
- message : 'You were selected in our giveaway. Follow the link to confirm your details and claim your prize.'
88
+ message : 'You were selected in our giveaway. Follow the link to confirm your details and claim your prize.',
89
+ // THE DEFAULT COPY'S SPAM SCORE, SHIPPED WITH THE COPY.
90
+ //
91
+ // A campaign is seeded from this object, so a score written here is a
92
+ // score every new campaign has from the moment it exists — which is
93
+ // what lets the dashboard's card open with an answer and ask the
94
+ // checker only when a merchant changes the words. Without it the copy
95
+ // arrived unmeasured and the panel had to go and find out, spending a
96
+ // request on a free service to be told what is measured right here.
97
+ //
98
+ // Verbatim what the checker returned for these two strings on
99
+ // 2026-09-10, so a campaign created today and one moved by
100
+ // backfillDrawNotifications carry the same object rather than two
101
+ // shapes of the same fact.
102
+ //
103
+ // The 0.5 is OURS, not the copy's: SpamAssassin charges the sending
104
+ // domain for its missing vowels on every message it will ever score.
105
+ // That is why `subtotal` — the merchant's own share — is what the
106
+ // dashboard reads and what it scores out of five.
107
+ score : {
108
+ rules : [
109
+ { description : 'From: domain has series of non-vowel letters', score : 0.5 },
110
+ { description : 'Informational: message was not relayed via SMTP', score : 0 },
111
+ { description : 'Informational: message has no Received headers', score : 0 }
112
+ ],
113
+ subtotal : 0,
114
+ threshold : 5,
115
+ total : 0.5
116
+ },
117
+ // The plain rendering that rides alongside `message`. Identical to it
118
+ // here because the default carries no markup, and stored rather than
119
+ // derived because the draw notification leaves as email OR as SMS and
120
+ // twilio.sms drops its body in verbatim.
121
+ text : 'You were selected in our giveaway. Follow the link to confirm your details and claim your prize.'
89
122
  }
90
123
  },
91
124
  settings : {