@blamejs/blamejs-shop 0.4.42 → 0.4.43

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - v0.4.43 (2026-06-13) — **Harden input handling: linear-time pattern matching on caller-influenced strings, complete escaping, and redacted edge-bridge error responses.** A set of input-handling hardening fixes. Several internal pattern matches that run on caller-influenced strings used regular expressions whose shape could be driven into super-linear (polynomial) backtracking by a long run of a single character — a denial-of-service shape. These are rewritten to linear-time equivalents that accept and reject exactly the same inputs: the email-shape guard in the analytics and clickstream PII filters, a whitespace trim in the captcha gate, and the trailing-slash trim on the D1 and R2 bridge URLs. A JSON-escape helper in the catalog draft path now escapes backslashes as well as quotes so a crafted value can't smuggle an escape. And the edge worker's database- and storage-bridge error responses no longer return internal error detail to the caller — the detail is logged at the edge and the client receives only a generic failure code. No migration to apply. **Security:** *Pattern matching on caller-influenced input is now linear-time* — The email-shape detector used by the analytics and clickstream PII guards, the whitespace trim in the captcha gate, and the trailing-slash trim applied to the D1 and R2 bridge URLs were regular expressions whose backtracking could grow polynomially with the input length — a long run of one character could pin a request thread. Each is replaced with a linear-time form (a bounded, unambiguous regex; a native trim; a small character loop) proven to accept and reject the identical set of inputs, so the same values still pass and fail while a crafted long input can no longer cause super-linear work. · *Edge bridge errors no longer leak internal detail* — The Cloudflare Worker's database-bridge and storage-bridge endpoints returned the underlying error message in their 500 responses. They now return only a generic failure code to the caller and log the (redacted) detail at the edge, so an internal error string or stack fragment is never exposed in a response body. · *Complete escaping in the catalog draft lookup* — The catalog draft path builds a JSON-fragment match string from a SKU; its escape step now escapes backslashes before quotes, so the escaping is complete and a value containing a backslash cannot break out of the quoted fragment.
12
+
11
13
  - v0.4.42 (2026-06-13) — **A return refund is now recorded against the order, so it can't be paid out a second time from the order console.** Issuing a provider refund for a return moved the money but never recorded it against the order, so order.refundedTotalMinor didn't count it. The order console's refund caps each refund at the order's remaining un-refunded balance — but with the return refund invisible to that balance, an operator could refund the full order total a second time from the order screen, paying the customer back twice. A return's provider refund now writes to the order's refund ledger, stamped with the provider refund id so the entry collapses with the payment provider's own refund webhook — whether the console or the webhook records it first, the same refund is counted exactly once. The order's move to a fully-refunded state and the gift-card / loyalty reversals remain driven by the refund webhook, as before. No migration to apply. **Fixed:** *A return refund now counts against the order's refunded total* — When the returns console issues a provider refund, the amount is now recorded against the order through the same deduplicating ledger path a partial console refund uses, stamped with the provider refund id. Previously the money moved but the order's refunded total never saw it, so the order console's over-refund cap — which limits a refund to the order's remaining balance — could be cleared again and pay the customer back a second time. Keying the entry on the provider refund id makes it idempotent against the provider's refund webhook: whichever path records first, a refund mirrored by both is counted exactly once. The order's terminal refunded state and the gift-card / loyalty reversals stay driven by the refund webhook.
12
14
 
13
15
  - v0.4.41 (2026-06-13) — **Subscription billing no longer double-charges on a retried period close or a concurrent immediate plan change.** Two billing paths could invoice the same charge twice. A metered-usage period close is cron-driven and at-least-once, but it enqueued its roll-up invoice with no idempotency key, so a retried tick — a transient error mid-run, an at-least-once redelivery, or an operator re-running the close — wrote a second invoice for the same period and billed the customer twice. And an immediate plan change recorded its proration adjustment after a guard that only blocked a queued (pending) change, so two concurrent immediate executions both transitioned the subscription and both recorded the proration. The period close now stamps a deterministic per-period idempotency key so a replay returns the existing invoice, and the immediate plan change now transitions the subscription with a conditional update so only the one call that actually moves the plan records the proration. No migration to apply. **Fixed:** *A retried metered-usage period close no longer bills the period twice* — Rolling a usage period into an invoice now carries a deterministic idempotency key derived from the subscription, period window, and currency. A period close that runs more than once — a cron retry, an at-least-once redelivery, or a manual re-run — now returns the invoice already recorded for that period instead of enqueuing a duplicate, so the customer is billed for the period exactly once. · *Concurrent immediate plan changes charge proration once* — Executing an immediate plan change now transitions the subscription with a conditional update keyed on the current plan, and records the proration adjustment only when that update actually moves the plan. The prior guard only refused a queued change, so two immediate executions racing the same change — a double-submit or a retry — could both apply the transition and both record the proration. Now only the one call that wins the transition charges it.
