@blamejs/blamejs-shop 0.5.8 → 0.5.9

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.5.x
10
10
 
11
+ - v0.5.9 (2026-07-03) — **A federated sign-in no longer auto-merges into a pre-existing account whose email was never proven, closing an account pre-hijacking path.** Closes an account pre-hijacking weakness in federated (Google / Apple) sign-in. When a verified OIDC sign-in found a pre-existing account with the same email, it linked the federated identity into that account on the strength of the incoming email being verified alone — without requiring the pre-existing account to have ever proven control of that email. Because a passkey (or otherwise passwordless) account is created with whatever email address was typed at registration and no ownership check, an attacker could register an account under a victim's email before the victim's first federated sign-in, and the victim's later Google/Apple sign-in would then be folded into the attacker's account — pulling the victim's guest orders in and leaving the attacker's credential enrolled. The merge now requires the pre-existing account to already carry a verified federated identity (the only account-creation path that proves the email); an account whose email was never proven falls through to the existing duplicate-email refusal, so the customer signs in with their existing method and links the provider from account settings. A first federated sign-in still creates a fresh account, and a second provider still links to an account whose email is already proven, so legitimate multi-provider sign-in is unchanged. **Security:** *Federated sign-in refuses to merge into an unverified pre-existing account* — A verified Google/Apple sign-in that matched a pre-existing account by email hash previously linked the federated identity into that account whenever the incoming identity-provider email was verified — even if the pre-existing account had never proven control of the email. A passkey/passwordless account stores the email typed at registration with no verification, so an attacker could pre-seed an account under a victim's email and capture the victim's later federated sign-in (account pre-hijacking, CWE-287). The merge now proceeds only when the pre-existing account already holds a verified federated identity — the one account-creation path that proves the email. An unverified match is refused with the existing duplicate-email conflict, directing the customer to sign in with their existing method and link the provider from account settings. First-time federated sign-in (new account) and second-provider linking into an already-proven account are unchanged.
12
+
11
13
  - v0.5.8 (2026-07-03) — **Inventory cost-layer consumption and subscription plan-change execution are now correct under concurrent requests; vendored framework refreshed to 0.16.3.** Two ledger-atomicity fixes for concurrent requests, plus a vendored framework security refresh. Cost-layer (COGS) consumption debited each inventory layer with an unguarded read-modify-write against a snapshot, so two overlapping sales or write-offs of the same SKU could both draw from the same layer — driving a layer's remaining quantity negative, stranding a later layer, and understating cost of goods sold. Each layer debit is now an atomic guarded claim (the decrement only applies while the layer still holds the planned quantity); a losing claim reverts its own partial work and re-plans against a fresh snapshot, and the cost attribution rows are written only after every layer in the plan is claimed, so the combined consumption is exact regardless of interleaving. Separately, an immediate subscription plan change wrote its history row as executed before claiming the plan transition, so a double-submitted or retried change that lost the transition claim left a phantom executed row — carrying charge and credit amounts even though no plan moved, no invoice was recorded, and no credit was issued. The losing change now voids that row, so subscription history and any revenue rollup over executed changes reflect only changes that actually happened. The vendored framework is refreshed to 0.16.3, which redacts credential-shaped values in the rendered /metrics exposition (they were masked only in the internal cardinality key), closes a SQL-guard fragment-mode bypass for PostgreSQL dollar-quoted strings, and fails closed on MCP tool input whose JSON Schema omits an explicit object type — all inherited without shop code changes. **Changed:** *Vendored framework refreshed to 0.16.3* — Refreshes the framework the shop builds on from 0.16.2 to 0.16.3, a security and robustness patch across the observability, SQL-guard, MCP, YAML-parser, content-credentials, and resumable-upload primitives. Most consequential: a credential-shaped metric label value was redacted only in the internal cardinality key, not in the rendered exposition, so a raw token on a label leaked to every /metrics scrape — redaction now covers the rendered labels; and the SQL guard's raw-fragment escape hatch now rejects a PostgreSQL dollar-quoted string and a lone statement terminator, closing a bypass of the placeholder-binding floor. The minimum Node.js engine is unchanged at 24.18.0. **Fixed:** *Concurrent cost-layer consumption no longer double-debits a layer* — FIFO, LIFO, and weighted-average COGS consumption planned each layer debit from a snapshot and then applied an unconditional decrement, so two concurrent consumptions of the same SKU could both debit the same front layer — driving its remaining quantity negative and understating COGS while a later layer was left stranded. Each debit is now an atomic guarded claim that only applies while the layer still holds the planned quantity; a losing claim reverts the attempt's earlier claims and re-plans against a re-read of the active layers, within a bounded retry. Cost attribution rows are persisted only after every layer in the plan is claimed, so a mid-plan contention loss leaves no orphaned rows and the summed consumption and attributed cost are exact. · *A lost immediate plan-change claim voids its row instead of leaving a phantom executed change* — An immediate subscription plan change inserted its history row as executed and then claimed the plan transition with a conditional update. A double-submit or retry could have two changes both insert an executed row while only one wins the transition; the loser previously returned leaving a phantom executed change with a charge and credit amount even though no plan moved, no proration invoice was recorded, and no store credit was issued. The losing change now voids its row (cancelled, reason recorded), matching how the same path already handles a failed processor push, so history and any rollup over executed changes count only changes that actually settled.
