@blamejs/blamejs-shop 0.5.19 → 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 (194) hide show
  1. package/CHANGELOG.md +4 -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 +49 -45
  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 +78 -44
  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/payment.js +103 -58
  54. package/lib/pixel-events.js +8 -23
  55. package/lib/plan-changes.js +2 -7
  56. package/lib/product-qa.js +4 -8
  57. package/lib/promo-banners.js +7 -15
  58. package/lib/purchase-orders.js +3 -6
  59. package/lib/push-notifications.js +7 -32
  60. package/lib/pwa-manifest.js +6 -11
  61. package/lib/quotes.js +5 -9
  62. package/lib/referrals.js +10 -6
  63. package/lib/reviews.js +4 -10
  64. package/lib/robots-config.js +6 -14
  65. package/lib/sales-tax-filings.js +2 -2
  66. package/lib/search-facets.js +5 -5
  67. package/lib/search-ranking.js +4 -7
  68. package/lib/search-synonyms.js +7 -21
  69. package/lib/security-middleware.js +125 -70
  70. package/lib/seller-signup.js +10 -26
  71. package/lib/sidebar-widgets.js +6 -15
  72. package/lib/site-redirects.js +49 -137
  73. package/lib/sitemap-generator.js +3 -7
  74. package/lib/sms-dispatcher.js +7 -23
  75. package/lib/stock-receipts.js +4 -10
  76. package/lib/storefront-dashboards.js +2 -10
  77. package/lib/storefront-forms.js +9 -22
  78. package/lib/storefront-pages.js +6 -20
  79. package/lib/storefront.js +21 -23
  80. package/lib/subscription-billing.js +3 -8
  81. package/lib/subscription-controls.js +2 -7
  82. package/lib/suggestion-box.js +9 -26
  83. package/lib/support-tickets.js +6 -24
  84. package/lib/tax-remittance.js +2 -2
  85. package/lib/tenants.js +2 -7
  86. package/lib/text-guard.js +137 -35
  87. package/lib/theme-assets.js +4 -16
  88. package/lib/translations.js +3 -6
  89. package/lib/trust-badges.js +4 -7
  90. package/lib/vendor/MANIFEST.json +107 -97
  91. package/lib/vendor/blamejs/CHANGELOG.md +180 -0
  92. package/lib/vendor/blamejs/NOTICE +14 -10
  93. package/lib/vendor/blamejs/README.md +17 -5
  94. package/lib/vendor/blamejs/index.js +2 -0
  95. package/lib/vendor/blamejs/lib/acme.js +9 -9
  96. package/lib/vendor/blamejs/lib/api-snapshot.js +35 -16
  97. package/lib/vendor/blamejs/lib/app-shutdown.js +1 -1
  98. package/lib/vendor/blamejs/lib/archive-adapters.js +1 -1
  99. package/lib/vendor/blamejs/lib/archive.js +13 -24
  100. package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -1
  101. package/lib/vendor/blamejs/lib/auth/bot-challenge.js +3 -6
  102. package/lib/vendor/blamejs/lib/auth/ciba.js +32 -3
  103. package/lib/vendor/blamejs/lib/auth/oauth.js +65 -8
  104. package/lib/vendor/blamejs/lib/auth/password.js +1 -1
  105. package/lib/vendor/blamejs/lib/auth/saml.js +21 -3
  106. package/lib/vendor/blamejs/lib/auth-bot-challenge.js +3 -3
  107. package/lib/vendor/blamejs/lib/backup/index.js +5 -5
  108. package/lib/vendor/blamejs/lib/case-fold-classes.js +491 -0
  109. package/lib/vendor/blamejs/lib/codepoint-class.js +131 -0
  110. package/lib/vendor/blamejs/lib/constants.js +79 -0
  111. package/lib/vendor/blamejs/lib/db-declare-view.js +8 -2
  112. package/lib/vendor/blamejs/lib/error-page.js +20 -1
  113. package/lib/vendor/blamejs/lib/external-db.js +5 -1
  114. package/lib/vendor/blamejs/lib/flag-evaluation-context.js +24 -6
  115. package/lib/vendor/blamejs/lib/flag.js +28 -2
  116. package/lib/vendor/blamejs/lib/framework-schema.js +14 -3
  117. package/lib/vendor/blamejs/lib/guard-regex.js +2345 -89
  118. package/lib/vendor/blamejs/lib/http-client.js +217 -40
  119. package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
  120. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +3 -2
  121. package/lib/vendor/blamejs/lib/mail-dav.js +26 -26
  122. package/lib/vendor/blamejs/lib/mail-deploy.js +10 -10
  123. package/lib/vendor/blamejs/lib/mail-scan.js +3 -0
  124. package/lib/vendor/blamejs/lib/mail-store.js +7 -1
  125. package/lib/vendor/blamejs/lib/mail.js +5 -1
  126. package/lib/vendor/blamejs/lib/middleware/age-gate.js +3 -2
  127. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +1 -1
  128. package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -1
  129. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -2
  130. package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -1
  131. package/lib/vendor/blamejs/lib/middleware/compression.js +1 -1
  132. package/lib/vendor/blamejs/lib/middleware/cors.js +1 -1
  133. package/lib/vendor/blamejs/lib/middleware/csp-report.js +5 -5
  134. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +2 -1
  135. package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +2 -2
  136. package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -1
  137. package/lib/vendor/blamejs/lib/middleware/dpop.js +2 -2
  138. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +2 -1
  139. package/lib/vendor/blamejs/lib/middleware/flag-context.js +17 -0
  140. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +10 -8
  141. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -2
  142. package/lib/vendor/blamejs/lib/middleware/require-aal.js +3 -2
  143. package/lib/vendor/blamejs/lib/middleware/require-auth.js +4 -3
  144. package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +4 -3
  145. package/lib/vendor/blamejs/lib/middleware/require-content-type.js +3 -2
  146. package/lib/vendor/blamejs/lib/middleware/require-methods.js +3 -2
  147. package/lib/vendor/blamejs/lib/middleware/require-mtls.js +3 -2
  148. package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -1
  149. package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -1
  150. package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
  151. package/lib/vendor/blamejs/lib/network-dns.js +3 -3
  152. package/lib/vendor/blamejs/lib/network-nts.js +35 -14
  153. package/lib/vendor/blamejs/lib/network-proxy.js +42 -4
  154. package/lib/vendor/blamejs/lib/network-smtp-policy.js +1 -1
  155. package/lib/vendor/blamejs/lib/network-tls.js +341 -11
  156. package/lib/vendor/blamejs/lib/nist-crosswalk.js +2 -2
  157. package/lib/vendor/blamejs/lib/ntp-check.js +30 -19
  158. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -1
  159. package/lib/vendor/blamejs/lib/object-store/gcs.js +1 -1
  160. package/lib/vendor/blamejs/lib/object-store/http-put.js +2 -1
  161. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -5
  162. package/lib/vendor/blamejs/lib/object-store/sigv4.js +2 -2
  163. package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +4 -2
  164. package/lib/vendor/blamejs/lib/openapi.js +1 -1
  165. package/lib/vendor/blamejs/lib/pqc-agent.js +4 -7
  166. package/lib/vendor/blamejs/lib/problem-details.js +2 -1
  167. package/lib/vendor/blamejs/lib/redis-client.js +12 -1
  168. package/lib/vendor/blamejs/lib/regex-linear.js +1349 -0
  169. package/lib/vendor/blamejs/lib/render.js +531 -7
  170. package/lib/vendor/blamejs/lib/request-helpers.js +181 -5
  171. package/lib/vendor/blamejs/lib/router.js +37 -13
  172. package/lib/vendor/blamejs/lib/safe-async.js +142 -0
  173. package/lib/vendor/blamejs/lib/safe-icap.js +3 -0
  174. package/lib/vendor/blamejs/lib/safe-sql.js +15 -3
  175. package/lib/vendor/blamejs/lib/self-update.js +2 -2
  176. package/lib/vendor/blamejs/lib/testing.js +54 -7
  177. package/lib/vendor/blamejs/lib/validate-opts.js +56 -0
  178. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +62 -25
  179. package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +2004 -142
  180. package/lib/vendor/blamejs/lib/vendor/browser/noble-ciphers.mjs +727 -0
  181. package/lib/vendor/blamejs/lib/vendor/browser/noble-hashes.mjs +1070 -0
  182. package/lib/vendor/blamejs/lib/vendor/browser/noble-post-quantum.mjs +1090 -0
  183. package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +510 -227
  184. package/lib/vendor/blamejs/lib/webhook-hmac.js +19 -2
  185. package/lib/vendor/blamejs/lib/webhook.js +1 -1
  186. package/lib/vendor/blamejs/lib/ws-client.js +24 -2
  187. package/lib/vendor/blamejs/package.json +1 -1
  188. package/lib/vendor/blamejs/sbom.cdx.json +6 -6
  189. package/lib/vendor-invoices.js +3 -6
  190. package/lib/vendors.js +5 -19
  191. package/lib/webhook-receiver.js +46 -83
  192. package/lib/webhook-subscriptions.js +3 -13
  193. package/lib/wishlist-sharing.js +5 -10
  194. package/package.json +1 -1
