@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
|
@@ -94,6 +94,7 @@ function _requireNonEmptyString(val, name) {
|
|
|
94
94
|
* sigField: string, // default: "v1"
|
|
95
95
|
* alg: string, // default: "hmac-sha256" (also "hmac-sha512")
|
|
96
96
|
* toleranceSec: number, // default: 300 (5 minutes)
|
|
97
|
+
* now: number, // epoch ms for the replay window; default: Date.now()
|
|
97
98
|
*
|
|
98
99
|
* @example
|
|
99
100
|
* var v = b.webhookHmac.verify({
|
|
@@ -107,7 +108,8 @@ function verify(opts) {
|
|
|
107
108
|
opts = validateOpts.requireObject(opts, "webhookHmac.verify",
|
|
108
109
|
WebhookHmacError, "webhook-hmac/bad-opts");
|
|
109
110
|
validateOpts(opts,
|
|
110
|
-
["header", "rawBody", "secret", "profile", "tsField", "sigField", "alg", "toleranceSec"
|
|
111
|
+
["header", "rawBody", "secret", "profile", "tsField", "sigField", "alg", "toleranceSec",
|
|
112
|
+
"now"],
|
|
111
113
|
"webhookHmac.verify");
|
|
112
114
|
|
|
113
115
|
// Resolve profile → field/alg defaults; explicit opts win.
|
|
@@ -141,6 +143,21 @@ function verify(opts) {
|
|
|
141
143
|
WebhookHmacError, "webhook-hmac/bad-tolerance");
|
|
142
144
|
var tolerance = typeof opts.toleranceSec === "number" ? opts.toleranceSec : DEFAULT_TOLERANCE_SEC;
|
|
143
145
|
|
|
146
|
+
// Replay window measured against an injectable clock (epoch milliseconds,
|
|
147
|
+
// the shape b.cwt.verify already takes), defaulting to wall time. A caller
|
|
148
|
+
// that owns its own time source -- a receiver built around an injected
|
|
149
|
+
// clock, a forensic replay of a captured delivery, a deterministic
|
|
150
|
+
// simulation -- otherwise has to abandon this primitive and rebuild the
|
|
151
|
+
// scheme by hand, since its window check and this one disagree the moment
|
|
152
|
+
// the injected clock is not now.
|
|
153
|
+
//
|
|
154
|
+
// An injected clock rather than a skip-the-check flag: the replay defense
|
|
155
|
+
// stays inside the primitive, and omitting the option falls back to wall
|
|
156
|
+
// time, so forgetting it fails closed instead of silently accepting an
|
|
157
|
+
// arbitrarily old delivery.
|
|
158
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.now, "now",
|
|
159
|
+
WebhookHmacError, "webhook-hmac/bad-now");
|
|
160
|
+
|
|
144
161
|
// Parse "t=<ts>,v1=<sig>,v1=<rotated>" — comma-separated k=v. Collect the ts
|
|
145
162
|
// and EVERY sigField value; ignore any other version keys.
|
|
146
163
|
var tsRaw = null;
|
|
@@ -169,7 +186,7 @@ function verify(opts) {
|
|
|
169
186
|
throw new WebhookHmacError("webhook-hmac/bad-timestamp",
|
|
170
187
|
"verify: '" + tsField + "' is not a positive integer");
|
|
171
188
|
}
|
|
172
|
-
var nowSec = Math.floor(Date.now() / 1000);
|
|
189
|
+
var nowSec = Math.floor((typeof opts.now === "number" ? opts.now : Date.now()) / 1000);
|
|
173
190
|
var skew = Math.abs(nowSec - ts);
|
|
174
191
|
if (skew > tolerance) {
|
|
175
192
|
throw new WebhookHmacError("webhook-hmac/timestamp-skew",
|
|
@@ -485,7 +485,7 @@ function signer(opts) {
|
|
|
485
485
|
}, retryOpts);
|
|
486
486
|
var statusCode = (res && (res.statusCode || res.status)) || 0;
|
|
487
487
|
_emitEvent("webhook.send", 1, {
|
|
488
|
-
outcome: statusCode
|
|
488
|
+
outcome: C.HTTP.success(statusCode) ? "success" : "failure",
|
|
489
489
|
status: statusCode,
|
|
490
490
|
host: hostLabel,
|
|
491
491
|
});
|
|
@@ -381,7 +381,23 @@ class WsClient extends EventEmitter {
|
|
|
381
381
|
(parsed.protocol === "wss:" ? 443 : 80); // TLS / HTTP default port
|
|
382
382
|
var host = parsed.hostname;
|
|
383
383
|
|
|
384
|
-
|
|
384
|
+
// Filled in on the wss:// path below with the options the dial actually
|
|
385
|
+
// used. dialTlsOpts is merged OVER the shared posture, so a caller that
|
|
386
|
+
// narrowed ecdhCurve or moved minVersion negotiated on something the
|
|
387
|
+
// shared object does not describe, and diagnosing against the shared one
|
|
388
|
+
// would answer for a connection that was never made.
|
|
389
|
+
var effectiveTls = null;
|
|
390
|
+
|
|
391
|
+
function _onError(err) {
|
|
392
|
+
// A wss:// handshake refused by the posture arrives as a bare OpenSSL
|
|
393
|
+
// alert; name the cause before it reaches the caller's handler.
|
|
394
|
+
networkTls().annotateOutboundFailure(err, {
|
|
395
|
+
host: host,
|
|
396
|
+
port: port,
|
|
397
|
+
tlsOpts: effectiveTls || {},
|
|
398
|
+
});
|
|
399
|
+
self._handleSocketError(err);
|
|
400
|
+
}
|
|
385
401
|
|
|
386
402
|
// Pin the connect to the SSRF-validated IPs returned by
|
|
387
403
|
// ssrfGuard.checkUrl — closes the DNS-rebinding TOCTOU window where
|
|
@@ -413,6 +429,12 @@ class WsClient extends EventEmitter {
|
|
|
413
429
|
port: port,
|
|
414
430
|
rejectUnauthorized: true,
|
|
415
431
|
}, networkTls().outboundPosture(), dialTlsOpts || {});
|
|
432
|
+
// The posture advertises RFC 8879 certificate compression, which is a
|
|
433
|
+
// TLS 1.3 extension. A caller who capped this dial lower asked for none
|
|
434
|
+
// of it, and Node refuses the whole options object rather than ignoring
|
|
435
|
+
// an extension it cannot use.
|
|
436
|
+
networkTls()._stripUnreachableCertCompression(tlsOpts, dialTlsOpts);
|
|
437
|
+
effectiveTls = tlsOpts;
|
|
416
438
|
// RFC 6066 §3: the TLS SNI ServerName MUST be a hostname, never an IP
|
|
417
439
|
// literal — Node throws ERR_INVALID_ARG_VALUE when servername is an IP,
|
|
418
440
|
// which made wss:// to any IP-literal target unusable. Send SNI only for
|
|
@@ -541,7 +563,7 @@ class WsClient extends EventEmitter {
|
|
|
541
563
|
return;
|
|
542
564
|
}
|
|
543
565
|
var status = parseInt(match[1], 10);
|
|
544
|
-
if (status !==
|
|
566
|
+
if (status !== C.HTTP.STATUS.SWITCHING_PROTOCOLS) { // HTTP 101
|
|
545
567
|
// Body bytes after the header section are the server's
|
|
546
568
|
// explanation. Surface them on the error so callers can branch
|
|
547
569
|
// on the status code and inspect the body without re-parsing
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:885de67e-71f9-4512-83e7-3538ecfd9d88",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-10T21:28:58.997Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.18.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.18.21",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.18.
|
|
25
|
+
"version": "0.18.21",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.18.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.18.21",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.18.
|
|
57
|
+
"ref": "@blamejs/core@0.18.21",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|
package/lib/vendor-invoices.js
CHANGED
|
@@ -95,10 +95,6 @@ var CURRENCY_RE = /^[A-Z]{3}$/;
|
|
|
95
95
|
// as the sibling accounts-payable primitives — operator-rendered text
|
|
96
96
|
// fields refuse these to keep the dashboard + printout safe from
|
|
97
97
|
// header-injection + visual-spoofing attacks.
|
|
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
98
|
|
|
103
99
|
var INVOICE_STATUSES = Object.freeze([
|
|
104
100
|
"received", "approved", "paid", "disputed", "voided",
|
|
@@ -107,6 +103,7 @@ var INVOICE_STATUSES = Object.freeze([
|
|
|
107
103
|
var MAX_DUE_WITHIN_DAYS = 3650; // ~10 years — bound the predicate
|
|
108
104
|
|
|
109
105
|
var b = require("./vendor/blamejs");
|
|
106
|
+
var textGuard = require("./text-guard");
|
|
110
107
|
|
|
111
108
|
var MS_PER_DAY = b.constants.TIME.days(1);
|
|
112
109
|
|
|
@@ -219,10 +216,10 @@ function _shortText(s, label, max) {
|
|
|
219
216
|
if (s.length > max) {
|
|
220
217
|
throw new TypeError("vendor-invoices: " + label + " must be <= " + max + " characters");
|
|
221
218
|
}
|
|
222
|
-
if (
|
|
219
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
223
220
|
throw new TypeError("vendor-invoices: " + label + " contains control bytes");
|
|
224
221
|
}
|
|
225
|
-
if (
|
|
222
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
226
223
|
throw new TypeError("vendor-invoices: " + label + " contains zero-width / direction-override bytes");
|
|
227
224
|
}
|
|
228
225
|
return s;
|
package/lib/vendors.js
CHANGED
|
@@ -97,10 +97,6 @@ var PHONE_RE = /^\+?[1-9]\d{1,14}$/;
|
|
|
97
97
|
// direction-override bytes are a slipping-class for header injection
|
|
98
98
|
// + visual-spoofing attacks downstream. Spelled with \u-escapes so
|
|
99
99
|
// ESLint's no-irregular-whitespace stays happy.
|
|
100
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
101
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
102
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
103
|
-
);
|
|
104
100
|
|
|
105
101
|
// Mutable columns for `updateVendor(slug, patch)`. Slug is immutable
|
|
106
102
|
// — it's the public handle + the join-table key; mutating it would
|
|
@@ -119,6 +115,7 @@ var ALLOWED_UPDATE_COLUMNS = Object.freeze([
|
|
|
119
115
|
|
|
120
116
|
// Framework handle (the vendored blamejs); index.js re-exports this as .framework.
|
|
121
117
|
var b = require("./vendor/blamejs");
|
|
118
|
+
var textGuard = require("./text-guard");
|
|
122
119
|
|
|
123
120
|
// ---- validators ---------------------------------------------------------
|
|
124
121
|
|
|
@@ -159,12 +156,7 @@ function _name(s) {
|
|
|
159
156
|
if (s.length > MAX_NAME_LEN) {
|
|
160
157
|
throw new TypeError("vendors: name must be <= " + MAX_NAME_LEN + " characters");
|
|
161
158
|
}
|
|
162
|
-
|
|
163
|
-
throw new TypeError("vendors: name contains control bytes");
|
|
164
|
-
}
|
|
165
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
166
|
-
throw new TypeError("vendors: name contains zero-width / direction-override bytes");
|
|
167
|
-
}
|
|
159
|
+
textGuard.freeText(s, "vendors: name", { singleLine: "reject", zeroWidth: "reject" });
|
|
168
160
|
return s;
|
|
169
161
|
}
|
|
170
162
|
|
|
@@ -186,9 +178,7 @@ function _payoutAddress(s) {
|
|
|
186
178
|
if (s.length > MAX_PAYOUT_ADDRESS_LEN) {
|
|
187
179
|
throw new TypeError("vendors: payout_address must be <= " + MAX_PAYOUT_ADDRESS_LEN + " characters");
|
|
188
180
|
}
|
|
189
|
-
|
|
190
|
-
throw new TypeError("vendors: payout_address contains control / zero-width bytes");
|
|
191
|
-
}
|
|
181
|
+
textGuard.freeText(s, "vendors: payout_address", { singleLine: "reject", zeroWidth: "reject" });
|
|
192
182
|
return s;
|
|
193
183
|
}
|
|
194
184
|
|
|
@@ -221,9 +211,7 @@ function _addressJson(value) {
|
|
|
221
211
|
if (encoded.length > MAX_ADDRESS_JSON_LEN) {
|
|
222
212
|
throw new TypeError("vendors: address JSON must be <= " + MAX_ADDRESS_JSON_LEN + " characters serialised");
|
|
223
213
|
}
|
|
224
|
-
|
|
225
|
-
throw new TypeError("vendors: address contains control / zero-width bytes");
|
|
226
|
-
}
|
|
214
|
+
textGuard.freeText(encoded, "vendors: address", { singleLine: "reject", zeroWidth: "reject" });
|
|
227
215
|
return encoded;
|
|
228
216
|
}
|
|
229
217
|
|
|
@@ -265,9 +253,7 @@ function _orderId(s) {
|
|
|
265
253
|
if (s.length > 256) {
|
|
266
254
|
throw new TypeError("vendors: order_id must be <= 256 characters");
|
|
267
255
|
}
|
|
268
|
-
|
|
269
|
-
throw new TypeError("vendors: order_id contains control / zero-width bytes");
|
|
270
|
-
}
|
|
256
|
+
textGuard.freeText(s, "vendors: order_id", { singleLine: "reject", zeroWidth: "reject" });
|
|
271
257
|
return s;
|
|
272
258
|
}
|
|
273
259
|
|
package/lib/webhook-receiver.js
CHANGED
|
@@ -44,12 +44,14 @@
|
|
|
44
44
|
* - `b.crypto.namespaceHash` — secret-at-rest fingerprint.
|
|
45
45
|
* - `b.crypto.sha3Hash` — body fingerprint for audit.
|
|
46
46
|
* - `b.crypto.generateBytes` — secret plaintext draw.
|
|
47
|
-
* - `b.crypto.timingSafeEqual` — constant-time hex compare on
|
|
48
|
-
* secret-hash
|
|
49
|
-
* - `b.
|
|
50
|
-
*
|
|
51
|
-
* (
|
|
52
|
-
* the
|
|
47
|
+
* - `b.crypto.timingSafeEqual` — constant-time hex compare on the
|
|
48
|
+
* secret-hash match.
|
|
49
|
+
* - `b.webhookHmac.verify` — the whole `t=<ts>,v1=<hex>` scheme:
|
|
50
|
+
* HMAC-SHA-256 over `<timestamp>.<raw-body>`,
|
|
51
|
+
* the replay window (on this receiver's
|
|
52
|
+
* injected clock and the source row's
|
|
53
|
+
* tolerance), and a constant-time compare
|
|
54
|
+
* across every signature the header carries.
|
|
53
55
|
* - `b.uuid.v7` — row ids.
|
|
54
56
|
*
|
|
55
57
|
* Surface:
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
*/
|
|
78
80
|
|
|
79
81
|
var b = require("./vendor/blamejs");
|
|
82
|
+
var textGuard = require("./text-guard");
|
|
80
83
|
var C = b.constants;
|
|
81
84
|
|
|
82
85
|
var SECRET_NAMESPACE = "webhook-receiver-secret";
|
|
@@ -116,10 +119,6 @@ var MAX_REASON_LEN = 1024;
|
|
|
116
119
|
|
|
117
120
|
var SIGNATURE_HEX_RE = /^[0-9a-f]{64}$/i; // 32 bytes of HMAC-SHA-256 = 64 hex chars
|
|
118
121
|
|
|
119
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
120
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
121
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
122
|
-
);
|
|
123
122
|
|
|
124
123
|
var STATUSES = ["received", "processed", "failed"];
|
|
125
124
|
|
|
@@ -196,9 +195,7 @@ function _outcome(s) {
|
|
|
196
195
|
if (s.length > MAX_OUTCOME_LEN) {
|
|
197
196
|
throw new TypeError("webhookReceiver: outcome must be <= " + MAX_OUTCOME_LEN + " characters");
|
|
198
197
|
}
|
|
199
|
-
|
|
200
|
-
throw new TypeError("webhookReceiver: outcome contains control / zero-width bytes");
|
|
201
|
-
}
|
|
198
|
+
textGuard.freeText(s, "webhookReceiver: outcome", { singleLine: "reject", zeroWidth: "reject" });
|
|
202
199
|
return s;
|
|
203
200
|
}
|
|
204
201
|
|
|
@@ -213,9 +210,7 @@ function _reason(s) {
|
|
|
213
210
|
if (s.length > MAX_REASON_LEN) {
|
|
214
211
|
throw new TypeError("webhookReceiver: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
215
212
|
}
|
|
216
|
-
|
|
217
|
-
throw new TypeError("webhookReceiver: reason contains control / zero-width bytes");
|
|
218
|
-
}
|
|
213
|
+
textGuard.freeText(s, "webhookReceiver: reason", { singleLine: "reject", zeroWidth: "reject" });
|
|
219
214
|
return s;
|
|
220
215
|
}
|
|
221
216
|
|
|
@@ -305,34 +300,6 @@ function _hashSecret(canonical) {
|
|
|
305
300
|
return b.crypto.namespaceHash(SECRET_NAMESPACE, canonical);
|
|
306
301
|
}
|
|
307
302
|
|
|
308
|
-
// Extract the v1 hex tag from a `t=<ts>,v1=<hex>` Stripe-shape header
|
|
309
|
-
// produced by b.webhook.sign. We pin to this composition rather than
|
|
310
|
-
// reach for node:crypto so the receiver inherits whatever HMAC-SHA-256
|
|
311
|
-
// hardening lives in the vendored blamejs surface.
|
|
312
|
-
function _hmacSha256Hex(secretPlaintext, timestampSeconds, bodyString) {
|
|
313
|
-
var header = b.webhook.sign({
|
|
314
|
-
alg: "hmac-sha256-stripe",
|
|
315
|
-
secret: secretPlaintext,
|
|
316
|
-
body: bodyString,
|
|
317
|
-
timestamp: timestampSeconds,
|
|
318
|
-
});
|
|
319
|
-
// Header shape is `t=<ts>,v1=<hex>` — the marker is the only place
|
|
320
|
-
// ",v1=" appears so a literal indexOf is the canonical parse.
|
|
321
|
-
var marker = ",v1=";
|
|
322
|
-
var i = header.indexOf(marker);
|
|
323
|
-
if (i < 0) {
|
|
324
|
-
// Defensive: a future blamejs version that changes the shape
|
|
325
|
-
// would slip past silently if we returned an empty string. Throw
|
|
326
|
-
// loudly so the operator notices at deploy time, not at the
|
|
327
|
-
// first inbound webhook.
|
|
328
|
-
throw new Error(
|
|
329
|
-
"webhookReceiver: b.webhook.sign returned an unexpected header shape " +
|
|
330
|
-
"(no ',v1=' marker) — vendored blamejs may need a refresh"
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
return header.slice(i + marker.length);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
303
|
// ---- factory ------------------------------------------------------------
|
|
337
304
|
|
|
338
305
|
function create(opts) {
|
|
@@ -610,40 +577,9 @@ function create(opts) {
|
|
|
610
577
|
timestamp_out_of_window: false,
|
|
611
578
|
};
|
|
612
579
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
ok: false,
|
|
617
|
-
event_id: null,
|
|
618
|
-
replay: false,
|
|
619
|
-
signature_invalid: true,
|
|
620
|
-
timestamp_out_of_window: false,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Replay-window check. The third-party's timestamp must land
|
|
625
|
-
// within ±replay_window_seconds of the receiver's current
|
|
626
|
-
// wall-clock. We accept future-skewed timestamps inside the
|
|
627
|
-
// window (a peer with a slightly fast clock shouldn't reject)
|
|
628
|
-
// but not arbitrarily-future timestamps that would let an
|
|
629
|
-
// attacker bypass replay protection by stamping a fresh ts.
|
|
630
|
-
var windowSec = Number(sourceRow.replay_window_seconds);
|
|
631
|
-
var nowSec = Math.floor(nowMs / 1000);
|
|
632
|
-
var ageSec = Math.abs(nowSec - timestampSeconds);
|
|
633
|
-
if (ageSec > windowSec) {
|
|
634
|
-
return {
|
|
635
|
-
ok: false,
|
|
636
|
-
event_id: null,
|
|
637
|
-
replay: false,
|
|
638
|
-
signature_invalid: false,
|
|
639
|
-
timestamp_out_of_window: true,
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Signature validation. The third-party signs the literal
|
|
644
|
-
// `<timestamp>.<body>` string with HMAC-SHA-256 under the
|
|
645
|
-
// secret. We recompute via b.webhook.sign and constant-time-
|
|
646
|
-
// compare the supplied hex against the expected hex.
|
|
580
|
+
// The signature must be exactly the hex digest before it is composed
|
|
581
|
+
// into the header below — a value carrying a comma or an `=` would
|
|
582
|
+
// otherwise add fields the parser reads as its own.
|
|
647
583
|
var supplied = input.signature_header.trim().toLowerCase();
|
|
648
584
|
if (!SIGNATURE_HEX_RE.test(supplied)) {
|
|
649
585
|
return {
|
|
@@ -654,15 +590,42 @@ function create(opts) {
|
|
|
654
590
|
timestamp_out_of_window: false,
|
|
655
591
|
};
|
|
656
592
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
593
|
+
|
|
594
|
+
// Signature + replay window, both from b.webhookHmac. It owns this exact
|
|
595
|
+
// scheme — `t=<ts>,v1=<hex>` over `<timestamp>.<raw-body>` — including
|
|
596
|
+
// the strict-integer timestamp, the skew window, a constant-time compare
|
|
597
|
+
// and multi-signature support for a rotated secret. This receiver takes
|
|
598
|
+
// its clock from the operator (opts.now) and its window from the source
|
|
599
|
+
// row, so both are passed in; the primitive gained the clock argument in
|
|
600
|
+
// 0.18.18, which is what made composing it possible at all.
|
|
601
|
+
//
|
|
602
|
+
// The body goes in as BYTES. Signing a UTF-8 string rebuild of it
|
|
603
|
+
// replaces anything that is not valid UTF-8 with U+FFFD, taking the
|
|
604
|
+
// digest over different bytes than the sender signed and rejecting a
|
|
605
|
+
// correctly-signed delivery. The timestamp goes in as the raw header
|
|
606
|
+
// text rather than a reparsed integer for the same reason.
|
|
607
|
+
var windowSec = Number(sourceRow.replay_window_seconds);
|
|
608
|
+
try {
|
|
609
|
+
b.webhookHmac.verify({
|
|
610
|
+
header: "t=" + tsStr + ",v1=" + supplied,
|
|
611
|
+
rawBody: bodyBytes,
|
|
612
|
+
secret: matched.plaintext,
|
|
613
|
+
profile: "stripe",
|
|
614
|
+
toleranceSec: windowSec,
|
|
615
|
+
now: nowMs,
|
|
616
|
+
});
|
|
617
|
+
} catch (e) {
|
|
618
|
+
// Out-of-window and bad-signature are distinct outcomes to an operator
|
|
619
|
+
// reading the delivery log: one is a clock or a replay, the other is a
|
|
620
|
+
// wrong secret. Every other refusal is a malformed header, which is
|
|
621
|
+
// not a signature the receiver can accept.
|
|
622
|
+
var skew = e && e.code === "webhook-hmac/timestamp-skew";
|
|
660
623
|
return {
|
|
661
624
|
ok: false,
|
|
662
625
|
event_id: null,
|
|
663
626
|
replay: false,
|
|
664
|
-
signature_invalid:
|
|
665
|
-
timestamp_out_of_window:
|
|
627
|
+
signature_invalid: !skew,
|
|
628
|
+
timestamp_out_of_window: skew,
|
|
666
629
|
};
|
|
667
630
|
}
|
|
668
631
|
|
|
@@ -68,10 +68,6 @@ var WILDCARD_EVENT = "*";
|
|
|
68
68
|
// control bytes are a slipping-class for header injection + visual
|
|
69
69
|
// spoofing downstream. Spelled with \u-escapes so ESLint's
|
|
70
70
|
// no-irregular-whitespace stays happy.
|
|
71
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
72
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
73
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
74
|
-
);
|
|
75
71
|
|
|
76
72
|
// Event-type identifier shape — `domain.action[.qualifier]` segments
|
|
77
73
|
// of ASCII letters / digits / underscore, joined by `.`. The wildcard
|
|
@@ -111,9 +107,7 @@ function _ownerId(s) {
|
|
|
111
107
|
if (s.length > MAX_OWNER_ID_LEN) {
|
|
112
108
|
throw new TypeError("webhookSubscriptions: owner_id must be <= " + MAX_OWNER_ID_LEN + " characters");
|
|
113
109
|
}
|
|
114
|
-
|
|
115
|
-
throw new TypeError("webhookSubscriptions: owner_id contains control / zero-width bytes");
|
|
116
|
-
}
|
|
110
|
+
textGuard.freeText(s, "webhookSubscriptions: owner_id", { singleLine: "reject", zeroWidth: "reject" });
|
|
117
111
|
return s;
|
|
118
112
|
}
|
|
119
113
|
|
|
@@ -191,9 +185,7 @@ function _name(s) {
|
|
|
191
185
|
if (s.length > MAX_NAME_LEN) {
|
|
192
186
|
throw new TypeError("webhookSubscriptions: name must be <= " + MAX_NAME_LEN + " characters");
|
|
193
187
|
}
|
|
194
|
-
|
|
195
|
-
throw new TypeError("webhookSubscriptions: name contains control / zero-width bytes");
|
|
196
|
-
}
|
|
188
|
+
textGuard.freeText(s, "webhookSubscriptions: name", { singleLine: "reject", zeroWidth: "reject" });
|
|
197
189
|
return s;
|
|
198
190
|
}
|
|
199
191
|
|
|
@@ -219,9 +211,7 @@ function _signingSecret(s) {
|
|
|
219
211
|
if (s.length > 512) {
|
|
220
212
|
throw new TypeError("webhookSubscriptions: signing_secret must be <= 512 characters");
|
|
221
213
|
}
|
|
222
|
-
|
|
223
|
-
throw new TypeError("webhookSubscriptions: signing_secret contains control / zero-width bytes");
|
|
224
|
-
}
|
|
214
|
+
textGuard.freeText(s, "webhookSubscriptions: signing_secret", { singleLine: "reject", zeroWidth: "reject" });
|
|
225
215
|
return s;
|
|
226
216
|
}
|
|
227
217
|
|
package/lib/wishlist-sharing.js
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
*/
|
|
64
64
|
|
|
65
65
|
var b = require("./vendor/blamejs");
|
|
66
|
+
var textGuard = require("./text-guard");
|
|
66
67
|
var wishlistModule = require("./wishlist");
|
|
67
68
|
|
|
68
69
|
// ---- constants ----------------------------------------------------------
|
|
@@ -82,8 +83,6 @@ var MAX_REASON_LEN = 500;
|
|
|
82
83
|
var MAX_EMAIL_LEN = 254;
|
|
83
84
|
var MAX_MEMBER_EMAILS = 50;
|
|
84
85
|
|
|
85
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
86
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
87
86
|
|
|
88
87
|
// Conservative single-line email shape — defence in depth against
|
|
89
88
|
// header-injection / control-byte smuggling when the operator
|
|
@@ -127,7 +126,7 @@ function _title(s, label) {
|
|
|
127
126
|
if (typeof s !== "string" || s.length > MAX_TITLE_LEN) {
|
|
128
127
|
throw new TypeError("wishlistSharing: " + label + " must be a string <= " + MAX_TITLE_LEN + " chars");
|
|
129
128
|
}
|
|
130
|
-
if (
|
|
129
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
131
130
|
throw new TypeError("wishlistSharing: " + label + " must not contain control bytes");
|
|
132
131
|
}
|
|
133
132
|
return s;
|
|
@@ -137,7 +136,7 @@ function _requiredTitle(s, label) {
|
|
|
137
136
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
138
137
|
throw new TypeError("wishlistSharing: " + label + " must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
|
|
139
138
|
}
|
|
140
|
-
if (
|
|
139
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
141
140
|
throw new TypeError("wishlistSharing: " + label + " must not contain control bytes");
|
|
142
141
|
}
|
|
143
142
|
return s;
|
|
@@ -162,9 +161,7 @@ function _reason(s) {
|
|
|
162
161
|
if (typeof s !== "string" || !s.length || s.length > MAX_REASON_LEN) {
|
|
163
162
|
throw new TypeError("wishlistSharing: reason must be a non-empty string <= " + MAX_REASON_LEN + " chars");
|
|
164
163
|
}
|
|
165
|
-
|
|
166
|
-
throw new TypeError("wishlistSharing: reason must not contain control bytes");
|
|
167
|
-
}
|
|
164
|
+
textGuard.freeText(s, "wishlistSharing: reason");
|
|
168
165
|
return s;
|
|
169
166
|
}
|
|
170
167
|
|
|
@@ -205,9 +202,7 @@ function _viewerSessionId(s) {
|
|
|
205
202
|
// Session ids are opaque to this primitive; refuse control bytes
|
|
206
203
|
// so the hashed breadcrumb has no log-injection vector even if a
|
|
207
204
|
// downstream tool ever rendered the raw value.
|
|
208
|
-
|
|
209
|
-
throw new TypeError("wishlistSharing: viewer_session_id must not contain control bytes");
|
|
210
|
-
}
|
|
205
|
+
textGuard.freeText(s, "wishlistSharing: viewer_session_id", { singleLine: "reject" });
|
|
211
206
|
return s;
|
|
212
207
|
}
|
|
213
208
|
|
package/package.json
CHANGED