@blamejs/blamejs-shop 0.5.20 → 0.5.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/SECURITY.md +23 -0
- package/lib/addresses.js +17 -3
- 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 +7 -20
- package/lib/cms-blocks.js +4 -7
- package/lib/compliance-export.js +11 -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 +39 -15
- package/lib/email-warmup.js +2 -7
- package/lib/experiments.js +6 -10
- package/lib/externaldb-d1.js +47 -31
- 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 +7 -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 +21 -28
- 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 +179 -35
- package/lib/theme-assets.js +4 -16
- package/lib/translations.js +3 -6
- package/lib/trust-badges.js +4 -7
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs/CHANGELOG.md +32 -0
- package/lib/vendor/blamejs/NOTICE +1 -1
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/lib/network-tls.js +98 -13
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +568 -304
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/sbom.cdx.json +6 -6
- 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/order-escalation.js
CHANGED
|
@@ -61,7 +61,6 @@ var COUNTRY_RE = /^[A-Z]{2}$/;
|
|
|
61
61
|
var PAYMENT_KIND_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
62
62
|
var TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._:\-]{0,63}$/;
|
|
63
63
|
var TEMPLATE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
64
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
65
64
|
|
|
66
65
|
var SEVERITIES = Object.freeze(["info", "warning", "critical"]);
|
|
67
66
|
var SEVERITY_RANK = Object.freeze({ info: 0, warning: 1, critical: 2 });
|
|
@@ -85,6 +84,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
|
|
|
85
84
|
]);
|
|
86
85
|
|
|
87
86
|
var b = require("./vendor/blamejs");
|
|
87
|
+
var textGuard = require("./text-guard");
|
|
88
88
|
|
|
89
89
|
// ---- monotonic clock ----------------------------------------------------
|
|
90
90
|
//
|
|
@@ -322,7 +322,7 @@ function _freeText(v, label, max) {
|
|
|
322
322
|
if (v.length > max) {
|
|
323
323
|
throw new TypeError("orderEscalation: " + label + " must be <= " + max + " characters");
|
|
324
324
|
}
|
|
325
|
-
if (
|
|
325
|
+
if (textGuard.hasCodepointThreat(v)) {
|
|
326
326
|
throw new TypeError("orderEscalation: " + label + " must not contain control bytes");
|
|
327
327
|
}
|
|
328
328
|
return v;
|
package/lib/order-export.js
CHANGED
|
@@ -117,6 +117,7 @@ var DEFAULT_BATCH_SIZE = 500;
|
|
|
117
117
|
var MAX_BATCH_SIZE = 5000;
|
|
118
118
|
var MAX_LIST_LIMIT = 200;
|
|
119
119
|
var EMAIL_HASH_PREFIX = "order-export-email";
|
|
120
|
+
var CSV_LINE_ENDING = "\r\n"; // RFC 4180
|
|
120
121
|
var FORMATS = Object.freeze(["csv", "ndjson"]);
|
|
121
122
|
var EXPORT_STATUSES = Object.freeze(["queued", "running", "complete", "failed", "cancelled"]);
|
|
122
123
|
|
|
@@ -209,17 +210,6 @@ function _limit(n) {
|
|
|
209
210
|
|
|
210
211
|
// ---- CSV helpers --------------------------------------------------------
|
|
211
212
|
|
|
212
|
-
// RFC-4180 quoting: every cell wrapped in `"`, embedded `"` doubled.
|
|
213
|
-
// We quote unconditionally — the cost is a few extra bytes per cell;
|
|
214
|
-
// the win is that a downstream parser never has to track quote-vs-
|
|
215
|
-
// bare-cell state for a column with mixed shapes. The injection
|
|
216
|
-
// neutralization runs first via the shared vendored primitive.
|
|
217
|
-
function _csvCell(value) {
|
|
218
|
-
var s = _coerceCell(value);
|
|
219
|
-
s = _neutralizeInjection(s);
|
|
220
|
-
return '"' + s.replace(/"/g, '""') + '"';
|
|
221
|
-
}
|
|
222
|
-
|
|
223
213
|
function _coerceCell(value) {
|
|
224
214
|
if (value == null) return "";
|
|
225
215
|
if (typeof value === "string") return value;
|
|
@@ -239,14 +229,29 @@ function _coerceCell(value) {
|
|
|
239
229
|
// numerics (`+15.00`); the shared primitive prefixes those too (the safe
|
|
240
230
|
// OWASP posture — `-2+3+cmd|…` is a real injection that begins like an
|
|
241
231
|
// amount), which is the more complete behavior this consolidation buys.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
232
|
+
// One export row, RFC 4180.
|
|
233
|
+
//
|
|
234
|
+
// b.guardCsv.serialize does the whole line: the formula-injection prefix on
|
|
235
|
+
// every cell, the unconditional quoting, doubling an embedded quote, the
|
|
236
|
+
// delimiter and the CRLF. Only the injection step was composed before; the
|
|
237
|
+
// quoting rules were written here and again in customer-segments.js.
|
|
238
|
+
//
|
|
239
|
+
// Cells are quoted unconditionally. It costs a few bytes and means a
|
|
240
|
+
// downstream parser never has to track quote-versus-bare state on a column
|
|
241
|
+
// with mixed shapes.
|
|
242
|
+
//
|
|
243
|
+
// serialize emits the rows it is given WITHOUT a trailing terminator, which is
|
|
244
|
+
// right for a whole document and wrong for a generator yielding one row at a
|
|
245
|
+
// time — so the line ending is appended here. Verified byte-identical to the
|
|
246
|
+
// previous builder across formula-injection, embedded-quote, embedded-comma,
|
|
247
|
+
// newline, tab and empty cells.
|
|
246
248
|
function _csvRow(cells) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
return b.guardCsv.serialize([cells.map(_coerceCell)], {
|
|
250
|
+
profile: "strict",
|
|
251
|
+
alwaysQuote: true,
|
|
252
|
+
headers: false,
|
|
253
|
+
lineEnding: CSV_LINE_ENDING,
|
|
254
|
+
}) + CSV_LINE_ENDING;
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
// ---- row projection -----------------------------------------------------
|
|
@@ -758,8 +763,7 @@ function create(opts) {
|
|
|
758
763
|
// Exposed for the test suite so it can assert column semantics
|
|
759
764
|
// without having to thread orders through a full DB round-trip.
|
|
760
765
|
_projectOrder: _projectOrder,
|
|
761
|
-
|
|
762
|
-
_neutralizeInjection: _neutralizeInjection,
|
|
766
|
+
_csvRow: _csvRow,
|
|
763
767
|
// Exposed only to keep the linter quiet about the unused `order`
|
|
764
768
|
// binding when an integrator wires it later — the export reads
|
|
765
769
|
// directly today, but the indirection is preserved.
|
package/lib/order-notes.js
CHANGED
|
@@ -46,7 +46,6 @@ var ALLOWED_VISIBILITY = ["internal", "customer_visible"];
|
|
|
46
46
|
// Refuse C0 control bytes + DEL + the LRM/RLM/ZWJ/ZWNJ family of
|
|
47
47
|
// zero-width / direction-override characters. Newlines (LF, CR) +
|
|
48
48
|
// tab are tolerated because operator notes legitimately wrap.
|
|
49
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
50
49
|
// Zero-width + direction-override family: ZWSP/ZWNJ/ZWJ (U+200B-200D),
|
|
51
50
|
// LRM/RLM (U+200E/U+200F), the bidi-formatting block
|
|
52
51
|
// (U+202A-U+202E), the invisible-math block (U+2060-U+2064), the
|
|
@@ -54,9 +53,6 @@ var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
|
54
53
|
// Arabic letter mark (U+061C). Spelled with \u-escapes so the
|
|
55
54
|
// detector pattern stays grep-able and ESLint's
|
|
56
55
|
// no-irregular-whitespace stays happy.
|
|
57
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
58
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
59
|
-
);
|
|
60
56
|
|
|
61
57
|
// (pinned DESC, created_at DESC, id DESC) — pinned notes float to
|
|
62
58
|
// the top; the id tie-break keeps cursor pagination deterministic
|
|
@@ -65,6 +61,7 @@ var LIST_ORDER_KEY = ["pinned:desc", "created_at:desc", "id:desc"];
|
|
|
65
61
|
|
|
66
62
|
// Framework handle (the vendored blamejs); index.js re-exports this as .framework.
|
|
67
63
|
var b = require("./vendor/blamejs");
|
|
64
|
+
var textGuard = require("./text-guard");
|
|
68
65
|
|
|
69
66
|
// ---- validators ---------------------------------------------------------
|
|
70
67
|
|
|
@@ -98,12 +95,7 @@ function _body(s) {
|
|
|
98
95
|
if (s.length > MAX_BODY_LEN) {
|
|
99
96
|
throw new TypeError("orderNotes: body must be <= " + MAX_BODY_LEN + " characters");
|
|
100
97
|
}
|
|
101
|
-
|
|
102
|
-
throw new TypeError("orderNotes: body contains control bytes");
|
|
103
|
-
}
|
|
104
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
105
|
-
throw new TypeError("orderNotes: body contains zero-width / direction-override characters");
|
|
106
|
-
}
|
|
98
|
+
textGuard.freeText(s, "orderNotes: body", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
107
99
|
return s;
|
|
108
100
|
}
|
|
109
101
|
|
|
@@ -124,7 +116,7 @@ function _tags(input) {
|
|
|
124
116
|
if (t.length > MAX_TAG_LEN) {
|
|
125
117
|
throw new TypeError("orderNotes: tags[" + i + "] must be <= " + MAX_TAG_LEN + " characters");
|
|
126
118
|
}
|
|
127
|
-
if (
|
|
119
|
+
if (textGuard.hasCodepointThreat(t, { zeroWidth: "reject" })) {
|
|
128
120
|
throw new TypeError("orderNotes: tags[" + i + "] contains control / zero-width bytes");
|
|
129
121
|
}
|
|
130
122
|
out.push(t);
|
|
@@ -139,9 +131,7 @@ function _resolution(s) {
|
|
|
139
131
|
if (s.length > MAX_RESOLUTION_LEN) {
|
|
140
132
|
throw new TypeError("orderNotes: resolution must be <= " + MAX_RESOLUTION_LEN + " characters");
|
|
141
133
|
}
|
|
142
|
-
|
|
143
|
-
throw new TypeError("orderNotes: resolution contains control / zero-width bytes");
|
|
144
|
-
}
|
|
134
|
+
textGuard.freeText(s, "orderNotes: resolution", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
145
135
|
return s;
|
|
146
136
|
}
|
|
147
137
|
|
package/lib/order-ratings.js
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
*/
|
|
75
75
|
|
|
76
76
|
var b = require("./vendor/blamejs");
|
|
77
|
+
var textGuard = require("./text-guard");
|
|
77
78
|
|
|
78
79
|
// ---- constants ----------------------------------------------------------
|
|
79
80
|
|
|
@@ -88,7 +89,6 @@ var MAX_LIST_LIMIT = 500;
|
|
|
88
89
|
var DEFAULT_TOP_LIMIT = 10;
|
|
89
90
|
var MAX_TOP_LIMIT = 100;
|
|
90
91
|
|
|
91
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
92
92
|
|
|
93
93
|
// ---- monotonic clock ----------------------------------------------------
|
|
94
94
|
//
|
|
@@ -130,9 +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
|
-
|
|
134
|
-
throw new TypeError("orderRatings: comment must not contain control bytes");
|
|
135
|
-
}
|
|
133
|
+
textGuard.freeText(s, "orderRatings: comment", { bidiMarks: "allow" });
|
|
136
134
|
return s;
|
|
137
135
|
}
|
|
138
136
|
|
|
@@ -140,9 +138,7 @@ function _flagReason(s) {
|
|
|
140
138
|
if (typeof s !== "string" || !s.length || s.length > MAX_FLAG_REASON_LEN) {
|
|
141
139
|
throw new TypeError("orderRatings: reason must be a non-empty string <= " + MAX_FLAG_REASON_LEN + " chars");
|
|
142
140
|
}
|
|
143
|
-
|
|
144
|
-
throw new TypeError("orderRatings: reason must not contain control bytes");
|
|
145
|
-
}
|
|
141
|
+
textGuard.freeText(s, "orderRatings: reason", { bidiMarks: "allow" });
|
|
146
142
|
return s;
|
|
147
143
|
}
|
|
148
144
|
|
|
@@ -150,9 +146,7 @@ function _responseText(s) {
|
|
|
150
146
|
if (typeof s !== "string" || !s.length || s.length > MAX_RESPONSE_LEN) {
|
|
151
147
|
throw new TypeError("orderRatings: response must be a non-empty string <= " + MAX_RESPONSE_LEN + " chars");
|
|
152
148
|
}
|
|
153
|
-
|
|
154
|
-
throw new TypeError("orderRatings: response must not contain control bytes");
|
|
155
|
-
}
|
|
149
|
+
textGuard.freeText(s, "orderRatings: response", { bidiMarks: "allow" });
|
|
156
150
|
return s;
|
|
157
151
|
}
|
|
158
152
|
|
package/lib/payment-methods.js
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
51
|
var b = require("./vendor/blamejs");
|
|
52
|
+
var textGuard = require("./text-guard");
|
|
52
53
|
|
|
53
54
|
var PROCESSORS = ["stripe", "paypal", "square", "braintree", "authorize_net"];
|
|
54
55
|
var ARCHIVE_REASONS = ["customer_request", "expired", "replaced", "fraud", "operator"];
|
|
@@ -65,7 +66,6 @@ var MAX_ACTOR_LEN = 128;
|
|
|
65
66
|
// branch-on-this dispatch key.
|
|
66
67
|
var BRAND_RE = /^[A-Za-z0-9 _.\-]{1,32}$/;
|
|
67
68
|
var LAST4_RE = /^[0-9]{4}$/;
|
|
68
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
69
69
|
|
|
70
70
|
// PAN screen: any field whose value is a string carrying 13-19
|
|
71
71
|
// consecutive ASCII digits (with or without space/dash separators
|
|
@@ -125,9 +125,7 @@ function _brand(s) {
|
|
|
125
125
|
if (s.length > MAX_BRAND_LEN) {
|
|
126
126
|
throw new TypeError("paymentMethods: brand must be <= " + MAX_BRAND_LEN + " characters");
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
throw new TypeError("paymentMethods: brand contains control bytes");
|
|
130
|
-
}
|
|
128
|
+
textGuard.freeText(s, "paymentMethods: brand", { singleLine: "reject" });
|
|
131
129
|
if (!BRAND_RE.test(s)) {
|
|
132
130
|
throw new TypeError("paymentMethods: brand contains characters outside [A-Za-z0-9 _.-]");
|
|
133
131
|
}
|
|
@@ -142,9 +140,7 @@ function _label(s) {
|
|
|
142
140
|
if (s.length > MAX_LABEL_LEN) {
|
|
143
141
|
throw new TypeError("paymentMethods: label must be <= " + MAX_LABEL_LEN + " characters");
|
|
144
142
|
}
|
|
145
|
-
|
|
146
|
-
throw new TypeError("paymentMethods: label contains control bytes");
|
|
147
|
-
}
|
|
143
|
+
textGuard.freeText(s, "paymentMethods: label", { singleLine: "reject", bidiMarks: "allow" });
|
|
148
144
|
return s;
|
|
149
145
|
}
|
|
150
146
|
|
|
@@ -155,9 +151,7 @@ function _token(s) {
|
|
|
155
151
|
if (s.length > MAX_TOKEN_LEN) {
|
|
156
152
|
throw new TypeError("paymentMethods: processor_token must be <= " + MAX_TOKEN_LEN + " characters");
|
|
157
153
|
}
|
|
158
|
-
|
|
159
|
-
throw new TypeError("paymentMethods: processor_token contains control bytes");
|
|
160
|
-
}
|
|
154
|
+
textGuard.freeText(s, "paymentMethods: processor_token", { singleLine: "reject" });
|
|
161
155
|
return s;
|
|
162
156
|
}
|
|
163
157
|
|
|
@@ -187,9 +181,7 @@ function _actor(s) {
|
|
|
187
181
|
if (s.length > MAX_ACTOR_LEN) {
|
|
188
182
|
throw new TypeError("paymentMethods: actor must be <= " + MAX_ACTOR_LEN + " characters");
|
|
189
183
|
}
|
|
190
|
-
|
|
191
|
-
throw new TypeError("paymentMethods: actor contains control bytes");
|
|
192
|
-
}
|
|
184
|
+
textGuard.freeText(s, "paymentMethods: actor", { singleLine: "reject" });
|
|
193
185
|
return s;
|
|
194
186
|
}
|
|
195
187
|
|
package/lib/payment-retries.js
CHANGED
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
*/
|
|
130
130
|
|
|
131
131
|
var b = require("./vendor/blamejs");
|
|
132
|
+
var textGuard = require("./text-guard");
|
|
132
133
|
var C = b.constants;
|
|
133
134
|
|
|
134
135
|
// ---- constants ----------------------------------------------------------
|
|
@@ -147,10 +148,6 @@ var MS_PER_HOUR = C.TIME.hours(1);
|
|
|
147
148
|
|
|
148
149
|
var SLUG_RE = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
149
150
|
var FAILURE_CODE_RE = /^[a-z](?:[a-z0-9_]*[a-z0-9])?$/;
|
|
150
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
151
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
152
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
153
|
-
);
|
|
154
151
|
|
|
155
152
|
// ---- validators ---------------------------------------------------------
|
|
156
153
|
|
|
@@ -232,9 +229,7 @@ function _reason(s) {
|
|
|
232
229
|
if (s.length > MAX_REASON_LEN) {
|
|
233
230
|
throw new TypeError("paymentRetries: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
234
231
|
}
|
|
235
|
-
|
|
236
|
-
throw new TypeError("paymentRetries: reason contains control / zero-width bytes");
|
|
237
|
-
}
|
|
232
|
+
textGuard.freeText(s, "paymentRetries: reason", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
238
233
|
return s;
|
|
239
234
|
}
|
|
240
235
|
|
package/lib/pixel-events.js
CHANGED
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
var nodeCrypto = require("node:crypto"); // allow:non-shop-require allow:weak-hash-sha2 — Provider spec (Meta CAPI, Google EC, TikTok Events, Pinterest CAPI, Snap CAPI) mandates SHA-256 of the normalised identifier; blamejs.crypto exposes SHA3-512 / SHAKE256 / namespaceHash, none of which match the wire format ad platforms accept. Node's stdlib SHA-256 hash is the minimum-surface route.
|
|
100
100
|
|
|
101
101
|
var b = require("./vendor/blamejs");
|
|
102
|
+
var textGuard = require("./text-guard");
|
|
102
103
|
var C = b.constants;
|
|
103
104
|
|
|
104
105
|
// ---- constants ----------------------------------------------------------
|
|
@@ -132,10 +133,6 @@ var CURRENCY_RE = /^[A-Z]{3}$/;
|
|
|
132
133
|
var UA_CLASS_RE = /^[a-z0-9._-]+$/;
|
|
133
134
|
var IP_HASH_RE = /^[0-9a-f]+$/;
|
|
134
135
|
var SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
135
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
136
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
137
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
138
|
-
);
|
|
139
136
|
// Conservative email shape — operator-facing storefronts already
|
|
140
137
|
// validated this via the framework's address-input primitive; the
|
|
141
138
|
// gate here is a defensive last-line check before we normalise +
|
|
@@ -218,9 +215,7 @@ function _pixelId(s) {
|
|
|
218
215
|
if (s.length > MAX_PIXEL_ID_LEN) {
|
|
219
216
|
throw new TypeError("pixelEvents: pixel_id must be <= " + MAX_PIXEL_ID_LEN + " characters");
|
|
220
217
|
}
|
|
221
|
-
|
|
222
|
-
throw new TypeError("pixelEvents: pixel_id contains control / zero-width bytes");
|
|
223
|
-
}
|
|
218
|
+
textGuard.freeText(s, "pixelEvents: pixel_id", { zeroWidth: "reject" });
|
|
224
219
|
return s;
|
|
225
220
|
}
|
|
226
221
|
|
|
@@ -231,9 +226,7 @@ function _accessToken(s) {
|
|
|
231
226
|
if (s.length > MAX_ACCESS_TOKEN_LEN) {
|
|
232
227
|
throw new TypeError("pixelEvents: access_token must be <= " + MAX_ACCESS_TOKEN_LEN + " characters");
|
|
233
228
|
}
|
|
234
|
-
|
|
235
|
-
throw new TypeError("pixelEvents: access_token contains control / zero-width bytes");
|
|
236
|
-
}
|
|
229
|
+
textGuard.freeText(s, "pixelEvents: access_token", { zeroWidth: "reject" });
|
|
237
230
|
return s;
|
|
238
231
|
}
|
|
239
232
|
|
|
@@ -244,9 +237,7 @@ function _conversionUrl(s) {
|
|
|
244
237
|
if (s.length > MAX_URL_LEN) {
|
|
245
238
|
throw new TypeError("pixelEvents: conversion_url must be <= " + MAX_URL_LEN + " characters");
|
|
246
239
|
}
|
|
247
|
-
|
|
248
|
-
throw new TypeError("pixelEvents: conversion_url contains control / zero-width bytes");
|
|
249
|
-
}
|
|
240
|
+
textGuard.freeText(s, "pixelEvents: conversion_url", { zeroWidth: "reject" });
|
|
250
241
|
// Every supported provider's conversion API is public HTTPS — refuse
|
|
251
242
|
// anything else so an operator typo can't egress conversion data in
|
|
252
243
|
// plaintext.
|
|
@@ -276,9 +267,7 @@ function _eventSourceUrl(s) {
|
|
|
276
267
|
if (s.length > MAX_URL_LEN) {
|
|
277
268
|
throw new TypeError("pixelEvents: event_source_url must be <= " + MAX_URL_LEN + " characters");
|
|
278
269
|
}
|
|
279
|
-
|
|
280
|
-
throw new TypeError("pixelEvents: event_source_url contains control / zero-width bytes");
|
|
281
|
-
}
|
|
270
|
+
textGuard.freeText(s, "pixelEvents: event_source_url", { zeroWidth: "reject" });
|
|
282
271
|
if (!/^https?:\/\//i.test(s)) {
|
|
283
272
|
throw new TypeError("pixelEvents: event_source_url must be an absolute http(s):// URL");
|
|
284
273
|
}
|
|
@@ -293,9 +282,7 @@ function _email(s) {
|
|
|
293
282
|
if (s.length > MAX_EMAIL_LEN) {
|
|
294
283
|
throw new TypeError("pixelEvents: customer_email must be <= " + MAX_EMAIL_LEN + " characters");
|
|
295
284
|
}
|
|
296
|
-
|
|
297
|
-
throw new TypeError("pixelEvents: customer_email contains control / zero-width bytes");
|
|
298
|
-
}
|
|
285
|
+
textGuard.freeText(s, "pixelEvents: customer_email", { zeroWidth: "reject" });
|
|
299
286
|
// Validate the trimmed form — normalisation strips leading /
|
|
300
287
|
// trailing whitespace before hashing, so a " alice@example.com "
|
|
301
288
|
// input still hashes to the canonical address. Reject only when
|
|
@@ -402,7 +389,7 @@ function _reason(s, label) {
|
|
|
402
389
|
if (s.length > MAX_REASON_LEN) {
|
|
403
390
|
throw new TypeError("pixelEvents: " + label + " must be <= " + MAX_REASON_LEN + " characters");
|
|
404
391
|
}
|
|
405
|
-
if (
|
|
392
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
406
393
|
throw new TypeError("pixelEvents: " + label + " contains control / zero-width bytes");
|
|
407
394
|
}
|
|
408
395
|
return s;
|
|
@@ -416,14 +403,20 @@ function _responseBody(s) {
|
|
|
416
403
|
if (s.length > MAX_RESPONSE_BODY_LEN) {
|
|
417
404
|
throw new TypeError("pixelEvents: response_body must be <= " + MAX_RESPONSE_BODY_LEN + " characters");
|
|
418
405
|
}
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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);
|
|
427
420
|
}
|
|
428
421
|
|
|
429
422
|
function _responseStatus(n) {
|
package/lib/plan-changes.js
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
*/
|
|
62
62
|
|
|
63
63
|
var b = require("./vendor/blamejs");
|
|
64
|
+
var textGuard = require("./text-guard");
|
|
64
65
|
|
|
65
66
|
// Observability sink for batch-path settlement failures: the scheduler
|
|
66
67
|
// reverts a due row to 'pending' and carries on when a Stripe push or a
|
|
@@ -79,10 +80,6 @@ var MAX_REASON_LEN = 280;
|
|
|
79
80
|
// can land in cancel_reason and replay into the customer profile
|
|
80
81
|
// screen, so the same "no direction-override / no invisible glyph"
|
|
81
82
|
// floor applies.
|
|
82
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
83
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
84
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
85
|
-
);
|
|
86
83
|
|
|
87
84
|
// ---- validators ---------------------------------------------------------
|
|
88
85
|
|
|
@@ -118,9 +115,7 @@ function _optReason(s) {
|
|
|
118
115
|
if (s.length > MAX_REASON_LEN) {
|
|
119
116
|
throw new TypeError("planChanges: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
120
117
|
}
|
|
121
|
-
|
|
122
|
-
throw new TypeError("planChanges: reason contains control / zero-width bytes");
|
|
123
|
-
}
|
|
118
|
+
textGuard.freeText(s, "planChanges: reason", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
124
119
|
return s;
|
|
125
120
|
}
|
|
126
121
|
|
package/lib/product-qa.js
CHANGED
|
@@ -81,9 +81,9 @@ var MIN_CLEANUP_DAYS = 1;
|
|
|
81
81
|
var STATUSES = Object.freeze(["pending", "approved", "rejected"]);
|
|
82
82
|
var AUTHORS = Object.freeze(["operator", "customer", "system"]);
|
|
83
83
|
|
|
84
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
85
84
|
|
|
86
85
|
var b = require("./vendor/blamejs");
|
|
86
|
+
var textGuard = require("./text-guard");
|
|
87
87
|
var C = b.constants;
|
|
88
88
|
|
|
89
89
|
// ---- monotonic clock ---------------------------------------------------
|
|
@@ -116,7 +116,7 @@ function _body(s, label) {
|
|
|
116
116
|
if (s.length > MAX_BODY_LEN) {
|
|
117
117
|
throw new TypeError("productQA: " + label + " must be <= " + MAX_BODY_LEN + " characters");
|
|
118
118
|
}
|
|
119
|
-
if (
|
|
119
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
120
120
|
throw new TypeError("productQA: " + label + " must not contain control bytes");
|
|
121
121
|
}
|
|
122
122
|
return s;
|
|
@@ -131,9 +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
|
-
|
|
135
|
-
throw new TypeError("productQA: reason must not contain control bytes");
|
|
136
|
-
}
|
|
134
|
+
textGuard.freeText(s, "productQA: reason", { bidiMarks: "allow" });
|
|
137
135
|
return s;
|
|
138
136
|
}
|
|
139
137
|
|
|
@@ -167,9 +165,7 @@ function _sessionId(s) {
|
|
|
167
165
|
if (s.length > MAX_SESSION_ID_LEN) {
|
|
168
166
|
throw new TypeError("productQA: session_id must be <= " + MAX_SESSION_ID_LEN + " characters");
|
|
169
167
|
}
|
|
170
|
-
|
|
171
|
-
throw new TypeError("productQA: session_id must not contain control bytes");
|
|
172
|
-
}
|
|
168
|
+
textGuard.freeText(s, "productQA: session_id");
|
|
173
169
|
return s;
|
|
174
170
|
}
|
|
175
171
|
|
package/lib/promo-banners.js
CHANGED
|
@@ -102,15 +102,10 @@ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
|
102
102
|
// operator dashboards. Newlines are allowed in `body` (multi-line
|
|
103
103
|
// marketing copy is common); `headline` / `cta_label` refuse LF / CR
|
|
104
104
|
// so a single-line presentation slot can't be smuggled into.
|
|
105
|
-
var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
|
|
106
|
-
var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
107
105
|
|
|
108
106
|
// Zero-width / direction-override family — mirrors the gift-options
|
|
109
107
|
// + order-notes catalogues. Spelled with \u-escapes so ESLint's
|
|
110
108
|
// no-irregular-whitespace stays happy.
|
|
111
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
112
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
113
|
-
);
|
|
114
109
|
|
|
115
110
|
var ALLOWED_PATCH_COLUMNS = Object.freeze([
|
|
116
111
|
"placement",
|
|
@@ -128,6 +123,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
|
|
|
128
123
|
]);
|
|
129
124
|
|
|
130
125
|
var b = require("./vendor/blamejs");
|
|
126
|
+
var textGuard = require("./text-guard");
|
|
131
127
|
|
|
132
128
|
// ---- validators ---------------------------------------------------------
|
|
133
129
|
|
|
@@ -164,10 +160,10 @@ function _line(s, label, maxLen) {
|
|
|
164
160
|
if (typeof s !== "string" || !s.length || s.length > maxLen) {
|
|
165
161
|
throw new TypeError("promoBanners: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
|
|
166
162
|
}
|
|
167
|
-
if (
|
|
163
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
168
164
|
throw new TypeError("promoBanners: " + label + " contains control bytes (incl. CR/LF)");
|
|
169
165
|
}
|
|
170
|
-
if (
|
|
166
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
171
167
|
throw new TypeError("promoBanners: " + label + " contains zero-width / direction-override characters");
|
|
172
168
|
}
|
|
173
169
|
return s;
|
|
@@ -181,10 +177,10 @@ function _block(s, label, maxLen) {
|
|
|
181
177
|
if (s.length > maxLen) {
|
|
182
178
|
throw new TypeError("promoBanners: " + label + " must be ≤ " + maxLen + " chars");
|
|
183
179
|
}
|
|
184
|
-
if (
|
|
180
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
185
181
|
throw new TypeError("promoBanners: " + label + " contains control bytes");
|
|
186
182
|
}
|
|
187
|
-
if (
|
|
183
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
188
184
|
throw new TypeError("promoBanners: " + label + " contains zero-width / direction-override characters");
|
|
189
185
|
}
|
|
190
186
|
return s;
|
|
@@ -200,9 +196,7 @@ function _ctaUrl(s) {
|
|
|
200
196
|
if (typeof s !== "string" || !s.length || s.length > MAX_CTA_URL_LEN) {
|
|
201
197
|
throw new TypeError("promoBanners: cta_url must be a non-empty string ≤ " + MAX_CTA_URL_LEN + " chars");
|
|
202
198
|
}
|
|
203
|
-
|
|
204
|
-
throw new TypeError("promoBanners: cta_url contains control / zero-width bytes");
|
|
205
|
-
}
|
|
199
|
+
textGuard.freeText(s, "promoBanners: cta_url", { singleLine: "reject", zeroWidth: "reject" });
|
|
206
200
|
if (s.charCodeAt(0) === 47 /* "/" */) {
|
|
207
201
|
// /-rooted absolute path — refuse protocol-relative `//host`
|
|
208
202
|
// (the second char being "/" turns the link into an off-site
|
|
@@ -230,9 +224,7 @@ function _imageUrl(s) {
|
|
|
230
224
|
if (typeof s !== "string" || !s.length || s.length > MAX_IMAGE_URL_LEN) {
|
|
231
225
|
throw new TypeError("promoBanners: image_url must be a non-empty string ≤ " + MAX_IMAGE_URL_LEN + " chars");
|
|
232
226
|
}
|
|
233
|
-
|
|
234
|
-
throw new TypeError("promoBanners: image_url contains control / zero-width bytes");
|
|
235
|
-
}
|
|
227
|
+
textGuard.freeText(s, "promoBanners: image_url", { singleLine: "reject", zeroWidth: "reject" });
|
|
236
228
|
if (s.charCodeAt(0) === 47 /* "/" */) {
|
|
237
229
|
if (s.length > 1 && s.charCodeAt(1) === 47) {
|
|
238
230
|
throw new TypeError("promoBanners: image_url protocol-relative `//host/...` refused — use absolute https://");
|
package/lib/purchase-orders.js
CHANGED
|
@@ -96,10 +96,6 @@ var REFERENCE_RE = /^[A-Za-z0-9][A-Za-z0-9._\/ -]{0,127}$/;
|
|
|
96
96
|
// as vendors. Operator-rendered text fields refuse these to keep the
|
|
97
97
|
// downstream dashboard / printout safe from header-injection +
|
|
98
98
|
// visual-spoofing attacks.
|
|
99
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
100
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
101
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
102
|
-
);
|
|
103
99
|
|
|
104
100
|
var PO_STATUSES = Object.freeze([
|
|
105
101
|
"draft", "submitted", "confirmed", "partially_received",
|
|
@@ -117,6 +113,7 @@ var ALLOWED_UPDATE_COLUMNS = Object.freeze([
|
|
|
117
113
|
|
|
118
114
|
// Framework handle (the vendored blamejs); index.js re-exports this as .framework.
|
|
119
115
|
var b = require("./vendor/blamejs");
|
|
116
|
+
var textGuard = require("./text-guard");
|
|
120
117
|
|
|
121
118
|
// ---- validators ---------------------------------------------------------
|
|
122
119
|
|
|
@@ -212,10 +209,10 @@ function _shortText(s, label, max) {
|
|
|
212
209
|
if (s.length > max) {
|
|
213
210
|
throw new TypeError("purchase-orders: " + label + " must be <= " + max + " characters");
|
|
214
211
|
}
|
|
215
|
-
if (
|
|
212
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
216
213
|
throw new TypeError("purchase-orders: " + label + " contains control bytes");
|
|
217
214
|
}
|
|
218
|
-
if (
|
|
215
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
219
216
|
throw new TypeError("purchase-orders: " + label + " contains zero-width / direction-override bytes");
|
|
220
217
|
}
|
|
221
218
|
return s;
|