@blamejs/blamejs-shop 0.5.19 → 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 +4 -0
- package/README.md +1 -0
- package/SECURITY.md +17 -0
- package/lib/addresses.js +2 -2
- package/lib/admin.js +49 -45
- 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 +78 -44
- 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/payment.js +103 -58
- 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/referrals.js +10 -6
- 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 +125 -70
- package/lib/seller-signup.js +10 -26
- package/lib/sidebar-widgets.js +6 -15
- package/lib/site-redirects.js +49 -137
- 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 +21 -23
- 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/MANIFEST.json +107 -97
- package/lib/vendor/blamejs/CHANGELOG.md +180 -0
- package/lib/vendor/blamejs/NOTICE +14 -10
- package/lib/vendor/blamejs/README.md +17 -5
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/acme.js +9 -9
- package/lib/vendor/blamejs/lib/api-snapshot.js +35 -16
- package/lib/vendor/blamejs/lib/app-shutdown.js +1 -1
- package/lib/vendor/blamejs/lib/archive-adapters.js +1 -1
- package/lib/vendor/blamejs/lib/archive.js +13 -24
- package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -1
- package/lib/vendor/blamejs/lib/auth/bot-challenge.js +3 -6
- package/lib/vendor/blamejs/lib/auth/ciba.js +32 -3
- package/lib/vendor/blamejs/lib/auth/oauth.js +65 -8
- package/lib/vendor/blamejs/lib/auth/password.js +1 -1
- package/lib/vendor/blamejs/lib/auth/saml.js +21 -3
- package/lib/vendor/blamejs/lib/auth-bot-challenge.js +3 -3
- package/lib/vendor/blamejs/lib/backup/index.js +5 -5
- package/lib/vendor/blamejs/lib/case-fold-classes.js +491 -0
- package/lib/vendor/blamejs/lib/codepoint-class.js +131 -0
- package/lib/vendor/blamejs/lib/constants.js +79 -0
- package/lib/vendor/blamejs/lib/db-declare-view.js +8 -2
- package/lib/vendor/blamejs/lib/error-page.js +20 -1
- package/lib/vendor/blamejs/lib/external-db.js +5 -1
- package/lib/vendor/blamejs/lib/flag-evaluation-context.js +24 -6
- package/lib/vendor/blamejs/lib/flag.js +28 -2
- package/lib/vendor/blamejs/lib/framework-schema.js +14 -3
- package/lib/vendor/blamejs/lib/guard-regex.js +2345 -89
- package/lib/vendor/blamejs/lib/http-client.js +217 -40
- package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +3 -2
- package/lib/vendor/blamejs/lib/mail-dav.js +26 -26
- package/lib/vendor/blamejs/lib/mail-deploy.js +10 -10
- package/lib/vendor/blamejs/lib/mail-scan.js +3 -0
- package/lib/vendor/blamejs/lib/mail-store.js +7 -1
- package/lib/vendor/blamejs/lib/mail.js +5 -1
- package/lib/vendor/blamejs/lib/middleware/age-gate.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/compression.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/cors.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +5 -5
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/dpop.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/flag-context.js +17 -0
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +10 -8
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-aal.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-auth.js +4 -3
- package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +4 -3
- package/lib/vendor/blamejs/lib/middleware/require-content-type.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-methods.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-mtls.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -1
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
- package/lib/vendor/blamejs/lib/network-dns.js +3 -3
- package/lib/vendor/blamejs/lib/network-nts.js +35 -14
- package/lib/vendor/blamejs/lib/network-proxy.js +42 -4
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +1 -1
- package/lib/vendor/blamejs/lib/network-tls.js +341 -11
- package/lib/vendor/blamejs/lib/nist-crosswalk.js +2 -2
- package/lib/vendor/blamejs/lib/ntp-check.js +30 -19
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +1 -1
- package/lib/vendor/blamejs/lib/object-store/http-put.js +2 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -5
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +2 -2
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +4 -2
- package/lib/vendor/blamejs/lib/openapi.js +1 -1
- package/lib/vendor/blamejs/lib/pqc-agent.js +4 -7
- package/lib/vendor/blamejs/lib/problem-details.js +2 -1
- package/lib/vendor/blamejs/lib/redis-client.js +12 -1
- package/lib/vendor/blamejs/lib/regex-linear.js +1349 -0
- package/lib/vendor/blamejs/lib/render.js +531 -7
- package/lib/vendor/blamejs/lib/request-helpers.js +181 -5
- package/lib/vendor/blamejs/lib/router.js +37 -13
- package/lib/vendor/blamejs/lib/safe-async.js +142 -0
- package/lib/vendor/blamejs/lib/safe-icap.js +3 -0
- package/lib/vendor/blamejs/lib/safe-sql.js +15 -3
- package/lib/vendor/blamejs/lib/self-update.js +2 -2
- package/lib/vendor/blamejs/lib/testing.js +54 -7
- package/lib/vendor/blamejs/lib/validate-opts.js +56 -0
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +62 -25
- package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +2004 -142
- package/lib/vendor/blamejs/lib/vendor/browser/noble-ciphers.mjs +727 -0
- package/lib/vendor/blamejs/lib/vendor/browser/noble-hashes.mjs +1070 -0
- package/lib/vendor/blamejs/lib/vendor/browser/noble-post-quantum.mjs +1090 -0
- package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +510 -227
- package/lib/vendor/blamejs/lib/webhook-hmac.js +19 -2
- package/lib/vendor/blamejs/lib/webhook.js +1 -1
- package/lib/vendor/blamejs/lib/ws-client.js +24 -2
- 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 +46 -83
- package/lib/webhook-subscriptions.js +3 -13
- package/lib/wishlist-sharing.js +5 -10
- package/package.json +1 -1
|
@@ -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.
|
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" });
|
|
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" });
|
|
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");
|
|
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");
|
|
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");
|
|
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" });
|
|
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" });
|
|
238
233
|
return s;
|
|
239
234
|
}
|
|
240
235
|
|