package/lib/analytics.js CHANGED
@@ -182,7 +182,10 @@ function _resolveEventWindow(opts) {
182
182
  // side (we'd rather reject a borderline string than ingest a real
183
183
  // address) and uses the same dotted-quad / hextet shapes the
184
184
  // vendored zod schemas exercise.
185
- var RAW_EMAIL_RE = /[^\s@]+@[^\s@]+\.[^\s@]+/;
185
+ // Each quantifier is bounded so a long run of non-@ characters can't drive
186
+ // backtracking: one char before the @, then a non-empty local part, then a
187
+ // lazy bridge to the first dot that is followed by another non-@ character.
188
+ var RAW_EMAIL_RE = /[^\s@]@[^\s@][^\s@]*?\.[^\s@]/;
186
189
  // IPv4 dotted-quad or IPv6 with at least one colon-delimited
187
190
  // hextet. The IPv6 shape catches the common forms ("::1",
188
191
  // "2001:db8::1", full eight-group) without trying to be RFC-precise
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.42",
2
+ "version": "0.4.43",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
@@ -236,7 +236,7 @@ function _secretKeyRaw(s) {
236
236
  }
237
237
  // Normalize trailing whitespace before length-checking so a paste
238
238
  // with a stray newline doesn't trip the length gate.
239
- var trimmed = s.replace(/^\s+|\s+$/g, "");
239
+ var trimmed = s.trim();
240
240
  if (!trimmed.length) {
241
241
  throw new TypeError("captchaGate: secret_key must contain non-whitespace");
242
242
  }
@@ -1535,7 +1535,7 @@ function create(opts) {
1535
1535
  // every sku field is stored as `"sku":"<value>"` so the LIKE
1536
1536
  // pattern is unambiguous (no false matches against a product_slug
1537
1537
  // that happens to share the substring).
1538
- var needle = '"sku":"' + sku.replace(/"/g, '\\"') + '"';
1538
+ var needle = '"sku":"' + sku.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
1539
1539
  var r = await query(
1540
1540
  "SELECT c.*, d.title AS draft_title, d.status AS draft_status, " +
1541
1541
  " d.published_at AS draft_published_at, d.rolled_back_at AS draft_rolled_back_at " +
@@ -128,7 +128,10 @@ var DEFAULT_WINDOW_MS = C.TIME.days(30);
128
128
  // guard). A hashed identifier is hex / base64url and never trips
129
129
  // these shapes, so the gate is a one-way "operator handed us raw
130
130
  // PII" detector.
131
- var RAW_EMAIL_RE = /[^\s@]+@[^\s@]+\.[^\s@]+/;
131
+ // Each quantifier is bounded so a long run of non-@ characters can't drive
132
+ // backtracking: one char before the @, then a non-empty local part, then a
133
+ // lazy bridge to the first dot that is followed by another non-@ character.
134
+ var RAW_EMAIL_RE = /[^\s@]@[^\s@][^\s@]*?\.[^\s@]/;
132
135
  var RAW_IPV4_RE = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
133
136
  var RAW_IPV6_RE = /(?:[0-9a-fA-F]{1,4}:){2,}[0-9a-fA-F]{0,4}/;
134
137
 
@@ -165,7 +165,11 @@ function _httpErr(status, label) {
165
165
  // ---- mode: service-binding -----------------------------------------------
166
166
 
167
167
  function _serviceBindingQuery(opts) {
168
- var url = opts.bridgeUrl.replace(/\/+$/, "") + (opts.bridgePath || DEFAULT_BRIDGE_PATH);
168
+ // Trailing-slash trim as a loop, not `/\/+$/` — the regex shape is
169
+ // superlinear on long runs of '/' and this value is library input.
170
+ var base = opts.bridgeUrl;
171
+ while (base.charCodeAt(base.length - 1) === 47) base = base.slice(0, -1);
172
+ var url = base + (opts.bridgePath || DEFAULT_BRIDGE_PATH);
169
173
  var secret = opts.bridgeSecret;
170
174
  var fetchImpl = opts.fetch || globalThis.fetch;
171
175
  var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
package/lib/r2-bridge.js CHANGED
@@ -97,7 +97,11 @@ function _httpErr(status, label) {
97
97
 
98
98
  function create(opts) {
99
99
  _validateOpts(opts);
100
- var url = opts.bridgeUrl.replace(/\/+$/, "") + (opts.bridgePath || DEFAULT_BRIDGE_PATH);
100
+ // Trailing-slash trim as a loop, not `/\/+$/` — the regex shape is
101
+ // superlinear on long runs of '/' and this value is library input.
102
+ var base = opts.bridgeUrl;
103
+ while (base.charCodeAt(base.length - 1) === 47) base = base.slice(0, -1);
104
+ var url = base + (opts.bridgePath || DEFAULT_BRIDGE_PATH);
101
105
  var secret = opts.bridgeSecret;
102
106
  var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
103
107
  var httpClient = opts.httpClient || b.httpClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.42",
3
+ "version": "0.4.43",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {