@blamejs/blamejs-shop 0.5.21 → 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 (70) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +1 -1
  3. package/SECURITY.md +7 -1
  4. package/lib/addresses.js +16 -2
  5. package/lib/affiliates.js +3 -3
  6. package/lib/announcement-bar.js +2 -2
  7. package/lib/api-keys.js +2 -2
  8. package/lib/asset-manifest.json +1 -1
  9. package/lib/blog-articles.js +2 -2
  10. package/lib/carrier-accounts.js +2 -2
  11. package/lib/catalog-drafts.js +1 -1
  12. package/lib/category-navigation.js +6 -3
  13. package/lib/compliance-export.js +8 -1
  14. package/lib/customer-notes.js +1 -1
  15. package/lib/customer-roles.js +1 -1
  16. package/lib/customer-surveys.js +2 -2
  17. package/lib/customers.js +1 -1
  18. package/lib/dispute-resolution.js +3 -3
  19. package/lib/dropship-forwarding.js +1 -1
  20. package/lib/dunning.js +1 -1
  21. package/lib/email-ab-tests.js +1 -1
  22. package/lib/email-campaigns.js +15 -2
  23. package/lib/email-warmup.js +1 -1
  24. package/lib/externaldb-d1.js +0 -8
  25. package/lib/gift-options.js +3 -3
  26. package/lib/knowledge-base.js +3 -3
  27. package/lib/line-gift-wrap.js +6 -2
  28. package/lib/live-chat.js +1 -1
  29. package/lib/notifications.js +1 -1
  30. package/lib/operator-accounts.js +1 -1
  31. package/lib/operator-help-center.js +3 -3
  32. package/lib/operator-inbox.js +2 -2
  33. package/lib/operator-roles.js +1 -1
  34. package/lib/order-notes.js +2 -2
  35. package/lib/order-ratings.js +3 -3
  36. package/lib/payment-methods.js +1 -1
  37. package/lib/payment-retries.js +1 -1
  38. package/lib/pixel-events.js +14 -6
  39. package/lib/plan-changes.js +1 -1
  40. package/lib/product-qa.js +1 -1
  41. package/lib/push-notifications.js +2 -2
  42. package/lib/reviews.js +3 -3
  43. package/lib/seller-signup.js +4 -4
  44. package/lib/sidebar-widgets.js +1 -1
  45. package/lib/sms-dispatcher.js +3 -3
  46. package/lib/stock-receipts.js +1 -1
  47. package/lib/storefront-dashboards.js +1 -1
  48. package/lib/storefront-forms.js +1 -1
  49. package/lib/storefront-pages.js +2 -2
  50. package/lib/subscription-billing.js +1 -1
  51. package/lib/subscription-controls.js +1 -1
  52. package/lib/suggestion-box.js +4 -4
  53. package/lib/support-tickets.js +3 -3
  54. package/lib/tenants.js +1 -1
  55. package/lib/text-guard.js +44 -2
  56. package/lib/theme-assets.js +1 -1
  57. package/lib/vendor/MANIFEST.json +12 -12
  58. package/lib/vendor/blamejs/CHANGELOG.md +32 -0
  59. package/lib/vendor/blamejs/NOTICE +1 -1
  60. package/lib/vendor/blamejs/README.md +1 -1
  61. package/lib/vendor/blamejs/lib/network-tls.js +98 -13
  62. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +12 -12
  63. package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +568 -304
  64. package/lib/vendor/blamejs/package.json +1 -1
  65. package/lib/vendor/blamejs/sbom.cdx.json +6 -6
  66. package/lib/vendors.js +3 -3
  67. package/lib/webhook-receiver.js +1 -1
  68. package/lib/webhook-subscriptions.js +1 -1
  69. package/lib/wishlist-sharing.js +1 -1
  70. package/package.json +1 -1
@@ -164,7 +164,7 @@ function _title(s) {
164
164
  if (s.length > MAX_TITLE_LEN) {
165
165
  throw new TypeError("operatorHelpCenter: title must be <= " + MAX_TITLE_LEN + " characters");
166
166
  }
167
- textGuard.freeText(s, "operatorHelpCenter: title", { singleLine: "reject", zeroWidth: "reject" });
167
+ textGuard.freeText(s, "operatorHelpCenter: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
168
168
  return s;
169
169
  }
170
170
 
@@ -179,7 +179,7 @@ function _body(s) {
179
179
  if (s.length > MAX_BODY_LEN) {
180
180
  throw new TypeError("operatorHelpCenter: body must be <= " + MAX_BODY_LEN + " characters");
181
181
  }
182
- textGuard.freeText(s, "operatorHelpCenter: body", { zeroWidth: "reject" });
182
+ textGuard.freeText(s, "operatorHelpCenter: body", { zeroWidth: "reject", bidiMarks: "allow" });
183
183
  return s;
184
184
  }
185
185
 
@@ -290,7 +290,7 @@ function _queryStr(s) {
290
290
  if (s.length > MAX_QUERY_LEN) {
291
291
  throw new TypeError("operatorHelpCenter: query must be <= " + MAX_QUERY_LEN + " characters");
292
292
  }
293
- textGuard.freeText(s, "operatorHelpCenter: query", { zeroWidth: "reject" });
293
+ textGuard.freeText(s, "operatorHelpCenter: query", { zeroWidth: "reject", bidiMarks: "allow" });
294
294
  return s;
295
295
  }
296
296
 
@@ -248,7 +248,7 @@ function _subject(s) {
248
248
  if (typeof s !== "string" || !s.length || s.length > MAX_SUBJECT_LEN) {
249
249
  throw new TypeError("operatorInbox: subject must be a non-empty string <= " + MAX_SUBJECT_LEN + " chars");
250
250
  }
251
- textGuard.freeText(s, "operatorInbox: subject");
251
+ textGuard.freeText(s, "operatorInbox: subject", { bidiMarks: "allow" });
252
252
  return s;
253
253
  }
254
254
 
@@ -256,7 +256,7 @@ function _body(s) {
256
256
  if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
257
257
  throw new TypeError("operatorInbox: body must be a non-empty string <= " + MAX_BODY_LEN + " chars");
258
258
  }
259
- textGuard.freeText(s, "operatorInbox: body");
259
+ textGuard.freeText(s, "operatorInbox: body", { bidiMarks: "allow" });
260
260
  return s;
261
261
  }
