@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
@@ -94,6 +94,7 @@ function _requireNonEmptyString(val, name) {
94
94
  * sigField: string, // default: "v1"
95
95
  * alg: string, // default: "hmac-sha256" (also "hmac-sha512")
96
96
  * toleranceSec: number, // default: 300 (5 minutes)
97
+ * now: number, // epoch ms for the replay window; default: Date.now()
97
98
  *
98
99
  * @example
99
100
  * var v = b.webhookHmac.verify({
@@ -107,7 +108,8 @@ function verify(opts) {
107
108
  opts = validateOpts.requireObject(opts, "webhookHmac.verify",
108
109
  WebhookHmacError, "webhook-hmac/bad-opts");
109
110
  validateOpts(opts,
110
- ["header", "rawBody", "secret", "profile", "tsField", "sigField", "alg", "toleranceSec"],
111
+ ["header", "rawBody", "secret", "profile", "tsField", "sigField", "alg", "toleranceSec",
112
+ "now"],
111
113
  "webhookHmac.verify");
112
114
 
113
115
  // Resolve profile → field/alg defaults; explicit opts win.
@@ -141,6 +143,21 @@ function verify(opts) {
141
143
  WebhookHmacError, "webhook-hmac/bad-tolerance");
142
144
  var tolerance = typeof opts.toleranceSec === "number" ? opts.toleranceSec : DEFAULT_TOLERANCE_SEC;
143
145
 
146
+ // Replay window measured against an injectable clock (epoch milliseconds,
147
+ // the shape b.cwt.verify already takes), defaulting to wall time. A caller
148
+ // that owns its own time source -- a receiver built around an injected
149
+ // clock, a forensic replay of a captured delivery, a deterministic
150
+ // simulation -- otherwise has to abandon this primitive and rebuild the
151
+ // scheme by hand, since its window check and this one disagree the moment
152
+ // the injected clock is not now.
153
+ //
154
+ // An injected clock rather than a skip-the-check flag: the replay defense
155
+ // stays inside the primitive, and omitting the option falls back to wall
156
+ // time, so forgetting it fails closed instead of silently accepting an
157
+ // arbitrarily old delivery.
158
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.now, "now",
159
+ WebhookHmacError, "webhook-hmac/bad-now");
160
+
144
161
  // Parse "t=<ts>,v1=<sig>,v1=<rotated>" — comma-separated k=v. Collect the ts
145
162
  // and EVERY sigField value; ignore any other version keys.
146
163
  var tsRaw = null;
@@ -169,7 +186,7 @@ function verify(opts) {
169
186
  throw new WebhookHmacError("webhook-hmac/bad-timestamp",
170
187
  "verify: '" + tsField + "' is not a positive integer");
171
188
  }
172
- var nowSec = Math.floor(Date.now() / 1000);
189
+ var nowSec = Math.floor((typeof opts.now === "number" ? opts.now : Date.now()) / 1000);
173
190
  var skew = Math.abs(nowSec - ts);
174
191
  if (skew > tolerance) {
175
192
  throw new WebhookHmacError("webhook-hmac/timestamp-skew",
@@ -485,7 +485,7 @@ function signer(opts) {
485
485
  }, retryOpts);
486
486
  var statusCode = (res && (res.statusCode || res.status)) || 0;
487
487
  _emitEvent("webhook.send", 1, {
488
- outcome: statusCode >= 200 && statusCode < 300 ? "success" : "failure",
488
+ outcome: C.HTTP.success(statusCode) ? "success" : "failure",
489
489
  status: statusCode,
490
490
  host: hostLabel,
491
491
  });
@@ -381,7 +381,23 @@ class WsClient extends EventEmitter {
381
381
  (parsed.protocol === "wss:" ? 443 : 80); // TLS / HTTP default port
382
382
  var host = parsed.hostname;
383
383
 
384
- function _onError(err) { self._handleSocketError(err); }
384
+ // Filled in on the wss:// path below with the options the dial actually
385
+ // used. dialTlsOpts is merged OVER the shared posture, so a caller that
386
+ // narrowed ecdhCurve or moved minVersion negotiated on something the
387
+ // shared object does not describe, and diagnosing against the shared one
388
+ // would answer for a connection that was never made.
389
+ var effectiveTls = null;
390
+
391
+ function _onError(err) {
392
+ // A wss:// handshake refused by the posture arrives as a bare OpenSSL
393
+ // alert; name the cause before it reaches the caller's handler.
394
+ networkTls().annotateOutboundFailure(err, {
395
+ host: host,
396
+ port: port,
397
+ tlsOpts: effectiveTls || {},
398
+ });
399
+ self._handleSocketError(err);
400
+ }
385
401
 
386
402
  // Pin the connect to the SSRF-validated IPs returned by
387
403
  // ssrfGuard.checkUrl — closes the DNS-rebinding TOCTOU window where
@@ -413,6 +429,12 @@ class WsClient extends EventEmitter {
413
429
  port: port,
414
430
  rejectUnauthorized: true,
415
431
  }, networkTls().outboundPosture(), dialTlsOpts || {});
432
+ // The posture advertises RFC 8879 certificate compression, which is a
433
+ // TLS 1.3 extension. A caller who capped this dial lower asked for none
434
+ // of it, and Node refuses the whole options object rather than ignoring
435
+ // an extension it cannot use.
436
+ networkTls()._stripUnreachableCertCompression(tlsOpts, dialTlsOpts);
437
+ effectiveTls = tlsOpts;
416
438
  // RFC 6066 §3: the TLS SNI ServerName MUST be a hostname, never an IP
417
439
  // literal — Node throws ERR_INVALID_ARG_VALUE when servername is an IP,
418
440
  // which made wss:// to any IP-literal target unusable. Send SNI only for
@@ -541,7 +563,7 @@ class WsClient extends EventEmitter {
541
563
  return;
542
564
  }
543
565
  var status = parseInt(match[1], 10);
544
- if (status !== 101) { // HTTP 101
566
+ if (status !== C.HTTP.STATUS.SWITCHING_PROTOCOLS) { // HTTP 101
545
567
  // Body bytes after the header section are the server's
546
568
  // explanation. Surface them on the error so callers can branch
547
569
  // on the status code and inspect the body without re-parsing
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.18.17",
3
+ "version": "0.18.21",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:64b32cc6-e5c4-4968-a1b3-33ca68975bed",
5
+ "serialNumber": "urn:uuid:885de67e-71f9-4512-83e7-3538ecfd9d88",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-08T17:14:55.062Z",
8
+ "timestamp": "2026-08-10T21:28:58.997Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.18.17",
22
+ "bom-ref": "@blamejs/core@0.18.21",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.18.17",
25
+ "version": "0.18.21",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.18.17",
29
+ "purl": "pkg:npm/%40blamejs/core@0.18.21",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.18.17",
57
+ "ref": "@blamejs/core@0.18.21",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]
@@ -95,10 +95,6 @@ var CURRENCY_RE = /^[A-Z]{3}$/;
95
95
  // as the sibling accounts-payable primitives — operator-rendered text
96
96
  // fields refuse these to keep the dashboard + printout safe from
97
97
  // header-injection + visual-spoofing attacks.
98
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
99
- var ZERO_WIDTH_RE = new RegExp(
100
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
101
- );
102
98
 
103
99
  var INVOICE_STATUSES = Object.freeze([
104
100
  "received", "approved", "paid", "disputed", "voided",
@@ -107,6 +103,7 @@ var INVOICE_STATUSES = Object.freeze([
107
103
  var MAX_DUE_WITHIN_DAYS = 3650; // ~10 years — bound the predicate
108
104
 
109
105
  var b = require("./vendor/blamejs");
106
+ var textGuard = require("./text-guard");
110
107
 
111
108
  var MS_PER_DAY = b.constants.TIME.days(1);
112
109
 
@@ -219,10 +216,10 @@ function _shortText(s, label, max) {
219
216
  if (s.length > max) {
220
217
  throw new TypeError("vendor-invoices: " + label + " must be <= " + max + " characters");
221
218
  }
222
- if (CONTROL_BYTE_RE.test(s)) {
219
+ if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
223
220
  throw new TypeError("vendor-invoices: " + label + " contains control bytes");
224
221
  }
225
- if (ZERO_WIDTH_RE.test(s)) {
222
+ if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
226
223
  throw new TypeError("vendor-invoices: " + label + " contains zero-width / direction-override bytes");
227
224
  }
228
225
  return s;
package/lib/vendors.js CHANGED
@@ -97,10 +97,6 @@ var PHONE_RE = /^\+?[1-9]\d{1,14}$/;
97
97
  // direction-override bytes are a slipping-class for header injection
98
98
  // + visual-spoofing attacks downstream. Spelled with \u-escapes so
99
99
  // ESLint's no-irregular-whitespace stays happy.
100
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
101
- var ZERO_WIDTH_RE = new RegExp(
102
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
103
- );
104
100
 
105
101
  // Mutable columns for `updateVendor(slug, patch)`. Slug is immutable
106
102
  // — it's the public handle + the join-table key; mutating it would
@@ -119,6 +115,7 @@ var ALLOWED_UPDATE_COLUMNS = Object.freeze([
119
115
 
120
116
  // Framework handle (the vendored blamejs); index.js re-exports this as .framework.
121
117
  var b = require("./vendor/blamejs");
118
+ var textGuard = require("./text-guard");
122
119
 
123
120
  // ---- validators ---------------------------------------------------------
124
121
 
@@ -159,12 +156,7 @@ function _name(s) {
159
156
  if (s.length > MAX_NAME_LEN) {
160
157
  throw new TypeError("vendors: name must be <= " + MAX_NAME_LEN + " characters");
161
158
  }
162
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
163
- throw new TypeError("vendors: name contains control bytes");
164
- }
165
- if (ZERO_WIDTH_RE.test(s)) {
166
- throw new TypeError("vendors: name contains zero-width / direction-override bytes");
167
- }
159
+ textGuard.freeText(s, "vendors: name", { singleLine: "reject", zeroWidth: "reject" });
168
160
  return s;
169
161
  }
170
162
 
@@ -186,9 +178,7 @@ function _payoutAddress(s) {
186
178
  if (s.length > MAX_PAYOUT_ADDRESS_LEN) {
187
179
  throw new TypeError("vendors: payout_address must be <= " + MAX_PAYOUT_ADDRESS_LEN + " characters");
188
180
  }
189
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
190
- throw new TypeError("vendors: payout_address contains control / zero-width bytes");
191
- }
181
+ textGuard.freeText(s, "vendors: payout_address", { singleLine: "reject", zeroWidth: "reject" });
192
182
  return s;
193
183
  }
194
184
 
@@ -221,9 +211,7 @@ function _addressJson(value) {
221
211
  if (encoded.length > MAX_ADDRESS_JSON_LEN) {
222
212
  throw new TypeError("vendors: address JSON must be <= " + MAX_ADDRESS_JSON_LEN + " characters serialised");
223
213
  }
224
- if (CONTROL_BYTE_STRICT_RE.test(encoded) || ZERO_WIDTH_RE.test(encoded)) {
225
- throw new TypeError("vendors: address contains control / zero-width bytes");
226
- }
214
+ textGuard.freeText(encoded, "vendors: address", { singleLine: "reject", zeroWidth: "reject" });
227
215
  return encoded;
228
216
  }
229
217
 
@@ -265,9 +253,7 @@ function _orderId(s) {
265
253
  if (s.length > 256) {
266
254
  throw new TypeError("vendors: order_id must be <= 256 characters");
267
255
  }
268
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
269
- throw new TypeError("vendors: order_id contains control / zero-width bytes");
270
- }
256
+ textGuard.freeText(s, "vendors: order_id", { singleLine: "reject", zeroWidth: "reject" });
271
257
  return s;
272
258
  }
273
259
 
@@ -44,12 +44,14 @@
44
44
  * - `b.crypto.namespaceHash` — secret-at-rest fingerprint.
45
45
  * - `b.crypto.sha3Hash` — body fingerprint for audit.
46
46
  * - `b.crypto.generateBytes` — secret plaintext draw.
47
- * - `b.crypto.timingSafeEqual` — constant-time hex compare on
48
- * secret-hash + signature match.
49
- * - `b.webhook.sign` HMAC-SHA-256 computation under
50
- * the Stripe-shape algorithm
51
- * (`t=<ts>,v1=<hex>`); we extract
52
- * the hex portion.
47
+ * - `b.crypto.timingSafeEqual` — constant-time hex compare on the
48
+ * secret-hash match.
49
+ * - `b.webhookHmac.verify` the whole `t=<ts>,v1=<hex>` scheme:
50
+ * HMAC-SHA-256 over `<timestamp>.<raw-body>`,
51
+ * the replay window (on this receiver's
52
+ * injected clock and the source row's
53
+ * tolerance), and a constant-time compare
54
+ * across every signature the header carries.
53
55
  * - `b.uuid.v7` — row ids.
54
56
  *
55
57
  * Surface:
@@ -77,6 +79,7 @@
77
79
  */
78
80
 
79
81
  var b = require("./vendor/blamejs");
82
+ var textGuard = require("./text-guard");
80
83
  var C = b.constants;
81
84
 
82
85
  var SECRET_NAMESPACE = "webhook-receiver-secret";
@@ -116,10 +119,6 @@ var MAX_REASON_LEN = 1024;
116
119
 
117
120
  var SIGNATURE_HEX_RE = /^[0-9a-f]{64}$/i; // 32 bytes of HMAC-SHA-256 = 64 hex chars
118
121
 
119
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
120
- var ZERO_WIDTH_RE = new RegExp(
121
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
122
- );
123
122
 
124
123
  var STATUSES = ["received", "processed", "failed"];
125
124
 
@@ -196,9 +195,7 @@ function _outcome(s) {
196
195
  if (s.length > MAX_OUTCOME_LEN) {
197
196
  throw new TypeError("webhookReceiver: outcome must be <= " + MAX_OUTCOME_LEN + " characters");
198
197
  }
199
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
200
- throw new TypeError("webhookReceiver: outcome contains control / zero-width bytes");
201
- }
198
+ textGuard.freeText(s, "webhookReceiver: outcome", { singleLine: "reject", zeroWidth: "reject" });
202
199
  return s;
203
200
  }
204
201
 
@@ -213,9 +210,7 @@ function _reason(s) {
213
210
  if (s.length > MAX_REASON_LEN) {
214
211
  throw new TypeError("webhookReceiver: reason must be <= " + MAX_REASON_LEN + " characters");
215
212
  }
216
- if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
217
- throw new TypeError("webhookReceiver: reason contains control / zero-width bytes");
218
- }
213
+ textGuard.freeText(s, "webhookReceiver: reason", { singleLine: "reject", zeroWidth: "reject" });
219
214
  return s;
220
215
  }
221
216
 
@@ -305,34 +300,6 @@ function _hashSecret(canonical) {
305
300
  return b.crypto.namespaceHash(SECRET_NAMESPACE, canonical);
306
301
  }
307
302
 
308
- // Extract the v1 hex tag from a `t=<ts>,v1=<hex>` Stripe-shape header
309
- // produced by b.webhook.sign. We pin to this composition rather than
310
- // reach for node:crypto so the receiver inherits whatever HMAC-SHA-256
311
- // hardening lives in the vendored blamejs surface.
312
- function _hmacSha256Hex(secretPlaintext, timestampSeconds, bodyString) {
313
- var header = b.webhook.sign({
314
- alg: "hmac-sha256-stripe",
315
- secret: secretPlaintext,
316
- body: bodyString,
317
- timestamp: timestampSeconds,
318
- });
319
- // Header shape is `t=<ts>,v1=<hex>` — the marker is the only place
320
- // ",v1=" appears so a literal indexOf is the canonical parse.
321
- var marker = ",v1=";
322
- var i = header.indexOf(marker);
323
- if (i < 0) {
324
- // Defensive: a future blamejs version that changes the shape
325
- // would slip past silently if we returned an empty string. Throw
326
- // loudly so the operator notices at deploy time, not at the
327
- // first inbound webhook.
328
- throw new Error(
329
- "webhookReceiver: b.webhook.sign returned an unexpected header shape " +
330
- "(no ',v1=' marker) — vendored blamejs may need a refresh"
331
- );
332
- }
333
- return header.slice(i + marker.length);
334
- }
335
-
336
303
  // ---- factory ------------------------------------------------------------
337
304
 
338
305
  function create(opts) {
@@ -610,40 +577,9 @@ function create(opts) {
610
577
  timestamp_out_of_window: false,
611
578
  };
612
579
  }
613
- var timestampSeconds = parseInt(tsStr, 10);
614
- if (!Number.isFinite(timestampSeconds) || timestampSeconds < 0) {
615
- return {
616
- ok: false,
617
- event_id: null,
618
- replay: false,
619
- signature_invalid: true,
620
- timestamp_out_of_window: false,
621
- };
622
- }
623
-
624
- // Replay-window check. The third-party's timestamp must land
625
- // within ±replay_window_seconds of the receiver's current
626
- // wall-clock. We accept future-skewed timestamps inside the
627
- // window (a peer with a slightly fast clock shouldn't reject)
628
- // but not arbitrarily-future timestamps that would let an
629
- // attacker bypass replay protection by stamping a fresh ts.
630
- var windowSec = Number(sourceRow.replay_window_seconds);
631
- var nowSec = Math.floor(nowMs / 1000);
632
- var ageSec = Math.abs(nowSec - timestampSeconds);
633
- if (ageSec > windowSec) {
634
- return {
635
- ok: false,
636
- event_id: null,
637
- replay: false,
638
- signature_invalid: false,
639
- timestamp_out_of_window: true,
640
- };
641
- }
642
-
643
- // Signature validation. The third-party signs the literal
644
- // `<timestamp>.<body>` string with HMAC-SHA-256 under the
645
- // secret. We recompute via b.webhook.sign and constant-time-
646
- // compare the supplied hex against the expected hex.
580
+ // The signature must be exactly the hex digest before it is composed
581
+ // into the header below a value carrying a comma or an `=` would
582
+ // otherwise add fields the parser reads as its own.
647
583
  var supplied = input.signature_header.trim().toLowerCase();
648
584
  if (!SIGNATURE_HEX_RE.test(supplied)) {
649
585
  return {
@@ -654,15 +590,42 @@ function create(opts) {
654
590
  timestamp_out_of_window: false,
655
591
  };
656
592
  }
657
- var expected = _hmacSha256Hex(matched.plaintext, timestampSeconds, bodyBytes.toString("utf8"));
658
- var sigOk = b.crypto.timingSafeEqual(supplied, expected);
659
- if (!sigOk) {
593
+
594
+ // Signature + replay window, both from b.webhookHmac. It owns this exact
595
+ // scheme — `t=<ts>,v1=<hex>` over `<timestamp>.<raw-body>` — including
596
+ // the strict-integer timestamp, the skew window, a constant-time compare
597
+ // and multi-signature support for a rotated secret. This receiver takes
598
+ // its clock from the operator (opts.now) and its window from the source
599
+ // row, so both are passed in; the primitive gained the clock argument in
600
+ // 0.18.18, which is what made composing it possible at all.
601
+ //
602
+ // The body goes in as BYTES. Signing a UTF-8 string rebuild of it
603
+ // replaces anything that is not valid UTF-8 with U+FFFD, taking the
604
+ // digest over different bytes than the sender signed and rejecting a
605
+ // correctly-signed delivery. The timestamp goes in as the raw header
606
+ // text rather than a reparsed integer for the same reason.
607
+ var windowSec = Number(sourceRow.replay_window_seconds);
608
+ try {
609
+ b.webhookHmac.verify({
610
+ header: "t=" + tsStr + ",v1=" + supplied,
611
+ rawBody: bodyBytes,
612
+ secret: matched.plaintext,
613
+ profile: "stripe",
614
+ toleranceSec: windowSec,
615
+ now: nowMs,
616
+ });
617
+ } catch (e) {
618
+ // Out-of-window and bad-signature are distinct outcomes to an operator
619
+ // reading the delivery log: one is a clock or a replay, the other is a
620
+ // wrong secret. Every other refusal is a malformed header, which is
621
+ // not a signature the receiver can accept.
622
+ var skew = e && e.code === "webhook-hmac/timestamp-skew";
660
623
  return {
661
624
  ok: false,
662
625
  event_id: null,
663
626
  replay: false,
664
- signature_invalid: true,
665
- timestamp_out_of_window: false,
627
+ signature_invalid: !skew,
628
+ timestamp_out_of_window: skew,
666
629
  };
667
630
  }
668
631
 
@@ -68,10 +68,6 @@ var WILDCARD_EVENT = "*";
68
68
  // control bytes are a slipping-class for header injection + visual
69
69
  // spoofing downstream. Spelled with \u-escapes so ESLint's
70
70
  // no-irregular-whitespace stays happy.
71
- var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
72
- var ZERO_WIDTH_RE = new RegExp(
73
- "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
74
- );
75
71
 
76
72
  // Event-type identifier shape — `domain.action[.qualifier]` segments
77
73
  // of ASCII letters / digits / underscore, joined by `.`. The wildcard
@@ -111,9 +107,7 @@ function _ownerId(s) {
111
107
  if (s.length > MAX_OWNER_ID_LEN) {
112
108
  throw new TypeError("webhookSubscriptions: owner_id must be <= " + MAX_OWNER_ID_LEN + " characters");
113
109
  }
114
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
115
- throw new TypeError("webhookSubscriptions: owner_id contains control / zero-width bytes");
116
- }
110
+ textGuard.freeText(s, "webhookSubscriptions: owner_id", { singleLine: "reject", zeroWidth: "reject" });
117
111
  return s;
118
112
  }
119
113
 
@@ -191,9 +185,7 @@ function _name(s) {
191
185
  if (s.length > MAX_NAME_LEN) {
192
186
  throw new TypeError("webhookSubscriptions: name must be <= " + MAX_NAME_LEN + " characters");
193
187
  }
194
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
195
- throw new TypeError("webhookSubscriptions: name contains control / zero-width bytes");
196
- }
188
+ textGuard.freeText(s, "webhookSubscriptions: name", { singleLine: "reject", zeroWidth: "reject" });
197
189
  return s;
198
190
  }
199
191
 
@@ -219,9 +211,7 @@ function _signingSecret(s) {
219
211
  if (s.length > 512) {
220
212
  throw new TypeError("webhookSubscriptions: signing_secret must be <= 512 characters");
221
213
  }
222
- if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
223
- throw new TypeError("webhookSubscriptions: signing_secret contains control / zero-width bytes");
224
- }
214
+ textGuard.freeText(s, "webhookSubscriptions: signing_secret", { singleLine: "reject", zeroWidth: "reject" });
225
215
  return s;
226
216
  }
227
217
 
@@ -63,6 +63,7 @@
63
63
  */
64
64
 
65
65
  var b = require("./vendor/blamejs");
66
+ var textGuard = require("./text-guard");
66
67
  var wishlistModule = require("./wishlist");
67
68
 
68
69
  // ---- constants ----------------------------------------------------------
@@ -82,8 +83,6 @@ var MAX_REASON_LEN = 500;
82
83
  var MAX_EMAIL_LEN = 254;
83
84
  var MAX_MEMBER_EMAILS = 50;
84
85
 
85
- var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
86
- var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
87
86
 
88
87
  // Conservative single-line email shape — defence in depth against
89
88
  // header-injection / control-byte smuggling when the operator
@@ -127,7 +126,7 @@ function _title(s, label) {
127
126
  if (typeof s !== "string" || s.length > MAX_TITLE_LEN) {
128
127
  throw new TypeError("wishlistSharing: " + label + " must be a string <= " + MAX_TITLE_LEN + " chars");
129
128
  }
130
- if (CONTROL_BYTE_RE.test(s)) {
129
+ if (textGuard.hasCodepointThreat(s)) {
131
130
  throw new TypeError("wishlistSharing: " + label + " must not contain control bytes");
132
131
  }
133
132
  return s;
@@ -137,7 +136,7 @@ function _requiredTitle(s, label) {
137
136
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
138
137
  throw new TypeError("wishlistSharing: " + label + " must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
139
138
  }
140
- if (CONTROL_BYTE_RE.test(s)) {
139
+ if (textGuard.hasCodepointThreat(s)) {
141
140
  throw new TypeError("wishlistSharing: " + label + " must not contain control bytes");
142
141
  }
143
142
  return s;
@@ -162,9 +161,7 @@ function _reason(s) {
162
161
  if (typeof s !== "string" || !s.length || s.length > MAX_REASON_LEN) {
163
162
  throw new TypeError("wishlistSharing: reason must be a non-empty string <= " + MAX_REASON_LEN + " chars");
164
163
  }
165
- if (CONTROL_BYTE_RE.test(s)) {
166
- throw new TypeError("wishlistSharing: reason must not contain control bytes");
167
- }
164
+ textGuard.freeText(s, "wishlistSharing: reason");
168
165
  return s;
169
166
  }
170
167
 
@@ -205,9 +202,7 @@ function _viewerSessionId(s) {
205
202
  // Session ids are opaque to this primitive; refuse control bytes
206
203
  // so the hashed breadcrumb has no log-injection vector even if a
207
204
  // downstream tool ever rendered the raw value.
208
- if (CONTROL_BYTE_STRICT_RE.test(s)) {
209
- throw new TypeError("wishlistSharing: viewer_session_id must not contain control bytes");
210
- }
205
+ textGuard.freeText(s, "wishlistSharing: viewer_session_id", { singleLine: "reject" });
211
206
  return s;
212
207
  }
213
208
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.5.19",
3
+ "version": "0.5.21",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {