sessions in a db

it.... works?

+1
server/.gitignore
··· 1 + db.sqlite3*
+122
server/db.js
··· 1 + import { readFileSync } from 'node:fs'; 2 + import Database from 'better-sqlite3'; 3 + 4 + const SUBS_PER_ACCOUNT_LIMIT = 5; 5 + const SCHEMA_FNAME = './schema.sql'; 6 + 7 + export class DB { 8 + #stmt_insert_account; 9 + #stmt_get_account; 10 + #stmt_delete_account; 11 + #stmt_insert_push_sub; 12 + #stmt_get_all_sub_dids; 13 + #stmt_get_push_subs; 14 + #stmt_update_push_sub; 15 + #stmt_delete_push_sub; 16 + #stmt_get_push_info; 17 + #transactionally; 18 + #db; 19 + 20 + constructor(filename, init = false, handleExit = true) { 21 + const db = new Database(filename); 22 + 23 + db.pragma('journal_mode = WAL'); 24 + db.pragma('foreign_keys = ON'); 25 + 26 + if (init) { 27 + const createSchema = readFileSync(SCHEMA_FNAME, 'utf8'); 28 + db.exec(createSchema); 29 + } 30 + 31 + if (handleExit) { // probably a better way to do this 🤷‍♀️ 32 + process.on('exit', () => db.close()); 33 + process.on('SIGHUP', () => process.exit(128 + 1)); 34 + process.on('SIGINT', () => process.exit(128 + 2)); 35 + process.on('SIGTERM', () => process.exit(128 + 15)); 36 + } 37 + 38 + this.#stmt_insert_account = db.prepare( 39 + `insert into accounts (did) 40 + values (?)`); 41 + 42 + this.#stmt_get_account = db.prepare( 43 + `select a.first_seen, 44 + count(*) as total_subs 45 + from accounts a 46 + left outer join push_subs p on (p.account_did = a.did) 47 + where a.did = ? 48 + group by a.did`); 49 + 50 + this.#stmt_delete_account = db.prepare( 51 + `delete from accounts 52 + where did = ?`); 53 + 54 + this.#stmt_insert_push_sub = db.prepare( 55 + `insert into push_subs (account_did, session, subscription) 56 + values (?, ?, ?)`); 57 + 58 + this.#stmt_get_all_sub_dids = db.prepare( 59 + `select distinct account_did 60 + from push_subs`); 61 + 62 + this.#stmt_get_push_subs = db.prepare( 63 + `select session, 64 + subscription, 65 + (julianday(CURRENT_TIMESTAMP) - julianday(last_push)) * 24 * 60 * 60 66 + as 'since_last_push' 67 + from push_subs 68 + where account_did = ?`); 69 + 70 + this.#stmt_update_push_sub = db.prepare( 71 + `update push_subs 72 + set last_push = CURRENT_TIMESTAMP, 73 + total_pushes = total_pushes + 1 74 + where session = ?`); 75 + 76 + this.#stmt_delete_push_sub = db.prepare( 77 + `delete from push_subs 78 + where session = ?`); 79 + 80 + this.#stmt_get_push_info = db.prepare( 81 + `select created, 82 + last_push, 83 + total_pushes 84 + from push_subs 85 + where account_did = ?`); 86 + 87 + this.#transactionally = t => db.transaction(t).immediate(); 88 + } 89 + 90 + addAccount(did) { 91 + this.#stmt_insert_account.run(did); 92 + } 93 + 94 + addPushSub(did, session, sub) { 95 + this.#transactionally(() => { 96 + const res = this.#stmt_get_account.get(did); 97 + if (!res) { 98 + throw new Error(`Could not find account for ${did}`); 99 + } 100 + if (res.total_subs >= SUBS_PER_ACCOUNT_LIMIT) { 101 + throw new Error(`Too many subscriptions for ${did}`); 102 + } 103 + this.#stmt_insert_push_sub.run(did, session, sub); 104 + }); 105 + } 106 + 107 + getSubscribedDids() { 108 + return this.#stmt_get_all_sub_dids.all().map(r => r.account_did); 109 + } 110 + 111 + getSubsByDid(did) { 112 + return this.#stmt_get_push_subs.all(did); 113 + } 114 + 115 + updateLastPush(session) { 116 + this.#stmt_update_push_sub.run(session); 117 + } 118 + 119 + deleteSub(session) { 120 + this.#stmt_delete_push_sub.run(session); 121 + } 122 + }
+84 -70
server/index.js
··· 7 7 const cookie = require('cookie'); 8 8 const cookieSig = require('cookie-signature'); 9 9 const webpush = require('web-push'); 10 + const { v4: uuidv4 } = require('uuid'); 11 + const { DB } = require('./db'); 10 12 11 13 // kind of silly but right now there's no way to tell spacedust that we want an alive connection 12 14 // but don't want the notification firehose (everything filtered out) ··· 23 25 24 26 let spacedust; 25 27 let spacedustEverStarted = false; 26 - const subs = new Map(); 27 28 28 - const addSub = (did, sub) => { 29 - if (!subs.has(did)) { 30 - subs.set(did, []); 31 - } 32 - sub.t = new Date(); 33 - subs.get(did).push(sub); 34 - updateSubs(); 35 - }; 36 29 37 - const updateSubs = () => { 30 + const updateSubs = db => { 38 31 if (!spacedust) { 39 32 console.warn('not updating subscription, no spacedust (reconnecting?)'); 40 33 return; 41 34 } 42 - const wantedSubjectDids = Array.from(subs.keys()); 35 + const wantedSubjectDids = db.getSubscribedDids(); 43 36 if (wantedSubjectDids.length === 0) { 44 37 wantedSubjectDids.push(DUMMY_DID); 45 38 } ··· 52 45 })); 53 46 }; 54 47 55 - const handleDust = async event => { 48 + async function push(db, pushSubscription, payload) { 49 + const { session, subscription, since_last_push } = pushSubscription; 50 + if (since_last_push !== null && since_last_push < 1.618) { 51 + console.warn(`rate limiter: dropping too-soon push (${since_last_push})`); 52 + return; 53 + } 54 + 55 + let sub; 56 + try { 57 + sub = JSON.parse(subscription); 58 + } catch (e) { 59 + console.error('failed to parse subscription json, dropping session', e); 60 + db.deleteSub(session); 61 + return; 62 + } 63 + 64 + try { 65 + await webpush.sendNotification(sub, payload); 66 + } catch (err) { 67 + if (400 <= err.statusCode && err.statusCode < 500) { 68 + console.info(`removing sub for ${err.statusCode}`); 69 + db.deleteSub(session); 70 + return; 71 + } else { 72 + console.warn('something went wrong for another reason', err); 73 + } 74 + } 75 + 76 + db.updateLastPush(session); 77 + } 78 + 79 + const handleDust = db => async event => { 56 80 console.log('got', event.data); 57 81 let data; 58 82 try { ··· 75 99 return; 76 100 } 77 101 78 - const expiredSubs = []; 79 - const now = new Date(); 102 + const subs = db.getSubsByDid(did); 80 103 const payload = JSON.stringify({ subject, source, source_record, timestamp }); 81 - console.log('pl', payload); 82 - for (const sub of subs.get(did) ?? []) { 83 - try { 84 - if (now - sub.t < 1500) { 85 - console.warn('skipping for rate limit'); 86 - continue; 87 - } 88 - sub.t = now; 89 - await webpush.sendNotification(sub, payload); 90 - } catch (err) { 91 - if (400 <= err.statusCode && err.statusCode < 500) { 92 - expiredSubs.push(sub); 93 - console.info(`removing sub for ${err.statusCode}`); 94 - } 95 - } 96 - } 97 - if (expiredSubs.length > 0) { 98 - const activeSubs = subs.get(did)?.filter(s => !expiredSubs.includes(s)); 99 - if (!activeSubs) { // concurrently removed already 100 - return; 101 - } 102 - if (activeSubs.length === 0) { 103 - console.info('removed last subscriber for', did); 104 - subs.delete(did); 105 - updateSubs(); 106 - } else { 107 - subs.set(did, activeSubs); 108 - } 109 - } 104 + await Promise.all(subs.map(pushSubscription => push(db, pushSubscription, payload))); 110 105 }; 111 106 112 - const connectSpacedust = host => { 107 + const connectSpacedust = (db, host) => { 113 108 spacedust = new WebSocket(`${host}/subscribe?instant=true&wantedSubjectDids=${DUMMY_DID}`); 114 109 let restarting = false; 115 110 ··· 118 113 restarting = true; 119 114 let wait = Math.round(500 + (Math.random() * 1000)); 120 115 console.info(`restarting spacedust connection in ${wait}ms...`); 121 - setTimeout(() => connectSpacedust(host), wait); 116 + setTimeout(() => connectSpacedust(db, host), wait); 122 117 spacedust = null; 123 118 } 124 119 125 - spacedust.onopen = updateSubs 126 - spacedust.onmessage = handleDust; 120 + spacedust.onopen = () => updateSubs(db); 121 + spacedust.onmessage = handleDust(db); 127 122 128 123 spacedust.onerror = e => { 129 124 console.error('spacedust errored:', e); ··· 159 154 }); 160 155 161 156 const COOKIE_BASE = { httpOnly: true, secure: true, partitioned: true, sameSite: 'None' }; 162 - const setDidCookie = (res, did, appSecret) => res.setHeader('Set-Cookie', cookie.serialize( 163 - 'verified-did', 164 - cookieSig.sign(did, appSecret), 157 + const setAccountCookie = (res, did, session, appSecret) => res.setHeader('Set-Cookie', cookie.serialize( 158 + 'verified-account', 159 + cookieSig.sign(JSON.stringify([did, session]), appSecret), 165 160 { ...COOKIE_BASE, maxAge: 90 * 86_400 }, 166 161 )); 167 - const clearDidCookie = res => res.setHeader('Set-Cookie', cookie.serialize( 168 - 'verified-did', 162 + const clearAccountCookie = res => res.setHeader('Set-Cookie', cookie.serialize( 163 + 'verified-account', 169 164 '', 170 165 { ...COOKIE_BASE, expires: new Date(0) }, 171 166 )); 172 - const getDidCookie = (req, res, appSecret) => { 167 + const getAccountCookie = (req, res, appSecret) => { 173 168 const cookies = cookie.parse(req.headers.cookie ?? ''); 174 - const untrusted = cookies['verified-did'] ?? ''; 175 - const did = cookieSig.unsign(untrusted, appSecret); 176 - if (!did) clearDidCookie(res); 177 - return did; 169 + const untrusted = cookies['verified-account'] ?? ''; 170 + const json = cookieSig.unsign(untrusted, appSecret); 171 + if (!json) { 172 + clearAccountCookie(res); 173 + return null; 174 + } 175 + try { 176 + const [did, session] = JSON.parse(json); 177 + return [did, session]; 178 + } catch (e) { 179 + console.warn('validated account cookie but failed to parse json', e); 180 + clearAccountCookie(res); 181 + return null; 182 + } 178 183 }; 179 184 180 185 const handleFile = (fname, ftype) => async (req, res, replace = {}) => { ··· 198 203 const handleIndex = handleFile('index.html', 'text/html'); 199 204 const handleServiceWorker = handleFile('service-worker.js', 'application/javascript'); 200 205 201 - const handleVerify = async (req, res, jwks, appSecret) => { 206 + const handleVerify = async (db, req, res, jwks, appSecret) => { 202 207 const body = await getRequesBody(req); 203 208 const { token } = JSON.parse(body); 204 209 let did; ··· 206 211 const verified = await jose.jwtVerify(token, jwks); 207 212 did = verified.payload.sub; 208 213 } catch (e) { 209 - return clearDidCookie(res).writeHead(400).end(JSON.stringify({ reason: 'verification failed' })); 214 + return clearAccountCookie(res).writeHead(400).end(JSON.stringify({ reason: 'verification failed' })); 210 215 } 211 - setDidCookie(res, did, appSecret); 216 + db.addAccount(did); 217 + const session = uuidv4(); 218 + setAccountCookie(res, did, session, appSecret); 212 219 return res.writeHead(200).end('okayyyy'); 213 220 }; 214 221 215 - const handleSubscribe = async (req, res, appSecret) => { 216 - let did = getDidCookie(req, res, appSecret); 217 - if (!did) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' })); 222 + const handleSubscribe = async (db, req, res, appSecret) => { 223 + let info = getAccountCookie(req, res, appSecret); 224 + if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' })); 225 + const [did, session] = info; 218 226 219 227 const body = await getRequesBody(req); 220 228 const { sub } = JSON.parse(body); 221 - addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG) 222 - addSub(did, sub); 229 + // addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG) 230 + db.addPushSub(did, session, JSON.stringify(sub)); 231 + updateSubs(db); 223 232 res.setHeader('Content-Type', 'application/json'); 224 233 res.writeHead(201); 225 234 res.end('{"oh": "hi"}'); 226 235 }; 227 236 228 - const requestListener = (pubkey, jwks, appSecret) => (req, res) => { 237 + const requestListener = (pubkey, jwks, appSecret, db) => (req, res) => { 229 238 if (req.method === 'GET' && req.url === '/') { 230 239 return handleIndex(req, res, { PUBKEY: pubkey }); 231 240 } ··· 239 248 } 240 249 if (req.method === 'POST' && req.url === '/verify') { 241 250 res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 242 - return handleVerify(req, res, jwks, appSecret); 251 + return handleVerify(db, req, res, jwks, appSecret); 243 252 } 244 253 245 254 if (req.method === 'OPTIONS' && req.url === '/subscribe') { ··· 248 257 } 249 258 if (req.method === 'POST' && req.url === '/subscribe') { 250 259 res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 251 - return handleSubscribe(req, res, appSecret); 260 + return handleSubscribe(db, req, res, appSecret); 252 261 } 253 262 254 263 res.writeHead(200); ··· 270 279 const whoamiHost = env.WHOAMI_HOST ?? 'https://who-am-i.microcosm.blue'; 271 280 const jwks = jose.createRemoteJWKSet(new URL(`${whoamiHost}/.well-known/jwks.json`)); 272 281 282 + const dbFilename = env.DB_FILE ?? './db.sqlite3'; 283 + const initDb = process.argv.includes('--init-db'); 284 + console.log(`connecting sqlite db file: ${dbFilename} (initializing: ${initDb})`); 285 + const db = new DB(dbFilename, initDb); 286 + 273 287 const spacedustHost = env.SPACEDUST_HOST ?? 'wss://spacedust.microcosm.blue'; 274 - connectSpacedust(spacedustHost); 288 + connectSpacedust(db, spacedustHost); 275 289 276 290 const host = env.HOST ?? 'localhost'; 277 291 const port = parseInt(env.PORT ?? 8000, 10); 278 292 279 293 http 280 - .createServer(requestListener(keys.publicKey, jwks, appSecret)) 294 + .createServer(requestListener(keys.publicKey, jwks, appSecret, db)) 281 295 .listen(port, host, () => console.log(`listening at http://${host}:${port}`)); 282 296 }; 283 297
+431
server/package-lock.json
··· 5 5 "packages": { 6 6 "": { 7 7 "dependencies": { 8 + "better-sqlite3": "^12.2.0", 8 9 "cookie": "^1.0.2", 9 10 "cookie-signature": "^1.2.2", 10 11 "jose": "^6.0.11", 12 + "uuid": "^11.1.0", 11 13 "web-push": "^3.6.7" 12 14 } 13 15 }, ··· 32 34 "safer-buffer": "^2.1.0" 33 35 } 34 36 }, 37 + "node_modules/base64-js": { 38 + "version": "1.5.1", 39 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 40 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 41 + "funding": [ 42 + { 43 + "type": "github", 44 + "url": "https://github.com/sponsors/feross" 45 + }, 46 + { 47 + "type": "patreon", 48 + "url": "https://www.patreon.com/feross" 49 + }, 50 + { 51 + "type": "consulting", 52 + "url": "https://feross.org/support" 53 + } 54 + ], 55 + "license": "MIT" 56 + }, 57 + "node_modules/better-sqlite3": { 58 + "version": "12.2.0", 59 + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz", 60 + "integrity": "sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==", 61 + "hasInstallScript": true, 62 + "license": "MIT", 63 + "dependencies": { 64 + "bindings": "^1.5.0", 65 + "prebuild-install": "^7.1.1" 66 + }, 67 + "engines": { 68 + "node": "20.x || 22.x || 23.x || 24.x" 69 + } 70 + }, 71 + "node_modules/bindings": { 72 + "version": "1.5.0", 73 + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 74 + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 75 + "license": "MIT", 76 + "dependencies": { 77 + "file-uri-to-path": "1.0.0" 78 + } 79 + }, 80 + "node_modules/bl": { 81 + "version": "4.1.0", 82 + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 83 + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 84 + "license": "MIT", 85 + "dependencies": { 86 + "buffer": "^5.5.0", 87 + "inherits": "^2.0.4", 88 + "readable-stream": "^3.4.0" 89 + } 90 + }, 35 91 "node_modules/bn.js": { 36 92 "version": "4.12.2", 37 93 "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", 38 94 "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", 39 95 "license": "MIT" 40 96 }, 97 + "node_modules/buffer": { 98 + "version": "5.7.1", 99 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 100 + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 101 + "funding": [ 102 + { 103 + "type": "github", 104 + "url": "https://github.com/sponsors/feross" 105 + }, 106 + { 107 + "type": "patreon", 108 + "url": "https://www.patreon.com/feross" 109 + }, 110 + { 111 + "type": "consulting", 112 + "url": "https://feross.org/support" 113 + } 114 + ], 115 + "license": "MIT", 116 + "dependencies": { 117 + "base64-js": "^1.3.1", 118 + "ieee754": "^1.1.13" 119 + } 120 + }, 41 121 "node_modules/buffer-equal-constant-time": { 42 122 "version": "1.0.1", 43 123 "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 44 124 "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", 45 125 "license": "BSD-3-Clause" 46 126 }, 127 + "node_modules/chownr": { 128 + "version": "1.1.4", 129 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 130 + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 131 + "license": "ISC" 132 + }, 47 133 "node_modules/cookie": { 48 134 "version": "1.0.2", 49 135 "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", ··· 79 165 } 80 166 } 81 167 }, 168 + "node_modules/decompress-response": { 169 + "version": "6.0.0", 170 + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 171 + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 172 + "license": "MIT", 173 + "dependencies": { 174 + "mimic-response": "^3.1.0" 175 + }, 176 + "engines": { 177 + "node": ">=10" 178 + }, 179 + "funding": { 180 + "url": "https://github.com/sponsors/sindresorhus" 181 + } 182 + }, 183 + "node_modules/deep-extend": { 184 + "version": "0.6.0", 185 + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 186 + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 187 + "license": "MIT", 188 + "engines": { 189 + "node": ">=4.0.0" 190 + } 191 + }, 192 + "node_modules/detect-libc": { 193 + "version": "2.0.4", 194 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", 195 + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", 196 + "license": "Apache-2.0", 197 + "engines": { 198 + "node": ">=8" 199 + } 200 + }, 82 201 "node_modules/ecdsa-sig-formatter": { 83 202 "version": "1.0.11", 84 203 "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", ··· 88 207 "safe-buffer": "^5.0.1" 89 208 } 90 209 }, 210 + "node_modules/end-of-stream": { 211 + "version": "1.4.5", 212 + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", 213 + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", 214 + "license": "MIT", 215 + "dependencies": { 216 + "once": "^1.4.0" 217 + } 218 + }, 219 + "node_modules/expand-template": { 220 + "version": "2.0.3", 221 + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 222 + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 223 + "license": "(MIT OR WTFPL)", 224 + "engines": { 225 + "node": ">=6" 226 + } 227 + }, 228 + "node_modules/file-uri-to-path": { 229 + "version": "1.0.0", 230 + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 231 + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", 232 + "license": "MIT" 233 + }, 234 + "node_modules/fs-constants": { 235 + "version": "1.0.0", 236 + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 237 + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 238 + "license": "MIT" 239 + }, 240 + "node_modules/github-from-package": { 241 + "version": "0.0.0", 242 + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 243 + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 244 + "license": "MIT" 245 + }, 91 246 "node_modules/http_ece": { 92 247 "version": "1.2.0", 93 248 "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", ··· 110 265 "node": ">= 14" 111 266 } 112 267 }, 268 + "node_modules/ieee754": { 269 + "version": "1.2.1", 270 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 271 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 272 + "funding": [ 273 + { 274 + "type": "github", 275 + "url": "https://github.com/sponsors/feross" 276 + }, 277 + { 278 + "type": "patreon", 279 + "url": "https://www.patreon.com/feross" 280 + }, 281 + { 282 + "type": "consulting", 283 + "url": "https://feross.org/support" 284 + } 285 + ], 286 + "license": "BSD-3-Clause" 287 + }, 113 288 "node_modules/inherits": { 114 289 "version": "2.0.4", 115 290 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 116 291 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 292 + "license": "ISC" 293 + }, 294 + "node_modules/ini": { 295 + "version": "1.3.8", 296 + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 297 + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 117 298 "license": "ISC" 118 299 }, 119 300 "node_modules/jose": { ··· 146 327 "safe-buffer": "^5.0.1" 147 328 } 148 329 }, 330 + "node_modules/mimic-response": { 331 + "version": "3.1.0", 332 + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 333 + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 334 + "license": "MIT", 335 + "engines": { 336 + "node": ">=10" 337 + }, 338 + "funding": { 339 + "url": "https://github.com/sponsors/sindresorhus" 340 + } 341 + }, 149 342 "node_modules/minimalistic-assert": { 150 343 "version": "1.0.1", 151 344 "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", ··· 161 354 "url": "https://github.com/sponsors/ljharb" 162 355 } 163 356 }, 357 + "node_modules/mkdirp-classic": { 358 + "version": "0.5.3", 359 + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 360 + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 361 + "license": "MIT" 362 + }, 164 363 "node_modules/ms": { 165 364 "version": "2.1.3", 166 365 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 167 366 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 168 367 "license": "MIT" 169 368 }, 369 + "node_modules/napi-build-utils": { 370 + "version": "2.0.0", 371 + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", 372 + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", 373 + "license": "MIT" 374 + }, 375 + "node_modules/node-abi": { 376 + "version": "3.75.0", 377 + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", 378 + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", 379 + "license": "MIT", 380 + "dependencies": { 381 + "semver": "^7.3.5" 382 + }, 383 + "engines": { 384 + "node": ">=10" 385 + } 386 + }, 387 + "node_modules/once": { 388 + "version": "1.4.0", 389 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 390 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 391 + "license": "ISC", 392 + "dependencies": { 393 + "wrappy": "1" 394 + } 395 + }, 396 + "node_modules/prebuild-install": { 397 + "version": "7.1.3", 398 + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", 399 + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", 400 + "license": "MIT", 401 + "dependencies": { 402 + "detect-libc": "^2.0.0", 403 + "expand-template": "^2.0.3", 404 + "github-from-package": "0.0.0", 405 + "minimist": "^1.2.3", 406 + "mkdirp-classic": "^0.5.3", 407 + "napi-build-utils": "^2.0.0", 408 + "node-abi": "^3.3.0", 409 + "pump": "^3.0.0", 410 + "rc": "^1.2.7", 411 + "simple-get": "^4.0.0", 412 + "tar-fs": "^2.0.0", 413 + "tunnel-agent": "^0.6.0" 414 + }, 415 + "bin": { 416 + "prebuild-install": "bin.js" 417 + }, 418 + "engines": { 419 + "node": ">=10" 420 + } 421 + }, 422 + "node_modules/pump": { 423 + "version": "3.0.3", 424 + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", 425 + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", 426 + "license": "MIT", 427 + "dependencies": { 428 + "end-of-stream": "^1.1.0", 429 + "once": "^1.3.1" 430 + } 431 + }, 432 + "node_modules/rc": { 433 + "version": "1.2.8", 434 + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 435 + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 436 + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", 437 + "dependencies": { 438 + "deep-extend": "^0.6.0", 439 + "ini": "~1.3.0", 440 + "minimist": "^1.2.0", 441 + "strip-json-comments": "~2.0.1" 442 + }, 443 + "bin": { 444 + "rc": "cli.js" 445 + } 446 + }, 447 + "node_modules/readable-stream": { 448 + "version": "3.6.2", 449 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 450 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 451 + "license": "MIT", 452 + "dependencies": { 453 + "inherits": "^2.0.3", 454 + "string_decoder": "^1.1.1", 455 + "util-deprecate": "^1.0.1" 456 + }, 457 + "engines": { 458 + "node": ">= 6" 459 + } 460 + }, 170 461 "node_modules/safe-buffer": { 171 462 "version": "5.2.1", 172 463 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", ··· 193 484 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 194 485 "license": "MIT" 195 486 }, 487 + "node_modules/semver": { 488 + "version": "7.7.2", 489 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", 490 + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", 491 + "license": "ISC", 492 + "bin": { 493 + "semver": "bin/semver.js" 494 + }, 495 + "engines": { 496 + "node": ">=10" 497 + } 498 + }, 499 + "node_modules/simple-concat": { 500 + "version": "1.0.1", 501 + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 502 + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 503 + "funding": [ 504 + { 505 + "type": "github", 506 + "url": "https://github.com/sponsors/feross" 507 + }, 508 + { 509 + "type": "patreon", 510 + "url": "https://www.patreon.com/feross" 511 + }, 512 + { 513 + "type": "consulting", 514 + "url": "https://feross.org/support" 515 + } 516 + ], 517 + "license": "MIT" 518 + }, 519 + "node_modules/simple-get": { 520 + "version": "4.0.1", 521 + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 522 + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 523 + "funding": [ 524 + { 525 + "type": "github", 526 + "url": "https://github.com/sponsors/feross" 527 + }, 528 + { 529 + "type": "patreon", 530 + "url": "https://www.patreon.com/feross" 531 + }, 532 + { 533 + "type": "consulting", 534 + "url": "https://feross.org/support" 535 + } 536 + ], 537 + "license": "MIT", 538 + "dependencies": { 539 + "decompress-response": "^6.0.0", 540 + "once": "^1.3.1", 541 + "simple-concat": "^1.0.0" 542 + } 543 + }, 544 + "node_modules/string_decoder": { 545 + "version": "1.3.0", 546 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 547 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 548 + "license": "MIT", 549 + "dependencies": { 550 + "safe-buffer": "~5.2.0" 551 + } 552 + }, 553 + "node_modules/strip-json-comments": { 554 + "version": "2.0.1", 555 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 556 + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", 557 + "license": "MIT", 558 + "engines": { 559 + "node": ">=0.10.0" 560 + } 561 + }, 562 + "node_modules/tar-fs": { 563 + "version": "2.1.3", 564 + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", 565 + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", 566 + "license": "MIT", 567 + "dependencies": { 568 + "chownr": "^1.1.1", 569 + "mkdirp-classic": "^0.5.2", 570 + "pump": "^3.0.0", 571 + "tar-stream": "^2.1.4" 572 + } 573 + }, 574 + "node_modules/tar-stream": { 575 + "version": "2.2.0", 576 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 577 + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 578 + "license": "MIT", 579 + "dependencies": { 580 + "bl": "^4.0.3", 581 + "end-of-stream": "^1.4.1", 582 + "fs-constants": "^1.0.0", 583 + "inherits": "^2.0.3", 584 + "readable-stream": "^3.1.1" 585 + }, 586 + "engines": { 587 + "node": ">=6" 588 + } 589 + }, 590 + "node_modules/tunnel-agent": { 591 + "version": "0.6.0", 592 + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 593 + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 594 + "license": "Apache-2.0", 595 + "dependencies": { 596 + "safe-buffer": "^5.0.1" 597 + }, 598 + "engines": { 599 + "node": "*" 600 + } 601 + }, 602 + "node_modules/util-deprecate": { 603 + "version": "1.0.2", 604 + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 605 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 606 + "license": "MIT" 607 + }, 608 + "node_modules/uuid": { 609 + "version": "11.1.0", 610 + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", 611 + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", 612 + "funding": [ 613 + "https://github.com/sponsors/broofa", 614 + "https://github.com/sponsors/ctavan" 615 + ], 616 + "license": "MIT", 617 + "bin": { 618 + "uuid": "dist/esm/bin/uuid" 619 + } 620 + }, 196 621 "node_modules/web-push": { 197 622 "version": "3.6.7", 198 623 "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", ··· 211 636 "engines": { 212 637 "node": ">= 16" 213 638 } 639 + }, 640 + "node_modules/wrappy": { 641 + "version": "1.0.2", 642 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 643 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 644 + "license": "ISC" 214 645 } 215 646 } 216 647 }
+2
server/package.json
··· 1 1 { 2 2 "dependencies": { 3 + "better-sqlite3": "^12.2.0", 3 4 "cookie": "^1.0.2", 4 5 "cookie-signature": "^1.2.2", 5 6 "jose": "^6.0.11", 7 + "uuid": "^11.1.0", 6 8 "web-push": "^3.6.7" 7 9 } 8 10 }
+20
server/schema.sql
··· 1 + create table accounts ( 2 + did text primary key, 3 + first_seen text not null default CURRENT_TIMESTAMP, 4 + 5 + check(did like 'did:%') 6 + ) strict; 7 + 8 + create table push_subs ( 9 + session text primary key, -- uuidv4, bound to signed browser cookie 10 + account_did text not null, 11 + subscription text not null, -- from browser, treat as opaque blob 12 + 13 + created text not null default CURRENT_TIMESTAMP, 14 + 15 + last_push text, 16 + total_pushes integer not null default 0, 17 + 18 + foreign key(account_did) references accounts(did) 19 + on delete cascade on update cascade 20 + ) strict;