262
262
 
@@ -185,7 +185,7 @@ function _title(s) {
185
185
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
186
186
  throw new TypeError("operatorRoles: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
187
187
  }
188
- textGuard.freeText(s, "operatorRoles: title", { singleLine: "reject" });
188
+ textGuard.freeText(s, "operatorRoles: title", { singleLine: "reject", bidiMarks: "allow" });
189
189
  return s;
190
190
  }
191
191
 
@@ -95,7 +95,7 @@ function _body(s) {
95
95
  if (s.length > MAX_BODY_LEN) {
96
96
  throw new TypeError("orderNotes: body must be <= " + MAX_BODY_LEN + " characters");
97
97
  }
98
- textGuard.freeText(s, "orderNotes: body", { zeroWidth: "reject" });
98
+ textGuard.freeText(s, "orderNotes: body", { zeroWidth: "reject", bidiMarks: "allow" });
99
99
  return s;
100
100
  }
101
101
 
@@ -131,7 +131,7 @@ function _resolution(s) {
131
131
  if (s.length > MAX_RESOLUTION_LEN) {
132
132
  throw new TypeError("orderNotes: resolution must be <= " + MAX_RESOLUTION_LEN + " characters");
133
133
  }
134
- textGuard.freeText(s, "orderNotes: resolution", { zeroWidth: "reject" });
134
+ textGuard.freeText(s, "orderNotes: resolution", { zeroWidth: "reject", bidiMarks: "allow" });
135
135
  return s;
136
136
  }
137
137
 
@@ -130,7 +130,7 @@ function _comment(s) {
130
130
  if (s.length > MAX_COMMENT_LEN) {
131
131
  throw new TypeError("orderRatings: comment must be <= " + MAX_COMMENT_LEN + " chars");
132
132
  }
133
- textGuard.freeText(s, "orderRatings: comment");
133
+ textGuard.freeText(s, "orderRatings: comment", { bidiMarks: "allow" });
134
134
  return s;
135
135
  }
136
136
 
@@ -138,7 +138,7 @@ function _flagReason(s) {
138
138
  if (typeof s !== "string" || !s.length || s.length > MAX_FLAG_REASON_LEN) {
139
139
  throw new TypeError("orderRatings: reason must be a non-empty string <= " + MAX_FLAG_REASON_LEN + " chars");
140
140
  }
141
- textGuard.freeText(s, "orderRatings: reason");
141
+ textGuard.freeText(s, "orderRatings: reason", { bidiMarks: "allow" });
142
142
  return s;
143
143
  }
144
144
 
@@ -146,7 +146,7 @@ function _responseText(s) {
146
146
  if (typeof s !== "string" || !s.length || s.length > MAX_RESPONSE_LEN) {
147
147
  throw new TypeError("orderRatings: response must be a non-empty string <= " + MAX_RESPONSE_LEN + " chars");
148
148
  }
149
- textGuard.freeText(s, "orderRatings: response");
149
+ textGuard.freeText(s, "orderRatings: response", { bidiMarks: "allow" });
150
150
  return s;
151
151
  }
152
152
 
@@ -140,7 +140,7 @@ function _label(s) {
140
140
  if (s.length > MAX_LABEL_LEN) {
141
141
  throw new TypeError("paymentMethods: label must be <= " + MAX_LABEL_LEN + " characters");
142
142
  }
143
- textGuard.freeText(s, "paymentMethods: label", { singleLine: "reject" });
143
+ textGuard.freeText(s, "paymentMethods: label", { singleLine: "reject", bidiMarks: "allow" });
144
144
  return s;
145
145
  }
146
146
 
@@ -229,7 +229,7 @@ function _reason(s) {
229
229
  if (s.length > MAX_REASON_LEN) {
230
230
  throw new TypeError("paymentRetries: reason must be <= " + MAX_REASON_LEN + " characters");
231
231
  }
232
- textGuard.freeText(s, "paymentRetries: reason", { zeroWidth: "reject" });
232
+ textGuard.freeText(s, "paymentRetries: reason", { zeroWidth: "reject", bidiMarks: "allow" });
233
233
  return s;
234
234
  }
235
235
 
@@ -403,12 +403,20 @@ function _responseBody(s) {
403
403
  if (s.length > MAX_RESPONSE_BODY_LEN) {
404
404
  throw new TypeError("pixelEvents: response_body must be <= " + MAX_RESPONSE_BODY_LEN + " characters");
405
405
  }
406
- // Response bodies from upstream providers are operator-observed
407
- // already (the dispatch hook captured them); allow the full ASCII
408
- // range minus the zero-width / bidi block so a malicious provider
409
- // can't smuggle invisible bytes through the audit trail.
410
- textGuard.freeText(s, "pixelEvents: response_body", { zeroWidth: "reject" });
411
- return s;
406
+ // SCRUBBED, never refused. This string is whatever an upstream provider sent
407
+ // back, and `markDispatched` runs this validator BEFORE it transitions the
408
+ // event so any refusal here leaves the row in `queued` and the scheduler
409
+ // re-sends the identical pixel on every tick, forever, because we disliked a
410
+ // byte in the reply. A provider must never be able to hold an event open by
411
+ // choosing its response body.
412
+ //
413
+ // The interest in this field is only that an operator reading the audit
414
+ // trail is not shown invisible characters. Removing them satisfies that
415
+ // without ever failing: the bidi overrides, the direction marks, the
416
+ // zero-width family and the Unicode Tags block all come out, and everything
417
+ // legible — including the tabs and newlines a real response is full of —
418
+ // stays exactly as the provider sent it.
419
+ return textGuard.scrubInvisible(s);
412
420
  }
413
421
 
