@blamejs/blamejs-shop 0.5.20 → 0.5.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -0
  3. package/SECURITY.md +23 -0
  4. package/lib/addresses.js +17 -3
  5. package/lib/admin.js +14 -5
  6. package/lib/affiliates.js +7 -23
  7. package/lib/announcement-bar.js +5 -22
  8. package/lib/api-keys.js +4 -13
  9. package/lib/asset-manifest.json +1 -1
  10. package/lib/banner-ab-tests.js +5 -9
  11. package/lib/blog-articles.js +8 -26
  12. package/lib/captcha-gate.js +7 -17
  13. package/lib/carrier-accounts.js +3 -10
  14. package/lib/catalog-drafts.js +5 -12
  15. package/lib/category-navigation.js +7 -20
  16. package/lib/cms-blocks.js +4 -7
  17. package/lib/compliance-export.js +11 -11
  18. package/lib/consent-ledger.js +2 -4
  19. package/lib/cost-layers.js +56 -21
  20. package/lib/customer-import.js +2 -2
  21. package/lib/customer-notes.js +7 -23
  22. package/lib/customer-roles.js +3 -5
  23. package/lib/customer-segments.js +17 -14
  24. package/lib/customer-surveys.js +7 -14
  25. package/lib/customers.js +2 -4
  26. package/lib/dispute-resolution.js +5 -13
  27. package/lib/dropship-forwarding.js +3 -5
  28. package/lib/dunning.js +2 -7
  29. package/lib/email-ab-tests.js +4 -8
  30. package/lib/email-campaigns.js +39 -15
  31. package/lib/email-warmup.js +2 -7
  32. package/lib/experiments.js +6 -10
  33. package/lib/externaldb-d1.js +47 -31
  34. package/lib/geolocation.js +18 -28
  35. package/lib/gift-options.js +5 -23
  36. package/lib/knowledge-base.js +7 -22
  37. package/lib/line-gift-wrap.js +7 -17
  38. package/lib/live-chat.js +5 -18
  39. package/lib/metered-usage.js +3 -8
  40. package/lib/notifications.js +3 -7
  41. package/lib/operator-accounts.js +3 -5
  42. package/lib/operator-approvals.js +6 -8
  43. package/lib/operator-audit-log.js +2 -7
  44. package/lib/operator-help-center.js +5 -21
  45. package/lib/operator-inbox.js +4 -11
  46. package/lib/operator-roles.js +5 -7
  47. package/lib/order-escalation.js +2 -2
  48. package/lib/order-export.js +24 -20
  49. package/lib/order-notes.js +4 -14
  50. package/lib/order-ratings.js +4 -10
  51. package/lib/payment-methods.js +5 -13
  52. package/lib/payment-retries.js +2 -7
  53. package/lib/pixel-events.js +21 -28
  54. package/lib/plan-changes.js +2 -7
  55. package/lib/product-qa.js +4 -8
  56. package/lib/promo-banners.js +7 -15
  57. package/lib/purchase-orders.js +3 -6
  58. package/lib/push-notifications.js +7 -32
  59. package/lib/pwa-manifest.js +6 -11
  60. package/lib/quotes.js +5 -9
  61. package/lib/reviews.js +4 -10
  62. package/lib/robots-config.js +6 -14
  63. package/lib/sales-tax-filings.js +2 -2
  64. package/lib/search-facets.js +5 -5
  65. package/lib/search-ranking.js +4 -7
  66. package/lib/search-synonyms.js +7 -21
  67. package/lib/security-middleware.js +86 -58
  68. package/lib/seller-signup.js +10 -26
  69. package/lib/sidebar-widgets.js +6 -15
  70. package/lib/site-redirects.js +5 -16
  71. package/lib/sitemap-generator.js +3 -7
  72. package/lib/sms-dispatcher.js +7 -23
  73. package/lib/stock-receipts.js +4 -10
  74. package/lib/storefront-dashboards.js +2 -10
  75. package/lib/storefront-forms.js +9 -22
  76. package/lib/storefront-pages.js +6 -20
  77. package/lib/storefront.js +2 -2
  78. package/lib/subscription-billing.js +3 -8
  79. package/lib/subscription-controls.js +2 -7
  80. package/lib/suggestion-box.js +9 -26
  81. package/lib/support-tickets.js +6 -24
  82. package/lib/tax-remittance.js +2 -2
  83. package/lib/tenants.js +2 -7
  84. package/lib/text-guard.js +179 -35
  85. package/lib/theme-assets.js +4 -16
  86. package/lib/translations.js +3 -6
  87. package/lib/trust-badges.js +4 -7
  88. package/lib/vendor/MANIFEST.json +12 -12
  89. package/lib/vendor/blamejs/CHANGELOG.md +32 -0
  90. package/lib/vendor/blamejs/NOTICE +1 -1
  91. package/lib/vendor/blamejs/README.md +1 -1
  92. package/lib/vendor/blamejs/lib/network-tls.js +98 -13
  93. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +12 -12
  94. package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +568 -304
  95. package/lib/vendor/blamejs/package.json +1 -1
  96. package/lib/vendor/blamejs/sbom.cdx.json +6 -6
  97. package/lib/vendor-invoices.js +3 -6
  98. package/lib/vendors.js +5 -19
  99. package/lib/webhook-receiver.js +3 -10
  100. package/lib/webhook-subscriptions.js +3 -13
  101. package/lib/wishlist-sharing.js +5 -10
  102. package/package.json +1 -1
