@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
@@ -101,18 +101,15 @@ var MAX_REGEX_SOURCE_LEN = 512;
101
101
  var MAX_LIMIT = 1000;
102
102
  var DEFAULT_LIMIT = 100;
103
103
 
104
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
105
104
  // Zero-width + invisible bytes: U+200B (zero-width space),
106
105
  // U+200C (zero-width non-joiner), U+200D (zero-width joiner),
107
106
  // U+2060 (word joiner), U+FEFF (zero-width no-break space / BOM).
108
107
  // Source code uses escape sequences so the file itself stays
109
108
  // linter-clean (raw zero-width bytes in source trip both
110
109
  // no-irregular-whitespace and no-misleading-character-class).
111
- var ZERO_WIDTH_RE = new RegExp(
112
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
113
- );
114
110
 
115
111
  var b = require("./vendor/blamejs");
112
+ var textGuard = require("./text-guard");
116
113
 
117
114
  // Monotonic clock — ensures consecutive _now() calls from the same
118
115
  // process never collide in the same millisecond, so updated_at /
@@ -153,9 +150,7 @@ function _sourcePath(s) {
153
150
  "siteRedirects: source_path must be <= " + MAX_SOURCE_PATH_LEN + " characters"
154
151
  );
155
152
  }
156
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
157
- throw new TypeError("siteRedirects: source_path contains control / zero-width bytes");
158
- }
153
+ textGuard.freeText(s, "siteRedirects: source_path", { singleLine: "reject", zeroWidth: "reject" });
159
154
  if (s.charCodeAt(0) !== 47 /* "/" */) {
160
155
  throw new TypeError("siteRedirects: source_path must be /-rooted (start with '/')");
161
156
  }
@@ -179,9 +174,7 @@ function _targetUrl(s) {
179
174
  "siteRedirects: target_url must be <= " + MAX_TARGET_URL_LEN + " characters"
180
175
  );
181
176
  }