@@ -119,7 +119,6 @@ var MAX_LIST_LIMIT = 200;
119
119
  // hyphen + underscore + dot, capped length.
120
120
  var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
121
121
 
122
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
123
122
 
124
123
  // Closed capability enum. Adding a new capability is a code change
125
124
  // here + the operator re-defines the affected roles.
@@ -137,6 +136,7 @@ var CAPABILITIES = Object.freeze([
137
136
  var ALLOWED_PATCH_COLUMNS = Object.freeze(["title", "capabilities"]);
138
137
 
139
138
  var b = require("./vendor/blamejs");
139
+ var textGuard = require("./text-guard");
140
140
 
141
141
  // ---- validators ---------------------------------------------------------
142
142
 
@@ -152,9 +152,7 @@ function _title(s) {
152
152
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
153
153
  throw new TypeError("customerRoles: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
154
154
  }
155
- if (CONTROL_BYTE_RE.test(s)) {
156
- throw new TypeError("customerRoles: title must not contain control bytes");
157
- }
155
+ textGuard.freeText(s, "customerRoles: title", { singleLine: "reject" });
158
156
  return s;
159
157
  }
160
158
 
@@ -192,7 +190,7 @@ function _opaqueId(s, label) {
192
190
  if (typeof s !== "string" || !s.length || s.length > 200) {
193
191
  throw new TypeError("customerRoles: " + label + " must be a non-empty string (<= 200 chars)");
194
192
  }
195
- if (CONTROL_BYTE_RE.test(s)) {
193
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
196
194
  throw new TypeError("customerRoles: " + label + " must not contain control bytes");
197
195
  }
198
196
  return s;
@@ -113,6 +113,7 @@ var C = b.constants;
113
113
 
114
114
  var DEFAULT_LIMIT = 100;
115
115
  var MAX_LIMIT = 1000;
116
+ var CSV_LINE_ENDING = "\r\n"; // RFC 4180
116
117
  var EXPORT_BATCH_SIZE = 500;
117
118
 
118
119
  // CSV column shape for a segment-members export. NO email column —
@@ -347,19 +348,22 @@ function _coerceCell(value) {
347
348
  return JSON.stringify(value);
348
349
  }
349
350
 
350
- function _neutralizeInjection(s) {
351
- return b.guardCsv.escapeCell(s);
352
- }
353
-
354
- function _csvCell(value) {
355
- var s = _neutralizeInjection(_coerceCell(value));
356
- return '"' + s.replace(/"/g, '""') + '"';
357
- }
358
-
351
+ // One member-export row, RFC 4180 — b.guardCsv.serialize writes the whole
352
+ // line: the formula-injection prefix per cell, the unconditional quoting,
353
+ // doubling an embedded quote, the delimiter and the CRLF. Only the injection
354
+ // step was composed before; the quoting rules were written here and again in
355
+ // order-export.js.
356
+ //
357
+ // serialize omits the trailing terminator, which suits a whole document and
358
+ // not a generator yielding a row at a time, so the line ending is appended
359
+ // here.
359
360
  function _csvRow(cells) {
360
- var parts = [];
361
- for (var i = 0; i < cells.length; i += 1) parts.push(_csvCell(cells[i]));
362
- return parts.join(",") + "\r\n";
361
+ return b.guardCsv.serialize([cells.map(_coerceCell)], {
362
+ profile: "strict",
363
+ alwaysQuote: true,
364
+ headers: false,
365
+ lineEnding: CSV_LINE_ENDING,
366
+ }) + CSV_LINE_ENDING;
363
367
  }
364
368
 
365
369
  // ---- factory ------------------------------------------------------------
@@ -975,6 +979,5 @@ module.exports = {
975
979
  MEMBER_EXPORT_COLUMNS: MEMBER_EXPORT_COLUMNS,
976
980
  // Exposed for the test suite to assert CSV cell semantics without a
977
981
  // full DB round-trip.
978
- _csvCell: _csvCell,
979
- _neutralizeInjection: _neutralizeInjection,
982
+ _csvRow: _csvRow,
980
983
  };
@@ -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 ----------------------------------------------------------
@@ -104,8 +105,6 @@ var MAX_EXPIRES_HOURS = 24 * 365; // 1 year
104
105
  var DEFAULT_LIMIT = 50;
105
106
  var MAX_LIMIT = 500;
106
107
 
107
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
108
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
109
108
 
110
109
  // NPS / CSAT / CES enforce a primary-question shape so the rollup
111
110
  // math has something well-defined to bucket against. Custom surveys
@@ -143,9 +142,7 @@ function _title(s) {
143
142
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
144
143
  throw new TypeError("customerSurveys: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
145
144
  }
146
- if (CONTROL_BYTE_RE.test(s)) {
147
- throw new TypeError("customerSurveys: title must not contain control bytes");
148
- }
145
+ textGuard.freeText(s, "customerSurveys: title");
149
146
  return s;
150
147
  }
151
148
 
@@ -173,9 +170,7 @@ function _sourceEventId(s) {
173
170
  if (typeof s !== "string" || s.length > MAX_SOURCE_EVENT_LEN) {
174
171
  throw new TypeError("customerSurveys: source_event_id must be a string <= " + MAX_SOURCE_EVENT_LEN + " chars");
175
172
  }
176
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
177
- throw new TypeError("customerSurveys: source_event_id must not contain control bytes");
178
- }
173
+ textGuard.freeText(s, "customerSurveys: source_event_id", { singleLine: "reject" });
179
174
  return s;
180
175
  }
181
176
 
@@ -192,9 +187,7 @@ function _closeReason(s) {
192
187
  if (typeof s !== "string" || !s.length || s.length > MAX_CLOSE_REASON_LEN) {
193
188
  throw new TypeError("customerSurveys: reason must be a non-empty string <= " + MAX_CLOSE_REASON_LEN + " chars");
194
189
  }
195
- if (CONTROL_BYTE_RE.test(s)) {
196
- throw new TypeError("customerSurveys: reason must not contain control bytes");
197
- }
190
+ textGuard.freeText(s, "customerSurveys: reason");
198
191
  return s;
199
192
  }
200
193
 
@@ -245,7 +238,7 @@ function _questions(arr, kind) {
245
238
  if (typeof q.label !== "string" || !q.label.length || q.label.length > MAX_LABEL_LEN) {
246
239
  throw new TypeError("customerSurveys: questions[" + i + "].label must be a non-empty string <= " + MAX_LABEL_LEN + " chars");
247
240
  }
248
- if (CONTROL_BYTE_RE.test(q.label)) {
241
+ if (textGuard.hasCodepointThreat(q.label)) {
249
242
  throw new TypeError("customerSurveys: questions[" + i + "].label must not contain control bytes");
250
243
  }
251
244
  var required = q.required == null ? true : q.required;
@@ -284,7 +277,7 @@ function _questions(arr, kind) {
284
277
  throw new TypeError("customerSurveys: questions[" + i + "].options[" + k +
285
278
  "] must be a non-empty string <= " + MAX_OPTION_LEN + " chars");
286
279
  }
287
- if (CONTROL_BYTE_RE.test(opt)) {
280
+ if (textGuard.hasCodepointThreat(opt)) {
288
281
  throw new TypeError("customerSurveys: questions[" + i + "].options[" + k +
289
282
  "] must not contain control bytes");
290
283
  }
@@ -392,7 +385,7 @@ function _validateAnswers(answers, questions) {
392
385
  throw new TypeError("customerSurveys: answer for " + JSON.stringify(q.id) +
393
386
  " must be <= " + q.max + " chars");
394
387
  }
395
- if (CONTROL_BYTE_RE.test(a)) {
388
+ if (textGuard.hasCodepointThreat(a)) {
396
389
  throw new TypeError("customerSurveys: answer for " + JSON.stringify(q.id) +
397
390
  " must not contain control bytes");
398
391
  }
package/lib/customers.js CHANGED
@@ -36,6 +36,7 @@
36
36
  var nodeCrypto = require("node:crypto"); // allow:non-shop-require — Apple mandates ES256/P1363; framework has no classical ES256 signer
37
37
 
38
38
  var b = require("./vendor/blamejs");
39
+ var textGuard = require("./text-guard");
39
40
 
40
41
  var EMAIL_NAMESPACE = "customer-email";
41
42
  // Federated identity providers the OIDC sign-in path accepts.
@@ -43,7 +44,6 @@ var OAUTH_PROVIDERS = ["google", "apple"];
43
44
  var MAX_DISPLAY_NAME_LEN = 128;
44
45
  var MAX_CRED_FIELD_BYTES = 4096;
45
46
  var TRANSPORTS_RE = /^[a-z]+(?:,[a-z]+)*$/;
46
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
47
47
 
48
48
  // Pagination cursor for the operator-facing `list` — newest first by
49
49
  // (created_at, id), HMAC-tagged via b.pagination so an operator can't
@@ -105,9 +105,7 @@ function _displayName(s) {
105
105
  if (s.length > MAX_DISPLAY_NAME_LEN) {
106
106
  throw new TypeError("customers: display_name must be <= " + MAX_DISPLAY_NAME_LEN + " characters");
107
107
  }
108
- if (CONTROL_BYTE_RE.test(s)) {
109
- throw new TypeError("customers: display_name contains control bytes");
110
- }
108
+ textGuard.freeText(s, "customers: display_name", { singleLine: "reject" });
111
109
  return s;
112
110
  }
113
111
 
@@ -114,7 +114,6 @@ var PROCESSOR_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
114
114
  var REASON_CODE_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
115
115
  var CURRENCY_RE = /^[A-Z]{3}$/;
116
116
  var OPERATOR_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
117
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
118
117
 
119
118
  var KINDS = Object.freeze([
120
119
  "chargeback",
@@ -151,6 +150,7 @@ var STATUSES = Object.freeze([
151
150
  ]);
152
151
 
153
152
  var b = require("./vendor/blamejs");
153
+ var textGuard = require("./text-guard");
154
154
 
155
155
  // ---- validators ---------------------------------------------------------
156
156
 
@@ -249,9 +249,7 @@ function _narrative(s) {
249
249
  " exceeds cap " + MAX_NARRATIVE_LEN
250
250
  );
251
251
  }
252
- if (CONTROL_BYTE_RE.test(s)) {
253
- throw new TypeError("disputeResolution: narrative must not contain control bytes");
254
- }
252
+ textGuard.freeText(s, "disputeResolution: narrative");
255
253
  return s;
256
254
  }
257
255
 
@@ -265,9 +263,7 @@ function _blobRef(s) {
265
263
  " exceeds cap " + MAX_BLOB_REF_LEN
266
264
  );
267
265
  }
268
- if (CONTROL_BYTE_RE.test(s)) {
269
- throw new TypeError("disputeResolution: blob_ref must not contain control bytes");
270
- }
266
+ textGuard.freeText(s, "disputeResolution: blob_ref");
271
267
  return s;
272
268
  }
273
269
 
@@ -281,9 +277,7 @@ function _notesOpt(s) {
281
277
  "disputeResolution: notes length " + s.length + " exceeds cap " + MAX_NOTES_LEN
282
278
  );
283
279
  }
284
- if (CONTROL_BYTE_RE.test(s)) {
285
- throw new TypeError("disputeResolution: notes must not contain control bytes");
286
- }
280
+ textGuard.freeText(s, "disputeResolution: notes");
287
281
  return s;
288
282
  }
289
283
 
@@ -307,9 +301,7 @@ function _writeoffReason(s) {
307
301
  " exceeds cap " + MAX_WRITEOFF_REASON_LEN
308
302
  );
309
303
  }
310
- if (CONTROL_BYTE_RE.test(s)) {
311
- throw new TypeError("disputeResolution: writeoff reason must not contain control bytes");
312
- }
304
+ textGuard.freeText(s, "disputeResolution: writeoff reason");
313
305
  return s;
314
306
  }
315
307
 
@@ -66,6 +66,7 @@
66
66
  */
67
67
 
68
68
  var b = require("./vendor/blamejs");
69
+ var textGuard = require("./text-guard");
69
70
 
70
71
  // ---- constants ----------------------------------------------------------
71
72
 
@@ -81,7 +82,6 @@ var FORWARDING_STATUSES = Object.freeze([
81
82
  var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
82
83
  var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
83
84
  var CURRENCY_RE = /^[A-Z]{3}$/;
84
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
85
85
 
86
86
  var MAX_RETURN_POLICY_LEN = 4000;
87
87
  var MAX_VENDOR_REF_LEN = 256;
@@ -183,9 +183,7 @@ function _returnPolicy(s) {
183
183
  if (typeof s !== "string" || !s.length || s.length > MAX_RETURN_POLICY_LEN) {
184
184
  throw new TypeError("dropship-forwarding: return_policy must be a non-empty string <= " + MAX_RETURN_POLICY_LEN + " chars");
185
185
  }
186
- if (CONTROL_BYTE_RE.test(s)) {
187
- throw new TypeError("dropship-forwarding: return_policy must not contain control bytes");
188
- }
186
+ textGuard.freeText(s, "dropship-forwarding: return_policy", { singleLine: "reject" });
189
187
  }
190
188
  function _positiveInt(n, label) {
191
189
  if (!Number.isInteger(n) || n <= 0) {
@@ -214,7 +212,7 @@ function _shortText(s, label, max) {
214
212
  if (typeof s !== "string" || !s.length || s.length > max) {
215
213
  throw new TypeError("dropship-forwarding: " + label + " must be a non-empty string <= " + max + " chars");
216
214
  }
217
- if (CONTROL_BYTE_RE.test(s)) {
215
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
218
216
  throw new TypeError("dropship-forwarding: " + label + " must not contain control bytes");
219
217
  }
220
218
  }
package/lib/dunning.js CHANGED
@@ -105,6 +105,7 @@
105
105
  */
106
106
 
107
107
  var b = require("./vendor/blamejs");
108
+ var textGuard = require("./text-guard");
108
109
  // Framework constants (C.TIME duration helpers). Safe at module-eval —
109
110
  // the index entry point exposes `framework` before the require cascade.
110
111
  var C = b.constants;
@@ -130,10 +131,6 @@ var DEFAULT_LIST_LIMIT = 100;
130
131
  var MS_PER_HOUR = C.TIME.hours(1);
131
132
 
132
133
  var SLUG_RE = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
133
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
134
- var ZERO_WIDTH_RE = new RegExp(
135
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
136
- );
137
134
 
138
135
  // ---- validators ---------------------------------------------------------
139
136
 
@@ -205,9 +202,7 @@ function _reason(s) {
205
202
  if (s.length > MAX_REASON_LEN) {
206
203
  throw new TypeError("dunning: reason must be <= " + MAX_REASON_LEN + " characters");
207
204
  }
208
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
209
- throw new TypeError("dunning: reason contains control / zero-width bytes");
210
- }
205
+ textGuard.freeText(s, "dunning: reason", { zeroWidth: "reject" });
211
206
  return s;
212
207
  }
213
208
 
@@ -105,6 +105,7 @@
105
105
  */
106
106
 
107
107
  var b = require("./vendor/blamejs");
108
+ var textGuard = require("./text-guard");
108
109
 
109
110
  // ---- constants ----------------------------------------------------------
110
111
 
@@ -133,7 +134,6 @@ var DEFAULT_LIST_LIMIT = 50;
133
134
  // but 1.96 is the de-facto convention for shipped dashboards.
134
135
  var WILSON_Z = 1.96;
135
136
 
136
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
137
137
 
138
138
  // ---- monotonic clock ----------------------------------------------------
139
139
  //
@@ -165,9 +165,7 @@ function _title(s) {
165
165
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
166
166
  throw new TypeError("emailABTests: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
167
167
  }
168
- if (CONTROL_BYTE_RE.test(s)) {
169
- throw new TypeError("emailABTests: title must not contain control bytes");
170
- }
168
+ textGuard.freeText(s, "emailABTests: title");
171
169
  return s;
172
170
  }
173
171
 
@@ -184,7 +182,7 @@ function _label(s, idx) {
184
182
  throw new TypeError("emailABTests: variants[" + idx + "].label must be a non-empty string <= " +
185
183
  MAX_LABEL_LEN + " chars");
186
184
  }
187
- if (CONTROL_BYTE_RE.test(s)) {
185
+ if (textGuard.hasCodepointThreat(s)) {
188
186
  throw new TypeError("emailABTests: variants[" + idx + "].label must not contain control bytes");
189
187
  }
190
188
  return s;
@@ -223,9 +221,7 @@ function _recipientId(s) {
223
221
  throw new TypeError("emailABTests: recipient_id must be a non-empty string <= " +
224
222
  MAX_RECIPIENT_ID_LEN + " chars");
225
223
  }
226
- if (CONTROL_BYTE_RE.test(s)) {
227
- throw new TypeError("emailABTests: recipient_id must not contain control bytes");
228
- }
224
+ textGuard.freeText(s, "emailABTests: recipient_id");
229
225
  return s;
230
226
  }
231
227
 
@@ -146,6 +146,7 @@ var DEFAULT_BATCH_SIZE = 100;
146
146
  var RESOLVE_PAGE_LIMIT = 500; // matches mailingAudiences MAX_LIST_LIMIT
147
147
 
148
148
  var b = require("./vendor/blamejs");
149
+ var textGuard = require("./text-guard");
149
150
  // Framework duration constants (C.TIME.minutes(n) etc.). The index entry
150
151
  // point exposes `framework` before the require cascade, so resolving this
151
152
  // at module-eval is safe — same pattern lib/admin.js / lib/cart.js use.
@@ -164,8 +165,16 @@ var SEND_RATE_WINDOW_MS = C.TIME.minutes(1);
164
165
  // Per-recipient broadcast outcomes recorded in email_campaign_sends.
165
166
  var SEND_OUTCOMES = ["sent", "failed", "skipped_unsubscribed", "skipped_suppressed"];
166
167
 
168
+ // A link URL is rendered on one line and, in the plain-text part, is shown
169
+ // to the recipient verbatim — so it is screened against the shared codepoint
170
+ // policy rather than a local class. The class this replaced covered the
171
+ // zero-width family but not the bidi overrides, which let a campaign body
172
+ // carry a URL that displays as one host and resolves to another.
173
+ var LINK_URL_POLICY = Object.freeze({ singleLine: "reject", zeroWidth: "reject" });
174
+
175
+ // Free-form text folded into one ledger line. Stripped rather than refused —
176
+ // this runs on an error path that must not itself fail.
167
177
  var CONTROL_BYTE_LINE_RE = /[\u0000-\u001f\u007f]/;
168
- var ZERO_WIDTH_RE = /[\u200b-\u200f\u2028\u2029\ufeff]/;
169
178
 
170
179
  var STATUSES = ["draft", "scheduled", "sending", "sent", "paused", "cancelled"];
171
180
  var EVENT_TYPES = ["delivered", "opened", "clicked", "bounced", "unsubscribed"];
@@ -190,7 +199,7 @@ function _esc(s) {
190
199
 
191
200
  function _mdSafeLinkUrl(url) {
192
201
  if (typeof url !== "string" || !url.length || url.length > 2048) return null;
193
- if (CONTROL_BYTE_LINE_RE.test(url) || ZERO_WIDTH_RE.test(url)) return null;
202
+ if (textGuard.hasCodepointThreat(url, LINK_URL_POLICY)) return null;
194
203
  if (url.charCodeAt(0) === 47 /* "/" */) {
195
204
  if (url.length > 1 && url.charCodeAt(1) === 47) return null;
196
205
  if (url.indexOf("..") !== -1) return null;
@@ -389,10 +398,13 @@ function _validateSubject(s) {
389
398
  if (s.length > MAX_SUBJECT_LEN) {
390
399
  throw new TypeError("emailCampaigns: subject must be <= " + MAX_SUBJECT_LEN + " characters");
391
400
  }
392
- if (/[\r\n\0]/.test(s)) {
393
- throw new TypeError("emailCampaigns: subject must not contain CR / LF / NUL");
394
- }
395
- return s;
401
+ // A subject becomes a `Subject:` header, so a CR or LF in it is a header
402
+ // injection the bytes after the break are read as another header. NUL goes
403
+ // with them: it truncates the field in some agents, which hides whatever
404
+ // follows. b.safeBuffer.assertHeaderSafe is the framework's check for a
405
+ // value destined for a `Name: value` field, and it refuses exactly that set.
406
+ return b.safeBuffer.assertHeaderSafe(
407
+ s, "subject", TypeError, "emailCampaigns: subject must not contain CR / LF / NUL");
396
408
  }
397
409
 
398
410
  function _validateBody(s, label) {
@@ -433,10 +445,9 @@ function _validateFromName(s) {
433
445
  if (s.length > MAX_FROM_NAME_LEN) {
434
446
  throw new TypeError("emailCampaigns: from_name must be <= " + MAX_FROM_NAME_LEN + " characters");
435
447
  }
436
- if (/[\r\n\0]/.test(s)) {
437
- throw new TypeError("emailCampaigns: from_name must not contain CR / LF / NUL");
438
- }
439
- return s;
448
+ // Lands in the `From:` header — same header-injection surface as the subject.
449
+ return b.safeBuffer.assertHeaderSafe(
450
+ s, "from_name", TypeError, "emailCampaigns: from_name must not contain CR / LF / NUL");
440
451
  }
441
452
 
442
453
  function _validateTs(ts, label) {
@@ -481,10 +492,10 @@ function _validateReason(s) {
481
492
  if (s.length > MAX_REASON_LEN) {
482
493
  throw new TypeError("emailCampaigns: reason must be <= " + MAX_REASON_LEN + " characters");
483
494
  }
484
- if (/[\r\n\0]/.test(s)) {
485
- throw new TypeError("emailCampaigns: reason must not contain CR / LF / NUL");
486
- }
487
- return s;
495
+ // Operator-authored and surfaced in logs and the console rather than a mail
496
+ // header, but held to the same rule: a newline here forges a log line.
497
+ return b.safeBuffer.assertHeaderSafe(
498
+ s, "reason", TypeError, "emailCampaigns: reason must not contain CR / LF / NUL");
488
499
  }
489
500
 
490
501
  function _validateStatus(s, label) {
@@ -93,6 +93,7 @@
93
93
  // ---- constants ----------------------------------------------------------
94
94
 
95
95
  var b = require("./vendor/blamejs");
96
+ var textGuard = require("./text-guard");
96
97
 
97
98
  var C = b.constants;
98
99
 
@@ -118,10 +119,6 @@ var IPV4_RE = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4
118
119
  var IPV6_RE = /^[0-9a-f:]+$/;
119
120
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
120
121
 
121
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
122
- var ZERO_WIDTH_RE = new RegExp(
123
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
124
- );
125
122
 
126
123
  var ALLOWED_STATUSES = Object.freeze(["active", "paused", "archived"]);
127
124
 
@@ -234,9 +231,7 @@ function _reason(s) {
234
231
  if (s.length > MAX_REASON_LEN) {
235
232
  throw new TypeError("emailWarmup: reason must be <= " + MAX_REASON_LEN + " characters");
236
233
  }
237
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
238
- throw new TypeError("emailWarmup: reason contains control / zero-width bytes");
239
- }
234
+ textGuard.freeText(s, "emailWarmup: reason", { singleLine: "reject", zeroWidth: "reject" });
240
235
  return s;
241
236
  }
242
237
 
@@ -116,15 +116,10 @@ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
116
116
  // title + hypothesis fields land into the operator dashboard, not the
117
117
  // storefront, but the discipline is the same — strings reach the
118
118
  // operator UI as inert text, never as live markup.
119
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
120
- var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
121
119
 
122
120
  // Zero-width / direction-override family — mirrors the promo-banners
123
121
  // + gift-options catalogues. Spelled with \u-escapes so ESLint's
124
122
  // no-irregular-whitespace stays happy.
125
- var ZERO_WIDTH_RE = new RegExp(
126
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
127
- );
128
123
 
129
124
  var ALLOWED_PATCH_COLUMNS = Object.freeze([
130
125
  "title",
@@ -134,6 +129,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
134
129
  ]);
135
130
 
136
131
  var b = require("./vendor/blamejs");
132
+ var textGuard = require("./text-guard");
137
133
 
138
134
  // ---- validators ---------------------------------------------------------
139
135
 
@@ -149,10 +145,10 @@ function _line(s, label, maxLen) {
149
145
  if (typeof s !== "string" || !s.length || s.length > maxLen) {
150
146
  throw new TypeError("experiments: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
151
147
  }
152
- if (CONTROL_BYTE_LINE_RE.test(s)) {
148
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
153
149
  throw new TypeError("experiments: " + label + " contains control bytes (incl. CR/LF)");
154
150
  }
155
- if (ZERO_WIDTH_RE.test(s)) {
151
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
156
152
  throw new TypeError("experiments: " + label + " contains zero-width / direction-override characters");
157
153
  }
158
154
  return s;
@@ -162,10 +158,10 @@ function _block(s, label, maxLen) {
162
158
  if (typeof s !== "string" || !s.length || s.length > maxLen) {
163
159
  throw new TypeError("experiments: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
164
160
  }
165
- if (CONTROL_BYTE_BLOCK_RE.test(s)) {
161
+ if (textGuard.hasCodepointThreat(s)) {
166
162
  throw new TypeError("experiments: " + label + " contains control bytes");
167
163
  }
168
- if (ZERO_WIDTH_RE.test(s)) {
164
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
169
165
  throw new TypeError("experiments: " + label + " contains zero-width / direction-override characters");
170
166
  }
171
167
  return s;
@@ -437,7 +433,7 @@ function create(opts) {
437
433
  if (typeof input.metric !== "string" || !input.metric.length || input.metric.length > MAX_METRIC_LEN) {
438
434
  return { recorded: false };
439
435
  }
440
- if (CONTROL_BYTE_LINE_RE.test(input.metric) || ZERO_WIDTH_RE.test(input.metric)) {
436
+ if (textGuard.hasCodepointThreat(input.metric, { singleLine: "reject", zeroWidth: "reject" })) {
441
437
  return { recorded: false };
442
438
  }
443
439
  var value = 1;