@@ -527,26 +527,6 @@ var _SCOPED_DIRECTIVES = {
527
527
  // default-src 'self', object-src 'none',
528
528
  // require-trusted-types-for 'script', frame-ancestors 'none', base-uri 'self'
529
529
 
530
- // Parse the vendored default CSP into an ordered directive map ONCE at
531
- // module load. The default is a `name a b; name c;` string; split on `;`,
532
- // the first token of each clause is the directive name, the rest its
533
- // source list.
534
- function _parseCsp(cspString) {
535
- var order = [];
536
- var map = {};
537
- String(cspString).split(";").forEach(function (clause) {
538
- var trimmed = clause.trim();
539
- if (!trimmed) return;
540
- var parts = trimmed.split(/\s+/);
541
- var name = parts[0];
542
- order.push(name);
543
- map[name] = parts.slice(1);
544
- });
545
- return { order: order, map: map };
546
- }
547
-
548
- var _DEFAULT_CSP_PARSED = _parseCsp(_vendoredSecurityHeaders.DEFAULT_CSP);
549
-
550
530
  /**
551
531
  * Build a route-scoped CSP string from the vendored strict default plus the
552
532
  * named providers' hosts. `keys` is an array of CSP_HOSTS keys
@@ -565,40 +545,51 @@ var _DEFAULT_CSP_PARSED = _parseCsp(_vendoredSecurityHeaders.DEFAULT_CSP);
565
545
  * which fails safe (the SDK is refused, not admitted too broadly).
566
546
  */
567
547
  function scopedCsp(keys) {
568
- // Clone the parsed default so the per-call mutation never leaks into the
569
- // module-level snapshot.
570
- var order = _DEFAULT_CSP_PARSED.order.slice();
571
- var map = {};
572
- _DEFAULT_CSP_PARSED.order.forEach(function (name) {
573
- map[name] = _DEFAULT_CSP_PARSED.map[name].slice();
574
- });
575
-
548
+ // Collect the hosts each named provider needs, per directive. Unknown keys
549
+ // fall out here, which is what keeps a mistyped key on the strict default
550
+ // rather than throwing mid-render.
551
+ var additions = {};
576
552
  if (Array.isArray(keys)) {
577
553
  keys.forEach(function (key) {
578
554
  var hostSet = CSP_HOSTS[key];
579
555
  if (!hostSet) return;
580
556
  Object.keys(_SCOPED_DIRECTIVES).forEach(function (directive) {
581
- var subKey = _SCOPED_DIRECTIVES[directive];
582
- var hosts = hostSet[subKey];
557
+ var hosts = hostSet[_SCOPED_DIRECTIVES[directive]];
583
558
  if (!hosts || !hosts.length) return;
584
- if (!map[directive]) {
585
- // The default omits frame-src (it relies on the default-src /
586
- // fenced-frame-src pair); create it from 'self' so admitting a
587
- // provider frame doesn't accidentally open it to everything.
588
- map[directive] = ["'self'"];
589
- order.push(directive);
590
- }
591
- hosts.forEach(function (h) {
592
- if (map[directive].indexOf(h) === -1) map[directive].push(h);
593
- });
559
+ additions[directive] = (additions[directive] || []).concat(hosts);
594
560
  });
595
561
  });
596
562
  }
597
563
 
598
- return order.map(function (name) {
599
- var srcs = map[name];
600
- return srcs.length ? (name + " " + srcs.join(" ")) : name;
601
- }).join("; ") + ";";
564
+ // b.csp.mergeDirectives owns the merge: append-and-dedupe per directive,
565
+ // seed a directive the base omits rather than leaving it wide open, and
566
+ // validate only what was added so the trusted default round-trips
567
+ // untouched. It replaces a CSP parser written here, which had to be kept
568
+ // in step with whatever shape the vendored default took.
569
+ //
570
+ // The primitive throws on a directive or source it will not accept. This
571
+ // runs per response while rendering a payment page, and the documented
572
+ // contract is that a bad input degrades to the strict default rather than
573
+ // failing the render — so a refusal is caught and answered with exactly
574
+ // that. Failing closed here means the provider is refused, never admitted
575
+ // more broadly than asked.
576
+ // The refusal is recorded rather than swallowed: it means a host in
577
+ // CSP_HOSTS is one the primitive will not accept, which is an operator
578
+ // configuration problem that would otherwise show up only as a payment SDK
579
+ // silently failing to load.
580
+ try {
581
+ return b.csp.mergeDirectives(undefined, additions);
582
+ } catch (e) {
583
+ b.audit.safeEmit({
584
+ action: "shop.security.scopedCsp.refused",
585
+ outcome: "failure",
586
+ metadata: {
587
+ keys: Array.isArray(keys) ? keys.join(",") : String(keys),
588
+ reason: (e && e.message) || "unknown",
589
+ },
590
+ });
591
+ return _vendoredSecurityHeaders.DEFAULT_CSP;
592
+ }
602
593
  }
603
594
 
604
595
  // ---- route-scoped Permissions-Policy (payment + passkey surfaces) --------
@@ -645,7 +636,10 @@ function scopedCsp(keys) {
645
636
  // top-level same-origin document). Quote-wrapped per RFC 9651 structured
646
637
  // fields. Single source — every page that enables the payment feature names
647
638
  // it here.
648
- var _PAYMENT_ALLOWLIST = 'payment=(self "https://js.stripe.com" "https://pay.google.com")';
639
+ // Allowlist VALUES, without the `feature=` prefix — that is the shape
640
+ // b.csp.mergePermissionsPolicy takes, and it keeps the feature name in exactly
641
+ // one place (the registry key) rather than repeated inside its own value.
642
+ var _PAYMENT_ALLOWLIST = '(self "https://js.stripe.com" "https://pay.google.com")';
649
643
 
650
644
  // WebAuthn assertion / attestation run in the page's OWN top-level document
651
645
  // (the islands call navigator.credentials.get / .create directly, never from
@@ -655,8 +649,8 @@ var _PAYMENT_ALLOWLIST = 'payment=(self "https://js.stripe.com" "https://pay.goo
655
649
  // still cannot drive WebAuthn. Single source — every passkey page names the
656
650
  // feature → allowlist mapping here.
657
651
  var _PASSKEY_FEATURE_ALLOWLIST = {
658
- "publickey-credentials-get": "publickey-credentials-get=(self)",
659
- "publickey-credentials-create": "publickey-credentials-create=(self)",
652
+ "publickey-credentials-get": "(self)",
653
+ "publickey-credentials-create": "(self)",
660
654
  };
661
655
 
662
656
  // Feature name → its scoped allowlist token. The pay surface relaxes
@@ -697,12 +691,25 @@ function scopedPermissionsPolicy(opts) {
697
691
  overrides[name] = _SCOPED_FEATURE_OVERRIDES[name];
698
692
  }
699
693
  });
