@blamejs/blamejs-shop 0.4.96 → 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,10 @@ 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
+
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.
14
+
11
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.
12
16
 
13
17
  - v0.4.95 (2026-06-25) — **An order state change no longer logs a phantom transition when it loses a concurrent race, and a transient failure writing the history row no longer strands a stock hold.** Advancing an order through its lifecycle reads the order's current status, resolves the next state, then writes it with an UPDATE guarded on the row still being in the state it was read in — so when two writers race the same order (the stale-order reaper cancelling while a payment webhook marks it paid), exactly one wins and the loser collapses to a no-op. Two issues in that path are fixed. First, the state machine's transition audit was emitted before the guarded write resolved, so a writer that LOST the race still recorded a 'success' transition into the audit trail for a state change that never landed; the audit is now written only for the writer that actually moved the row. Second, the denormalized history-row insert ran between the committed status write and the inventory settlement with no failure handling: a transient error writing that row aborted the whole call after the status had already advanced, so the stock hold was never converted to a shelf debit (or released) and a webhook retry — finding the order already advanced — skipped settlement entirely, stranding the hold. The history insert is now best-effort (the state-machine audit is the authoritative record), so a failure writing it never prevents the hold from settling. No configuration changes. **Changed:** *Vendored framework refreshed to 0.15.24* — Updates the vendored blamejs framework to 0.15.24, an upstream build supply-chain hardening release with no runtime API changes. No operator action required. **Fixed:** *A lost order-transition race no longer records a phantom 'success' in the audit trail* — The order state machine emitted its transition audit during the in-memory replay, before the guarded status write that decides the race. A writer that lost the race (its conditional update matched zero rows and it no-opped) had nonetheless already written a 'success' transition into the audit trail — a record an auditor could not tell apart from a real one. The destination state is now resolved side-effect-free, and the transition audit is emitted only after the guarded write wins, so the audit trail records exactly the transitions that actually landed. · *A transient failure writing an order's history row no longer strands its stock hold* — After an order's status write committed, the order_transitions history row was inserted with no failure handling, ahead of the inventory settlement. A transient error on that insert aborted the call with the order already advanced, so the held stock was never debited or released; a webhook retry then found the order past its starting state, no-opped at the concurrency guard, and skipped settlement — leaving the hold stranded with no automatic recovery. A history-row write failure no longer aborts settlement, so the hold always settles once the state change commits. Because that row is also where a PayPal capture id is recovered from, a write failure is not silently dropped: the row's full payload — including any capture metadata — is recorded to the durable error feed (and the audit chain) for reconciliation.
package/lib/admin.js CHANGED
@@ -724,12 +724,23 @@ function _safeNotice(e, auditAction) {
724
724
  return { status: 400, code: "bad-request", message: "Invalid input." };
725
725
  }
726
726
  var msg = (e && e.message) || "";
