@drawbridge/drawbridge-utils 0.0.163 → 0.0.165
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.
- package/dist/connections/index.cjs +6 -0
- package/dist/connections/index.d.cts +6 -0
- package/dist/connections/index.d.ts +6 -0
- package/dist/connections/index.js +6 -0
- package/dist/notification.cjs +78 -21
- package/dist/notification.d.cts +115 -39
- package/dist/notification.d.ts +115 -39
- package/dist/notification.js +76 -21
- package/dist/providers.cjs +6 -0
- package/dist/providers.js +6 -0
- package/dist/sendgrid.cjs +45 -0
- package/dist/sendgrid.d.cts +72 -1
- package/dist/sendgrid.d.ts +72 -1
- package/dist/sendgrid.js +43 -0
- package/dist/twilio.cjs +8 -0
- package/dist/twilio.d.cts +18 -1
- package/dist/twilio.d.ts +18 -1
- package/dist/twilio.js +8 -0
- package/package.json +1 -1
|
@@ -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
|
package/dist/notification.cjs
CHANGED
|
@@ -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,96 @@ 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
|
|
54
|
+
var POINTS = {
|
|
55
|
+
currency: 1,
|
|
56
|
+
emoji: 0.5,
|
|
57
|
+
length: 0.5,
|
|
58
|
+
phrase: 1,
|
|
59
|
+
punctuation: 1,
|
|
60
|
+
shouting: 1.5
|
|
61
|
+
};
|
|
62
|
+
var subjectRules = ({ subject }) => {
|
|
53
63
|
const value = String(subject || "").trim();
|
|
54
64
|
if (!value) return [];
|
|
55
65
|
const found = phrasesIn(value);
|
|
56
66
|
const emojis = emojiCount(value);
|
|
57
67
|
return [
|
|
58
|
-
value.length > SUBJECT_DISPLAY_LIMIT &&
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
value.length > SUBJECT_DISPLAY_LIMIT && {
|
|
69
|
+
message: "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
|
|
70
|
+
points: POINTS.length,
|
|
71
|
+
rule: "length"
|
|
72
|
+
},
|
|
73
|
+
shouting(value) && {
|
|
74
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
75
|
+
points: POINTS.shouting,
|
|
76
|
+
rule: "shouting"
|
|
77
|
+
},
|
|
78
|
+
/[!?]{2,}/.test(value) && {
|
|
79
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
80
|
+
points: POINTS.punctuation,
|
|
81
|
+
rule: "punctuation"
|
|
82
|
+
},
|
|
83
|
+
emojis > 1 && {
|
|
84
|
+
message: "More than one emoji in a subject is a common spam signal.",
|
|
85
|
+
points: POINTS.emoji,
|
|
86
|
+
rule: "emoji"
|
|
87
|
+
},
|
|
88
|
+
/[$£€]\s?\d/.test(value) && {
|
|
89
|
+
message: "A currency amount in the subject is a common spam signal.",
|
|
90
|
+
points: POINTS.currency,
|
|
91
|
+
rule: "currency"
|
|
92
|
+
},
|
|
93
|
+
found.length > 0 && {
|
|
94
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
95
|
+
points: POINTS.phrase * found.length,
|
|
96
|
+
rule: "phrase"
|
|
97
|
+
}
|
|
98
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "subject" }));
|
|
66
99
|
};
|
|
67
|
-
var
|
|
100
|
+
var messageRules = ({ message }) => {
|
|
68
101
|
const value = String(message || "").trim();
|
|
69
102
|
if (!value) return [];
|
|
70
103
|
const found = phrasesIn(value);
|
|
71
104
|
return [
|
|
72
|
-
value.length < MESSAGE_SHORT_LIMIT &&
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
105
|
+
value.length < MESSAGE_SHORT_LIMIT && {
|
|
106
|
+
message: "Very short messages read as a fragment, and bulk senders send exactly this shape.",
|
|
107
|
+
points: POINTS.length,
|
|
108
|
+
rule: "length"
|
|
109
|
+
},
|
|
110
|
+
shouting(value) && {
|
|
111
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
112
|
+
points: POINTS.shouting,
|
|
113
|
+
rule: "shouting"
|
|
114
|
+
},
|
|
115
|
+
/[!?]{2,}/.test(value) && {
|
|
116
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
117
|
+
points: POINTS.punctuation,
|
|
118
|
+
rule: "punctuation"
|
|
119
|
+
},
|
|
120
|
+
found.length > 0 && {
|
|
121
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
122
|
+
points: POINTS.phrase * found.length,
|
|
123
|
+
rule: "phrase"
|
|
124
|
+
}
|
|
125
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "message" }));
|
|
77
126
|
};
|
|
78
|
-
var
|
|
79
|
-
brand,
|
|
127
|
+
var notificationRules = ({
|
|
80
128
|
message,
|
|
81
129
|
subject
|
|
82
|
-
} = {}) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
130
|
+
} = {}) => [
|
|
131
|
+
...subjectRules({ subject }),
|
|
132
|
+
...messageRules({ message })
|
|
133
|
+
];
|
|
134
|
+
var notificationWarnings = (input) => {
|
|
135
|
+
const rules = notificationRules(input);
|
|
136
|
+
return {
|
|
137
|
+
message: rules.filter((rule) => rule.field === "message").map((rule) => rule.message),
|
|
138
|
+
subject: rules.filter((rule) => rule.field === "subject").map((rule) => rule.message)
|
|
139
|
+
};
|
|
140
|
+
};
|
|
86
141
|
// Annotate the CommonJS export names for ESM import in node:
|
|
87
142
|
0 && (module.exports = {
|
|
143
|
+
POINTS,
|
|
144
|
+
notificationRules,
|
|
88
145
|
notificationWarnings
|
|
89
146
|
});
|
package/dist/notification.d.cts
CHANGED
|
@@ -75,11 +75,41 @@ const phrasesIn = ( value ) => {
|
|
|
75
75
|
|
|
76
76
|
const list = ( values ) => values.map( ( value ) => '“' + value + '”' ).join( ', ' );
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
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.
|
|
93
|
+
//
|
|
94
|
+
// NO BRAND RULE. There was one — "nothing here says the mail is from Acme" —
|
|
95
|
+
// and it was asking the merchant to do by hand what the send already does:
|
|
96
|
+
// email goes out with the organization's title as the From name, and SMS puts
|
|
97
|
+
// it on the first line of the body (twilio.sms). Advice to repeat what the
|
|
98
|
+
// envelope carries is noise, and a static default could never satisfy it.
|
|
99
|
+
//
|
|
100
|
+
// PHRASES COUNT PER MATCH. "Congratulations, click here, limited time" is
|
|
101
|
+
// three of them and reads like three of them; one sentence lists them all,
|
|
102
|
+
// the points say how many. A product table, and Darren's to move.
|
|
103
|
+
const POINTS = {
|
|
104
|
+
currency : 1,
|
|
105
|
+
emoji : 0.5,
|
|
106
|
+
length : 0.5,
|
|
107
|
+
phrase : 1,
|
|
108
|
+
punctuation : 1,
|
|
109
|
+
shouting : 1.5
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const subjectRules = ({ subject }) => {
|
|
83
113
|
|
|
84
114
|
const value = String( subject || '' ).trim();
|
|
85
115
|
|
|
@@ -89,25 +119,41 @@ const subjectWarnings = ({ brand, subject }) => {
|
|
|
89
119
|
const emojis = emojiCount( value );
|
|
90
120
|
|
|
91
121
|
return [
|
|
92
|
-
value.length > SUBJECT_DISPLAY_LIMIT
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
value.length > SUBJECT_DISPLAY_LIMIT && {
|
|
123
|
+
message : 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
|
|
124
|
+
points : POINTS.length,
|
|
125
|
+
rule : 'length'
|
|
126
|
+
},
|
|
127
|
+
shouting( value ) && {
|
|
128
|
+
message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
129
|
+
points : POINTS.shouting,
|
|
130
|
+
rule : 'shouting'
|
|
131
|
+
},
|
|
132
|
+
/[!?]{2,}/.test( value ) && {
|
|
133
|
+
message : 'Repeated exclamation or question marks are a common spam signal.',
|
|
134
|
+
points : POINTS.punctuation,
|
|
135
|
+
rule : 'punctuation'
|
|
136
|
+
},
|
|
137
|
+
emojis > 1 && {
|
|
138
|
+
message : 'More than one emoji in a subject is a common spam signal.',
|
|
139
|
+
points : POINTS.emoji,
|
|
140
|
+
rule : 'emoji'
|
|
141
|
+
},
|
|
142
|
+
/[$£€]\s?\d/.test( value ) && {
|
|
143
|
+
message : 'A currency amount in the subject is a common spam signal.',
|
|
144
|
+
points : POINTS.currency,
|
|
145
|
+
rule : 'currency'
|
|
146
|
+
},
|
|
147
|
+
found.length > 0 && {
|
|
148
|
+
message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
149
|
+
points : POINTS.phrase * found.length,
|
|
150
|
+
rule : 'phrase'
|
|
151
|
+
}
|
|
152
|
+
].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'subject' }) );
|
|
107
153
|
|
|
108
154
|
};
|
|
109
155
|
|
|
110
|
-
const
|
|
156
|
+
const messageRules = ({ message }) => {
|
|
111
157
|
|
|
112
158
|
const value = String( message || '' ).trim();
|
|
113
159
|
|
|
@@ -116,20 +162,38 @@ const messageWarnings = ({ message }) => {
|
|
|
116
162
|
const found = phrasesIn( value );
|
|
117
163
|
|
|
118
164
|
return [
|
|
119
|
-
value.length < MESSAGE_SHORT_LIMIT
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
165
|
+
value.length < MESSAGE_SHORT_LIMIT && {
|
|
166
|
+
message : 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
|
|
167
|
+
points : POINTS.length,
|
|
168
|
+
rule : 'length'
|
|
169
|
+
},
|
|
170
|
+
shouting( value ) && {
|
|
171
|
+
message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
172
|
+
points : POINTS.shouting,
|
|
173
|
+
rule : 'shouting'
|
|
174
|
+
},
|
|
175
|
+
/[!?]{2,}/.test( value ) && {
|
|
176
|
+
message : 'Repeated exclamation or question marks are a common spam signal.',
|
|
177
|
+
points : POINTS.punctuation,
|
|
178
|
+
rule : 'punctuation'
|
|
179
|
+
},
|
|
180
|
+
found.length > 0 && {
|
|
181
|
+
message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
182
|
+
points : POINTS.phrase * found.length,
|
|
183
|
+
rule : 'phrase'
|
|
184
|
+
}
|
|
185
|
+
].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'message' }) );
|
|
128
186
|
|
|
129
187
|
};
|
|
130
188
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
189
|
+
// THE RULES, WITH THEIR WEIGHT: `[ { field, message, points, rule } ]`, subject
|
|
190
|
+
// first. This is what the api's scorer folds into the spam score beside
|
|
191
|
+
// SpamAssassin's own rules, so the sentence a merchant reads and the points
|
|
192
|
+
// it cost are one object.
|
|
193
|
+
//
|
|
194
|
+
// The same rule tripped in both fields comes back twice, once per field, each
|
|
195
|
+
// with its points. Whether that is one habit or two is the scorer's call to
|
|
196
|
+
// make; here the facts are just reported.
|
|
133
197
|
//
|
|
134
198
|
// NO CHANNEL ARGUMENT, deliberately, and it is worth saying why since one was
|
|
135
199
|
// tried twice.
|
|
@@ -150,13 +214,25 @@ const messageWarnings = ({ message }) => {
|
|
|
150
214
|
// wording, and the segment mechanics where one emoji drops the limit from 160
|
|
151
215
|
// characters to 70. Those belong with the SMS connection work rather than here,
|
|
152
216
|
// and their absence is a reason to write them, not to stay quiet meanwhile.
|
|
153
|
-
const
|
|
154
|
-
brand,
|
|
217
|
+
const notificationRules = ({
|
|
155
218
|
message,
|
|
156
219
|
subject
|
|
157
|
-
} = {}) =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
220
|
+
} = {}) => [
|
|
221
|
+
...subjectRules({ subject }),
|
|
222
|
+
...messageRules({ message })
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
// The sentences alone, per field — what the rules looked like before they had
|
|
226
|
+
// points. Kept for anything that only wants the words.
|
|
227
|
+
const notificationWarnings = ( input ) => {
|
|
228
|
+
|
|
229
|
+
const rules = notificationRules( input );
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
message : rules.filter( ( rule ) => rule.field === 'message' ).map( ( rule ) => rule.message ),
|
|
233
|
+
subject : rules.filter( ( rule ) => rule.field === 'subject' ).map( ( rule ) => rule.message )
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
};
|
|
161
237
|
|
|
162
|
-
export { notificationWarnings };
|
|
238
|
+
export { POINTS, notificationRules, notificationWarnings };
|
package/dist/notification.d.ts
CHANGED
|
@@ -75,11 +75,41 @@ const phrasesIn = ( value ) => {
|
|
|
75
75
|
|
|
76
76
|
const list = ( values ) => values.map( ( value ) => '“' + value + '”' ).join( ', ' );
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
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.
|
|
93
|
+
//
|
|
94
|
+
// NO BRAND RULE. There was one — "nothing here says the mail is from Acme" —
|
|
95
|
+
// and it was asking the merchant to do by hand what the send already does:
|
|
96
|
+
// email goes out with the organization's title as the From name, and SMS puts
|
|
97
|
+
// it on the first line of the body (twilio.sms). Advice to repeat what the
|
|
98
|
+
// envelope carries is noise, and a static default could never satisfy it.
|
|
99
|
+
//
|
|
100
|
+
// PHRASES COUNT PER MATCH. "Congratulations, click here, limited time" is
|
|
101
|
+
// three of them and reads like three of them; one sentence lists them all,
|
|
102
|
+
// the points say how many. A product table, and Darren's to move.
|
|
103
|
+
const POINTS = {
|
|
104
|
+
currency : 1,
|
|
105
|
+
emoji : 0.5,
|
|
106
|
+
length : 0.5,
|
|
107
|
+
phrase : 1,
|
|
108
|
+
punctuation : 1,
|
|
109
|
+
shouting : 1.5
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const subjectRules = ({ subject }) => {
|
|
83
113
|
|
|
84
114
|
const value = String( subject || '' ).trim();
|
|
85
115
|
|
|
@@ -89,25 +119,41 @@ const subjectWarnings = ({ brand, subject }) => {
|
|
|
89
119
|
const emojis = emojiCount( value );
|
|
90
120
|
|
|
91
121
|
return [
|
|
92
|
-
value.length > SUBJECT_DISPLAY_LIMIT
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
value.length > SUBJECT_DISPLAY_LIMIT && {
|
|
123
|
+
message : 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
|
|
124
|
+
points : POINTS.length,
|
|
125
|
+
rule : 'length'
|
|
126
|
+
},
|
|
127
|
+
shouting( value ) && {
|
|
128
|
+
message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
129
|
+
points : POINTS.shouting,
|
|
130
|
+
rule : 'shouting'
|
|
131
|
+
},
|
|
132
|
+
/[!?]{2,}/.test( value ) && {
|
|
133
|
+
message : 'Repeated exclamation or question marks are a common spam signal.',
|
|
134
|
+
points : POINTS.punctuation,
|
|
135
|
+
rule : 'punctuation'
|
|
136
|
+
},
|
|
137
|
+
emojis > 1 && {
|
|
138
|
+
message : 'More than one emoji in a subject is a common spam signal.',
|
|
139
|
+
points : POINTS.emoji,
|
|
140
|
+
rule : 'emoji'
|
|
141
|
+
},
|
|
142
|
+
/[$£€]\s?\d/.test( value ) && {
|
|
143
|
+
message : 'A currency amount in the subject is a common spam signal.',
|
|
144
|
+
points : POINTS.currency,
|
|
145
|
+
rule : 'currency'
|
|
146
|
+
},
|
|
147
|
+
found.length > 0 && {
|
|
148
|
+
message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
149
|
+
points : POINTS.phrase * found.length,
|
|
150
|
+
rule : 'phrase'
|
|
151
|
+
}
|
|
152
|
+
].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'subject' }) );
|
|
107
153
|
|
|
108
154
|
};
|
|
109
155
|
|
|
110
|
-
const
|
|
156
|
+
const messageRules = ({ message }) => {
|
|
111
157
|
|
|
112
158
|
const value = String( message || '' ).trim();
|
|
113
159
|
|
|
@@ -116,20 +162,38 @@ const messageWarnings = ({ message }) => {
|
|
|
116
162
|
const found = phrasesIn( value );
|
|
117
163
|
|
|
118
164
|
return [
|
|
119
|
-
value.length < MESSAGE_SHORT_LIMIT
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
165
|
+
value.length < MESSAGE_SHORT_LIMIT && {
|
|
166
|
+
message : 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
|
|
167
|
+
points : POINTS.length,
|
|
168
|
+
rule : 'length'
|
|
169
|
+
},
|
|
170
|
+
shouting( value ) && {
|
|
171
|
+
message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
172
|
+
points : POINTS.shouting,
|
|
173
|
+
rule : 'shouting'
|
|
174
|
+
},
|
|
175
|
+
/[!?]{2,}/.test( value ) && {
|
|
176
|
+
message : 'Repeated exclamation or question marks are a common spam signal.',
|
|
177
|
+
points : POINTS.punctuation,
|
|
178
|
+
rule : 'punctuation'
|
|
179
|
+
},
|
|
180
|
+
found.length > 0 && {
|
|
181
|
+
message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
182
|
+
points : POINTS.phrase * found.length,
|
|
183
|
+
rule : 'phrase'
|
|
184
|
+
}
|
|
185
|
+
].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'message' }) );
|
|
128
186
|
|
|
129
187
|
};
|
|
130
188
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
189
|
+
// THE RULES, WITH THEIR WEIGHT: `[ { field, message, points, rule } ]`, subject
|
|
190
|
+
// first. This is what the api's scorer folds into the spam score beside
|
|
191
|
+
// SpamAssassin's own rules, so the sentence a merchant reads and the points
|
|
192
|
+
// it cost are one object.
|
|
193
|
+
//
|
|
194
|
+
// The same rule tripped in both fields comes back twice, once per field, each
|
|
195
|
+
// with its points. Whether that is one habit or two is the scorer's call to
|
|
196
|
+
// make; here the facts are just reported.
|
|
133
197
|
//
|
|
134
198
|
// NO CHANNEL ARGUMENT, deliberately, and it is worth saying why since one was
|
|
135
199
|
// tried twice.
|
|
@@ -150,13 +214,25 @@ const messageWarnings = ({ message }) => {
|
|
|
150
214
|
// wording, and the segment mechanics where one emoji drops the limit from 160
|
|
151
215
|
// characters to 70. Those belong with the SMS connection work rather than here,
|
|
152
216
|
// and their absence is a reason to write them, not to stay quiet meanwhile.
|
|
153
|
-
const
|
|
154
|
-
brand,
|
|
217
|
+
const notificationRules = ({
|
|
155
218
|
message,
|
|
156
219
|
subject
|
|
157
|
-
} = {}) =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
220
|
+
} = {}) => [
|
|
221
|
+
...subjectRules({ subject }),
|
|
222
|
+
...messageRules({ message })
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
// The sentences alone, per field — what the rules looked like before they had
|
|
226
|
+
// points. Kept for anything that only wants the words.
|
|
227
|
+
const notificationWarnings = ( input ) => {
|
|
228
|
+
|
|
229
|
+
const rules = notificationRules( input );
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
message : rules.filter( ( rule ) => rule.field === 'message' ).map( ( rule ) => rule.message ),
|
|
233
|
+
subject : rules.filter( ( rule ) => rule.field === 'subject' ).map( ( rule ) => rule.message )
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
};
|
|
161
237
|
|
|
162
|
-
export { notificationWarnings };
|
|
238
|
+
export { POINTS, notificationRules, notificationWarnings };
|
package/dist/notification.js
CHANGED
|
@@ -26,40 +26,95 @@ 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
|
|
29
|
+
var POINTS = {
|
|
30
|
+
currency: 1,
|
|
31
|
+
emoji: 0.5,
|
|
32
|
+
length: 0.5,
|
|
33
|
+
phrase: 1,
|
|
34
|
+
punctuation: 1,
|
|
35
|
+
shouting: 1.5
|
|
36
|
+
};
|
|
37
|
+
var subjectRules = ({ subject }) => {
|
|
30
38
|
const value = String(subject || "").trim();
|
|
31
39
|
if (!value) return [];
|
|
32
40
|
const found = phrasesIn(value);
|
|
33
41
|
const emojis = emojiCount(value);
|
|
34
42
|
return [
|
|
35
|
-
value.length > SUBJECT_DISPLAY_LIMIT &&
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
value.length > SUBJECT_DISPLAY_LIMIT && {
|
|
44
|
+
message: "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
|
|
45
|
+
points: POINTS.length,
|
|
46
|
+
rule: "length"
|
|
47
|
+
},
|
|
48
|
+
shouting(value) && {
|
|
49
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
50
|
+
points: POINTS.shouting,
|
|
51
|
+
rule: "shouting"
|
|
52
|
+
},
|
|
53
|
+
/[!?]{2,}/.test(value) && {
|
|
54
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
55
|
+
points: POINTS.punctuation,
|
|
56
|
+
rule: "punctuation"
|
|
57
|
+
},
|
|
58
|
+
emojis > 1 && {
|
|
59
|
+
message: "More than one emoji in a subject is a common spam signal.",
|
|
60
|
+
points: POINTS.emoji,
|
|
61
|
+
rule: "emoji"
|
|
62
|
+
},
|
|
63
|
+
/[$£€]\s?\d/.test(value) && {
|
|
64
|
+
message: "A currency amount in the subject is a common spam signal.",
|
|
65
|
+
points: POINTS.currency,
|
|
66
|
+
rule: "currency"
|
|
67
|
+
},
|
|
68
|
+
found.length > 0 && {
|
|
69
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
70
|
+
points: POINTS.phrase * found.length,
|
|
71
|
+
rule: "phrase"
|
|
72
|
+
}
|
|
73
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "subject" }));
|
|
43
74
|
};
|
|
44
|
-
var
|
|
75
|
+
var messageRules = ({ message }) => {
|
|
45
76
|
const value = String(message || "").trim();
|
|
46
77
|
if (!value) return [];
|
|
47
78
|
const found = phrasesIn(value);
|
|
48
79
|
return [
|
|
49
|
-
value.length < MESSAGE_SHORT_LIMIT &&
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
value.length < MESSAGE_SHORT_LIMIT && {
|
|
81
|
+
message: "Very short messages read as a fragment, and bulk senders send exactly this shape.",
|
|
82
|
+
points: POINTS.length,
|
|
83
|
+
rule: "length"
|
|
84
|
+
},
|
|
85
|
+
shouting(value) && {
|
|
86
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
87
|
+
points: POINTS.shouting,
|
|
88
|
+
rule: "shouting"
|
|
89
|
+
},
|
|
90
|
+
/[!?]{2,}/.test(value) && {
|
|
91
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
92
|
+
points: POINTS.punctuation,
|
|
93
|
+
rule: "punctuation"
|
|
94
|
+
},
|
|
95
|
+
found.length > 0 && {
|
|
96
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
97
|
+
points: POINTS.phrase * found.length,
|
|
98
|
+
rule: "phrase"
|
|
99
|
+
}
|
|
100
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "message" }));
|
|
54
101
|
};
|
|
55
|
-
var
|
|
56
|
-
brand,
|
|
102
|
+
var notificationRules = ({
|
|
57
103
|
message,
|
|
58
104
|
subject
|
|
59
|
-
} = {}) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
105
|
+
} = {}) => [
|
|
106
|
+
...subjectRules({ subject }),
|
|
107
|
+
...messageRules({ message })
|
|
108
|
+
];
|
|
109
|
+
var notificationWarnings = (input) => {
|
|
110
|
+
const rules = notificationRules(input);
|
|
111
|
+
return {
|
|
112
|
+
message: rules.filter((rule) => rule.field === "message").map((rule) => rule.message),
|
|
113
|
+
subject: rules.filter((rule) => rule.field === "subject").map((rule) => rule.message)
|
|
114
|
+
};
|
|
115
|
+
};
|
|
63
116
|
export {
|
|
117
|
+
POINTS,
|
|
118
|
+
notificationRules,
|
|
64
119
|
notificationWarnings
|
|
65
120
|
};
|
package/dist/providers.cjs
CHANGED
|
@@ -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/dist/sendgrid.cjs
CHANGED
|
@@ -19,6 +19,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// lib/sendgrid.js
|
|
20
20
|
var sendgrid_exports = {};
|
|
21
21
|
__export(sendgrid_exports, {
|
|
22
|
+
dnsRows: () => dnsRows,
|
|
23
|
+
domain: () => domain,
|
|
22
24
|
sendWithRetry: () => sendWithRetry,
|
|
23
25
|
sendgrid: () => sendgrid,
|
|
24
26
|
sendgridRequest: () => sendgridRequest
|
|
@@ -111,7 +113,48 @@ var sendgridRequest = ({ apiKey, body, method, path, query, request: request2 =
|
|
|
111
113
|
url: SENDGRID_BASE + path
|
|
112
114
|
});
|
|
113
115
|
};
|
|
116
|
+
var dnsRows = (dns) => Object.values(dns || {}).filter((row) => (row == null ? void 0 : row.host) && (row == null ? void 0 : row.type)).map((row) => ({
|
|
117
|
+
data: String(row.data ?? ""),
|
|
118
|
+
host: String(row.host),
|
|
119
|
+
type: String(row.type),
|
|
120
|
+
valid: typeof row.valid === "boolean" ? row.valid : null
|
|
121
|
+
}));
|
|
122
|
+
var domain = {
|
|
123
|
+
// automatic_security is what makes this CNAME-based: the provider holds the
|
|
124
|
+
// keys and the merchant publishes three CNAMEs. Without it they paste DKIM
|
|
125
|
+
// keys into TXT records by hand.
|
|
126
|
+
register: async ({ apiKey, domain: name, via = sendgridRequest }) => {
|
|
127
|
+
const created = await via({
|
|
128
|
+
apiKey,
|
|
129
|
+
body: {
|
|
130
|
+
automatic_security: true,
|
|
131
|
+
domain: name
|
|
132
|
+
},
|
|
133
|
+
method: "POST",
|
|
134
|
+
path: "/v3/whitelabel/domains"
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
records: dnsRows(created == null ? void 0 : created.dns),
|
|
138
|
+
reference: (created == null ? void 0 : created.id) == null ? null : String(created.id)
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
// Asks SendGrid to look the records up now. `records` carries per-record
|
|
142
|
+
// validity so a UI can say WHICH record has not landed rather than only that
|
|
143
|
+
// something has not; `valid` is the whole domain's verdict.
|
|
144
|
+
validate: async ({ apiKey, reference, via = sendgridRequest }) => {
|
|
145
|
+
const result = await via({
|
|
146
|
+
apiKey,
|
|
147
|
+
method: "POST",
|
|
148
|
+
path: "/v3/whitelabel/domains/" + encodeURIComponent(reference) + "/validate"
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
records: dnsRows(result == null ? void 0 : result.validation_results),
|
|
152
|
+
valid: Boolean(result == null ? void 0 : result.valid)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
};
|
|
114
156
|
var sendgrid = {
|
|
157
|
+
domain,
|
|
115
158
|
// `headers` (optional) carries the List-Unsubscribe pair on lead-facing
|
|
116
159
|
// commercial sends; omitted, the request body is byte-identical to the
|
|
117
160
|
// pre-opt-out-floor shape so system mail is untouched.
|
|
@@ -177,6 +220,8 @@ var sendgrid = {
|
|
|
177
220
|
};
|
|
178
221
|
// Annotate the CommonJS export names for ESM import in node:
|
|
179
222
|
0 && (module.exports = {
|
|
223
|
+
dnsRows,
|
|
224
|
+
domain,
|
|
180
225
|
sendWithRetry,
|
|
181
226
|
sendgrid,
|
|
182
227
|
sendgridRequest
|
package/dist/sendgrid.d.cts
CHANGED
|
@@ -109,8 +109,79 @@ const sendgridRequest = ({ apiKey, body, method, path, query, request: request$1
|
|
|
109
109
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
// SendGrid returns the records to publish as an OBJECT keyed by their role
|
|
113
|
+
// (mail_cname, dkim1, dkim2), not a list. The keys are provider vocabulary the
|
|
114
|
+
// merchant never sees and the set differs by security mode, so they are
|
|
115
|
+
// flattened to the rows the identity schema stores — the UI then renders
|
|
116
|
+
// whatever came back instead of naming roles it would have to keep in step
|
|
117
|
+
// with.
|
|
118
|
+
const dnsRows = ( dns ) => Object.values( dns || {} )
|
|
119
|
+
.filter( ( row ) => row?.host && row?.type )
|
|
120
|
+
.map( ( row ) => ({
|
|
121
|
+
data : String( row.data ?? '' ),
|
|
122
|
+
host : String( row.host ),
|
|
123
|
+
type : String( row.type ),
|
|
124
|
+
valid : typeof row.valid === 'boolean' ? row.valid : null
|
|
125
|
+
}) );
|
|
126
|
+
|
|
127
|
+
// DOMAIN AUTHENTICATION, the two calls behind a custom sending domain. They
|
|
128
|
+
// lived inline in sync's identity worker; the api needed the second one too, so
|
|
129
|
+
// a merchant pressing Check now gets an answer in the request rather than a
|
|
130
|
+
// timestamp that a change stream turns into a job that answers a socket later.
|
|
131
|
+
// One copy, both callers, same shape back.
|
|
132
|
+
//
|
|
133
|
+
// `via` is the SendGrid-level call, injectable: sync's identity worker stubs
|
|
134
|
+
// exactly that boundary (its tests assert on the `path`), and tsup's exports
|
|
135
|
+
// are read-only so a test cannot reassign one.
|
|
136
|
+
const domain = {
|
|
137
|
+
|
|
138
|
+
// automatic_security is what makes this CNAME-based: the provider holds the
|
|
139
|
+
// keys and the merchant publishes three CNAMEs. Without it they paste DKIM
|
|
140
|
+
// keys into TXT records by hand.
|
|
141
|
+
register : async ({ apiKey, domain : name, via = sendgridRequest }) => {
|
|
142
|
+
|
|
143
|
+
const created = await via({
|
|
144
|
+
apiKey,
|
|
145
|
+
body : {
|
|
146
|
+
automatic_security : true,
|
|
147
|
+
domain : name
|
|
148
|
+
},
|
|
149
|
+
method : 'POST',
|
|
150
|
+
path : '/v3/whitelabel/domains'
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
records : dnsRows( created?.dns ),
|
|
155
|
+
reference : created?.id == null ? null : String( created.id )
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// Asks SendGrid to look the records up now. `records` carries per-record
|
|
161
|
+
// validity so a UI can say WHICH record has not landed rather than only that
|
|
162
|
+
// something has not; `valid` is the whole domain's verdict.
|
|
163
|
+
validate : async ({ apiKey, reference, via = sendgridRequest }) => {
|
|
164
|
+
|
|
165
|
+
const result = await via({
|
|
166
|
+
apiKey,
|
|
167
|
+
method : 'POST',
|
|
168
|
+
path : '/v3/whitelabel/domains/' + encodeURIComponent( reference ) + '/validate'
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
records : dnsRows( result?.validation_results ),
|
|
173
|
+
valid : Boolean( result?.valid )
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
};
|
|
179
|
+
|
|
112
180
|
const sendgrid = {
|
|
113
181
|
|
|
182
|
+
domain,
|
|
183
|
+
|
|
184
|
+
|
|
114
185
|
// `headers` (optional) carries the List-Unsubscribe pair on lead-facing
|
|
115
186
|
// commercial sends; omitted, the request body is byte-identical to the
|
|
116
187
|
// pre-opt-out-floor shape so system mail is untouched.
|
|
@@ -210,4 +281,4 @@ const sendgrid = {
|
|
|
210
281
|
// domain-authentication endpoints (/v3/whitelabel/domains) through the same
|
|
211
282
|
// authenticated client the mail send uses, rather than minting a second one.
|
|
212
283
|
|
|
213
|
-
export { sendWithRetry, sendgrid, sendgridRequest };
|
|
284
|
+
export { dnsRows, domain, sendWithRetry, sendgrid, sendgridRequest };
|
package/dist/sendgrid.d.ts
CHANGED
|
@@ -109,8 +109,79 @@ const sendgridRequest = ({ apiKey, body, method, path, query, request: request$1
|
|
|
109
109
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
// SendGrid returns the records to publish as an OBJECT keyed by their role
|
|
113
|
+
// (mail_cname, dkim1, dkim2), not a list. The keys are provider vocabulary the
|
|
114
|
+
// merchant never sees and the set differs by security mode, so they are
|
|
115
|
+
// flattened to the rows the identity schema stores — the UI then renders
|
|
116
|
+
// whatever came back instead of naming roles it would have to keep in step
|
|
117
|
+
// with.
|
|
118
|
+
const dnsRows = ( dns ) => Object.values( dns || {} )
|
|
119
|
+
.filter( ( row ) => row?.host && row?.type )
|
|
120
|
+
.map( ( row ) => ({
|
|
121
|
+
data : String( row.data ?? '' ),
|
|
122
|
+
host : String( row.host ),
|
|
123
|
+
type : String( row.type ),
|
|
124
|
+
valid : typeof row.valid === 'boolean' ? row.valid : null
|
|
125
|
+
}) );
|
|
126
|
+
|
|
127
|
+
// DOMAIN AUTHENTICATION, the two calls behind a custom sending domain. They
|
|
128
|
+
// lived inline in sync's identity worker; the api needed the second one too, so
|
|
129
|
+
// a merchant pressing Check now gets an answer in the request rather than a
|
|
130
|
+
// timestamp that a change stream turns into a job that answers a socket later.
|
|
131
|
+
// One copy, both callers, same shape back.
|
|
132
|
+
//
|
|
133
|
+
// `via` is the SendGrid-level call, injectable: sync's identity worker stubs
|
|
134
|
+
// exactly that boundary (its tests assert on the `path`), and tsup's exports
|
|
135
|
+
// are read-only so a test cannot reassign one.
|
|
136
|
+
const domain = {
|
|
137
|
+
|
|
138
|
+
// automatic_security is what makes this CNAME-based: the provider holds the
|
|
139
|
+
// keys and the merchant publishes three CNAMEs. Without it they paste DKIM
|
|
140
|
+
// keys into TXT records by hand.
|
|
141
|
+
register : async ({ apiKey, domain : name, via = sendgridRequest }) => {
|
|
142
|
+
|
|
143
|
+
const created = await via({
|
|
144
|
+
apiKey,
|
|
145
|
+
body : {
|
|
146
|
+
automatic_security : true,
|
|
147
|
+
domain : name
|
|
148
|
+
},
|
|
149
|
+
method : 'POST',
|
|
150
|
+
path : '/v3/whitelabel/domains'
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
records : dnsRows( created?.dns ),
|
|
155
|
+
reference : created?.id == null ? null : String( created.id )
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// Asks SendGrid to look the records up now. `records` carries per-record
|
|
161
|
+
// validity so a UI can say WHICH record has not landed rather than only that
|
|
162
|
+
// something has not; `valid` is the whole domain's verdict.
|
|
163
|
+
validate : async ({ apiKey, reference, via = sendgridRequest }) => {
|
|
164
|
+
|
|
165
|
+
const result = await via({
|
|
166
|
+
apiKey,
|
|
167
|
+
method : 'POST',
|
|
168
|
+
path : '/v3/whitelabel/domains/' + encodeURIComponent( reference ) + '/validate'
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
records : dnsRows( result?.validation_results ),
|
|
173
|
+
valid : Boolean( result?.valid )
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
};
|
|
179
|
+
|
|
112
180
|
const sendgrid = {
|
|
113
181
|
|
|
182
|
+
domain,
|
|
183
|
+
|
|
184
|
+
|
|
114
185
|
// `headers` (optional) carries the List-Unsubscribe pair on lead-facing
|
|
115
186
|
// commercial sends; omitted, the request body is byte-identical to the
|
|
116
187
|
// pre-opt-out-floor shape so system mail is untouched.
|
|
@@ -210,4 +281,4 @@ const sendgrid = {
|
|
|
210
281
|
// domain-authentication endpoints (/v3/whitelabel/domains) through the same
|
|
211
282
|
// authenticated client the mail send uses, rather than minting a second one.
|
|
212
283
|
|
|
213
|
-
export { sendWithRetry, sendgrid, sendgridRequest };
|
|
284
|
+
export { dnsRows, domain, sendWithRetry, sendgrid, sendgridRequest };
|
package/dist/sendgrid.js
CHANGED
|
@@ -84,7 +84,48 @@ var sendgridRequest = ({ apiKey, body, method, path, query, request: request2 =
|
|
|
84
84
|
url: SENDGRID_BASE + path
|
|
85
85
|
});
|
|
86
86
|
};
|
|
87
|
+
var dnsRows = (dns) => Object.values(dns || {}).filter((row) => (row == null ? void 0 : row.host) && (row == null ? void 0 : row.type)).map((row) => ({
|
|
88
|
+
data: String(row.data ?? ""),
|
|
89
|
+
host: String(row.host),
|
|
90
|
+
type: String(row.type),
|
|
91
|
+
valid: typeof row.valid === "boolean" ? row.valid : null
|
|
92
|
+
}));
|
|
93
|
+
var domain = {
|
|
94
|
+
// automatic_security is what makes this CNAME-based: the provider holds the
|
|
95
|
+
// keys and the merchant publishes three CNAMEs. Without it they paste DKIM
|
|
96
|
+
// keys into TXT records by hand.
|
|
97
|
+
register: async ({ apiKey, domain: name, via = sendgridRequest }) => {
|
|
98
|
+
const created = await via({
|
|
99
|
+
apiKey,
|
|
100
|
+
body: {
|
|
101
|
+
automatic_security: true,
|
|
102
|
+
domain: name
|
|
103
|
+
},
|
|
104
|
+
method: "POST",
|
|
105
|
+
path: "/v3/whitelabel/domains"
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
records: dnsRows(created == null ? void 0 : created.dns),
|
|
109
|
+
reference: (created == null ? void 0 : created.id) == null ? null : String(created.id)
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
// Asks SendGrid to look the records up now. `records` carries per-record
|
|
113
|
+
// validity so a UI can say WHICH record has not landed rather than only that
|
|
114
|
+
// something has not; `valid` is the whole domain's verdict.
|
|
115
|
+
validate: async ({ apiKey, reference, via = sendgridRequest }) => {
|
|
116
|
+
const result = await via({
|
|
117
|
+
apiKey,
|
|
118
|
+
method: "POST",
|
|
119
|
+
path: "/v3/whitelabel/domains/" + encodeURIComponent(reference) + "/validate"
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
records: dnsRows(result == null ? void 0 : result.validation_results),
|
|
123
|
+
valid: Boolean(result == null ? void 0 : result.valid)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
};
|
|
87
127
|
var sendgrid = {
|
|
128
|
+
domain,
|
|
88
129
|
// `headers` (optional) carries the List-Unsubscribe pair on lead-facing
|
|
89
130
|
// commercial sends; omitted, the request body is byte-identical to the
|
|
90
131
|
// pre-opt-out-floor shape so system mail is untouched.
|
|
@@ -149,6 +190,8 @@ var sendgrid = {
|
|
|
149
190
|
}
|
|
150
191
|
};
|
|
151
192
|
export {
|
|
193
|
+
dnsRows,
|
|
194
|
+
domain,
|
|
152
195
|
sendWithRetry,
|
|
153
196
|
sendgrid,
|
|
154
197
|
sendgridRequest
|
package/dist/twilio.cjs
CHANGED
|
@@ -71,6 +71,12 @@ var twilio = {
|
|
|
71
71
|
sms: async ({
|
|
72
72
|
accountSid,
|
|
73
73
|
authToken,
|
|
74
|
+
// WHO IS TEXTING. A platform number carries no sender name, so the body is
|
|
75
|
+
// the only place a recipient learns who this is from — and a text from a
|
|
76
|
+
// bare number with no name is the exact shape of smishing, which carriers
|
|
77
|
+
// filter on and people delete. Lead-facing sends pass the organization's
|
|
78
|
+
// title; OTC/auth sends pass nothing and keep their exact copy.
|
|
79
|
+
brand,
|
|
74
80
|
from,
|
|
75
81
|
link,
|
|
76
82
|
message,
|
|
@@ -83,7 +89,9 @@ var twilio = {
|
|
|
83
89
|
}) => {
|
|
84
90
|
const missing = Object.entries({ accountSid, authToken, from }).filter(([, value]) => !value).map(([name]) => name);
|
|
85
91
|
if (missing.length) throw new Error("Twilio credentials missing \u2014 " + missing.join(", ") + " (the drawbridge provider's smsSid, smsToken and smsFrom)");
|
|
92
|
+
const named = brand && String(title || "").trim().toLowerCase().startsWith(String(brand).trim().toLowerCase());
|
|
86
93
|
const body = [
|
|
94
|
+
...brand && !named ? [brand] : [],
|
|
87
95
|
title,
|
|
88
96
|
message,
|
|
89
97
|
...prize ? [prize.title + " (" + prize.value + ")"] : [],
|
package/dist/twilio.d.cts
CHANGED
|
@@ -15,6 +15,12 @@ const twilio = {
|
|
|
15
15
|
sms : async ({
|
|
16
16
|
accountSid,
|
|
17
17
|
authToken,
|
|
18
|
+
// WHO IS TEXTING. A platform number carries no sender name, so the body is
|
|
19
|
+
// the only place a recipient learns who this is from — and a text from a
|
|
20
|
+
// bare number with no name is the exact shape of smishing, which carriers
|
|
21
|
+
// filter on and people delete. Lead-facing sends pass the organization's
|
|
22
|
+
// title; OTC/auth sends pass nothing and keep their exact copy.
|
|
23
|
+
brand,
|
|
18
24
|
from,
|
|
19
25
|
link,
|
|
20
26
|
message,
|
|
@@ -38,12 +44,23 @@ const twilio = {
|
|
|
38
44
|
|
|
39
45
|
if( missing.length ) throw new Error( 'Twilio credentials missing — ' + missing.join( ', ' ) + ' (the drawbridge provider\'s smsSid, smsToken and smsFrom)' );
|
|
40
46
|
|
|
41
|
-
// Newline-separated,
|
|
47
|
+
// Newline-separated, BRAND FIRST, then title, and no "Your prize is" /
|
|
42
48
|
// "Click here to verify your prize" phrasing — textbook carrier-filter
|
|
43
49
|
// bait that got these messages flagged. The opt-out line is appended
|
|
44
50
|
// on lead-facing sends (carrier compliance); OTC/auth sends pass
|
|
45
51
|
// nothing new and keep their exact copy.
|
|
52
|
+
//
|
|
53
|
+
// The brand line is what the toll-free verification sample below
|
|
54
|
+
// promised Twilio we send — "Cedar & Sage / Thanks for entering… / Reply
|
|
55
|
+
// STOP to opt out" — and for a while it was the one line the real sends
|
|
56
|
+
// left out. It was a merchant's job to type it into the subject, and a
|
|
57
|
+
// warning nagged them when they did not; now it is put here, once, and
|
|
58
|
+
// the warning is gone. Skipped when the title already opens with it, so
|
|
59
|
+
// a merchant who wrote "Cedar & Sage: your prize" is not branded twice.
|
|
60
|
+
const named = brand && String( title || '' ).trim().toLowerCase().startsWith( String( brand ).trim().toLowerCase() );
|
|
61
|
+
|
|
46
62
|
const body = [
|
|
63
|
+
...( brand && ! named ? [ brand ] : [] ),
|
|
47
64
|
title,
|
|
48
65
|
message,
|
|
49
66
|
...( prize ? [ prize.title + ' (' + prize.value + ')' ] : [] ),
|
package/dist/twilio.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ const twilio = {
|
|
|
15
15
|
sms : async ({
|
|
16
16
|
accountSid,
|
|
17
17
|
authToken,
|
|
18
|
+
// WHO IS TEXTING. A platform number carries no sender name, so the body is
|
|
19
|
+
// the only place a recipient learns who this is from — and a text from a
|
|
20
|
+
// bare number with no name is the exact shape of smishing, which carriers
|
|
21
|
+
// filter on and people delete. Lead-facing sends pass the organization's
|
|
22
|
+
// title; OTC/auth sends pass nothing and keep their exact copy.
|
|
23
|
+
brand,
|
|
18
24
|
from,
|
|
19
25
|
link,
|
|
20
26
|
message,
|
|
@@ -38,12 +44,23 @@ const twilio = {
|
|
|
38
44
|
|
|
39
45
|
if( missing.length ) throw new Error( 'Twilio credentials missing — ' + missing.join( ', ' ) + ' (the drawbridge provider\'s smsSid, smsToken and smsFrom)' );
|
|
40
46
|
|
|
41
|
-
// Newline-separated,
|
|
47
|
+
// Newline-separated, BRAND FIRST, then title, and no "Your prize is" /
|
|
42
48
|
// "Click here to verify your prize" phrasing — textbook carrier-filter
|
|
43
49
|
// bait that got these messages flagged. The opt-out line is appended
|
|
44
50
|
// on lead-facing sends (carrier compliance); OTC/auth sends pass
|
|
45
51
|
// nothing new and keep their exact copy.
|
|
52
|
+
//
|
|
53
|
+
// The brand line is what the toll-free verification sample below
|
|
54
|
+
// promised Twilio we send — "Cedar & Sage / Thanks for entering… / Reply
|
|
55
|
+
// STOP to opt out" — and for a while it was the one line the real sends
|
|
56
|
+
// left out. It was a merchant's job to type it into the subject, and a
|
|
57
|
+
// warning nagged them when they did not; now it is put here, once, and
|
|
58
|
+
// the warning is gone. Skipped when the title already opens with it, so
|
|
59
|
+
// a merchant who wrote "Cedar & Sage: your prize" is not branded twice.
|
|
60
|
+
const named = brand && String( title || '' ).trim().toLowerCase().startsWith( String( brand ).trim().toLowerCase() );
|
|
61
|
+
|
|
46
62
|
const body = [
|
|
63
|
+
...( brand && ! named ? [ brand ] : [] ),
|
|
47
64
|
title,
|
|
48
65
|
message,
|
|
49
66
|
...( prize ? [ prize.title + ' (' + prize.value + ')' ] : [] ),
|
package/dist/twilio.js
CHANGED
|
@@ -46,6 +46,12 @@ var twilio = {
|
|
|
46
46
|
sms: async ({
|
|
47
47
|
accountSid,
|
|
48
48
|
authToken,
|
|
49
|
+
// WHO IS TEXTING. A platform number carries no sender name, so the body is
|
|
50
|
+
// the only place a recipient learns who this is from — and a text from a
|
|
51
|
+
// bare number with no name is the exact shape of smishing, which carriers
|
|
52
|
+
// filter on and people delete. Lead-facing sends pass the organization's
|
|
53
|
+
// title; OTC/auth sends pass nothing and keep their exact copy.
|
|
54
|
+
brand,
|
|
49
55
|
from,
|
|
50
56
|
link,
|
|
51
57
|
message,
|
|
@@ -58,7 +64,9 @@ var twilio = {
|
|
|
58
64
|
}) => {
|
|
59
65
|
const missing = Object.entries({ accountSid, authToken, from }).filter(([, value]) => !value).map(([name]) => name);
|
|
60
66
|
if (missing.length) throw new Error("Twilio credentials missing \u2014 " + missing.join(", ") + " (the drawbridge provider's smsSid, smsToken and smsFrom)");
|
|
67
|
+
const named = brand && String(title || "").trim().toLowerCase().startsWith(String(brand).trim().toLowerCase());
|
|
61
68
|
const body = [
|
|
69
|
+
...brand && !named ? [brand] : [],
|
|
62
70
|
title,
|
|
63
71
|
message,
|
|
64
72
|
...prize ? [prize.title + " (" + prize.value + ")"] : [],
|
package/package.json
CHANGED