@blamejs/blamejs-shop 0.5.20 → 0.5.22

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 (102) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -0
  3. package/SECURITY.md +23 -0
  4. package/lib/addresses.js +17 -3
  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 +7 -20
  16. package/lib/cms-blocks.js +4 -7
  17. package/lib/compliance-export.js +11 -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 +39 -15
  31. package/lib/email-warmup.js +2 -7
  32. package/lib/experiments.js +6 -10
  33. package/lib/externaldb-d1.js +47 -31
  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 +7 -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 +21 -28
  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 +179 -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/MANIFEST.json +12 -12
  89. package/lib/vendor/blamejs/CHANGELOG.md +32 -0
  90. package/lib/vendor/blamejs/NOTICE +1 -1
  91. package/lib/vendor/blamejs/README.md +1 -1
  92. package/lib/vendor/blamejs/lib/network-tls.js +98 -13
  93. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +12 -12
  94. package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +568 -304
  95. package/lib/vendor/blamejs/package.json +1 -1
  96. package/lib/vendor/blamejs/sbom.cdx.json +6 -6
  97. package/lib/vendor-invoices.js +3 -6
  98. package/lib/vendors.js +5 -19
  99. package/lib/webhook-receiver.js +3 -10
  100. package/lib/webhook-subscriptions.js +3 -13
  101. package/lib/wishlist-sharing.js +5 -10
  102. package/package.json +1 -1
@@ -69,6 +69,7 @@
69
69
  */
70
70
 
71
71
  var b = require("./vendor/blamejs");
72
+ var textGuard = require("./text-guard");
72
73
  var C = b.constants;
73
74
 
74
75
  // ---- constants ----------------------------------------------------------
@@ -114,10 +115,6 @@ var RETRY_BACKOFF_MS = [
114
115
 
115
116
  var SLUG_RE = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
116
117
  var LOCALE_RE = /^[A-Za-z]{2,8}(?:[-_][A-Za-z0-9]{2,8}){0,3}$/;
117
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
118
- var ZERO_WIDTH_RE = new RegExp(
119
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
120
- );
121
118
 
122
119
  // ---- validators ---------------------------------------------------------
123
120
 
@@ -162,12 +159,7 @@ function _credentials(s) {
162
159
  if (s.length > MAX_CREDENTIALS_LEN) {
163
160
  throw new TypeError("pushNotifications: credentials must be <= " + MAX_CREDENTIALS_LEN + " characters");
164
161
  }
165
- if (CONTROL_BYTE_RE.test(s)) {
166
- throw new TypeError("pushNotifications: credentials contains control bytes");
167
- }
168
- if (ZERO_WIDTH_RE.test(s)) {
169
- throw new TypeError("pushNotifications: credentials contains zero-width / bidi-override codepoints");
170
- }
162
+ textGuard.freeText(s, "pushNotifications: credentials", { zeroWidth: "reject" });
171
163
  return s;
172
164
  }
173
165
 
@@ -178,12 +170,7 @@ function _deviceToken(s) {
178
170
  if (s.length > MAX_TOKEN_LEN) {
179
171
  throw new TypeError("pushNotifications: device_token must be <= " + MAX_TOKEN_LEN + " characters");
180
172
  }
181
- if (CONTROL_BYTE_RE.test(s)) {
182
- throw new TypeError("pushNotifications: device_token contains control bytes");
183
- }
184
- if (ZERO_WIDTH_RE.test(s)) {
185
- throw new TypeError("pushNotifications: device_token contains zero-width / bidi-override codepoints");
186
- }
173
+ textGuard.freeText(s, "pushNotifications: device_token", { zeroWidth: "reject" });
187
174
  return s;
188
175
  }
189
176
 
@@ -208,12 +195,7 @@ function _title(s) {
208
195
  if (s.length > MAX_TITLE_LEN) {
209
196
  throw new TypeError("pushNotifications: title must be <= " + MAX_TITLE_LEN + " characters");
210
197
  }
211
- if (CONTROL_BYTE_RE.test(s)) {
212
- throw new TypeError("pushNotifications: title contains control bytes");
213
- }
214
- if (ZERO_WIDTH_RE.test(s)) {
215
- throw new TypeError("pushNotifications: title contains zero-width / bidi-override codepoints");
216
- }
198
+ textGuard.freeText(s, "pushNotifications: title", { zeroWidth: "reject", bidiMarks: "allow" });
217
199
  return s;
218
200
  }
219
201
 
@@ -224,12 +206,7 @@ function _body(s) {
224
206
  if (s.length > MAX_BODY_LEN) {
225
207
  throw new TypeError("pushNotifications: body must be <= " + MAX_BODY_LEN + " characters");
226
208
  }
227
- if (CONTROL_BYTE_RE.test(s)) {
228
- throw new TypeError("pushNotifications: body contains control bytes");
229
- }
230
- if (ZERO_WIDTH_RE.test(s)) {
231
- throw new TypeError("pushNotifications: body contains zero-width / bidi-override codepoints");
232
- }
209
+ textGuard.freeText(s, "pushNotifications: body", { zeroWidth: "reject", bidiMarks: "allow" });
233
210
  return s;
234
211
  }
235
212
 
@@ -267,7 +244,7 @@ function _reason(s, label) {
267
244
  if (s.length > MAX_REASON_LEN) {
268
245
  throw new TypeError("pushNotifications: " + label + " must be <= " + MAX_REASON_LEN + " characters");
269
246
  }
270
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
247
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
271
248
  throw new TypeError("pushNotifications: " + label + " contains control / zero-width bytes");
272
249
  }
273
250
  return s;
@@ -280,9 +257,7 @@ function _providerMessageId(s) {
280
257
  if (s.length > MAX_PROV_MSG_ID_LEN) {
281
258
  throw new TypeError("pushNotifications: provider_message_id must be <= " + MAX_PROV_MSG_ID_LEN + " characters");
282
259
  }
283
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
284
- throw new TypeError("pushNotifications: provider_message_id contains control / zero-width bytes");
285
- }
260
+ textGuard.freeText(s, "pushNotifications: provider_message_id", { zeroWidth: "reject" });
286
261
  return s;
287
262
  }
