@blamejs/blamejs-shop 0.5.20 → 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.
- package/CHANGELOG.md +2 -0
- package/README.md +1 -0
- package/SECURITY.md +17 -0
- package/lib/addresses.js +2 -2
- package/lib/admin.js +14 -5
- package/lib/affiliates.js +7 -23
- package/lib/announcement-bar.js +5 -22
- package/lib/api-keys.js +4 -13
- package/lib/asset-manifest.json +1 -1
- package/lib/banner-ab-tests.js +5 -9
- package/lib/blog-articles.js +8 -26
- package/lib/captcha-gate.js +7 -17
- package/lib/carrier-accounts.js +3 -10
- package/lib/catalog-drafts.js +5 -12
- package/lib/category-navigation.js +4 -20
- package/lib/cms-blocks.js +4 -7
- package/lib/compliance-export.js +4 -11
- package/lib/consent-ledger.js +2 -4
- package/lib/cost-layers.js +56 -21
- package/lib/customer-import.js +2 -2
- package/lib/customer-notes.js +7 -23
- package/lib/customer-roles.js +3 -5
- package/lib/customer-segments.js +17 -14
- package/lib/customer-surveys.js +7 -14
- package/lib/customers.js +2 -4
- package/lib/dispute-resolution.js +5 -13
- package/lib/dropship-forwarding.js +3 -5
- package/lib/dunning.js +2 -7
- package/lib/email-ab-tests.js +4 -8
- package/lib/email-campaigns.js +25 -14
- package/lib/email-warmup.js +2 -7
- package/lib/experiments.js +6 -10
- package/lib/externaldb-d1.js +47 -23
- package/lib/geolocation.js +18 -28
- package/lib/gift-options.js +5 -23
- package/lib/knowledge-base.js +7 -22
- package/lib/line-gift-wrap.js +3 -17
- package/lib/live-chat.js +5 -18
- package/lib/metered-usage.js +3 -8
- package/lib/notifications.js +3 -7
- package/lib/operator-accounts.js +3 -5
- package/lib/operator-approvals.js +6 -8
- package/lib/operator-audit-log.js +2 -7
- package/lib/operator-help-center.js +5 -21
- package/lib/operator-inbox.js +4 -11
- package/lib/operator-roles.js +5 -7
- package/lib/order-escalation.js +2 -2
- package/lib/order-export.js +24 -20
- package/lib/order-notes.js +4 -14
- package/lib/order-ratings.js +4 -10
- package/lib/payment-methods.js +5 -13
- package/lib/payment-retries.js +2 -7
- package/lib/pixel-events.js +8 -23
- package/lib/plan-changes.js +2 -7
- package/lib/product-qa.js +4 -8
- package/lib/promo-banners.js +7 -15
- package/lib/purchase-orders.js +3 -6
- package/lib/push-notifications.js +7 -32
- package/lib/pwa-manifest.js +6 -11
- package/lib/quotes.js +5 -9
- package/lib/reviews.js +4 -10
- package/lib/robots-config.js +6 -14
- package/lib/sales-tax-filings.js +2 -2
- package/lib/search-facets.js +5 -5
- package/lib/search-ranking.js +4 -7
- package/lib/search-synonyms.js +7 -21
- package/lib/security-middleware.js +86 -58
- package/lib/seller-signup.js +10 -26
- package/lib/sidebar-widgets.js +6 -15
- package/lib/site-redirects.js +5 -16
- package/lib/sitemap-generator.js +3 -7
- package/lib/sms-dispatcher.js +7 -23
- package/lib/stock-receipts.js +4 -10
- package/lib/storefront-dashboards.js +2 -10
- package/lib/storefront-forms.js +9 -22
- package/lib/storefront-pages.js +6 -20
- package/lib/storefront.js +2 -2
- package/lib/subscription-billing.js +3 -8
- package/lib/subscription-controls.js +2 -7
- package/lib/suggestion-box.js +9 -26
- package/lib/support-tickets.js +6 -24
- package/lib/tax-remittance.js +2 -2
- package/lib/tenants.js +2 -7
- package/lib/text-guard.js +137 -35
- package/lib/theme-assets.js +4 -16
- package/lib/translations.js +3 -6
- package/lib/trust-badges.js +4 -7
- package/lib/vendor-invoices.js +3 -6
- package/lib/vendors.js +5 -19
- package/lib/webhook-receiver.js +3 -10
- package/lib/webhook-subscriptions.js +3 -13
- package/lib/wishlist-sharing.js +5 -10
- package/package.json +1 -1
package/lib/customers.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
var nodeCrypto = require("node:crypto"); // allow:non-shop-require — Apple mandates ES256/P1363; framework has no classical ES256 signer
|
|
37
37
|
|
|
38
38
|
var b = require("./vendor/blamejs");
|
|
39
|
+
var textGuard = require("./text-guard");
|
|
39
40
|
|
|
40
41
|
var EMAIL_NAMESPACE = "customer-email";
|
|
41
42
|
// Federated identity providers the OIDC sign-in path accepts.
|
|
@@ -43,7 +44,6 @@ var OAUTH_PROVIDERS = ["google", "apple"];
|
|
|
43
44
|
var MAX_DISPLAY_NAME_LEN = 128;
|
|
44
45
|
var MAX_CRED_FIELD_BYTES = 4096;
|
|
45
46
|
var TRANSPORTS_RE = /^[a-z]+(?:,[a-z]+)*$/;
|
|
46
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
47
47
|
|
|
48
48
|
// Pagination cursor for the operator-facing `list` — newest first by
|
|
49
49
|
// (created_at, id), HMAC-tagged via b.pagination so an operator can't
|
|
@@ -105,9 +105,7 @@ function _displayName(s) {
|
|
|
105
105
|
if (s.length > MAX_DISPLAY_NAME_LEN) {
|
|
106
106
|
throw new TypeError("customers: display_name must be <= " + MAX_DISPLAY_NAME_LEN + " characters");
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
throw new TypeError("customers: display_name contains control bytes");
|
|
110
|
-
}
|
|
108
|
+
textGuard.freeText(s, "customers: display_name", { singleLine: "reject" });
|
|
111
109
|
return s;
|
|
112
110
|
}
|
|
113
111
|
|
|
@@ -114,7 +114,6 @@ var PROCESSOR_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
|
114
114
|
var REASON_CODE_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
115
115
|
var CURRENCY_RE = /^[A-Z]{3}$/;
|
|
116
116
|
var OPERATOR_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
117
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
118
117
|
|
|
119
118
|
var KINDS = Object.freeze([
|
|
120
119
|
"chargeback",
|
|
@@ -151,6 +150,7 @@ var STATUSES = Object.freeze([
|
|
|
151
150
|
]);
|
|
152
151
|
|
|
153
152
|
var b = require("./vendor/blamejs");
|
|
153
|
+
var textGuard = require("./text-guard");
|
|
154
154
|
|
|
155
155
|
// ---- validators ---------------------------------------------------------
|
|
156
156
|
|
|
@@ -249,9 +249,7 @@ function _narrative(s) {
|
|
|
249
249
|
" exceeds cap " + MAX_NARRATIVE_LEN
|
|
250
250
|
);
|
|
251
251
|
}
|
|
252
|
-
|
|
253
|
-
throw new TypeError("disputeResolution: narrative must not contain control bytes");
|
|
254
|
-
}
|
|
252
|
+
textGuard.freeText(s, "disputeResolution: narrative");
|
|
255
253
|
return s;
|
|
256
254
|
}
|
|
257
255
|
|
|
@@ -265,9 +263,7 @@ function _blobRef(s) {
|
|
|
265
263
|
" exceeds cap " + MAX_BLOB_REF_LEN
|
|
266
264
|
);
|
|
267
265
|
}
|
|
268
|
-
|
|
269
|
-
throw new TypeError("disputeResolution: blob_ref must not contain control bytes");
|
|
270
|
-
}
|
|
266
|
+
textGuard.freeText(s, "disputeResolution: blob_ref");
|
|
271
267
|
return s;
|
|
272
268
|
}
|
|
273
269
|
|
|
@@ -281,9 +277,7 @@ function _notesOpt(s) {
|
|
|
281
277
|
"disputeResolution: notes length " + s.length + " exceeds cap " + MAX_NOTES_LEN
|
|
282
278
|
);
|
|
283
279
|
}
|
|
284
|
-
|
|
285
|
-
throw new TypeError("disputeResolution: notes must not contain control bytes");
|
|
286
|
-
}
|
|
280
|
+
textGuard.freeText(s, "disputeResolution: notes");
|
|
287
281
|
return s;
|
|
288
282
|
}
|
|
289
283
|
|
|
@@ -307,9 +301,7 @@ function _writeoffReason(s) {
|
|
|
307
301
|
" exceeds cap " + MAX_WRITEOFF_REASON_LEN
|
|
308
302
|
);
|
|
309
303
|
}
|
|
310
|
-
|
|
311
|
-
throw new TypeError("disputeResolution: writeoff reason must not contain control bytes");
|
|
312
|
-
}
|
|
304
|
+
textGuard.freeText(s, "disputeResolution: writeoff reason");
|
|
313
305
|
return s;
|
|
314
306
|
}
|
|
315
307
|
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
*/
|
|
67
67
|
|
|
68
68
|
var b = require("./vendor/blamejs");
|
|
69
|
+
var textGuard = require("./text-guard");
|
|
69
70
|
|
|
70
71
|
// ---- constants ----------------------------------------------------------
|
|
71
72
|
|
|
@@ -81,7 +82,6 @@ var FORWARDING_STATUSES = Object.freeze([
|
|
|
81
82
|
var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
82
83
|
var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
83
84
|
var CURRENCY_RE = /^[A-Z]{3}$/;
|
|
84
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
85
85
|
|
|
86
86
|
var MAX_RETURN_POLICY_LEN = 4000;
|
|
87
87
|
var MAX_VENDOR_REF_LEN = 256;
|
|
@@ -183,9 +183,7 @@ function _returnPolicy(s) {
|
|
|
183
183
|
if (typeof s !== "string" || !s.length || s.length > MAX_RETURN_POLICY_LEN) {
|
|
184
184
|
throw new TypeError("dropship-forwarding: return_policy must be a non-empty string <= " + MAX_RETURN_POLICY_LEN + " chars");
|
|
185
185
|
}
|
|
186
|
-
|
|
187
|
-
throw new TypeError("dropship-forwarding: return_policy must not contain control bytes");
|
|
188
|
-
}
|
|
186
|
+
textGuard.freeText(s, "dropship-forwarding: return_policy", { singleLine: "reject" });
|
|
189
187
|
}
|
|
190
188
|
function _positiveInt(n, label) {
|
|
191
189
|
if (!Number.isInteger(n) || n <= 0) {
|
|
@@ -214,7 +212,7 @@ function _shortText(s, label, max) {
|
|
|
214
212
|
if (typeof s !== "string" || !s.length || s.length > max) {
|
|
215
213
|
throw new TypeError("dropship-forwarding: " + label + " must be a non-empty string <= " + max + " chars");
|
|
216
214
|
}
|
|
217
|
-
if (
|
|
215
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
218
216
|
throw new TypeError("dropship-forwarding: " + label + " must not contain control bytes");
|
|
219
217
|
}
|
|
220
218
|
}
|
package/lib/dunning.js
CHANGED
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
*/
|
|
106
106
|
|
|
107
107
|
var b = require("./vendor/blamejs");
|
|
108
|
+
var textGuard = require("./text-guard");
|
|
108
109
|
// Framework constants (C.TIME duration helpers). Safe at module-eval —
|
|
109
110
|
// the index entry point exposes `framework` before the require cascade.
|
|
110
111
|
var C = b.constants;
|
|
@@ -130,10 +131,6 @@ var DEFAULT_LIST_LIMIT = 100;
|
|
|
130
131
|
var MS_PER_HOUR = C.TIME.hours(1);
|
|
131
132
|
|
|
132
133
|
var SLUG_RE = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
133
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
134
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
135
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
136
|
-
);
|
|
137
134
|
|
|
138
135
|
// ---- validators ---------------------------------------------------------
|
|
139
136
|
|
|
@@ -205,9 +202,7 @@ function _reason(s) {
|
|
|
205
202
|
if (s.length > MAX_REASON_LEN) {
|
|
206
203
|
throw new TypeError("dunning: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
207
204
|
}
|
|
208
|
-
|
|
209
|
-
throw new TypeError("dunning: reason contains control / zero-width bytes");
|
|
210
|
-
}
|
|
205
|
+
textGuard.freeText(s, "dunning: reason", { zeroWidth: "reject" });
|
|
211
206
|
return s;
|
|
212
207
|
}
|
|
213
208
|
|
package/lib/email-ab-tests.js
CHANGED
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
*/
|
|
106
106
|
|
|
107
107
|
var b = require("./vendor/blamejs");
|
|
108
|
+
var textGuard = require("./text-guard");
|
|
108
109
|
|
|
109
110
|
// ---- constants ----------------------------------------------------------
|
|
110
111
|
|
|
@@ -133,7 +134,6 @@ var DEFAULT_LIST_LIMIT = 50;
|
|
|
133
134
|
// but 1.96 is the de-facto convention for shipped dashboards.
|
|
134
135
|
var WILSON_Z = 1.96;
|
|
135
136
|
|
|
136
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
137
137
|
|
|
138
138
|
// ---- monotonic clock ----------------------------------------------------
|
|
139
139
|
//
|
|
@@ -165,9 +165,7 @@ function _title(s) {
|
|
|
165
165
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
166
166
|
throw new TypeError("emailABTests: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
|
|
167
167
|
}
|
|
168
|
-
|
|
169
|
-
throw new TypeError("emailABTests: title must not contain control bytes");
|
|
170
|
-
}
|
|
168
|
+
textGuard.freeText(s, "emailABTests: title");
|
|
171
169
|
return s;
|
|
172
170
|
}
|
|
173
171
|
|
|
@@ -184,7 +182,7 @@ function _label(s, idx) {
|
|
|
184
182
|
throw new TypeError("emailABTests: variants[" + idx + "].label must be a non-empty string <= " +
|
|
185
183
|
MAX_LABEL_LEN + " chars");
|
|
186
184
|
}
|
|
187
|
-
if (
|
|
185
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
188
186
|
throw new TypeError("emailABTests: variants[" + idx + "].label must not contain control bytes");
|
|
189
187
|
}
|
|
190
188
|
return s;
|
|
@@ -223,9 +221,7 @@ function _recipientId(s) {
|
|
|
223
221
|
throw new TypeError("emailABTests: recipient_id must be a non-empty string <= " +
|
|
224
222
|
MAX_RECIPIENT_ID_LEN + " chars");
|
|
225
223
|
}
|
|
226
|
-
|
|
227
|
-
throw new TypeError("emailABTests: recipient_id must not contain control bytes");
|
|
228
|
-
}
|
|
224
|
+
textGuard.freeText(s, "emailABTests: recipient_id");
|
|
229
225
|
return s;
|
|
230
226
|
}
|
|
231
227
|
|
package/lib/email-campaigns.js
CHANGED
|
@@ -146,6 +146,7 @@ var DEFAULT_BATCH_SIZE = 100;
|
|
|
146
146
|
var RESOLVE_PAGE_LIMIT = 500; // matches mailingAudiences MAX_LIST_LIMIT
|
|
147
147
|
|
|
148
148
|
var b = require("./vendor/blamejs");
|
|
149
|
+
var textGuard = require("./text-guard");
|
|
149
150
|
// Framework duration constants (C.TIME.minutes(n) etc.). The index entry
|
|
150
151
|
// point exposes `framework` before the require cascade, so resolving this
|
|
151
152
|
// at module-eval is safe — same pattern lib/admin.js / lib/cart.js use.
|
|
@@ -164,8 +165,16 @@ var SEND_RATE_WINDOW_MS = C.TIME.minutes(1);
|
|
|
164
165
|
// Per-recipient broadcast outcomes recorded in email_campaign_sends.
|
|
165
166
|
var SEND_OUTCOMES = ["sent", "failed", "skipped_unsubscribed", "skipped_suppressed"];
|
|
166
167
|
|
|
168
|
+
// A link URL is rendered on one line and, in the plain-text part, is shown
|
|
169
|
+
// to the recipient verbatim — so it is screened against the shared codepoint
|
|
170
|
+
// policy rather than a local class. The class this replaced covered the
|
|
171
|
+
// zero-width family but not the bidi overrides, which let a campaign body
|
|
172
|
+
// carry a URL that displays as one host and resolves to another.
|
|
173
|
+
var LINK_URL_POLICY = Object.freeze({ singleLine: "reject", zeroWidth: "reject" });
|
|
174
|
+
|
|
175
|
+
// Free-form text folded into one ledger line. Stripped rather than refused —
|
|
176
|
+
// this runs on an error path that must not itself fail.
|
|
167
177
|
var CONTROL_BYTE_LINE_RE = /[\u0000-\u001f\u007f]/;
|
|
168
|
-
var ZERO_WIDTH_RE = /[\u200b-\u200f\u2028\u2029\ufeff]/;
|
|
169
178
|
|
|
170
179
|
var STATUSES = ["draft", "scheduled", "sending", "sent", "paused", "cancelled"];
|
|
171
180
|
var EVENT_TYPES = ["delivered", "opened", "clicked", "bounced", "unsubscribed"];
|
|
@@ -190,7 +199,7 @@ function _esc(s) {
|
|
|
190
199
|
|
|
191
200
|
function _mdSafeLinkUrl(url) {
|
|
192
201
|
if (typeof url !== "string" || !url.length || url.length > 2048) return null;
|
|
193
|
-
if (
|
|
202
|
+
if (textGuard.hasCodepointThreat(url, LINK_URL_POLICY)) return null;
|
|
194
203
|
if (url.charCodeAt(0) === 47 /* "/" */) {
|
|
195
204
|
if (url.length > 1 && url.charCodeAt(1) === 47) return null;
|
|
196
205
|
if (url.indexOf("..") !== -1) return null;
|
|
@@ -389,10 +398,13 @@ function _validateSubject(s) {
|
|
|
389
398
|
if (s.length > MAX_SUBJECT_LEN) {
|
|
390
399
|
throw new TypeError("emailCampaigns: subject must be <= " + MAX_SUBJECT_LEN + " characters");
|
|
391
400
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
401
|
+
// A subject becomes a `Subject:` header, so a CR or LF in it is a header
|
|
402
|
+
// injection — the bytes after the break are read as another header. NUL goes
|
|
403
|
+
// with them: it truncates the field in some agents, which hides whatever
|
|
404
|
+
// follows. b.safeBuffer.assertHeaderSafe is the framework's check for a
|
|
405
|
+
// value destined for a `Name: value` field, and it refuses exactly that set.
|
|
406
|
+
return b.safeBuffer.assertHeaderSafe(
|
|
407
|
+
s, "subject", TypeError, "emailCampaigns: subject must not contain CR / LF / NUL");
|
|
396
408
|
}
|
|
397
409
|
|
|
398
410
|
function _validateBody(s, label) {
|
|
@@ -433,10 +445,9 @@ function _validateFromName(s) {
|
|
|
433
445
|
if (s.length > MAX_FROM_NAME_LEN) {
|
|
434
446
|
throw new TypeError("emailCampaigns: from_name must be <= " + MAX_FROM_NAME_LEN + " characters");
|
|
435
447
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return s;
|
|
448
|
+
// Lands in the `From:` header — same header-injection surface as the subject.
|
|
449
|
+
return b.safeBuffer.assertHeaderSafe(
|
|
450
|
+
s, "from_name", TypeError, "emailCampaigns: from_name must not contain CR / LF / NUL");
|
|
440
451
|
}
|
|
441
452
|
|
|
442
453
|
function _validateTs(ts, label) {
|
|
@@ -481,10 +492,10 @@ function _validateReason(s) {
|
|
|
481
492
|
if (s.length > MAX_REASON_LEN) {
|
|
482
493
|
throw new TypeError("emailCampaigns: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
483
494
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
495
|
+
// Operator-authored and surfaced in logs and the console rather than a mail
|
|
496
|
+
// header, but held to the same rule: a newline here forges a log line.
|
|
497
|
+
return b.safeBuffer.assertHeaderSafe(
|
|
498
|
+
s, "reason", TypeError, "emailCampaigns: reason must not contain CR / LF / NUL");
|
|
488
499
|
}
|
|
489
500
|
|
|
490
501
|
function _validateStatus(s, label) {
|
package/lib/email-warmup.js
CHANGED
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
// ---- constants ----------------------------------------------------------
|
|
94
94
|
|
|
95
95
|
var b = require("./vendor/blamejs");
|
|
96
|
+
var textGuard = require("./text-guard");
|
|
96
97
|
|
|
97
98
|
var C = b.constants;
|
|
98
99
|
|
|
@@ -118,10 +119,6 @@ var IPV4_RE = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4
|
|
|
118
119
|
var IPV6_RE = /^[0-9a-f:]+$/;
|
|
119
120
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
120
121
|
|
|
121
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
122
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
123
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
124
|
-
);
|
|
125
122
|
|
|
126
123
|
var ALLOWED_STATUSES = Object.freeze(["active", "paused", "archived"]);
|
|
127
124
|
|
|
@@ -234,9 +231,7 @@ function _reason(s) {
|
|
|
234
231
|
if (s.length > MAX_REASON_LEN) {
|
|
235
232
|
throw new TypeError("emailWarmup: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
236
233
|
}
|
|
237
|
-
|
|
238
|
-
throw new TypeError("emailWarmup: reason contains control / zero-width bytes");
|
|
239
|
-
}
|
|
234
|
+
textGuard.freeText(s, "emailWarmup: reason", { singleLine: "reject", zeroWidth: "reject" });
|
|
240
235
|
return s;
|
|
241
236
|
}
|
|
242
237
|
|
package/lib/experiments.js
CHANGED
|
@@ -116,15 +116,10 @@ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
|
116
116
|
// title + hypothesis fields land into the operator dashboard, not the
|
|
117
117
|
// storefront, but the discipline is the same — strings reach the
|
|
118
118
|
// operator UI as inert text, never as live markup.
|
|
119
|
-
var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
|
|
120
|
-
var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
121
119
|
|
|
122
120
|
// Zero-width / direction-override family — mirrors the promo-banners
|
|
123
121
|
// + gift-options catalogues. Spelled with \u-escapes so ESLint's
|
|
124
122
|
// no-irregular-whitespace stays happy.
|
|
125
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
126
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
127
|
-
);
|
|
128
123
|
|
|
129
124
|
var ALLOWED_PATCH_COLUMNS = Object.freeze([
|
|
130
125
|
"title",
|
|
@@ -134,6 +129,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
|
|
|
134
129
|
]);
|
|
135
130
|
|
|
136
131
|
var b = require("./vendor/blamejs");
|
|
132
|
+
var textGuard = require("./text-guard");
|
|
137
133
|
|
|
138
134
|
// ---- validators ---------------------------------------------------------
|
|
139
135
|
|
|
@@ -149,10 +145,10 @@ function _line(s, label, maxLen) {
|
|
|
149
145
|
if (typeof s !== "string" || !s.length || s.length > maxLen) {
|
|
150
146
|
throw new TypeError("experiments: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
|
|
151
147
|
}
|
|
152
|
-
if (
|
|
148
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
153
149
|
throw new TypeError("experiments: " + label + " contains control bytes (incl. CR/LF)");
|
|
154
150
|
}
|
|
155
|
-
if (
|
|
151
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
156
152
|
throw new TypeError("experiments: " + label + " contains zero-width / direction-override characters");
|
|
157
153
|
}
|
|
158
154
|
return s;
|
|
@@ -162,10 +158,10 @@ function _block(s, label, maxLen) {
|
|
|
162
158
|
if (typeof s !== "string" || !s.length || s.length > maxLen) {
|
|
163
159
|
throw new TypeError("experiments: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
|
|
164
160
|
}
|
|
165
|
-
if (
|
|
161
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
166
162
|
throw new TypeError("experiments: " + label + " contains control bytes");
|
|
167
163
|
}
|
|
168
|
-
if (
|
|
164
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
169
165
|
throw new TypeError("experiments: " + label + " contains zero-width / direction-override characters");
|
|
170
166
|
}
|
|
171
167
|
return s;
|
|
@@ -437,7 +433,7 @@ function create(opts) {
|
|
|
437
433
|
if (typeof input.metric !== "string" || !input.metric.length || input.metric.length > MAX_METRIC_LEN) {
|
|
438
434
|
return { recorded: false };
|
|
439
435
|
}
|
|
440
|
-
if (
|
|
436
|
+
if (textGuard.hasCodepointThreat(input.metric, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
441
437
|
return { recorded: false };
|
|
442
438
|
}
|
|
443
439
|
var value = 1;
|
package/lib/externaldb-d1.js
CHANGED
|
@@ -125,34 +125,58 @@ function _timeoutErr(ms, label) {
|
|
|
125
125
|
return e;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// The attempt loop is b.retry.withRetry — exponential backoff, jitter, and
|
|
129
|
+
// the final rethrow — with two things this adapter has to supply.
|
|
130
|
+
//
|
|
131
|
+
// The classifier is ours. The framework's default reads `err.code` and
|
|
132
|
+
// `err.statusCode` off the error itself, and undici does not put them there:
|
|
133
|
+
// a transport failure arrives as TypeError("fetch failed") carrying the real
|
|
134
|
+
// code on `.cause`. Its retryable set also omits EAI_AGAIN and UND_ERR_SOCKET,
|
|
135
|
+
// which are precisely the DNS and socket failures this adapter exists to ride
|
|
136
|
+
// out. Handing it the default classifier would leave the loop in place and
|
|
137
|
+
// silently stop retrying anything that matters.
|
|
138
|
+
//
|
|
139
|
+
// The HTTP arm has to throw to be seen at all. b.httpClient and fetch both
|
|
140
|
+
// resolve a 5xx as an ordinary response, so a retryable status is raised
|
|
141
|
+
// inside the attempt and the response handed back at the boundary once the
|
|
142
|
+
// retries are spent — the caller still inspects `res.status` itself.
|
|
128
143
|
async function _retryingFetch(fetchImpl, url, init, attempts, label) {
|
|
129
|
-
var
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
throw e;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
throw lastErr;
|
|
144
|
+
var received = null;
|
|
145
|
+
return b.retry.withRetry(async function () {
|
|
146
|
+
var res = await fetchImpl(url, init);
|
|
147
|
+
received = res;
|
|
148
|
+
// Retry only on 5xx / 429; a 4xx is the request's own fault.
|
|
149
|
+
if (res.status >= 500 || res.status === 429) throw _httpErr(res.status, label);
|
|
150
|
+
return res;
|
|
151
|
+
}, {
|
|
152
|
+
maxAttempts: attempts + 1, // `attempts` counts RETRIES, not tries
|
|
153
|
+
baseDelayMs: RETRY_BACKOFF_MS,
|
|
154
|
+
isRetryable: _isTransient,
|
|
155
|
+
}).catch(function (e) {
|
|
156
|
+
// Retries exhausted on a status we did receive: give the caller the
|
|
157
|
+
// response so it reports the upstream's own error, exactly as the previous
|
|
158
|
+
// loop did. A transport failure has no response and propagates.
|
|
159
|
+
if (received && e && e.status === received.status) return received;
|
|
160
|
+
throw e;
|
|
161
|
+
});
|
|
152
162
|
}
|
|
153
163
|
|
|
164
|
+
// Which failures are worth another attempt. Both arms matter and neither is
|
|
165
|
+
// what the framework's default classifier looks at.
|
|
166
|
+
//
|
|
167
|
+
// Transport: undici reports a network failure as TypeError("fetch failed")
|
|
168
|
+
// with the real code nested on `.cause`, so the code is read from either
|
|
169
|
+
// place. EAI_AGAIN (transient DNS) and UND_ERR_SOCKET (socket torn down
|
|
170
|
+
// mid-request) are the two this adapter most needs and the two the framework's
|
|
171
|
+
// set leaves out.
|
|
172
|
+
//
|
|
173
|
+
// HTTP: a 5xx or 429 is raised as an error inside the attempt so the loop can
|
|
174
|
+
// see it at all — fetch resolves those as ordinary responses. `_httpErr`
|
|
175
|
+
// records the status on `.status`, so that is what is read here. A 4xx is the
|
|
176
|
+
// request's own fault and is never retried.
|
|
154
177
|
function _isTransient(err) {
|
|
155
178
|
if (!err) return false;
|
|
179
|
+
if (typeof err.status === "number") return err.status >= 500 || err.status === 429;
|
|
156
180
|
var c = err.code || (err.cause && err.cause.code);
|
|
157
181
|
return c === "ECONNRESET" || c === "ETIMEDOUT" || c === "ENOTFOUND" || c === "EAI_AGAIN" || c === "UND_ERR_SOCKET";
|
|
158
182
|
}
|
package/lib/geolocation.js
CHANGED
|
@@ -254,34 +254,24 @@ function _now() { return Date.now(); }
|
|
|
254
254
|
// through to the country's default_locale.
|
|
255
255
|
function _parseAcceptLanguage(raw) {
|
|
256
256
|
if (typeof raw !== "string" || !raw.length) return [];
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
if (tag === "*") continue;
|
|
276
|
-
if (!LOCALE_RE.test(tag)) continue;
|
|
277
|
-
parsed.push({ tag: tag, q: q, order: i });
|
|
278
|
-
}
|
|
279
|
-
// Stable q-desc sort (preserve original order on ties).
|
|
280
|
-
parsed.sort(function (a, b) {
|
|
281
|
-
if (a.q !== b.q) return b.q - a.q;
|
|
282
|
-
return a.order - b.order;
|
|
283
|
-
});
|
|
284
|
-
return parsed;
|
|
257
|
+
// b.requestHelpers.parseQualityList is the RFC 9110 §12.5 parser, and the
|
|
258
|
+
// edge bypass check reads the same header through it — which is what keeps
|
|
259
|
+
// the two agreeing on which tag wins. They had drifted: a header spelling
|
|
260
|
+
// the parameter `Q=0.9` was read correctly here and ignored at the edge, so
|
|
261
|
+
// a visitor sending it got one locale decided at the edge and another here.
|
|
262
|
+
//
|
|
263
|
+
// `caseSensitive` because these are BCP 47 tags and the chosen one is
|
|
264
|
+
// returned to the caller — a browser asking for `de-AT` should get `de-AT`
|
|
265
|
+
// back, not `de-at`. Matching lowercases separately in _primarySubtag.
|
|
266
|
+
//
|
|
267
|
+
// A tag carrying `q=0` is dropped rather than ranked last: q=0 means "not
|
|
268
|
+
// acceptable", and keeping it meant a locale the visitor had explicitly
|
|
269
|
+
// ruled out could still be selected when nothing else matched.
|
|
270
|
+
return b.requestHelpers.parseQualityList(raw, { caseSensitive: true })
|
|
271
|
+
.filter(function (e) {
|
|
272
|
+
return e.value !== "*" && e.q > 0 && LOCALE_RE.test(e.value);
|
|
273
|
+
})
|
|
274
|
+
.map(function (e) { return { tag: e.value, q: e.q }; });
|
|
285
275
|
}
|
|
286
276
|
|
|
287
277
|
function _primarySubtag(tag) {
|
package/lib/gift-options.js
CHANGED
|
@@ -69,8 +69,6 @@ var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
|
69
69
|
// in adjacent ecosystems. Newlines are allowed in gift_message
|
|
70
70
|
// (people write multi-line messages); the recipient name is a
|
|
71
71
|
// single line and refuses LF / CR too.
|
|
72
|
-
var CONTROL_BYTE_MSG_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
73
|
-
var CONTROL_BYTE_NAME_RE = /[\x00-\x1f\x7f]/;
|
|
74
72
|
|
|
75
73
|
// Zero-width / direction-override family — mirrors the order-notes
|
|
76
74
|
// primitive's catalogue: ZWSP/ZWNJ/ZWJ (U+200B-200D), LRM/RLM
|
|
@@ -79,15 +77,13 @@ var CONTROL_BYTE_NAME_RE = /[\x00-\x1f\x7f]/;
|
|
|
79
77
|
// (U+2066-U+2069), the BOM (U+FEFF), and the Arabic letter mark
|
|
80
78
|
// (U+061C). Spelled with \u-escapes so ESLint's
|
|
81
79
|
// no-irregular-whitespace stays happy.
|
|
82
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
83
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
84
|
-
);
|
|
85
80
|
|
|
86
81
|
var ALLOWED_WRAP_COLUMNS = Object.freeze([
|
|
87
82
|
"title", "fee_minor", "image_url", "max_per_order", "active",
|
|
88
83
|
]);
|
|
89
84
|
|
|
90
85
|
var b = require("./vendor/blamejs");
|
|
86
|
+
var textGuard = require("./text-guard");
|
|
91
87
|
|
|
92
88
|
// ---- validators ---------------------------------------------------------
|
|
93
89
|
|
|
@@ -107,9 +103,7 @@ function _title(s) {
|
|
|
107
103
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
108
104
|
throw new TypeError("giftOptions: title must be a non-empty string ≤ " + MAX_TITLE_LEN + " chars");
|
|
109
105
|
}
|
|
110
|
-
|
|
111
|
-
throw new TypeError("giftOptions: title contains control / zero-width bytes");
|
|
112
|
-
}
|
|
106
|
+
textGuard.freeText(s, "giftOptions: title", { singleLine: "reject", zeroWidth: "reject" });
|
|
113
107
|
return s;
|
|
114
108
|
}
|
|
115
109
|
|
|
@@ -133,9 +127,7 @@ function _imageUrl(s) {
|
|
|
133
127
|
if (!(/^https:\/\//.test(s) || /^\/\//.test(s) || /^\//.test(s))) {
|
|
134
128
|
throw new TypeError("giftOptions: image_url must be https://, // (protocol-relative), or / (absolute path)");
|
|
135
129
|
}
|
|
136
|
-
|
|
137
|
-
throw new TypeError("giftOptions: image_url contains control / zero-width bytes");
|
|
138
|
-
}
|
|
130
|
+
textGuard.freeText(s, "giftOptions: image_url", { singleLine: "reject", zeroWidth: "reject" });
|
|
139
131
|
return s;
|
|
140
132
|
}
|
|
141
133
|
|
|
@@ -162,12 +154,7 @@ function _giftMessage(s) {
|
|
|
162
154
|
if (s.length > MAX_MESSAGE_LEN) {
|
|
163
155
|
throw new TypeError("giftOptions: gift_message must be ≤ " + MAX_MESSAGE_LEN + " chars");
|
|
164
156
|
}
|
|
165
|
-
|
|
166
|
-
throw new TypeError("giftOptions: gift_message contains control bytes");
|
|
167
|
-
}
|
|
168
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
169
|
-
throw new TypeError("giftOptions: gift_message contains zero-width / direction-override characters");
|
|
170
|
-
}
|
|
157
|
+
textGuard.freeText(s, "giftOptions: gift_message", { zeroWidth: "reject" });
|
|
171
158
|
return s;
|
|
172
159
|
}
|
|
173
160
|
|
|
@@ -179,12 +166,7 @@ function _recipientName(s) {
|
|
|
179
166
|
if (s.length > MAX_RECIPIENT_LEN) {
|
|
180
167
|
throw new TypeError("giftOptions: recipient_name must be ≤ " + MAX_RECIPIENT_LEN + " chars");
|
|
181
168
|
}
|
|
182
|
-
|
|
183
|
-
throw new TypeError("giftOptions: recipient_name contains control bytes (incl. CR/LF)");
|
|
184
|
-
}
|
|
185
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
186
|
-
throw new TypeError("giftOptions: recipient_name contains zero-width / direction-override characters");
|
|
187
|
-
}
|
|
169
|
+
textGuard.freeText(s, "giftOptions: recipient_name", { singleLine: "reject", zeroWidth: "reject" });
|
|
188
170
|
return s;
|
|
189
171
|
}
|
|
190
172
|
|