@blamejs/blamejs-shop 0.5.20 → 0.5.21

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +1 -0
  3. package/SECURITY.md +17 -0
  4. package/lib/addresses.js +2 -2
  5. package/lib/admin.js +14 -5
  6. package/lib/affiliates.js +7 -23
  7. package/lib/announcement-bar.js +5 -22
  8. package/lib/api-keys.js +4 -13
  9. package/lib/asset-manifest.json +1 -1
  10. package/lib/banner-ab-tests.js +5 -9
  11. package/lib/blog-articles.js +8 -26
  12. package/lib/captcha-gate.js +7 -17
  13. package/lib/carrier-accounts.js +3 -10
  14. package/lib/catalog-drafts.js +5 -12
  15. package/lib/category-navigation.js +4 -20
  16. package/lib/cms-blocks.js +4 -7
  17. package/lib/compliance-export.js +4 -11
  18. package/lib/consent-ledger.js +2 -4
  19. package/lib/cost-layers.js +56 -21
  20. package/lib/customer-import.js +2 -2
  21. package/lib/customer-notes.js +7 -23
  22. package/lib/customer-roles.js +3 -5
  23. package/lib/customer-segments.js +17 -14
  24. package/lib/customer-surveys.js +7 -14
  25. package/lib/customers.js +2 -4
  26. package/lib/dispute-resolution.js +5 -13
  27. package/lib/dropship-forwarding.js +3 -5
  28. package/lib/dunning.js +2 -7
  29. package/lib/email-ab-tests.js +4 -8
  30. package/lib/email-campaigns.js +25 -14
  31. package/lib/email-warmup.js +2 -7
  32. package/lib/experiments.js +6 -10
  33. package/lib/externaldb-d1.js +47 -23
  34. package/lib/geolocation.js +18 -28
  35. package/lib/gift-options.js +5 -23
  36. package/lib/knowledge-base.js +7 -22
  37. package/lib/line-gift-wrap.js +3 -17
  38. package/lib/live-chat.js +5 -18
  39. package/lib/metered-usage.js +3 -8
  40. package/lib/notifications.js +3 -7
  41. package/lib/operator-accounts.js +3 -5
  42. package/lib/operator-approvals.js +6 -8
  43. package/lib/operator-audit-log.js +2 -7
  44. package/lib/operator-help-center.js +5 -21
  45. package/lib/operator-inbox.js +4 -11
  46. package/lib/operator-roles.js +5 -7
  47. package/lib/order-escalation.js +2 -2
  48. package/lib/order-export.js +24 -20
  49. package/lib/order-notes.js +4 -14
  50. package/lib/order-ratings.js +4 -10
  51. package/lib/payment-methods.js +5 -13
  52. package/lib/payment-retries.js +2 -7
  53. package/lib/pixel-events.js +8 -23
  54. package/lib/plan-changes.js +2 -7
  55. package/lib/product-qa.js +4 -8
  56. package/lib/promo-banners.js +7 -15
  57. package/lib/purchase-orders.js +3 -6
  58. package/lib/push-notifications.js +7 -32
  59. package/lib/pwa-manifest.js +6 -11
  60. package/lib/quotes.js +5 -9
  61. package/lib/reviews.js +4 -10
  62. package/lib/robots-config.js +6 -14
  63. package/lib/sales-tax-filings.js +2 -2
  64. package/lib/search-facets.js +5 -5
  65. package/lib/search-ranking.js +4 -7
  66. package/lib/search-synonyms.js +7 -21
  67. package/lib/security-middleware.js +86 -58
  68. package/lib/seller-signup.js +10 -26
  69. package/lib/sidebar-widgets.js +6 -15
  70. package/lib/site-redirects.js +5 -16
  71. package/lib/sitemap-generator.js +3 -7
  72. package/lib/sms-dispatcher.js +7 -23
  73. package/lib/stock-receipts.js +4 -10
  74. package/lib/storefront-dashboards.js +2 -10
  75. package/lib/storefront-forms.js +9 -22
  76. package/lib/storefront-pages.js +6 -20
  77. package/lib/storefront.js +2 -2
  78. package/lib/subscription-billing.js +3 -8
  79. package/lib/subscription-controls.js +2 -7
  80. package/lib/suggestion-box.js +9 -26
  81. package/lib/support-tickets.js +6 -24
  82. package/lib/tax-remittance.js +2 -2
  83. package/lib/tenants.js +2 -7
  84. package/lib/text-guard.js +137 -35
  85. package/lib/theme-assets.js +4 -16
  86. package/lib/translations.js +3 -6
  87. package/lib/trust-badges.js +4 -7
  88. package/lib/vendor-invoices.js +3 -6
  89. package/lib/vendors.js +5 -19
  90. package/lib/webhook-receiver.js +3 -10
  91. package/lib/webhook-subscriptions.js +3 -13
  92. package/lib/wishlist-sharing.js +5 -10
  93. package/package.json +1 -1
