@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/knowledge-base.js
CHANGED
|
@@ -108,11 +108,6 @@ var LOCALE_RE = /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/;
|
|
|
108
108
|
var CATEGORY_RE = /^[a-z][a-z0-9_-]*$/;
|
|
109
109
|
var TAG_RE = /^[a-z][a-z0-9_-]*$/;
|
|
110
110
|
|
|
111
|
-
var CONTROL_BYTE_BODY_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
112
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
113
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
114
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
115
|
-
);
|
|
116
111
|
|
|
117
112
|
var SEARCH_WEIGHTS = Object.freeze({
|
|
118
113
|
title: 3,
|
|
@@ -121,6 +116,7 @@ var SEARCH_WEIGHTS = Object.freeze({
|
|
|
121
116
|
});
|
|
122
117
|
|
|
123
118
|
var b = require("./vendor/blamejs");
|
|
119
|
+
var textGuard = require("./text-guard");
|
|
124
120
|
|
|
125
121
|
// ---- monotonic clock ---------------------------------------------------
|
|
126
122
|
//
|
|
@@ -163,12 +159,7 @@ function _title(s) {
|
|
|
163
159
|
if (s.length > MAX_TITLE_LEN) {
|
|
164
160
|
throw new TypeError("knowledgeBase: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
165
161
|
}
|
|
166
|
-
|
|
167
|
-
throw new TypeError("knowledgeBase: title contains control bytes");
|
|
168
|
-
}
|
|
169
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
170
|
-
throw new TypeError("knowledgeBase: title contains zero-width / direction-override characters");
|
|
171
|
-
}
|
|
162
|
+
textGuard.freeText(s, "knowledgeBase: title", { singleLine: "reject", zeroWidth: "reject" });
|
|
172
163
|
return s;
|
|
173
164
|
}
|
|
174
165
|
|
|
@@ -183,12 +174,10 @@ function _body(s) {
|
|
|
183
174
|
if (s.length > MAX_BODY_LEN) {
|
|
184
175
|
throw new TypeError("knowledgeBase: body must be <= " + MAX_BODY_LEN + " characters");
|
|
185
176
|
}
|
|
186
|
-
if (
|
|
177
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
187
178
|
throw new TypeError("knowledgeBase: body contains control bytes");
|
|
188
179
|
}
|
|
189
|
-
|
|
190
|
-
throw new TypeError("knowledgeBase: body contains zero-width / direction-override characters");
|
|
191
|
-
}
|
|
180
|
+
textGuard.freeText(s, "knowledgeBase: body", { zeroWidth: "reject" });
|
|
192
181
|
return s;
|
|
193
182
|
}
|
|
194
183
|
|
|
@@ -282,9 +271,7 @@ function _queryStr(s) {
|
|
|
282
271
|
if (s.length > MAX_QUERY_LEN) {
|
|
283
272
|
throw new TypeError("knowledgeBase: query must be <= " + MAX_QUERY_LEN + " characters");
|
|
284
273
|
}
|
|
285
|
-
|
|
286
|
-
throw new TypeError("knowledgeBase: query contains control / zero-width bytes");
|
|
287
|
-
}
|
|
274
|
+
textGuard.freeText(s, "knowledgeBase: query", { zeroWidth: "reject" });
|
|
288
275
|
return s;
|
|
289
276
|
}
|
|
290
277
|
|
|
@@ -303,9 +290,7 @@ function _sessionIdRaw(s) {
|
|
|
303
290
|
if (s.length > 256) {
|
|
304
291
|
throw new TypeError("knowledgeBase: session_id must be <= 256 characters");
|
|
305
292
|
}
|
|
306
|
-
|
|
307
|
-
throw new TypeError("knowledgeBase: session_id contains control / zero-width bytes");
|
|
308
|
-
}
|
|
293
|
+
textGuard.freeText(s, "knowledgeBase: session_id", { singleLine: "reject", zeroWidth: "reject" });
|
|
309
294
|
return s;
|
|
310
295
|
}
|
|
311
296
|
|
|
@@ -347,7 +332,7 @@ function _esc(s) {
|
|
|
347
332
|
|
|
348
333
|
function _safeLinkUrl(url) {
|
|
349
334
|
if (typeof url !== "string" || !url.length || url.length > 2048) return null;
|
|
350
|
-
if (
|
|
335
|
+
if (textGuard.hasCodepointThreat(url, { zeroWidth: "reject" })) return null;
|
|
351
336
|
if (url.charCodeAt(0) === 47 /* "/" */) {
|
|
352
337
|
if (url.length > 1 && url.charCodeAt(1) === 47) return null;
|
|
353
338
|
if (url.indexOf("..") !== -1) return null;
|
package/lib/line-gift-wrap.js
CHANGED
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
// ---- constants ----------------------------------------------------------
|
|
70
70
|
|
|
71
71
|
var b = require("./vendor/blamejs");
|
|
72
|
+
var textGuard = require("./text-guard");
|
|
72
73
|
|
|
73
74
|
var C = b.constants;
|
|
74
75
|
|
|
@@ -89,8 +90,6 @@ var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
|
89
90
|
// adjacent ecosystems. Newlines are allowed in gift_message (people
|
|
90
91
|
// write multi-line messages); the recipient name is a single line
|
|
91
92
|
// and refuses LF / CR too.
|
|
92
|
-
var CONTROL_BYTE_MSG_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
93
|
-
var CONTROL_BYTE_NAME_RE = /[\x00-\x1f\x7f]/;
|
|
94
93
|
|
|
95
94
|
// Zero-width / direction-override family — mirrors the gift-options
|
|
96
95
|
// primitive's catalogue: ZWSP/ZWNJ/ZWJ (U+200B-200D), LRM/RLM
|
|
@@ -99,9 +98,6 @@ var CONTROL_BYTE_NAME_RE = /[\x00-\x1f\x7f]/;
|
|
|
99
98
|
// (U+2066-U+2069), the BOM (U+FEFF), and the Arabic letter mark
|
|
100
99
|
// (U+061C). Spelled with \u-escapes so ESLint's
|
|
101
100
|
// no-irregular-whitespace stays happy.
|
|
102
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
103
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
104
|
-
);
|
|
105
101
|
|
|
106
102
|
var BCP47_RE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
107
103
|
|
|
@@ -155,12 +151,7 @@ function _giftMessage(s) {
|
|
|
155
151
|
if (s.length > MAX_MESSAGE_LEN) {
|
|
156
152
|
throw new TypeError("lineGiftWrap: gift_message must be ≤ " + MAX_MESSAGE_LEN + " chars");
|
|
157
153
|
}
|
|
158
|
-
|
|
159
|
-
throw new TypeError("lineGiftWrap: gift_message contains control bytes");
|
|
160
|
-
}
|
|
161
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
162
|
-
throw new TypeError("lineGiftWrap: gift_message contains zero-width / direction-override characters");
|
|
163
|
-
}
|
|
154
|
+
textGuard.freeText(s, "lineGiftWrap: gift_message", { zeroWidth: "reject" });
|
|
164
155
|
return s;
|
|
165
156
|
}
|
|
166
157
|
|
|
@@ -172,12 +163,7 @@ function _recipientName(s) {
|
|
|
172
163
|
if (s.length > MAX_RECIPIENT_LEN) {
|
|
173
164
|
throw new TypeError("lineGiftWrap: recipient_name must be ≤ " + MAX_RECIPIENT_LEN + " chars");
|
|
174
165
|
}
|
|
175
|
-
|
|
176
|
-
throw new TypeError("lineGiftWrap: recipient_name contains control bytes (incl. CR/LF)");
|
|
177
|
-
}
|
|
178
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
179
|
-
throw new TypeError("lineGiftWrap: recipient_name contains zero-width / direction-override characters");
|
|
180
|
-
}
|
|
166
|
+
textGuard.freeText(s, "lineGiftWrap: recipient_name", { singleLine: "reject", zeroWidth: "reject" });
|
|
181
167
|
return s;
|
|
182
168
|
}
|
|
183
169
|
|
package/lib/live-chat.js
CHANGED
|
@@ -98,15 +98,11 @@ var TRANSITIONS = {
|
|
|
98
98
|
abandoned: {},
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
102
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
103
101
|
// Zero-width + direction-override characters. Spelled via \u escapes
|
|
104
102
|
// so the file stays ESLint no-irregular-whitespace clean.
|
|
105
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
106
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
107
|
-
);
|
|
108
103
|
|
|
109
104
|
var b = require("./vendor/blamejs");
|
|
105
|
+
var textGuard = require("./text-guard");
|
|
110
106
|
var C = b.constants;
|
|
111
107
|
|
|
112
108
|
// ---- validators ---------------------------------------------------------
|
|
@@ -134,12 +130,7 @@ function _body(s) {
|
|
|
134
130
|
if (s.length > MAX_BODY_LEN) {
|
|
135
131
|
throw new TypeError("live-chat: body must be <= " + MAX_BODY_LEN + " characters");
|
|
136
132
|
}
|
|
137
|
-
|
|
138
|
-
throw new TypeError("live-chat: body contains control bytes");
|
|
139
|
-
}
|
|
140
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
141
|
-
throw new TypeError("live-chat: body contains zero-width / direction-override characters");
|
|
142
|
-
}
|
|
133
|
+
textGuard.freeText(s, "live-chat: body", { zeroWidth: "reject" });
|
|
143
134
|
return s;
|
|
144
135
|
}
|
|
145
136
|
|
|
@@ -152,7 +143,7 @@ function _reason(r, label) {
|
|
|
152
143
|
if (r.length > MAX_REASON_LEN) {
|
|
153
144
|
throw new TypeError("live-chat: " + (label || "reason") + " must be <= " + MAX_REASON_LEN + " characters");
|
|
154
145
|
}
|
|
155
|
-
if (
|
|
146
|
+
if (textGuard.hasCodepointThreat(r, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
156
147
|
throw new TypeError("live-chat: " + (label || "reason") + " contains control / zero-width bytes");
|
|
157
148
|
}
|
|
158
149
|
return r;
|
|
@@ -165,9 +156,7 @@ function _sourcePage(s) {
|
|
|
165
156
|
if (s.length > MAX_SOURCE_PAGE) {
|
|
166
157
|
throw new TypeError("live-chat: source_page must be <= " + MAX_SOURCE_PAGE + " characters");
|
|
167
158
|
}
|
|
168
|
-
|
|
169
|
-
throw new TypeError("live-chat: source_page contains control / zero-width bytes");
|
|
170
|
-
}
|
|
159
|
+
textGuard.freeText(s, "live-chat: source_page", { singleLine: "reject", zeroWidth: "reject" });
|
|
171
160
|
return s;
|
|
172
161
|
}
|
|
173
162
|
|
|
@@ -178,9 +167,7 @@ function _visitorSessionId(s) {
|
|
|
178
167
|
if (s.length > MAX_SESSION_ID_LEN) {
|
|
179
168
|
throw new TypeError("live-chat: session_id must be <= " + MAX_SESSION_ID_LEN + " characters");
|
|
180
169
|
}
|
|
181
|
-
|
|
182
|
-
throw new TypeError("live-chat: session_id contains control / zero-width bytes");
|
|
183
|
-
}
|
|
170
|
+
textGuard.freeText(s, "live-chat: session_id", { singleLine: "reject", zeroWidth: "reject" });
|
|
184
171
|
return s;
|
|
185
172
|
}
|
|
186
173
|
|
package/lib/metered-usage.js
CHANGED
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
*/
|
|
91
91
|
|
|
92
92
|
var b = require("./vendor/blamejs");
|
|
93
|
+
var textGuard = require("./text-guard");
|
|
93
94
|
|
|
94
95
|
// ---- constants ----------------------------------------------------------
|
|
95
96
|
|
|
@@ -111,10 +112,6 @@ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
|
111
112
|
// primitives — operator-authored copy (title, unit) ends up on
|
|
112
113
|
// receipt emails + the operator dashboard, so the same "no direction-
|
|
113
114
|
// override / no invisible glyph" floor applies.
|
|
114
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
115
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
116
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
117
|
-
);
|
|
118
115
|
|
|
119
116
|
var ALLOWED_PATCH_COLUMNS = Object.freeze([
|
|
120
117
|
"title",
|
|
@@ -146,7 +143,7 @@ function _shortText(s, label, max) {
|
|
|
146
143
|
if (s.length > max) {
|
|
147
144
|
throw new TypeError("meteredUsage: " + label + " must be <= " + max + " characters");
|
|
148
145
|
}
|
|
149
|
-
if (
|
|
146
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
150
147
|
throw new TypeError("meteredUsage: " + label + " contains control / zero-width bytes");
|
|
151
148
|
}
|
|
152
149
|
return s;
|
|
@@ -253,9 +250,7 @@ function _idempotencyKey(s) {
|
|
|
253
250
|
if (s.length > MAX_IDEM_KEY_LEN) {
|
|
254
251
|
throw new TypeError("meteredUsage: idempotency_key must be <= " + MAX_IDEM_KEY_LEN + " characters");
|
|
255
252
|
}
|
|
256
|
-
|
|
257
|
-
throw new TypeError("meteredUsage: idempotency_key contains control bytes");
|
|
258
|
-
}
|
|
253
|
+
textGuard.freeText(s, "meteredUsage: idempotency_key");
|
|
259
254
|
return s;
|
|
260
255
|
}
|
|
261
256
|
|
package/lib/notifications.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
var b = require("./vendor/blamejs");
|
|
42
|
+
var textGuard = require("./text-guard");
|
|
42
43
|
|
|
43
44
|
var RECIPIENT_NAMESPACE = "notification-recipient";
|
|
44
45
|
|
|
@@ -54,7 +55,6 @@ var MAX_PAYLOAD_BYTES = 65536;
|
|
|
54
55
|
var MAX_ERROR_LEN = 1024;
|
|
55
56
|
|
|
56
57
|
var EVENT_TYPE_RE = /^[a-z](?:[a-z0-9._-]*[a-z0-9])?$/;
|
|
57
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
58
58
|
var DEFAULT_PAGE_SIZE = 50;
|
|
59
59
|
var MAX_PAGE_SIZE = 500;
|
|
60
60
|
|
|
@@ -67,9 +67,7 @@ function _recipientId(s) {
|
|
|
67
67
|
if (s.length > MAX_RECIPIENT_LEN) {
|
|
68
68
|
throw new TypeError("notifications: recipient_id must be <= " + MAX_RECIPIENT_LEN + " characters");
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
throw new TypeError("notifications: recipient_id contains control bytes");
|
|
72
|
-
}
|
|
70
|
+
textGuard.freeText(s, "notifications: recipient_id");
|
|
73
71
|
return s;
|
|
74
72
|
}
|
|
75
73
|
|
|
@@ -101,9 +99,7 @@ function _title(s) {
|
|
|
101
99
|
if (s.length > MAX_TITLE_LEN) {
|
|
102
100
|
throw new TypeError("notifications: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
103
101
|
}
|
|
104
|
-
|
|
105
|
-
throw new TypeError("notifications: title contains control bytes");
|
|
106
|
-
}
|
|
102
|
+
textGuard.freeText(s, "notifications: title");
|
|
107
103
|
return s;
|
|
108
104
|
}
|
|
109
105
|
|
package/lib/operator-accounts.js
CHANGED
|
@@ -156,9 +156,9 @@ var MAX_LIST_LIMIT = 200;
|
|
|
156
156
|
var ROLES = Object.freeze(["owner", "manager", "viewer"]);
|
|
157
157
|
var STATUS = Object.freeze(["active", "disabled"]);
|
|
158
158
|
|
|
159
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
160
159
|
|
|
161
160
|
var b = require("./vendor/blamejs");
|
|
161
|
+
var textGuard = require("./text-guard");
|
|
162
162
|
|
|
163
163
|
// ---- validators ---------------------------------------------------------
|
|
164
164
|
|
|
@@ -174,7 +174,7 @@ function _actorId(s, label) {
|
|
|
174
174
|
if (typeof s !== "string" || !s.length || s.length > 128) {
|
|
175
175
|
throw new TypeError("operatorAccounts: " + (label || "actor_id") + " must be a non-empty string (<= 128 chars)");
|
|
176
176
|
}
|
|
177
|
-
if (
|
|
177
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
178
178
|
throw new TypeError("operatorAccounts: " + (label || "actor_id") + " must not contain control bytes");
|
|
179
179
|
}
|
|
180
180
|
return s;
|
|
@@ -212,9 +212,7 @@ function _displayName(s) {
|
|
|
212
212
|
if (typeof s !== "string" || !s.length || s.length > MAX_DISPLAY_NAME_LEN) {
|
|
213
213
|
throw new TypeError("operatorAccounts: display_name must be a non-empty string (<= " + MAX_DISPLAY_NAME_LEN + " chars)");
|
|
214
214
|
}
|
|
215
|
-
|
|
216
|
-
throw new TypeError("operatorAccounts: display_name must not contain control bytes");
|
|
217
|
-
}
|
|
215
|
+
textGuard.freeText(s, "operatorAccounts: display_name", { singleLine: "reject" });
|
|
218
216
|
return s;
|
|
219
217
|
}
|
|
220
218
|
|
|
@@ -164,13 +164,13 @@ var MAX_HOURS = 24 * 365; // one-year cap
|
|
|
164
164
|
|
|
165
165
|
var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
166
166
|
var ACTION_KIND_RE = /^[a-z][a-z0-9_.\-]{0,79}$/;
|
|
167
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
168
167
|
|
|
169
168
|
var DECISIONS = Object.freeze(["approve", "reject", "abstain"]);
|
|
170
169
|
var STATUSES = Object.freeze(["pending", "approved", "rejected",
|
|
171
170
|
"executed", "cancelled", "escalated"]);
|
|
172
171
|
|
|
173
172
|
var b = require("./vendor/blamejs");
|
|
173
|
+
var textGuard = require("./text-guard");
|
|
174
174
|
|
|
175
175
|
// ---- validators ---------------------------------------------------------
|
|
176
176
|
|
|
@@ -195,7 +195,7 @@ function _operatorId(s, label) {
|
|
|
195
195
|
throw new TypeError("operatorApprovals: " + label +
|
|
196
196
|
" must be a non-empty string (<= " + MAX_OPERATOR_ID_LEN + " chars)");
|
|
197
197
|
}
|
|
198
|
-
if (
|
|
198
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
199
199
|
throw new TypeError("operatorApprovals: " + label + " must not contain control bytes");
|
|
200
200
|
}
|
|
201
201
|
return s;
|
|
@@ -206,7 +206,7 @@ function _reason(s) {
|
|
|
206
206
|
throw new TypeError("operatorApprovals: reason must be a non-empty string <= " +
|
|
207
207
|
MAX_REASON_LEN + " chars");
|
|
208
208
|
}
|
|
209
|
-
if (
|
|
209
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
210
210
|
throw new TypeError("operatorApprovals: reason must not contain control bytes (except whitespace)");
|
|
211
211
|
}
|
|
212
212
|
return s;
|
|
@@ -217,7 +217,7 @@ function _justification(s) {
|
|
|
217
217
|
throw new TypeError("operatorApprovals: justification must be a non-empty string <= " +
|
|
218
218
|
MAX_JUSTIFICATION_LEN + " chars");
|
|
219
219
|
}
|
|
220
|
-
if (
|
|
220
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
221
221
|
throw new TypeError("operatorApprovals: justification must not contain control bytes (except whitespace)");
|
|
222
222
|
}
|
|
223
223
|
return s;
|
|
@@ -229,7 +229,7 @@ function _comment(s) {
|
|
|
229
229
|
throw new TypeError("operatorApprovals: comment must be a string <= " +
|
|
230
230
|
MAX_COMMENT_LEN + " chars");
|
|
231
231
|
}
|
|
232
|
-
if (
|
|
232
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
233
233
|
throw new TypeError("operatorApprovals: comment must not contain control bytes (except whitespace)");
|
|
234
234
|
}
|
|
235
235
|
return s;
|
|
@@ -317,9 +317,7 @@ function _capability(s) {
|
|
|
317
317
|
if (typeof s !== "string" || !s.length || s.length > MAX_ACTION_KIND_LEN) {
|
|
318
318
|
throw new TypeError("operatorApprovals: required_capability must be a non-empty string or null");
|
|
319
319
|
}
|
|
320
|
-
|
|
321
|
-
throw new TypeError("operatorApprovals: required_capability must not contain control bytes");
|
|
322
|
-
}
|
|
320
|
+
textGuard.freeText(s, "operatorApprovals: required_capability", { singleLine: "reject" });
|
|
323
321
|
return s;
|
|
324
322
|
}
|
|
325
323
|
|
|
@@ -104,16 +104,13 @@ var ALLOWED_UA_CLASSES = Object.freeze([
|
|
|
104
104
|
// discipline matches every other operator-facing primitive (catalog,
|
|
105
105
|
// experiments, promo-banners) — strings reach the dashboard as inert
|
|
106
106
|
// text, never as live markup.
|
|
107
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
108
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
109
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
110
|
-
);
|
|
111
107
|
|
|
112
108
|
var IDENT_RE = /^[A-Za-z0-9][A-Za-z0-9._:\-]{0,127}$/;
|
|
113
109
|
var ACTION_RE = /^[A-Za-z0-9][A-Za-z0-9._:\-]{0,127}$/;
|
|
114
110
|
var KIND_RE = /^[A-Za-z0-9][A-Za-z0-9._\-]{0,63}$/;
|
|
115
111
|
|
|
116
112
|
var b = require("./vendor/blamejs");
|
|
113
|
+
var textGuard = require("./text-guard");
|
|
117
114
|
|
|
118
115
|
// ---- validators ---------------------------------------------------------
|
|
119
116
|
|
|
@@ -148,9 +145,7 @@ function _ipHash(s) {
|
|
|
148
145
|
throw new TypeError("operatorAuditLog: ip_hash must be a non-empty string ≤ " +
|
|
149
146
|
MAX_IP_HASH_LEN + " chars");
|
|
150
147
|
}
|
|
151
|
-
|
|
152
|
-
throw new TypeError("operatorAuditLog: ip_hash contains control / zero-width characters");
|
|
153
|
-
}
|
|
148
|
+
textGuard.freeText(s, "operatorAuditLog: ip_hash", { singleLine: "reject", zeroWidth: "reject" });
|
|
154
149
|
return s;
|
|
155
150
|
}
|
|
156
151
|
|
|
@@ -109,11 +109,6 @@ var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
|
109
109
|
var SECTION_RE = /^[a-z][a-z0-9_-]*$/;
|
|
110
110
|
var ACTION_RE = /^[a-z0-9](?:[a-z0-9._/:-]*[a-z0-9])?$/;
|
|
111
111
|
|
|
112
|
-
var CONTROL_BYTE_BODY_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
113
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
114
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
115
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
116
|
-
);
|
|
117
112
|
|
|
118
113
|
var SEARCH_WEIGHTS = Object.freeze({
|
|
119
114
|
title: 3,
|
|
@@ -126,6 +121,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
|
|
|
126
121
|
]);
|
|
127
122
|
|
|
128
123
|
var b = require("./vendor/blamejs");
|
|
124
|
+
var textGuard = require("./text-guard");
|
|
129
125
|
|
|
130
126
|
// ---- monotonic clock ---------------------------------------------------
|
|
131
127
|
//
|
|
@@ -168,12 +164,7 @@ function _title(s) {
|
|
|
168
164
|
if (s.length > MAX_TITLE_LEN) {
|
|
169
165
|
throw new TypeError("operatorHelpCenter: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
170
166
|
}
|
|
171
|
-
|
|
172
|
-
throw new TypeError("operatorHelpCenter: title contains control bytes");
|
|
173
|
-
}
|
|
174
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
175
|
-
throw new TypeError("operatorHelpCenter: title contains zero-width / direction-override characters");
|
|
176
|
-
}
|
|
167
|
+
textGuard.freeText(s, "operatorHelpCenter: title", { singleLine: "reject", zeroWidth: "reject" });
|
|
177
168
|
return s;
|
|
178
169
|
}
|
|
179
170
|
|
|
@@ -188,12 +179,7 @@ function _body(s) {
|
|
|
188
179
|
if (s.length > MAX_BODY_LEN) {
|
|
189
180
|
throw new TypeError("operatorHelpCenter: body must be <= " + MAX_BODY_LEN + " characters");
|
|
190
181
|
}
|
|
191
|
-
|
|
192
|
-
throw new TypeError("operatorHelpCenter: body contains control bytes");
|
|
193
|
-
}
|
|
194
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
195
|
-
throw new TypeError("operatorHelpCenter: body contains zero-width / direction-override characters");
|
|
196
|
-
}
|
|
182
|
+
textGuard.freeText(s, "operatorHelpCenter: body", { zeroWidth: "reject" });
|
|
197
183
|
return s;
|
|
198
184
|
}
|
|
199
185
|
|
|
@@ -304,9 +290,7 @@ function _queryStr(s) {
|
|
|
304
290
|
if (s.length > MAX_QUERY_LEN) {
|
|
305
291
|
throw new TypeError("operatorHelpCenter: query must be <= " + MAX_QUERY_LEN + " characters");
|
|
306
292
|
}
|
|
307
|
-
|
|
308
|
-
throw new TypeError("operatorHelpCenter: query contains control / zero-width bytes");
|
|
309
|
-
}
|
|
293
|
+
textGuard.freeText(s, "operatorHelpCenter: query", { zeroWidth: "reject" });
|
|
310
294
|
return s;
|
|
311
295
|
}
|
|
312
296
|
|
|
@@ -346,7 +330,7 @@ function _esc(s) {
|
|
|
346
330
|
|
|
347
331
|
function _safeLinkUrl(url) {
|
|
348
332
|
if (typeof url !== "string" || !url.length || url.length > 2048) return null;
|
|
349
|
-
if (
|
|
333
|
+
if (textGuard.hasCodepointThreat(url, { zeroWidth: "reject" })) return null;
|
|
350
334
|
if (url.charCodeAt(0) === 47 /* "/" */) {
|
|
351
335
|
if (url.length > 1 && url.charCodeAt(1) === 47) return null;
|
|
352
336
|
if (url.indexOf("..") !== -1) return null;
|
package/lib/operator-inbox.js
CHANGED
|
@@ -165,6 +165,7 @@
|
|
|
165
165
|
*/
|
|
166
166
|
|
|
167
167
|
var b = require("./vendor/blamejs");
|
|
168
|
+
var textGuard = require("./text-guard");
|
|
168
169
|
|
|
169
170
|
// ---- constants ----------------------------------------------------------
|
|
170
171
|
|
|
@@ -183,8 +184,6 @@ var MAX_BULK_IDS = 200;
|
|
|
183
184
|
var DEFAULT_LIMIT = 50;
|
|
184
185
|
var MAX_LIMIT = 500;
|
|
185
186
|
|
|
186
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
187
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
188
187
|
|
|
189
188
|
var CURSOR_ORDER_KEY = Object.freeze(["created_at", "id"]);
|
|
190
189
|
|
|
@@ -249,9 +248,7 @@ function _subject(s) {
|
|
|
249
248
|
if (typeof s !== "string" || !s.length || s.length > MAX_SUBJECT_LEN) {
|
|
250
249
|
throw new TypeError("operatorInbox: subject must be a non-empty string <= " + MAX_SUBJECT_LEN + " chars");
|
|
251
250
|
}
|
|
252
|
-
|
|
253
|
-
throw new TypeError("operatorInbox: subject must not contain control bytes");
|
|
254
|
-
}
|
|
251
|
+
textGuard.freeText(s, "operatorInbox: subject");
|
|
255
252
|
return s;
|
|
256
253
|
}
|
|
257
254
|
|
|
@@ -259,9 +256,7 @@ function _body(s) {
|
|
|
259
256
|
if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
|
|
260
257
|
throw new TypeError("operatorInbox: body must be a non-empty string <= " + MAX_BODY_LEN + " chars");
|
|
261
258
|
}
|
|
262
|
-
|
|
263
|
-
throw new TypeError("operatorInbox: body must not contain control bytes");
|
|
264
|
-
}
|
|
259
|
+
textGuard.freeText(s, "operatorInbox: body");
|
|
265
260
|
return s;
|
|
266
261
|
}
|
|
267
262
|
|
|
@@ -284,9 +279,7 @@ function _sourceEventId(s) {
|
|
|
284
279
|
if (typeof s !== "string" || s.length > MAX_SOURCE_LEN) {
|
|
285
280
|
throw new TypeError("operatorInbox: source_event_id must be a string <= " + MAX_SOURCE_LEN + " chars");
|
|
286
281
|
}
|
|
287
|
-
|
|
288
|
-
throw new TypeError("operatorInbox: source_event_id must not contain control bytes");
|
|
289
|
-
}
|
|
282
|
+
textGuard.freeText(s, "operatorInbox: source_event_id", { singleLine: "reject" });
|
|
290
283
|
return s;
|
|
291
284
|
}
|
|
292
285
|
|
package/lib/operator-roles.js
CHANGED
|
@@ -144,7 +144,6 @@ var MAX_LIST_LIMIT = 500;
|
|
|
144
144
|
// Slug shape matches the rest of the codebase — alnum-leading, alnum
|
|
145
145
|
// + hyphen + underscore + dot, capped length.
|
|
146
146
|
var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
147
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
148
147
|
|
|
149
148
|
// Closed permission allow-list. Adding a new permission is a code
|
|
150
149
|
// change here AND the operator re-defining the affected roles.
|
|
@@ -170,6 +169,7 @@ var PERMISSIONS = Object.freeze([
|
|
|
170
169
|
var ALLOWED_PATCH_COLUMNS = Object.freeze(["title", "permissions", "description"]);
|
|
171
170
|
|
|
172
171
|
var b = require("./vendor/blamejs");
|
|
172
|
+
var textGuard = require("./text-guard");
|
|
173
173
|
|
|
174
174
|
// ---- validators ---------------------------------------------------------
|
|
175
175
|
|
|
@@ -185,9 +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
|
-
|
|
189
|
-
throw new TypeError("operatorRoles: title must not contain control bytes");
|
|
190
|
-
}
|
|
188
|
+
textGuard.freeText(s, "operatorRoles: title", { singleLine: "reject" });
|
|
191
189
|
return s;
|
|
192
190
|
}
|
|
193
191
|
|
|
@@ -196,7 +194,7 @@ function _description(s) {
|
|
|
196
194
|
if (typeof s !== "string" || s.length > MAX_DESC_LEN) {
|
|
197
195
|
throw new TypeError("operatorRoles: description must be a string <= " + MAX_DESC_LEN + " chars");
|
|
198
196
|
}
|
|
199
|
-
if (
|
|
197
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
200
198
|
throw new TypeError("operatorRoles: description must not contain control bytes (except whitespace)");
|
|
201
199
|
}
|
|
202
200
|
return s;
|
|
@@ -231,7 +229,7 @@ function _operatorId(s, label) {
|
|
|
231
229
|
if (typeof s !== "string" || !s.length || s.length > MAX_OPERATOR_ID_LEN) {
|
|
232
230
|
throw new TypeError("operatorRoles: " + label + " must be a non-empty string (<= " + MAX_OPERATOR_ID_LEN + " chars)");
|
|
233
231
|
}
|
|
234
|
-
if (
|
|
232
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
235
233
|
throw new TypeError("operatorRoles: " + label + " must not contain control bytes");
|
|
236
234
|
}
|
|
237
235
|
return s;
|
|
@@ -241,7 +239,7 @@ function _reason(s) {
|
|
|
241
239
|
if (typeof s !== "string" || !s.length || s.length > MAX_REASON_LEN) {
|
|
242
240
|
throw new TypeError("operatorRoles: reason must be a non-empty string <= " + MAX_REASON_LEN + " chars");
|
|
243
241
|
}
|
|
244
|
-
if (
|
|
242
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
245
243
|
throw new TypeError("operatorRoles: reason must not contain control bytes (except whitespace)");
|
|
246
244
|
}
|
|
247
245
|
return s;
|
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.
|