@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/carrier-accounts.js
CHANGED
|
@@ -101,6 +101,7 @@ var STATUSES = Object.freeze(["active", "disabled", "rotating"]);
|
|
|
101
101
|
|
|
102
102
|
// Framework handle (the vendored blamejs); index.js re-exports this as .framework.
|
|
103
103
|
var b = require("./vendor/blamejs");
|
|
104
|
+
var textGuard = require("./text-guard");
|
|
104
105
|
|
|
105
106
|
var ROTATION_GRACE_MS = b.constants.TIME.days(1);
|
|
106
107
|
|
|
@@ -124,10 +125,6 @@ var MAX_KEY_LEN = 512;
|
|
|
124
125
|
// reason / operation string. These columns surface on operator
|
|
125
126
|
// dashboards and inline into log lines — embedded control bytes are a
|
|
126
127
|
// slipping-class for header injection + visual spoofing.
|
|
127
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
128
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
129
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
130
|
-
);
|
|
131
128
|
|
|
132
129
|
// account_number alphabet — operators occasionally space-pad or
|
|
133
130
|
// hyphenate the printed form; we accept alnum + dash + space + dot and
|
|
@@ -233,9 +230,7 @@ function _label(s) {
|
|
|
233
230
|
if (s.length === 0 || s.length > MAX_LABEL_LEN) {
|
|
234
231
|
throw new TypeError("carrier-accounts: account_label must be 1.." + MAX_LABEL_LEN + " characters");
|
|
235
232
|
}
|
|
236
|
-
|
|
237
|
-
throw new TypeError("carrier-accounts: account_label contains control / zero-width bytes");
|
|
238
|
-
}
|
|
233
|
+
textGuard.freeText(s, "carrier-accounts: account_label", { singleLine: "reject", zeroWidth: "reject" });
|
|
239
234
|
return s;
|
|
240
235
|
}
|
|
241
236
|
|
|
@@ -250,9 +245,7 @@ function _reason(s) {
|
|
|
250
245
|
if (s.length > MAX_REASON_LEN) {
|
|
251
246
|
throw new TypeError("carrier-accounts: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
252
247
|
}
|
|
253
|
-
|
|
254
|
-
throw new TypeError("carrier-accounts: reason contains control / zero-width bytes");
|
|
255
|
-
}
|
|
248
|
+
textGuard.freeText(s, "carrier-accounts: reason", { singleLine: "reject", zeroWidth: "reject" });
|
|
256
249
|
return s;
|
|
257
250
|
}
|
|
258
251
|
|
package/lib/catalog-drafts.js
CHANGED
|
@@ -106,6 +106,7 @@ var MAX_CHANGES_PER_DRAFT = 5000;
|
|
|
106
106
|
|
|
107
107
|
// Framework handle (the vendored blamejs); index.js re-exports this as .framework.
|
|
108
108
|
var b = require("./vendor/blamejs");
|
|
109
|
+
var textGuard = require("./text-guard");
|
|
109
110
|
|
|
110
111
|
var ROLLBACK_WINDOW_MS = b.constants.TIME.days(7);
|
|
111
112
|
|
|
@@ -125,10 +126,6 @@ var PRODUCT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,198}[a-z0-9])?$/;
|
|
|
125
126
|
// to operator-authored fields — refuse control bytes + zero-width /
|
|
126
127
|
// direction-override codepoints so a rollback_reason / cancel_reason
|
|
127
128
|
// can't smuggle UI-confusing glyphs into the audit dashboard.
|
|
128
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
129
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
130
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
131
|
-
);
|
|
132
129
|
|
|
133
130
|
|
|
134
131
|
// ---- validators ---------------------------------------------------------
|
|
@@ -144,7 +141,7 @@ function _title(s, label) {
|
|
|
144
141
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
145
142
|
throw new TypeError("catalogDrafts: " + label + " must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
|
|
146
143
|
}
|
|
147
|
-
if (
|
|
144
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
148
145
|
throw new TypeError("catalogDrafts: " + label + " must not contain control bytes");
|
|
149
146
|
}
|
|
150
147
|
return s;
|
|
@@ -155,9 +152,7 @@ function _description(s) {
|
|
|
155
152
|
if (typeof s !== "string" || s.length > MAX_DESC_LEN) {
|
|
156
153
|
throw new TypeError("catalogDrafts: description must be a string <= " + MAX_DESC_LEN + " chars when provided");
|
|
157
154
|
}
|
|
158
|
-
|
|
159
|
-
throw new TypeError("catalogDrafts: description must not contain control bytes");
|
|
160
|
-
}
|
|
155
|
+
textGuard.freeText(s, "catalogDrafts: description");
|
|
161
156
|
return s;
|
|
162
157
|
}
|
|
163
158
|
|
|
@@ -169,7 +164,7 @@ function _optReason(s, label) {
|
|
|
169
164
|
if (s.length > MAX_REASON_LEN) {
|
|
170
165
|
throw new TypeError("catalogDrafts: " + label + " must be <= " + MAX_REASON_LEN + " chars");
|
|
171
166
|
}
|
|
172
|
-
if (
|
|
167
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
173
168
|
throw new TypeError("catalogDrafts: " + label + " contains control / zero-width bytes");
|
|
174
169
|
}
|
|
175
170
|
return s;
|
|
@@ -187,9 +182,7 @@ function _optOwner(s) {
|
|
|
187
182
|
if (typeof s !== "string" || !s.length || s.length > MAX_OWNER_LEN) {
|
|
188
183
|
throw new TypeError("catalogDrafts: owner_id must be a non-empty string <= " + MAX_OWNER_LEN + " chars when provided");
|
|
189
184
|
}
|
|
190
|
-
|
|
191
|
-
throw new TypeError("catalogDrafts: owner_id must not contain control bytes");
|
|
192
|
-
}
|
|
185
|
+
textGuard.freeText(s, "catalogDrafts: owner_id");
|
|
193
186
|
return s;
|
|
194
187
|
}
|
|
195
188
|
|
|
@@ -111,13 +111,9 @@ var MAX_POPULAR_LIMIT = 100;
|
|
|
111
111
|
var MAX_REORDER_BATCH = 500;
|
|
112
112
|
|
|
113
113
|
var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
114
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
115
|
-
var CONTROL_BYTE_BODY_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
116
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
117
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
118
|
-
);
|
|
119
114
|
|
|
120
115
|
var b = require("./vendor/blamejs");
|
|
116
|
+
var textGuard = require("./text-guard");
|
|
121
117
|
|
|
122
118
|
// ---- monotonic clock ---------------------------------------------------
|
|
123
119
|
//
|
|
@@ -161,12 +157,7 @@ function _title(s) {
|
|
|
161
157
|
if (s.length > MAX_TITLE_LEN) {
|
|
162
158
|
throw new TypeError("categoryNavigation: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
163
159
|
}
|
|
164
|
-
|
|
165
|
-
throw new TypeError("categoryNavigation: title contains control bytes");
|
|
166
|
-
}
|
|
167
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
168
|
-
throw new TypeError("categoryNavigation: title contains zero-width / direction-override characters");
|
|
169
|
-
}
|
|
160
|
+
textGuard.freeText(s, "categoryNavigation: title", { singleLine: "reject", zeroWidth: "reject" });
|
|
170
161
|
return s;
|
|
171
162
|
}
|
|
172
163
|
|
|
@@ -178,12 +169,7 @@ function _description(s) {
|
|
|
178
169
|
if (s.length > MAX_DESCRIPTION_LEN) {
|
|
179
170
|
throw new TypeError("categoryNavigation: description must be <= " + MAX_DESCRIPTION_LEN + " characters");
|
|
180
171
|
}
|
|
181
|
-
|
|
182
|
-
throw new TypeError("categoryNavigation: description contains control bytes");
|
|
183
|
-
}
|
|
184
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
185
|
-
throw new TypeError("categoryNavigation: description contains zero-width / direction-override characters");
|
|
186
|
-
}
|
|
172
|
+
textGuard.freeText(s, "categoryNavigation: description", { zeroWidth: "reject" });
|
|
187
173
|
return s;
|
|
188
174
|
}
|
|
189
175
|
|
|
@@ -200,9 +186,7 @@ function _heroImageUrl(s) {
|
|
|
200
186
|
if (s.length > MAX_HERO_URL_LEN) {
|
|
201
187
|
throw new TypeError("categoryNavigation: hero_image_url must be <= " + MAX_HERO_URL_LEN + " characters");
|
|
202
188
|
}
|
|
203
|
-
|
|
204
|
-
throw new TypeError("categoryNavigation: hero_image_url contains control / zero-width bytes");
|
|
205
|
-
}
|
|
189
|
+
textGuard.freeText(s, "categoryNavigation: hero_image_url", { zeroWidth: "reject" });
|
|
206
190
|
if (s.charCodeAt(0) === 47 /* "/" */) {
|
|
207
191
|
if (s.length > 1 && s.charCodeAt(1) === 47) {
|
|
208
192
|
throw new TypeError("categoryNavigation: hero_image_url protocol-relative `//host/...` refused — use absolute https://");
|
package/lib/cms-blocks.js
CHANGED
|
@@ -116,16 +116,13 @@ var LOCALE_RE = /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/;
|
|
|
116
116
|
|
|
117
117
|
// Refuse C0 control bytes + DEL in operator-authored bodies. LF / CR
|
|
118
118
|
// / tab are allowed — operators author multi-line Markdown.
|
|
119
|
-
var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
120
119
|
|
|
121
120
|
// Zero-width / direction-override family. Spelled with \u-escapes
|
|
122
121
|
// so ESLint's no-irregular-whitespace stays happy. Same defence as
|
|
123
122
|
// the rest of the shop primitives.
|
|
124
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
125
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
126
|
-
);
|
|
127
123
|
|
|
128
124
|
var b = require("./vendor/blamejs");
|
|
125
|
+
var textGuard = require("./text-guard");
|
|
129
126
|
|
|
130
127
|
// ---- validators ---------------------------------------------------------
|
|
131
128
|
|
|
@@ -141,10 +138,10 @@ function _body(s, label) {
|
|
|
141
138
|
if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
|
|
142
139
|
throw new TypeError("cmsBlocks: " + label + " must be a non-empty string <= " + MAX_BODY_LEN + " chars");
|
|
143
140
|
}
|
|
144
|
-
if (
|
|
141
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
145
142
|
throw new TypeError("cmsBlocks: " + label + " contains control bytes");
|
|
146
143
|
}
|
|
147
|
-
if (
|
|
144
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
148
145
|
throw new TypeError("cmsBlocks: " + label + " contains zero-width / direction-override characters");
|
|
149
146
|
}
|
|
150
147
|
return s;
|
|
@@ -241,7 +238,7 @@ function _esc(s) {
|
|
|
241
238
|
|
|
242
239
|
function _safeLinkUrl(url) {
|
|
243
240
|
if (typeof url !== "string" || !url.length || url.length > 2048) return null;
|
|
244
|
-
if (
|
|
241
|
+
if (textGuard.hasCodepointThreat(url, { zeroWidth: "reject" })) return null;
|
|
245
242
|
if (url.charCodeAt(0) === 47 /* "/" */) {
|
|
246
243
|
if (url.length > 1 && url.charCodeAt(1) === 47) return null;
|
|
247
244
|
if (url.indexOf("..") !== -1) return null;
|
package/lib/compliance-export.js
CHANGED
|
@@ -114,6 +114,7 @@
|
|
|
114
114
|
*/
|
|
115
115
|
|
|
116
116
|
var b = require("./vendor/blamejs");
|
|
117
|
+
var textGuard = require("./text-guard");
|
|
117
118
|
|
|
118
119
|
// ---- constants ----------------------------------------------------------
|
|
119
120
|
|
|
@@ -177,10 +178,6 @@ var DEFAULT_LIST_LIMIT = 50;
|
|
|
177
178
|
// Operator-authored prose lands in reason / dismiss_reason and
|
|
178
179
|
// replays into compliance review screens. Same control-byte +
|
|
179
180
|
// zero-width refusal posture the sibling primitives carry.
|
|
180
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
181
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
182
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
183
|
-
);
|
|
184
181
|
|
|
185
182
|
// Scope -> which injected readers contribute on export. Keeps the
|
|
186
183
|
// fulfillRequest logic a single lookup instead of nested if-else.
|
|
@@ -256,9 +253,7 @@ function _requestedBy(s) {
|
|
|
256
253
|
if (typeof s !== "string" || !s.length || s.length > MAX_REQUESTED_BY_LEN) {
|
|
257
254
|
throw new TypeError("complianceExport: requested_by must be a non-empty string <= " + MAX_REQUESTED_BY_LEN + " chars");
|
|
258
255
|
}
|
|
259
|
-
|
|
260
|
-
throw new TypeError("complianceExport: requested_by must not contain control bytes or zero-width characters");
|
|
261
|
-
}
|
|
256
|
+
textGuard.freeText(s, "complianceExport: requested_by", { zeroWidth: "reject" });
|
|
262
257
|
return s;
|
|
263
258
|
}
|
|
264
259
|
|
|
@@ -266,7 +261,7 @@ function _prose(s, label, maxLen) {
|
|
|
266
261
|
if (typeof s !== "string" || !s.length || s.length > maxLen) {
|
|
267
262
|
throw new TypeError("complianceExport: " + label + " must be a non-empty string <= " + maxLen + " chars");
|
|
268
263
|
}
|
|
269
|
-
if (
|
|
264
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
270
265
|
throw new TypeError("complianceExport: " + label + " must not contain control bytes or zero-width characters");
|
|
271
266
|
}
|
|
272
267
|
return s;
|
|
@@ -286,9 +281,7 @@ function _deliveryAddress(s) {
|
|
|
286
281
|
if (typeof s !== "string" || !s.length || s.length > MAX_DELIVERY_ADDR_LEN) {
|
|
287
282
|
throw new TypeError("complianceExport: delivery_address must be a non-empty string <= " + MAX_DELIVERY_ADDR_LEN + " chars");
|
|
288
283
|
}
|
|
289
|
-
|
|
290
|
-
throw new TypeError("complianceExport: delivery_address must not contain control bytes or zero-width characters");
|
|
291
|
-
}
|
|
284
|
+
textGuard.freeText(s, "complianceExport: delivery_address", { zeroWidth: "reject" });
|
|
292
285
|
return s;
|
|
293
286
|
}
|
|
294
287
|
|
package/lib/consent-ledger.js
CHANGED
|
@@ -123,7 +123,6 @@ var JURISDICTION_RE = /^[A-Z]{2}$/;
|
|
|
123
123
|
var EVIDENCE_REF_MAX_LEN = 256;
|
|
124
124
|
var EVIDENCE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
|
|
125
125
|
|
|
126
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
127
126
|
|
|
128
127
|
var CSV_COLUMNS = Object.freeze([
|
|
129
128
|
"id",
|
|
@@ -138,6 +137,7 @@ var CSV_COLUMNS = Object.freeze([
|
|
|
138
137
|
]);
|
|
139
138
|
|
|
140
139
|
var b = require("./vendor/blamejs");
|
|
140
|
+
var textGuard = require("./text-guard");
|
|
141
141
|
|
|
142
142
|
// The GDPR Art. 6(1) lawful bases, sourced from the framework's
|
|
143
143
|
// consent primitive so this ledger's vocabulary tracks the
|
|
@@ -269,9 +269,7 @@ function _optEvidenceRef(s) {
|
|
|
269
269
|
"consentLedger: evidence_ref must be <= " + EVIDENCE_REF_MAX_LEN + " characters"
|
|
270
270
|
);
|
|
271
271
|
}
|
|
272
|
-
|
|
273
|
-
throw new TypeError("consentLedger: evidence_ref must not contain control bytes");
|
|
274
|
-
}
|
|
272
|
+
textGuard.freeText(s, "consentLedger: evidence_ref", { singleLine: "reject" });
|
|
275
273
|
if (!EVIDENCE_REF_RE.test(s)) {
|
|
276
274
|
throw new TypeError(
|
|
277
275
|
"consentLedger: evidence_ref must match /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/"
|
package/lib/cost-layers.js
CHANGED
|
@@ -108,6 +108,16 @@ var b = require("./vendor/blamejs");
|
|
|
108
108
|
|
|
109
109
|
var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
110
110
|
var CURRENCY_RE = /^[A-Z]{3}$/;
|
|
111
|
+
|
|
112
|
+
// Carrier for the largest-remainder split of a line's COGS across the layers
|
|
113
|
+
// it drew from (see consumeForSale). b.money.allocate distributes a count of
|
|
114
|
+
// MINOR UNITS, so the currency is a type tag and not an input to the result —
|
|
115
|
+
// the split of 10040 across 40/40/20 is 4016/4016/2008 whether the money is
|
|
116
|
+
// 2-decimal, 0-decimal or this. It is deliberately NOT the layers' currency:
|
|
117
|
+
// receipts accept any well-formed ISO 4217 code while b.money accepts a
|
|
118
|
+
// narrower catalog, and pricing layers in one outside it would otherwise make
|
|
119
|
+
// them receivable but never sellable.
|
|
120
|
+
var ALLOCATION_CARRIER = "USD";
|
|
111
121
|
var METHODS = Object.freeze(["fifo", "lifo", "weighted_average"]);
|
|
112
122
|
var RECEIPT_SOURCES = Object.freeze(["receipt", "transfer", "adjustment"]);
|
|
113
123
|
// Internal source value used by `recordReversal` only; operators
|
|
@@ -453,11 +463,6 @@ function create(opts) {
|
|
|
453
463
|
// don't accumulate a fractional debt.
|
|
454
464
|
var lineNumerator = sumValue * quantity;
|
|
455
465
|
var lineCogs = Math.floor(lineNumerator / sumQty);
|
|
456
|
-
// Residual carried forward to per-layer attribution rows so
|
|
457
|
-
// the sum of attributions matches lineCogs exactly. The first
|
|
458
|
-
// layer in the iteration absorbs the residual (smallest
|
|
459
|
-
// possible deviation, deterministic placement).
|
|
460
|
-
var residual = lineCogs;
|
|
461
466
|
// Debit every layer proportionally. Use integer division so
|
|
462
467
|
// each layer's debit is an integer; spread any leftover
|
|
463
468
|
// single units across the earliest layers (deterministic).
|
|
@@ -491,20 +496,45 @@ function create(opts) {
|
|
|
491
496
|
remainingToConsume -= 1;
|
|
492
497
|
}
|
|
493
498
|
}
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
499
|
+
// Split the line's COGS across the debits in proportion to the units
|
|
500
|
+
// each one contributes. b.money.allocate distributes by largest
|
|
501
|
+
// remainder: every row lands within a single minor unit of its exact
|
|
502
|
+
// share, and the parts sum to the total with nothing left over.
|
|
503
|
+
//
|
|
504
|
+
// What it replaces priced each debit at the weighted-average unit cost
|
|
505
|
+
// ROUNDED to an integer, then handed the last debit whatever remained.
|
|
506
|
+
// Rounding the average absorbed a fraction of a minor unit per unit
|
|
507
|
+
// debited, and every one of those fractions landed on the final row.
|
|
508
|
+
// Consuming 40 + 40 + 20 units at an average of 100.4 attributed
|
|
509
|
+
// 4000 / 4000 / 2040 where the true shares are 4016 / 4016 / 2008 — a
|
|
510
|
+
// 32-minor-unit misattribution from one sale, growing with the
|
|
511
|
+
// quantities ahead of the last debit. The line total was always right,
|
|
512
|
+
// so nothing was over- or under-charged; it was the per-layer COGS the
|
|
513
|
+
// margin and valuation reports read that was wrong.
|
|
514
|
+
//
|
|
515
|
+
// The split is carried out on a Money in ALLOCATION_CARRIER rather than
|
|
516
|
+
// in the layers' own currency, and the currency genuinely does not
|
|
517
|
+
// enter the arithmetic: allocate distributes a count of minor units by
|
|
518
|
+
// largest remainder, so the exponent never participates. The identical
|
|
519
|
+
// split for a 2-decimal, a 0-decimal and this carrier is asserted in
|
|
520
|
+
// cost-layers.test.js so the assumption cannot rot silently.
|
|
521
|
+
//
|
|
522
|
+
// Using the layers' own currency would read better and is what this
|
|
523
|
+
// wanted to do. It cannot: b.money accepts a deliberately narrow
|
|
524
|
+
// catalog of ISO 4217 codes, while recordReceipt accepts any
|
|
525
|
+
// well-formed one. Pricing layers in a code outside that catalog is
|
|
526
|
+
// accepted today, and passing it here would throw on every
|
|
527
|
+
// weighted-average sale — turning a rounding inaccuracy into inventory
|
|
528
|
+
// that can be received but never sold. Asked upstream for an
|
|
529
|
+
// integer-level allocation in blamejs/blamejs#578.
|
|
530
|
+
var attrShares = debits.length
|
|
531
|
+
? b.money.fromMinorUnits(BigInt(lineCogs), ALLOCATION_CARRIER)
|
|
532
|
+
.allocate(debits.map(function (dd) { return dd.qty; }))
|
|
533
|
+
.map(function (m) { return Number(m.toMinorUnits()); })
|
|
534
|
+
: [];
|
|
498
535
|
for (var e = 0; e < debits.length; e += 1) {
|
|
499
536
|
var dRow = debits[e];
|
|
500
|
-
var attrCogs;
|
|
501
|
-
if (e === debits.length - 1) {
|
|
502
|
-
attrCogs = residual;
|
|
503
|
-
} else {
|
|
504
|
-
attrCogs = dRow.qty * avgUnitCost;
|
|
505
|
-
if (attrCogs > residual) attrCogs = residual;
|
|
506
|
-
residual -= attrCogs;
|
|
507
|
-
}
|
|
537
|
+
var attrCogs = attrShares[e];
|
|
508
538
|
// Attribute the debit's cost so `qty * unit_cost_minor` summed over
|
|
509
539
|
// its attribution rows reconstructs attrCogs EXACTLY — the COGS
|
|
510
540
|
// reports (cogsForOrder / cogsForPeriod) recover each line's cost
|
|
@@ -515,10 +545,15 @@ function create(opts) {
|
|
|
515
545
|
// (qty - r) units at floor, r units at floor+1, where
|
|
516
546
|
// r = attrCogs - floor*qty. (qty-r)*floor + r*(floor+1) =
|
|
517
547
|
// qty*floor + r = attrCogs — no penny drift between the sale's COGS
|
|
518
|
-
// and the persisted attributions (
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
548
|
+
// and the persisted attributions (a single rounded per-unit price
|
|
549
|
+
// drifts by up to qty-1 minor units on the row that carries the
|
|
550
|
+
// remainder).
|
|
551
|
+
//
|
|
552
|
+
// A debit is only recorded when its share is at least one unit (see
|
|
553
|
+
// the push above), so the division below always has a positive
|
|
554
|
+
// divisor.
|
|
555
|
+
var floorUnit = Math.floor(attrCogs / dRow.qty);
|
|
556
|
+
var rem = attrCogs - (floorUnit * dRow.qty);
|
|
522
557
|
var parts = [];
|
|
523
558
|
if (dRow.qty - rem > 0) parts.push({ qty: dRow.qty - rem, unit_cost_minor: floorUnit });
|
|
524
559
|
if (rem > 0) parts.push({ qty: rem, unit_cost_minor: floorUnit + 1 });
|
package/lib/customer-import.js
CHANGED
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
var b = require("./vendor/blamejs");
|
|
53
|
+
var textGuard = require("./text-guard");
|
|
53
54
|
|
|
54
55
|
var DEFAULT_MAX_ROWS = 200000;
|
|
55
56
|
var DEFAULT_MAX_ERRORS = 1000;
|
|
56
57
|
var SOURCES = Object.freeze(["csv", "ndjson", "api"]);
|
|
57
58
|
var ON_CONFLICT_MODES = Object.freeze(["update", "skip", "error"]);
|
|
58
59
|
var CSV_EXPECTED_HEADERS = Object.freeze(["email", "display_name"]);
|
|
59
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
60
60
|
var MAX_DISPLAY_NAME_LEN = 128;
|
|
61
61
|
|
|
62
62
|
// ---- factory ------------------------------------------------------------
|
|
@@ -143,7 +143,7 @@ function _normalizeRow(raw, rowIndex) {
|
|
|
143
143
|
if (displayName.length > MAX_DISPLAY_NAME_LEN) {
|
|
144
144
|
throw new TypeError("row " + rowIndex + ": display_name must be <= " + MAX_DISPLAY_NAME_LEN + " characters");
|
|
145
145
|
}
|
|
146
|
-
if (
|
|
146
|
+
if (textGuard.hasCodepointThreat(displayName)) {
|
|
147
147
|
throw new TypeError("row " + rowIndex + ": display_name contains control bytes");
|
|
148
148
|
}
|
|
149
149
|
// guardEmail strict profile — refuses smuggling / header-injection /
|
package/lib/customer-notes.js
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
48
|
var b = require("./vendor/blamejs");
|
|
49
|
+
var textGuard = require("./text-guard");
|
|
49
50
|
|
|
50
51
|
// ---- constants ----------------------------------------------------------
|
|
51
52
|
|
|
@@ -63,17 +64,9 @@ var MAX_LIMIT = 200;
|
|
|
63
64
|
var DEFAULT_TAG_LIMIT = 20;
|
|
64
65
|
var MAX_TAG_FETCH = 100;
|
|
65
66
|
|
|
66
|
-
//
|
|
67
|
-
// + tab survive in `body` so
|
|
68
|
-
// tags + ids are single-line and refuse
|
|
69
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
70
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
71
|
-
// Zero-width / direction-override / BOM family. Spelled with
|
|
72
|
-
// \u-escapes so the source file stays free of irregular-whitespace
|
|
73
|
-
// ESLint hits.
|
|
74
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
75
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
76
|
-
);
|
|
67
|
+
// Control bytes, bidi overrides and the zero-width family are screened by
|
|
68
|
+
// textGuard.freeText. CR/LF + tab survive in `body` so an operator can wrap a
|
|
69
|
+
// multi-line note; tags + ids are single-line and refuse them.
|
|
77
70
|
|
|
78
71
|
// Tag shape — lowercase alnum + dash + underscore. The operator UI
|
|
79
72
|
// canonicalises on input (lower-case) so the popularTags aggregate
|
|
@@ -110,9 +103,7 @@ function _customerId(s) {
|
|
|
110
103
|
if (typeof s !== "string" || !s.length || s.length > MAX_CUSTOMER_ID) {
|
|
111
104
|
throw new TypeError("customerNotes: customer_id must be a non-empty string (<= " + MAX_CUSTOMER_ID + " chars)");
|
|
112
105
|
}
|
|
113
|
-
|
|
114
|
-
throw new TypeError("customerNotes: customer_id must not contain control bytes");
|
|
115
|
-
}
|
|
106
|
+
textGuard.freeText(s, "customerNotes: customer_id", { singleLine: "reject" });
|
|
116
107
|
return s;
|
|
117
108
|
}
|
|
118
109
|
|
|
@@ -128,9 +119,7 @@ function _authorId(s) {
|
|
|
128
119
|
if (typeof s !== "string" || !s.length || s.length > MAX_AUTHOR_ID) {
|
|
129
120
|
throw new TypeError("customerNotes: author_id must be a non-empty string (<= " + MAX_AUTHOR_ID + " chars) when provided");
|
|
130
121
|
}
|
|
131
|
-
|
|
132
|
-
throw new TypeError("customerNotes: author_id must not contain control bytes");
|
|
133
|
-
}
|
|
122
|
+
textGuard.freeText(s, "customerNotes: author_id", { singleLine: "reject" });
|
|
134
123
|
return s;
|
|
135
124
|
}
|
|
136
125
|
|
|
@@ -153,12 +142,7 @@ function _body(s) {
|
|
|
153
142
|
if (s.length > MAX_BODY_LEN) {
|
|
154
143
|
throw new TypeError("customerNotes: body must be <= " + MAX_BODY_LEN + " characters");
|
|
155
144
|
}
|
|
156
|
-
|
|
157
|
-
throw new TypeError("customerNotes: body must not contain control bytes");
|
|
158
|
-
}
|
|
159
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
160
|
-
throw new TypeError("customerNotes: body must not contain zero-width / direction-override characters");
|
|
161
|
-
}
|
|
145
|
+
textGuard.freeText(s, "customerNotes: body", { zeroWidth: "reject" });
|
|
162
146
|
return s;
|
|
163
147
|
}
|
|
164
148
|
|
package/lib/customer-roles.js
CHANGED
|
@@ -119,7 +119,6 @@ var MAX_LIST_LIMIT = 200;
|
|
|
119
119
|
// hyphen + underscore + dot, capped length.
|
|
120
120
|
var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
121
121
|
|
|
122
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
123
122
|
|
|
124
123
|
// Closed capability enum. Adding a new capability is a code change
|
|
125
124
|
// here + the operator re-defines the affected roles.
|
|
@@ -137,6 +136,7 @@ var CAPABILITIES = Object.freeze([
|
|
|
137
136
|
var ALLOWED_PATCH_COLUMNS = Object.freeze(["title", "capabilities"]);
|
|
138
137
|
|
|
139
138
|
var b = require("./vendor/blamejs");
|
|
139
|
+
var textGuard = require("./text-guard");
|
|
140
140
|
|
|
141
141
|
// ---- validators ---------------------------------------------------------
|
|
142
142
|
|
|
@@ -152,9 +152,7 @@ function _title(s) {
|
|
|
152
152
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
153
153
|
throw new TypeError("customerRoles: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
|
|
154
154
|
}
|
|
155
|
-
|
|
156
|
-
throw new TypeError("customerRoles: title must not contain control bytes");
|
|
157
|
-
}
|
|
155
|
+
textGuard.freeText(s, "customerRoles: title", { singleLine: "reject" });
|
|
158
156
|
return s;
|
|
159
157
|
}
|
|
160
158
|
|
|
@@ -192,7 +190,7 @@ function _opaqueId(s, label) {
|
|
|
192
190
|
if (typeof s !== "string" || !s.length || s.length > 200) {
|
|
193
191
|
throw new TypeError("customerRoles: " + label + " must be a non-empty string (<= 200 chars)");
|
|
194
192
|
}
|
|
195
|
-
if (
|
|
193
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
196
194
|
throw new TypeError("customerRoles: " + label + " must not contain control bytes");
|
|
197
195
|
}
|
|
198
196
|
return s;
|
package/lib/customer-segments.js
CHANGED
|
@@ -113,6 +113,7 @@ var C = b.constants;
|
|
|
113
113
|
|
|
114
114
|
var DEFAULT_LIMIT = 100;
|
|
115
115
|
var MAX_LIMIT = 1000;
|
|
116
|
+
var CSV_LINE_ENDING = "\r\n"; // RFC 4180
|
|
116
117
|
var EXPORT_BATCH_SIZE = 500;
|
|
117
118
|
|
|
118
119
|
// CSV column shape for a segment-members export. NO email column —
|
|
@@ -347,19 +348,22 @@ function _coerceCell(value) {
|
|
|
347
348
|
return JSON.stringify(value);
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
351
|
+
// One member-export row, RFC 4180 — b.guardCsv.serialize writes the whole
|
|
352
|
+
// line: the formula-injection prefix per cell, the unconditional quoting,
|
|
353
|
+
// doubling an embedded quote, the delimiter and the CRLF. Only the injection
|
|
354
|
+
// step was composed before; the quoting rules were written here and again in
|
|
355
|
+
// order-export.js.
|
|
356
|
+
//
|
|
357
|
+
// serialize omits the trailing terminator, which suits a whole document and
|
|
358
|
+
// not a generator yielding a row at a time, so the line ending is appended
|
|
359
|
+
// here.
|
|
359
360
|
function _csvRow(cells) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
return b.guardCsv.serialize([cells.map(_coerceCell)], {
|
|
362
|
+
profile: "strict",
|
|
363
|
+
alwaysQuote: true,
|
|
364
|
+
headers: false,
|
|
365
|
+
lineEnding: CSV_LINE_ENDING,
|
|
366
|
+
}) + CSV_LINE_ENDING;
|
|
363
367
|
}
|
|
364
368
|
|
|
365
369
|
// ---- factory ------------------------------------------------------------
|
|
@@ -975,6 +979,5 @@ module.exports = {
|
|
|
975
979
|
MEMBER_EXPORT_COLUMNS: MEMBER_EXPORT_COLUMNS,
|
|
976
980
|
// Exposed for the test suite to assert CSV cell semantics without a
|
|
977
981
|
// full DB round-trip.
|
|
978
|
-
|
|
979
|
-
_neutralizeInjection: _neutralizeInjection,
|
|
982
|
+
_csvRow: _csvRow,
|
|
980
983
|
};
|
package/lib/customer-surveys.js
CHANGED
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
*/
|
|
70
70
|
|
|
71
71
|
var b = require("./vendor/blamejs");
|
|
72
|
+
var textGuard = require("./text-guard");
|
|
72
73
|
var C = b.constants;
|
|
73
74
|
|
|
74
75
|
// ---- constants ----------------------------------------------------------
|
|
@@ -104,8 +105,6 @@ var MAX_EXPIRES_HOURS = 24 * 365; // 1 year
|
|
|
104
105
|
var DEFAULT_LIMIT = 50;
|
|
105
106
|
var MAX_LIMIT = 500;
|
|
106
107
|
|
|
107
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
108
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
109
108
|
|
|
110
109
|
// NPS / CSAT / CES enforce a primary-question shape so the rollup
|
|
111
110
|
// math has something well-defined to bucket against. Custom surveys
|
|
@@ -143,9 +142,7 @@ function _title(s) {
|
|
|
143
142
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
144
143
|
throw new TypeError("customerSurveys: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
|
|
145
144
|
}
|
|
146
|
-
|
|
147
|
-
throw new TypeError("customerSurveys: title must not contain control bytes");
|
|
148
|
-
}
|
|
145
|
+
textGuard.freeText(s, "customerSurveys: title");
|
|
149
146
|
return s;
|
|
150
147
|
}
|
|
151
148
|
|
|
@@ -173,9 +170,7 @@ function _sourceEventId(s) {
|
|
|
173
170
|
if (typeof s !== "string" || s.length > MAX_SOURCE_EVENT_LEN) {
|
|
174
171
|
throw new TypeError("customerSurveys: source_event_id must be a string <= " + MAX_SOURCE_EVENT_LEN + " chars");
|
|
175
172
|
}
|
|
176
|
-
|
|
177
|
-
throw new TypeError("customerSurveys: source_event_id must not contain control bytes");
|
|
178
|
-
}
|
|
173
|
+
textGuard.freeText(s, "customerSurveys: source_event_id", { singleLine: "reject" });
|
|
179
174
|
return s;
|
|
180
175
|
}
|
|
181
176
|
|
|
@@ -192,9 +187,7 @@ function _closeReason(s) {
|
|
|
192
187
|
if (typeof s !== "string" || !s.length || s.length > MAX_CLOSE_REASON_LEN) {
|
|
193
188
|
throw new TypeError("customerSurveys: reason must be a non-empty string <= " + MAX_CLOSE_REASON_LEN + " chars");
|
|
194
189
|
}
|
|
195
|
-
|
|
196
|
-
throw new TypeError("customerSurveys: reason must not contain control bytes");
|
|
197
|
-
}
|
|
190
|
+
textGuard.freeText(s, "customerSurveys: reason");
|
|
198
191
|
return s;
|
|
199
192
|
}
|
|
200
193
|
|
|
@@ -245,7 +238,7 @@ function _questions(arr, kind) {
|
|
|
245
238
|
if (typeof q.label !== "string" || !q.label.length || q.label.length > MAX_LABEL_LEN) {
|
|
246
239
|
throw new TypeError("customerSurveys: questions[" + i + "].label must be a non-empty string <= " + MAX_LABEL_LEN + " chars");
|
|
247
240
|
}
|
|
248
|
-
if (
|
|
241
|
+
if (textGuard.hasCodepointThreat(q.label)) {
|
|
249
242
|
throw new TypeError("customerSurveys: questions[" + i + "].label must not contain control bytes");
|
|
250
243
|
}
|
|
251
244
|
var required = q.required == null ? true : q.required;
|
|
@@ -284,7 +277,7 @@ function _questions(arr, kind) {
|
|
|
284
277
|
throw new TypeError("customerSurveys: questions[" + i + "].options[" + k +
|
|
285
278
|
"] must be a non-empty string <= " + MAX_OPTION_LEN + " chars");
|
|
286
279
|
}
|
|
287
|
-
if (
|
|
280
|
+
if (textGuard.hasCodepointThreat(opt)) {
|
|
288
281
|
throw new TypeError("customerSurveys: questions[" + i + "].options[" + k +
|
|
289
282
|
"] must not contain control bytes");
|
|
290
283
|
}
|
|
@@ -392,7 +385,7 @@ function _validateAnswers(answers, questions) {
|
|
|
392
385
|
throw new TypeError("customerSurveys: answer for " + JSON.stringify(q.id) +
|
|
393
386
|
" must be <= " + q.max + " chars");
|
|
394
387
|
}
|
|
395
|
-
if (
|
|
388
|
+
if (textGuard.hasCodepointThreat(a)) {
|
|
396
389
|
throw new TypeError("customerSurveys: answer for " + JSON.stringify(q.id) +
|
|
397
390
|
" must not contain control bytes");
|
|
398
391
|
}
|