@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
@@ -158,16 +158,12 @@ var PRODUCT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
158
158
  // body permits LF (Markdown is line-oriented); single-line fields
159
159
  // refuse LF / CR so they can't smuggle a second line into a post
160
160
  // header / meta tag / hero-image attribute.
161
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
162
- var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
163
161
 
164
162
  // Zero-width / direction-override family — spelled with \u-escapes so
165
163
  // ESLint's no-irregular-whitespace stays happy.
166
- var ZERO_WIDTH_RE = new RegExp(
167
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
168
- );
169
164
 
170
165
  var b = require("./vendor/blamejs");
166
+ var textGuard = require("./text-guard");
171
167
 
172
168
  // ---- monotonic clock ---------------------------------------------------
173
169
  //
@@ -204,12 +200,7 @@ function _title(s) {
204
200
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
205
201
  throw new TypeError("blogArticles: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
206
202
  }
207
- if (CONTROL_BYTE_LINE_RE.test(s)) {
208
- throw new TypeError("blogArticles: title contains control bytes (incl. CR/LF)");
209
- }
210
- if (ZERO_WIDTH_RE.test(s)) {
211
- throw new TypeError("blogArticles: title contains zero-width / direction-override characters");
212
- }
203
+ textGuard.freeText(s, "blogArticles: title", { singleLine: "reject", zeroWidth: "reject" });
213
204
  return s;
214
205
  }
215
206
 
@@ -217,12 +208,7 @@ function _body(s) {
217
208
  if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
218
209
  throw new TypeError("blogArticles: body must be a non-empty string <= " + MAX_BODY_LEN + " chars");
219
210
  }
220
- if (CONTROL_BYTE_BLOCK_RE.test(s)) {
221
- throw new TypeError("blogArticles: body contains control bytes");
222
- }
223
- if (ZERO_WIDTH_RE.test(s)) {
224
- throw new TypeError("blogArticles: body contains zero-width / direction-override characters");
225
- }
211
+ textGuard.freeText(s, "blogArticles: body", { zeroWidth: "reject" });
226
212
  return s;
227
213
  }
228
214
 
