@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
@@ -108,11 +108,6 @@ var LOCALE_RE = /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/;
108
108
  var CATEGORY_RE = /^[a-z][a-z0-9_-]*$/;
109
109
  var TAG_RE = /^[a-z][a-z0-9_-]*$/;
110
110
 
111
- var CONTROL_BYTE_BODY_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
112
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
113
- var ZERO_WIDTH_RE = new RegExp(
114
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
115
- );
116
111
 
117
112
  var SEARCH_WEIGHTS = Object.freeze({
118
113
  title: 3,
@@ -121,6 +116,7 @@ var SEARCH_WEIGHTS = Object.freeze({
121
116
  });
122
117
 
123
118
  var b = require("./vendor/blamejs");
119
+ var textGuard = require("./text-guard");
124
120
 
125
121
  // ---- monotonic clock ---------------------------------------------------
126
122
  //
@@ -163,12 +159,7 @@ function _title(s) {
163
159
  if (s.length > MAX_TITLE_LEN) {
164
160
  throw new TypeError("knowledgeBase: title must be <= " + MAX_TITLE_LEN + " characters");
165
161
  }
166
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
167
- throw new TypeError("knowledgeBase: title contains control bytes");
168
- }
169
- if (ZERO_WIDTH_RE.test(s)) {
170
- throw new TypeError("knowledgeBase: title contains zero-width / direction-override characters");
171
- }
162
+ textGuard.freeText(s, "knowledgeBase: title", { singleLine: "reject", zeroWidth: "reject" });
172
163
  return s;
173
164
  }
174
165
 
@@ -183,12 +174,10 @@ function _body(s) {
183
174
  if (s.length > MAX_BODY_LEN) {
184
175
  throw new TypeError("knowledgeBase: body must be <= " + MAX_BODY_LEN + " characters");
185
176
  }
186
- if (CONTROL_BYTE_BODY_RE.test(s)) {
177
+ if (textGuard.hasCodepointThreat(s)) {
187
178
  throw new TypeError("knowledgeBase: body contains control bytes");
188
179
  }
189
- if (ZERO_WIDTH_RE.test(s)) {
190
- throw new TypeError("knowledgeBase: body contains zero-width / direction-override characters");
191
- }
180
+ textGuard.freeText(s, "knowledgeBase: body", { zeroWidth: "reject" });
192
181
  return s;
193
182
  }
194
183
 
@@ -282,9 +271,7 @@ function _queryStr(s) {
282
271
  if (s.length > MAX_QUERY_LEN) {
283
272
  throw new TypeError("knowledgeBase: query must be <= " + MAX_QUERY_LEN + " characters");
284
273
  }
285
- if (CONTROL_BYTE_BODY_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
286
- throw new TypeError("knowledgeBase: query contains control / zero-width bytes");
287
- }
274
+ textGuard.freeText(s, "knowledgeBase: query", { zeroWidth: "reject" });
288
275
  return s;
289
276
  }
290
277
 
@@ -303,9 +290,7 @@ function _sessionIdRaw(s) {
303
290
  if (s.length > 256) {
304
291
  throw new TypeError("knowledgeBase: session_id must be <= 256 characters");
305
292
  }
306
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
307
- throw new TypeError("knowledgeBase: session_id contains control / zero-width bytes");
308
- }
293
+ textGuard.freeText(s, "knowledgeBase: session_id", { singleLine: "reject", zeroWidth: "reject" });
309
294
  return s;
310
295
  }
311
296
 
@@ -347,7 +332,7 @@ function _esc(s) {
347
332
 
348
333
  function _safeLinkUrl(url) {
349
334
  if (typeof url !== "string" || !url.length || url.length > 2048) return null;
350
- if (CONTROL_BYTE_BODY_RE.test(url) || ZERO_WIDTH_RE.test(url)) return null;
335
+ if (textGuard.hasCodepointThreat(url, { zeroWidth: "reject" })) return null;
351
336
  if (url.charCodeAt(0) === 47 /* "/" */) {
352
337
  if (url.length > 1 && url.charCodeAt(1) === 47) return null;
353
338
  if (url.indexOf("..") !== -1) return null;
@@ -69,6 +69,7 @@
69
69
  // ---- constants ----------------------------------------------------------
70
70
 
71
71
  var b = require("./vendor/blamejs");
72
+ var textGuard = require("./text-guard");
72
73
 
73
74
  var C = b.constants;
74
75
 
@@ -89,8 +90,6 @@ var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
89
90
  // adjacent ecosystems. Newlines are allowed in gift_message (people
90
91
  // write multi-line messages); the recipient name is a single line
91
92
  // and refuses LF / CR too.
92
- var CONTROL_BYTE_MSG_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
93
- var CONTROL_BYTE_NAME_RE = /[\x00-\x1f\x7f]/;
94
93
 
95
94
  // Zero-width / direction-override family — mirrors the gift-options
96
95
  // primitive's catalogue: ZWSP/ZWNJ/ZWJ (U+200B-200D), LRM/RLM
@@ -99,9 +98,6 @@ var CONTROL_BYTE_NAME_RE = /[\x00-\x1f\x7f]/;
99
98
  // (U+2066-U+2069), the BOM (U+FEFF), and the Arabic letter mark
100
99
  // (U+061C). Spelled with \u-escapes so ESLint's
101
100
  // no-irregular-whitespace stays happy.
102
- var ZERO_WIDTH_RE = new RegExp(
103
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
104
- );
105
101
 
106
102
  var BCP47_RE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
107
103
 
@@ -155,12 +151,7 @@ function _giftMessage(s) {
155
151
  if (s.length > MAX_MESSAGE_LEN) {
156
152
  throw new TypeError("lineGiftWrap: gift_message must be ≤ " + MAX_MESSAGE_LEN + " chars");
157
153
  }
158
- if (CONTROL_BYTE_MSG_RE.test(s)) {
159
- throw new TypeError("lineGiftWrap: gift_message contains control bytes");
160
- }
161
- if (ZERO_WIDTH_RE.test(s)) {
162
- throw new TypeError("lineGiftWrap: gift_message contains zero-width / direction-override characters");
163
- }
154
+ textGuard.freeText(s, "lineGiftWrap: gift_message", { zeroWidth: "reject" });
164
155
  return s;
165
156
  }
166
157
 
@@ -172,12 +163,7 @@ function _recipientName(s) {
172
163
  if (s.length > MAX_RECIPIENT_LEN) {
173
164
  throw new TypeError("lineGiftWrap: recipient_name must be ≤ " + MAX_RECIPIENT_LEN + " chars");
174
165
  }
175
- if (CONTROL_BYTE_NAME_RE.test(s)) {
176
- throw new TypeError("lineGiftWrap: recipient_name contains control bytes (incl. CR/LF)");
177
- }
178
- if (ZERO_WIDTH_RE.test(s)) {
179
- throw new TypeError("lineGiftWrap: recipient_name contains zero-width / direction-override characters");
180
- }
166
+ textGuard.freeText(s, "lineGiftWrap: recipient_name", { singleLine: "reject", zeroWidth: "reject" });
181
167
  return s;
182
168
  }
183
169
 
package/lib/live-chat.js CHANGED
@@ -98,15 +98,11 @@ var TRANSITIONS = {
98
98
  abandoned: {},
99
99
  };
100
100
 
101
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
102
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
103
101
  // Zero-width + direction-override characters. Spelled via \u escapes
104
102
  // so the file stays ESLint no-irregular-whitespace clean.
105
- var ZERO_WIDTH_RE = new RegExp(
106
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
107
- );
108
103
 
109
104
  var b = require("./vendor/blamejs");
105
+ var textGuard = require("./text-guard");
110
106
  var C = b.constants;
111
107
 
112
108
  // ---- validators ---------------------------------------------------------
@@ -134,12 +130,7 @@ function _body(s) {
134
130
  if (s.length > MAX_BODY_LEN) {
135
131
  throw new TypeError("live-chat: body must be <= " + MAX_BODY_LEN + " characters");
136
132
  }
137
- if (CONTROL_BYTE_RE.test(s)) {
138
- throw new TypeError("live-chat: body contains control bytes");
139
- }
140
- if (ZERO_WIDTH_RE.test(s)) {
141
- throw new TypeError("live-chat: body contains zero-width / direction-override characters");
142
- }
133
+ textGuard.freeText(s, "live-chat: body", { zeroWidth: "reject" });
143
134
  return s;
144
135
  }
145
136
 
@@ -152,7 +143,7 @@ function _reason(r, label) {
152
143
  if (r.length > MAX_REASON_LEN) {
153
144
  throw new TypeError("live-chat: " + (label || "reason") + " must be <= " + MAX_REASON_LEN + " characters");
154
145
  }
155
- if (CONTROL_BYTE_STRICT_RE.test(r) || ZERO_WIDTH_RE.test(r)) {
146
+ if (textGuard.hasCodepointThreat(r, { singleLine: "reject", zeroWidth: "reject" })) {
156
147
  throw new TypeError("live-chat: " + (label || "reason") + " contains control / zero-width bytes");
157
148
  }
158
149
  return r;
@@ -165,9 +156,7 @@ function _sourcePage(s) {
165
156
  if (s.length > MAX_SOURCE_PAGE) {
166
157
  throw new TypeError("live-chat: source_page must be <= " + MAX_SOURCE_PAGE + " characters");
167
158
  }
168
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
169
- throw new TypeError("live-chat: source_page contains control / zero-width bytes");
170
- }
159
+ textGuard.freeText(s, "live-chat: source_page", { singleLine: "reject", zeroWidth: "reject" });
171
160
  return s;
172
161
  }
173
162
 
@@ -178,9 +167,7 @@ function _visitorSessionId(s) {
178
167
  if (s.length > MAX_SESSION_ID_LEN) {
179
168
  throw new TypeError("live-chat: session_id must be <= " + MAX_SESSION_ID_LEN + " characters");
180
169
  }
181
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
182
- throw new TypeError("live-chat: session_id contains control / zero-width bytes");
183
- }
170
+ textGuard.freeText(s, "live-chat: session_id", { singleLine: "reject", zeroWidth: "reject" });
184
171
  return s;
185
172
  }
186
173
 
@@ -90,6 +90,7 @@
90
90
  */
91
91
 
92
92
  var b = require("./vendor/blamejs");
93
+ var textGuard = require("./text-guard");
93
94
 
94
95
  // ---- constants ----------------------------------------------------------
95
96
 
@@ -111,10 +112,6 @@ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
111
112
  // primitives — operator-authored copy (title, unit) ends up on
112
113
  // receipt emails + the operator dashboard, so the same "no direction-
113
114
  // override / no invisible glyph" floor applies.
114
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
115
- var ZERO_WIDTH_RE = new RegExp(
116
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
117
- );
118
115
 
119
116
  var ALLOWED_PATCH_COLUMNS = Object.freeze([
120
117
  "title",
@@ -146,7 +143,7 @@ function _shortText(s, label, max) {
146
143
  if (s.length > max) {
147
144
  throw new TypeError("meteredUsage: " + label + " must be <= " + max + " characters");
148
145
  }
149
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
146
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
150
147
  throw new TypeError("meteredUsage: " + label + " contains control / zero-width bytes");
151
148
  }
152
149
  return s;
@@ -253,9 +250,7 @@ function _idempotencyKey(s) {
253
250
  if (s.length > MAX_IDEM_KEY_LEN) {
254
251
  throw new TypeError("meteredUsage: idempotency_key must be <= " + MAX_IDEM_KEY_LEN + " characters");
255
252
  }
256
- if (CONTROL_BYTE_RE.test(s)) {
257
- throw new TypeError("meteredUsage: idempotency_key contains control bytes");
258
- }
253
+ textGuard.freeText(s, "meteredUsage: idempotency_key");
259
254
  return s;
260
255
  }
261
256
 
@@ -39,6 +39,7 @@
39
39
  */
40
40
 
41
41
  var b = require("./vendor/blamejs");
42
+ var textGuard = require("./text-guard");
42
43
 
43
44
  var RECIPIENT_NAMESPACE = "notification-recipient";
44
45
 
@@ -54,7 +55,6 @@ var MAX_PAYLOAD_BYTES = 65536;
54
55
  var MAX_ERROR_LEN = 1024;
55
56
 
56
57
  var EVENT_TYPE_RE = /^[a-z](?:[a-z0-9._-]*[a-z0-9])?$/;
57
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
58
58
  var DEFAULT_PAGE_SIZE = 50;
59
59
  var MAX_PAGE_SIZE = 500;
60
60
 
@@ -67,9 +67,7 @@ function _recipientId(s) {
67
67
  if (s.length > MAX_RECIPIENT_LEN) {
68
68
  throw new TypeError("notifications: recipient_id must be <= " + MAX_RECIPIENT_LEN + " characters");
69
69
  }
70
- if (CONTROL_BYTE_RE.test(s)) {
71
- throw new TypeError("notifications: recipient_id contains control bytes");
72
- }
70
+ textGuard.freeText(s, "notifications: recipient_id");
73
71
  return s;
74
72
  }
75
73
 
@@ -101,9 +99,7 @@ function _title(s) {
101
99
  if (s.length > MAX_TITLE_LEN) {
102
100
  throw new TypeError("notifications: title must be <= " + MAX_TITLE_LEN + " characters");
103
101
  }
104
- if (CONTROL_BYTE_RE.test(s)) {
105
- throw new TypeError("notifications: title contains control bytes");
106
- }
102
+ textGuard.freeText(s, "notifications: title");
107
103
  return s;
108
104
  }
109
105
 
@@ -156,9 +156,9 @@ var MAX_LIST_LIMIT = 200;
156
156
  var ROLES = Object.freeze(["owner", "manager", "viewer"]);
157
157
  var STATUS = Object.freeze(["active", "disabled"]);
158
158
 
159
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
160
159
 
161
160
  var b = require("./vendor/blamejs");
161
+ var textGuard = require("./text-guard");
162
162
 
163
163
  // ---- validators ---------------------------------------------------------
164
164
 
@@ -174,7 +174,7 @@ function _actorId(s, label) {
174
174
  if (typeof s !== "string" || !s.length || s.length > 128) {
175
175
  throw new TypeError("operatorAccounts: " + (label || "actor_id") + " must be a non-empty string (<= 128 chars)");
176
176
  }
177
- if (CONTROL_BYTE_RE.test(s)) {
177
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
178
178
  throw new TypeError("operatorAccounts: " + (label || "actor_id") + " must not contain control bytes");
179
179
  }
180
180
  return s;
@@ -212,9 +212,7 @@ function _displayName(s) {
212
212
  if (typeof s !== "string" || !s.length || s.length > MAX_DISPLAY_NAME_LEN) {
213
213
  throw new TypeError("operatorAccounts: display_name must be a non-empty string (<= " + MAX_DISPLAY_NAME_LEN + " chars)");
214
214
  }
215
- if (CONTROL_BYTE_RE.test(s)) {
216
- throw new TypeError("operatorAccounts: display_name must not contain control bytes");
217
- }
215
+ textGuard.freeText(s, "operatorAccounts: display_name", { singleLine: "reject" });
218
216
  return s;
219
217
  }
220
218
 
@@ -164,13 +164,13 @@ var MAX_HOURS = 24 * 365; // one-year cap
164
164
 
165
165
  var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
166
166
  var ACTION_KIND_RE = /^[a-z][a-z0-9_.\-]{0,79}$/;
167
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
168
167
 
169
168
  var DECISIONS = Object.freeze(["approve", "reject", "abstain"]);
170
169
  var STATUSES = Object.freeze(["pending", "approved", "rejected",
171
170
  "executed", "cancelled", "escalated"]);
172
171
 
173
172
  var b = require("./vendor/blamejs");
173
+ var textGuard = require("./text-guard");
174
174
 
175
175
  // ---- validators ---------------------------------------------------------
176
176
 
@@ -195,7 +195,7 @@ function _operatorId(s, label) {
195
195
  throw new TypeError("operatorApprovals: " + label +
196
196
  " must be a non-empty string (<= " + MAX_OPERATOR_ID_LEN + " chars)");
197
197
  }
198
- if (CONTROL_BYTE_RE.test(s)) {
198
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
199
199
  throw new TypeError("operatorApprovals: " + label + " must not contain control bytes");
200
200
  }
201
201
  return s;
@@ -206,7 +206,7 @@ function _reason(s) {
206
206
  throw new TypeError("operatorApprovals: reason must be a non-empty string <= " +
207
207
  MAX_REASON_LEN + " chars");
208
208
  }
209
- if (CONTROL_BYTE_RE.test(s.replace(/[\t\r\n]/g, ""))) {
209
+ if (textGuard.hasCodepointThreat(s)) {
210
210
  throw new TypeError("operatorApprovals: reason must not contain control bytes (except whitespace)");
211
211
  }
212
212
  return s;
@@ -217,7 +217,7 @@ function _justification(s) {
217
217
  throw new TypeError("operatorApprovals: justification must be a non-empty string <= " +
218
218
  MAX_JUSTIFICATION_LEN + " chars");
219
219
  }
220
- if (CONTROL_BYTE_RE.test(s.replace(/[\t\r\n]/g, ""))) {
220
+ if (textGuard.hasCodepointThreat(s)) {
221
221
  throw new TypeError("operatorApprovals: justification must not contain control bytes (except whitespace)");
222
222
  }
223
223
  return s;
@@ -229,7 +229,7 @@ function _comment(s) {
229
229
  throw new TypeError("operatorApprovals: comment must be a string <= " +
230
230
  MAX_COMMENT_LEN + " chars");
231
231
  }
232
- if (CONTROL_BYTE_RE.test(s.replace(/[\t\r\n]/g, ""))) {
232
+ if (textGuard.hasCodepointThreat(s)) {
233
233
  throw new TypeError("operatorApprovals: comment must not contain control bytes (except whitespace)");
234
234
  }
235
235
  return s;
@@ -317,9 +317,7 @@ function _capability(s) {
317
317
  if (typeof s !== "string" || !s.length || s.length > MAX_ACTION_KIND_LEN) {
318
318
  throw new TypeError("operatorApprovals: required_capability must be a non-empty string or null");
319
319
  }
320
- if (CONTROL_BYTE_RE.test(s)) {
321
- throw new TypeError("operatorApprovals: required_capability must not contain control bytes");
322
- }
320
+ textGuard.freeText(s, "operatorApprovals: required_capability", { singleLine: "reject" });
323
321
  return s;
324
322
  }
325
323
 
@@ -104,16 +104,13 @@ var ALLOWED_UA_CLASSES = Object.freeze([
104
104
  // discipline matches every other operator-facing primitive (catalog,
105
105
  // experiments, promo-banners) — strings reach the dashboard as inert
106
106
  // text, never as live markup.
107
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
108
- var ZERO_WIDTH_RE = new RegExp(
109
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
110
- );
111
107
 
112
108
  var IDENT_RE = /^[A-Za-z0-9][A-Za-z0-9._:\-]{0,127}$/;
113
109
  var ACTION_RE = /^[A-Za-z0-9][A-Za-z0-9._:\-]{0,127}$/;
114
110
  var KIND_RE = /^[A-Za-z0-9][A-Za-z0-9._\-]{0,63}$/;
115
111
 
116
112
  var b = require("./vendor/blamejs");
113
+ var textGuard = require("./text-guard");
117
114
 
118
115
  // ---- validators ---------------------------------------------------------
119
116
 
@@ -148,9 +145,7 @@ function _ipHash(s) {
148
145
  throw new TypeError("operatorAuditLog: ip_hash must be a non-empty string ≤ " +
149
146
  MAX_IP_HASH_LEN + " chars");
150
147
  }
151
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
152
- throw new TypeError("operatorAuditLog: ip_hash contains control / zero-width characters");
153
- }
148
+ textGuard.freeText(s, "operatorAuditLog: ip_hash", { singleLine: "reject", zeroWidth: "reject" });
154
149
  return s;
155
150
  }
156
151
 
@@ -109,11 +109,6 @@ var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
109
109
  var SECTION_RE = /^[a-z][a-z0-9_-]*$/;
110
110
  var ACTION_RE = /^[a-z0-9](?:[a-z0-9._/:-]*[a-z0-9])?$/;
111
111
 
112
- var CONTROL_BYTE_BODY_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
113
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
114
- var ZERO_WIDTH_RE = new RegExp(
115
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
116
- );
117
112
 
118
113
  var SEARCH_WEIGHTS = Object.freeze({
119
114
  title: 3,
@@ -126,6 +121,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
126
121
  ]);
127
122
 
128
123
  var b = require("./vendor/blamejs");
124
+ var textGuard = require("./text-guard");
129
125
 
130
126
  // ---- monotonic clock ---------------------------------------------------
131
127
  //
@@ -168,12 +164,7 @@ function _title(s) {
168
164
  if (s.length > MAX_TITLE_LEN) {
169
165
  throw new TypeError("operatorHelpCenter: title must be <= " + MAX_TITLE_LEN + " characters");
170
166
  }
171
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
172
- throw new TypeError("operatorHelpCenter: title contains control bytes");
173
- }
174
- if (ZERO_WIDTH_RE.test(s)) {
175
- throw new TypeError("operatorHelpCenter: title contains zero-width / direction-override characters");
176
- }
167
+ textGuard.freeText(s, "operatorHelpCenter: title", { singleLine: "reject", zeroWidth: "reject" });
177
168
  return s;
178
169
  }
179
170
 
@@ -188,12 +179,7 @@ function _body(s) {
188
179
  if (s.length > MAX_BODY_LEN) {
189
180
  throw new TypeError("operatorHelpCenter: body must be <= " + MAX_BODY_LEN + " characters");
190
181
  }
191
- if (CONTROL_BYTE_BODY_RE.test(s)) {
192
- throw new TypeError("operatorHelpCenter: body contains control bytes");
193
- }
194
- if (ZERO_WIDTH_RE.test(s)) {
195
- throw new TypeError("operatorHelpCenter: body contains zero-width / direction-override characters");
196
- }
182
+ textGuard.freeText(s, "operatorHelpCenter: body", { zeroWidth: "reject" });
197
183
  return s;
198
184
  }
199
185
 
@@ -304,9 +290,7 @@ function _queryStr(s) {
304
290
  if (s.length > MAX_QUERY_LEN) {
305
291
  throw new TypeError("operatorHelpCenter: query must be <= " + MAX_QUERY_LEN + " characters");
306
292
  }
307
- if (CONTROL_BYTE_BODY_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
308
- throw new TypeError("operatorHelpCenter: query contains control / zero-width bytes");
309
- }
293
+ textGuard.freeText(s, "operatorHelpCenter: query", { zeroWidth: "reject" });
310
294
  return s;
311
295
  }
312
296
 
@@ -346,7 +330,7 @@ function _esc(s) {
346
330
 
347
331
  function _safeLinkUrl(url) {
348
332
  if (typeof url !== "string" || !url.length || url.length > 2048) return null;
349
- if (CONTROL_BYTE_BODY_RE.test(url) || ZERO_WIDTH_RE.test(url)) return null;
333
+ if (textGuard.hasCodepointThreat(url, { zeroWidth: "reject" })) return null;
350
334
  if (url.charCodeAt(0) === 47 /* "/" */) {
351
335
  if (url.length > 1 && url.charCodeAt(1) === 47) return null;
352
336
  if (url.indexOf("..") !== -1) return null;
@@ -165,6 +165,7 @@
165
165
  */
166
166
 
167
167
  var b = require("./vendor/blamejs");
168
+ var textGuard = require("./text-guard");
168
169
 
169
170
  // ---- constants ----------------------------------------------------------
170
171
 
@@ -183,8 +184,6 @@ var MAX_BULK_IDS = 200;
183
184
  var DEFAULT_LIMIT = 50;
184
185
  var MAX_LIMIT = 500;
185
186
 
186
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
187
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
188
187
 
189
188
  var CURSOR_ORDER_KEY = Object.freeze(["created_at", "id"]);
190
189
 
@@ -249,9 +248,7 @@ function _subject(s) {
249
248
  if (typeof s !== "string" || !s.length || s.length > MAX_SUBJECT_LEN) {
250
249
  throw new TypeError("operatorInbox: subject must be a non-empty string <= " + MAX_SUBJECT_LEN + " chars");
251
250
  }
252
- if (CONTROL_BYTE_RE.test(s)) {
253
- throw new TypeError("operatorInbox: subject must not contain control bytes");
254
- }
251
+ textGuard.freeText(s, "operatorInbox: subject");
255
252
  return s;
256
253
  }
257
254
 
@@ -259,9 +256,7 @@ function _body(s) {
259
256
  if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
260
257
  throw new TypeError("operatorInbox: body must be a non-empty string <= " + MAX_BODY_LEN + " chars");
261
258
  }
262
- if (CONTROL_BYTE_RE.test(s)) {
263
- throw new TypeError("operatorInbox: body must not contain control bytes");
264
- }
259
+ textGuard.freeText(s, "operatorInbox: body");
265
260
  return s;
266
261
  }
267
262
 
@@ -284,9 +279,7 @@ function _sourceEventId(s) {
284
279
  if (typeof s !== "string" || s.length > MAX_SOURCE_LEN) {
285
280
  throw new TypeError("operatorInbox: source_event_id must be a string <= " + MAX_SOURCE_LEN + " chars");
286
281
  }
287
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
288
- throw new TypeError("operatorInbox: source_event_id must not contain control bytes");
289
- }
282
+ textGuard.freeText(s, "operatorInbox: source_event_id", { singleLine: "reject" });
290
283
  return s;
291
284
  }
292
285
 
@@ -144,7 +144,6 @@ var MAX_LIST_LIMIT = 500;
144
144
  // Slug shape matches the rest of the codebase — alnum-leading, alnum
145
145
  // + hyphen + underscore + dot, capped length.
146
146
  var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
147
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
148
147
 
149
148
  // Closed permission allow-list. Adding a new permission is a code
150
149
  // change here AND the operator re-defining the affected roles.
@@ -170,6 +169,7 @@ var PERMISSIONS = Object.freeze([
170
169
  var ALLOWED_PATCH_COLUMNS = Object.freeze(["title", "permissions", "description"]);
171
170
 
172
171
  var b = require("./vendor/blamejs");
172
+ var textGuard = require("./text-guard");
173
173
 
174
174
  // ---- validators ---------------------------------------------------------
175
175
 
@@ -185,9 +185,7 @@ function _title(s) {
185
185
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
186
186
  throw new TypeError("operatorRoles: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
187
187
  }
188
- if (CONTROL_BYTE_RE.test(s)) {
189
- throw new TypeError("operatorRoles: title must not contain control bytes");
190
- }
188
+ textGuard.freeText(s, "operatorRoles: title", { singleLine: "reject" });
191
189
  return s;
192
190
  }
193
191
 
@@ -196,7 +194,7 @@ function _description(s) {
196
194
  if (typeof s !== "string" || s.length > MAX_DESC_LEN) {
197
195
  throw new TypeError("operatorRoles: description must be a string <= " + MAX_DESC_LEN + " chars");
198
196
  }
199
- if (CONTROL_BYTE_RE.test(s.replace(/[\t\r\n]/g, ""))) {
197
+ if (textGuard.hasCodepointThreat(s)) {
200
198
  throw new TypeError("operatorRoles: description must not contain control bytes (except whitespace)");
201
199
  }
202
200
  return s;
@@ -231,7 +229,7 @@ function _operatorId(s, label) {
231
229
  if (typeof s !== "string" || !s.length || s.length > MAX_OPERATOR_ID_LEN) {
232
230
  throw new TypeError("operatorRoles: " + label + " must be a non-empty string (<= " + MAX_OPERATOR_ID_LEN + " chars)");
233
231
  }
234
- if (CONTROL_BYTE_RE.test(s)) {
232
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
235
233
  throw new TypeError("operatorRoles: " + label + " must not contain control bytes");
236
234
  }
237
235
  return s;
@@ -241,7 +239,7 @@ function _reason(s) {
241
239
  if (typeof s !== "string" || !s.length || s.length > MAX_REASON_LEN) {
242
240
  throw new TypeError("operatorRoles: reason must be a non-empty string <= " + MAX_REASON_LEN + " chars");
243
241
  }
244
- if (CONTROL_BYTE_RE.test(s.replace(/[\t\r\n]/g, ""))) {
242
+ if (textGuard.hasCodepointThreat(s)) {
245
243
  throw new TypeError("operatorRoles: reason must not contain control bytes (except whitespace)");
246
244
  }
247
245
  return s;
@@ -61,7 +61,6 @@ var COUNTRY_RE = /^[A-Z]{2}$/;
61
61
  var PAYMENT_KIND_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
62
62
  var TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._:\-]{0,63}$/;
63
63
  var TEMPLATE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
64
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
65
64
 
66
65
  var SEVERITIES = Object.freeze(["info", "warning", "critical"]);
67
66
  var SEVERITY_RANK = Object.freeze({ info: 0, warning: 1, critical: 2 });
@@ -85,6 +84,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
85
84
  ]);
86
85
 
87
86
  var b = require("./vendor/blamejs");
87
+ var textGuard = require("./text-guard");
88
88
 
89
89
  // ---- monotonic clock ----------------------------------------------------
90
90
  //
@@ -322,7 +322,7 @@ function _freeText(v, label, max) {
322
322
  if (v.length > max) {
323
323
  throw new TypeError("orderEscalation: " + label + " must be <= " + max + " characters");
324
324
  }
325
- if (CONTROL_BYTE_RE.test(v)) {
325
+ if (textGuard.hasCodepointThreat(v)) {
326
326
  throw new TypeError("orderEscalation: " + label + " must not contain control bytes");
327
327
  }
328
328
  return v;
@@ -117,6 +117,7 @@ var DEFAULT_BATCH_SIZE = 500;
117
117
  var MAX_BATCH_SIZE = 5000;
118
118
  var MAX_LIST_LIMIT = 200;
119
119
  var EMAIL_HASH_PREFIX = "order-export-email";
120
+ var CSV_LINE_ENDING = "\r\n"; // RFC 4180
120
121
  var FORMATS = Object.freeze(["csv", "ndjson"]);
121
122
  var EXPORT_STATUSES = Object.freeze(["queued", "running", "complete", "failed", "cancelled"]);
122
123
 
@@ -209,17 +210,6 @@ function _limit(n) {
209
210
 
210
211
  // ---- CSV helpers --------------------------------------------------------
211
212
 
212
- // RFC-4180 quoting: every cell wrapped in `"`, embedded `"` doubled.
213
- // We quote unconditionally — the cost is a few extra bytes per cell;
214
- // the win is that a downstream parser never has to track quote-vs-
215
- // bare-cell state for a column with mixed shapes. The injection
216
- // neutralization runs first via the shared vendored primitive.
217
- function _csvCell(value) {
218
- var s = _coerceCell(value);
219
- s = _neutralizeInjection(s);
220
- return '"' + s.replace(/"/g, '""') + '"';
221
- }
222
-
223
213
  function _coerceCell(value) {
224
214
  if (value == null) return "";
225
215
  if (typeof value === "string") return value;
@@ -239,14 +229,29 @@ function _coerceCell(value) {
239
229
  // numerics (`+15.00`); the shared primitive prefixes those too (the safe
240
230
  // OWASP posture — `-2+3+cmd|…` is a real injection that begins like an
241
231
  // amount), which is the more complete behavior this consolidation buys.
242
- function _neutralizeInjection(s) {
243
- return b.guardCsv.escapeCell(s);
244
- }
245
-
232
+ // One export row, RFC 4180.
233
+ //
234
+ // b.guardCsv.serialize does the whole line: the formula-injection prefix on
235
+ // every cell, the unconditional quoting, doubling an embedded quote, the
236
+ // delimiter and the CRLF. Only the injection step was composed before; the
237
+ // quoting rules were written here and again in customer-segments.js.
238
+ //
239
+ // Cells are quoted unconditionally. It costs a few bytes and means a
240
+ // downstream parser never has to track quote-versus-bare state on a column
241
+ // with mixed shapes.
242
+ //
243
+ // serialize emits the rows it is given WITHOUT a trailing terminator, which is
244
+ // right for a whole document and wrong for a generator yielding one row at a
245
+ // time — so the line ending is appended here. Verified byte-identical to the
246
+ // previous builder across formula-injection, embedded-quote, embedded-comma,
247
+ // newline, tab and empty cells.
246
248
  function _csvRow(cells) {
247
- var parts = [];
248
- for (var i = 0; i < cells.length; i += 1) parts.push(_csvCell(cells[i]));
249
- return parts.join(",") + "\r\n";
249
+ return b.guardCsv.serialize([cells.map(_coerceCell)], {
250
+ profile: "strict",
251
+ alwaysQuote: true,
252
+ headers: false,
253
+ lineEnding: CSV_LINE_ENDING,
254
+ }) + CSV_LINE_ENDING;
250
255
  }
251
256
 
252
257
  // ---- row projection -----------------------------------------------------
@@ -758,8 +763,7 @@ function create(opts) {
758
763
  // Exposed for the test suite so it can assert column semantics
759
764
  // without having to thread orders through a full DB round-trip.
760
765
  _projectOrder: _projectOrder,
761
- _csvCell: _csvCell,
762
- _neutralizeInjection: _neutralizeInjection,
766
+ _csvRow: _csvRow,
763
767
  // Exposed only to keep the linter quiet about the unused `order`
764
768
  // binding when an integrator wires it later — the export reads
765
769
  // directly today, but the indirection is preserved.