700
- return _vendoredSecurityHeaders.DEFAULT_PERMISSIONS.map(function (entry) {
701
- var feature = entry.split("=")[0];
702
- return Object.prototype.hasOwnProperty.call(overrides, feature)
703
- ? overrides[feature]
704
- : entry;
705
- }).join(", ");
694
+
695
+ // b.csp.mergePermissionsPolicy applies the overrides onto the vendored
696
+ // denylist, replacing a map-and-rejoin over that list here. The registry
697
+ // filter above still runs first, so an unnamed feature cannot be relaxed and
698
+ // an unknown one is dropped rather than passed to a primitive that refuses
699
+ // it — which preserves the documented never-throw behaviour on a render
700
+ // path. A refusal is still possible if a registry VALUE is malformed, and
701
+ // that is an operator configuration error worth surfacing rather than
702
+ // hiding behind a silently strict header.
703
+ try {
704
+ return b.csp.mergePermissionsPolicy(undefined, overrides);
705
+ } catch (e) {
706
+ b.audit.safeEmit({
707
+ action: "shop.security.scopedPermissionsPolicy.refused",
708
+ outcome: "failure",
709
+ metadata: { features: features.join(","), reason: (e && e.message) || "unknown" },
710
+ });
711
+ return _vendoredSecurityHeaders.DEFAULT_PERMISSIONS.join(", ");
712
+ }
706
713
  }
707
714
 
