Changeset 6cbd014 in Klonkt for src/routes/paid.js
- Timestamp:
- 07/21/2026 01:32:23 AM (7 weeks ago)
- Branches:
- main
- Children:
- d48ea02
- Parents:
- d43230f
- git-author:
- Robin <roboburr@…> (07/21/2026 01:30:40 AM)
- git-committer:
- Robin <roboburr@…> (07/21/2026 01:32:23 AM)
- File:
-
- 1 edited
-
src/routes/paid.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/paid.js
rd43230f r6cbd014 15 15 import PaidPatreon from '../services/PaidPatreonService.js'; 16 16 import Passkey from '../services/PasskeyService.js'; 17 import { renderPostBodyHtml } from './posts.js'; 17 18 18 19 const router = express.Router(); … … 95 96 }); 96 97 98 // Step 4 (unlock): hand out authentication options for a passkey assertion. 99 router.get('/challenge', async (req, res) => { 100 const r = ready(req, res); if (!r) return; 101 const slug = String(req.query.post || '').trim(); 102 const post = slug ? db.prepare('SELECT slug, paid, paid_min_cents FROM posts WHERE site_id = ? AND slug = ?').get(r.site.id, slug) : null; 103 if (!post || !post.paid) return res.status(404).json({ error: 'not_paid' }); 104 const cents = post.paid_min_cents || PaidPatreon.defaultMinCents(r.site.id); 105 const options = await Passkey.authenticationOptions(baseUrl(req)); 106 const blob = signBlob({ purpose: 'auth', siteId: r.site.id, cents, post: post.slug, challenge: options.challenge }, 300); 107 res.json({ options, blob }); 108 }); 109 110 // Verify the assertion, check the entitlement, and return the full post body in 111 // the SAME response. No unlock token becomes state (design decision). 112 router.post('/unlock', express.json({ limit: '64kb' }), async (req, res) => { 113 const r = ready(req, res); if (!r) return res.status(404).json({ error: 'unavailable' }); 114 const { response, blob } = req.body || {}; 115 const payload = verifyBlob(String(blob || '')); 116 if (!payload || payload.purpose !== 'auth' || payload.siteId !== r.site.id) return res.status(400).json({ error: 'bad_challenge' }); 117 const credId = response && response.id; 118 const ent = credId ? Passkey.getEntitlement(credId, r.site.id) : null; 119 if (!ent) return res.status(403).json({ error: 'no_entitlement' }); // unknown/expired passkey 120 if ((ent.min_cents || 0) < payload.cents) return res.status(403).json({ error: 'tier' }); 121 const vr = await Passkey.verifyAssertion(baseUrl(req), response, payload.challenge, ent); 122 if (!vr) return res.status(400).json({ error: 'verify_failed' }); 123 Passkey.bumpCounter(credId, vr.newCounter); 124 const post = db.prepare("SELECT * FROM posts WHERE site_id = ? AND slug = ? AND status = 'published'").get(r.site.id, String(payload.post || '')); 125 if (!post || !post.paid) return res.status(404).json({ error: 'gone' }); 126 res.json({ ok: true, title: post.title || '', html: renderPostBodyHtml(r.site, post, req) }); 127 }); 128 97 129 export default router;
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)