182
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
183
- throw new TypeError("siteRedirects: target_url contains control / zero-width bytes");
184
- }
177
+ textGuard.freeText(s, "siteRedirects: target_url", { singleLine: "reject", zeroWidth: "reject" });
185
178
  if (s.charCodeAt(0) === 47 /* "/" */) {
186
179
  if (s.length > 1 && s.charCodeAt(1) === 47) {
187
180
  throw new TypeError(
@@ -272,9 +265,7 @@ function _compileRegex(source) {
272
265
  "siteRedirects: regex source must be <= " + MAX_REGEX_SOURCE_LEN + " characters"
273
266
  );
274
267
  }
275
- if (CONTROL_BYTE_RE.test(source) || ZERO_WIDTH_RE.test(source)) {
276
- throw new TypeError("siteRedirects: regex source contains control / zero-width bytes");
277
- }
268
+ textGuard.freeText(source, "siteRedirects: regex source", { singleLine: "reject", zeroWidth: "reject" });
278
269
  var anchored = "^(?:" + source + ")$";
279
270
  try {
280
271
  return b.regexLinear.compile(anchored);
@@ -500,9 +491,7 @@ function create(opts) {
500
491
  if (typeof path !== "string" || !path.length) {
501
492
  throw new TypeError("siteRedirects.resolveForPath: path must be a non-empty string");
502
493
  }
503
- if (CONTROL_BYTE_RE.test(path)) {
504
- throw new TypeError("siteRedirects.resolveForPath: path contains control bytes");
505
- }
494
+ textGuard.freeText(path, "siteRedirects.resolveForPath: path", { singleLine: "reject" });
506
495
  var now = _now();
507
496
 
508
497
  // -- exact ----------------------------------------------------------
@@ -107,9 +107,9 @@ var ALLOWED_CHANGEFREQS = Object.freeze([
107
107
  "never",
108
108
  ]);
109
109
 
110
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
111
110
 
112
111
  var b = require("./vendor/blamejs");
112
+ var textGuard = require("./text-guard");
113
113
 
114
114
  // ---- validators ---------------------------------------------------------
115
115
 
@@ -134,9 +134,7 @@ function _basePath(s) {
134
134
  if (typeof s !== "string" || s.length < 1 || s.length > MAX_BASE_PATH_LEN) {
135
135
  throw new TypeError("sitemapGenerator: base_path must be a string 1.." + MAX_BASE_PATH_LEN + " chars");
136
136
  }
137
- if (CONTROL_BYTE_RE.test(s)) {
138
- throw new TypeError("sitemapGenerator: base_path contains control bytes");
139
- }
137
+ textGuard.freeText(s, "sitemapGenerator: base_path", { singleLine: "reject" });
140
138
  if (s.charCodeAt(0) !== 47 /* "/" */) {
141
139
  throw new TypeError("sitemapGenerator: base_path must start with '/'");
142
140
  }
@@ -177,9 +175,7 @@ function _originUrl(u) {
177
175
  if (typeof u !== "string" || u.length < 1 || u.length > MAX_ORIGIN_URL_LEN) {
178
176
  throw new TypeError("sitemapGenerator: origin_url must be a string 1.." + MAX_ORIGIN_URL_LEN + " chars");
179
177
  }
180
- if (CONTROL_BYTE_RE.test(u)) {
181
- throw new TypeError("sitemapGenerator: origin_url contains control bytes");
182
- }
178
+ textGuard.freeText(u, "sitemapGenerator: origin_url", { singleLine: "reject" });
183
179
  try {
184
180
  b.safeUrl.parse(u, { allowedProtocols: ["https:"] });
185
181
  } catch (e) {
@@ -62,6 +62,7 @@
62
62
  */
63
63
 
64
64
  var b = require("./vendor/blamejs");
65
+ var textGuard = require("./text-guard");
65
66
  var C = b.constants;
66
67
 
67
68
  // ---- constants ----------------------------------------------------------
@@ -103,10 +104,6 @@ var SLUG_RE = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
103
104
  var COUNTRY_RE = /^[A-Z]{2}$/;
104
105
  var PHONE_HASH_RE = /^[0-9a-f]{128}$/;
105
106
  var PHONE_NORM_RE = /^\+[1-9][0-9]{6,14}$/; // E.164: +<country><subscriber>, 7-15 digits total
106
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
107
- var ZERO_WIDTH_RE = new RegExp(
108
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
109
- );
110
107
 
111
108
  // Inbound STOP-keyword detection. Operators in the US/UK/AU regulatory
112
109
  // envelope have all converged on this short list; the primitive
@@ -150,9 +147,7 @@ function _name(s) {
150
147
  if (s.length > MAX_NAME_LEN) {
151
148
  throw new TypeError("smsDispatcher: name must be <= " + MAX_NAME_LEN + " characters");
152
149
  }
153
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
154
- throw new TypeError("smsDispatcher: name contains control / zero-width bytes");
155
- }
150
+ textGuard.freeText(s, "smsDispatcher: name", { zeroWidth: "reject" });
156
151
  return s;
157
152
  }
158
153
 
@@ -163,9 +158,7 @@ function _endpointUrl(s) {
163
158
  if (s.length > MAX_URL_LEN) {
164
159
  throw new TypeError("smsDispatcher: endpoint_url must be <= " + MAX_URL_LEN + " characters");
165
160
  }
166
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
167
- throw new TypeError("smsDispatcher: endpoint_url contains control / zero-width bytes");
168
- }
161
+ textGuard.freeText(s, "smsDispatcher: endpoint_url", { zeroWidth: "reject" });
169
162
  // Shape gate — refuse anything that isn't an absolute https URL.
170
163
  // Provider endpoints are public APIs; operators using a non-https
171
164
  // egress are leaking the message body across the wire.
@@ -260,7 +253,7 @@ function _reason(s, label) {
260
253
  if (s.length > MAX_REASON_LEN) {
261
254
  throw new TypeError("smsDispatcher: " + label + " must be <= " + MAX_REASON_LEN + " characters");
262
255
  }
263
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
256
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
264
257
  throw new TypeError("smsDispatcher: " + label + " contains control / zero-width bytes");
265
258
  }
266
259
  return s;
@@ -274,9 +267,7 @@ function _optInText(s) {
274
267
  if (s.length > MAX_OPT_IN_TEXT_LEN) {
275
268
  throw new TypeError("smsDispatcher: opt_in_text must be <= " + MAX_OPT_IN_TEXT_LEN + " characters");
276
269
  }
277
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
278
- throw new TypeError("smsDispatcher: opt_in_text contains control / zero-width bytes");
279
- }
270
+ textGuard.freeText(s, "smsDispatcher: opt_in_text", { zeroWidth: "reject" });
280
271
  return s;
281
272
  }
282
273
 
@@ -287,9 +278,7 @@ function _providerRef(s) {
287
278
  if (s.length > MAX_PROVIDER_REF_LEN) {
288
279
  throw new TypeError("smsDispatcher: provider_ref must be <= " + MAX_PROVIDER_REF_LEN + " characters");
289
280
  }
290
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
291
- throw new TypeError("smsDispatcher: provider_ref contains control / zero-width bytes");
292
- }
281
+ textGuard.freeText(s, "smsDispatcher: provider_ref", { zeroWidth: "reject" });
293
282
  return s;
294
283
  }
295
284
 
@@ -333,12 +322,7 @@ function _body(s) {
333
322
  if (s.length > MAX_BODY_LEN) {
334
323
  throw new TypeError("smsDispatcher: body must be <= " + MAX_BODY_LEN + " characters");
335
324
  }
336
- if (CONTROL_BYTE_RE.test(s)) {
337
- throw new TypeError("smsDispatcher: body contains control bytes");
338
- }
339
- if (ZERO_WIDTH_RE.test(s)) {
340
- throw new TypeError("smsDispatcher: body contains zero-width / bidi-override codepoints");
341
- }
325
+ textGuard.freeText(s, "smsDispatcher: body", { zeroWidth: "reject" });
342
326
  // Refuse the degenerate emoji-only shape. Walk the string by code
343
327
  // point so surrogate pairs in the SMP emoji blocks count as one
344
328
  // character.
@@ -70,6 +70,7 @@
70
70
  */
71
71
 
72
72
  var b = require("./vendor/blamejs");
73
+ var textGuard = require("./text-guard");
73
74
  var C = b.constants;
74
75
 
75
76
  // ---- constants ----------------------------------------------------------
@@ -100,7 +101,6 @@ var LINE_STATES = Object.freeze([
100
101
  "pending", "received", "damaged", "partial",
101
102
  ]);
102
103
 
103
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
104
104
 
105
105
  // ---- monotonic clock ----------------------------------------------------
106
106
  //
@@ -171,9 +171,7 @@ function _reason(s) {
171
171
  if (typeof s !== "string" || !s.length || s.length > MAX_REASON_LEN) {
172
172
  throw new TypeError("stockReceipts: reason must be a non-empty string <= " + MAX_REASON_LEN + " chars");
173
173
  }
174
- if (CONTROL_BYTE_RE.test(s)) {
175
- throw new TypeError("stockReceipts: reason must not contain control bytes");
176
- }
174
+ textGuard.freeText(s, "stockReceipts: reason");
177
175
  return s;
178
176
  }
179
177
 
@@ -192,9 +190,7 @@ function _uaOpt(s) {
192
190
  if (typeof s !== "string" || s.length > MAX_UA_LEN) {
193
191
  throw new TypeError("stockReceipts: user_agent must be a string <= " + MAX_UA_LEN + " chars");
194
192
  }
195
- if (CONTROL_BYTE_RE.test(s)) {
196
- throw new TypeError("stockReceipts: user_agent must not contain control bytes");
197
- }
193
+ textGuard.freeText(s, "stockReceipts: user_agent");
198
194
  return s;
199
195
  }
200
196
 
@@ -203,9 +199,7 @@ function _ipOpt(s) {
203
199
  if (typeof s !== "string" || s.length > MAX_IP_LEN) {
204
200
  throw new TypeError("stockReceipts: client_ip must be a string <= " + MAX_IP_LEN + " chars");
205
201
  }
206
- if (CONTROL_BYTE_RE.test(s)) {
207
- throw new TypeError("stockReceipts: client_ip must not contain control bytes");
208
- }
202
+ textGuard.freeText(s, "stockReceipts: client_ip");
209
203
  return s;
210
204
  }
211
205
 
@@ -89,6 +89,7 @@
89
89
  */
90
90
 
91
91
  var b = require("./vendor/blamejs");
92
+ var textGuard = require("./text-guard");
92
93
 
93
94
  var MAX_SLUG_LEN = 80;
94
95
  var MAX_TITLE_LEN = 200;
@@ -140,14 +141,10 @@ var WIDGET_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
140
141
  var LOCALE_RE = /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/;
141
142
 
142
143
  // Refuse C0 control bytes + DEL in operator-authored text fields.
143
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
144
144
 
145
145
  // Zero-width / direction-override family — mirrors the rest of the
146
146
  // shop primitives. Spelled with \u-escapes so ESLint's
147
147
  // no-irregular-whitespace stays happy.
148
- var ZERO_WIDTH_RE = new RegExp(
149
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
150
- );
151
148
 
152
149
  // ---- validators ---------------------------------------------------------
153
150
 
@@ -166,12 +163,7 @@ function _title(s) {
166
163
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
167
164
  throw new TypeError("storefrontDashboards: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
168
165
  }
169
- if (CONTROL_BYTE_LINE_RE.test(s)) {
170
- throw new TypeError("storefrontDashboards: title contains control bytes (incl. CR/LF)");
171
- }
172
- if (ZERO_WIDTH_RE.test(s)) {
173
- throw new TypeError("storefrontDashboards: title contains zero-width / direction-override characters");
174
- }
166
+ textGuard.freeText(s, "storefrontDashboards: title", { singleLine: "reject", zeroWidth: "reject" });
175
167
  return s;
176
168
  }
177
169
 
@@ -74,6 +74,7 @@
74
74
  */
75
75
 
76
76
  var b = require("./vendor/blamejs");
77
+ var textGuard = require("./text-guard");
77
78
 
78
79
  var MAX_SLUG_LEN = 80;
79
80
  var MAX_TITLE_LEN = 200;
@@ -144,14 +145,9 @@ var WEBHOOK_EVENT_RE = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$/;
144
145
  // Refuse C0 control bytes + DEL in single-line strings. Textarea
145
146
  // values permit LF (the customer's free-text response is line-
146
147
  // oriented).
147
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
148
- var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
149
148
 
150
149
  // Zero-width / direction-override family. Spelled with \u-escapes
151
150
  // so ESLint's no-irregular-whitespace stays happy.
152
- var ZERO_WIDTH_RE = new RegExp(
153
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
154
- );
155
151
 
156
152
  // ---- validators ---------------------------------------------------------
157
153
 
@@ -170,10 +166,10 @@ function _line(s, label, maxLen, optional) {
170
166
  if (typeof s !== "string" || !s.length || s.length > maxLen) {
171
167
  throw new TypeError("storefrontForms: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
172
168
  }
173
- if (CONTROL_BYTE_LINE_RE.test(s)) {
169
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
174
170
  throw new TypeError("storefrontForms: " + label + " contains control bytes (incl. CR/LF)");
175
171
  }
176
- if (ZERO_WIDTH_RE.test(s)) {
172
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
177
173
  throw new TypeError("storefrontForms: " + label + " contains zero-width / direction-override characters");
178
174
  }
179
175
  return s;
@@ -184,12 +180,7 @@ function _description(s) {
184
180
  if (typeof s !== "string" || s.length > MAX_DESCRIPTION_LEN) {
185
181
  throw new TypeError("storefrontForms: description must be a string ≤ " + MAX_DESCRIPTION_LEN + " chars");
186
182
  }
187
- if (CONTROL_BYTE_BLOCK_RE.test(s)) {
188
- throw new TypeError("storefrontForms: description contains control bytes");
189
- }
190
- if (ZERO_WIDTH_RE.test(s)) {
191
- throw new TypeError("storefrontForms: description contains zero-width / direction-override characters");
192
- }
183
+ textGuard.freeText(s, "storefrontForms: description", { zeroWidth: "reject" });
193
184
  return s;
194
185
  }
195
186
 
@@ -246,7 +237,7 @@ function _fields(input) {
246
237
  if (typeof opt !== "string" || !opt.length || opt.length > MAX_OPTION_LEN) {
247
238
  throw new TypeError("storefrontForms: fields[" + i + "].options[" + j + "] must be a non-empty string ≤ " + MAX_OPTION_LEN + " chars");
248
239
  }
249
- if (CONTROL_BYTE_LINE_RE.test(opt) || ZERO_WIDTH_RE.test(opt)) {
240
+ if (textGuard.hasCodepointThreat(opt, { singleLine: "reject", zeroWidth: "reject" })) {
250
241
  throw new TypeError("storefrontForms: fields[" + i + "].options[" + j + "] contains control / zero-width characters");
251
242
  }
252
243
  if (Object.prototype.hasOwnProperty.call(seenOpts, opt)) {
@@ -275,9 +266,7 @@ function _submitTo(input) {
275
266
  if (typeof input.value !== "string" || !input.value.length || input.value.length > MAX_SUBMIT_TO_VALUE_LEN) {
276
267
  throw new TypeError("storefrontForms: submit_to.value must be a non-empty string ≤ " + MAX_SUBMIT_TO_VALUE_LEN + " chars");
277
268
  }
278
- if (CONTROL_BYTE_LINE_RE.test(input.value) || ZERO_WIDTH_RE.test(input.value)) {
279
- throw new TypeError("storefrontForms: submit_to.value contains control / zero-width characters");
280
- }
269
+ textGuard.freeText(input.value, "storefrontForms: submit_to.value", { singleLine: "reject", zeroWidth: "reject" });
281
270
  if (input.kind === "email") {
282
271
  var v = b.guardEmail.validate(input.value, { profile: "strict" });
283
272
  if (!v.ok) {
@@ -329,9 +318,7 @@ function _ipHash(s) {
329
318
  if (typeof s !== "string" || !s.length || s.length > 256) {
330
319
  throw new TypeError("storefrontForms: ip_hash must be a non-empty string ≤ 256 chars");
331
320
  }
332
- if (CONTROL_BYTE_LINE_RE.test(s)) {
333
- throw new TypeError("storefrontForms: ip_hash contains control bytes");
334
- }
321
+ textGuard.freeText(s, "storefrontForms: ip_hash", { singleLine: "reject" });
335
322
  return s;
336
323
  }
337
324
 
@@ -358,7 +345,7 @@ function _validateValue(field, raw) {
358
345
  if (typeof raw !== "string" || raw.length > MAX_TEXT_VALUE_LEN) {
359
346
  return { ok: false, error: "text value must be a string ≤ " + MAX_TEXT_VALUE_LEN + " chars" };
360
347
  }
361
- if (CONTROL_BYTE_LINE_RE.test(raw) || ZERO_WIDTH_RE.test(raw)) {
348
+ if (textGuard.hasCodepointThreat(raw, { singleLine: "reject", zeroWidth: "reject" })) {
362
349
  return { ok: false, error: "text value contains control / zero-width characters" };
363
350
  }
364
351
  return { ok: true, value: raw };
@@ -387,7 +374,7 @@ function _validateValue(field, raw) {
387
374
  if (typeof raw !== "string" || !raw.length || raw.length > MAX_TEXTAREA_VALUE_LEN) {
388
375
  return { ok: false, error: "textarea value must be a non-empty string ≤ " + MAX_TEXTAREA_VALUE_LEN + " chars" };
389
376
  }
390
- if (CONTROL_BYTE_BLOCK_RE.test(raw) || ZERO_WIDTH_RE.test(raw)) {
377
+ if (textGuard.hasCodepointThreat(raw, { zeroWidth: "reject" })) {
391
378
  return { ok: false, error: "textarea value contains control / zero-width characters" };
392
379
  }
393
380
  return { ok: true, value: raw };
@@ -74,6 +74,7 @@
74
74
  */
75
75
 
76
76
  var b = require("./vendor/blamejs");
77
+ var textGuard = require("./text-guard");
77
78
 
78
79
  var MAX_SLUG_LEN = 80;
79
80
  var MAX_TITLE_LEN = 200;
@@ -112,15 +113,10 @@ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
112
113
  // body permits LF (Markdown is line-oriented); single-line fields
113
114
  // refuse LF / CR so they can't smuggle a second line into a page
114
115
  // header / meta tag.
115
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
116
- var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
117
116
 
118
117
  // Zero-width / direction-override family — mirrors the promo-banners
119
118
  // + gift-options catalogues. Spelled with \u-escapes so ESLint's
120
119
  // no-irregular-whitespace stays happy.
121
- var ZERO_WIDTH_RE = new RegExp(
122
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
123
- );
124
120
 
125
121
  // ---- validators ---------------------------------------------------------
126
122
 
@@ -135,12 +131,7 @@ function _title(s) {
135
131
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
136
132
  throw new TypeError("storefrontPages: title must be a non-empty string ≤ " + MAX_TITLE_LEN + " chars");
137
133
  }
138
- if (CONTROL_BYTE_LINE_RE.test(s)) {
139
- throw new TypeError("storefrontPages: title contains control bytes (incl. CR/LF)");
140
- }
141
- if (ZERO_WIDTH_RE.test(s)) {
142
- throw new TypeError("storefrontPages: title contains zero-width / direction-override characters");
143
- }
134
+ textGuard.freeText(s, "storefrontPages: title", { singleLine: "reject", zeroWidth: "reject" });
144
135
  return s;
145
136
  }
146
137
 
@@ -148,12 +139,7 @@ function _body(s) {
148
139
  if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
149
140
  throw new TypeError("storefrontPages: body must be a non-empty string ≤ " + MAX_BODY_LEN + " chars");
150
141
  }
151
- if (CONTROL_BYTE_BLOCK_RE.test(s)) {
152
- throw new TypeError("storefrontPages: body contains control bytes");
153
- }
154
- if (ZERO_WIDTH_RE.test(s)) {
155
- throw new TypeError("storefrontPages: body contains zero-width / direction-override characters");
156
- }
142
+ textGuard.freeText(s, "storefrontPages: body", { zeroWidth: "reject" });
157
143
  return s;
158
144
  }
159
145
 
@@ -162,10 +148,10 @@ function _metaLine(s, label, maxLen) {
162
148
  if (typeof s !== "string" || s.length > maxLen) {
163
149
  throw new TypeError("storefrontPages: " + label + " must be a string ≤ " + maxLen + " chars");
164
150
  }
165
- if (CONTROL_BYTE_LINE_RE.test(s)) {
151
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
166
152
  throw new TypeError("storefrontPages: " + label + " contains control bytes (incl. CR/LF)");
167
153
  }
168
- if (ZERO_WIDTH_RE.test(s)) {
154
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
169
155
  throw new TypeError("storefrontPages: " + label + " contains zero-width / direction-override characters");
170
156
  }
171
157
  return s;
@@ -239,7 +225,7 @@ function _esc(s) {
239
225
  // downgrade an inline link.
240
226
  function _safeLinkUrl(url) {
241
227
  if (typeof url !== "string" || !url.length || url.length > 2048) return null;
242
- if (CONTROL_BYTE_LINE_RE.test(url) || ZERO_WIDTH_RE.test(url)) return null;
228
+ if (textGuard.hasCodepointThreat(url, { singleLine: "reject", zeroWidth: "reject" })) return null;
243
229
  if (url.charCodeAt(0) === 47 /* "/" */) {
244
230
  if (url.length > 1 && url.charCodeAt(1) === 47) return null;
245
231
  if (url.indexOf("..") !== -1) return null;
package/lib/storefront.js CHANGED
@@ -39,6 +39,7 @@ var REGISTRY_OCCASIONS = giftRegistryModule.OCCASIONS;
39
39
  var { AsyncLocalStorage } = require("node:async_hooks"); // allow:non-shop-require — Node-core per-request context (no npm dep); the framework itself composes it in db-role-context / log. No b.* request-context primitive exists to wrap it.
40
40
 
41
41
  var b = require("./vendor/blamejs");
42
+ var textGuard = require("./text-guard");
42
43
 
43
44
  // Per-request locale context store. The storefront's locale middleware
44
45
  // resolves the request's locale + chrome strings once and seeds this
@@ -2141,7 +2142,6 @@ function _clampPage(page, total, pageSize) {
2141
2142
  var SEARCH_MAX_FACET_KEYS = 32;
2142
2143
  var SEARCH_MAX_FACET_VALUES = 64;
2143
2144
  var SEARCH_MAX_VALUE_LEN = 256;
2144
- var SEARCH_CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
2145
2145
  // Global twin for strip-all replaces (the non-global one above is used for
2146
2146
  // presence tests; `.replace` needs the `g` flag to clear every byte).
2147
2147
  var SEARCH_CONTROL_BYTE_RE_G = /[\x00-\x1f\x7f]/g;
@@ -2162,7 +2162,7 @@ function _parseSearchFilters(url, facetDefs) {
2162
2162
  if (!Object.prototype.hasOwnProperty.call(known, rawKey)) return;
2163
2163
  if (typeof rawVal !== "string") return;
2164
2164
  if (!rawVal.length || rawVal.length > SEARCH_MAX_VALUE_LEN) return;
2165
- if (SEARCH_CONTROL_BYTE_RE.test(rawVal)) return;
2165
+ if (textGuard.hasCodepointThreat(rawVal, { singleLine: "reject" })) return;
2166
2166
  if (!Object.prototype.hasOwnProperty.call(out, rawKey)) {
2167
2167
  if (keyCount >= SEARCH_MAX_FACET_KEYS) return;
2168
2168
  out[rawKey] = [];
@@ -75,6 +75,7 @@
75
75
  */
76
76
 
77
77
  var b = require("./vendor/blamejs");
78
+ var textGuard = require("./text-guard");
78
79
 
79
80
  // ---- constants ----------------------------------------------------------
80
81
 
@@ -94,10 +95,6 @@ var DEFAULT_LIST_LIMIT = 100;
94
95
  // sibling subscription-controls primitive — operator-authored prose
95
96
  // flows into receipt emails + the operator dashboard, so the same
96
97
  // "no direction-override / no invisible glyph" floor applies.
97
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
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
 
@@ -146,9 +143,7 @@ function _reason(s) {
146
143
  if (s.length > MAX_REASON_LEN) {
147
144
  throw new TypeError("subscriptionBilling: reason must be <= " + MAX_REASON_LEN + " characters");
148
145
  }
149
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
150
- throw new TypeError("subscriptionBilling: reason contains control / zero-width bytes");
151
- }
146
+ textGuard.freeText(s, "subscriptionBilling: reason", { zeroWidth: "reject" });
152
147
  return s;
153
148
  }
154
149
 
@@ -164,7 +159,7 @@ function _shortText(s, label, max) {
164
159
  if (s.length > max) {
165
160
  throw new TypeError("subscriptionBilling: " + label + " must be <= " + max + " characters");
166
161
  }
167
- if (CONTROL_BYTE_RE.test(s)) {
162
+ if (textGuard.hasCodepointThreat(s)) {
168
163
  throw new TypeError("subscriptionBilling: " + label + " contains control bytes");
169
164
  }
170
165
  return s;
@@ -51,6 +51,7 @@
51
51
  */
52
52
 
53
53
  var b = require("./vendor/blamejs");
54
+ var textGuard = require("./text-guard");
54
55
 
55
56
  var C = b.constants;
56
57
 
@@ -123,10 +124,6 @@ var MAX_QUANTITY = 1000000;
123
124
  // prose. Refuses C0 / DEL and the LRM/RLM/ZWJ/ZWNJ family so a
124
125
  // malicious reason can't smuggle direction-override / invisible
125
126
  // content into the operator dashboard.
126
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
127
- var ZERO_WIDTH_RE = new RegExp(
128
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
129
- );
130
127
 
131
128
  // ---- validators ---------------------------------------------------------
132
129
 
@@ -142,9 +139,7 @@ function _reason(s) {
142
139
  if (s.length > MAX_REASON_LEN) {
143
140
  throw new TypeError("subscriptionControls: reason must be <= " + MAX_REASON_LEN + " characters");
144
141
  }
145
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
146
- throw new TypeError("subscriptionControls: reason contains control / zero-width bytes");
147
- }
142
+ textGuard.freeText(s, "subscriptionControls: reason", { zeroWidth: "reject" });
148
143
  return s;
149
144
  }
150
145
 
@@ -119,6 +119,7 @@
119
119
  */
120
120
 
121
121
  var b = require("./vendor/blamejs");
122
+ var textGuard = require("./text-guard");
122
123
 
123
124
  // ---- constants ----------------------------------------------------------
124
125
 
@@ -168,11 +169,6 @@ var ORDER_KEY_NEWEST = ["created_at:desc", "id:desc"];
168
169
  var ORDER_KEY_TOP_VOTED = ["vote_count:desc", "id:desc"];
169
170
  var ORDER_KEY_MOST_DISCUSSED = ["comment_count:desc", "id:desc"];
170
171
 
171
- var CONTROL_BYTE_BODY_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
172
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
173
- var ZERO_WIDTH_RE = new RegExp(
174
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
175
- );
176
172
 
177
173
  // ---- monotonic clock ----------------------------------------------------
178
174
  //
@@ -205,12 +201,7 @@ function _title(s) {
205
201
  if (s.length > MAX_TITLE_LEN) {
206
202
  throw new TypeError("suggestionBox: title must be <= " + MAX_TITLE_LEN + " characters");
207
203
  }
208
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
209
- throw new TypeError("suggestionBox: title must not contain control bytes");
210
- }
211
- if (ZERO_WIDTH_RE.test(s)) {
212
- throw new TypeError("suggestionBox: title contains zero-width / direction-override characters");
213
- }
204
+ textGuard.freeText(s, "suggestionBox: title", { singleLine: "reject", zeroWidth: "reject" });
214
205
  return s;
215
206
  }
216
207
 
@@ -225,12 +216,10 @@ function _body(s) {
225
216
  if (s.length > MAX_BODY_LEN) {
226
217
  throw new TypeError("suggestionBox: body must be <= " + MAX_BODY_LEN + " characters");
227
218
  }
228
- if (CONTROL_BYTE_BODY_RE.test(s)) {
219
+ if (textGuard.hasCodepointThreat(s)) {
229
220
  throw new TypeError("suggestionBox: body must not contain control bytes");
230
221
  }
231
- if (ZERO_WIDTH_RE.test(s)) {
232
- throw new TypeError("suggestionBox: body contains zero-width / direction-override characters");
233
- }
222
+ textGuard.freeText(s, "suggestionBox: body", { zeroWidth: "reject" });
234
223
  return s;
235
224
  }
236
225
 
@@ -307,9 +296,7 @@ function _sessionIdRaw(s) {
307
296
  if (s.length > 256) {
308
297
  throw new TypeError("suggestionBox: session_id must be <= 256 characters");
309
298
  }
310
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
311
- throw new TypeError("suggestionBox: session_id contains control / zero-width bytes");
312
- }
299
+ textGuard.freeText(s, "suggestionBox: session_id", { singleLine: "reject", zeroWidth: "reject" });
313
300
  return s;
314
301
  }
315
302
 
@@ -320,12 +307,10 @@ function _response(s) {
320
307
  if (s.length > MAX_RESPONSE_LEN) {
321
308
  throw new TypeError("suggestionBox: response must be <= " + MAX_RESPONSE_LEN + " characters");
322
309
  }
323
- if (CONTROL_BYTE_BODY_RE.test(s)) {
310
+ if (textGuard.hasCodepointThreat(s)) {
324
311
  throw new TypeError("suggestionBox: response must not contain control bytes");
325
312
  }
326
- if (ZERO_WIDTH_RE.test(s)) {
327
- throw new TypeError("suggestionBox: response contains zero-width / direction-override characters");
328
- }
313
+ textGuard.freeText(s, "suggestionBox: response", { zeroWidth: "reject" });
329
314
  // A status-only transition is allowed with response === "" (no
330
315
  // operator-visible reply, just a state change). Trim-then-check
331
316
  // for the operator-supplied case below in respondToSuggestion.
@@ -339,9 +324,7 @@ function _responder(s) {
339
324
  if (s.length > MAX_RESPONDER_LEN) {
340
325
  throw new TypeError("suggestionBox: responder must be <= " + MAX_RESPONDER_LEN + " characters");
341
326
  }
342
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
343
- throw new TypeError("suggestionBox: responder contains control / zero-width characters");
344
- }
327
+ textGuard.freeText(s, "suggestionBox: responder", { singleLine: "reject", zeroWidth: "reject" });
345
328
  return s;
346
329
  }
347
330
 
@@ -925,7 +908,7 @@ function create(opts) {
925
908
  if (typeof input.email_hash !== "string" || !input.email_hash.length || input.email_hash.length > 256) {
926
909
  throw new TypeError("suggestionBox." + method + ": email_hash must be a non-empty string <= 256 chars when provided");
927
910
  }
928
- if (CONTROL_BYTE_STRICT_RE.test(input.email_hash) || ZERO_WIDTH_RE.test(input.email_hash)) {
911
+ if (textGuard.hasCodepointThreat(input.email_hash, { singleLine: "reject", zeroWidth: "reject" })) {
929
912
  throw new TypeError("suggestionBox." + method + ": email_hash contains control / zero-width bytes");
930
913
  }
931
914
  emailHash = input.email_hash;