@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
@@ -46,7 +46,6 @@ var ALLOWED_VISIBILITY = ["internal", "customer_visible"];
46
46
  // Refuse C0 control bytes + DEL + the LRM/RLM/ZWJ/ZWNJ family of
47
47
  // zero-width / direction-override characters. Newlines (LF, CR) +
48
48
  // tab are tolerated because operator notes legitimately wrap.
49
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
50
49
  // Zero-width + direction-override family: ZWSP/ZWNJ/ZWJ (U+200B-200D),
51
50
  // LRM/RLM (U+200E/U+200F), the bidi-formatting block
52
51
  // (U+202A-U+202E), the invisible-math block (U+2060-U+2064), the
@@ -54,9 +53,6 @@ var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
54
53
  // Arabic letter mark (U+061C). Spelled with \u-escapes so the
55
54
  // detector pattern stays grep-able and ESLint's
56
55
  // no-irregular-whitespace stays happy.
57
- var ZERO_WIDTH_RE = new RegExp(
58
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
59
- );
60
56
 
61
57
  // (pinned DESC, created_at DESC, id DESC) — pinned notes float to
62
58
  // the top; the id tie-break keeps cursor pagination deterministic
@@ -65,6 +61,7 @@ var LIST_ORDER_KEY = ["pinned:desc", "created_at:desc", "id:desc"];
65
61
 
66
62
  // Framework handle (the vendored blamejs); index.js re-exports this as .framework.
67
63
  var b = require("./vendor/blamejs");
64
+ var textGuard = require("./text-guard");
68
65
 
69
66
  // ---- validators ---------------------------------------------------------
70
67
 
@@ -98,12 +95,7 @@ function _body(s) {
98
95
  if (s.length > MAX_BODY_LEN) {
99
96
  throw new TypeError("orderNotes: body must be <= " + MAX_BODY_LEN + " characters");
100
97
  }
101
- if (CONTROL_BYTE_RE.test(s)) {
102
- throw new TypeError("orderNotes: body contains control bytes");
103
- }
104
- if (ZERO_WIDTH_RE.test(s)) {
105
- throw new TypeError("orderNotes: body contains zero-width / direction-override characters");
106
- }
98
+ textGuard.freeText(s, "orderNotes: body", { zeroWidth: "reject" });
107
99
  return s;
108
100
  }
109
101
 
@@ -124,7 +116,7 @@ function _tags(input) {
124
116
  if (t.length > MAX_TAG_LEN) {
125
117
  throw new TypeError("orderNotes: tags[" + i + "] must be <= " + MAX_TAG_LEN + " characters");
126
118
  }
127
- if (CONTROL_BYTE_RE.test(t) || ZERO_WIDTH_RE.test(t)) {
119
+ if (textGuard.hasCodepointThreat(t, { zeroWidth: "reject" })) {
128
120
  throw new TypeError("orderNotes: tags[" + i + "] contains control / zero-width bytes");
129
121
  }
130
122
  out.push(t);
@@ -139,9 +131,7 @@ function _resolution(s) {
139
131
  if (s.length > MAX_RESOLUTION_LEN) {
140
132
  throw new TypeError("orderNotes: resolution must be <= " + MAX_RESOLUTION_LEN + " characters");
141
133
  }
142
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
143
- throw new TypeError("orderNotes: resolution contains control / zero-width bytes");
144
- }
134
+ textGuard.freeText(s, "orderNotes: resolution", { zeroWidth: "reject" });
145
135
  return s;
146
136
  }
147
137
 
@@ -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
  // ---- constants ----------------------------------------------------------
79
80
 
@@ -88,7 +89,6 @@ var MAX_LIST_LIMIT = 500;
88
89
  var DEFAULT_TOP_LIMIT = 10;
89
90
  var MAX_TOP_LIMIT = 100;
90
91
 
91
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
92
92
 
93
93
  // ---- monotonic clock ----------------------------------------------------
94
94
  //
@@ -130,9 +130,7 @@ function _comment(s) {
130
130
  if (s.length > MAX_COMMENT_LEN) {
131
131
  throw new TypeError("orderRatings: comment must be <= " + MAX_COMMENT_LEN + " chars");
132
132
  }
133
- if (CONTROL_BYTE_RE.test(s)) {
134
- throw new TypeError("orderRatings: comment must not contain control bytes");
135
- }
133
+ textGuard.freeText(s, "orderRatings: comment");
136
134
  return s;
137
135
  }
138
136
 
@@ -140,9 +138,7 @@ function _flagReason(s) {
140
138
  if (typeof s !== "string" || !s.length || s.length > MAX_FLAG_REASON_LEN) {
141
139
  throw new TypeError("orderRatings: reason must be a non-empty string <= " + MAX_FLAG_REASON_LEN + " chars");
142
140
  }
143
- if (CONTROL_BYTE_RE.test(s)) {
144
- throw new TypeError("orderRatings: reason must not contain control bytes");
145
- }
141
+ textGuard.freeText(s, "orderRatings: reason");
146
142
  return s;
147
143
  }
148
144
 
@@ -150,9 +146,7 @@ function _responseText(s) {
150
146
  if (typeof s !== "string" || !s.length || s.length > MAX_RESPONSE_LEN) {
151
147
  throw new TypeError("orderRatings: response must be a non-empty string <= " + MAX_RESPONSE_LEN + " chars");
152
148
  }
153
- if (CONTROL_BYTE_RE.test(s)) {
154
- throw new TypeError("orderRatings: response must not contain control bytes");
155
- }
149
+ textGuard.freeText(s, "orderRatings: response");
156
150
  return s;
157
151
  }
158
152
 
@@ -49,6 +49,7 @@
49
49
  */
50
50
 
51
51
  var b = require("./vendor/blamejs");
52
+ var textGuard = require("./text-guard");
52
53
 
53
54
  var PROCESSORS = ["stripe", "paypal", "square", "braintree", "authorize_net"];
54
55
  var ARCHIVE_REASONS = ["customer_request", "expired", "replaced", "fraud", "operator"];
@@ -65,7 +66,6 @@ var MAX_ACTOR_LEN = 128;
65
66
  // branch-on-this dispatch key.
66
67
  var BRAND_RE = /^[A-Za-z0-9 _.\-]{1,32}$/;
67
68
  var LAST4_RE = /^[0-9]{4}$/;
68
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
69
69
 
70
70
  // PAN screen: any field whose value is a string carrying 13-19
71
71
  // consecutive ASCII digits (with or without space/dash separators
@@ -125,9 +125,7 @@ function _brand(s) {
125
125
  if (s.length > MAX_BRAND_LEN) {
126
126
  throw new TypeError("paymentMethods: brand must be <= " + MAX_BRAND_LEN + " characters");
127
127
  }
128
- if (CONTROL_BYTE_RE.test(s)) {
129
- throw new TypeError("paymentMethods: brand contains control bytes");
130
- }
128
+ textGuard.freeText(s, "paymentMethods: brand", { singleLine: "reject" });
131
129
  if (!BRAND_RE.test(s)) {
132
130
  throw new TypeError("paymentMethods: brand contains characters outside [A-Za-z0-9 _.-]");
133
131
  }
@@ -142,9 +140,7 @@ function _label(s) {
142
140
  if (s.length > MAX_LABEL_LEN) {
143
141
  throw new TypeError("paymentMethods: label must be <= " + MAX_LABEL_LEN + " characters");
144
142
  }
145
- if (CONTROL_BYTE_RE.test(s)) {
146
- throw new TypeError("paymentMethods: label contains control bytes");
147
- }
143
+ textGuard.freeText(s, "paymentMethods: label", { singleLine: "reject" });
148
144
  return s;
149
145
  }
150
146
 
@@ -155,9 +151,7 @@ function _token(s) {
155
151
  if (s.length > MAX_TOKEN_LEN) {
156
152
  throw new TypeError("paymentMethods: processor_token must be <= " + MAX_TOKEN_LEN + " characters");
157
153
  }
158
- if (CONTROL_BYTE_RE.test(s)) {
159
- throw new TypeError("paymentMethods: processor_token contains control bytes");
160
- }
154
+ textGuard.freeText(s, "paymentMethods: processor_token", { singleLine: "reject" });
161
155
  return s;
162
156
  }
163
157
 
@@ -187,9 +181,7 @@ function _actor(s) {
187
181
  if (s.length > MAX_ACTOR_LEN) {
188
182
  throw new TypeError("paymentMethods: actor must be <= " + MAX_ACTOR_LEN + " characters");
189
183
  }
190
- if (CONTROL_BYTE_RE.test(s)) {
191
- throw new TypeError("paymentMethods: actor contains control bytes");
192
- }
184
+ textGuard.freeText(s, "paymentMethods: actor", { singleLine: "reject" });
193
185
  return s;
194
186
  }
195
187
 
@@ -129,6 +129,7 @@
129
129
  */
130
130
 
131
131
  var b = require("./vendor/blamejs");
132
+ var textGuard = require("./text-guard");
132
133
  var C = b.constants;
133
134
 
134
135
  // ---- constants ----------------------------------------------------------
@@ -147,10 +148,6 @@ var MS_PER_HOUR = C.TIME.hours(1);
147
148
 
148
149
  var SLUG_RE = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
149
150
  var FAILURE_CODE_RE = /^[a-z](?:[a-z0-9_]*[a-z0-9])?$/;
150
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
151
- var ZERO_WIDTH_RE = new RegExp(
152
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
153
- );
154
151
 
155
152
  // ---- validators ---------------------------------------------------------
156
153
 
@@ -232,9 +229,7 @@ function _reason(s) {
232
229
  if (s.length > MAX_REASON_LEN) {
233
230
  throw new TypeError("paymentRetries: reason must be <= " + MAX_REASON_LEN + " characters");
234
231
  }
235
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
236
- throw new TypeError("paymentRetries: reason contains control / zero-width bytes");
237
- }
232
+ textGuard.freeText(s, "paymentRetries: reason", { zeroWidth: "reject" });
238
233
  return s;
239
234
  }
240
235
 
@@ -99,6 +99,7 @@
99
99
  var nodeCrypto = require("node:crypto"); // allow:non-shop-require allow:weak-hash-sha2 — Provider spec (Meta CAPI, Google EC, TikTok Events, Pinterest CAPI, Snap CAPI) mandates SHA-256 of the normalised identifier; blamejs.crypto exposes SHA3-512 / SHAKE256 / namespaceHash, none of which match the wire format ad platforms accept. Node's stdlib SHA-256 hash is the minimum-surface route.
100
100
 
101
101
  var b = require("./vendor/blamejs");
102
+ var textGuard = require("./text-guard");
102
103
  var C = b.constants;
103
104
 
104
105
  // ---- constants ----------------------------------------------------------
@@ -132,10 +133,6 @@ var CURRENCY_RE = /^[A-Z]{3}$/;
132
133
  var UA_CLASS_RE = /^[a-z0-9._-]+$/;
133
134
  var IP_HASH_RE = /^[0-9a-f]+$/;
134
135
  var SHA256_HEX_RE = /^[0-9a-f]{64}$/;
135
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
136
- var ZERO_WIDTH_RE = new RegExp(
137
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
138
- );
139
136
  // Conservative email shape — operator-facing storefronts already
140
137
  // validated this via the framework's address-input primitive; the
141
138
  // gate here is a defensive last-line check before we normalise +
@@ -218,9 +215,7 @@ function _pixelId(s) {
218
215
  if (s.length > MAX_PIXEL_ID_LEN) {
219
216
  throw new TypeError("pixelEvents: pixel_id must be <= " + MAX_PIXEL_ID_LEN + " characters");
220
217
  }
221
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
222
- throw new TypeError("pixelEvents: pixel_id contains control / zero-width bytes");
223
- }
218
+ textGuard.freeText(s, "pixelEvents: pixel_id", { zeroWidth: "reject" });
224
219
  return s;
225
220
  }