727
- if (/UNIQUE constraint failed/i.test(msg) || /FOREIGN KEY constraint failed/i.test(msg)) {
728
- return /FOREIGN KEY/i.test(msg)
727
+ // Prefer a typed constraint code over message text: the production D1
728
+ // service-binding redacts the SQLite constraint string to a generic
729
+ // "HTTP 500", so a message regex is blind in prod (it only matches under
730
+ // node:sqlite). A generic, table-agnostic mapper has no UNIQUE key to
731
+ // re-read, so it can't use the state-agnostic re-read pattern the typed
732
+ // call sites use — it relies on `e.code` set by the caller / DB driver,
733
+ // falling back to the (test-only-reliable) message scan.
734
+ var sqlState = (e && (e.code || e.sqliteCode)) || "";
735
+ var isUnique = sqlState === "SQLITE_CONSTRAINT_UNIQUE" || sqlState === "UNIQUE" || /UNIQUE constraint failed/i.test(msg);
736
+ var isForeign = sqlState === "SQLITE_CONSTRAINT_FOREIGNKEY" || sqlState === "FOREIGN_KEY" || /FOREIGN KEY constraint failed/i.test(msg);
737
+ var isCheckNN = sqlState === "SQLITE_CONSTRAINT_CHECK" || sqlState === "SQLITE_CONSTRAINT_NOTNULL" || /(?:CHECK|NOT NULL) constraint failed/i.test(msg);
738
+ if (isUnique || isForeign) {
739
+ return isForeign
729
740
  ? { status: 409, code: "conflict", message: "A referenced record does not exist." }
730
741
  : { status: 409, code: "conflict", message: "That value is already in use." };
731
742
  }
732
- if (/(?:CHECK|NOT NULL) constraint failed/i.test(msg)) {
743
+ if (isCheckNN) {
733
744
  return { status: 400, code: "bad-request", message: "A required value is missing or invalid." };
734
745
  }
735
746
  // Genuine unknown error — record it server-side via the framework audit
package/lib/affiliates.js CHANGED
@@ -474,7 +474,17 @@ function create(opts) {
474
474
  break;
475
475
  } catch (e) {
476
476
  lastErr = e;
477
- if (!e || !e.message || e.message.indexOf("UNIQUE") === -1) throw e;
477
+ // State-agnostic collision detection never error-message matching.
478
+ // The production D1 service-binding redacts the SQLite "UNIQUE
479
+ // constraint failed" text to a generic "HTTP 500", so a message regex
480
+ // is blind in prod (it only matches under node:sqlite). Re-read by the
481
+ // colliding UNIQUE key (`code`): if a row now holds the code we tried,
482
+ // the failure WAS the collision, so loop and regenerate; if no such
483
+ // row exists the insert failed for another reason, so re-throw.
484
+ var clash = await query(
485
+ "SELECT id FROM affiliates WHERE code = ?1", [row.code]
486
+ );
487
+ if (!clash.rows.length) throw e;
478
488
  }
479
489
  }
480
490
  if (lastErr) throw lastErr;
package/lib/api-keys.js CHANGED
@@ -352,7 +352,17 @@ function create(opts) {
352
352
  break;
353
353
  } catch (e) {
354
354
  lastErr = e;
355
- if (!e || !e.message || e.message.indexOf("UNIQUE") === -1) throw e;
355
+ // State-agnostic collision detection never error-message matching.
356
+ // The production D1 service-binding redacts the SQLite "UNIQUE
357
+ // constraint failed" text to a generic "HTTP 500", so a message regex
358
+ // is blind in prod (it only matches under node:sqlite). Re-read by the
359
+ // colliding UNIQUE key (`token_hash`): if a row now holds this hash the
360
+ // failure WAS the collision, so regenerate and retry; otherwise the
361
+ // insert failed for another reason, so re-throw.
362
+ var clash = await query(
363
+ "SELECT id FROM api_keys WHERE token_hash = ?1", [row.token_hash]
364
+ );
365
+ if (!clash.rows.length) throw e;
356
366
  // Regenerate on collision.
357
367
  plaintext = _generateToken();
358
368
  row.token_hash = _hashToken(plaintext);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.96",
2
+ "version": "0.4.98",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
@@ -1275,17 +1275,25 @@ function create(opts) {
1275
1275
  // a claim from a prior attempt whose rollback didn't complete —
1276
1276
  // reuse it (its counter reservation was already paid), after
1277
1277
  // returning the increment this call just took.
1278
- if (/UNIQUE constraint failed/i.test((e && e.message) || "")) {
1278
+ //
1279
+ // State-agnostic collision detection — never error-message matching.
1280
+ // The production D1 service-binding redacts the SQLite "UNIQUE
1281
+ // constraint failed" text to a generic "HTTP 500", so a message regex
1282
+ // is blind in prod (it only matches under node:sqlite). Re-read by the
1283
+ // colliding UNIQUE key (rule_slug, order_id): if a row now exists the
1284
+ // failure WAS the collision, so reuse it; otherwise the insert failed
1285
+ // for another reason, so re-throw (without returning the increment).
1286
+ var held = (await query(
1287
+ "SELECT id FROM auto_discount_applications WHERE rule_slug = ?1 AND order_id = ?2",
1288
+ [slug, claimRef],
1289
+ )).rows[0];
1290
+ if (held) {
1279
1291
  await query(
1280
1292
  "UPDATE auto_discount_rules SET redemptions_used = " +
1281
1293
  "CASE WHEN redemptions_used > 0 THEN redemptions_used - 1 ELSE 0 END, updated_at = ?1 WHERE slug = ?2",
1282
1294
  [_now(), slug],
1283
1295
  );
1284
- var held = (await query(
1285
- "SELECT id FROM auto_discount_applications WHERE rule_slug = ?1 AND order_id = ?2",
1286
- [slug, claimRef],
1287
- )).rows[0];
1288
- return { claimed: true, id: held ? held.id : null, reused: true };
1296
+ return { claimed: true, id: held.id, reused: true };
1289
1297
  }
1290
1298
  throw e;
1291
1299
  }
@@ -476,26 +476,29 @@ function create(opts) {
476
476
  [id, customerId, tier, periodLabel, pointsBudget, ts],
477
477
  );
478
478
  } catch (e) {
479
- if (e && e.message && e.message.indexOf("UNIQUE") !== -1) {
480
- // Lost the race to a concurrent caller return their row.
481
- var raced = await query(
482
- "SELECT id, customer_id, tier, period_label, points_awarded, occurred_at " +
483
- "FROM referral_leaderboard_bonuses " +
484
- "WHERE customer_id = ?1 AND period_label = ?2 AND tier = ?3 LIMIT 1",
485
- [customerId, periodLabel, tier],
486
- );
487
- if (raced.rows.length) {
488
- var rr = raced.rows[0];
489
- return {
490
- id: rr.id,
491
- customer_id: rr.customer_id,
492
- tier: rr.tier,
493
- period_label: rr.period_label,
494
- points_awarded: Number(rr.points_awarded || 0),
495
- occurred_at: Number(rr.occurred_at || 0),
496
- status: "already-awarded",
497
- };
498
- }
479
+ // State-agnostic collision detection: prod D1 redacts the SQLite
480
+ // "UNIQUE constraint failed" message to a generic "HTTP 500", so a
481
+ // message regex is blind there. Re-read by the UNIQUE key
482
+ // (customer_id, period_label, tier) if the row now exists we lost
483
+ // the race to a concurrent caller, so return their row; otherwise
484
+ // the insert failed for another reason, so re-throw.
485
+ var raced = await query(
486
+ "SELECT id, customer_id, tier, period_label, points_awarded, occurred_at " +
487
+ "FROM referral_leaderboard_bonuses " +
488
+ "WHERE customer_id = ?1 AND period_label = ?2 AND tier = ?3 LIMIT 1",
489
+ [customerId, periodLabel, tier],
490
+ );
491
+ if (raced.rows.length) {
492
+ var rr = raced.rows[0];
493
+ return {
494
+ id: rr.id,
495
+ customer_id: rr.customer_id,
496
+ tier: rr.tier,
497
+ period_label: rr.period_label,
498
+ points_awarded: Number(rr.points_awarded || 0),
499
+ occurred_at: Number(rr.occurred_at || 0),
500
+ status: "already-awarded",
501
+ };
499
502
  }
500
503
  throw e;
501
504
  }
package/lib/referrals.js CHANGED
@@ -260,8 +260,16 @@ function create(opts) {
260
260
  break;
261
261
  } catch (e) {
262
262
  lastErr = e;
263
- // UNIQUE-violation: regenerate. Any other error propagates.
264
- if (!e || !e.message || e.message.indexOf("UNIQUE") === -1) {
263
+ // State-agnostic collision detection never error-message
264
+ // matching. The production D1 service-binding redacts the
265
+ // SQLite "UNIQUE constraint failed" text to a generic "HTTP
266
+ // 500", so a message regex is blind in prod. Re-read the
267
+ // colliding `code`: if a row with that code now exists the
268
+ // failure WAS the UNIQUE race, so we regenerate and retry; if
269
+ // no such row exists the insert failed for another reason and
270
+ // we propagate. The cap (5 attempts) is unchanged.
271
+ var clash = await _codeRowByCode(code);
272
+ if (!clash) {
265
273
  throw e;
266
274
  }
267
275
  }
@@ -385,7 +385,16 @@ function create(opts) {
385
385
  break;
386
386
  } catch (e) {
387
387
  lastErr = e;
388
- if (!e || !e.message || e.message.indexOf("UNIQUE") === -1) throw e;
388
+ // State-agnostic collision detection never error-message
389
+ // matching. The production D1 service-binding redacts the SQLite
390
+ // "UNIQUE constraint failed" text to a generic "HTTP 500", so a
391
+ // message regex is blind in prod. Re-read by the colliding
392
+ // token_hash: if a row with that hash now exists the failure WAS
393
+ // the UNIQUE race, so we regenerate the token and retry; if no
394
+ // such row exists the insert failed for another reason and we
395
+ // propagate. The cap (5 attempts) is unchanged.
396
+ var clash = await _getRawByTokenHash(tokenHash);
397
+ if (!clash) throw e;
389
398
  }
390
399
  }
391
400
  if (lastErr) throw lastErr;
package/lib/tenants.js CHANGED
@@ -265,7 +265,17 @@ function create(opts) {
265
265
  [id, tenantId, domain, isPrimary ? 1 : 0, ts]
266
266
  );
267
267
  } catch (e) {
268
- if (e && e.message && e.message.indexOf("UNIQUE") !== -1) {
268
+ // State-agnostic collision detection: the production D1 service
269
+ // binding redacts the SQLite "UNIQUE constraint failed" text to a
270
+ // generic "HTTP 500", so a message regex is blind in prod. Re-read
271
+ // by the UNIQUE key (tenant_domains.domain is globally UNIQUE) — if
272
+ // the row now exists, the insert lost a duplicate-domain race; if
273
+ // not, the insert failed for another reason, so re-throw.
274
+ var clash = await query(
275
+ "SELECT 1 FROM tenant_domains WHERE domain = ?1 LIMIT 1",
276
+ [domain]
277
+ );
278
+ if (clash.rows.length) {
269
279
  var dupe = new Error("tenants: domain '" + domain + "' is already registered");
270
280
  dupe.code = "TENANT_DOMAIN_DUPLICATE";
271
281
  throw dupe;
@@ -337,7 +347,14 @@ function create(opts) {
337
347
  [id, slug, name, defaultCurrency, defaultLocale, themeSlug, status, ts]
338
348
  );
339
349
  } catch (e) {
340
- if (e && e.message && e.message.indexOf("UNIQUE") !== -1) {
350
+ // State-agnostic collision detection: prod D1 redacts the SQLite
351
+ // "UNIQUE constraint failed" message to a generic "HTTP 500", so a
352
+ // message regex is blind there. Re-read by the UNIQUE key
353
+ // (tenants.slug) — if the row now exists, a concurrent define won
354
+ // the slug; otherwise the insert failed for another reason, so
355
+ // re-throw.
356
+ var clash = await _tenantBySlug(slug);
357
+ if (clash) {
341
358
  var slugDupe = new Error("tenants.defineTenant: slug '" + slug + "' is already registered");
342
359
  slugDupe.code = "TENANT_SLUG_DUPLICATE";
343
360
  throw slugDupe;
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
 
@@ -615,7 +632,16 @@ function create(opts) {
615
632
  [vendorSlug, sku, ts],
616
633
  );
617
634
  } catch (e) {
618
- if (e && e.message && e.message.indexOf("UNIQUE") !== -1) {
635
+ // State-agnostic collision detection: prod D1 redacts the SQLite
636
+ // "UNIQUE constraint failed" message to a generic "HTTP 500", so a
637
+ // message regex is blind there. Re-read by the UNIQUE key
638
+ // (vendor_skus.sku) — if the row now exists, a concurrent assign
639
+ // claimed the SKU; otherwise the insert failed for another reason,
640
+ // so re-throw.
641
+ var clash = await query(
642
+ "SELECT vendor_slug FROM vendor_skus WHERE sku = ?1", [sku],
643
+ );
644
+ if (clash.rows.length) {
619
645
  var raced = new Error("vendors.assignSku: sku " + JSON.stringify(sku) + " is already assigned");
620
646
  raced.code = "VENDOR_SKU_TAKEN";
621
647
  throw raced;
@@ -704,12 +704,21 @@ function create(opts) {
704
704
  [eventId, slug, idempotencyKey, bodySha, bodyBytes.length, nowMs],
705
705
  );
706
706
  } catch (e) {
707
- // The UNIQUE(source_slug, idempotency_key) constraint races
708
- // when two deliveries with the same key arrive simultaneously
709
- // — the second insert collapses to a replay rather than a
710
- // hard error. The caller saw a successful verify; we treat
711
- // the late-arriving duplicate as a benign replay.
712
- if (e && e.message && e.message.indexOf("UNIQUE") !== -1 && idempotencyKey != null) {
707
+ // The UNIQUE(source_slug, idempotency_key) constraint races when two
708
+ // deliveries with the same key arrive simultaneously — the second
709
+ // insert collapses to a replay rather than a hard error.
710
+ //
711
+ // State-agnostic collision detection never error-message matching.
712
+ // The production D1 service-binding redacts the SQLite "UNIQUE
713
+ // constraint failed" text to a generic "HTTP 500", so a message regex
714
+ // is blind in prod (it only matches under node:sqlite, where the full
715
+ // text survives). On ANY insert error we re-read the (source_slug,
716
+ // idempotency_key) row: if it now exists the failure WAS the UNIQUE
717
+ // race, so we collapse to a benign replay; if it does not exist the
718
+ // insert failed for another reason and we re-throw the original error.
719
+ // The idempotencyKey != null guard stays — a row with a NULL key has
720
+ // no unique slot to replay onto.
721
+ if (idempotencyKey != null) {
713
722
  var late = await query(
714
723
  "SELECT id FROM webhook_received_events " +
715
724
  "WHERE source_slug = ?1 AND idempotency_key = ?2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.96",
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": {