[object Object]

← back to CelebritySignatures

payments(TEST): Cody gate fixes — atomic USED_SIDS guard (closes concurrent double-credit race) + gate on status===complete (Stripe fulfillment best-practice)

f2fc2abe7679afe46a4d07ed76e8130e6fff5b05 · 2026-08-04 10:17:23 -0700 · Steve Abrams

Files touched

Diff

commit f2fc2abe7679afe46a4d07ed76e8130e6fff5b05
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 4 10:17:23 2026 -0700

    payments(TEST): Cody gate fixes — atomic USED_SIDS guard (closes concurrent double-credit race) + gate on status===complete (Stripe fulfillment best-practice)
---
 server.js | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/server.js b/server.js
index 6e59650..49401c8 100644
--- a/server.js
+++ b/server.js
@@ -58,6 +58,9 @@ function stripeTestKey() {
   return null;
 }
 const STRIPE_TEST_KEY = (() => { const k = stripeTestKey(); return k && k.startsWith('sk_test_') ? k : null; })();
+// Atomic in-process guard against concurrent double-credit for the same paid session
+// (claimed synchronously before any await; the download-ledger is the restart-surviving backstop).
+const USED_SIDS = new Set();
 
 // Merged signatures feed (used by /api/signatures, the crawlable /a/:qid pages,
 // and the sitemap). The server-side occupation gate keeps the Artists category
@@ -423,13 +426,20 @@ ${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
       let paid = false;
       try {
         const s = await (await fetch(`https://api.stripe.com/v1/checkout/sessions/${sid}`, { headers: { Authorization: `Bearer ${STRIPE_TEST_KEY}` } })).json();
-        paid = s.payment_status === 'paid' && String(s.metadata?.upload_id) === x.id;
+        // gate fulfillment on the settled session (status===complete), not just payment_status
+        paid = s.status === 'complete' && s.payment_status === 'paid' && String(s.metadata?.upload_id) === x.id;
       } catch {}
       if (!paid) return sendJSON(res, 402, { ok: false, error: 'payment not verified' });
-      // Idempotent credit: append to the ledger + increment downloads ONCE per session id
-      // (re-hitting the success URL re-downloads the file but never double-credits).
-      let firstUse = true;
-      try { firstUse = !(await readFile(join(DATA, 'download-ledger.jsonl'), 'utf8')).includes(`"sid":"${sid}"`); } catch {}
+      // Idempotent credit: claim the sid ATOMICALLY in-process (closes the concurrent
+      // double-credit race — has+add are synchronous, no await between them), then confirm
+      // against the persistent ledger so a restart can't re-credit an already-paid sid.
+      let firstUse = false;
+      if (!USED_SIDS.has(sid)) {
+        USED_SIDS.add(sid);
+        let inLedger = false;
+        try { inLedger = (await readFile(join(DATA, 'download-ledger.jsonl'), 'utf8')).includes(`"sid":"${sid}"`); } catch {}
+        firstUse = !inLedger;
+      }
       if (firstUse) {
         const dl = await currentUser(req); // downloader identity when signed in
         const commission = +(x.priceUsd * x.commissionPct / 100).toFixed(2);

← 0881033 payments(TEST): gate signature downloads behind Stripe check  ·  back to CelebritySignatures  ·  SECURITY (HOLE 5, blocks live money): paid download link no d8cfb50 →