226
221
 
@@ -231,9 +226,7 @@ function _accessToken(s) {
231
226
  if (s.length > MAX_ACCESS_TOKEN_LEN) {
232
227
  throw new TypeError("pixelEvents: access_token must be <= " + MAX_ACCESS_TOKEN_LEN + " characters");
233
228
  }
234
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
235
- throw new TypeError("pixelEvents: access_token contains control / zero-width bytes");
236
- }
229
+ textGuard.freeText(s, "pixelEvents: access_token", { zeroWidth: "reject" });
237
230
  return s;
238
231
  }
239
232
 
@@ -244,9 +237,7 @@ function _conversionUrl(s) {
244
237
  if (s.length > MAX_URL_LEN) {
245
238
  throw new TypeError("pixelEvents: conversion_url must be <= " + MAX_URL_LEN + " characters");
246
239
  }
247
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
248
- throw new TypeError("pixelEvents: conversion_url contains control / zero-width bytes");
249
- }
240
+ textGuard.freeText(s, "pixelEvents: conversion_url", { zeroWidth: "reject" });
250
241
  // Every supported provider's conversion API is public HTTPS — refuse
251
242
  // anything else so an operator typo can't egress conversion data in
252
243
  // plaintext.
@@ -276,9 +267,7 @@ function _eventSourceUrl(s) {
276
267
  if (s.length > MAX_URL_LEN) {
277
268
  throw new TypeError("pixelEvents: event_source_url must be <= " + MAX_URL_LEN + " characters");
278
269
  }
279
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
280
- throw new TypeError("pixelEvents: event_source_url contains control / zero-width bytes");
281
- }
270
+ textGuard.freeText(s, "pixelEvents: event_source_url", { zeroWidth: "reject" });
282
271
  if (!/^https?:\/\//i.test(s)) {
283
272
  throw new TypeError("pixelEvents: event_source_url must be an absolute http(s):// URL");
284
273
  }