@@ -306,9 +292,7 @@ function _heroImageUrl(s) {
306
292
  if (s.length > MAX_HERO_IMAGE_URL_LEN) {
307
293
  throw new TypeError("blogArticles: hero_image_url must be <= " + MAX_HERO_IMAGE_URL_LEN + " chars");
308
294
  }
309
- if (CONTROL_BYTE_LINE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
310
- throw new TypeError("blogArticles: hero_image_url contains control / zero-width bytes");
311
- }
295
+ textGuard.freeText(s, "blogArticles: hero_image_url", { singleLine: "reject", zeroWidth: "reject" });
312
296
  if (s.charCodeAt(0) === 47 /* "/" */) {
313
297
  if (s.length > 1 && s.charCodeAt(1) === 47) {
314
298
  throw new TypeError("blogArticles: hero_image_url protocol-relative `//host/...` refused — use absolute https://");
@@ -331,10 +315,10 @@ function _metaLine(s, label, maxLen) {
331
315
  if (typeof s !== "string" || s.length > maxLen) {
332
316
  throw new TypeError("blogArticles: " + label + " must be a string <= " + maxLen + " chars");
333
317
  }
334
- if (CONTROL_BYTE_LINE_RE.test(s)) {
318
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
335
319
  throw new TypeError("blogArticles: " + label + " contains control bytes (incl. CR/LF)");
336
320
  }
337
- if (ZERO_WIDTH_RE.test(s)) {
321
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
338
322
  throw new TypeError("blogArticles: " + label + " contains zero-width / direction-override characters");
339
323
  }
340
324
  return s;
@@ -367,9 +351,7 @@ function _sessionIdRaw(s) {
367
351
  if (s.length > 256) {
368
352
  throw new TypeError("blogArticles: session_id must be <= 256 characters");
369
353
  }
370
- if (CONTROL_BYTE_LINE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
371
- throw new TypeError("blogArticles: session_id contains control / zero-width bytes");
372
- }
354
+ textGuard.freeText(s, "blogArticles: session_id", { singleLine: "reject", zeroWidth: "reject" });
373
355
  return s;
374
356
  }
375
357
 
@@ -424,7 +406,7 @@ function _esc(s) {
424
406
 
425
407
  function _safeLinkUrl(url) {
426
408
  if (typeof url !== "string" || !url.length || url.length > 2048) return null;
427
- if (CONTROL_BYTE_LINE_RE.test(url) || ZERO_WIDTH_RE.test(url)) return null;
409
+ if (textGuard.hasCodepointThreat(url, { singleLine: "reject", zeroWidth: "reject" })) return null;
428
410
  if (url.charCodeAt(0) === 47 /* "/" */) {
429
411
  if (url.length > 1 && url.charCodeAt(1) === 47) return null;
430
412
  if (url.indexOf("..") !== -1) return null;
@@ -165,7 +165,6 @@ var DEFAULT_LIST_LIMIT = 100;
165
165
  var MAX_LIST_LIMIT = 500;
166
166
 
167
167
  var SLUG_RE = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
168
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
169
168
  var ACTION_RE = /^[A-Za-z0-9_./-]+$/;
170
169
 
171
170
  var ALLOWED_PATCH_COLUMNS = Object.freeze([
@@ -177,6 +176,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
177
176
 
178
177
  // Framework handle (the vendored blamejs); index.js re-exports this as .framework.
179
178
  var b = require("./vendor/blamejs");
179
+ var textGuard = require("./text-guard");
180
180
 
181
181
  // ---- validators --------------------------------------------------------
182
182
 
@@ -224,9 +224,7 @@ function _publicKey(s) {
224
224
  "captchaGate: public_key must be <= " + PUBLIC_KEY_MAX_LEN + " characters"
225
225
  );
226
226
  }
227
- if (CONTROL_BYTE_RE.test(s)) {
228
- throw new TypeError("captchaGate: public_key contains control bytes");
229
- }
227
+ textGuard.freeText(s, "captchaGate: public_key");
230
228
  return s;
231
229
  }
232
230
 
@@ -245,9 +243,7 @@ function _secretKeyRaw(s) {
245
243
  "captchaGate: secret_key must be <= " + SECRET_KEY_MAX_LEN + " characters"
246
244
  );
247
245
  }
248
- if (CONTROL_BYTE_RE.test(trimmed)) {
249
- throw new TypeError("captchaGate: secret_key contains control bytes");
250
- }
246
+ textGuard.freeText(trimmed, "captchaGate: secret_key");
251
247
  return trimmed;
252
248
  }
253
249
 
@@ -290,9 +286,7 @@ function _token(s) {
290
286
  "captchaGate: token must be <= " + TOKEN_MAX_LEN + " characters"
291
287
  );
292
288
  }
293
- if (CONTROL_BYTE_RE.test(s)) {
294
- throw new TypeError("captchaGate: token contains control bytes");
295
- }
289
+ textGuard.freeText(s, "captchaGate: token");
296
290
  return s;
297
291
  }
298
292
 
@@ -337,10 +331,8 @@ function _optionalHash(s, label) {
337
331
  "captchaGate: " + label + " must be <= " + HASH_MAX_LEN + " characters"
338
332
  );
339
333
  }
340
- if (CONTROL_BYTE_RE.test(s)) {
341
- throw new TypeError(
342
- "captchaGate: " + label + " contains control bytes"
343
- );
334
+ if (textGuard.hasCodepointThreat(s)) {
335
+ throw new TypeError("captchaGate: " + label + " contains control bytes");
344
336
  }
345
337
  return s;
346
338
  }
@@ -355,9 +347,7 @@ function _optionalSessionId(s) {
355
347
  "captchaGate: session_id must be <= " + SESSION_ID_MAX_LEN + " characters"
356
348
  );
357
349
  }
358
- if (CONTROL_BYTE_RE.test(s)) {
359
- throw new TypeError("captchaGate: session_id contains control bytes");
360
- }
350
+ textGuard.freeText(s, "captchaGate: session_id");
361
351
  return s;
362
352
  }
363
353
 
@@ -101,6 +101,7 @@ var STATUSES = Object.freeze(["active", "disabled", "rotating"]);
101
101
 
102
102
  // Framework handle (the vendored blamejs); index.js re-exports this as .framework.
