@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
package/lib/storefront-forms.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
|
var MAX_SLUG_LEN = 80;
|
|
79
80
|
var MAX_TITLE_LEN = 200;
|
|
@@ -144,14 +145,9 @@ var WEBHOOK_EVENT_RE = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$/;
|
|
|
144
145
|
// Refuse C0 control bytes + DEL in single-line strings. Textarea
|
|
145
146
|
// values permit LF (the customer's free-text response is line-
|
|
146
147
|
// oriented).
|
|
147
|
-
var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
|
|
148
|
-
var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
149
148
|
|
|
150
149
|
// Zero-width / direction-override family. Spelled with \u-escapes
|
|
151
150
|
// so ESLint's no-irregular-whitespace stays happy.
|
|
152
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
153
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
154
|
-
);
|
|
155
151
|
|
|
156
152
|
// ---- validators ---------------------------------------------------------
|
|
157
153
|
|
|
@@ -170,10 +166,10 @@ function _line(s, label, maxLen, optional) {
|
|
|
170
166
|
if (typeof s !== "string" || !s.length || s.length > maxLen) {
|
|
171
167
|
throw new TypeError("storefrontForms: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
|
|
172
168
|
}
|
|
173
|
-
if (
|
|
169
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
174
170
|
throw new TypeError("storefrontForms: " + label + " contains control bytes (incl. CR/LF)");
|
|
175
171
|
}
|
|
176
|
-
if (
|
|
172
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
177
173
|
throw new TypeError("storefrontForms: " + label + " contains zero-width / direction-override characters");
|
|
178
174
|
}
|
|
179
175
|
return s;
|
|
@@ -184,12 +180,7 @@ function _description(s) {
|
|
|
184
180
|
if (typeof s !== "string" || s.length > MAX_DESCRIPTION_LEN) {
|
|
185
181
|
throw new TypeError("storefrontForms: description must be a string ≤ " + MAX_DESCRIPTION_LEN + " chars");
|
|
186
182
|
}
|
|
187
|
-
|
|
188
|
-
throw new TypeError("storefrontForms: description contains control bytes");
|
|
189
|
-
}
|
|
190
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
191
|
-
throw new TypeError("storefrontForms: description contains zero-width / direction-override characters");
|
|
192
|
-
}
|
|
183
|
+
textGuard.freeText(s, "storefrontForms: description", { zeroWidth: "reject" });
|
|
193
184
|
return s;
|
|
194
185
|
}
|
|
195
186
|
|
|
@@ -246,7 +237,7 @@ function _fields(input) {
|
|
|
246
237
|
if (typeof opt !== "string" || !opt.length || opt.length > MAX_OPTION_LEN) {
|
|
247
238
|
throw new TypeError("storefrontForms: fields[" + i + "].options[" + j + "] must be a non-empty string ≤ " + MAX_OPTION_LEN + " chars");
|
|
248
239
|
}
|
|
249
|
-
if (
|
|
240
|
+
if (textGuard.hasCodepointThreat(opt, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
250
241
|
throw new TypeError("storefrontForms: fields[" + i + "].options[" + j + "] contains control / zero-width characters");
|
|
251
242
|
}
|
|
252
243
|
if (Object.prototype.hasOwnProperty.call(seenOpts, opt)) {
|
|
@@ -275,9 +266,7 @@ function _submitTo(input) {
|
|
|
275
266
|
if (typeof input.value !== "string" || !input.value.length || input.value.length > MAX_SUBMIT_TO_VALUE_LEN) {
|
|
276
267
|
throw new TypeError("storefrontForms: submit_to.value must be a non-empty string ≤ " + MAX_SUBMIT_TO_VALUE_LEN + " chars");
|
|
277
268
|
}
|
|
278
|
-
|
|
279
|
-
throw new TypeError("storefrontForms: submit_to.value contains control / zero-width characters");
|
|
280
|
-
}
|
|
269
|
+
textGuard.freeText(input.value, "storefrontForms: submit_to.value", { singleLine: "reject", zeroWidth: "reject" });
|
|
281
270
|
if (input.kind === "email") {
|
|
282
271
|
var v = b.guardEmail.validate(input.value, { profile: "strict" });
|
|
283
272
|
if (!v.ok) {
|
|
@@ -329,9 +318,7 @@ function _ipHash(s) {
|
|
|
329
318
|
if (typeof s !== "string" || !s.length || s.length > 256) {
|
|
330
319
|
throw new TypeError("storefrontForms: ip_hash must be a non-empty string ≤ 256 chars");
|
|
331
320
|
}
|
|
332
|
-
|
|
333
|
-
throw new TypeError("storefrontForms: ip_hash contains control bytes");
|
|
334
|
-
}
|
|
321
|
+
textGuard.freeText(s, "storefrontForms: ip_hash", { singleLine: "reject" });
|
|
335
322
|
return s;
|
|
336
323
|
}
|
|
337
324
|
|
|
@@ -358,7 +345,7 @@ function _validateValue(field, raw) {
|
|
|
358
345
|
if (typeof raw !== "string" || raw.length > MAX_TEXT_VALUE_LEN) {
|
|
359
346
|
return { ok: false, error: "text value must be a string ≤ " + MAX_TEXT_VALUE_LEN + " chars" };
|
|
360
347
|
}
|
|
361
|
-
if (
|
|
348
|
+
if (textGuard.hasCodepointThreat(raw, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
362
349
|
return { ok: false, error: "text value contains control / zero-width characters" };
|
|
363
350
|
}
|
|
364
351
|
return { ok: true, value: raw };
|
|
@@ -387,7 +374,7 @@ function _validateValue(field, raw) {
|
|
|
387
374
|
if (typeof raw !== "string" || !raw.length || raw.length > MAX_TEXTAREA_VALUE_LEN) {
|
|
388
375
|
return { ok: false, error: "textarea value must be a non-empty string ≤ " + MAX_TEXTAREA_VALUE_LEN + " chars" };
|
|
389
376
|
}
|
|
390
|
-
if (
|
|
377
|
+
if (textGuard.hasCodepointThreat(raw, { zeroWidth: "reject" })) {
|
|
391
378
|
return { ok: false, error: "textarea value contains control / zero-width characters" };
|
|
392
379
|
}
|
|
393
380
|
return { ok: true, value: raw };
|
package/lib/storefront-pages.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
|
var MAX_SLUG_LEN = 80;
|
|
79
80
|
var MAX_TITLE_LEN = 200;
|
|
@@ -112,15 +113,10 @@ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
|
112
113
|
// body permits LF (Markdown is line-oriented); single-line fields
|
|
113
114
|
// refuse LF / CR so they can't smuggle a second line into a page
|
|
114
115
|
// header / meta tag.
|
|
115
|
-
var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
|
|
116
|
-
var CONTROL_BYTE_BLOCK_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
117
116
|
|
|
118
117
|
// Zero-width / direction-override family — mirrors the promo-banners
|
|
119
118
|
// + gift-options catalogues. Spelled with \u-escapes so ESLint's
|
|
120
119
|
// no-irregular-whitespace stays happy.
|
|
121
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
122
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
123
|
-
);
|
|
124
120
|
|
|
125
121
|
// ---- validators ---------------------------------------------------------
|
|
126
122
|
|
|
@@ -135,12 +131,7 @@ function _title(s) {
|
|
|
135
131
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
136
132
|
throw new TypeError("storefrontPages: title must be a non-empty string ≤ " + MAX_TITLE_LEN + " chars");
|
|
137
133
|
}
|
|
138
|
-
|
|
139
|
-
throw new TypeError("storefrontPages: title contains control bytes (incl. CR/LF)");
|
|
140
|
-
}
|
|
141
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
142
|
-
throw new TypeError("storefrontPages: title contains zero-width / direction-override characters");
|
|
143
|
-
}
|
|
134
|
+
textGuard.freeText(s, "storefrontPages: title", { singleLine: "reject", zeroWidth: "reject" });
|
|
144
135
|
return s;
|
|
145
136
|
}
|
|
146
137
|
|
|
@@ -148,12 +139,7 @@ function _body(s) {
|
|
|
148
139
|
if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
|
|
149
140
|
throw new TypeError("storefrontPages: body must be a non-empty string ≤ " + MAX_BODY_LEN + " chars");
|
|
150
141
|
}
|
|
151
|
-
|
|
152
|
-
throw new TypeError("storefrontPages: body contains control bytes");
|
|
153
|
-
}
|
|
154
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
155
|
-
throw new TypeError("storefrontPages: body contains zero-width / direction-override characters");
|
|
156
|
-
}
|
|
142
|
+
textGuard.freeText(s, "storefrontPages: body", { zeroWidth: "reject" });
|
|
157
143
|
return s;
|
|
158
144
|
}
|
|
159
145
|
|
|
@@ -162,10 +148,10 @@ function _metaLine(s, label, maxLen) {
|
|
|
162
148
|
if (typeof s !== "string" || s.length > maxLen) {
|
|
163
149
|
throw new TypeError("storefrontPages: " + label + " must be a string ≤ " + maxLen + " chars");
|
|
164
150
|
}
|
|
165
|
-
if (
|
|
151
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
166
152
|
throw new TypeError("storefrontPages: " + label + " contains control bytes (incl. CR/LF)");
|
|
167
153
|
}
|
|
168
|
-
if (
|
|
154
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
169
155
|
throw new TypeError("storefrontPages: " + label + " contains zero-width / direction-override characters");
|
|
170
156
|
}
|
|
171
157
|
return s;
|
|
@@ -239,7 +225,7 @@ function _esc(s) {
|
|
|
239
225
|
// downgrade an inline link.
|
|
240
226
|
function _safeLinkUrl(url) {
|
|
241
227
|
if (typeof url !== "string" || !url.length || url.length > 2048) return null;
|
|
242
|
-
if (
|
|
228
|
+
if (textGuard.hasCodepointThreat(url, { singleLine: "reject", zeroWidth: "reject" })) return null;
|
|
243
229
|
if (url.charCodeAt(0) === 47 /* "/" */) {
|
|
244
230
|
if (url.length > 1 && url.charCodeAt(1) === 47) return null;
|
|
245
231
|
if (url.indexOf("..") !== -1) return null;
|
package/lib/storefront.js
CHANGED
|
@@ -39,6 +39,7 @@ var REGISTRY_OCCASIONS = giftRegistryModule.OCCASIONS;
|
|
|
39
39
|
var { AsyncLocalStorage } = require("node:async_hooks"); // allow:non-shop-require — Node-core per-request context (no npm dep); the framework itself composes it in db-role-context / log. No b.* request-context primitive exists to wrap it.
|
|
40
40
|
|
|
41
41
|
var b = require("./vendor/blamejs");
|
|
42
|
+
var textGuard = require("./text-guard");
|
|
42
43
|
|
|
43
44
|
// Per-request locale context store. The storefront's locale middleware
|
|
44
45
|
// resolves the request's locale + chrome strings once and seeds this
|
|
@@ -2141,7 +2142,6 @@ function _clampPage(page, total, pageSize) {
|
|
|
2141
2142
|
var SEARCH_MAX_FACET_KEYS = 32;
|
|
2142
2143
|
var SEARCH_MAX_FACET_VALUES = 64;
|
|
2143
2144
|
var SEARCH_MAX_VALUE_LEN = 256;
|
|
2144
|
-
var SEARCH_CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
2145
2145
|
// Global twin for strip-all replaces (the non-global one above is used for
|
|
2146
2146
|
// presence tests; `.replace` needs the `g` flag to clear every byte).
|
|
2147
2147
|
var SEARCH_CONTROL_BYTE_RE_G = /[\x00-\x1f\x7f]/g;
|
|
@@ -2162,7 +2162,7 @@ function _parseSearchFilters(url, facetDefs) {
|
|
|
2162
2162
|
if (!Object.prototype.hasOwnProperty.call(known, rawKey)) return;
|
|
2163
2163
|
if (typeof rawVal !== "string") return;
|
|
2164
2164
|
if (!rawVal.length || rawVal.length > SEARCH_MAX_VALUE_LEN) return;
|
|
2165
|
-
if (
|
|
2165
|
+
if (textGuard.hasCodepointThreat(rawVal, { singleLine: "reject" })) return;
|
|
2166
2166
|
if (!Object.prototype.hasOwnProperty.call(out, rawKey)) {
|
|
2167
2167
|
if (keyCount >= SEARCH_MAX_FACET_KEYS) return;
|
|
2168
2168
|
out[rawKey] = [];
|
|
@@ -15928,28 +15928,26 @@ function mount(router, deps) {
|
|
|
15928
15928
|
// is built from the validated UUID order id alone (hex + hyphens),
|
|
15929
15929
|
// carrying no quote/CRLF that could break out of the header.
|
|
15930
15930
|
var safeId = String(o.id).replace(/[^A-Za-z0-9._-]/g, "");
|
|
15931
|
-
res.status(200);
|
|
15932
|
-
if (res.setHeader) {
|
|
15933
|
-
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
15934
|
-
res.setHeader("content-disposition", "attachment; filename=\"receipt-" + safeId + ".html\"");
|
|
15935
|
-
res.setHeader("x-content-type-options", "nosniff");
|
|
15936
|
-
// The receipt carries the buyer's name + address + line items —
|
|
15937
|
-
// keep it out of any shared/proxy cache.
|
|
15938
|
-
res.setHeader("cache-control", "no-store");
|
|
15939
|
-
}
|
|
15940
15931
|
// One chunk per batch as the source yields it — bounded memory
|
|
15941
|
-
// regardless of document size.
|
|
15942
|
-
//
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
15950
|
-
|
|
15951
|
-
|
|
15952
|
-
|
|
15932
|
+
// regardless of document size. render.stream awaits backpressure so a
|
|
15933
|
+
// slow client cannot queue the whole document in the socket buffer,
|
|
15934
|
+
// stops when the peer disconnects, and signals a mid-stream failure as
|
|
15935
|
+
// a broken transfer rather than ending a truncated body as a clean 200.
|
|
15936
|
+
//
|
|
15937
|
+
// Headers go in the call, not via setHeader beforehand: render.stream
|
|
15938
|
+
// writes its own defaults with the first chunk, and its default
|
|
15939
|
+
// Content-Type would replace one set earlier. Cache-Control is set
|
|
15940
|
+
// explicitly because the receipt carries the buyer's name, address and
|
|
15941
|
+
// line items and must stay out of any shared or proxy cache.
|
|
15942
|
+
await b.render.stream(res, _streamReceiptDocument(doc), {
|
|
15943
|
+
status: 200,
|
|
15944
|
+
headers: {
|
|
15945
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
15946
|
+
"Content-Disposition": "attachment; filename=\"receipt-" + safeId + ".html\"",
|
|
15947
|
+
"X-Content-Type-Options": "nosniff",
|
|
15948
|
+
"Cache-Control": "no-store",
|
|
15949
|
+
},
|
|
15950
|
+
});
|
|
15953
15951
|
});
|
|
15954
15952
|
}
|
|
15955
15953
|
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
*/
|
|
76
76
|
|
|
77
77
|
var b = require("./vendor/blamejs");
|
|
78
|
+
var textGuard = require("./text-guard");
|
|
78
79
|
|
|
79
80
|
// ---- constants ----------------------------------------------------------
|
|
80
81
|
|
|
@@ -94,10 +95,6 @@ var DEFAULT_LIST_LIMIT = 100;
|
|
|
94
95
|
// sibling subscription-controls primitive — operator-authored prose
|
|
95
96
|
// flows into receipt emails + the operator dashboard, so the same
|
|
96
97
|
// "no direction-override / no invisible glyph" floor applies.
|
|
97
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
98
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
99
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
100
|
-
);
|
|
101
98
|
|
|
102
99
|
// ---- validators ---------------------------------------------------------
|
|
103
100
|
|
|
@@ -146,9 +143,7 @@ function _reason(s) {
|
|
|
146
143
|
if (s.length > MAX_REASON_LEN) {
|
|
147
144
|
throw new TypeError("subscriptionBilling: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
148
145
|
}
|
|
149
|
-
|
|
150
|
-
throw new TypeError("subscriptionBilling: reason contains control / zero-width bytes");
|
|
151
|
-
}
|
|
146
|
+
textGuard.freeText(s, "subscriptionBilling: reason", { zeroWidth: "reject" });
|
|
152
147
|
return s;
|
|
153
148
|
}
|
|
154
149
|
|
|
@@ -164,7 +159,7 @@ function _shortText(s, label, max) {
|
|
|
164
159
|
if (s.length > max) {
|
|
165
160
|
throw new TypeError("subscriptionBilling: " + label + " must be <= " + max + " characters");
|
|
166
161
|
}
|
|
167
|
-
if (
|
|
162
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
168
163
|
throw new TypeError("subscriptionBilling: " + label + " contains control bytes");
|
|
169
164
|
}
|
|
170
165
|
return s;
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
53
|
var b = require("./vendor/blamejs");
|
|
54
|
+
var textGuard = require("./text-guard");
|
|
54
55
|
|
|
55
56
|
var C = b.constants;
|
|
56
57
|
|
|
@@ -123,10 +124,6 @@ var MAX_QUANTITY = 1000000;
|
|
|
123
124
|
// prose. Refuses C0 / DEL and the LRM/RLM/ZWJ/ZWNJ family so a
|
|
124
125
|
// malicious reason can't smuggle direction-override / invisible
|
|
125
126
|
// content into the operator dashboard.
|
|
126
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
127
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
128
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
129
|
-
);
|
|
130
127
|
|
|
131
128
|
// ---- validators ---------------------------------------------------------
|
|
132
129
|
|
|
@@ -142,9 +139,7 @@ function _reason(s) {
|
|
|
142
139
|
if (s.length > MAX_REASON_LEN) {
|
|
143
140
|
throw new TypeError("subscriptionControls: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
144
141
|
}
|
|
145
|
-
|
|
146
|
-
throw new TypeError("subscriptionControls: reason contains control / zero-width bytes");
|
|
147
|
-
}
|
|
142
|
+
textGuard.freeText(s, "subscriptionControls: reason", { zeroWidth: "reject" });
|
|
148
143
|
return s;
|
|
149
144
|
}
|
|
150
145
|
|
package/lib/suggestion-box.js
CHANGED
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
*/
|
|
120
120
|
|
|
121
121
|
var b = require("./vendor/blamejs");
|
|
122
|
+
var textGuard = require("./text-guard");
|
|
122
123
|
|
|
123
124
|
// ---- constants ----------------------------------------------------------
|
|
124
125
|
|
|
@@ -168,11 +169,6 @@ var ORDER_KEY_NEWEST = ["created_at:desc", "id:desc"];
|
|
|
168
169
|
var ORDER_KEY_TOP_VOTED = ["vote_count:desc", "id:desc"];
|
|
169
170
|
var ORDER_KEY_MOST_DISCUSSED = ["comment_count:desc", "id:desc"];
|
|
170
171
|
|
|
171
|
-
var CONTROL_BYTE_BODY_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
172
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
173
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
174
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
175
|
-
);
|
|
176
172
|
|
|
177
173
|
// ---- monotonic clock ----------------------------------------------------
|
|
178
174
|
//
|
|
@@ -205,12 +201,7 @@ function _title(s) {
|
|
|
205
201
|
if (s.length > MAX_TITLE_LEN) {
|
|
206
202
|
throw new TypeError("suggestionBox: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
207
203
|
}
|
|
208
|
-
|
|
209
|
-
throw new TypeError("suggestionBox: title must not contain control bytes");
|
|
210
|
-
}
|
|
211
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
212
|
-
throw new TypeError("suggestionBox: title contains zero-width / direction-override characters");
|
|
213
|
-
}
|
|
204
|
+
textGuard.freeText(s, "suggestionBox: title", { singleLine: "reject", zeroWidth: "reject" });
|
|
214
205
|
return s;
|
|
215
206
|
}
|
|
216
207
|
|
|
@@ -225,12 +216,10 @@ function _body(s) {
|
|
|
225
216
|
if (s.length > MAX_BODY_LEN) {
|
|
226
217
|
throw new TypeError("suggestionBox: body must be <= " + MAX_BODY_LEN + " characters");
|
|
227
218
|
}
|
|
228
|
-
if (
|
|
219
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
229
220
|
throw new TypeError("suggestionBox: body must not contain control bytes");
|
|
230
221
|
}
|
|
231
|
-
|
|
232
|
-
throw new TypeError("suggestionBox: body contains zero-width / direction-override characters");
|
|
233
|
-
}
|
|
222
|
+
textGuard.freeText(s, "suggestionBox: body", { zeroWidth: "reject" });
|
|
234
223
|
return s;
|
|
235
224
|
}
|
|
236
225
|
|
|
@@ -307,9 +296,7 @@ function _sessionIdRaw(s) {
|
|
|
307
296
|
if (s.length > 256) {
|
|
308
297
|
throw new TypeError("suggestionBox: session_id must be <= 256 characters");
|
|
309
298
|
}
|
|
310
|
-
|
|
311
|
-
throw new TypeError("suggestionBox: session_id contains control / zero-width bytes");
|
|
312
|
-
}
|
|
299
|
+
textGuard.freeText(s, "suggestionBox: session_id", { singleLine: "reject", zeroWidth: "reject" });
|
|
313
300
|
return s;
|
|
314
301
|
}
|
|
315
302
|
|
|
@@ -320,12 +307,10 @@ function _response(s) {
|
|
|
320
307
|
if (s.length > MAX_RESPONSE_LEN) {
|
|
321
308
|
throw new TypeError("suggestionBox: response must be <= " + MAX_RESPONSE_LEN + " characters");
|
|
322
309
|
}
|
|
323
|
-
if (
|
|
310
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
324
311
|
throw new TypeError("suggestionBox: response must not contain control bytes");
|
|
325
312
|
}
|
|
326
|
-
|
|
327
|
-
throw new TypeError("suggestionBox: response contains zero-width / direction-override characters");
|
|
328
|
-
}
|
|
313
|
+
textGuard.freeText(s, "suggestionBox: response", { zeroWidth: "reject" });
|
|
329
314
|
// A status-only transition is allowed with response === "" (no
|
|
330
315
|
// operator-visible reply, just a state change). Trim-then-check
|
|
331
316
|
// for the operator-supplied case below in respondToSuggestion.
|
|
@@ -339,9 +324,7 @@ function _responder(s) {
|
|
|
339
324
|
if (s.length > MAX_RESPONDER_LEN) {
|
|
340
325
|
throw new TypeError("suggestionBox: responder must be <= " + MAX_RESPONDER_LEN + " characters");
|
|
341
326
|
}
|
|
342
|
-
|
|
343
|
-
throw new TypeError("suggestionBox: responder contains control / zero-width characters");
|
|
344
|
-
}
|
|
327
|
+
textGuard.freeText(s, "suggestionBox: responder", { singleLine: "reject", zeroWidth: "reject" });
|
|
345
328
|
return s;
|
|
346
329
|
}
|
|
347
330
|
|
|
@@ -925,7 +908,7 @@ function create(opts) {
|
|
|
925
908
|
if (typeof input.email_hash !== "string" || !input.email_hash.length || input.email_hash.length > 256) {
|
|
926
909
|
throw new TypeError("suggestionBox." + method + ": email_hash must be a non-empty string <= 256 chars when provided");
|
|
927
910
|
}
|
|
928
|
-
if (
|
|
911
|
+
if (textGuard.hasCodepointThreat(input.email_hash, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
929
912
|
throw new TypeError("suggestionBox." + method + ": email_hash contains control / zero-width bytes");
|
|
930
913
|
}
|
|
931
914
|
emailHash = input.email_hash;
|
package/lib/support-tickets.js
CHANGED
|
@@ -62,6 +62,7 @@ var DEFAULT_LIST_LIMIT = 25;
|
|
|
62
62
|
var EMAIL_NAMESPACE = "support-ticket-email";
|
|
63
63
|
|
|
64
64
|
var b = require("./vendor/blamejs");
|
|
65
|
+
var textGuard = require("./text-guard");
|
|
65
66
|
|
|
66
67
|
var ALLOWED_AUTHORS = ["customer", "operator", "system"];
|
|
67
68
|
var ALLOWED_CATEGORIES = [
|
|
@@ -104,14 +105,9 @@ var LIST_ORDER_KEY = ["opened_at:desc", "id:desc"];
|
|
|
104
105
|
// Refuse C0 control bytes + DEL. Newlines (LF/CR) + tab are tolerated
|
|
105
106
|
// in body so multi-line customer narratives work; subject is single-
|
|
106
107
|
// line by convention and refuses them.
|
|
107
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
108
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
109
108
|
// Zero-width + direction-override family (see orderNotes for the
|
|
110
109
|
// same justification). Spelled via \u escapes so the file stays
|
|
111
110
|
// ESLint no-irregular-whitespace clean.
|
|
112
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
113
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
114
|
-
);
|
|
115
111
|
|
|
116
112
|
// ---- validators ---------------------------------------------------------
|
|
117
113
|
|
|
@@ -159,12 +155,7 @@ function _subject(s) {
|
|
|
159
155
|
if (s.length > MAX_SUBJECT_LEN) {
|
|
160
156
|
throw new TypeError("supportTickets: subject must be <= " + MAX_SUBJECT_LEN + " characters");
|
|
161
157
|
}
|
|
162
|
-
|
|
163
|
-
throw new TypeError("supportTickets: subject contains control bytes");
|
|
164
|
-
}
|
|
165
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
166
|
-
throw new TypeError("supportTickets: subject contains zero-width / direction-override characters");
|
|
167
|
-
}
|
|
158
|
+
textGuard.freeText(s, "supportTickets: subject", { singleLine: "reject", zeroWidth: "reject" });
|
|
168
159
|
return s;
|
|
169
160
|
}
|
|
170
161
|
|
|
@@ -179,12 +170,7 @@ function _body(s) {
|
|
|
179
170
|
if (s.length > MAX_BODY_LEN) {
|
|
180
171
|
throw new TypeError("supportTickets: body must be <= " + MAX_BODY_LEN + " characters");
|
|
181
172
|
}
|
|
182
|
-
|
|
183
|
-
throw new TypeError("supportTickets: body contains control bytes");
|
|
184
|
-
}
|
|
185
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
186
|
-
throw new TypeError("supportTickets: body contains zero-width / direction-override characters");
|
|
187
|
-
}
|
|
173
|
+
textGuard.freeText(s, "supportTickets: body", { zeroWidth: "reject" });
|
|
188
174
|
return s;
|
|
189
175
|
}
|
|
190
176
|
|
|
@@ -205,7 +191,7 @@ function _tags(input) {
|
|
|
205
191
|
if (t.length > MAX_TAG_LEN) {
|
|
206
192
|
throw new TypeError("supportTickets: tags[" + i + "] must be <= " + MAX_TAG_LEN + " characters");
|
|
207
193
|
}
|
|
208
|
-
if (
|
|
194
|
+
if (textGuard.hasCodepointThreat(t, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
209
195
|
throw new TypeError("supportTickets: tags[" + i + "] contains control / zero-width bytes");
|
|
210
196
|
}
|
|
211
197
|
out.push(t);
|
|
@@ -220,9 +206,7 @@ function _singleTag(t) {
|
|
|
220
206
|
if (t.length > MAX_TAG_LEN) {
|
|
221
207
|
throw new TypeError("supportTickets: tag must be <= " + MAX_TAG_LEN + " characters");
|
|
222
208
|
}
|
|
223
|
-
|
|
224
|
-
throw new TypeError("supportTickets: tag contains control / zero-width bytes");
|
|
225
|
-
}
|
|
209
|
+
textGuard.freeText(t, "supportTickets: tag", { singleLine: "reject", zeroWidth: "reject" });
|
|
226
210
|
return t;
|
|
227
211
|
}
|
|
228
212
|
|
|
@@ -235,9 +219,7 @@ function _reason(r) {
|
|
|
235
219
|
if (r.length > MAX_REASON_LEN) {
|
|
236
220
|
throw new TypeError("supportTickets: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
237
221
|
}
|
|
238
|
-
|
|
239
|
-
throw new TypeError("supportTickets: reason contains control / zero-width bytes");
|
|
240
|
-
}
|
|
222
|
+
textGuard.freeText(r, "supportTickets: reason", { zeroWidth: "reject" });
|
|
241
223
|
return r;
|
|
242
224
|
}
|
|
243
225
|
|
package/lib/tax-remittance.js
CHANGED
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
*/
|
|
87
87
|
|
|
88
88
|
var b = require("./vendor/blamejs");
|
|
89
|
+
var textGuard = require("./text-guard");
|
|
89
90
|
|
|
90
91
|
var C = b.constants;
|
|
91
92
|
|
|
@@ -107,7 +108,6 @@ var DEFAULT_DAYS_LATE_MIN = 1;
|
|
|
107
108
|
var MAX_DAYS_LATE_MIN = 3650; // ~10 years
|
|
108
109
|
var MS_PER_DAY = C.TIME.days(1);
|
|
109
110
|
|
|
110
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
111
111
|
|
|
112
112
|
// ---- monotonic clock ----------------------------------------------------
|
|
113
113
|
//
|
|
@@ -182,7 +182,7 @@ function _shortText(s, label, max) {
|
|
|
182
182
|
throw new TypeError("taxRemittance: " + label +
|
|
183
183
|
" must be a non-empty string <= " + max + " chars");
|
|
184
184
|
}
|
|
185
|
-
if (
|
|
185
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
186
186
|
throw new TypeError("taxRemittance: " + label + " must not contain control bytes");
|
|
187
187
|
}
|
|
188
188
|
return s;
|
package/lib/tenants.js
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
58
|
var b = require("./vendor/blamejs");
|
|
59
|
+
var textGuard = require("./text-guard");
|
|
59
60
|
|
|
60
61
|
var MAX_SLUG_LEN = 64;
|
|
61
62
|
var MAX_NAME_LEN = 200;
|
|
@@ -95,10 +96,6 @@ var THEME_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
|
95
96
|
// renders in operator dashboards; embedded control / direction-
|
|
96
97
|
// override bytes are a slipping-class for visual-spoofing attacks
|
|
97
98
|
// downstream.
|
|
98
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
99
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
100
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
101
|
-
);
|
|
102
99
|
|
|
103
100
|
var ALLOWED_UPDATE_COLUMNS = Object.freeze([
|
|
104
101
|
"name", "default_currency", "default_locale", "theme_slug",
|
|
@@ -131,9 +128,7 @@ function _name(s) {
|
|
|
131
128
|
if (s.length > MAX_NAME_LEN) {
|
|
132
129
|
throw new TypeError("tenants: name must be <= " + MAX_NAME_LEN + " characters");
|
|
133
130
|
}
|
|
134
|
-
|
|
135
|
-
throw new TypeError("tenants: name contains control / zero-width bytes");
|
|
136
|
-
}
|
|
131
|
+
textGuard.freeText(s, "tenants: name", { singleLine: "reject", zeroWidth: "reject" });
|
|
137
132
|
return s;
|
|
138
133
|
}
|
|
139
134
|
|