+1
server/.gitignore
+1
server/.gitignore
···
1
+
db.sqlite3*
+122
server/db.js
+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
+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
+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
+2
server/package.json
+20
server/schema.sql
+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;