Changeset 9e9e6f9 in Klonkt
- 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)
- Files:
-
- 6 added
- 5 edited
-
package-lock.json (modified) (8 diffs)
-
package.json (modified) (1 diff)
-
src/assets/vendor/simplewebauthn-browser.umd.min.js (added)
-
src/config/database.js (modified) (1 diff)
-
src/routes/paid.js (added)
-
src/server.js (modified) (2 diffs)
-
src/services/PaidPatreonService.js (modified) (2 diffs)
-
src/services/PasskeyService.js (added)
-
src/views/pages/paid-passkey.ejs (added)
-
src/views/pages/paid-result.ejs (added)
-
test/paid-patron.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
package-lock.json
r928d1c7 r9e9e6f9 1 1 { 2 2 "name": "klonkt", 3 "version": "1. 0.0",3 "version": "1.6.0-dev", 4 4 "lockfileVersion": 3, 5 5 "requires": true, … … 7 7 "": { 8 8 "name": "klonkt", 9 "version": "1. 0.0",9 "version": "1.6.0-dev", 10 10 "license": "AGPL-3.0-or-later", 11 11 "dependencies": { 12 12 "@resvg/resvg-js": "^2.6.2", 13 "@simplewebauthn/browser": "^13.3.0", 14 "@simplewebauthn/server": "^13.3.2", 13 15 "bcryptjs": "^2.4.3", 14 16 "better-sqlite3": "^11.3.0", … … 64 66 } 65 67 }, 68 "node_modules/@hexagon/base64": { 69 "version": "1.1.28", 70 "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", 71 "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", 72 "license": "MIT" 73 }, 74 "node_modules/@levischuck/tiny-cbor": { 75 "version": "0.2.11", 76 "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", 77 "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", 78 "license": "MIT" 79 }, 80 "node_modules/@peculiar/asn1-android": { 81 "version": "2.8.0", 82 "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.8.0.tgz", 83 "integrity": "sha512-skLbS+IOGv1lUgDqtChr8xvtvEr3HMse/JGBaL2r1J1o/n7a8wqOrovMtlRq/UXLhxvmLaONP67hwtshgzwfzA==", 84 "license": "MIT", 85 "dependencies": { 86 "@peculiar/asn1-schema": "^2.8.0", 87 "asn1js": "^3.0.10", 88 "tslib": "^2.8.1" 89 } 90 }, 91 "node_modules/@peculiar/asn1-cms": { 92 "version": "2.8.0", 93 "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", 94 "integrity": "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==", 95 "license": "MIT", 96 "dependencies": { 97 "@peculiar/asn1-schema": "^2.8.0", 98 "@peculiar/asn1-x509": "^2.8.0", 99 "@peculiar/asn1-x509-attr": "^2.8.0", 100 "asn1js": "^3.0.10", 101 "tslib": "^2.8.1" 102 } 103 }, 104 "node_modules/@peculiar/asn1-csr": { 105 "version": "2.8.0", 106 "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz", 107 "integrity": "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==", 108 "license": "MIT", 109 "dependencies": { 110 "@peculiar/asn1-schema": "^2.8.0", 111 "@peculiar/asn1-x509": "^2.8.0", 112 "asn1js": "^3.0.10", 113 "tslib": "^2.8.1" 114 } 115 }, 116 "node_modules/@peculiar/asn1-ecc": { 117 "version": "2.8.0", 118 "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz", 119 "integrity": "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==", 120 "license": "MIT", 121 "dependencies": { 122 "@peculiar/asn1-schema": "^2.8.0", 123 "@peculiar/asn1-x509": "^2.8.0", 124 "asn1js": "^3.0.10", 125 "tslib": "^2.8.1" 126 } 127 }, 128 "node_modules/@peculiar/asn1-pfx": { 129 "version": "2.8.0", 130 "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz", 131 "integrity": "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==", 132 "license": "MIT", 133 "dependencies": { 134 "@peculiar/asn1-cms": "^2.8.0", 135 "@peculiar/asn1-pkcs8": "^2.8.0", 136 "@peculiar/asn1-rsa": "^2.8.0", 137 "@peculiar/asn1-schema": "^2.8.0", 138 "asn1js": "^3.0.10", 139 "tslib": "^2.8.1" 140 } 141 }, 142 "node_modules/@peculiar/asn1-pkcs8": { 143 "version": "2.8.0", 144 "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz", 145 "integrity": "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==", 146 "license": "MIT", 147 "dependencies": { 148 "@peculiar/asn1-schema": "^2.8.0", 149 "@peculiar/asn1-x509": "^2.8.0", 150 "asn1js": "^3.0.10", 151 "tslib": "^2.8.1" 152 } 153 }, 154 "node_modules/@peculiar/asn1-pkcs9": { 155 "version": "2.8.0", 156 "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz", 157 "integrity": "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==", 158 "license": "MIT", 159 "dependencies": { 160 "@peculiar/asn1-cms": "^2.8.0", 161 "@peculiar/asn1-pfx": "^2.8.0", 162 "@peculiar/asn1-pkcs8": "^2.8.0", 163 "@peculiar/asn1-schema": "^2.8.0", 164 "@peculiar/asn1-x509": "^2.8.0", 165 "@peculiar/asn1-x509-attr": "^2.8.0", 166 "asn1js": "^3.0.10", 167 "tslib": "^2.8.1" 168 } 169 }, 170 "node_modules/@peculiar/asn1-rsa": { 171 "version": "2.8.0", 172 "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz", 173 "integrity": "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==", 174 "license": "MIT", 175 "dependencies": { 176 "@peculiar/asn1-schema": "^2.8.0", 177 "@peculiar/asn1-x509": "^2.8.0", 178 "asn1js": "^3.0.10", 179 "tslib": "^2.8.1" 180 } 181 }, 182 "node_modules/@peculiar/asn1-schema": { 183 "version": "2.8.0", 184 "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", 185 "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", 186 "license": "MIT", 187 "dependencies": { 188 "@peculiar/utils": "^2.0.2", 189 "asn1js": "^3.0.10", 190 "tslib": "^2.8.1" 191 } 192 }, 193 "node_modules/@peculiar/asn1-x509": { 194 "version": "2.8.0", 195 "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz", 196 "integrity": "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==", 197 "license": "MIT", 198 "dependencies": { 199 "@peculiar/asn1-schema": "^2.8.0", 200 "@peculiar/utils": "^2.0.2", 201 "asn1js": "^3.0.10", 202 "tslib": "^2.8.1" 203 } 204 }, 205 "node_modules/@peculiar/asn1-x509-attr": { 206 "version": "2.8.0", 207 "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz", 208 "integrity": "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==", 209 "license": "MIT", 210 "dependencies": { 211 "@peculiar/asn1-schema": "^2.8.0", 212 "@peculiar/asn1-x509": "^2.8.0", 213 "asn1js": "^3.0.10", 214 "tslib": "^2.8.1" 215 } 216 }, 217 "node_modules/@peculiar/utils": { 218 "version": "2.0.3", 219 "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", 220 "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", 221 "license": "MIT", 222 "dependencies": { 223 "tslib": "^2.8.1" 224 } 225 }, 226 "node_modules/@peculiar/x509": { 227 "version": "1.14.3", 228 "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", 229 "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", 230 "license": "MIT", 231 "dependencies": { 232 "@peculiar/asn1-cms": "^2.6.0", 233 "@peculiar/asn1-csr": "^2.6.0", 234 "@peculiar/asn1-ecc": "^2.6.0", 235 "@peculiar/asn1-pkcs9": "^2.6.0", 236 "@peculiar/asn1-rsa": "^2.6.0", 237 "@peculiar/asn1-schema": "^2.6.0", 238 "@peculiar/asn1-x509": "^2.6.0", 239 "pvtsutils": "^1.3.6", 240 "reflect-metadata": "^0.2.2", 241 "tslib": "^2.8.1", 242 "tsyringe": "^4.10.0" 243 }, 244 "engines": { 245 "node": ">=20.0.0" 246 } 247 }, 66 248 "node_modules/@resvg/resvg-js": { 67 249 "version": "2.6.2", … … 279 461 } 280 462 }, 463 "node_modules/@simplewebauthn/browser": { 464 "version": "13.3.0", 465 "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", 466 "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", 467 "license": "MIT" 468 }, 469 "node_modules/@simplewebauthn/server": { 470 "version": "13.3.2", 471 "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.2.tgz", 472 "integrity": "sha512-KEDhfcGP1PAKRVSDjA3npTQFqS2b/srm+ipoNBNHdkzrHAlaRQUTE+a5f4ywsx6thxAw1NU2rYcLEY1949RGbQ==", 473 "license": "MIT", 474 "dependencies": { 475 "@hexagon/base64": "^1.1.27", 476 "@levischuck/tiny-cbor": "^0.2.2", 477 "@peculiar/asn1-android": "^2.6.0", 478 "@peculiar/asn1-ecc": "^2.6.1", 479 "@peculiar/asn1-rsa": "^2.6.1", 480 "@peculiar/asn1-schema": "^2.6.0", 481 "@peculiar/asn1-x509": "^2.6.1", 482 "@peculiar/x509": "^1.14.3" 483 }, 484 "engines": { 485 "node": ">=20.0.0" 486 } 487 }, 281 488 "node_modules/@types/node": { 282 489 "version": "10.17.60", … … 344 551 "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 345 552 "license": "MIT" 553 }, 554 "node_modules/asn1js": { 555 "version": "3.0.10", 556 "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", 557 "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", 558 "license": "BSD-3-Clause", 559 "dependencies": { 560 "pvtsutils": "^1.3.6", 561 "pvutils": "^1.1.5", 562 "tslib": "^2.8.1" 563 }, 564 "engines": { 565 "node": ">=12.0.0" 566 } 346 567 }, 347 568 "node_modules/async": { … … 1776 1997 } 1777 1998 }, 1999 "node_modules/pvtsutils": { 2000 "version": "1.3.6", 2001 "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", 2002 "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", 2003 "license": "MIT", 2004 "dependencies": { 2005 "tslib": "^2.8.1" 2006 } 2007 }, 2008 "node_modules/pvutils": { 2009 "version": "1.1.5", 2010 "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", 2011 "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", 2012 "license": "MIT", 2013 "engines": { 2014 "node": ">=16.0.0" 2015 } 2016 }, 1778 2017 "node_modules/qs": { 1779 2018 "version": "6.15.1", … … 1852 2091 "node": ">= 6" 1853 2092 } 2093 }, 2094 "node_modules/reflect-metadata": { 2095 "version": "0.2.2", 2096 "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", 2097 "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", 2098 "license": "Apache-2.0" 1854 2099 }, 1855 2100 "node_modules/safe-buffer": { … … 2154 2399 } 2155 2400 }, 2401 "node_modules/tslib": { 2402 "version": "2.8.1", 2403 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2404 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2405 "license": "0BSD" 2406 }, 2407 "node_modules/tsyringe": { 2408 "version": "4.10.0", 2409 "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", 2410 "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", 2411 "license": "MIT", 2412 "dependencies": { 2413 "tslib": "^1.9.3" 2414 }, 2415 "engines": { 2416 "node": ">= 6.0.0" 2417 } 2418 }, 2419 "node_modules/tsyringe/node_modules/tslib": { 2420 "version": "1.14.1", 2421 "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 2422 "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 2423 "license": "0BSD" 2424 }, 2156 2425 "node_modules/tunnel-agent": { 2157 2426 "version": "0.6.0", -
package.json
r928d1c7 r9e9e6f9 16 16 "dependencies": { 17 17 "@resvg/resvg-js": "^2.6.2", 18 "@simplewebauthn/browser": "^13.3.0", 19 "@simplewebauthn/server": "^13.3.2", 18 20 "bcryptjs": "^2.4.3", 19 21 "better-sqlite3": "^11.3.0", -
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)