103
103
  var b = require("./vendor/blamejs");
104
+ var textGuard = require("./text-guard");
104
105
 
105
106
  var ROTATION_GRACE_MS = b.constants.TIME.days(1);
106
107
 
@@ -124,10 +125,6 @@ var MAX_KEY_LEN = 512;
124
125
  // reason / operation string. These columns surface on operator
125
126
  // dashboards and inline into log lines — embedded control bytes are a
126
127
  // slipping-class for header injection + visual spoofing.
127
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
128
- var ZERO_WIDTH_RE = new RegExp(
129
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
130
- );
131
128
 
132
129
  // account_number alphabet — operators occasionally space-pad or
133
130
  // hyphenate the printed form; we accept alnum + dash + space + dot and
@@ -233,9 +230,7 @@ function _label(s) {
233
230
  if (s.length === 0 || s.length > MAX_LABEL_LEN) {
234
231
  throw new TypeError("carrier-accounts: account_label must be 1.." + MAX_LABEL_LEN + " characters");
235
232
  }
236
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
237
- throw new TypeError("carrier-accounts: account_label contains control / zero-width bytes");
238
- }
233
+ textGuard.freeText(s, "carrier-accounts: account_label", { singleLine: "reject", zeroWidth: "reject" });
239
234
  return s;
240
235
  }
241
236
 
@@ -250,9 +245,7 @@ function _reason(s) {
250
245
  if (s.length > MAX_REASON_LEN) {
251
246
  throw new TypeError("carrier-accounts: reason must be <= " + MAX_REASON_LEN + " characters");
252
247
  }
253
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
254
- throw new TypeError("carrier-accounts: reason contains control / zero-width bytes");
255
- }
248
+ textGuard.freeText(s, "carrier-accounts: reason", { singleLine: "reject", zeroWidth: "reject" });
256
249
  return s;
257
250
  }
258
251
 
@@ -106,6 +106,7 @@ var MAX_CHANGES_PER_DRAFT = 5000;
106
106
 
107
107
  // Framework handle (the vendored blamejs); index.js re-exports this as .framework.
108
108
  var b = require("./vendor/blamejs");
109
+ var textGuard = require("./text-guard");
109
110
 
110
111
  var ROLLBACK_WINDOW_MS = b.constants.TIME.days(7);
111
112
 
@@ -125,10 +126,6 @@ var PRODUCT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,198}[a-z0-9])?$/;
125
126
  // to operator-authored fields — refuse control bytes + zero-width /
126
127
  // direction-override codepoints so a rollback_reason / cancel_reason
127
128
  // can't smuggle UI-confusing glyphs into the audit dashboard.
128
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
129
- var ZERO_WIDTH_RE = new RegExp(
130
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
131
- );
132
129
 
133
130
 
134
131
  // ---- validators ---------------------------------------------------------