12
14
 
13
15
  - v0.5.7 (2026-07-03) — **Storefront redirect targets route through the framework open-redirect guard, closing a TAB-injection open redirect on return_to / redirect_to.** Closes an open-redirect (CWE-601) in the storefront's post-action redirects. The cookie-consent submit, currency and locale switchers, and the wishlist and compare toggles validated their return_to / redirect_to target with a hand-rolled leading-slash regex that accepted a horizontal TAB (0x09) at the second byte. A user agent strips ASCII TAB, CR, and LF from a URL before resolving it, so a target like /<TAB>/evil.example collapsed to the protocol-relative //evil.example and navigated the shopper off-origin under the shop's trust — a phishing bounce. Every request-derived redirect target now routes through the framework's b.safeRedirect.resolve primitive (exposed once as a shared same-origin helper), which rejects every control character including TAB, protocol-relative //host and \\host forms, and full URLs — only genuine same-origin relative paths survive; anything else falls back to a safe default. The two guards that already rejected control characters are consolidated onto the same primitive so there is one redirect-safety path to reason about, and a codebase check refuses any future hand-rolled leading-slash redirect guard. No operator action and no configuration change. **Security:** *Open-redirect on storefront return_to / redirect_to closed* — The post-action redirect guards (cookie consent, currency switch, locale switch, wishlist toggle, compare toggle, announcement dismiss) accepted a leading-slash target whose second byte was a control character. A horizontal TAB there is stripped by the browser before URL parsing, turning /<TAB>/evil.example into the protocol-relative //evil.example — an off-origin redirect an attacker could use to launder a phishing link through the shop's origin. All these targets now resolve through b.safeRedirect.resolve, which rejects control characters (TAB, CR, LF, NUL), protocol-relative and backslash forms, and off-origin absolute URLs; an invalid target falls back to a same-origin default. A codebase-patterns check prevents a hand-rolled leading-slash redirect guard from reappearing.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.5.8",
2
+ "version": "0.5.9",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
package/lib/customers.js CHANGED
@@ -382,6 +382,23 @@ function create(opts) {
382
382
  return r.rows[0] || null;
383
383
  },
384
384
 
385
+ // True when the account has proven control of its email — i.e. it carries
386
+ // at least one OAuth identity the IdP marked email_verified. This is the
387
+ // ONLY account-creation path that proves the email: register /
388
+ // registerWithPasskey store whatever address was typed with no ownership
389
+ // proof, so a passkey-registered (or otherwise unverified) account returns
390
+ // false here. Used to gate the OIDC email-hash merge against account
391
+ // pre-hijacking (see signInWithOIDC step 2).
392
+ hasVerifiedOAuthEmail: async function (customerId) {
393
+ _uuid(customerId, "customer id");
394
+ var r = await query(
395
+ "SELECT 1 FROM customer_oauth_identities " +
396
+ "WHERE customer_id = ?1 AND email_verified = 1 LIMIT 1",
397
+ [customerId],
398
+ );
399
+ return r.rows.length > 0;
400
+ },
401
+
385
402
  // Resolve (or create) the customer for a verified OIDC sign-in.
386
403
  // Resolution order, per standard federated-identity discipline:
387
404
  // 1. an existing (provider, subject) link → that customer (the
@@ -419,11 +436,24 @@ function create(opts) {
419
436
  return { customer: existing, created: false, linked_via: "oauth-subject" };
420
437
  }
421
438
 
422
- // (2) Link to an existing customer — verified email only.
439
+ // (2) Link to an existing customer — verified email only, AND only when
440
+ // that pre-existing account has ITSELF proven control of the email (it
441
+ // already carries a verified OAuth identity). Checking that the INCOMING
442
+ // IdP email is verified is not enough: register / registerWithPasskey
443
+ // store whatever address was typed with no ownership proof, so an
444
+ // attacker can pre-seed a passkey account under the victim's email and
445
+ // then capture the victim's later Google/Apple sign-in when it auto-
446
+ // merged into that account. An unverified pre-existing account falls
447
+ // through to the duplicate-email conflict in step (3) instead — the
448
+ // victim signs in with their existing method and links the provider from
449
+ // account settings.
423
450
  var customer = null, linkedVia = null, created = false;
424
451
  if (emailVerified && canonicalEmail) {
425
- customer = await api.byEmailHash(_hashEmail(canonicalEmail));
426
- if (customer) linkedVia = "verified-email";
452
+ var candidate = await api.byEmailHash(_hashEmail(canonicalEmail));
453
+ if (candidate && await api.hasVerifiedOAuthEmail(candidate.id)) {
454
+ customer = candidate;
455
+ linkedVia = "verified-email";
456
+ }
427
457
  }
428
458
 
429
459
  // (3) New account. Reaching here with a duplicate email means the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
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": {