@drawbridge/drawbridge-utils 0.0.163 → 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.
@@ -5808,6 +5808,12 @@ var publicConnectionKeys = Object.freeze([
5808
5808
  // excerpt, guide, and any vendor redirect copy.
5809
5809
  "content",
5810
5810
  "createdAt",
5811
+ // THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
5812
+ // paused — kept, configured, and not run — as distinct from a vendor the
5813
+ // platform switched off (which the api overlays as an error). Absent means
5814
+ // on; the dashboard renders Paused from it and the sync gates refuse a paused
5815
+ // connection the way they refuse a missing one.
5816
+ "enabled",
5811
5817
  // The connection DOCUMENT's own errors array — scope-drift entries written by
5812
5818
  // drawbridge-sync. NOT the manifest's error copy, which is content.errors:
5813
5819
  // the document is spread OVER the resolved manifest downstream, so the two
@@ -7500,6 +7500,12 @@ const publicConnectionKeys = Object.freeze([
7500
7500
  // excerpt, guide, and any vendor redirect copy.
7501
7501
  'content',
7502
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',
7503
7509
  // The connection DOCUMENT's own errors array — scope-drift entries written by
7504
7510
  // drawbridge-sync. NOT the manifest's error copy, which is content.errors:
7505
7511
  // the document is spread OVER the resolved manifest downstream, so the two
@@ -7500,6 +7500,12 @@ const publicConnectionKeys = Object.freeze([
7500
7500
  // excerpt, guide, and any vendor redirect copy.
7501
7501
  'content',
7502
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',
7503
7509
  // The connection DOCUMENT's own errors array — scope-drift entries written by
7504
7510
  // drawbridge-sync. NOT the manifest's error copy, which is content.errors:
7505
7511
  // the document is spread OVER the resolved manifest downstream, so the two
@@ -5732,6 +5732,12 @@ var publicConnectionKeys = Object.freeze([
5732
5732
  // excerpt, guide, and any vendor redirect copy.
5733
5733
  "content",
5734
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",
5735
5741
  // The connection DOCUMENT's own errors array — scope-drift entries written by
5736
5742
  // drawbridge-sync. NOT the manifest's error copy, which is content.errors:
5737
5743
  // the document is spread OVER the resolved manifest downstream, so the two
@@ -19,6 +19,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // lib/notification.js
20
20
  var notification_exports = {};
21
21
  __export(notification_exports, {
22
+ POINTS: () => POINTS,
23
+ notificationRules: () => notificationRules,
22
24
  notificationWarnings: () => notificationWarnings
23
25
  });
24
26
  module.exports = __toCommonJS(notification_exports);
@@ -49,41 +51,103 @@ var phrasesIn = (value) => {
49
51
  return PHRASES.filter((phrase) => lower.includes(phrase));
50
52
  };
51
53
  var list = (values) => values.map((value) => "\u201C" + value + "\u201D").join(", ");
52
- var subjectWarnings = ({ brand, subject }) => {
54
+ var POINTS = {
55
+ brand: 0,
56
+ currency: 1,
57
+ emoji: 0.5,
58
+ length: 0.5,
59
+ phrase: 1,
60
+ punctuation: 1,
61
+ shouting: 1.5
62
+ };
63
+ var subjectRules = ({ brand, subject }) => {
53
64
  const value = String(subject || "").trim();
54
65
  if (!value) return [];
55
66
  const found = phrasesIn(value);
56
67
  const emojis = emojiCount(value);
57
68
  return [
58
- value.length > SUBJECT_DISPLAY_LIMIT && "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
59
- shouting(value) && "Mostly capitals reads as shouting, and filters treat it that way too.",
60
- /[!?]{2,}/.test(value) && "Repeated exclamation or question marks are a common spam signal.",
61
- emojis > 1 && "More than one emoji in a subject is a common spam signal.",
62
- /[$£€]\s?\d/.test(value) && "A currency amount in the subject is a common spam signal.",
63
- found.length > 0 && "Reads like a scam to filters and to people: " + list(found) + ".",
64
- brand && !value.toLowerCase().includes(String(brand).toLowerCase()) && "Nothing here says the mail is from " + brand + " \u2014 subjects that name the sender get opened more."
65
- ].filter(Boolean);
69
+ value.length > SUBJECT_DISPLAY_LIMIT && {
70
+ message: "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
71
+ points: POINTS.length,
72
+ rule: "length"
73
+ },
74
+ shouting(value) && {
75
+ message: "Mostly capitals reads as shouting, and filters treat it that way too.",
76
+ points: POINTS.shouting,
77
+ rule: "shouting"
78
+ },
79
+ /[!?]{2,}/.test(value) && {
80
+ message: "Repeated exclamation or question marks are a common spam signal.",
81
+ points: POINTS.punctuation,
82
+ rule: "punctuation"
83
+ },
84
+ emojis > 1 && {
85
+ message: "More than one emoji in a subject is a common spam signal.",
86
+ points: POINTS.emoji,
87
+ rule: "emoji"
88
+ },
89
+ /[$£€]\s?\d/.test(value) && {
90
+ message: "A currency amount in the subject is a common spam signal.",
91
+ points: POINTS.currency,
92
+ rule: "currency"
93
+ },
94
+ found.length > 0 && {
95
+ message: "Reads like a scam to filters and to people: " + list(found) + ".",
96
+ points: POINTS.phrase * found.length,
97
+ rule: "phrase"
98
+ },
99
+ brand && !value.toLowerCase().includes(String(brand).toLowerCase()) && {
100
+ message: "Nothing here says the mail is from " + brand + " \u2014 subjects that name the sender get opened more.",
101
+ points: POINTS.brand,
102
+ rule: "brand"
103
+ }
104
+ ].filter(Boolean).map((rule) => ({ ...rule, field: "subject" }));
66
105
  };
67
- var messageWarnings = ({ message }) => {
106
+ var messageRules = ({ message }) => {
68
107
  const value = String(message || "").trim();
69
108
  if (!value) return [];
70
109
  const found = phrasesIn(value);
71
110
  return [
72
- value.length < MESSAGE_SHORT_LIMIT && "Very short messages read as a fragment, and bulk senders send exactly this shape.",
73
- shouting(value) && "Mostly capitals reads as shouting, and filters treat it that way too.",
74
- /[!?]{2,}/.test(value) && "Repeated exclamation or question marks are a common spam signal.",
75
- found.length > 0 && "Reads like a scam to filters and to people: " + list(found) + "."
76
- ].filter(Boolean);
111
+ value.length < MESSAGE_SHORT_LIMIT && {
112
+ message: "Very short messages read as a fragment, and bulk senders send exactly this shape.",
113
+ points: POINTS.length,
114
+ rule: "length"
115
+ },
116
+ shouting(value) && {
117
+ message: "Mostly capitals reads as shouting, and filters treat it that way too.",
118
+ points: POINTS.shouting,
119
+ rule: "shouting"
120
+ },
121
+ /[!?]{2,}/.test(value) && {
122
+ message: "Repeated exclamation or question marks are a common spam signal.",
123
+ points: POINTS.punctuation,
124
+ rule: "punctuation"
125
+ },
126
+ found.length > 0 && {
127
+ message: "Reads like a scam to filters and to people: " + list(found) + ".",
128
+ points: POINTS.phrase * found.length,
129
+ rule: "phrase"
130
+ }
131
+ ].filter(Boolean).map((rule) => ({ ...rule, field: "message" }));
77
132
  };
78
- var notificationWarnings = ({
133
+ var notificationRules = ({
79
134
  brand,
80
135
  message,
81
136
  subject
82
- } = {}) => ({
83
- message: messageWarnings({ message }),
84
- subject: subjectWarnings({ brand, subject })
85
- });
137
+ } = {}) => [
138
+ ...subjectRules({ brand, subject }),
139
+ ...messageRules({ message })
140
+ ];
141
+ var notificationWarnings = (input) => {
142
+ const rules = notificationRules(input);
143
+ return {
144
+ message: rules.filter((rule) => rule.field === "message").map((rule) => rule.message),
145
+ subject: rules.filter((rule) => rule.field === "subject").map((rule) => rule.message)
146
+ };
147
+ };
86
148
  // Annotate the CommonJS export names for ESM import in node:
87
149
  0 && (module.exports = {
150
+ POINTS,
151
+ notificationRules,
88
152
  notificationWarnings
89
153
  });
@@ -75,11 +75,42 @@ const phrasesIn = ( value ) => {
75
75
 
76
76
  const list = ( values ) => values.map( ( value ) => '“' + value + '”' ).join( ', ' );
77
77
 
78
+ // WHAT EACH RULE IS WORTH, on SpamAssassin's scale — its own rules run 0.1 to
79
+ // about 2.0 and five is the line most installs call spam at.
80
+ //
81
+ // THESE CARRY THE NUMBER. Measured 2026-09-10 through Postmark's SpamCheck:
82
+ // stock SpamAssassin scored "Congratulations!!!" at ZERO — its rules are tuned
83
+ // for 2005-era pharma and lottery mail and header forgery, and a two-line
84
+ // giveaway email trips none of them. So for the copy this product sends, the
85
+ // filter's opinion is close to noise and these rules are the actual signal.
86
+ // A flat half-point each (the first version) left "Congratulations!!!" reading
87
+ // as fine, which is the opposite of what a merchant needs to hear.
88
+ //
89
+ // Weighted by how reliably each one gets mail binned, and by how likely it is
90
+ // to be an accident. Shouting and scam phrasing are never accidents. Repeated
91
+ // punctuation and a currency amount are strong. Emoji and length are habits
92
+ // worth a nudge. The brand line is advice about context rather than the
93
+ // words — and the default copy every campaign ships with cannot name a brand
94
+ // it does not know — so it is listed at zero and never marks a default down.
95
+ //
96
+ // PHRASES COUNT PER MATCH. "Congratulations, click here, limited time" is
97
+ // three of them and reads like three of them; one sentence lists them all,
98
+ // the points say how many. A product table, and Darren's to move.
99
+ const POINTS = {
100
+ brand : 0,
101
+ currency : 1,
102
+ emoji : 0.5,
103
+ length : 0.5,
104
+ phrase : 1,
105
+ punctuation : 1,
106
+ shouting : 1.5
107
+ };
108
+
78
109
  // `brand` is the organization's own name. A subject that never mentions who is
79
110
  // writing is the single most recognisable trait of the mail people delete
80
111
  // unread — and the one a merchant can fix in five seconds once it is pointed
81
112
  // out. Skipped entirely when no brand is supplied rather than guessed at.
82
- const subjectWarnings = ({ brand, subject }) => {
113
+ const subjectRules = ({ brand, subject }) => {
83
114
 
84
115
  const value = String( subject || '' ).trim();
85
116
 
@@ -89,25 +120,46 @@ const subjectWarnings = ({ brand, subject }) => {
89
120
  const emojis = emojiCount( value );
90
121
 
91
122
  return [
92
- value.length > SUBJECT_DISPLAY_LIMIT
93
- && 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
94
- shouting( value )
95
- && 'Mostly capitals reads as shouting, and filters treat it that way too.',
96
- /[!?]{2,}/.test( value )
97
- && 'Repeated exclamation or question marks are a common spam signal.',
98
- emojis > 1
99
- && 'More than one emoji in a subject is a common spam signal.',
100
- /[$£€]\s?\d/.test( value )
101
- && 'A currency amount in the subject is a common spam signal.',
102
- found.length > 0
103
- && 'Reads like a scam to filters and to people: ' + list( found ) + '.',
104
- brand && ! value.toLowerCase().includes( String( brand ).toLowerCase() )
105
- && 'Nothing here says the mail is from ' + brand + ' — subjects that name the sender get opened more.'
106
- ].filter( Boolean );
123
+ value.length > SUBJECT_DISPLAY_LIMIT && {
124
+ message : 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
125
+ points : POINTS.length,
126
+ rule : 'length'
127
+ },
128
+ shouting( value ) && {
129
+ message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
130
+ points : POINTS.shouting,
131
+ rule : 'shouting'
132
+ },
133
+ /[!?]{2,}/.test( value ) && {
134
+ message : 'Repeated exclamation or question marks are a common spam signal.',
135
+ points : POINTS.punctuation,
136
+ rule : 'punctuation'
137
+ },
138
+ emojis > 1 && {
139
+ message : 'More than one emoji in a subject is a common spam signal.',
140
+ points : POINTS.emoji,
141
+ rule : 'emoji'
142
+ },
143
+ /[$£€]\s?\d/.test( value ) && {
144
+ message : 'A currency amount in the subject is a common spam signal.',
145
+ points : POINTS.currency,
146
+ rule : 'currency'
147
+ },
148
+ found.length > 0 && {
149
+ message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
150
+ points : POINTS.phrase * found.length,
151
+ rule : 'phrase'
152
+ },
153
+ brand && ! value.toLowerCase().includes( String( brand ).toLowerCase() ) && {
154
+ message : 'Nothing here says the mail is from ' + brand + ' — subjects that name the sender get opened more.',
155
+ points : POINTS.brand,
156
+ rule : 'brand'
157
+ }
158
+ ].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'subject' }) );
107
159
 
108
160
  };
109
161
 
110
- const messageWarnings = ({ message }) => {
162
+ const messageRules = ({ message }) => {
111
163
 
112
164
  const value = String( message || '' ).trim();
113
165
 
@@ -116,20 +168,38 @@ const messageWarnings = ({ message }) => {
116
168
  const found = phrasesIn( value );
117
169
 
118
170
  return [
119
- value.length < MESSAGE_SHORT_LIMIT
120
- && 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
121
- shouting( value )
122
- && 'Mostly capitals reads as shouting, and filters treat it that way too.',
123
- /[!?]{2,}/.test( value )
124
- && 'Repeated exclamation or question marks are a common spam signal.',
125
- found.length > 0
126
- && 'Reads like a scam to filters and to people: ' + list( found ) + '.'
127
- ].filter( Boolean );
171
+ value.length < MESSAGE_SHORT_LIMIT && {
172
+ message : 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
173
+ points : POINTS.length,
174
+ rule : 'length'
175
+ },
176
+ shouting( value ) && {
177
+ message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
178
+ points : POINTS.shouting,
179
+ rule : 'shouting'
180
+ },
181
+ /[!?]{2,}/.test( value ) && {
182
+ message : 'Repeated exclamation or question marks are a common spam signal.',
183
+ points : POINTS.punctuation,
184
+ rule : 'punctuation'
185
+ },
186
+ found.length > 0 && {
187
+ message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
188
+ points : POINTS.phrase * found.length,
189
+ rule : 'phrase'
190
+ }
191
+ ].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'message' }) );
128
192
 
129
193
  };
130
194
 
131
- // ONE ENTRY POINT, answering per field so each warning renders against the
132
- // input it is about rather than as a pile at the bottom of the form.
195
+ // THE RULES, WITH THEIR WEIGHT: `[ { field, message, points, rule } ]`, subject
196
+ // first. This is what the api's scorer folds into the spam score beside
197
+ // SpamAssassin's own rules, so the sentence a merchant reads and the points
198
+ // it cost are one object.
199
+ //
200
+ // The same rule tripped in both fields comes back twice, once per field, each
201
+ // with its points. Whether that is one habit or two is the scorer's call to
202
+ // make; here the facts are just reported.
133
203
  //
134
204
  // NO CHANNEL ARGUMENT, deliberately, and it is worth saying why since one was
135
205
  // tried twice.
@@ -150,13 +220,26 @@ const messageWarnings = ({ message }) => {
150
220
  // wording, and the segment mechanics where one emoji drops the limit from 160
151
221
  // characters to 70. Those belong with the SMS connection work rather than here,
152
222
  // and their absence is a reason to write them, not to stay quiet meanwhile.
153
- const notificationWarnings = ({
223
+ const notificationRules = ({
154
224
  brand,
155
225
  message,
156
226
  subject
157
- } = {}) => ({
158
- message : messageWarnings({ message }),
159
- subject : subjectWarnings({ brand, subject })
160
- });
227
+ } = {}) => [
228
+ ...subjectRules({ brand, subject }),
229
+ ...messageRules({ message })
230
+ ];
231
+
232
+ // The sentences alone, per field — what the rules looked like before they had
233
+ // points. Kept for anything that only wants the words.
234
+ const notificationWarnings = ( input ) => {
235
+
236
+ const rules = notificationRules( input );
237
+
238
+ return {
239
+ message : rules.filter( ( rule ) => rule.field === 'message' ).map( ( rule ) => rule.message ),
240
+ subject : rules.filter( ( rule ) => rule.field === 'subject' ).map( ( rule ) => rule.message )
241
+ };
242
+
243
+ };
161
244
 
162
- export { notificationWarnings };
245
+ export { POINTS, notificationRules, notificationWarnings };
@@ -75,11 +75,42 @@ const phrasesIn = ( value ) => {
75
75
 
76
76
  const list = ( values ) => values.map( ( value ) => '“' + value + '”' ).join( ', ' );
77
77
 
78
+ // WHAT EACH RULE IS WORTH, on SpamAssassin's scale — its own rules run 0.1 to
79
+ // about 2.0 and five is the line most installs call spam at.
80
+ //
81
+ // THESE CARRY THE NUMBER. Measured 2026-09-10 through Postmark's SpamCheck:
82
+ // stock SpamAssassin scored "Congratulations!!!" at ZERO — its rules are tuned
83
+ // for 2005-era pharma and lottery mail and header forgery, and a two-line
84
+ // giveaway email trips none of them. So for the copy this product sends, the
85
+ // filter's opinion is close to noise and these rules are the actual signal.
86
+ // A flat half-point each (the first version) left "Congratulations!!!" reading
87
+ // as fine, which is the opposite of what a merchant needs to hear.
88
+ //
89
+ // Weighted by how reliably each one gets mail binned, and by how likely it is
90
+ // to be an accident. Shouting and scam phrasing are never accidents. Repeated
91
+ // punctuation and a currency amount are strong. Emoji and length are habits
92
+ // worth a nudge. The brand line is advice about context rather than the
93
+ // words — and the default copy every campaign ships with cannot name a brand
94
+ // it does not know — so it is listed at zero and never marks a default down.
95
+ //
96
+ // PHRASES COUNT PER MATCH. "Congratulations, click here, limited time" is
97
+ // three of them and reads like three of them; one sentence lists them all,
98
+ // the points say how many. A product table, and Darren's to move.
99
+ const POINTS = {
100
+ brand : 0,
101
+ currency : 1,
102
+ emoji : 0.5,
103
+ length : 0.5,
104
+ phrase : 1,
105
+ punctuation : 1,
106
+ shouting : 1.5
107
+ };
108
+
78
109
  // `brand` is the organization's own name. A subject that never mentions who is
79
110
  // writing is the single most recognisable trait of the mail people delete
80
111
  // unread — and the one a merchant can fix in five seconds once it is pointed
81
112
  // out. Skipped entirely when no brand is supplied rather than guessed at.
82
- const subjectWarnings = ({ brand, subject }) => {
113
+ const subjectRules = ({ brand, subject }) => {
83
114
 
84
115
  const value = String( subject || '' ).trim();
85
116
 
@@ -89,25 +120,46 @@ const subjectWarnings = ({ brand, subject }) => {
89
120
  const emojis = emojiCount( value );
90
121
 
91
122
  return [
92
- value.length > SUBJECT_DISPLAY_LIMIT
93
- && 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
94
- shouting( value )
95
- && 'Mostly capitals reads as shouting, and filters treat it that way too.',
96
- /[!?]{2,}/.test( value )
97
- && 'Repeated exclamation or question marks are a common spam signal.',
98
- emojis > 1
99
- && 'More than one emoji in a subject is a common spam signal.',
100
- /[$£€]\s?\d/.test( value )
101
- && 'A currency amount in the subject is a common spam signal.',
102
- found.length > 0
103
- && 'Reads like a scam to filters and to people: ' + list( found ) + '.',
104
- brand && ! value.toLowerCase().includes( String( brand ).toLowerCase() )
105
- && 'Nothing here says the mail is from ' + brand + ' — subjects that name the sender get opened more.'
106
- ].filter( Boolean );
123
+ value.length > SUBJECT_DISPLAY_LIMIT && {
124
+ message : 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
125
+ points : POINTS.length,
126
+ rule : 'length'
127
+ },
128
+ shouting( value ) && {
129
+ message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
130
+ points : POINTS.shouting,
131
+ rule : 'shouting'
132
+ },
133
+ /[!?]{2,}/.test( value ) && {
134
+ message : 'Repeated exclamation or question marks are a common spam signal.',
135
+ points : POINTS.punctuation,
136
+ rule : 'punctuation'
137
+ },
138
+ emojis > 1 && {
139
+ message : 'More than one emoji in a subject is a common spam signal.',
140
+ points : POINTS.emoji,
141
+ rule : 'emoji'
142
+ },
143
+ /[$£€]\s?\d/.test( value ) && {
144
+ message : 'A currency amount in the subject is a common spam signal.',
145
+ points : POINTS.currency,
146
+ rule : 'currency'
147
+ },
148
+ found.length > 0 && {
149
+ message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
150
+ points : POINTS.phrase * found.length,
151
+ rule : 'phrase'
152
+ },
153
+ brand && ! value.toLowerCase().includes( String( brand ).toLowerCase() ) && {
154
+ message : 'Nothing here says the mail is from ' + brand + ' — subjects that name the sender get opened more.',
155
+ points : POINTS.brand,
156
+ rule : 'brand'
157
+ }
158
+ ].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'subject' }) );
107
159
 
108
160
  };
109
161
 
110
- const messageWarnings = ({ message }) => {
162
+ const messageRules = ({ message }) => {
111
163
 
112
164
  const value = String( message || '' ).trim();
113
165
 
@@ -116,20 +168,38 @@ const messageWarnings = ({ message }) => {
116
168
  const found = phrasesIn( value );
117
169
 
118
170
  return [
119
- value.length < MESSAGE_SHORT_LIMIT
120
- && 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
121
- shouting( value )
122
- && 'Mostly capitals reads as shouting, and filters treat it that way too.',
123
- /[!?]{2,}/.test( value )
124
- && 'Repeated exclamation or question marks are a common spam signal.',
125
- found.length > 0
126
- && 'Reads like a scam to filters and to people: ' + list( found ) + '.'
127
- ].filter( Boolean );
171
+ value.length < MESSAGE_SHORT_LIMIT && {
172
+ message : 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
173
+ points : POINTS.length,
174
+ rule : 'length'
175
+ },
176
+ shouting( value ) && {
177
+ message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
178
+ points : POINTS.shouting,
179
+ rule : 'shouting'
180
+ },
181
+ /[!?]{2,}/.test( value ) && {
182
+ message : 'Repeated exclamation or question marks are a common spam signal.',
183
+ points : POINTS.punctuation,
184
+ rule : 'punctuation'
185
+ },
186
+ found.length > 0 && {
187
+ message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
188
+ points : POINTS.phrase * found.length,
189
+ rule : 'phrase'
190
+ }
191
+ ].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'message' }) );
128
192
 
129
193
  };
130
194
 
131
- // ONE ENTRY POINT, answering per field so each warning renders against the
132
- // input it is about rather than as a pile at the bottom of the form.
195
+ // THE RULES, WITH THEIR WEIGHT: `[ { field, message, points, rule } ]`, subject
196
+ // first. This is what the api's scorer folds into the spam score beside
197
+ // SpamAssassin's own rules, so the sentence a merchant reads and the points
198
+ // it cost are one object.
199
+ //
200
+ // The same rule tripped in both fields comes back twice, once per field, each
201
+ // with its points. Whether that is one habit or two is the scorer's call to
202
+ // make; here the facts are just reported.
133
203
  //
134
204
  // NO CHANNEL ARGUMENT, deliberately, and it is worth saying why since one was
135
205
  // tried twice.
@@ -150,13 +220,26 @@ const messageWarnings = ({ message }) => {
150
220
  // wording, and the segment mechanics where one emoji drops the limit from 160
151
221
  // characters to 70. Those belong with the SMS connection work rather than here,
152
222
  // and their absence is a reason to write them, not to stay quiet meanwhile.
153
- const notificationWarnings = ({
223
+ const notificationRules = ({
154
224
  brand,
155
225
  message,
156
226
  subject
157
- } = {}) => ({
158
- message : messageWarnings({ message }),
159
- subject : subjectWarnings({ brand, subject })
160
- });
227
+ } = {}) => [
228
+ ...subjectRules({ brand, subject }),
229
+ ...messageRules({ message })
230
+ ];
231
+
232
+ // The sentences alone, per field — what the rules looked like before they had
233
+ // points. Kept for anything that only wants the words.
234
+ const notificationWarnings = ( input ) => {
235
+
236
+ const rules = notificationRules( input );
237
+
238
+ return {
239
+ message : rules.filter( ( rule ) => rule.field === 'message' ).map( ( rule ) => rule.message ),
240
+ subject : rules.filter( ( rule ) => rule.field === 'subject' ).map( ( rule ) => rule.message )
241
+ };
242
+
243
+ };
161
244
 
162
- export { notificationWarnings };
245
+ export { POINTS, notificationRules, notificationWarnings };
@@ -26,40 +26,102 @@ var phrasesIn = (value) => {
26
26
  return PHRASES.filter((phrase) => lower.includes(phrase));
27
27
  };
28
28
  var list = (values) => values.map((value) => "\u201C" + value + "\u201D").join(", ");
29
- var subjectWarnings = ({ brand, subject }) => {
29
+ var POINTS = {
30
+ brand: 0,
31
+ currency: 1,
32
+ emoji: 0.5,
33
+ length: 0.5,
34
+ phrase: 1,
35
+ punctuation: 1,
36
+ shouting: 1.5
37
+ };
38
+ var subjectRules = ({ brand, subject }) => {
30
39
  const value = String(subject || "").trim();
31
40
  if (!value) return [];
32
41
  const found = phrasesIn(value);
33
42
  const emojis = emojiCount(value);
34
43
  return [
35
- value.length > SUBJECT_DISPLAY_LIMIT && "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
36
- shouting(value) && "Mostly capitals reads as shouting, and filters treat it that way too.",
37
- /[!?]{2,}/.test(value) && "Repeated exclamation or question marks are a common spam signal.",
38
- emojis > 1 && "More than one emoji in a subject is a common spam signal.",
39
- /[$£€]\s?\d/.test(value) && "A currency amount in the subject is a common spam signal.",
40
- found.length > 0 && "Reads like a scam to filters and to people: " + list(found) + ".",
41
- brand && !value.toLowerCase().includes(String(brand).toLowerCase()) && "Nothing here says the mail is from " + brand + " \u2014 subjects that name the sender get opened more."
42
- ].filter(Boolean);
44
+ value.length > SUBJECT_DISPLAY_LIMIT && {
45
+ message: "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
46
+ points: POINTS.length,
47
+ rule: "length"
48
+ },
49
+ shouting(value) && {
50
+ message: "Mostly capitals reads as shouting, and filters treat it that way too.",
51
+ points: POINTS.shouting,
52
+ rule: "shouting"
53
+ },
54
+ /[!?]{2,}/.test(value) && {
55
+ message: "Repeated exclamation or question marks are a common spam signal.",
56
+ points: POINTS.punctuation,
57
+ rule: "punctuation"
58
+ },
59
+ emojis > 1 && {
60
+ message: "More than one emoji in a subject is a common spam signal.",
61
+ points: POINTS.emoji,
62
+ rule: "emoji"
63
+ },
64
+ /[$£€]\s?\d/.test(value) && {
65
+ message: "A currency amount in the subject is a common spam signal.",
66
+ points: POINTS.currency,
67
+ rule: "currency"
68
+ },
69
+ found.length > 0 && {
70
+ message: "Reads like a scam to filters and to people: " + list(found) + ".",
71
+ points: POINTS.phrase * found.length,
72
+ rule: "phrase"
73
+ },
74
+ brand && !value.toLowerCase().includes(String(brand).toLowerCase()) && {
75
+ message: "Nothing here says the mail is from " + brand + " \u2014 subjects that name the sender get opened more.",
76
+ points: POINTS.brand,
77
+ rule: "brand"
78
+ }
79
+ ].filter(Boolean).map((rule) => ({ ...rule, field: "subject" }));
43
80
  };
44
- var messageWarnings = ({ message }) => {
81
+ var messageRules = ({ message }) => {
45
82
  const value = String(message || "").trim();
46
83
  if (!value) return [];
47
84
  const found = phrasesIn(value);
48
85
  return [
49
- value.length < MESSAGE_SHORT_LIMIT && "Very short messages read as a fragment, and bulk senders send exactly this shape.",
50
- shouting(value) && "Mostly capitals reads as shouting, and filters treat it that way too.",
51
- /[!?]{2,}/.test(value) && "Repeated exclamation or question marks are a common spam signal.",
52
- found.length > 0 && "Reads like a scam to filters and to people: " + list(found) + "."
53
- ].filter(Boolean);
86
+ value.length < MESSAGE_SHORT_LIMIT && {
87
+ message: "Very short messages read as a fragment, and bulk senders send exactly this shape.",
88
+ points: POINTS.length,
89
+ rule: "length"
90
+ },
91
+ shouting(value) && {
92
+ message: "Mostly capitals reads as shouting, and filters treat it that way too.",
93
+ points: POINTS.shouting,
94
+ rule: "shouting"
95
+ },
96
+ /[!?]{2,}/.test(value) && {
97
+ message: "Repeated exclamation or question marks are a common spam signal.",
98
+ points: POINTS.punctuation,
99
+ rule: "punctuation"
100
+ },
101
+ found.length > 0 && {
102
+ message: "Reads like a scam to filters and to people: " + list(found) + ".",
103
+ points: POINTS.phrase * found.length,
104
+ rule: "phrase"
105
+ }
106
+ ].filter(Boolean).map((rule) => ({ ...rule, field: "message" }));
54
107
  };
55
- var notificationWarnings = ({
108
+ var notificationRules = ({
56
109
  brand,
57
110
  message,
58
111
  subject
59
- } = {}) => ({
60
- message: messageWarnings({ message }),
61
- subject: subjectWarnings({ brand, subject })
62
- });
112
+ } = {}) => [
113
+ ...subjectRules({ brand, subject }),
114
+ ...messageRules({ message })
115
+ ];
116
+ var notificationWarnings = (input) => {
117
+ const rules = notificationRules(input);
118
+ return {
119
+ message: rules.filter((rule) => rule.field === "message").map((rule) => rule.message),
120
+ subject: rules.filter((rule) => rule.field === "subject").map((rule) => rule.message)
121
+ };
122
+ };
63
123
  export {
124
+ POINTS,
125
+ notificationRules,
64
126
  notificationWarnings
65
127
  };
@@ -5653,6 +5653,12 @@ var publicConnectionKeys = Object.freeze([
5653
5653
  // excerpt, guide, and any vendor redirect copy.
5654
5654
  "content",
5655
5655
  "createdAt",
5656
+ // THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
5657
+ // paused — kept, configured, and not run — as distinct from a vendor the
5658
+ // platform switched off (which the api overlays as an error). Absent means
5659
+ // on; the dashboard renders Paused from it and the sync gates refuse a paused
5660
+ // connection the way they refuse a missing one.
5661
+ "enabled",
5656
5662
  // The connection DOCUMENT's own errors array — scope-drift entries written by
5657
5663
  // drawbridge-sync. NOT the manifest's error copy, which is content.errors:
5658
5664
  // the document is spread OVER the resolved manifest downstream, so the two
package/dist/providers.js CHANGED
@@ -5604,6 +5604,12 @@ var publicConnectionKeys = Object.freeze([
5604
5604
  // excerpt, guide, and any vendor redirect copy.
5605
5605
  "content",
5606
5606
  "createdAt",
5607
+ // THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
5608
+ // paused — kept, configured, and not run — as distinct from a vendor the
5609
+ // platform switched off (which the api overlays as an error). Absent means
5610
+ // on; the dashboard renders Paused from it and the sync gates refuse a paused
5611
+ // connection the way they refuse a missing one.
5612
+ "enabled",
5607
5613
  // The connection DOCUMENT's own errors array — scope-drift entries written by
5608
5614
  // drawbridge-sync. NOT the manifest's error copy, which is content.errors:
5609
5615
  // the document is spread OVER the resolved manifest downstream, so the two
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.163"
229
+ "version": "0.0.164"
230
230
  }