708
715
  /**
@@ -759,7 +766,7 @@ function botGuardOpts() {
759
766
  // directory (e.g. /.well-known/apple-...-association/anything), re-opening
760
767
  // the guard on routes that aren't the single static association file.
761
768
  .concat(PUBLIC_WELL_KNOWN_PATHS.map(function (p) {
762
- return new RegExp("^" + p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&") + "$");
769
+ return new RegExp("^" + b.codepointClass.escapeRegExp(p) + "$");
763
770
  }))
764
771
  .concat([/^\/admin(\/|$)/]),
765
772
  };
@@ -772,6 +779,28 @@ function _hasPrefix(pathname, prefixes) {
772
779
  return false;
773
780
  }
774
781
 
782
+ // Skip matchers for the request-lifecycle guards, built once.
783
+ //
784
+ // b.requestHelpers.makeSkipMatcher takes the REQUEST rather than a pathname
785
+ // the caller derived, which removes three copies of `req.pathname || req.url
786
+ // || "/"` and keeps the exemption correct if a guard is ever mounted somewhere
787
+ // the router has not already set `req.pathname` — the matcher strips the query
788
+ // string itself rather than trusting the caller to have done it.
789
+ //
790
+ // Two matchers because the semantics differ: a webhook path is a PREFIX (the
791
+ // processor may append to it) while the health path is EXACT (`/_/healthz`
792
+ // must not inherit the exemption).
793
+ var _skipWebhookPath = b.requestHelpers.makeSkipMatcher(
794
+ { skipPaths: WEBHOOK_PATHS.slice() }, "securityMiddleware.webhookSkip");
795
+ var _skipHealthPath = b.requestHelpers.makeSkipMatcher(
796
+ { skipPaths: [HEALTH_PATH], exact: true }, "securityMiddleware.healthSkip");
797
+
798
+ // Both guards exempt the same set: a server-to-server webhook authenticated by
799
+ // its own signature, and the liveness probe.
800
+ function _isUnguardedPath(req) {
801
+ return _skipWebhookPath(req) || _skipHealthPath(req);
802
+ }
803
+
775
804
  /**
776
805
  * Mount the per-route tight rate limiters + the fetch-metadata gate on
777
806
  * the router inside the operator's `routes(r)` chain. Call AFTER the
@@ -830,7 +859,7 @@ function mountRouteGuards(r) {
830
859
  });
831
860
  r.use(function csrfGuard(req, res, next) {
832
861
  var pathname = req.pathname || req.url || "/";
833
- if (_hasPrefix(pathname, WEBHOOK_PATHS) || pathname === HEALTH_PATH) return next();
862
+ if (_isUnguardedPath(req)) return next();
834
863
  if (isEdgeExemptPath(pathname)) return next();
835
864
  return csrfGate(req, res, next);
836
865
  });
@@ -852,8 +881,7 @@ function mountRouteGuards(r) {
852
881
  allowedNavigate: true,
853
882
  });
854
883
  r.use(function fetchMetadataGuard(req, res, next) {
855
- var pathname = req.pathname || req.url || "/";
856
- if (_hasPrefix(pathname, WEBHOOK_PATHS)) return next();
884
+ if (_skipWebhookPath(req)) return next();
857
885
  return fmGate(req, res, next);
858
886
  });
859
887
 
@@ -907,7 +935,7 @@ function mountRouteGuards(r) {
907
935
  r.use(function tightRateGuard(req, res, next) {
908
936
  var pathname = req.pathname || req.url || "/";
909
937
  // Never throttle the webhook or health paths.
910
- if (_hasPrefix(pathname, WEBHOOK_PATHS) || pathname === HEALTH_PATH) return next();
938
+ if (_isUnguardedPath(req)) return next();
911
939
  if (!_hasPrefix(pathname, TIGHT_PREFIXES)) return next();
912
940
  // `/products/`, `/gift-cards`, and `/orders/` carry GET reads (PDP,
913
941
  // balance page render, order confirmation page) that a shopper hits
@@ -129,12 +129,9 @@ var MIME_RE = /^[A-Za-z0-9!#$&^_.+\-]{1,127}\/[A-Za-z0-9!#$&^_
129
129
  // the shape vendors / purchase-orders use. Operator-rendered fields
130
130
  // refuse these to keep downstream dashboards / KYC printouts safe
131
131
  // from header-injection + visual-spoofing attacks.
132
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
133
- var ZERO_WIDTH_RE = new RegExp(
134
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
135
- );
136
132
 
137
133
  var b = require("./vendor/blamejs");
134
+ var textGuard = require("./text-guard");
138
135
 
139
136
  // ---- validators ---------------------------------------------------------
140
137
 
@@ -157,12 +154,7 @@ function _businessName(s) {
157
154
  if (s.length > MAX_BUSINESS_NAME_LEN) {
158
155
  throw new TypeError("seller-signup: business_name must be <= " + MAX_BUSINESS_NAME_LEN + " characters");
159
156
  }
160
- if (CONTROL_BYTE_RE.test(s)) {
161
- throw new TypeError("seller-signup: business_name contains control bytes");
162
- }
163
- if (ZERO_WIDTH_RE.test(s)) {
164
- throw new TypeError("seller-signup: business_name contains zero-width / direction-override bytes");
165
- }
157
+ textGuard.freeText(s, "seller-signup: business_name", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
166
158
  return s;
167
159
  }
168
160
 
@@ -219,9 +211,7 @@ function _businessAddress(value) {
219
211
  if (encoded.length > MAX_ADDRESS_JSON_LEN) {
220
212
  throw new TypeError("seller-signup: business_address JSON must be <= " + MAX_ADDRESS_JSON_LEN + " characters serialised");
221
213
  }
222
- if (CONTROL_BYTE_RE.test(encoded) || ZERO_WIDTH_RE.test(encoded)) {
223
- throw new TypeError("seller-signup: business_address contains control / zero-width bytes");
224
- }
214
+ textGuard.freeText(encoded, "seller-signup: business_address", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
225
215
  return encoded;
226
216
  }
227
217
 
@@ -260,7 +250,7 @@ function _categoryFocus(value) {
260
250
  if (typeof value[i] !== "string" || !value[i].length) {
261
251
  throw new TypeError("seller-signup: category_focus[" + i + "] must be a non-empty string");
262
252
  }
263
- if (CONTROL_BYTE_RE.test(value[i]) || ZERO_WIDTH_RE.test(value[i])) {
253
+ if (textGuard.hasCodepointThreat(value[i], { singleLine: "reject", zeroWidth: "reject" })) {
264
254
  throw new TypeError("seller-signup: category_focus[" + i + "] contains control / zero-width bytes");
265
255
  }
266
256
  }
@@ -305,9 +295,7 @@ function _referencesJson(value) {
305
295
  if (encoded.length > MAX_REFERENCES_JSON_LEN) {
306
296
  throw new TypeError("seller-signup: references_json must be <= " + MAX_REFERENCES_JSON_LEN + " characters serialised");
307
297
  }
308
- if (CONTROL_BYTE_RE.test(encoded) || ZERO_WIDTH_RE.test(encoded)) {
309
- throw new TypeError("seller-signup: references_json contains control / zero-width bytes");
310
- }
298
+ textGuard.freeText(encoded, "seller-signup: references_json", { singleLine: "reject", zeroWidth: "reject" });
311
299
  return encoded;
312
300
  }
313
301
 
@@ -325,12 +313,10 @@ function _instructions(s) {
325
313
  if (s.length > MAX_INSTRUCTIONS_LEN) {
326
314
  throw new TypeError("seller-signup: instructions must be <= " + MAX_INSTRUCTIONS_LEN + " characters");
327
315
  }
328
- if (CONTROL_BYTE_RE.test(s.replace(/[\t\n\r]/g, ""))) {
316
+ if (textGuard.hasCodepointThreat(s)) {
329
317
  throw new TypeError("seller-signup: instructions contains control bytes");
330
318
  }
331
- if (ZERO_WIDTH_RE.test(s)) {
332
- throw new TypeError("seller-signup: instructions contains zero-width / direction-override bytes");
333
- }
319
+ textGuard.freeText(s, "seller-signup: instructions", { zeroWidth: "reject", bidiMarks: "allow" });
334
320
  return s;
335
321
  }
336
322
 
@@ -341,12 +327,10 @@ function _rejectReason(s) {
341
327
  if (s.length > MAX_REJECT_REASON_LEN) {
342
328
  throw new TypeError("seller-signup: reason must be <= " + MAX_REJECT_REASON_LEN + " characters");
343
329
  }
344
- if (CONTROL_BYTE_RE.test(s.replace(/[\t\n\r]/g, ""))) {
330
+ if (textGuard.hasCodepointThreat(s)) {
345
331
  throw new TypeError("seller-signup: reason contains control bytes");
346
332
  }
347
- if (ZERO_WIDTH_RE.test(s)) {
348
- throw new TypeError("seller-signup: reason contains zero-width / direction-override bytes");
349
- }
333
+ textGuard.freeText(s, "seller-signup: reason", { zeroWidth: "reject", bidiMarks: "allow" });
350
334
  return s;
351
335
  }
352
336
 
@@ -357,7 +341,7 @@ function _operatorId(s, label) {
357
341
  if (s.length > MAX_OPERATOR_ID_LEN) {
358
342
  throw new TypeError("seller-signup: " + label + " must be <= " + MAX_OPERATOR_ID_LEN + " characters");
359
343
  }
360
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
344
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject", zeroWidth: "reject" })) {
361
345
  throw new TypeError("seller-signup: " + label + " contains control / zero-width bytes");
362
346
  }
363
347
  return s;
@@ -75,6 +75,7 @@
75
75
  */
