- Timestamp:
- 07/21/2026 01:16:03 AM (7 weeks ago)
- Branches:
- main
- Children:
- d43230f
- Parents:
- 928d1c7
- git-author:
- Robin <roboburr@…> (07/21/2026 01:15:39 AM)
- git-committer:
- Robin <roboburr@…> (07/21/2026 01:16:03 AM)
- Location:
- src
- Files:
-
- 5 added
- 3 edited
-
assets/vendor/simplewebauthn-browser.umd.min.js (added)
-
config/database.js (modified) (1 diff)
-
routes/paid.js (added)
-
server.js (modified) (2 diffs)
-
services/PaidPatreonService.js (modified) (2 diffs)
-
services/PasskeyService.js (added)
-
views/pages/paid-passkey.ejs (added)
-
views/pages/paid-result.ejs (added)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
r928d1c7 r9e9e6f9 323 323 default_min_cents INTEGER DEFAULT 0, 324 324 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 325 ); 326 -- One row per passkey. NO patron identity is stored (design decision): 327 -- {passkey, site, proven cents, expiry}. Not traceable to a person. 328 CREATE TABLE IF NOT EXISTS paid_entitlements ( 329 credential_id TEXT PRIMARY KEY, -- WebAuthn credential id (opaque, base64url) 330 site_id TEXT NOT NULL, 331 public_key TEXT NOT NULL, -- COSE public key, base64url 332 counter INTEGER DEFAULT 0, 333 transports TEXT, 334 min_cents INTEGER DEFAULT 0, -- the amount proven at link time 335 expires_at INTEGER NOT NULL, -- unix seconds; re-link after 336 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 325 337 ); 326 338 CREATE TABLE IF NOT EXISTS ap_outbox ( -
src/server.js
r928d1c7 r9e9e6f9 41 41 import feedRoutes from './routes/feed.js'; 42 42 import postsRoutes from './routes/posts.js'; 43 import paidRoutes from './routes/paid.js'; 43 44 import langRoutes from './routes/lang.js'; 44 45 import adminUpdatesRoutes from './routes/admin-updates.js'; … … 400 401 app.use('/', changelogRoutes); // /changelog publieke release-/wijzigingen-pagina 401 402 app.use('/', langRoutes); // /lang/:code — interface-taal kiezen (vóór de catch-all) 403 app.use('/paid', paidRoutes); // paid-posts patron/passkey flow (before the /:slug catch-all) 402 404 app.use('/', postsRoutes); 403 405 -
src/services/PaidPatreonService.js
r928d1c7 r9e9e6f9 124 124 } 125 125 126 // Pure: pick the membership for the owner's campaign out of a Patreon 127 // identity?include=memberships.campaign response (JSON:API). Returns 128 // { status, cents } or null. 129 export function pickCampaignMembership(identity, campaignId) { 130 const inc = (identity && identity.included) || []; 131 for (const it of inc) { 132 if (it.type !== 'member') continue; 133 const camp = it.relationships && it.relationships.campaign && it.relationships.campaign.data; 134 if (!camp || String(camp.id) !== String(campaignId)) continue; 135 const a = it.attributes || {}; 136 return { status: a.patron_status || null, cents: a.currently_entitled_amount_cents || 0 }; 137 } 138 return null; 139 } 140 141 // Exchange a patron's auth code and read their membership of the owner's 142 // campaign. Returns { status, cents } or null. The patron token is used once 143 // and discarded here: nothing identifying is stored (design decision). 144 export async function verifyPatron(siteId, code, redirectUri, fetchImpl = fetch) { 145 const c = getOwnerConfig(siteId); 146 if (!c || !c.clientId || !c.clientSecret || !c.campaignId) return null; 147 const tokenRes = await fetchImpl(TOKEN_URL, { 148 method: 'POST', 149 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 150 body: new URLSearchParams({ 151 grant_type: 'authorization_code', code, 152 client_id: c.clientId, client_secret: c.clientSecret, redirect_uri: redirectUri, 153 }).toString(), 154 }); 155 if (!tokenRes.ok) return null; 156 const tok = await tokenRes.json(); 157 if (!tok || !tok.access_token) return null; 158 const url = 'https://www.patreon.com/api/oauth2/v2/identity' 159 + '?include=memberships.campaign' 160 + '&fields%5Bmember%5D=patron_status,currently_entitled_amount_cents'; 161 const idRes = await fetchImpl(url, { headers: { Authorization: `Bearer ${tok.access_token}` } }); 162 if (!idRes.ok) return null; 163 const identity = await idRes.json(); 164 return pickCampaignMembership(identity, c.campaignId); // token goes out of scope, discarded 165 } 166 126 167 function safeDecrypt(blob) { 127 168 try { return decrypt(blob); } catch { return null; } … … 131 172 getOwnerConfig, ownerStatus, saveOwnerConfig, disconnect, 132 173 defaultMinCents, needsRefresh, refreshCreatorToken, creatorAccessToken, 174 pickCampaignMembership, verifyPatron, 133 175 };
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)