288
263
 
@@ -168,12 +168,9 @@ var ICON_SIZES_RE = /^(?:any|\d+x\d+)(?:\s+(?:any|\d+x\d+))*$/;
168
168
  // spec recommends (image/png / image/svg+xml / image/webp).
169
169
  var ICON_TYPE_RE = /^[a-z]+\/[a-z0-9.+-]+$/;
170
170
 
171
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
172
- var ZERO_WIDTH_RE = new RegExp(
173
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
174
- );
175
171
 
176
172
  var b = require("./vendor/blamejs");
173
+ var textGuard = require("./text-guard");
177
174
 
178
175
  // ---- monotonic clock ---------------------------------------------------
179
176
  //
@@ -202,10 +199,10 @@ function _string(s, label, max) {
202
199
  if (s.length > max) {
203
200
  throw new TypeError("pwaManifest: " + label + " must be <= " + max + " characters");
204
201
  }
205
- if (CONTROL_BYTE_RE.test(s)) {
202
+ if (textGuard.hasCodepointThreat(s)) {
206
203
  throw new TypeError("pwaManifest: " + label + " contains control bytes");
207
204
  }
208
- if (ZERO_WIDTH_RE.test(s)) {
205
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
209
206
  throw new TypeError("pwaManifest: " + label + " contains zero-width / direction-override characters");
210
207
  }
211
208
  return s;
@@ -270,7 +267,7 @@ function _url(s, label) {
270
267
  if (s.length > MAX_URL_LEN) {
271
268
  throw new TypeError("pwaManifest: " + label + " must be <= " + MAX_URL_LEN + " characters");
272
269
  }
273
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
270
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
274
271
  throw new TypeError("pwaManifest: " + label + " contains control / zero-width bytes");
275
272
  }
276
273
  if (s.charCodeAt(0) === 47 /* "/" */) {
@@ -416,10 +413,8 @@ function _validateRuntimeRules(rules) {
416
413
  MAX_URL_PATTERN_LEN + " characters"
417
414
  );
418
415
  }
419
- if (CONTROL_BYTE_RE.test(r.url_pattern) || ZERO_WIDTH_RE.test(r.url_pattern)) {
420
- throw new TypeError(
421
- "pwaManifest: runtime_rules[" + i + "].url_pattern contains control / zero-width bytes"
422
- );
416
+ if (textGuard.hasCodepointThreat(r.url_pattern, { zeroWidth: "reject" })) {
417
+ throw new TypeError("pwaManifest: runtime_rules[" + i + "].url_pattern contains control / zero-width bytes");
423
418
  }
424
419
  _enum(r.strategy, ALLOWED_STRATEGIES, "runtime_rules[" + i + "].strategy");
425
420
  if (seen[r.url_pattern]) {
package/lib/quotes.js CHANGED
@@ -145,11 +145,6 @@ var VIEW_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/;
145
145
  // are allowed inside multi-line free-text fields (message,
146
146
  // operator_notes, line.notes); single-line fields (terms, reject /
147
147
  // cancel reason, accepted_by) refuse them too.
148
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
149
- var CONTROL_BYTE_MULTILINE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
150
- var ZERO_WIDTH_RE = new RegExp(
151
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
152
- );
153
148
 
154
149
  var QUOTE_STATUSES = Object.freeze([
155
150
  "requested", "responded", "accepted", "rejected",
@@ -160,6 +155,7 @@ var TERMINAL_STATUSES = Object.freeze([
160
155
  ]);
161
156
 
162
157
  var b = require("./vendor/blamejs");
158
+ var textGuard = require("./text-guard");
163
159
 
164
160
  // The quote lifecycle, modelled on b.fsm — the same composition the order
165
161
  // primitive uses (lib/order.js#_getOrderFsm). The FSM is the single source of
@@ -316,10 +312,10 @@ function _optShortText(s, label, max) {
316
312
  if (s.length > max) {
317
313
  throw new TypeError("quotes: " + label + " must be <= " + max + " characters");
318
314
  }
319
- if (CONTROL_BYTE_RE.test(s)) {
315
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
320
316
  throw new TypeError("quotes: " + label + " contains control bytes");
321
317
  }
322
- if (ZERO_WIDTH_RE.test(s)) {
318
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
323
319
  throw new TypeError("quotes: " + label + " contains zero-width / direction-override bytes");
324
320
  }
325
321
  return s;
@@ -336,10 +332,10 @@ function _optLongText(s, label, max) {
336
332
  if (s.length > max) {
337
333
  throw new TypeError("quotes: " + label + " must be <= " + max + " characters");
338
334
  }
339
- if (CONTROL_BYTE_MULTILINE_RE.test(s)) {
335
+ if (textGuard.hasCodepointThreat(s)) {
340
336
  throw new TypeError("quotes: " + label + " contains control bytes");
341
337
  }
342
- if (ZERO_WIDTH_RE.test(s)) {
338
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
343
339
  throw new TypeError("quotes: " + label + " contains zero-width / direction-override bytes");
344
340
  }
345
341
  return s;
package/lib/reviews.js CHANGED
@@ -50,9 +50,9 @@ var REVIEW_ORDER_KEY = ["created_at:desc", "id:desc"];
50
50
  var CUSTOMER_ORDER_KEY = ["created_at:desc", "id:desc"];
51
51
 
52
52
  var ALLOWED_STATUSES = ["pending", "published", "rejected"];
53
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
54
53
 
55
54
  var b = require("./vendor/blamejs");
55
+ var textGuard = require("./text-guard");
56
56
 
57
57
  // ---- validators ---------------------------------------------------------
58
58
 
@@ -75,9 +75,7 @@ function _title(s) {
75
75
  if (s.length > MAX_TITLE_LEN) {
76
76
  throw new TypeError("reviews: title must be <= " + MAX_TITLE_LEN + " characters");
77
77
  }
78
- if (CONTROL_BYTE_RE.test(s)) {
79
- throw new TypeError("reviews: title contains control bytes");
80
- }
78
+ textGuard.freeText(s, "reviews: title", { bidiMarks: "allow" });
81
79
  return s;
82
80
  }
83
81
 
@@ -89,9 +87,7 @@ function _body(s) {
89
87
  if (s.length > MAX_BODY_LEN) {
90
88
  throw new TypeError("reviews: body must be <= " + MAX_BODY_LEN + " characters");
91
89
  }
92
- if (CONTROL_BYTE_RE.test(s)) {
93
- throw new TypeError("reviews: body contains control bytes");
94
- }
90
+ textGuard.freeText(s, "reviews: body", { bidiMarks: "allow" });
95
91
  return s;
96
92
  }
97
93
 
@@ -288,9 +284,7 @@ function create(opts) {
288
284
  if (reason.length > MAX_REJECT_REASON_LEN) {
289
285
  throw new TypeError("reviews.reject: reason must be <= " + MAX_REJECT_REASON_LEN + " characters");
290
286
  }
291
- if (CONTROL_BYTE_RE.test(reason)) {
292
- throw new TypeError("reviews.reject: reason contains control bytes");
293
- }
287
+ textGuard.freeText(reason, "reviews.reject: reason", { bidiMarks: "allow" });
294
288
  }
295
289
  var ts = _now();
296
290
  var existing = (await query("SELECT id, status FROM reviews WHERE id = ?1", [id])).rows[0];
@@ -96,9 +96,9 @@ var MAX_CRAWL_DELAY = 86400; // 24 h — anything larger is a typo
96
96
  var MAX_PRIORITY = 1000000;
97
97
  var MAX_RULE_ID_LEN = 80;
98
98
 
99
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
100
99
 
101
100
  var b = require("./vendor/blamejs");
101
+ var textGuard = require("./text-guard");
102
102
 
103
103
  // ---- predefined templates ---------------------------------------------
104
104
  //
@@ -183,9 +183,7 @@ function _userAgent(s) {
183
183
  "robotsConfig: user_agent must be a string 1.." + MAX_USER_AGENT_LEN + " chars"
184
184
  );
185
185
  }
186
- if (CONTROL_BYTE_RE.test(s)) {
187
- throw new TypeError("robotsConfig: user_agent must not contain control bytes");
188
- }
186
+ textGuard.freeText(s, "robotsConfig: user_agent", { singleLine: "reject" });
189
187
  // robots.txt is a colon-delimited key/value format — a colon in the
190
188
  // user-agent value would shift the parser onto a different key.
191
189
  if (s.indexOf(":") !== -1) {
@@ -200,7 +198,7 @@ function _pathEntry(s, label) {
200
198
  "robotsConfig: " + label + " entry must be a string 1.." + MAX_PATH_LEN + " chars"
201
199
  );
202
200
  }
203
- if (CONTROL_BYTE_RE.test(s)) {
201
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
204
202
  throw new TypeError("robotsConfig: " + label + " entry must not contain control bytes");
205
203
  }
206
204
  // robots.txt path entries are /-rooted absolute paths. Wildcards
@@ -258,9 +256,7 @@ function _ruleId(s) {
258
256
  "robotsConfig: rule_id must be a string 1.." + MAX_RULE_ID_LEN + " chars"
259
257
  );
260
258
  }
261
- if (CONTROL_BYTE_RE.test(s)) {
262
- throw new TypeError("robotsConfig: rule_id must not contain control bytes");
263
- }
259
+ textGuard.freeText(s, "robotsConfig: rule_id", { singleLine: "reject" });
264
260
  return s;
265
261
  }
266
262
 
@@ -270,9 +266,7 @@ function _sitemapUrl(u) {
270
266
  "robotsConfig: sitemap url must be a string 1.." + MAX_SITEMAP_URL_LEN + " chars"
271
267
  );
272
268
  }
273
- if (CONTROL_BYTE_RE.test(u)) {
274
- throw new TypeError("robotsConfig: sitemap url must not contain control bytes");
275
- }
269
+ textGuard.freeText(u, "robotsConfig: sitemap url", { singleLine: "reject" });
276
270
  // robots.txt only carries the URL itself, so the path-rooted form
277
271
  // promo-banners accepts isn't meaningful here — every crawler treats
278
272
  // the Sitemap: value as an absolute URL. https-only protects against
@@ -293,9 +287,7 @@ function _hostDirective(s) {
293
287
  "robotsConfig: host must be a string 1.." + MAX_HOST_LEN + " chars"
294
288
  );
295
289
  }
296
- if (CONTROL_BYTE_RE.test(s)) {
297
- throw new TypeError("robotsConfig: host must not contain control bytes");
298
- }
290
+ textGuard.freeText(s, "robotsConfig: host", { singleLine: "reject" });
299
291
  // The host directive is a bare hostname (optionally with :port) —
300
292
  // no scheme, no path. Refuse anything that looks like a URL so an
301
293
  // operator can't accidentally paste the full origin URL in.
@@ -104,6 +104,7 @@
104
104
  */
105
105
 
106
106
  var b = require("./vendor/blamejs");
107
+ var textGuard = require("./text-guard");
107
108
  var C = b.constants;
108
109
 
109
110
  // ---- constants ----------------------------------------------------------
@@ -122,7 +123,6 @@ var DEFAULT_LIMIT = 50;
122
123
  var MAX_LIMIT = 500;
123
124
  var MAX_DAYS_AHEAD = 365;
124
125
 
125
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
126
126
 
127
127
  // FSM transition map. The keys are (from_status, action) → to_status.
128
128
  // Defined as a table so a misuse surfaces with the actual current
@@ -211,7 +211,7 @@ function _shortText(s, label, max) {
211
211
  throw new TypeError("salesTaxFilings: " + label +
212
212
  " must be a non-empty string <= " + max + " chars");
213
213
  }
214
- if (CONTROL_BYTE_RE.test(s)) {
214
+ if (textGuard.hasCodepointThreat(s)) {
215
215
  throw new TypeError("salesTaxFilings: " + label + " must not contain control bytes");
216
216
  }
217
217
  return s;
@@ -106,7 +106,6 @@ var ALLOWED_KINDS = ["categorical", "numeric_range", "boolean"];
106
106
  var ALLOWED_COLS = ["field", "kind", "buckets_json", "display_limit", "active"];
107
107
 
108
108
  // Refuse C0 control bytes + DEL on every operator-supplied string.
109
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
110
109
 
111
110
  // Operator-chosen identifier shape — lowercase alnum + hyphen +
112
111
  // underscore, must start with a letter. Same shape the rest of the
@@ -118,6 +117,7 @@ var KEY_RE = /^[a-z][a-z0-9_-]*$/;
118
117
  var FIELD_RE = /^[a-z][a-z0-9_-]*$/;
119
118
 
120
119
  var b = require("./vendor/blamejs");
120
+ var textGuard = require("./text-guard");
121
121
 
122
122
  // ---- validators ---------------------------------------------------------
123
123
 
@@ -175,7 +175,7 @@ function _label(s, fnLabel) {
175
175
  if (s.length > MAX_LABEL_LEN) {
176
176
  throw new TypeError(fnLabel + ": bucket label must be <= " + MAX_LABEL_LEN + " characters");
177
177
  }
178
- if (CONTROL_BYTE_RE.test(s)) {
178
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
179
179
  throw new TypeError(fnLabel + ": bucket label must not contain control bytes");
180
180
  }
181
181
  return s;
@@ -291,7 +291,7 @@ function _appliedFilters(input, fnLabel) {
291
291
  if (s.length > MAX_VALUE_LEN) {
292
292
  throw new TypeError(fnLabel + ": applied_filters['" + k + "'][" + j + "] must be <= " + MAX_VALUE_LEN + " characters");
293
293
  }
294
- if (CONTROL_BYTE_RE.test(s)) {
294
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
295
295
  throw new TypeError(fnLabel + ": applied_filters['" + k + "'][" + j + "] must not contain control bytes");
296
296
  }
297
297
  values.push(s);
@@ -318,7 +318,7 @@ function _value(raw, fnLabel) {
318
318
  if (s.length > MAX_VALUE_LEN) {
319
319
  throw new TypeError(fnLabel + ": value must be <= " + MAX_VALUE_LEN + " characters");
320
320
  }
321
- if (CONTROL_BYTE_RE.test(s)) {
321
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
322
322
  throw new TypeError(fnLabel + ": value must not contain control bytes");
323
323
  }
324
324
  return s;
@@ -331,7 +331,7 @@ function _sessionId(s, fnLabel) {
331
331
  if (s.length > 256) {
332
332
  throw new TypeError(fnLabel + ": session_id must be <= 256 characters");
333
333
  }
334
- if (CONTROL_BYTE_RE.test(s)) {
334
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
335
335
  throw new TypeError(fnLabel + ": session_id must not contain control bytes");
336
336
  }
337
337
  return s;
@@ -121,12 +121,9 @@ var SIGNAL_RE = /^[a-z][a-z0-9_-]*$/;
121
121
  // which covers SKU notation across every common upstream system
122
122
  // without opening the door to control bytes or whitespace.
123
123
  var PRODUCT_ID_RE = /^[A-Za-z0-9._:/-]+$/;
124
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
125
- var ZERO_WIDTH_RE = new RegExp(
126
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
127
- );
128
124
 
129
125
  var b = require("./vendor/blamejs");
126
+ var textGuard = require("./text-guard");
130
127
 
131
128
  // ---- monotonic clock ---------------------------------------------------
132
129
  //
@@ -175,7 +172,7 @@ function _name(s, fnLabel) {
175
172
  if (s.length > MAX_NAME_LEN) {
176
173
  throw new TypeError(fnLabel + ": name must be <= " + MAX_NAME_LEN + " characters");
177
174
  }
178
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
175
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject", zeroWidth: "reject" })) {
179
176
  throw new TypeError(fnLabel + ": name contains control / zero-width bytes");
180
177
  }
181
178
  return s;
@@ -226,7 +223,7 @@ function _normalizeQuery(s, fnLabel) {
226
223
  if (s.length > MAX_QUERY_LEN) {
227
224
  throw new TypeError(fnLabel + ": query must be <= " + MAX_QUERY_LEN + " characters");
228
225
  }
229
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
226
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject", zeroWidth: "reject" })) {
230
227
  throw new TypeError(fnLabel + ": query contains control / zero-width bytes");
231
228
  }
232
229
  var normalized = s.toLowerCase().replace(/\s+/g, " ").trim();
@@ -270,7 +267,7 @@ function _sessionIdRaw(s, fnLabel) {
270
267
  if (s.length > MAX_SESSION_ID_LEN) {
271
268
  throw new TypeError(fnLabel + ": session_id must be <= " + MAX_SESSION_ID_LEN + " characters");
272
269
  }
273
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
270
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject", zeroWidth: "reject" })) {
274
271
  throw new TypeError(fnLabel + ": session_id contains control / zero-width bytes");
275
272
  }
276
273
  return s;
@@ -102,18 +102,6 @@ var MAX_QUERY_LEN = 500;
102
102
  var ALLOWED_KINDS = ["bidirectional", "directional"];
103
103
  var ALLOWED_GROUP_COLS = ["kind", "terms_json"];
104
104
 
105
- // Refuse C0 control bytes + DEL. Tokens are single-line search terms;
106
- // any newline / tab in a synonym entry would either be malformed
107
- // operator input or an attempt to smuggle separator-confusion payload
108
- // into the search log.
109
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
110
- // Zero-width + direction-override family — same defense as
111
- // `supportTickets`. Spelled with \u escapes so the file stays
112
- // ESLint no-irregular-whitespace clean.
113
- var ZERO_WIDTH_RE = new RegExp(
114
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
115
- );
116
-
117
105
  // Slug shape — operator-chosen identifier on synonym groups. Same
118
106
  // shape the rest of the shop uses: lowercase alnum + hyphen, must
119
107
  // start with a letter, 1..MAX_SLUG_LEN.
@@ -126,6 +114,7 @@ var TOKEN_SPLIT_RE = /[^a-z0-9-]+/g;
126
114
 
127
115
  // Framework handle (the vendored blamejs); index.js re-exports this as .framework.
128
116
  var b = require("./vendor/blamejs");
117
+ var textGuard = require("./text-guard");
129
118
 
130
119
  // ---- validators ---------------------------------------------------------
131
120
 
@@ -166,10 +155,10 @@ function _term(s, label, fnLabel) {
166
155
  if (trimmed.length > MAX_TERM_LEN) {
167
156
  throw new TypeError(fnLabel + ": " + label + " must be <= " + MAX_TERM_LEN + " characters");
168
157
  }
169
- if (CONTROL_BYTE_RE.test(trimmed)) {
158
+ if (textGuard.hasCodepointThreat(trimmed, { singleLine: "reject" })) {
170
159
  throw new TypeError(fnLabel + ": " + label + " contains control bytes");
171
160
  }
172
- if (ZERO_WIDTH_RE.test(trimmed)) {
161
+ if (textGuard.hasCodepointThreat(trimmed, { zeroWidth: "reject" })) {
173
162
  throw new TypeError(fnLabel + ": " + label + " contains zero-width / direction-override characters");
174
163
  }
175
164
  return trimmed;
@@ -247,13 +236,10 @@ function _queryString(s, fnLabel) {
247
236
  // give attackers a denial-of-search vector against legitimate
248
237
  // shoppers who happened to paste a stray BOM.
249
238
  function _sanitiseQuery(s) {
250
- return s
251
- .replace(/[\x00-\x1f\x7f]/g, " ")
252
- .replace(
253
- // Zero-width + direction-override family — see ZERO_WIDTH_RE.
254
- new RegExp("[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]", "g"),
255
- ""
256
- );
239
+ // Controls become a SPACE, not nothing: deleting them outright would weld
240
+ // the words on either side into one token. The invisible characters do get
241
+ // deleted — they sit inside a word rather than between two.
242
+ return textGuard.scrubInvisible(s.replace(/[\x00-\x1f\x7f]/g, " "));
257
243
  }
258
244
 
259
245
  function _tokenise(s) {