@@ -293,9 +282,7 @@ function _email(s) {
293
282
  if (s.length > MAX_EMAIL_LEN) {
294
283
  throw new TypeError("pixelEvents: customer_email must be <= " + MAX_EMAIL_LEN + " characters");
295
284
  }
296
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
297
- throw new TypeError("pixelEvents: customer_email contains control / zero-width bytes");
298
- }
285
+ textGuard.freeText(s, "pixelEvents: customer_email", { zeroWidth: "reject" });
299
286
  // Validate the trimmed form — normalisation strips leading /
300
287
  // trailing whitespace before hashing, so a " alice@example.com "
301
288
  // input still hashes to the canonical address. Reject only when
@@ -402,7 +389,7 @@ function _reason(s, label) {
402
389
  if (s.length > MAX_REASON_LEN) {
403
390
  throw new TypeError("pixelEvents: " + label + " must be <= " + MAX_REASON_LEN + " characters");
404
391
  }
405
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
392
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
406
393
  throw new TypeError("pixelEvents: " + label + " contains control / zero-width bytes");
407
394
  }
408
395
  return s;
@@ -420,9 +407,7 @@ function _responseBody(s) {
420
407
  // already (the dispatch hook captured them); allow the full ASCII
421
408
  // range minus the zero-width / bidi block so a malicious provider
422
409
  // can't smuggle invisible bytes through the audit trail.
423
- if (ZERO_WIDTH_RE.test(s)) {
424
- throw new TypeError("pixelEvents: response_body contains zero-width / bidi-override bytes");
425
- }
410
+ textGuard.freeText(s, "pixelEvents: response_body", { zeroWidth: "reject" });
426
411
  return s;
427
412
  }
428
413
 
@@ -61,6 +61,7 @@
61
61
  */
62
62
 
63
63
  var b = require("./vendor/blamejs");
64
+ var textGuard = require("./text-guard");
64
65
 
65
66
  // Observability sink for batch-path settlement failures: the scheduler
66
67
  // reverts a due row to 'pending' and carries on when a Stripe push or a
@@ -79,10 +80,6 @@ var MAX_REASON_LEN = 280;
79
80
  // can land in cancel_reason and replay into the customer profile
80
81
  // screen, so the same "no direction-override / no invisible glyph"
81
82
  // floor applies.
82
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
83
- var ZERO_WIDTH_RE = new RegExp(
84
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
85
- );
86
83
 
87
84
  // ---- validators ---------------------------------------------------------
88
85
 
