@blamejs/blamejs-shop 0.4.97 → 0.4.98

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.98 (2026-06-25) — **A concurrent vendor registration with the same slug now returns a typed duplicate error instead of leaking a raw database error.** Registering a vendor checks whether the slug is already taken before inserting, then inserts. Two registrations of the same slug racing each other can both pass that pre-check, so the second insert collides on the slug primary key. That insert had no failure handling, so the raw database error surfaced — and in production, where the data layer redacts it to a generic 'HTTP 500', the caller got an opaque error rather than the typed 'slug already registered' it gets from the pre-check. The insert is now wrapped: on any failure it re-reads by slug, and if a row exists it raises the same typed duplicate error the pre-check raises, otherwise it re-throws. No configuration changes. **Fixed:** *Racing vendor registrations surface a typed duplicate error, not a raw 500* — vendors.registerVendor pre-checks the slug and then inserts, but a concurrent registration of the same slug can slip past the pre-check and collide on the slug primary key. The insert is now wrapped to detect that state-agnostically — re-reading by slug after a failure and raising the same typed VENDOR_SLUG_TAKEN error the pre-check raises (rather than leaking the raw constraint error, which production redacts to a bare 500). Mirrors the assignSku duplicate handling.
12
+
11
13
  - v0.4.97 (2026-06-25) — **Constraint-collision handling no longer depends on the database error text, so duplicate detection works in production instead of only in tests.** Several write paths detected a UNIQUE (or related) constraint collision by matching the database error message — looking for the literal text 'UNIQUE constraint failed'. That text only survives in local/test runs: in production, the data layer redacts the underlying SQLite error to a generic 'HTTP 500' with no constraint text, so the match silently failed and the intended collision handling was skipped — the raw error surfaced instead of the idempotent replay, the typed duplicate error, the regenerate-and-retry, or the claim reuse. Every such site now detects the collision by re-reading state through the unique key after the insert fails: if the row is present the failure was the collision and the site runs its existing handling; if not, the original error is re-thrown. This makes idempotent webhook replays, duplicate-key errors, code/token retries, and discount-claim reuse behave correctly in production, not just under tests. No configuration changes. **Fixed:** *Constraint-collision detection is now state-based, not error-message-based* — Duplicate-delivery webhook replays (webhook receiver), duplicate-key errors (tenant domain + slug, vendor SKU), regenerate-and-retry on a code/token collision (referral codes, gift-redemption tokens, API keys, affiliate codes), and the auto-discount per-order claim reuse all decided 'was this a constraint collision?' by scanning the SQLite error message. Production redacts that message to a bare 'HTTP 500', so the check was blind there and the collision branch never ran. Each site now re-reads by its unique key after an insert error and runs its handling only when the row is actually present, re-throwing any other error — the same approach the gift-card and affiliate-commission ledgers already use. The admin error-to-response mapper, which has no single key to re-read, now prefers a typed constraint code over the message text where the driver provides one.
12
14
 
13
15
  - v0.4.96 (2026-06-25) — **Compose two newly-public framework primitives directly, removing an internal-path dependency and a drift-prone duplicate transition table.** Two internal refactors that adopt framework surface the application previously had to work around, with no change in behaviour. The text-guard module composed the framework's codepoint-threat catalog by reaching into an internal vendored module path, because the catalog was not on the framework's public surface; it now composes the public b.codepointClass (the same module, reference-identical), so a future framework refresh that moves the internal file can't break the import. The quotes module kept a hand-rolled loop that re-derived a quote's next status from a second copy of its transition table; it now resolves the destination through the state machine's own side-effect-free resolver, off the single transition definition, so the two can no longer drift apart. No configuration changes. **Changed:** *Text-guard composes the public codepoint-threat catalog* — The free-text guard built its bidi / control / null-byte / zero-width / mixed-script screening on the framework's codepoint catalog by requiring an internal vendored module path, since the catalog was not yet exported publicly. It now composes the public b.codepointClass — the identical module — so the screening no longer depends on an internal file location that an upstream refresh could relocate. · *Quote transitions resolve through the state machine instead of a duplicate table* — Validating a quote's lifecycle edge (respond / accept / reject / cancel / expire / convert) used the state machine to check the edge was legal, then a separate hand-rolled loop over a second copy of the transition table to read the destination status. The destination is now resolved by the state machine's own side-effect-free resolver, off the single transition definition the machine is built from, removing the duplicate table walk that could drift from it.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.97",
2
+ "version": "0.4.98",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
package/lib/vendors.js CHANGED
@@ -398,19 +398,36 @@ function create(opts) {
398
398
  }
399
399
 
400
400
  var ts = _now();
401
- await query(
402
- "INSERT INTO vendors " +
403
- "(slug, name, contact_email_hash, contact_email_normalised, " +
404
- " contact_phone, address_json, payout_method, payout_address, " +
405
- " commission_split_bps, status, paused_at, archived_at, " +
406
- " created_at, updated_at) " +
407
- "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, NULL, ?12, ?12)",
408
- [
409
- slug, name, emailHash, emailNorm, phone, addressJson,
410
- payoutMethod, payoutAddr, splitBps, status,
411
- status === "paused" ? ts : null, ts,
412
- ],
413
- );
401
+ try {
402
+ await query(
403
+ "INSERT INTO vendors " +
404
+ "(slug, name, contact_email_hash, contact_email_normalised, " +
405
+ " contact_phone, address_json, payout_method, payout_address, " +
406
+ " commission_split_bps, status, paused_at, archived_at, " +
407
+ " created_at, updated_at) " +
408
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, NULL, ?12, ?12)",
409
+ [
410
+ slug, name, emailHash, emailNorm, phone, addressJson,
411
+ payoutMethod, payoutAddr, splitBps, status,
412
+ status === "paused" ? ts : null, ts,
413
+ ],
414
+ );
415
+ } catch (e) {
416
+ // The pre-check above can miss a CONCURRENT registration, and the
417
+ // production D1 service binding redacts the SQLite slug-PRIMARY-KEY
418
+ // constraint message to a generic "HTTP 500", so a message regex would
419
+ // be blind there. State-agnostic detection: re-read by the UNIQUE key
420
+ // (vendors.slug) — if the row now exists a racing register won, so
421
+ // surface the SAME typed duplicate the pre-check throws; otherwise the
422
+ // insert failed for another reason, so re-throw.
423
+ var raced = await _getVendorRaw(slug);
424
+ if (raced) {
425
+ var racedDupe = new Error("vendors.registerVendor: slug " + JSON.stringify(slug) + " already registered");
426
+ racedDupe.code = "VENDOR_SLUG_TAKEN";
427
+ throw racedDupe;
428
+ }
429
+ throw e;
430
+ }
414
431
  return _hydrateVendor(await _getVendorRaw(slug));
415
432
  },
416
433
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.97",
3
+ "version": "0.4.98",
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": {