@@ -62,6 +62,7 @@ var DEFAULT_LIST_LIMIT = 25;
62
62
  var EMAIL_NAMESPACE = "support-ticket-email";
63
63
 
64
64
  var b = require("./vendor/blamejs");
65
+ var textGuard = require("./text-guard");
65
66
 
66
67
  var ALLOWED_AUTHORS = ["customer", "operator", "system"];
67
68
  var ALLOWED_CATEGORIES = [
@@ -104,14 +105,9 @@ var LIST_ORDER_KEY = ["opened_at:desc", "id:desc"];
104
105
  // Refuse C0 control bytes + DEL. Newlines (LF/CR) + tab are tolerated
105
106
  // in body so multi-line customer narratives work; subject is single-
106
107
  // line by convention and refuses them.
107
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
108
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
109
108
  // Zero-width + direction-override family (see orderNotes for the
110
109
  // same justification). Spelled via \u escapes so the file stays
111
110
  // ESLint no-irregular-whitespace clean.
112
- var ZERO_WIDTH_RE = new RegExp(
113
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
114
- );
115
111
 
116
112
  // ---- validators ---------------------------------------------------------
117
113
 
@@ -159,12 +155,7 @@ function _subject(s) {
159
155
  if (s.length > MAX_SUBJECT_LEN) {
160
156
  throw new TypeError("supportTickets: subject must be <= " + MAX_SUBJECT_LEN + " characters");
161
157
  }
162
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
163
- throw new TypeError("supportTickets: subject contains control bytes");
164
- }
165
- if (ZERO_WIDTH_RE.test(s)) {
166
- throw new TypeError("supportTickets: subject contains zero-width / direction-override characters");
167
- }
158
+ textGuard.freeText(s, "supportTickets: subject", { singleLine: "reject", zeroWidth: "reject" });
168
159
  return s;
169
160
  }
170
161
 
@@ -179,12 +170,7 @@ function _body(s) {
179
170
  if (s.length > MAX_BODY_LEN) {
180
171
  throw new TypeError("supportTickets: body must be <= " + MAX_BODY_LEN + " characters");
181
172
  }
182
- if (CONTROL_BYTE_RE.test(s)) {
183
- throw new TypeError("supportTickets: body contains control bytes");
184
- }
185
- if (ZERO_WIDTH_RE.test(s)) {
186
- throw new TypeError("supportTickets: body contains zero-width / direction-override characters");
187
- }
173
+ textGuard.freeText(s, "supportTickets: body", { zeroWidth: "reject" });
188
174
  return s;
189
175
  }
190
176
 
@@ -205,7 +191,7 @@ function _tags(input) {
205
191
  if (t.length > MAX_TAG_LEN) {
206
192
  throw new TypeError("supportTickets: tags[" + i + "] must be <= " + MAX_TAG_LEN + " characters");
207
193
  }
208
- if (CONTROL_BYTE_STRICT_RE.test(t) || ZERO_WIDTH_RE.test(t)) {
194
+ if (textGuard.hasCodepointThreat(t, { singleLine: "reject", zeroWidth: "reject" })) {
209
195
  throw new TypeError("supportTickets: tags[" + i + "] contains control / zero-width bytes");
210
196
  }
211
197
  out.push(t);
@@ -220,9 +206,7 @@ function _singleTag(t) {
220
206
  if (t.length > MAX_TAG_LEN) {
221
207
  throw new TypeError("supportTickets: tag must be <= " + MAX_TAG_LEN + " characters");
222
208
  }
223
- if (CONTROL_BYTE_STRICT_RE.test(t) || ZERO_WIDTH_RE.test(t)) {
224
- throw new TypeError("supportTickets: tag contains control / zero-width bytes");
225
- }
209
+ textGuard.freeText(t, "supportTickets: tag", { singleLine: "reject", zeroWidth: "reject" });
226
210
  return t;
227
211
  }
228
212
 
@@ -235,9 +219,7 @@ function _reason(r) {
235
219
  if (r.length > MAX_REASON_LEN) {
236
220
  throw new TypeError("supportTickets: reason must be <= " + MAX_REASON_LEN + " characters");
237
221
  }
238
- if (CONTROL_BYTE_RE.test(r) || ZERO_WIDTH_RE.test(r)) {
239
- throw new TypeError("supportTickets: reason contains control / zero-width bytes");
240
- }
222
+ textGuard.freeText(r, "supportTickets: reason", { zeroWidth: "reject" });
241
223
  return r;
242
224
  }
243
225
 
@@ -86,6 +86,7 @@
86
86
  */
87
87
 
88
88
  var b = require("./vendor/blamejs");
89
+ var textGuard = require("./text-guard");
89
90
 
90
91
  var C = b.constants;
91
92
 
@@ -107,7 +108,6 @@ var DEFAULT_DAYS_LATE_MIN = 1;
107
108
  var MAX_DAYS_LATE_MIN = 3650; // ~10 years
108
109
  var MS_PER_DAY = C.TIME.days(1);
109
110
 
110
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
111
111
 
112
112
  // ---- monotonic clock ----------------------------------------------------
113
113
  //
@@ -182,7 +182,7 @@ function _shortText(s, label, max) {
182
182
  throw new TypeError("taxRemittance: " + label +
183
183
  " must be a non-empty string <= " + max + " chars");
184
184
  }
185
- if (CONTROL_BYTE_RE.test(s)) {
185
+ if (textGuard.hasCodepointThreat(s)) {
186
186
  throw new TypeError("taxRemittance: " + label + " must not contain control bytes");
187
187
  }
188
188
  return s;
package/lib/tenants.js CHANGED
@@ -56,6 +56,7 @@
56
56
  */
57
57
 
58
58
  var b = require("./vendor/blamejs");
59
+ var textGuard = require("./text-guard");
59
60
 
60
61
  var MAX_SLUG_LEN = 64;
61
62
  var MAX_NAME_LEN = 200;
@@ -95,10 +96,6 @@ var THEME_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
95
96
  // renders in operator dashboards; embedded control / direction-
96
97
  // override bytes are a slipping-class for visual-spoofing attacks
97
98
  // downstream.
98
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
99
- var ZERO_WIDTH_RE = new RegExp(
100
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
101
- );
102
99
 
103
100
  var ALLOWED_UPDATE_COLUMNS = Object.freeze([
104
101
  "name", "default_currency", "default_locale", "theme_slug",
@@ -131,9 +128,7 @@ function _name(s) {
131
128
  if (s.length > MAX_NAME_LEN) {
132
129
  throw new TypeError("tenants: name must be <= " + MAX_NAME_LEN + " characters");
133
130
  }
134
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
135
- throw new TypeError("tenants: name contains control / zero-width bytes");
136
- }
131
+ textGuard.freeText(s, "tenants: name", { singleLine: "reject", zeroWidth: "reject" });
137
132
  return s;
138
133
  }
139
134
 
package/lib/text-guard.js CHANGED
@@ -36,7 +36,11 @@
36
36
  * - slugLabel(s, label, opts?) — lowercase URL-slug label
37
37
  * - hostLabel(host, label?) — outbound host SSRF + by-name denylist
38
38
  * - freeText(s, label, policy?) — dangerous-codepoint screen for
39
- * unconstrained UTF-8 text fields
39
+ * unconstrained UTF-8 text fields, throwing on a refusal
40
+ * - hasCodepointThreat(s, policy?) — the same screen for a defensive
41
+ * reader that DROPS a bad value instead of refusing the request
42
+ * - firstCodepointThreat(s, policy?) — the one decision both run, so
43
+ * the throwing and dropping paths cannot disagree
40
44
  *
41
45
  * @primitive textGuard
42
46
  * @related b.money, b.ssrfGuard, b.safeUrl, shop.currencyRounding,
@@ -52,6 +56,41 @@ var b = require("./vendor/blamejs");
52
56
  // second copy of the tables.
53
57
  var codepointClass = b.codepointClass;
54
58
 
59
+ // Two invisible-codepoint groups this shop refuses that the framework catalog
60
+ // does not carry yet. Built with codepointClass.charClass off a range table so
61
+ // the escaping is the catalog's, not a second hand-spelled class:
62
+ //
63
+ // U+007F DELETE — grouped with the C0 controls everywhere it matters, but
64
+ // outside the 0x00-0x1F block the framework's C0_CTRL_RANGES covers.
65
+ // U+2061-2064 FUNCTION APPLICATION / INVISIBLE TIMES / INVISIBLE SEPARATOR /
66
+ // INVISIBLE PLUS — zero-width invisible operators, the same spoofing class
67
+ // as U+2060 WORD JOINER, which the framework's ZERO_WIDTH_RANGES does have.
68
+ //
69
+ // Requested upstream (blamejs#580). When a vendor refresh lands them in the
70
+ // catalog this supplement becomes dead weight and comes out — the drift test
71
+ // in test/layer-1-state/text-guard.test.js fails the moment that happens, so
72
+ // it cannot be silently carried forever.
73
+ //
74
+ // Kept as two classes rather than one so each joins the check whose refusal
75
+ // message is already true of it.
76
+ // allow:dynamic-regex — codepoints from a literal range table
77
+ var DEL_RE = new RegExp("[" + codepointClass.charClass([0x007F]) + "]");
78
+ // allow:dynamic-regex — codepoints from a literal range table
79
+ var INVISIBLE_OP_RE = new RegExp("[" + codepointClass.charClass([[0x2061, 0x2064]]) + "]");
80
+ // Global twin for the strip pass; the unglobbed one above is the presence test.
81
+ // allow:dynamic-regex — codepoints from a literal range table
82
+ var INVISIBLE_OP_RE_G = new RegExp("[" + codepointClass.charClass([[0x2061, 0x2064]]) + "]", "g");
83
+
84
+ // Everything that ends a line or pads it out, for a value that must stay on
85
+ // one. Tab / LF / CR sit outside the framework's C0 table on purpose — a
86
+ // multi-line body needs them — so a single-line field asks for them here.
87
+ // U+2028 / U+2029 are not control characters at all, but they terminate a
88
+ // line for both a renderer and a JS string literal.
89
+ // allow:dynamic-regex — codepoints from a literal range table
90
+ var LINE_BREAK_RE = new RegExp(
91
+ "[" + codepointClass.charClass([0x0009, 0x000A, 0x000D, [0x2028, 0x2029]]) + "]"
92
+ );
93
+
55
94
  // ---- shop validators ----------------------------------------------------
56
95
 
57
96
  // asciiUpperLetters(s, label, n?) — exactly `n` (default 3) ASCII
@@ -157,45 +196,105 @@ function hostLabel(host, label) {
157
196
  return h;
158
197
  }
159
198
 
160
- // freeText(s, label, policy?) — the dangerous-codepoint screen for an
161
- // UNCONSTRAINED UTF-8 text field (customer note, review body, gift
162
- // message, product Q&A). Unlike the ASCII-allowlist validators above,
163
- // these fields legitimately carry arbitrary letters, so the codepoint
164
- // catalog is the right backing: bidi overrides (CVE-2021-42574 Trojan
165
- // Source), null bytes, and C0 control characters are refused by default;
166
- // zero-width / invisible formatting is refused when policy.zeroWidth is
167
- // "reject"; a mixed-script confusable (Cyrillic 'а' inside an otherwise
168
- // Latin label) is refused when policy.mixedScript is "reject". Throws
169
- // TypeError on a refused codepoint. Returns the input on success.
170
- function freeText(s, label, policy) {
171
- var name = label || "value";
172
- if (typeof s !== "string") {
173
- throw new TypeError(name + " must be a string");
174
- }
199
+ // firstCodepointThreat(s, policy?) — the single decision both the throwing
200
+ // screen (freeText) and the drop-silent one (hasCodepointThreat) run, so a
201
+ // field guarded one way and a field guarded the other can never disagree
202
+ // about what "dangerous" means. Returns a short reason string naming the
203
+ // class, or null when the text is clean. The reason is the clause that
204
+ // follows the field name in a refusal ("contains a null byte"), so a caller
205
+ // building a message just joins the two.
206
+ //
207
+ // Policies, each "reject" to enable and any other value to allow:
208
+ // bidi — default "reject". Bidi overrides (CVE-2021-42574).
209
+ // nullByte — default "reject". U+0000.
210
+ // control — default "reject". C0 controls + U+007F DELETE. Tab, LF and
211
+ // CR are ALLOWED here: a multi-line body legitimately has
212
+ // them. Use singleLine for a field that must not.
213
+ // singleLine — off by default. Tab / LF / CR, plus U+2028 LINE SEPARATOR
214
+ // and U+2029 PARAGRAPH SEPARATOR, which break a line without
215
+ // being C0 at all. For a value rendered on one line — a URL,
216
+ // a subject, a title, a header value.
217
+ // zeroWidth — off by default. Zero-width / invisible formatting. Includes
218
+ // U+00AD SOFT HYPHEN, which text pasted out of a hyphenating
219
+ // word processor or PDF carries invisibly.
220
+ // mixedScript — off by default. Confusable / homograph across writing
221
+ // systems, narrowed by policy.allowedScripts.
222
+ function firstCodepointThreat(s, policy) {
175
223
  policy = policy || {};
176
- var bidiP = policy.bidi || "reject";
177
- var nullP = policy.nullByte || "reject";
178
- var controlP = policy.control || "reject";
179
- if (bidiP === "reject" && codepointClass.BIDI_RE.test(s)) {
180
- throw new TypeError(name + " contains a Unicode bidi override (CVE-2021-42574 Trojan Source)");
224
+ if ((policy.bidi || "reject") === "reject" && codepointClass.BIDI_RE.test(s)) {
225
+ return "contains a Unicode bidi override (CVE-2021-42574 Trojan Source)";
181
226
  }
182
- if (nullP === "reject" && s.indexOf(codepointClass.NULL_BYTE) !== -1) {
183
- throw new TypeError(name + " contains a null byte");
227
+ if ((policy.nullByte || "reject") === "reject" &&
228
+ s.indexOf(codepointClass.NULL_BYTE) !== -1) {
229
+ return "contains a null byte";
184
230
  }
185
- if (controlP === "reject" && codepointClass.C0_CTRL_RE.test(s)) {
186
- throw new TypeError(name + " contains a C0 control character");
231
+ if ((policy.control || "reject") === "reject" &&
232
+ (codepointClass.C0_CTRL_RE.test(s) || DEL_RE.test(s))) {
233
+ return "contains a C0 control character";
187
234
  }
188
- if (policy.zeroWidth === "reject" && codepointClass.ZERO_WIDTH_RE.test(s)) {
189
- throw new TypeError(name + " contains a zero-width / invisible formatting character");
235
+ if (policy.singleLine === "reject" && LINE_BREAK_RE.test(s)) {
236
+ return "contains a line break or tab but must be a single line";
237
+ }
238
+ if (policy.zeroWidth === "reject" &&
239
+ (codepointClass.ZERO_WIDTH_RE.test(s) || INVISIBLE_OP_RE.test(s))) {
240
+ return "contains a zero-width / invisible formatting character";
190
241
  }
191
242
  if (policy.mixedScript === "reject") {
192
243
  var scripts = codepointClass.detectMixedScripts(s, policy.allowedScripts);
193
244
  if (scripts) {
194
- throw new TypeError(
195
- name + " mixes writing systems (" + scripts.join(", ") + ") — possible confusable / homograph"
196
- );
245
+ return "mixes writing systems (" + scripts.join(", ") +
246
+ ") — possible confusable / homograph";
197
247
  }
198
248
  }
249
+ return null;
250
+ }
251
+
252
+ // hasCodepointThreat(s, policy?) — the non-throwing sibling of freeText, for
253
+ // the defensive readers that DROP a bad value rather than refusing the
254
+ // request (a link URL inside a rendered body, a header line being assembled).
255
+ // A non-string is dangerous by definition here: the caller is about to render
256
+ // or transmit it. Returns true when the text must not be used.
257
+ function hasCodepointThreat(s, policy) {
258
+ if (typeof s !== "string") return true;
259
+ return firstCodepointThreat(s, policy) !== null;
260
+ }
261
+
262
+ // scrubInvisible(s) — remove every bidi override and invisible-formatting
263
+ // codepoint and return the cleaned string. The third disposition, alongside
264
+ // throwing and dropping: for a value that must be ACCEPTED whatever it
265
+ // contains, because refusing it would hand an attacker a denial-of-service
266
+ // against a legitimate user (a search query is the standing example — a
267
+ // pasted BOM must not cost the shopper their search).
268
+ //
269
+ // Composes the framework's strip pass and then removes the codepoints the
270
+ // catalog does not carry yet, so what this deletes is exactly what
271
+ // hasCodepointThreat would have objected to under zeroWidth: "reject".
272
+ // Control characters are deliberately NOT touched: a caller stripping those
273
+ // usually wants to substitute a space to avoid welding two words together,
274
+ // which is its own decision.
275
+ function scrubInvisible(s) {
276
+ if (typeof s !== "string") return s;
277
+ var out = codepointClass.applyCharStripPolicies(s, {
278
+ bidiPolicy: "strip",
279
+ zeroWidthPolicy: "strip",
280
+ tagsPolicy: "strip",
281
+ });
282
+ return out.replace(INVISIBLE_OP_RE_G, "");
283
+ }
284
+
285
+ // freeText(s, label, policy?) — the dangerous-codepoint screen for an
286
+ // UNCONSTRAINED UTF-8 text field (customer note, review body, gift
287
+ // message, product Q&A). Unlike the ASCII-allowlist validators above,
288
+ // these fields legitimately carry arbitrary letters, so the codepoint
289
+ // catalog is the right backing. Throws TypeError on a refused codepoint;
290
+ // returns the input on success. See firstCodepointThreat for the policies.
291
+ function freeText(s, label, policy) {
292
+ var name = label || "value";
293
+ if (typeof s !== "string") {
294
+ throw new TypeError(name + " must be a string");
295
+ }
296
+ var reason = firstCodepointThreat(s, policy);
297
+ if (reason) throw new TypeError(name + " " + reason);
199
298
  return s;
200
299
  }
201
300
 
@@ -213,9 +312,12 @@ module.exports = {
213
312
  applyCharStripPolicies: codepointClass.applyCharStripPolicies,
214
313
 
215
314
  // Shop validators.
216
- asciiUpperLetters: asciiUpperLetters,
217
- currencyCode: currencyCode,
218
- slugLabel: slugLabel,
219
- hostLabel: hostLabel,
220
- freeText: freeText,
315
+ asciiUpperLetters: asciiUpperLetters,
316
+ currencyCode: currencyCode,
317
+ slugLabel: slugLabel,
318
+ hostLabel: hostLabel,
319
+ freeText: freeText,
320
+ hasCodepointThreat: hasCodepointThreat,
321
+ firstCodepointThreat: firstCodepointThreat,
322
+ scrubInvisible: scrubInvisible,
221
323
  };
@@ -84,6 +84,7 @@
84
84
  */
85
85
 
86
86
  var b = require("./vendor/blamejs");
87
+ var textGuard = require("./text-guard");
87
88
 
88
89
  var MAX_SLUG_LEN = 80;
89
90
  var MAX_THEME_SLUG_LEN = 80;
@@ -130,14 +131,10 @@ var SHA3_512_HEX_RE = /^[0-9a-f]{128}$/;
130
131
  var CONTENT_TYPE_RE = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+(?:\s*;\s*[A-Za-z0-9!#$&^_.+-]+\s*=\s*(?:"[^"\x00-\x1f]*"|[A-Za-z0-9!#$&^_.+-]+))*$/;
131
132
 
132
133
  // Refuse C0 control bytes + DEL in operator-authored text fields.
133
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
134
134
 
135
135
  // Zero-width / direction-override family — mirrors the rest of the
136
136
  // shop primitives. Spelled with \u-escapes so ESLint's
137
137
  // no-irregular-whitespace stays happy.
138
- var ZERO_WIDTH_RE = new RegExp(
139
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
140
- );
141
138
 
142
139
  var MS_PER_DAY = b.constants.TIME.days(1);
143
140
 
@@ -181,9 +178,7 @@ function _contentType(s) {
181
178
  MAX_CONTENT_TYPE_LEN + " chars"
182
179
  );
183
180
  }
184
- if (CONTROL_BYTE_LINE_RE.test(s)) {
185
- throw new TypeError("themeAssets: content_type contains control bytes");
186
- }
181
+ textGuard.freeText(s, "themeAssets: content_type", { singleLine: "reject" });
187
182
  if (!CONTENT_TYPE_RE.test(s)) {
188
183
  throw new TypeError(
189
184
  "themeAssets: content_type " + JSON.stringify(s) +
@@ -230,9 +225,7 @@ function _sourceUrl(s) {
230
225
  MAX_SOURCE_URL_LEN + " chars"
231
226
  );
232
227
  }
233
- if (CONTROL_BYTE_LINE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
234
- throw new TypeError("themeAssets: source_url contains control / zero-width bytes");
235
- }
228
+ textGuard.freeText(s, "themeAssets: source_url", { singleLine: "reject", zeroWidth: "reject" });
236
229
  if (s.charCodeAt(0) === 47 /* "/" */) {
237
230
  if (s.length > 1 && s.charCodeAt(1) === 47) {
238
231
  throw new TypeError(
@@ -264,12 +257,7 @@ function _altText(s) {
264
257
  MAX_ALT_TEXT_LEN + " chars (or null)"
265
258
  );
266
259
  }
267
- if (CONTROL_BYTE_LINE_RE.test(s)) {
268
- throw new TypeError("themeAssets: alt_text contains control bytes (incl. CR/LF)");
269
- }
270
- if (ZERO_WIDTH_RE.test(s)) {
271
- throw new TypeError("themeAssets: alt_text contains zero-width / direction-override characters");
272
- }
260
+ textGuard.freeText(s, "themeAssets: alt_text", { singleLine: "reject", zeroWidth: "reject" });
273
261
  return s;
274
262
  }
275
263
 
@@ -58,6 +58,7 @@
58
58
  */
59
59
 
60
60
  var b = require("./vendor/blamejs");
61
+ var textGuard = require("./text-guard");
61
62
 
62
63
  var MAX_KIND_LEN = 64;
63
64
  var MAX_RESOURCE_ID_LEN = 128;
@@ -90,14 +91,10 @@ var LOCALE_RE = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
90
91
  // allowed (operators author multi-line page bodies) — the rest of
91
92
  // the C0 range would only reach the storefront as a smuggling
92
93
  // payload or a copy-paste accident from a terminal.
93
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
94
94
 
95
95
  // Zero-width / direction-override family. Spelled with \u-escapes
96
96
  // so ESLint's no-irregular-whitespace stays happy. Same defence the
97
97
  // rest of the shop uses on operator-authored strings.
98
- var ZERO_WIDTH_RE = new RegExp(
99
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
100
- );
101
98
 
102
99
  // ---- validators ---------------------------------------------------------
103
100
 
@@ -193,10 +190,10 @@ function _value(s, fnLabel) {
193
190
  if (s.length > MAX_VALUE_LEN) {
194
191
  throw new TypeError(fnLabel + ": value must be <= " + MAX_VALUE_LEN + " characters");
195
192
  }
196
- if (CONTROL_BYTE_RE.test(s)) {
193
+ if (textGuard.hasCodepointThreat(s)) {
197
194
  throw new TypeError(fnLabel + ": value contains forbidden control bytes");
198
195
  }
199
- if (ZERO_WIDTH_RE.test(s)) {
196
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
200
197
  throw new TypeError(fnLabel + ": value contains zero-width / direction-override characters");
201
198
  }
202
199
  return s;
@@ -116,13 +116,9 @@ var EVENT_TYPES = Object.freeze(["impression", "click"]);
116
116
  var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
117
117
 
118
118
  // Refuse C0 control bytes + DEL in single-line operator strings.
119
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
120
119
 
121
120
  // Zero-width / direction-override family — same catalogue as the
122
121
  // other operator-authored primitives.
123
- var ZERO_WIDTH_RE = new RegExp(
124
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
125
- );
126
122
 
127
123
  // Hash namespace — opaque per-primitive constant so a session_id_hash
128
124
  // emitted by trustBadges can't collide with one emitted by analytics
@@ -142,6 +138,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
142
138
  ]);
143
139
 
144
140
  var b = require("./vendor/blamejs");
141
+ var textGuard = require("./text-guard");
145
142
 
146
143
  // ---- validators ---------------------------------------------------------
147
144
 
@@ -184,10 +181,10 @@ function _line(s, label, maxLen) {
184
181
  if (typeof s !== "string" || !s.length || s.length > maxLen) {
185
182
  throw new TypeError("trustBadges: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
186
183
  }
187
- if (CONTROL_BYTE_LINE_RE.test(s)) {
184
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
188
185
  throw new TypeError("trustBadges: " + label + " contains control bytes (incl. CR/LF)");
189
186
  }
190
- if (ZERO_WIDTH_RE.test(s)) {
187
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
191
188
  throw new TypeError("trustBadges: " + label + " contains zero-width / direction-override characters");
192
189
  }
193
190
  return s;
@@ -201,7 +198,7 @@ function _httpsOrRootUrl(s, label, maxLen) {
201
198
  if (typeof s !== "string" || !s.length || s.length > maxLen) {
202
199
  throw new TypeError("trustBadges: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
203
200
  }
204
- if (CONTROL_BYTE_LINE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
201
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject", zeroWidth: "reject" })) {
205
202
  throw new TypeError("trustBadges: " + label + " contains control / zero-width bytes");
206
203
  }
207
204
  if (s.charCodeAt(0) === 47 /* "/" */) {
@@ -95,10 +95,6 @@ var CURRENCY_RE = /^[A-Z]{3}$/;
95
95
  // as the sibling accounts-payable primitives — operator-rendered text
96
96
  // fields refuse these to keep the dashboard + printout safe from
97
97
  // header-injection + visual-spoofing attacks.
98
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
99
- var ZERO_WIDTH_RE = new RegExp(
100
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
101
- );
102
98
 
103
99
  var INVOICE_STATUSES = Object.freeze([
104
100
  "received", "approved", "paid", "disputed", "voided",
@@ -107,6 +103,7 @@ var INVOICE_STATUSES = Object.freeze([
107
103
  var MAX_DUE_WITHIN_DAYS = 3650; // ~10 years — bound the predicate
108
104
 
109
105
  var b = require("./vendor/blamejs");
106
+ var textGuard = require("./text-guard");
110
107
 
111
108
  var MS_PER_DAY = b.constants.TIME.days(1);
112
109
 
@@ -219,10 +216,10 @@ function _shortText(s, label, max) {
219
216
  if (s.length > max) {
220
217
  throw new TypeError("vendor-invoices: " + label + " must be <= " + max + " characters");
221
218
  }
222
- if (CONTROL_BYTE_RE.test(s)) {
219
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
223
220
  throw new TypeError("vendor-invoices: " + label + " contains control bytes");
224
221
  }
225
- if (ZERO_WIDTH_RE.test(s)) {
222
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
226
223
  throw new TypeError("vendor-invoices: " + label + " contains zero-width / direction-override bytes");
227
224
  }
228
225
  return s;
package/lib/vendors.js CHANGED
@@ -97,10 +97,6 @@ var PHONE_RE = /^\+?[1-9]\d{1,14}$/;
97
97
  // direction-override bytes are a slipping-class for header injection
98
98
  // + visual-spoofing attacks downstream. Spelled with \u-escapes so
99
99
  // ESLint's no-irregular-whitespace stays happy.
100
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
101
- var ZERO_WIDTH_RE = new RegExp(
102
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
103
- );
104
100
 
105
101
  // Mutable columns for `updateVendor(slug, patch)`. Slug is immutable
106
102
  // — it's the public handle + the join-table key; mutating it would
@@ -119,6 +115,7 @@ var ALLOWED_UPDATE_COLUMNS = Object.freeze([
119
115
 
120
116
  // Framework handle (the vendored blamejs); index.js re-exports this as .framework.
121
117
  var b = require("./vendor/blamejs");
118
+ var textGuard = require("./text-guard");
122
119
 
123
120
  // ---- validators ---------------------------------------------------------
124
121
 
@@ -159,12 +156,7 @@ function _name(s) {
159
156
  if (s.length > MAX_NAME_LEN) {
160
157
  throw new TypeError("vendors: name must be <= " + MAX_NAME_LEN + " characters");
161
158
  }
162
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
163
- throw new TypeError("vendors: name contains control bytes");
164
- }
165
- if (ZERO_WIDTH_RE.test(s)) {
166
- throw new TypeError("vendors: name contains zero-width / direction-override bytes");
167
- }
159
+ textGuard.freeText(s, "vendors: name", { singleLine: "reject", zeroWidth: "reject" });
168
160
  return s;
169
161
  }
170
162
 
@@ -186,9 +178,7 @@ function _payoutAddress(s) {
186
178
  if (s.length > MAX_PAYOUT_ADDRESS_LEN) {
187
179
  throw new TypeError("vendors: payout_address must be <= " + MAX_PAYOUT_ADDRESS_LEN + " characters");
188
180
  }
189
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
190
- throw new TypeError("vendors: payout_address contains control / zero-width bytes");
191
- }
181
+ textGuard.freeText(s, "vendors: payout_address", { singleLine: "reject", zeroWidth: "reject" });
192
182
  return s;
193
183
  }
194
184
 
@@ -221,9 +211,7 @@ function _addressJson(value) {
221
211
  if (encoded.length > MAX_ADDRESS_JSON_LEN) {
222
212
  throw new TypeError("vendors: address JSON must be <= " + MAX_ADDRESS_JSON_LEN + " characters serialised");
223
213
  }
224
- if (CONTROL_BYTE_STRICT_RE.test(encoded) || ZERO_WIDTH_RE.test(encoded)) {
225
- throw new TypeError("vendors: address contains control / zero-width bytes");
226
- }
214
+ textGuard.freeText(encoded, "vendors: address", { singleLine: "reject", zeroWidth: "reject" });
227
215
  return encoded;
228
216
  }
229
217
 
@@ -265,9 +253,7 @@ function _orderId(s) {
265
253
  if (s.length > 256) {
266
254
  throw new TypeError("vendors: order_id must be <= 256 characters");
267
255
  }
268
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
269
- throw new TypeError("vendors: order_id contains control / zero-width bytes");
270
- }
256
+ textGuard.freeText(s, "vendors: order_id", { singleLine: "reject", zeroWidth: "reject" });
271
257
  return s;
272
258
  }
273
259
 
@@ -79,6 +79,7 @@
79
79
  */
80
80
 
81
81
  var b = require("./vendor/blamejs");
82
+ var textGuard = require("./text-guard");
82
83
  var C = b.constants;
83
84
 
84
85
  var SECRET_NAMESPACE = "webhook-receiver-secret";
@@ -118,10 +119,6 @@ var MAX_REASON_LEN = 1024;
118
119
 
119
120
  var SIGNATURE_HEX_RE = /^[0-9a-f]{64}$/i; // 32 bytes of HMAC-SHA-256 = 64 hex chars
120
121
 
121
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
122
- var ZERO_WIDTH_RE = new RegExp(
123
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
124
- );
125
122
 
126
123
  var STATUSES = ["received", "processed", "failed"];
127
124
 
@@ -198,9 +195,7 @@ function _outcome(s) {
198
195
  if (s.length > MAX_OUTCOME_LEN) {
199
196
  throw new TypeError("webhookReceiver: outcome must be <= " + MAX_OUTCOME_LEN + " characters");
200
197
  }
201
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
202
- throw new TypeError("webhookReceiver: outcome contains control / zero-width bytes");
203
- }
198
+ textGuard.freeText(s, "webhookReceiver: outcome", { singleLine: "reject", zeroWidth: "reject" });
204
199
  return s;
205
200
  }
206
201
 
@@ -215,9 +210,7 @@ function _reason(s) {
215
210
  if (s.length > MAX_REASON_LEN) {
216
211
  throw new TypeError("webhookReceiver: reason must be <= " + MAX_REASON_LEN + " characters");
217
212
  }
218
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
219
- throw new TypeError("webhookReceiver: reason contains control / zero-width bytes");
220
- }
213
+ textGuard.freeText(s, "webhookReceiver: reason", { singleLine: "reject", zeroWidth: "reject" });
221
214
  return s;
222
215
  }
223
216