414
422
  function _responseStatus(n) {
@@ -115,7 +115,7 @@ function _optReason(s) {
115
115
  if (s.length > MAX_REASON_LEN) {
116
116
  throw new TypeError("planChanges: reason must be <= " + MAX_REASON_LEN + " characters");
117
117
  }
118
- textGuard.freeText(s, "planChanges: reason", { zeroWidth: "reject" });
118
+ textGuard.freeText(s, "planChanges: reason", { zeroWidth: "reject", bidiMarks: "allow" });
119
119
  return s;
120
120
  }
121
121
 
package/lib/product-qa.js CHANGED
@@ -131,7 +131,7 @@ function _reason(s) {
131
131
  if (s.length > MAX_REASON_LEN) {
132
132
  throw new TypeError("productQA: reason must be <= " + MAX_REASON_LEN + " characters");
133
133
  }
134
- textGuard.freeText(s, "productQA: reason");
134
+ textGuard.freeText(s, "productQA: reason", { bidiMarks: "allow" });
135
135
  return s;
136
136
  }
137
137
 
@@ -195,7 +195,7 @@ function _title(s) {
195
195
  if (s.length > MAX_TITLE_LEN) {
196
196
  throw new TypeError("pushNotifications: title must be <= " + MAX_TITLE_LEN + " characters");
197
197
  }
198
- textGuard.freeText(s, "pushNotifications: title", { zeroWidth: "reject" });
198
+ textGuard.freeText(s, "pushNotifications: title", { zeroWidth: "reject", bidiMarks: "allow" });
199
199
  return s;
200
200
  }
201
201
 
@@ -206,7 +206,7 @@ function _body(s) {
206
206
  if (s.length > MAX_BODY_LEN) {
207
207
  throw new TypeError("pushNotifications: body must be <= " + MAX_BODY_LEN + " characters");
208
208
  }
209
- textGuard.freeText(s, "pushNotifications: body", { zeroWidth: "reject" });
209
+ textGuard.freeText(s, "pushNotifications: body", { zeroWidth: "reject", bidiMarks: "allow" });
210
210
  return s;
211
211
  }
212
212
 
package/lib/reviews.js CHANGED
@@ -75,7 +75,7 @@ function _title(s) {
75
75
  if (s.length > MAX_TITLE_LEN) {
76
76
  throw new TypeError("reviews: title must be <= " + MAX_TITLE_LEN + " characters");
77
77
  }
78
- textGuard.freeText(s, "reviews: title");
78
+ textGuard.freeText(s, "reviews: title", { bidiMarks: "allow" });
79
79
  return s;
80
80
  }
81
81
 
@@ -87,7 +87,7 @@ function _body(s) {
87
87
  if (s.length > MAX_BODY_LEN) {
88
88
  throw new TypeError("reviews: body must be <= " + MAX_BODY_LEN + " characters");
89
89
  }
90
- textGuard.freeText(s, "reviews: body");
90
+ textGuard.freeText(s, "reviews: body", { bidiMarks: "allow" });
91
91
  return s;
92
92
  }
93
93
 
@@ -284,7 +284,7 @@ function create(opts) {
284
284
  if (reason.length > MAX_REJECT_REASON_LEN) {
285
285
  throw new TypeError("reviews.reject: reason must be <= " + MAX_REJECT_REASON_LEN + " characters");
286
286
  }
287
- textGuard.freeText(reason, "reviews.reject: reason");
287
+ textGuard.freeText(reason, "reviews.reject: reason", { bidiMarks: "allow" });
288
288
  }
289
289
  var ts = _now();
290
290
  var existing = (await query("SELECT id, status FROM reviews WHERE id = ?1", [id])).rows[0];
@@ -154,7 +154,7 @@ function _businessName(s) {
154
154
  if (s.length > MAX_BUSINESS_NAME_LEN) {
155
155
  throw new TypeError("seller-signup: business_name must be <= " + MAX_BUSINESS_NAME_LEN + " characters");
156
156
  }
157
- textGuard.freeText(s, "seller-signup: business_name", { singleLine: "reject", zeroWidth: "reject" });
157
+ textGuard.freeText(s, "seller-signup: business_name", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
158
158
  return s;
159
159
  }
160
160
 
@@ -211,7 +211,7 @@ function _businessAddress(value) {
211
211
  if (encoded.length > MAX_ADDRESS_JSON_LEN) {
212
212
  throw new TypeError("seller-signup: business_address JSON must be <= " + MAX_ADDRESS_JSON_LEN + " characters serialised");
213
213
  }
214
- textGuard.freeText(encoded, "seller-signup: business_address", { singleLine: "reject", zeroWidth: "reject" });
214
+ textGuard.freeText(encoded, "seller-signup: business_address", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
215
215
  return encoded;
216
216
  }
217
217
 
@@ -316,7 +316,7 @@ function _instructions(s) {
316
316
  if (textGuard.hasCodepointThreat(s)) {
317
317
  throw new TypeError("seller-signup: instructions contains control bytes");
318
318
  }
319
- textGuard.freeText(s, "seller-signup: instructions", { zeroWidth: "reject" });
319
+ textGuard.freeText(s, "seller-signup: instructions", { zeroWidth: "reject", bidiMarks: "allow" });
320
320
  return s;
321
321
  }
322
322
 
@@ -330,7 +330,7 @@ function _rejectReason(s) {
330
330
  if (textGuard.hasCodepointThreat(s)) {
331
331
  throw new TypeError("seller-signup: reason contains control bytes");
332
332
  }
333
- textGuard.freeText(s, "seller-signup: reason", { zeroWidth: "reject" });
333
+ textGuard.freeText(s, "seller-signup: reason", { zeroWidth: "reject", bidiMarks: "allow" });
334
334
  return s;
335
335
  }
336
336
 
@@ -172,7 +172,7 @@ function _title(s) {
172
172
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
173
173
  throw new TypeError("sidebarWidgets: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
174
174
  }
175
- textGuard.freeText(s, "sidebarWidgets: title", { singleLine: "reject", zeroWidth: "reject" });
175
+ textGuard.freeText(s, "sidebarWidgets: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
176
176
  return s;
177
177
  }
178
178
 
@@ -147,7 +147,7 @@ function _name(s) {
147
147
  if (s.length > MAX_NAME_LEN) {
148
148
  throw new TypeError("smsDispatcher: name must be <= " + MAX_NAME_LEN + " characters");
149
149
  }
150
- textGuard.freeText(s, "smsDispatcher: name", { zeroWidth: "reject" });
150
+ textGuard.freeText(s, "smsDispatcher: name", { zeroWidth: "reject", bidiMarks: "allow" });
151
151
  return s;
152
152
  }
153
153
 
@@ -267,7 +267,7 @@ function _optInText(s) {
267
267
  if (s.length > MAX_OPT_IN_TEXT_LEN) {
268
268
  throw new TypeError("smsDispatcher: opt_in_text must be <= " + MAX_OPT_IN_TEXT_LEN + " characters");
269
269
  }
270
- textGuard.freeText(s, "smsDispatcher: opt_in_text", { zeroWidth: "reject" });
270
+ textGuard.freeText(s, "smsDispatcher: opt_in_text", { zeroWidth: "reject", bidiMarks: "allow" });
271
271
  return s;
272
272
  }
273
273
 
@@ -322,7 +322,7 @@ function _body(s) {
322
322
  if (s.length > MAX_BODY_LEN) {
323
323
  throw new TypeError("smsDispatcher: body must be <= " + MAX_BODY_LEN + " characters");
324
324
  }
325
- textGuard.freeText(s, "smsDispatcher: body", { zeroWidth: "reject" });
325
+ textGuard.freeText(s, "smsDispatcher: body", { zeroWidth: "reject", bidiMarks: "allow" });
326
326
  // Refuse the degenerate emoji-only shape. Walk the string by code
327
327
  // point so surrogate pairs in the SMP emoji blocks count as one
328
328
  // character.
@@ -171,7 +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
- textGuard.freeText(s, "stockReceipts: reason");
174
+ textGuard.freeText(s, "stockReceipts: reason", { bidiMarks: "allow" });
175
175
  return s;
176
176
  }
177
177
 
@@ -163,7 +163,7 @@ function _title(s) {
163
163
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
164
164
  throw new TypeError("storefrontDashboards: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
165
165
  }
166
- textGuard.freeText(s, "storefrontDashboards: title", { singleLine: "reject", zeroWidth: "reject" });
166
+ textGuard.freeText(s, "storefrontDashboards: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
167
167
  return s;
168
168
  }
169
169
 
@@ -180,7 +180,7 @@ function _description(s) {
180
180
  if (typeof s !== "string" || s.length > MAX_DESCRIPTION_LEN) {
181
181
  throw new TypeError("storefrontForms: description must be a string ≤ " + MAX_DESCRIPTION_LEN + " chars");
182
182
  }
183
- textGuard.freeText(s, "storefrontForms: description", { zeroWidth: "reject" });
183
+ textGuard.freeText(s, "storefrontForms: description", { zeroWidth: "reject", bidiMarks: "allow" });
184
184
  return s;
185
185
  }
186
186
 
@@ -131,7 +131,7 @@ function _title(s) {
131
131
  if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
132
132
  throw new TypeError("storefrontPages: title must be a non-empty string ≤ " + MAX_TITLE_LEN + " chars");
133
133
  }
134
- textGuard.freeText(s, "storefrontPages: title", { singleLine: "reject", zeroWidth: "reject" });
134
+ textGuard.freeText(s, "storefrontPages: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
135
135
  return s;
136
136
  }
137
137
 
@@ -139,7 +139,7 @@ function _body(s) {
139
139
  if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
140
140
  throw new TypeError("storefrontPages: body must be a non-empty string ≤ " + MAX_BODY_LEN + " chars");
141
141
  }
142
- textGuard.freeText(s, "storefrontPages: body", { zeroWidth: "reject" });
142
+ textGuard.freeText(s, "storefrontPages: body", { zeroWidth: "reject", bidiMarks: "allow" });
143
143
  return s;
144
144
  }
145
145
 
@@ -143,7 +143,7 @@ function _reason(s) {
143
143
  if (s.length > MAX_REASON_LEN) {
144
144
  throw new TypeError("subscriptionBilling: reason must be <= " + MAX_REASON_LEN + " characters");
145
145
  }
146
- textGuard.freeText(s, "subscriptionBilling: reason", { zeroWidth: "reject" });
146
+ textGuard.freeText(s, "subscriptionBilling: reason", { zeroWidth: "reject", bidiMarks: "allow" });
147
147
  return s;
148
148
  }
149
149
 
@@ -139,7 +139,7 @@ function _reason(s) {
139
139
  if (s.length > MAX_REASON_LEN) {
140
140
  throw new TypeError("subscriptionControls: reason must be <= " + MAX_REASON_LEN + " characters");
141
141
  }
142
- textGuard.freeText(s, "subscriptionControls: reason", { zeroWidth: "reject" });
142
+ textGuard.freeText(s, "subscriptionControls: reason", { zeroWidth: "reject", bidiMarks: "allow" });
143
143
  return s;
144
144
  }
145
145
 
@@ -201,7 +201,7 @@ function _title(s) {
201
201
  if (s.length > MAX_TITLE_LEN) {
202
202
  throw new TypeError("suggestionBox: title must be <= " + MAX_TITLE_LEN + " characters");
203
203
  }
204
- textGuard.freeText(s, "suggestionBox: title", { singleLine: "reject", zeroWidth: "reject" });
204
+ textGuard.freeText(s, "suggestionBox: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
205
205
  return s;
206
206
  }
207
207
 
@@ -219,7 +219,7 @@ function _body(s) {
219
219
  if (textGuard.hasCodepointThreat(s)) {
220
220
  throw new TypeError("suggestionBox: body must not contain control bytes");
221
221
  }
222
- textGuard.freeText(s, "suggestionBox: body", { zeroWidth: "reject" });
222
+ textGuard.freeText(s, "suggestionBox: body", { zeroWidth: "reject", bidiMarks: "allow" });
223
223
  return s;
224
224
  }
225
225
 
@@ -310,7 +310,7 @@ function _response(s) {
310
310
  if (textGuard.hasCodepointThreat(s)) {
311
311
  throw new TypeError("suggestionBox: response must not contain control bytes");
312
312
  }
313
- textGuard.freeText(s, "suggestionBox: response", { zeroWidth: "reject" });
313
+ textGuard.freeText(s, "suggestionBox: response", { zeroWidth: "reject", bidiMarks: "allow" });
314
314
  // A status-only transition is allowed with response === "" (no
315
315
  // operator-visible reply, just a state change). Trim-then-check
316
316
  // for the operator-supplied case below in respondToSuggestion.
@@ -324,7 +324,7 @@ function _responder(s) {
324
324
  if (s.length > MAX_RESPONDER_LEN) {
325
325
  throw new TypeError("suggestionBox: responder must be <= " + MAX_RESPONDER_LEN + " characters");
326
326
  }
327
- textGuard.freeText(s, "suggestionBox: responder", { singleLine: "reject", zeroWidth: "reject" });
327
+ textGuard.freeText(s, "suggestionBox: responder", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
328
328
  return s;
329
329
  }
330
330
 
@@ -155,7 +155,7 @@ function _subject(s) {
155
155
  if (s.length > MAX_SUBJECT_LEN) {
156
156
  throw new TypeError("supportTickets: subject must be <= " + MAX_SUBJECT_LEN + " characters");
157
157
  }
158
- textGuard.freeText(s, "supportTickets: subject", { singleLine: "reject", zeroWidth: "reject" });
158
+ textGuard.freeText(s, "supportTickets: subject", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
159
159
  return s;
160
160
  }
161
161
 
@@ -170,7 +170,7 @@ function _body(s) {
170
170
  if (s.length > MAX_BODY_LEN) {
171
171
  throw new TypeError("supportTickets: body must be <= " + MAX_BODY_LEN + " characters");
172
172
  }
173
- textGuard.freeText(s, "supportTickets: body", { zeroWidth: "reject" });
173
+ textGuard.freeText(s, "supportTickets: body", { zeroWidth: "reject", bidiMarks: "allow" });
174
174
  return s;
175
175
  }
176
176
 
@@ -219,7 +219,7 @@ function _reason(r) {
219
219
  if (r.length > MAX_REASON_LEN) {
220
220
  throw new TypeError("supportTickets: reason must be <= " + MAX_REASON_LEN + " characters");
221
221
  }
222
- textGuard.freeText(r, "supportTickets: reason", { zeroWidth: "reject" });
222
+ textGuard.freeText(r, "supportTickets: reason", { zeroWidth: "reject", bidiMarks: "allow" });
223
223
  return r;
224
224
  }
225
225
 
package/lib/tenants.js CHANGED
@@ -128,7 +128,7 @@ function _name(s) {
128
128
  if (s.length > MAX_NAME_LEN) {
129
129
  throw new TypeError("tenants: name must be <= " + MAX_NAME_LEN + " characters");
130
130
  }
131
- textGuard.freeText(s, "tenants: name", { singleLine: "reject", zeroWidth: "reject" });
131
+ textGuard.freeText(s, "tenants: name", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
132
132
  return s;
133
133
  }
134
134
 
package/lib/text-guard.js CHANGED
@@ -81,6 +81,32 @@ var INVISIBLE_OP_RE = new RegExp("[" + codepointClass.charClass([[0x2061, 0x2064
81
81
  // allow:dynamic-regex — codepoints from a literal range table
82
82
  var INVISIBLE_OP_RE_G = new RegExp("[" + codepointClass.charClass([[0x2061, 0x2064]]) + "]", "g");
83
83
 
84
+ // The framework's BIDI_RE is the union of two groups that do NOT carry the
85
+ // same risk, and refusing both is what broke right-to-left addresses:
86
+ //
87
+ // OVERRIDES + ISOLATES — U+202A-202E and U+2066-2069. These reorder a RUN
88
+ // of surrounding text, which is the Trojan Source primitive: a value that
89
+ // displays as something other than what it says. Nothing legitimate in a
90
+ // shop's stored text needs them.
91
+ // MARKS — U+200E LEFT-TO-RIGHT MARK, U+200F RIGHT-TO-LEFT MARK, U+061C
92
+ // ARABIC LETTER MARK. These only resolve the direction of ADJACENT
93
+ // NEUTRAL characters — a house number inside an Arabic street name, a
94
+ // Latin brand inside a Hebrew sentence. They are how mixed-direction text
95
+ // is correctly authored, and an Arabic or Hebrew postal address routinely
96
+ // contains one. They cannot reverse a run.
97
+ //
98
+ // So the marks are ALLOWED by default and the overrides are refused. A field
99
+ // that genuinely wants neither (a machine-readable identifier, a slug) asks
100
+ // for `bidiMarks: "reject"`.
101
+ // allow:dynamic-regex — codepoints from a literal range table
102
+ var BIDI_OVERRIDE_RE = new RegExp(
103
+ "[" + codepointClass.charClass([[0x202A, 0x202E], [0x2066, 0x2069]]) + "]"
104
+ );
105
+ // allow:dynamic-regex — codepoints from a literal range table
106
+ var BIDI_MARK_RE = new RegExp(
107
+ "[" + codepointClass.charClass([0x200E, 0x200F, 0x061C]) + "]"
108
+ );
109
+
84
110
  // Everything that ends a line or pads it out, for a value that must stay on
85
111
  // one. Tab / LF / CR sit outside the framework's C0 table on purpose — a
86
112
  // multi-line body needs them — so a single-line field asks for them here.
@@ -205,7 +231,20 @@ function hostLabel(host, label) {
205
231
  // building a message just joins the two.
206
232
  //
207
233
  // Policies, each "reject" to enable and any other value to allow:
208
- // bidi — default "reject". Bidi overrides (CVE-2021-42574).
234
+ // bidi — default "reject". Bidi OVERRIDES and ISOLATES only
235
+ // (U+202A-202E, U+2066-2069) — the codepoints that reorder a
236
+ // run of text (CVE-2021-42574).
237
+ // bidiMarks — default "reject". U+200E / U+200F / U+061C, the direction
238
+ // MARKS. These are legitimate in NATURAL-LANGUAGE text — an
239
+ // Arabic postal address or a Hebrew gift message carries one
240
+ // wherever a Latin word or number sits inside the line — and
241
+ // illegitimate in a machine-readable value, where the only
242
+ // reason for an invisible character is to make the value read
243
+ // as something it is not. Neither case is the majority, so
244
+ // the default is chosen by how each mistake fails: refusing a
245
+ // mark that belonged is reported by the customer it turned
246
+ // away, while accepting one that did not is silent. A prose
247
+ // field therefore opts OUT with `bidiMarks: "allow"`.
209
248
  // nullByte — default "reject". U+0000.
210
249
  // control — default "reject". C0 controls + U+007F DELETE. Tab, LF and
211
250
  // CR are ALLOWED here: a multi-line body legitimately has
@@ -221,9 +260,12 @@ function hostLabel(host, label) {
221
260
  // systems, narrowed by policy.allowedScripts.
222
261
  function firstCodepointThreat(s, policy) {
223
262
  policy = policy || {};
224
- if ((policy.bidi || "reject") === "reject" && codepointClass.BIDI_RE.test(s)) {
263
+ if ((policy.bidi || "reject") === "reject" && BIDI_OVERRIDE_RE.test(s)) {
225
264
  return "contains a Unicode bidi override (CVE-2021-42574 Trojan Source)";
226
265
  }
266
+ if ((policy.bidiMarks || "reject") === "reject" && BIDI_MARK_RE.test(s)) {
267
+ return "contains a Unicode bidi direction mark";
268
+ }
227
269
  if ((policy.nullByte || "reject") === "reject" &&
228
270
  s.indexOf(codepointClass.NULL_BYTE) !== -1) {
229
271
  return "contains a null byte";
@@ -257,7 +257,7 @@ function _altText(s) {
257
257
  MAX_ALT_TEXT_LEN + " chars (or null)"
258
258
  );
259
259
  }
260
- textGuard.freeText(s, "themeAssets: alt_text", { singleLine: "reject", zeroWidth: "reject" });
260
+ textGuard.freeText(s, "themeAssets: alt_text", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
261
261
  return s;
262
262
  }
263
263
 
@@ -3,8 +3,8 @@
3
3
  "_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
4
4
  "packages": {
5
5
  "blamejs": {
6
- "version": "0.18.21",
7
- "tag": "v0.18.21",
6
+ "version": "0.18.24",
7
+ "tag": "v0.18.24",
8
8
  "license": "Apache-2.0",
9
9
  "author": "blamejs contributors",
10
10
  "source": "https://github.com/blamejs/blamejs",
@@ -599,15 +599,15 @@
599
599
  "package.json": "lib/vendor/blamejs/package.json",
600
600
  "sbom.cdx.json": "lib/vendor/blamejs/sbom.cdx.json"
601
601
  },
602
- "bundler": "signed release tarball blamejs-core-0.18.21.tgz from github.com/blamejs/blamejs — SHA3-512 digest and ML-DSA-65 signature verified against keys/vendor-blamejs-pqc-pub.json before unpacking",
603
- "bundledAt": "2026-08-10",
602
+ "bundler": "signed release tarball blamejs-core-0.18.24.tgz from github.com/blamejs/blamejs — SHA3-512 digest and ML-DSA-65 signature verified against keys/vendor-blamejs-pqc-pub.json before unpacking",
603
+ "bundledAt": "2026-08-11",
604
604
  "hashes": {
605
- "CHANGELOG.md": "sha256:8604c2e45b6e3575b1cc2df0021fd97ce959a8b6e269d129388fe365bbe69f47",
605
+ "CHANGELOG.md": "sha256:2ac9860b3cc5e84c9103557a9610d3e60af5f9a3f648d22aa127c55f9c72cf0e",
606
606
  "LICENSE": "sha256:812075be5abe785284e823a97d09b5d1515b0221e3128059e1f5065de3c907d5",
607
607
  "LTS-CALENDAR.md": "sha256:fbdf074ca0d9544bf10fff87c8947ba38aa5ac0500267796dc8f0c92822aacbe",
608
608
  "MIGRATING.md": "sha256:7bee6f55c28f017d4083132dbe45e0e63bcdb7ad3086b95b02551eaa4cc052bc",
609
- "NOTICE": "sha256:73a48bab0eb79e491df655fb57d4ec690da51228a8698c1c828c3870fbd22c31",
610
- "README.md": "sha256:3c2b7909d68c10b80560b1fcc94ae70f79ef760fdfa6d7fe914c62035bfac842",
609
+ "NOTICE": "sha256:979a65f58486faf2defacd821fa97476149a3a4efe1ddea8f6b53b3283175fe5",
610
+ "README.md": "sha256:4ea822a1e856e860ea6d24b01a6dbb34252d519f78707eccdcb0a6de3d3775e8",
611
611
  "bin/blamejs.js": "sha256:59798e63ad43c0afdf03ab88aaae105fa1f525c2703d80903eee48717db567c4",
612
612
  "index.js": "sha256:2d2c81c19e4177450b701dba05f582a8b6ef9581d202f01ec04d912556266a2f",
613
613
  "lib/_test/crypto-fixtures.js": "sha256:16055e38d3c1a3065735e50fe43e81de1ed5d90cf72d7fb90ac263d07deb7ced",
@@ -1019,7 +1019,7 @@
1019
1019
  "lib/network-nts.js": "sha256:947e1887f339ef1bb20891d5c9531e2969653b162886b576c9b8a723d6b62002",
1020
1020
  "lib/network-proxy.js": "sha256:392e79ddc58ae7125cd7a8abec872df50ffee76170ea40ff6847d581b2a405a3",
1021
1021
  "lib/network-smtp-policy.js": "sha256:b8b88766be9dd1a0e4dfe60b21f97fe8b166d76328ecbbbb4e1349bfb2335e95",
1022
- "lib/network-tls.js": "sha256:ef77f7f153636b28cd6b28ab8de60b14fa3d60fc675e2fc7fa4063033c513074",
1022
+ "lib/network-tls.js": "sha256:0a2b52cf7d9e35d988ba0a6d3d0afd22536c7f68ff047f24981e0732964a4fa3",
1023
1023
  "lib/network-tsig.js": "sha256:233b008815bbbd60e6914e4a74b41de0d9136c27331bf00827ca4a983ddd29e2",
1024
1024
  "lib/network.js": "sha256:a4a4bd54396c0e162f1d5469d4d3e264a9f4537179446843589674e551ac1f29",
1025
1025
  "lib/nis2-report.js": "sha256:d54937f1fe996c5316756cfcf9095c67c8f955621c199079abb87862a82afd75",
@@ -1158,10 +1158,10 @@
1158
1158
  "lib/vc.js": "sha256:2f0e2a32bb52f53e4c38df64a2a658665cf5e442b56882d3b3bbdf31c7124132",
1159
1159
  "lib/vendor-data.js": "sha256:365b0dcca1198e4fad647b3873b82702fcf90a5ee2d8c7f25c78864cadb7d717",
1160
1160
  "lib/vendor/.vendor-data-pubkey": "sha256:73a935b8c72f55d821c22ddb8188453bab324f80d229d033a0a1549819a1f096",
1161
- "lib/vendor/MANIFEST.json": "sha256:f32f5ed3c3bc4b0475246cc2e8822749e9ed235d331e9e5eeb186e70b42e76d9",
1161
+ "lib/vendor/MANIFEST.json": "sha256:75576b6fbcb07383d382c94fbf3fb317703d5cb4bf838b55801b93d7ead3de08",
1162
1162
  "lib/vendor/bimi-trust-anchors.data.js": "sha256:aa7a4d33b65a68422a2a2c1670177689f66fdcaa08bd2514d78798b827bd1608",
1163
1163
  "lib/vendor/bimi-trust-anchors.pem": "sha256:81ff9f5ab3c9774132c845684e783be95cf73146f8b670d964105f0a3765b4b4",
1164
- "lib/vendor/blamejs-pki.cjs": "sha256:067ac05899ef04f7851a8d010dc4289e7a01706d5fd2cdc1ce59860631bba284",
1164
+ "lib/vendor/blamejs-pki.cjs": "sha256:96a873b733c95b53b8e2ced065a82192b6acabcede312cf68a45a128857cc3bc",
1165
1165
  "lib/vendor/browser/noble-ciphers.mjs": "sha256:0ffd91540bcb586a29b56e52ee1c29df69097b50776beb4036a07558f7a4e12e",
1166
1166
  "lib/vendor/browser/noble-hashes.mjs": "sha256:dfe4b7ae3c9880e388c8da4b68f44742b229b53afacd1e674179527e33da62b0",
1167
1167
  "lib/vendor/browser/noble-post-quantum.mjs": "sha256:cae1d5bbdc7184b202b6ca68df6e1db7b0d0f668c77809ded189ca7f271accc9",
@@ -1188,8 +1188,8 @@
1188
1188
  "lib/ws-client.js": "sha256:348d01cd552fed583f05944d1853f71b0119105cb067c2e607b19ec9e6f47aee",
1189
1189
  "lib/x509-chain.js": "sha256:c2b5544202601a50230c7c652ff028e73e9d29a34d58db349beb5b5164b7b4d5",
1190
1190
  "lib/xml-c14n.js": "sha256:6239db09b021f032a78aa101fe295238db4ad91acc93ae13eef7d14efd797b94",
1191
- "package.json": "sha256:0feb395d6b015bdf53375147fd96972507ea80974bb81a93841a77aa95e5e0f6",
1192
- "sbom.cdx.json": "sha256:d659ac36810bd4f2c120e75c09ba0b1ae40c48a8a4a6aef36d9569cb3899dbe2"
1191
+ "package.json": "sha256:ab52ca260fa6408d4910a3d9a726d442e61127e883e920c965e7ef31c9319a0d",
1192
+ "sbom.cdx.json": "sha256:b47182cff49c56cc0534ba835f62bd8ddebcb659ae3d693bbfc1f4b059a427c7"
1193
1193
  }
1194
1194
  }
1195
1195
  }