@@ -144,7 +141,7 @@ function _title(s, label) {
144
141
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
145
142
  throw new TypeError("catalogDrafts: " + label + " must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
146
143
  }
147
- if (CONTROL_BYTE_RE.test(s)) {
144
+ if (textGuard.hasCodepointThreat(s)) {
148
145
  throw new TypeError("catalogDrafts: " + label + " must not contain control bytes");
149
146
  }
150
147
  return s;
@@ -155,9 +152,7 @@ function _description(s) {
155
152
  if (typeof s !== "string" || s.length > MAX_DESC_LEN) {
156
153
  throw new TypeError("catalogDrafts: description must be a string <= " + MAX_DESC_LEN + " chars when provided");
157
154
  }
158
- if (CONTROL_BYTE_RE.test(s)) {
159
- throw new TypeError("catalogDrafts: description must not contain control bytes");
160
- }
155
+ textGuard.freeText(s, "catalogDrafts: description");
161
156
  return s;
162
157
  }
163
158
 
@@ -169,7 +164,7 @@ function _optReason(s, label) {
169
164
  if (s.length > MAX_REASON_LEN) {
170
165
  throw new TypeError("catalogDrafts: " + label + " must be <= " + MAX_REASON_LEN + " chars");
171
166
  }
172
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
167
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
173
168
  throw new TypeError("catalogDrafts: " + label + " contains control / zero-width bytes");
174
169
  }
175
170
  return s;
@@ -187,9 +182,7 @@ function _optOwner(s) {
187
182
  if (typeof s !== "string" || !s.length || s.length > MAX_OWNER_LEN) {
188
183
  throw new TypeError("catalogDrafts: owner_id must be a non-empty string <= " + MAX_OWNER_LEN + " chars when provided");
189
184
  }
190
- if (CONTROL_BYTE_RE.test(s)) {
191
- throw new TypeError("catalogDrafts: owner_id must not contain control bytes");
192
- }
185
+ textGuard.freeText(s, "catalogDrafts: owner_id");
193
186
  return s;
194
187
  }
195
188
 
@@ -111,13 +111,9 @@ var MAX_POPULAR_LIMIT = 100;
111
111
  var MAX_REORDER_BATCH = 500;
112
112
 
113
113
  var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
114
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
115
- var CONTROL_BYTE_BODY_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
116
- var ZERO_WIDTH_RE = new RegExp(
117
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
118
- );
119
114
 
120
115
  var b = require("./vendor/blamejs");
116
+ var textGuard = require("./text-guard");
121
117
 
122
118
  // ---- monotonic clock ---------------------------------------------------
123
119
  //
@@ -161,12 +157,7 @@ function _title(s) {
161
157
  if (s.length > MAX_TITLE_LEN) {
162
158
  throw new TypeError("categoryNavigation: title must be <= " + MAX_TITLE_LEN + " characters");
163
159
  }
164
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
165
- throw new TypeError("categoryNavigation: title contains control bytes");
166
- }
167
- if (ZERO_WIDTH_RE.test(s)) {
168
- throw new TypeError("categoryNavigation: title contains zero-width / direction-override characters");
169
- }
160
+ textGuard.freeText(s, "categoryNavigation: title", { singleLine: "reject", zeroWidth: "reject" });
170
161
  return s;
171
162
  }
172
163
 
@@ -178,12 +169,7 @@ function _description(s) {
178
169
  if (s.length > MAX_DESCRIPTION_LEN) {
179
170
  throw new TypeError("categoryNavigation: description must be <= " + MAX_DESCRIPTION_LEN + " characters");
180
171
  }
181
- if (CONTROL_BYTE_BODY_RE.test(s)) {
182
- throw new TypeError("categoryNavigation: description contains control bytes");
183
- }
184
- if (ZERO_WIDTH_RE.test(s)) {
185
- throw new TypeError("categoryNavigation: description contains zero-width / direction-override characters");
186
- }
172
+ textGuard.freeText(s, "categoryNavigation: description", { zeroWidth: "reject" });
187
173
  return s;
188
174
  }
189
175
 
@@ -200,9 +186,7 @@ function _heroImageUrl(s) {
200
186
  if (s.length > MAX_HERO_URL_LEN) {
201
187
  throw new TypeError("categoryNavigation: hero_image_url must be <= " + MAX_HERO_URL_LEN + " characters");
202
188
  }
203
- if (CONTROL_BYTE_BODY_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
204
- throw new TypeError("categoryNavigation: hero_image_url contains control / zero-width bytes");
205
- }
189
+ textGuard.freeText(s, "categoryNavigation: hero_image_url", { zeroWidth: "reject" });
206
190
  if (s.charCodeAt(0) === 47 /* "/" */) {
207
191
  if (s.length > 1 && s.charCodeAt(1) === 47) {
208
192
  throw new TypeError("categoryNavigation: hero_image_url protocol-relative `//host/...` refused — use absolute https://");
package/lib/cms-blocks.js CHANGED
@@ -116,16 +116,13 @@ var LOCALE_RE = /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/;
116
116
 
117
117
  // Refuse C0 control bytes + DEL in operator-authored bodies. LF / CR
118
118
  // / tab are allowed — operators author multi-line Markdown.
119
- var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
120
119
 
121
120
  // Zero-width / direction-override family. Spelled with \u-escapes
122
121
  // so ESLint's no-irregular-whitespace stays happy. Same defence as
123
122
  // the rest of the shop primitives.
124
- var ZERO_WIDTH_RE = new RegExp(
125
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
126
- );
127
123
 
128
124
  var b = require("./vendor/blamejs");
125
+ var textGuard = require("./text-guard");
129
126
 
130
127
  // ---- validators ---------------------------------------------------------
131
128
 
@@ -141,10 +138,10 @@ function _body(s, label) {
141
138
  if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
142
139
  throw new TypeError("cmsBlocks: " + label + " must be a non-empty string <= " + MAX_BODY_LEN + " chars");
143
140
  }
144
- if (CONTROL_BYTE_BLOCK_RE.test(s)) {
141
+ if (textGuard.hasCodepointThreat(s)) {
145
142
  throw new TypeError("cmsBlocks: " + label + " contains control bytes");
146
143
  }
147
- if (ZERO_WIDTH_RE.test(s)) {
144
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
148
145
  throw new TypeError("cmsBlocks: " + label + " contains zero-width / direction-override characters");
149
146
  }
150
147
  return s;
@@ -241,7 +238,7 @@ function _esc(s) {
241
238
 
242
239
  function _safeLinkUrl(url) {
243
240
  if (typeof url !== "string" || !url.length || url.length > 2048) return null;
244
- if (CONTROL_BYTE_BLOCK_RE.test(url) || ZERO_WIDTH_RE.test(url)) return null;
241
+ if (textGuard.hasCodepointThreat(url, { zeroWidth: "reject" })) return null;
245
242
  if (url.charCodeAt(0) === 47 /* "/" */) {
246
243
  if (url.length > 1 && url.charCodeAt(1) === 47) return null;
247
244
  if (url.indexOf("..") !== -1) return null;
@@ -114,6 +114,7 @@
114
114
  */
115
115
 
116
116
  var b = require("./vendor/blamejs");
117
+ var textGuard = require("./text-guard");
117
118
 
118
119
  // ---- constants ----------------------------------------------------------
119
120
 
@@ -177,10 +178,6 @@ var DEFAULT_LIST_LIMIT = 50;
177
178
  // Operator-authored prose lands in reason / dismiss_reason and
178
179
  // replays into compliance review screens. Same control-byte +
179
180
  // zero-width refusal posture the sibling primitives carry.
180
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
181
- var ZERO_WIDTH_RE = new RegExp(
182
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
183
- );
184
181
 
185
182
  // Scope -> which injected readers contribute on export. Keeps the
186
183
  // fulfillRequest logic a single lookup instead of nested if-else.
@@ -256,9 +253,7 @@ function _requestedBy(s) {
256
253
  if (typeof s !== "string" || !s.length || s.length > MAX_REQUESTED_BY_LEN) {
257
254
  throw new TypeError("complianceExport: requested_by must be a non-empty string <= " + MAX_REQUESTED_BY_LEN + " chars");
258
255
  }
259
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
260
- throw new TypeError("complianceExport: requested_by must not contain control bytes or zero-width characters");
261
- }
256
+ textGuard.freeText(s, "complianceExport: requested_by", { zeroWidth: "reject" });
262
257
  return s;
263
258
  }
264
259
 
@@ -266,7 +261,7 @@ function _prose(s, label, maxLen) {
266
261
  if (typeof s !== "string" || !s.length || s.length > maxLen) {
267
262
  throw new TypeError("complianceExport: " + label + " must be a non-empty string <= " + maxLen + " chars");
268
263
  }
269
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
264
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
270
265
  throw new TypeError("complianceExport: " + label + " must not contain control bytes or zero-width characters");
271
266
  }
272
267
  return s;
@@ -286,9 +281,7 @@ function _deliveryAddress(s) {
286
281
  if (typeof s !== "string" || !s.length || s.length > MAX_DELIVERY_ADDR_LEN) {
287
282
  throw new TypeError("complianceExport: delivery_address must be a non-empty string <= " + MAX_DELIVERY_ADDR_LEN + " chars");
288
283
  }
289
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
290
- throw new TypeError("complianceExport: delivery_address must not contain control bytes or zero-width characters");
291
- }
284
+ textGuard.freeText(s, "complianceExport: delivery_address", { zeroWidth: "reject" });
292
285
  return s;
293
286
  }
294
287
 
@@ -123,7 +123,6 @@ var JURISDICTION_RE = /^[A-Z]{2}$/;
123
123
  var EVIDENCE_REF_MAX_LEN = 256;
124
124
  var EVIDENCE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
125
125
 
126
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
127
126
 
128
127
  var CSV_COLUMNS = Object.freeze([
129
128
  "id",
@@ -138,6 +137,7 @@ var CSV_COLUMNS = Object.freeze([
138
137
  ]);
139
138
 
140
139
  var b = require("./vendor/blamejs");
140
+ var textGuard = require("./text-guard");
141
141
 
142
142
  // The GDPR Art. 6(1) lawful bases, sourced from the framework's
143
143
  // consent primitive so this ledger's vocabulary tracks the
@@ -269,9 +269,7 @@ function _optEvidenceRef(s) {
269
269
  "consentLedger: evidence_ref must be <= " + EVIDENCE_REF_MAX_LEN + " characters"
270
270
  );
271
271
  }
272
- if (CONTROL_BYTE_RE.test(s)) {
273
- throw new TypeError("consentLedger: evidence_ref must not contain control bytes");
274
- }
272
+ textGuard.freeText(s, "consentLedger: evidence_ref", { singleLine: "reject" });
275
273
  if (!EVIDENCE_REF_RE.test(s)) {
276
274
  throw new TypeError(
277
275
  "consentLedger: evidence_ref must match /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/"
@@ -108,6 +108,16 @@ var b = require("./vendor/blamejs");
108
108
 
109
109
  var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
110
110
  var CURRENCY_RE = /^[A-Z]{3}$/;
111
+
112
+ // Carrier for the largest-remainder split of a line's COGS across the layers
113
+ // it drew from (see consumeForSale). b.money.allocate distributes a count of
114
+ // MINOR UNITS, so the currency is a type tag and not an input to the result —
115
+ // the split of 10040 across 40/40/20 is 4016/4016/2008 whether the money is
116
+ // 2-decimal, 0-decimal or this. It is deliberately NOT the layers' currency:
117
+ // receipts accept any well-formed ISO 4217 code while b.money accepts a
118
+ // narrower catalog, and pricing layers in one outside it would otherwise make
119
+ // them receivable but never sellable.
120
+ var ALLOCATION_CARRIER = "USD";
111
121
  var METHODS = Object.freeze(["fifo", "lifo", "weighted_average"]);
112
122
  var RECEIPT_SOURCES = Object.freeze(["receipt", "transfer", "adjustment"]);
113
123
  // Internal source value used by `recordReversal` only; operators
@@ -453,11 +463,6 @@ function create(opts) {
453
463
  // don't accumulate a fractional debt.
454
464
  var lineNumerator = sumValue * quantity;
455
465
  var lineCogs = Math.floor(lineNumerator / sumQty);
456
- // Residual carried forward to per-layer attribution rows so
457
- // the sum of attributions matches lineCogs exactly. The first
458
- // layer in the iteration absorbs the residual (smallest
459
- // possible deviation, deterministic placement).
460
- var residual = lineCogs;
461
466
  // Debit every layer proportionally. Use integer division so
462
467
  // each layer's debit is an integer; spread any leftover
463
468
  // single units across the earliest layers (deterministic).
@@ -491,20 +496,45 @@ function create(opts) {
491
496
  remainingToConsume -= 1;
492
497
  }
493
498
  }
494
- // Per-attribution unit cost: weighted-average rounded to the
495
- // nearest integer minor unit. Track the residual so the last
496
- // attribution carries any rounding adjustment.
497
- var avgUnitCost = sumQty > 0 ? Math.round(sumValue / sumQty) : 0;
499
+ // Split the line's COGS across the debits in proportion to the units
500
+ // each one contributes. b.money.allocate distributes by largest
501
+ // remainder: every row lands within a single minor unit of its exact
502
+ // share, and the parts sum to the total with nothing left over.
503
+ //
504
+ // What it replaces priced each debit at the weighted-average unit cost
505
+ // ROUNDED to an integer, then handed the last debit whatever remained.
506
+ // Rounding the average absorbed a fraction of a minor unit per unit
507
+ // debited, and every one of those fractions landed on the final row.
508
+ // Consuming 40 + 40 + 20 units at an average of 100.4 attributed
509
+ // 4000 / 4000 / 2040 where the true shares are 4016 / 4016 / 2008 — a
510
+ // 32-minor-unit misattribution from one sale, growing with the
511
+ // quantities ahead of the last debit. The line total was always right,
512
+ // so nothing was over- or under-charged; it was the per-layer COGS the
513
+ // margin and valuation reports read that was wrong.
514
+ //
515
+ // The split is carried out on a Money in ALLOCATION_CARRIER rather than
516
+ // in the layers' own currency, and the currency genuinely does not
517
+ // enter the arithmetic: allocate distributes a count of minor units by
518
+ // largest remainder, so the exponent never participates. The identical
519
+ // split for a 2-decimal, a 0-decimal and this carrier is asserted in
520
+ // cost-layers.test.js so the assumption cannot rot silently.
521
+ //
522
+ // Using the layers' own currency would read better and is what this
523
+ // wanted to do. It cannot: b.money accepts a deliberately narrow
524
+ // catalog of ISO 4217 codes, while recordReceipt accepts any
525
+ // well-formed one. Pricing layers in a code outside that catalog is
526
+ // accepted today, and passing it here would throw on every
527
+ // weighted-average sale — turning a rounding inaccuracy into inventory
528
+ // that can be received but never sold. Asked upstream for an
529
+ // integer-level allocation in blamejs/blamejs#578.
530
+ var attrShares = debits.length
531
+ ? b.money.fromMinorUnits(BigInt(lineCogs), ALLOCATION_CARRIER)
532
+ .allocate(debits.map(function (dd) { return dd.qty; }))
533
+ .map(function (m) { return Number(m.toMinorUnits()); })
534
+ : [];
498
535
  for (var e = 0; e < debits.length; e += 1) {
499
536
  var dRow = debits[e];
500
- var attrCogs;
501
- if (e === debits.length - 1) {
502
- attrCogs = residual;
503
- } else {
504
- attrCogs = dRow.qty * avgUnitCost;
505
- if (attrCogs > residual) attrCogs = residual;
506
- residual -= attrCogs;
507
- }
537
+ var attrCogs = attrShares[e];
508
538
  // Attribute the debit's cost so `qty * unit_cost_minor` summed over
509
539
  // its attribution rows reconstructs attrCogs EXACTLY — the COGS
510
540
  // reports (cogsForOrder / cogsForPeriod) recover each line's cost
@@ -515,10 +545,15 @@ function create(opts) {
515
545
  // (qty - r) units at floor, r units at floor+1, where
516
546
  // r = attrCogs - floor*qty. (qty-r)*floor + r*(floor+1) =
517
547
  // qty*floor + r = attrCogs — no penny drift between the sale's COGS
518
- // and the persisted attributions (the prior single rounded per-unit
519
- // drifted by up to qty-1 minor units on the residual-bearing row).
520
- var floorUnit = dRow.qty > 0 ? Math.floor(attrCogs / dRow.qty) : avgUnitCost;
521
- var rem = dRow.qty > 0 ? attrCogs - (floorUnit * dRow.qty) : 0;
548
+ // and the persisted attributions (a single rounded per-unit price
549
+ // drifts by up to qty-1 minor units on the row that carries the
550
+ // remainder).
551
+ //
552
+ // A debit is only recorded when its share is at least one unit (see
553
+ // the push above), so the division below always has a positive
554
+ // divisor.
555
+ var floorUnit = Math.floor(attrCogs / dRow.qty);
556
+ var rem = attrCogs - (floorUnit * dRow.qty);
522
557
  var parts = [];
523
558
  if (dRow.qty - rem > 0) parts.push({ qty: dRow.qty - rem, unit_cost_minor: floorUnit });
524
559
  if (rem > 0) parts.push({ qty: rem, unit_cost_minor: floorUnit + 1 });
@@ -50,13 +50,13 @@
50
50
  */
51
51
 
52
52
  var b = require("./vendor/blamejs");
53
+ var textGuard = require("./text-guard");
53
54
 
54
55
  var DEFAULT_MAX_ROWS = 200000;
55
56
  var DEFAULT_MAX_ERRORS = 1000;
56
57
  var SOURCES = Object.freeze(["csv", "ndjson", "api"]);
57
58
  var ON_CONFLICT_MODES = Object.freeze(["update", "skip", "error"]);
58
59
  var CSV_EXPECTED_HEADERS = Object.freeze(["email", "display_name"]);
59
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
60
60
  var MAX_DISPLAY_NAME_LEN = 128;
61
61
 
62
62
  // ---- factory ------------------------------------------------------------
@@ -143,7 +143,7 @@ function _normalizeRow(raw, rowIndex) {
143
143
  if (displayName.length > MAX_DISPLAY_NAME_LEN) {
144
144
  throw new TypeError("row " + rowIndex + ": display_name must be <= " + MAX_DISPLAY_NAME_LEN + " characters");
145
145
  }
146
- if (CONTROL_BYTE_RE.test(displayName)) {
146
+ if (textGuard.hasCodepointThreat(displayName)) {
147
147
  throw new TypeError("row " + rowIndex + ": display_name contains control bytes");
148
148
  }
149
149
  // guardEmail strict profile — refuses smuggling / header-injection /
@@ -46,6 +46,7 @@
46
46
  */
47
47
 
48
48
  var b = require("./vendor/blamejs");
49
+ var textGuard = require("./text-guard");
49
50
 
50
51
  // ---- constants ----------------------------------------------------------
51
52
 
@@ -63,17 +64,9 @@ var MAX_LIMIT = 200;
63
64
  var DEFAULT_TAG_LIMIT = 20;
64
65
  var MAX_TAG_FETCH = 100;
65
66
 
66
- // Refuse C0 control bytes + DEL across the body, tags, and ids. CR/LF
67
- // + tab survive in `body` so operators can wrap a multi-line note;
68
- // tags + ids are single-line and refuse CR/LF too via the strict re.
69
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
70
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
71
- // Zero-width / direction-override / BOM family. Spelled with
72
- // \u-escapes so the source file stays free of irregular-whitespace
73
- // ESLint hits.
74
- var ZERO_WIDTH_RE = new RegExp(
75
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
76
- );
67
+ // Control bytes, bidi overrides and the zero-width family are screened by
68
+ // textGuard.freeText. CR/LF + tab survive in `body` so an operator can wrap a
69
+ // multi-line note; tags + ids are single-line and refuse them.
77
70
 
78
71
  // Tag shape — lowercase alnum + dash + underscore. The operator UI
79
72
  // canonicalises on input (lower-case) so the popularTags aggregate
@@ -110,9 +103,7 @@ function _customerId(s) {
110
103
  if (typeof s !== "string" || !s.length || s.length > MAX_CUSTOMER_ID) {
111
104
  throw new TypeError("customerNotes: customer_id must be a non-empty string (<= " + MAX_CUSTOMER_ID + " chars)");
112
105
  }
113
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
114
- throw new TypeError("customerNotes: customer_id must not contain control bytes");
115
- }
106
+ textGuard.freeText(s, "customerNotes: customer_id", { singleLine: "reject" });
116
107
  return s;
117
108
  }
118
109
 
@@ -128,9 +119,7 @@ function _authorId(s) {
128
119
  if (typeof s !== "string" || !s.length || s.length > MAX_AUTHOR_ID) {
129
120
  throw new TypeError("customerNotes: author_id must be a non-empty string (<= " + MAX_AUTHOR_ID + " chars) when provided");
130
121
  }
131
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
132
- throw new TypeError("customerNotes: author_id must not contain control bytes");
133
- }
122
+ textGuard.freeText(s, "customerNotes: author_id", { singleLine: "reject" });
134
123
  return s;
135
124
  }
136
125
 
@@ -153,12 +142,7 @@ function _body(s) {
153
142
  if (s.length > MAX_BODY_LEN) {
154
143
  throw new TypeError("customerNotes: body must be <= " + MAX_BODY_LEN + " characters");
155
144
  }
156
- if (CONTROL_BYTE_RE.test(s)) {
157
- throw new TypeError("customerNotes: body must not contain control bytes");
158
- }
159
- if (ZERO_WIDTH_RE.test(s)) {
160
- throw new TypeError("customerNotes: body must not contain zero-width / direction-override characters");
161
- }
145
+ textGuard.freeText(s, "customerNotes: body", { zeroWidth: "reject" });
162
146
  return s;
163
147
  }
164
148