@@ -118,9 +115,7 @@ function _optReason(s) {
118
115
  if (s.length > MAX_REASON_LEN) {
119
116
  throw new TypeError("planChanges: reason must be <= " + MAX_REASON_LEN + " characters");
120
117
  }
121
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
122
- throw new TypeError("planChanges: reason contains control / zero-width bytes");
123
- }
118
+ textGuard.freeText(s, "planChanges: reason", { zeroWidth: "reject" });
124
119
  return s;
125
120
  }
126
121
 
package/lib/product-qa.js CHANGED
@@ -81,9 +81,9 @@ var MIN_CLEANUP_DAYS = 1;
81
81
  var STATUSES = Object.freeze(["pending", "approved", "rejected"]);
82
82
  var AUTHORS = Object.freeze(["operator", "customer", "system"]);
83
83
 
84
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
85
84
 
86
85
  var b = require("./vendor/blamejs");
86
+ var textGuard = require("./text-guard");
87
87
  var C = b.constants;
88
88
 
89
89
  // ---- monotonic clock ---------------------------------------------------
@@ -116,7 +116,7 @@ function _body(s, label) {
116
116
  if (s.length > MAX_BODY_LEN) {
117
117
  throw new TypeError("productQA: " + label + " must be <= " + MAX_BODY_LEN + " characters");
118
118
  }
119
- if (CONTROL_BYTE_RE.test(s)) {
119
+ if (textGuard.hasCodepointThreat(s)) {
120
120
  throw new TypeError("productQA: " + label + " must not contain control bytes");
121
121
  }
122
122
  return s;
@@ -131,9 +131,7 @@ function _reason(s) {
131
131
  if (s.length > MAX_REASON_LEN) {
132
132
  throw new TypeError("productQA: reason must be <= " + MAX_REASON_LEN + " characters");
133
133
  }
134
- if (CONTROL_BYTE_RE.test(s)) {
135
- throw new TypeError("productQA: reason must not contain control bytes");
136
- }
134
+ textGuard.freeText(s, "productQA: reason");
137
135
  return s;
138
136
  }
139
137
 
@@ -167,9 +165,7 @@ function _sessionId(s) {
167
165
  if (s.length > MAX_SESSION_ID_LEN) {
168
166
  throw new TypeError("productQA: session_id must be <= " + MAX_SESSION_ID_LEN + " characters");
169
167
  }
170
- if (CONTROL_BYTE_RE.test(s)) {
171
- throw new TypeError("productQA: session_id must not contain control bytes");
172
- }
168
+ textGuard.freeText(s, "productQA: session_id");
173
169
  return s;
174
170
  }
175
171
 
@@ -102,15 +102,10 @@ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
102
102
  // operator dashboards. Newlines are allowed in `body` (multi-line
103
103
  // marketing copy is common); `headline` / `cta_label` refuse LF / CR
104
104
  // so a single-line presentation slot can't be smuggled into.
105
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
106
- var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
107
105
 
108
106
  // Zero-width / direction-override family — mirrors the gift-options
109
107
  // + order-notes catalogues. Spelled with \u-escapes so ESLint's
110
108
  // no-irregular-whitespace stays happy.
111
- var ZERO_WIDTH_RE = new RegExp(
112
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
113
- );
114
109
 
115
110
  var ALLOWED_PATCH_COLUMNS = Object.freeze([
116
111
  "placement",
@@ -128,6 +123,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
128
123
  ]);
129
124
 
130
125
  var b = require("./vendor/blamejs");
126
+ var textGuard = require("./text-guard");
131
127
 
132
128
  // ---- validators ---------------------------------------------------------
133
129
 
@@ -164,10 +160,10 @@ function _line(s, label, maxLen) {
164
160
  if (typeof s !== "string" || !s.length || s.length > maxLen) {
165
161
  throw new TypeError("promoBanners: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
166
162
  }
167
- if (CONTROL_BYTE_LINE_RE.test(s)) {
163
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
168
164
  throw new TypeError("promoBanners: " + label + " contains control bytes (incl. CR/LF)");
169
165
  }
170
- if (ZERO_WIDTH_RE.test(s)) {
166
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
171
167
  throw new TypeError("promoBanners: " + label + " contains zero-width / direction-override characters");
172
168
  }
173
169
  return s;
@@ -181,10 +177,10 @@ function _block(s, label, maxLen) {
181
177
  if (s.length > maxLen) {
182
178
  throw new TypeError("promoBanners: " + label + " must be ≤ " + maxLen + " chars");
183
179
  }
184
- if (CONTROL_BYTE_BLOCK_RE.test(s)) {
180
+ if (textGuard.hasCodepointThreat(s)) {
185
181
  throw new TypeError("promoBanners: " + label + " contains control bytes");
186
182
  }
187
- if (ZERO_WIDTH_RE.test(s)) {
183
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
188
184
  throw new TypeError("promoBanners: " + label + " contains zero-width / direction-override characters");
189
185
  }
190
186
  return s;
@@ -200,9 +196,7 @@ function _ctaUrl(s) {
200
196
  if (typeof s !== "string" || !s.length || s.length > MAX_CTA_URL_LEN) {
201
197
  throw new TypeError("promoBanners: cta_url must be a non-empty string ≤ " + MAX_CTA_URL_LEN + " chars");
202
198
  }
203
- if (CONTROL_BYTE_LINE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
204
- throw new TypeError("promoBanners: cta_url contains control / zero-width bytes");
205
- }
199
+ textGuard.freeText(s, "promoBanners: cta_url", { singleLine: "reject", zeroWidth: "reject" });
206
200
  if (s.charCodeAt(0) === 47 /* "/" */) {
207
201
  // /-rooted absolute path — refuse protocol-relative `//host`
208
202
  // (the second char being "/" turns the link into an off-site
@@ -230,9 +224,7 @@ function _imageUrl(s) {
230
224
  if (typeof s !== "string" || !s.length || s.length > MAX_IMAGE_URL_LEN) {
231
225
  throw new TypeError("promoBanners: image_url must be a non-empty string ≤ " + MAX_IMAGE_URL_LEN + " chars");
232
226
  }
233
- if (CONTROL_BYTE_LINE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
234
- throw new TypeError("promoBanners: image_url contains control / zero-width bytes");
235
- }
227
+ textGuard.freeText(s, "promoBanners: image_url", { singleLine: "reject", zeroWidth: "reject" });
236
228
  if (s.charCodeAt(0) === 47 /* "/" */) {
237
229
  if (s.length > 1 && s.charCodeAt(1) === 47) {
238
230
  throw new TypeError("promoBanners: image_url protocol-relative `//host/...` refused — use absolute https://");
@@ -96,10 +96,6 @@ var REFERENCE_RE = /^[A-Za-z0-9][A-Za-z0-9._\/ -]{0,127}$/;
96
96
  // as vendors. Operator-rendered text fields refuse these to keep the
97
97
  // downstream dashboard / printout safe from header-injection +
98
98
  // visual-spoofing attacks.
99
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
100
- var ZERO_WIDTH_RE = new RegExp(
101
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
102
- );
103
99
 
104
100
  var PO_STATUSES = Object.freeze([
105
101
  "draft", "submitted", "confirmed", "partially_received",
@@ -117,6 +113,7 @@ var ALLOWED_UPDATE_COLUMNS = Object.freeze([
117
113
 
118
114
  // Framework handle (the vendored blamejs); index.js re-exports this as .framework.
119
115
  var b = require("./vendor/blamejs");
116
+ var textGuard = require("./text-guard");
120
117
 
121
118
  // ---- validators ---------------------------------------------------------
122
119
 
@@ -212,10 +209,10 @@ function _shortText(s, label, max) {
212
209
  if (s.length > max) {
213
210
  throw new TypeError("purchase-orders: " + label + " must be <= " + max + " characters");
214
211
  }
215
- if (CONTROL_BYTE_RE.test(s)) {
212
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
216
213
  throw new TypeError("purchase-orders: " + label + " contains control bytes");
217
214
  }
218
- if (ZERO_WIDTH_RE.test(s)) {
215
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
219
216
  throw new TypeError("purchase-orders: " + label + " contains zero-width / direction-override bytes");
220
217
  }
221
218
  return s;
@@ -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" });
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" });
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;