76
76
 
77
77
  var b = require("./vendor/blamejs");
78
+ var textGuard = require("./text-guard");
78
79
 
79
80
  // ---- constants ----------------------------------------------------------
80
81
 
@@ -125,11 +126,6 @@ var PAGE_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9:._\/-]{0,119}$/;
125
126
  var BADGE_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
126
127
  var SEGMENT_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
127
128
 
128
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
129
- var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
130
- var ZERO_WIDTH_RE = new RegExp(
131
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
132
- );
133
129
 
134
130
  var ALLOWED_PATCH_COLUMNS = Object.freeze([
135
131
  "title",
@@ -176,12 +172,7 @@ function _title(s) {
176
172
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
177
173
  throw new TypeError("sidebarWidgets: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
178
174
  }
179
- if (CONTROL_BYTE_LINE_RE.test(s)) {
180
- throw new TypeError("sidebarWidgets: title must not contain control bytes (incl. CR/LF)");
181
- }
182
- if (ZERO_WIDTH_RE.test(s)) {
183
- throw new TypeError("sidebarWidgets: title must not contain zero-width / direction-override characters");
184
- }
175
+ textGuard.freeText(s, "sidebarWidgets: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
185
176
  return s;
186
177
  }
187
178
 
@@ -260,10 +251,10 @@ function _line(s, label, maxLen) {
260
251
  if (typeof s !== "string" || !s.length || s.length > maxLen) {
261
252
  throw new TypeError("sidebarWidgets: " + label + " must be a non-empty string <= " + maxLen + " chars");
262
253
  }
263
- if (CONTROL_BYTE_LINE_RE.test(s)) {
254
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
264
255
  throw new TypeError("sidebarWidgets: " + label + " must not contain control bytes (incl. CR/LF)");
265
256
  }
266
- if (ZERO_WIDTH_RE.test(s)) {
257
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
267
258
  throw new TypeError("sidebarWidgets: " + label + " must not contain zero-width / direction-override characters");
268
259
  }
269
260
  return s;
@@ -273,10 +264,10 @@ function _block(s, label, maxLen) {
273
264
  if (typeof s !== "string" || !s.length || s.length > maxLen) {
274
265
  throw new TypeError("sidebarWidgets: " + label + " must be a non-empty string <= " + maxLen + " chars");
275
266
  }
276
- if (CONTROL_BYTE_BLOCK_RE.test(s)) {
267
+ if (textGuard.hasCodepointThreat(s)) {
277
268
  throw new TypeError("sidebarWidgets: " + label + " must not contain control bytes");
278
269
  }
279
- if (ZERO_WIDTH_RE.test(s)) {
270
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
280
271
  throw new TypeError("sidebarWidgets: " + label + " must not contain zero-width / direction-override characters");
281
272
  }
282
273
  return s;
@@ -101,18 +101,15 @@ var MAX_REGEX_SOURCE_LEN = 512;
101
101
  var MAX_LIMIT = 1000;
102
102
  var DEFAULT_LIMIT = 100;
103
103
 
104
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
105
104
  // Zero-width + invisible bytes: U+200B (zero-width space),
106
105
  // U+200C (zero-width non-joiner), U+200D (zero-width joiner),
107
106
  // U+2060 (word joiner), U+FEFF (zero-width no-break space / BOM).
108
107
  // Source code uses escape sequences so the file itself stays
109
108
  // linter-clean (raw zero-width bytes in source trip both
110
109
  // no-irregular-whitespace and no-misleading-character-class).
111
- var ZERO_WIDTH_RE = new RegExp(
112
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
113
- );
114
110
 
115
111
  var b = require("./vendor/blamejs");
112
+ var textGuard = require("./text-guard");
116
113
 
117
114
  // Monotonic clock — ensures consecutive _now() calls from the same
118
115
  // process never collide in the same millisecond, so updated_at /
@@ -153,9 +150,7 @@ function _sourcePath(s) {
153
150
  "siteRedirects: source_path must be <= " + MAX_SOURCE_PATH_LEN + " characters"
154
151
  );
155
152
  }
156
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
157
- throw new TypeError("siteRedirects: source_path contains control / zero-width bytes");
158
- }
153
+ textGuard.freeText(s, "siteRedirects: source_path", { singleLine: "reject", zeroWidth: "reject" });
159
154
  if (s.charCodeAt(0) !== 47 /* "/" */) {
160
155
  throw new TypeError("siteRedirects: source_path must be /-rooted (start with '/')");
161
156
  }
@@ -179,9 +174,7 @@ function _targetUrl(s) {
179
174
  "siteRedirects: target_url must be <= " + MAX_TARGET_URL_LEN + " characters"
180
175
  );
181
176
  }
182
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
183
- throw new TypeError("siteRedirects: target_url contains control / zero-width bytes");
184
- }
177
+ textGuard.freeText(s, "siteRedirects: target_url", { singleLine: "reject", zeroWidth: "reject" });
185
178
  if (s.charCodeAt(0) === 47 /* "/" */) {
186
179
  if (s.length > 1 && s.charCodeAt(1) === 47) {
187
180
  throw new TypeError(
@@ -272,9 +265,7 @@ function _compileRegex(source) {
272
265
  "siteRedirects: regex source must be <= " + MAX_REGEX_SOURCE_LEN + " characters"
273
266
  );
274
267
  }
275
- if (CONTROL_BYTE_RE.test(source) || ZERO_WIDTH_RE.test(source)) {
276
- throw new TypeError("siteRedirects: regex source contains control / zero-width bytes");
277
- }
268
+ textGuard.freeText(source, "siteRedirects: regex source", { singleLine: "reject", zeroWidth: "reject" });
278
269
  var anchored = "^(?:" + source + ")$";
279
270
  try {
280
271
  return b.regexLinear.compile(anchored);
@@ -500,9 +491,7 @@ function create(opts) {
500
491
  if (typeof path !== "string" || !path.length) {
501
492
  throw new TypeError("siteRedirects.resolveForPath: path must be a non-empty string");
502
493
  }
503
- if (CONTROL_BYTE_RE.test(path)) {
504
- throw new TypeError("siteRedirects.resolveForPath: path contains control bytes");
505
- }
494
+ textGuard.freeText(path, "siteRedirects.resolveForPath: path", { singleLine: "reject" });
506
495
  var now = _now();
507
496
 
508
497
  // -- exact ----------------------------------------------------------
@@ -107,9 +107,9 @@ var ALLOWED_CHANGEFREQS = Object.freeze([
107
107
  "never",
108
108
  ]);
109
109
 
110
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
111
110
 
112
111
  var b = require("./vendor/blamejs");
112
+ var textGuard = require("./text-guard");
113
113
 
114
114
  // ---- validators ---------------------------------------------------------
115
115
 
@@ -134,9 +134,7 @@ function _basePath(s) {
134
134
  if (typeof s !== "string" || s.length < 1 || s.length > MAX_BASE_PATH_LEN) {
135
135
  throw new TypeError("sitemapGenerator: base_path must be a string 1.." + MAX_BASE_PATH_LEN + " chars");
136
136
  }
137
- if (CONTROL_BYTE_RE.test(s)) {
138
- throw new TypeError("sitemapGenerator: base_path contains control bytes");
139
- }
137
+ textGuard.freeText(s, "sitemapGenerator: base_path", { singleLine: "reject" });
140
138
  if (s.charCodeAt(0) !== 47 /* "/" */) {
141
139
  throw new TypeError("sitemapGenerator: base_path must start with '/'");
142
140
  }
@@ -177,9 +175,7 @@ function _originUrl(u) {
177
175
  if (typeof u !== "string" || u.length < 1 || u.length > MAX_ORIGIN_URL_LEN) {
178
176
  throw new TypeError("sitemapGenerator: origin_url must be a string 1.." + MAX_ORIGIN_URL_LEN + " chars");
179
177
  }
180
- if (CONTROL_BYTE_RE.test(u)) {
181
- throw new TypeError("sitemapGenerator: origin_url contains control bytes");
182
- }
178
+ textGuard.freeText(u, "sitemapGenerator: origin_url", { singleLine: "reject" });
183
179
  try {
184
180
  b.safeUrl.parse(u, { allowedProtocols: ["https:"] });
185
181
  } catch (e) {
@@ -62,6 +62,7 @@
62
62
  */
63
63
 
64
64
  var b = require("./vendor/blamejs");
65
+ var textGuard = require("./text-guard");
65
66
  var C = b.constants;
66
67
 
67
68
  // ---- constants ----------------------------------------------------------
@@ -103,10 +104,6 @@ var SLUG_RE = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
103
104
  var COUNTRY_RE = /^[A-Z]{2}$/;
104
105
  var PHONE_HASH_RE = /^[0-9a-f]{128}$/;
105
106
  var PHONE_NORM_RE = /^\+[1-9][0-9]{6,14}$/; // E.164: +<country><subscriber>, 7-15 digits total
106
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
107
- var ZERO_WIDTH_RE = new RegExp(
108
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
109
- );
110
107
 
111
108
  // Inbound STOP-keyword detection. Operators in the US/UK/AU regulatory
112
109
  // envelope have all converged on this short list; the primitive
@@ -150,9 +147,7 @@ function _name(s) {
150
147
  if (s.length > MAX_NAME_LEN) {
151
148
  throw new TypeError("smsDispatcher: name must be <= " + MAX_NAME_LEN + " characters");
152
149
  }
153
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
154
- throw new TypeError("smsDispatcher: name contains control / zero-width bytes");
155
- }
150
+ textGuard.freeText(s, "smsDispatcher: name", { zeroWidth: "reject", bidiMarks: "allow" });
156
151
  return s;
157
152
  }
158
153
 
@@ -163,9 +158,7 @@ function _endpointUrl(s) {
163
158
  if (s.length > MAX_URL_LEN) {
164
159
  throw new TypeError("smsDispatcher: endpoint_url must be <= " + MAX_URL_LEN + " characters");
165
160
  }
166
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
167
- throw new TypeError("smsDispatcher: endpoint_url contains control / zero-width bytes");
168
- }
161
+ textGuard.freeText(s, "smsDispatcher: endpoint_url", { zeroWidth: "reject" });
169
162
  // Shape gate — refuse anything that isn't an absolute https URL.
170
163
  // Provider endpoints are public APIs; operators using a non-https
171
164
  // egress are leaking the message body across the wire.
@@ -260,7 +253,7 @@ function _reason(s, label) {
260
253
  if (s.length > MAX_REASON_LEN) {
261
254
  throw new TypeError("smsDispatcher: " + label + " must be <= " + MAX_REASON_LEN + " characters");
262
255
  }
263
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
256
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
264
257
  throw new TypeError("smsDispatcher: " + label + " contains control / zero-width bytes");
265
258
  }
266
259
  return s;
@@ -274,9 +267,7 @@ function _optInText(s) {
274
267
  if (s.length > MAX_OPT_IN_TEXT_LEN) {
275
268
  throw new TypeError("smsDispatcher: opt_in_text must be <= " + MAX_OPT_IN_TEXT_LEN + " characters");
276
269
  }
277
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
278
- throw new TypeError("smsDispatcher: opt_in_text contains control / zero-width bytes");
279
- }
270
+ textGuard.freeText(s, "smsDispatcher: opt_in_text", { zeroWidth: "reject", bidiMarks: "allow" });
280
271
  return s;
281
272
  }
282
273
 
@@ -287,9 +278,7 @@ function _providerRef(s) {
287
278
  if (s.length > MAX_PROVIDER_REF_LEN) {
288
279
  throw new TypeError("smsDispatcher: provider_ref must be <= " + MAX_PROVIDER_REF_LEN + " characters");
289
280
  }
290
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
291
- throw new TypeError("smsDispatcher: provider_ref contains control / zero-width bytes");
292
- }
281
+ textGuard.freeText(s, "smsDispatcher: provider_ref", { zeroWidth: "reject" });
293
282
  return s;
294
283
  }
295
284
 
@@ -333,12 +322,7 @@ function _body(s) {
333
322
  if (s.length > MAX_BODY_LEN) {
334
323
  throw new TypeError("smsDispatcher: body must be <= " + MAX_BODY_LEN + " characters");
335
324
  }
336
- if (CONTROL_BYTE_RE.test(s)) {
337
- throw new TypeError("smsDispatcher: body contains control bytes");
338
- }
339
- if (ZERO_WIDTH_RE.test(s)) {
340
- throw new TypeError("smsDispatcher: body contains zero-width / bidi-override codepoints");
341
- }
325
+ textGuard.freeText(s, "smsDispatcher: body", { zeroWidth: "reject", bidiMarks: "allow" });
342
326
  // Refuse the degenerate emoji-only shape. Walk the string by code
343
327
  // point so surrogate pairs in the SMP emoji blocks count as one
344
328
  // character.
@@ -70,6 +70,7 @@
70
70
  */
71
71
 
72
72
  var b = require("./vendor/blamejs");
73
+ var textGuard = require("./text-guard");
73
74
  var C = b.constants;
74
75
 
75
76
  // ---- constants ----------------------------------------------------------
@@ -100,7 +101,6 @@ var LINE_STATES = Object.freeze([
100
101
  "pending", "received", "damaged", "partial",
101
102
  ]);
102
103
 
103
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
104
104
 
105
105
  // ---- monotonic clock ----------------------------------------------------
106
106
  //
@@ -171,9 +171,7 @@ function _reason(s) {
171
171
  if (typeof s !== "string" || !s.length || s.length > MAX_REASON_LEN) {
172
172
  throw new TypeError("stockReceipts: reason must be a non-empty string <= " + MAX_REASON_LEN + " chars");
173
173
  }
174
- if (CONTROL_BYTE_RE.test(s)) {
175
- throw new TypeError("stockReceipts: reason must not contain control bytes");
176
- }
174
+ textGuard.freeText(s, "stockReceipts: reason", { bidiMarks: "allow" });
177
175
  return s;
178
176
  }
179
177
 
@@ -192,9 +190,7 @@ function _uaOpt(s) {
192
190
  if (typeof s !== "string" || s.length > MAX_UA_LEN) {
193
191
  throw new TypeError("stockReceipts: user_agent must be a string <= " + MAX_UA_LEN + " chars");
194
192
  }
195
- if (CONTROL_BYTE_RE.test(s)) {
196
- throw new TypeError("stockReceipts: user_agent must not contain control bytes");
197
- }
193
+ textGuard.freeText(s, "stockReceipts: user_agent");
198
194
  return s;
199
195
  }
200
196
 
@@ -203,9 +199,7 @@ function _ipOpt(s) {
203
199
  if (typeof s !== "string" || s.length > MAX_IP_LEN) {
204
200
  throw new TypeError("stockReceipts: client_ip must be a string <= " + MAX_IP_LEN + " chars");
205
201
  }
206
- if (CONTROL_BYTE_RE.test(s)) {
207
- throw new TypeError("stockReceipts: client_ip must not contain control bytes");
208
- }
202
+ textGuard.freeText(s, "stockReceipts: client_ip");
209
203
  return s;
210
204
  }
211
205
 
@@ -89,6 +89,7 @@
89
89
  */
90
90
 
91
91
  var b = require("./vendor/blamejs");
92
+ var textGuard = require("./text-guard");
92
93
 
93
94
  var MAX_SLUG_LEN = 80;
94
95
  var MAX_TITLE_LEN = 200;
@@ -140,14 +141,10 @@ var WIDGET_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
140
141
  var LOCALE_RE = /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/;
141
142
 
142
143
  // Refuse C0 control bytes + DEL in operator-authored text fields.
143
- var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
144
144
 
145
145
  // Zero-width / direction-override family — mirrors the rest of the
146
146
  // shop primitives. Spelled with \u-escapes so ESLint's
147
147
  // no-irregular-whitespace stays happy.
148
- var ZERO_WIDTH_RE = new RegExp(
149
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
150
- );
151
148
 
152
149
  // ---- validators ---------------------------------------------------------
153
150
 
@@ -166,12 +163,7 @@ function _title(s) {
166
163
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
167
164
  throw new TypeError("storefrontDashboards: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
168
165
  }
169
- if (CONTROL_BYTE_LINE_RE.test(s)) {
170
- throw new TypeError("storefrontDashboards: title contains control bytes (incl. CR/LF)");
171
- }
172
- if (ZERO_WIDTH_RE.test(s)) {
173
- throw new TypeError("storefrontDashboards: title contains zero-width / direction-override characters");
174
- }
166
+ textGuard.freeText(s, "storefrontDashboards: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
175
167
  return s;
176
168
  }
177
169