← back to CelebritySignatures
payments(TEST): gate signature downloads behind Stripe checkout — /api/signature-checkout + paid-session verification on /api/signature-file (idempotent by sid, inert without test key, live key never read)
08810333dcb52e44f360ff51d7104eff2da4315a · 2026-08-04 10:14:46 -0700 · Steve Abrams
Files touched
Diff
commit 08810333dcb52e44f360ff51d7104eff2da4315a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 4 10:14:46 2026 -0700
payments(TEST): gate signature downloads behind Stripe checkout — /api/signature-checkout + paid-session verification on /api/signature-file (idempotent by sid, inert without test key, live key never read)
---
server.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 55 insertions(+), 8 deletions(-)
diff --git a/server.js b/server.js
index c6a9511..6e59650 100644
--- a/server.js
+++ b/server.js
@@ -380,19 +380,66 @@ ${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
return sendJSON(res, 200, ups.filter(x => x.status === 'approved')
.map(x => ({ id: x.id, celebrity_name: x.celebrity_name, downloads: x.downloads, priceUsd: x.priceUsd, at: x.at })));
}
+ // ===== PAID SIGNATURE DOWNLOAD (Stripe TEST mode) =====
+ // Create a Stripe TEST checkout for a single signature file. On payment,
+ // Stripe redirects to /api/signature-file/:id?sid=… which verifies the paid
+ // session before releasing the file. Inert (503) until a test key is set;
+ // the LIVE key is deliberately never read here — going live is a separate gate.
+ if (path === '/api/signature-checkout' && M === 'POST') {
+ if (!STRIPE_TEST_KEY) return sendJSON(res, 503, { ok: false, error: 'downloads not purchasable yet (awaiting Stripe test key)' });
+ const b = await readBody(req);
+ const ups = await load('celebrity-uploads.json', []);
+ const x = ups.find(e => e.id === String(b.id || ''));
+ if (!x || x.status !== 'approved') return sendJSON(res, 404, { ok: false, error: 'not available' });
+ const params = new URLSearchParams();
+ params.set('mode', 'payment');
+ params.set('success_url', `https://celebsignatures.com/api/signature-file/${x.id}?sid={CHECKOUT_SESSION_ID}`);
+ params.set('cancel_url', 'https://celebsignatures.com/');
+ params.set('line_items[0][quantity]', '1');
+ params.set('line_items[0][price_data][currency]', 'usd');
+ params.set('line_items[0][price_data][unit_amount]', String(Math.round(x.priceUsd * 100)));
+ params.set('line_items[0][price_data][product_data][name]', `${x.celebrity_name} — signature file`);
+ params.set('metadata[upload_id]', x.id);
+ try {
+ const sres = await fetch('https://api.stripe.com/v1/checkout/sessions', {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${STRIPE_TEST_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: params.toString(),
+ });
+ const sj = await sres.json();
+ if (!sres.ok) return sendJSON(res, 502, { ok: false, error: sj.error?.message || 'stripe error' });
+ return sendJSON(res, 200, { ok: true, url: sj.url });
+ } catch (e) { return sendJSON(res, 502, { ok: false, error: 'payment gateway unreachable' }); }
+ }
const fm = path.match(/^\/api\/signature-file\/(up_[a-f0-9]+)$/);
if (fm && M === 'GET') {
const ups = await load('celebrity-uploads.json', []);
const x = ups.find(e => e.id === fm[1]);
if (!x || x.status !== 'approved') return sendJSON(res, 404, { ok: false, error: 'not available' });
- const dl = await currentUser(req); // downloader identity when signed in
- const commission = +(x.priceUsd * x.commissionPct / 100).toFixed(2);
- x.downloads++;
- await store('celebrity-uploads.json', ups);
- await appendFile(join(DATA, 'download-ledger.jsonl'), JSON.stringify({
- ts: new Date().toISOString(), uploadId: x.id, ownerEmail: x.ownerEmail,
- downloader: dl ? dl.email : null, priceUsd: x.priceUsd, commissionUsd: commission,
- }) + '\n');
+ // PAYMENT GATE (TEST mode): require a PAID Stripe checkout session for this file.
+ const sid = url.searchParams.get('sid') || '';
+ if (!STRIPE_TEST_KEY) return sendJSON(res, 503, { ok: false, error: 'downloads require payment — not configured yet' });
+ if (!/^cs_test_[A-Za-z0-9]+$/.test(sid)) return sendJSON(res, 402, { ok: false, error: 'payment required — purchase this download first', purchase: '/api/signature-checkout' });
+ 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;
+ } 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 {}
+ if (firstUse) {
+ const dl = await currentUser(req); // downloader identity when signed in
+ const commission = +(x.priceUsd * x.commissionPct / 100).toFixed(2);
+ x.downloads++;
+ await store('celebrity-uploads.json', ups);
+ await appendFile(join(DATA, 'download-ledger.jsonl'), JSON.stringify({
+ ts: new Date().toISOString(), uploadId: x.id, ownerEmail: x.ownerEmail,
+ downloader: dl ? dl.email : null, priceUsd: x.priceUsd, commissionUsd: commission, sid,
+ }) + '\n');
+ }
const buf = await readFile(join(UPLOADS_DIR, x.file));
res.writeHead(200, {
'Content-Type': x.mime,
← faf97c9 GA4 game_start: guard category param — only log when #catOpt
·
back to CelebritySignatures
·
payments(TEST): Cody gate fixes — atomic USED_SIDS guard (cl f2fc2ab →