@xtr-dev/rondevu-server 0.5.12 → 0.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -21
- package/dist/index.js +939 -1110
- package/dist/index.js.map +4 -4
- package/migrations/0009_public_key_auth.sql +74 -0
- package/migrations/fresh_schema.sql +20 -21
- package/package.json +2 -1
- package/src/config.ts +1 -47
- package/src/crypto.ts +70 -304
- package/src/index.ts +2 -3
- package/src/rpc.ts +90 -272
- package/src/storage/d1.ts +72 -235
- package/src/storage/factory.ts +4 -17
- package/src/storage/memory.ts +46 -151
- package/src/storage/mysql.ts +66 -187
- package/src/storage/postgres.ts +66 -186
- package/src/storage/sqlite.ts +65 -194
- package/src/storage/types.ts +30 -88
- package/src/worker.ts +4 -9
- package/wrangler.toml +1 -1
package/src/storage/sqlite.ts
CHANGED
|
@@ -4,62 +4,67 @@ import {
|
|
|
4
4
|
Offer,
|
|
5
5
|
IceCandidate,
|
|
6
6
|
CreateOfferRequest,
|
|
7
|
-
Credential,
|
|
8
|
-
GenerateCredentialsRequest,
|
|
9
7
|
} from './types.ts';
|
|
10
8
|
import { generateOfferHash } from './hash-id.ts';
|
|
11
9
|
|
|
12
|
-
const YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000; // 365 days
|
|
13
|
-
|
|
14
10
|
/**
|
|
15
11
|
* SQLite storage adapter for rondevu signaling system
|
|
16
12
|
* Supports both file-based and in-memory databases
|
|
17
13
|
*/
|
|
18
14
|
export class SQLiteStorage implements Storage {
|
|
19
15
|
private db: Database.Database;
|
|
20
|
-
private masterEncryptionKey: string;
|
|
21
16
|
|
|
22
17
|
/**
|
|
23
18
|
* Creates a new SQLite storage instance
|
|
24
19
|
* @param path Path to SQLite database file, or ':memory:' for in-memory database
|
|
25
|
-
* @param masterEncryptionKey 64-char hex string for encrypting secrets (32 bytes)
|
|
26
20
|
*/
|
|
27
|
-
constructor(path: string = ':memory:'
|
|
21
|
+
constructor(path: string = ':memory:') {
|
|
28
22
|
this.db = new Database(path);
|
|
29
|
-
this.masterEncryptionKey = masterEncryptionKey;
|
|
30
23
|
this.initializeDatabase();
|
|
31
24
|
}
|
|
32
25
|
|
|
33
26
|
/**
|
|
34
|
-
* Initializes database schema with
|
|
27
|
+
* Initializes database schema with Ed25519 public key identity
|
|
35
28
|
*/
|
|
36
29
|
private initializeDatabase(): void {
|
|
37
30
|
this.db.exec(`
|
|
31
|
+
-- Identities table (Ed25519 public key as identity)
|
|
32
|
+
CREATE TABLE IF NOT EXISTS identities (
|
|
33
|
+
public_key TEXT PRIMARY KEY,
|
|
34
|
+
created_at INTEGER NOT NULL,
|
|
35
|
+
expires_at INTEGER NOT NULL,
|
|
36
|
+
last_used INTEGER NOT NULL,
|
|
37
|
+
CHECK(length(public_key) = 64)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_identities_expires ON identities(expires_at);
|
|
41
|
+
|
|
38
42
|
-- WebRTC signaling offers with tags
|
|
39
43
|
CREATE TABLE IF NOT EXISTS offers (
|
|
40
44
|
id TEXT PRIMARY KEY,
|
|
41
|
-
|
|
45
|
+
public_key TEXT NOT NULL,
|
|
42
46
|
tags TEXT NOT NULL,
|
|
43
47
|
sdp TEXT NOT NULL,
|
|
44
48
|
created_at INTEGER NOT NULL,
|
|
45
49
|
expires_at INTEGER NOT NULL,
|
|
46
50
|
last_seen INTEGER NOT NULL,
|
|
47
|
-
|
|
51
|
+
answerer_public_key TEXT,
|
|
48
52
|
answer_sdp TEXT,
|
|
49
53
|
answered_at INTEGER,
|
|
50
|
-
matched_tags TEXT
|
|
54
|
+
matched_tags TEXT,
|
|
55
|
+
FOREIGN KEY (public_key) REFERENCES identities(public_key) ON DELETE CASCADE
|
|
51
56
|
);
|
|
52
57
|
|
|
53
|
-
CREATE INDEX IF NOT EXISTS
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_offers_public_key ON offers(public_key);
|
|
54
59
|
CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
|
|
55
60
|
CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
|
|
56
|
-
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_public_key);
|
|
57
62
|
|
|
58
63
|
-- ICE candidates table
|
|
59
64
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
60
65
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
61
66
|
offer_id TEXT NOT NULL,
|
|
62
|
-
|
|
67
|
+
public_key TEXT NOT NULL,
|
|
63
68
|
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
|
|
64
69
|
candidate TEXT NOT NULL,
|
|
65
70
|
created_at INTEGER NOT NULL,
|
|
@@ -67,22 +72,9 @@ export class SQLiteStorage implements Storage {
|
|
|
67
72
|
);
|
|
68
73
|
|
|
69
74
|
CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
|
|
70
|
-
CREATE INDEX IF NOT EXISTS
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_ice_public_key ON ice_candidates(public_key);
|
|
71
76
|
CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
|
|
72
77
|
|
|
73
|
-
-- Credentials table (replaces usernames with simpler name + secret auth)
|
|
74
|
-
CREATE TABLE IF NOT EXISTS credentials (
|
|
75
|
-
name TEXT PRIMARY KEY,
|
|
76
|
-
secret TEXT NOT NULL UNIQUE,
|
|
77
|
-
created_at INTEGER NOT NULL,
|
|
78
|
-
expires_at INTEGER NOT NULL,
|
|
79
|
-
last_used INTEGER NOT NULL,
|
|
80
|
-
CHECK(length(name) >= 3 AND length(name) <= 32)
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
CREATE INDEX IF NOT EXISTS idx_credentials_expires ON credentials(expires_at);
|
|
84
|
-
CREATE INDEX IF NOT EXISTS idx_credentials_secret ON credentials(secret);
|
|
85
|
-
|
|
86
78
|
-- Rate limits table (for distributed rate limiting)
|
|
87
79
|
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
88
80
|
identifier TEXT PRIMARY KEY,
|
|
@@ -121,7 +113,7 @@ export class SQLiteStorage implements Storage {
|
|
|
121
113
|
// Use transaction for atomic creation
|
|
122
114
|
const transaction = this.db.transaction((offersWithIds: (CreateOfferRequest & { id: string })[]) => {
|
|
123
115
|
const offerStmt = this.db.prepare(`
|
|
124
|
-
INSERT INTO offers (id,
|
|
116
|
+
INSERT INTO offers (id, public_key, tags, sdp, created_at, expires_at, last_seen)
|
|
125
117
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
126
118
|
`);
|
|
127
119
|
|
|
@@ -131,7 +123,7 @@ export class SQLiteStorage implements Storage {
|
|
|
131
123
|
// Insert offer with JSON-serialized tags
|
|
132
124
|
offerStmt.run(
|
|
133
125
|
offer.id,
|
|
134
|
-
offer.
|
|
126
|
+
offer.publicKey,
|
|
135
127
|
JSON.stringify(offer.tags),
|
|
136
128
|
offer.sdp,
|
|
137
129
|
now,
|
|
@@ -141,7 +133,7 @@ export class SQLiteStorage implements Storage {
|
|
|
141
133
|
|
|
142
134
|
created.push({
|
|
143
135
|
id: offer.id,
|
|
144
|
-
|
|
136
|
+
publicKey: offer.publicKey,
|
|
145
137
|
tags: offer.tags,
|
|
146
138
|
sdp: offer.sdp,
|
|
147
139
|
createdAt: now,
|
|
@@ -155,14 +147,14 @@ export class SQLiteStorage implements Storage {
|
|
|
155
147
|
return created;
|
|
156
148
|
}
|
|
157
149
|
|
|
158
|
-
async
|
|
150
|
+
async getOffersByPublicKey(publicKey: string): Promise<Offer[]> {
|
|
159
151
|
const stmt = this.db.prepare(`
|
|
160
152
|
SELECT * FROM offers
|
|
161
|
-
WHERE
|
|
153
|
+
WHERE public_key = ? AND expires_at > ?
|
|
162
154
|
ORDER BY last_seen DESC
|
|
163
155
|
`);
|
|
164
156
|
|
|
165
|
-
const rows = stmt.all(
|
|
157
|
+
const rows = stmt.all(publicKey, Date.now()) as any[];
|
|
166
158
|
return rows.map(row => this.rowToOffer(row));
|
|
167
159
|
}
|
|
168
160
|
|
|
@@ -181,13 +173,13 @@ export class SQLiteStorage implements Storage {
|
|
|
181
173
|
return this.rowToOffer(row);
|
|
182
174
|
}
|
|
183
175
|
|
|
184
|
-
async deleteOffer(offerId: string,
|
|
176
|
+
async deleteOffer(offerId: string, ownerPublicKey: string): Promise<boolean> {
|
|
185
177
|
const stmt = this.db.prepare(`
|
|
186
178
|
DELETE FROM offers
|
|
187
|
-
WHERE id = ? AND
|
|
179
|
+
WHERE id = ? AND public_key = ?
|
|
188
180
|
`);
|
|
189
181
|
|
|
190
|
-
const result = stmt.run(offerId,
|
|
182
|
+
const result = stmt.run(offerId, ownerPublicKey);
|
|
191
183
|
return result.changes > 0;
|
|
192
184
|
}
|
|
193
185
|
|
|
@@ -199,7 +191,7 @@ export class SQLiteStorage implements Storage {
|
|
|
199
191
|
|
|
200
192
|
async answerOffer(
|
|
201
193
|
offerId: string,
|
|
202
|
-
|
|
194
|
+
answererPublicKey: string,
|
|
203
195
|
answerSdp: string,
|
|
204
196
|
matchedTags?: string[]
|
|
205
197
|
): Promise<{ success: boolean; error?: string }> {
|
|
@@ -214,7 +206,7 @@ export class SQLiteStorage implements Storage {
|
|
|
214
206
|
}
|
|
215
207
|
|
|
216
208
|
// Check if offer already has an answerer
|
|
217
|
-
if (offer.
|
|
209
|
+
if (offer.answererPublicKey) {
|
|
218
210
|
return {
|
|
219
211
|
success: false,
|
|
220
212
|
error: 'Offer already answered'
|
|
@@ -224,12 +216,12 @@ export class SQLiteStorage implements Storage {
|
|
|
224
216
|
// Update offer with answer
|
|
225
217
|
const stmt = this.db.prepare(`
|
|
226
218
|
UPDATE offers
|
|
227
|
-
SET
|
|
228
|
-
WHERE id = ? AND
|
|
219
|
+
SET answerer_public_key = ?, answer_sdp = ?, answered_at = ?, matched_tags = ?
|
|
220
|
+
WHERE id = ? AND answerer_public_key IS NULL
|
|
229
221
|
`);
|
|
230
222
|
|
|
231
223
|
const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
|
|
232
|
-
const result = stmt.run(
|
|
224
|
+
const result = stmt.run(answererPublicKey, answerSdp, Date.now(), matchedTagsJson, offerId);
|
|
233
225
|
|
|
234
226
|
if (result.changes === 0) {
|
|
235
227
|
return {
|
|
@@ -241,25 +233,25 @@ export class SQLiteStorage implements Storage {
|
|
|
241
233
|
return { success: true };
|
|
242
234
|
}
|
|
243
235
|
|
|
244
|
-
async getAnsweredOffers(
|
|
236
|
+
async getAnsweredOffers(offererPublicKey: string): Promise<Offer[]> {
|
|
245
237
|
const stmt = this.db.prepare(`
|
|
246
238
|
SELECT * FROM offers
|
|
247
|
-
WHERE
|
|
239
|
+
WHERE public_key = ? AND answerer_public_key IS NOT NULL AND expires_at > ?
|
|
248
240
|
ORDER BY answered_at DESC
|
|
249
241
|
`);
|
|
250
242
|
|
|
251
|
-
const rows = stmt.all(
|
|
243
|
+
const rows = stmt.all(offererPublicKey, Date.now()) as any[];
|
|
252
244
|
return rows.map(row => this.rowToOffer(row));
|
|
253
245
|
}
|
|
254
246
|
|
|
255
|
-
async getOffersAnsweredBy(
|
|
247
|
+
async getOffersAnsweredBy(answererPublicKey: string): Promise<Offer[]> {
|
|
256
248
|
const stmt = this.db.prepare(`
|
|
257
249
|
SELECT * FROM offers
|
|
258
|
-
WHERE
|
|
250
|
+
WHERE answerer_public_key = ? AND expires_at > ?
|
|
259
251
|
ORDER BY answered_at DESC
|
|
260
252
|
`);
|
|
261
253
|
|
|
262
|
-
const rows = stmt.all(
|
|
254
|
+
const rows = stmt.all(answererPublicKey, Date.now()) as any[];
|
|
263
255
|
return rows.map(row => this.rowToOffer(row));
|
|
264
256
|
}
|
|
265
257
|
|
|
@@ -267,7 +259,7 @@ export class SQLiteStorage implements Storage {
|
|
|
267
259
|
|
|
268
260
|
async discoverOffers(
|
|
269
261
|
tags: string[],
|
|
270
|
-
|
|
262
|
+
excludePublicKey: string | null,
|
|
271
263
|
limit: number,
|
|
272
264
|
offset: number
|
|
273
265
|
): Promise<Offer[]> {
|
|
@@ -283,14 +275,14 @@ export class SQLiteStorage implements Storage {
|
|
|
283
275
|
SELECT DISTINCT o.* FROM offers o, json_each(o.tags) as t
|
|
284
276
|
WHERE t.value IN (${placeholders})
|
|
285
277
|
AND o.expires_at > ?
|
|
286
|
-
AND o.
|
|
278
|
+
AND o.answerer_public_key IS NULL
|
|
287
279
|
`;
|
|
288
280
|
|
|
289
281
|
const params: any[] = [...tags, Date.now()];
|
|
290
282
|
|
|
291
|
-
if (
|
|
292
|
-
query += ' AND o.
|
|
293
|
-
params.push(
|
|
283
|
+
if (excludePublicKey) {
|
|
284
|
+
query += ' AND o.public_key != ?';
|
|
285
|
+
params.push(excludePublicKey);
|
|
294
286
|
}
|
|
295
287
|
|
|
296
288
|
query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
|
|
@@ -303,7 +295,7 @@ export class SQLiteStorage implements Storage {
|
|
|
303
295
|
|
|
304
296
|
async getRandomOffer(
|
|
305
297
|
tags: string[],
|
|
306
|
-
|
|
298
|
+
excludePublicKey: string | null
|
|
307
299
|
): Promise<Offer | null> {
|
|
308
300
|
if (tags.length === 0) {
|
|
309
301
|
return null;
|
|
@@ -316,14 +308,14 @@ export class SQLiteStorage implements Storage {
|
|
|
316
308
|
SELECT DISTINCT o.* FROM offers o, json_each(o.tags) as t
|
|
317
309
|
WHERE t.value IN (${placeholders})
|
|
318
310
|
AND o.expires_at > ?
|
|
319
|
-
AND o.
|
|
311
|
+
AND o.answerer_public_key IS NULL
|
|
320
312
|
`;
|
|
321
313
|
|
|
322
314
|
const params: any[] = [...tags, Date.now()];
|
|
323
315
|
|
|
324
|
-
if (
|
|
325
|
-
query += ' AND o.
|
|
326
|
-
params.push(
|
|
316
|
+
if (excludePublicKey) {
|
|
317
|
+
query += ' AND o.public_key != ?';
|
|
318
|
+
params.push(excludePublicKey);
|
|
327
319
|
}
|
|
328
320
|
|
|
329
321
|
query += ' ORDER BY RANDOM() LIMIT 1';
|
|
@@ -338,12 +330,12 @@ export class SQLiteStorage implements Storage {
|
|
|
338
330
|
|
|
339
331
|
async addIceCandidates(
|
|
340
332
|
offerId: string,
|
|
341
|
-
|
|
333
|
+
publicKey: string,
|
|
342
334
|
role: 'offerer' | 'answerer',
|
|
343
335
|
candidates: any[]
|
|
344
336
|
): Promise<number> {
|
|
345
337
|
const stmt = this.db.prepare(`
|
|
346
|
-
INSERT INTO ice_candidates (offer_id,
|
|
338
|
+
INSERT INTO ice_candidates (offer_id, public_key, role, candidate, created_at)
|
|
347
339
|
VALUES (?, ?, ?, ?, ?)
|
|
348
340
|
`);
|
|
349
341
|
|
|
@@ -352,7 +344,7 @@ export class SQLiteStorage implements Storage {
|
|
|
352
344
|
for (let i = 0; i < candidates.length; i++) {
|
|
353
345
|
stmt.run(
|
|
354
346
|
offerId,
|
|
355
|
-
|
|
347
|
+
publicKey,
|
|
356
348
|
role,
|
|
357
349
|
JSON.stringify(candidates[i]),
|
|
358
350
|
baseTimestamp + i
|
|
@@ -389,7 +381,7 @@ export class SQLiteStorage implements Storage {
|
|
|
389
381
|
return rows.map(row => ({
|
|
390
382
|
id: row.id,
|
|
391
383
|
offerId: row.offer_id,
|
|
392
|
-
|
|
384
|
+
publicKey: row.public_key,
|
|
393
385
|
role: row.role,
|
|
394
386
|
candidate: JSON.parse(row.candidate),
|
|
395
387
|
createdAt: row.created_at,
|
|
@@ -398,7 +390,7 @@ export class SQLiteStorage implements Storage {
|
|
|
398
390
|
|
|
399
391
|
async getIceCandidatesForMultipleOffers(
|
|
400
392
|
offerIds: string[],
|
|
401
|
-
|
|
393
|
+
publicKey: string,
|
|
402
394
|
since?: number
|
|
403
395
|
): Promise<Map<string, IceCandidate[]>> {
|
|
404
396
|
const result = new Map<string, IceCandidate[]>();
|
|
@@ -423,17 +415,17 @@ export class SQLiteStorage implements Storage {
|
|
|
423
415
|
const placeholders = offerIds.map(() => '?').join(',');
|
|
424
416
|
|
|
425
417
|
let query = `
|
|
426
|
-
SELECT ic.*, o.
|
|
418
|
+
SELECT ic.*, o.public_key as offer_public_key
|
|
427
419
|
FROM ice_candidates ic
|
|
428
420
|
INNER JOIN offers o ON o.id = ic.offer_id
|
|
429
421
|
WHERE ic.offer_id IN (${placeholders})
|
|
430
422
|
AND (
|
|
431
|
-
(o.
|
|
432
|
-
OR (o.
|
|
423
|
+
(o.public_key = ? AND ic.role = 'answerer')
|
|
424
|
+
OR (o.answerer_public_key = ? AND ic.role = 'offerer')
|
|
433
425
|
)
|
|
434
426
|
`;
|
|
435
427
|
|
|
436
|
-
const params: any[] = [...offerIds,
|
|
428
|
+
const params: any[] = [...offerIds, publicKey, publicKey];
|
|
437
429
|
|
|
438
430
|
if (since !== undefined) {
|
|
439
431
|
query += ' AND ic.created_at > ?';
|
|
@@ -450,7 +442,7 @@ export class SQLiteStorage implements Storage {
|
|
|
450
442
|
const candidate: IceCandidate = {
|
|
451
443
|
id: row.id,
|
|
452
444
|
offerId: row.offer_id,
|
|
453
|
-
|
|
445
|
+
publicKey: row.public_key,
|
|
454
446
|
role: row.role,
|
|
455
447
|
candidate: JSON.parse(row.candidate),
|
|
456
448
|
createdAt: row.created_at,
|
|
@@ -465,122 +457,6 @@ export class SQLiteStorage implements Storage {
|
|
|
465
457
|
return result;
|
|
466
458
|
}
|
|
467
459
|
|
|
468
|
-
// ===== Credential Management =====
|
|
469
|
-
|
|
470
|
-
async generateCredentials(request: GenerateCredentialsRequest): Promise<Credential> {
|
|
471
|
-
const now = Date.now();
|
|
472
|
-
const expiresAt = request.expiresAt || (now + YEAR_IN_MS);
|
|
473
|
-
|
|
474
|
-
const { generateCredentialName, generateSecret } = await import('../crypto.ts');
|
|
475
|
-
|
|
476
|
-
let name: string;
|
|
477
|
-
|
|
478
|
-
if (request.name) {
|
|
479
|
-
// User requested specific username - check if available
|
|
480
|
-
const existing = this.db.prepare(`
|
|
481
|
-
SELECT name FROM credentials WHERE name = ?
|
|
482
|
-
`).get(request.name);
|
|
483
|
-
|
|
484
|
-
if (existing) {
|
|
485
|
-
throw new Error('Username already taken');
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
name = request.name;
|
|
489
|
-
} else {
|
|
490
|
-
// Generate random name - retry until unique
|
|
491
|
-
let attempts = 0;
|
|
492
|
-
const maxAttempts = 100;
|
|
493
|
-
|
|
494
|
-
while (attempts < maxAttempts) {
|
|
495
|
-
name = generateCredentialName();
|
|
496
|
-
|
|
497
|
-
const existing = this.db.prepare(`
|
|
498
|
-
SELECT name FROM credentials WHERE name = ?
|
|
499
|
-
`).get(name);
|
|
500
|
-
|
|
501
|
-
if (!existing) {
|
|
502
|
-
break;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
attempts++;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (attempts >= maxAttempts) {
|
|
509
|
-
throw new Error(`Failed to generate unique credential name after ${maxAttempts} attempts`);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const secret = generateSecret();
|
|
514
|
-
|
|
515
|
-
// Encrypt secret before storing (AES-256-GCM)
|
|
516
|
-
const { encryptSecret } = await import('../crypto.ts');
|
|
517
|
-
const encryptedSecret = await encryptSecret(secret, this.masterEncryptionKey);
|
|
518
|
-
|
|
519
|
-
// Insert credential with encrypted secret
|
|
520
|
-
const stmt = this.db.prepare(`
|
|
521
|
-
INSERT INTO credentials (name, secret, created_at, expires_at, last_used)
|
|
522
|
-
VALUES (?, ?, ?, ?, ?)
|
|
523
|
-
`);
|
|
524
|
-
|
|
525
|
-
stmt.run(name!, encryptedSecret, now, expiresAt, now);
|
|
526
|
-
|
|
527
|
-
// Return plaintext secret to user (only time they'll see it)
|
|
528
|
-
return {
|
|
529
|
-
name: name!,
|
|
530
|
-
secret, // Return plaintext secret, not encrypted
|
|
531
|
-
createdAt: now,
|
|
532
|
-
expiresAt,
|
|
533
|
-
lastUsed: now,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
async getCredential(name: string): Promise<Credential | null> {
|
|
538
|
-
const stmt = this.db.prepare(`
|
|
539
|
-
SELECT * FROM credentials
|
|
540
|
-
WHERE name = ? AND expires_at > ?
|
|
541
|
-
`);
|
|
542
|
-
|
|
543
|
-
const row = stmt.get(name, Date.now()) as any;
|
|
544
|
-
|
|
545
|
-
if (!row) {
|
|
546
|
-
return null;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Decrypt secret before returning
|
|
550
|
-
// If decryption fails (e.g., master key rotated), treat as credential not found
|
|
551
|
-
try {
|
|
552
|
-
const { decryptSecret } = await import('../crypto.ts');
|
|
553
|
-
const decryptedSecret = await decryptSecret(row.secret, this.masterEncryptionKey);
|
|
554
|
-
|
|
555
|
-
return {
|
|
556
|
-
name: row.name,
|
|
557
|
-
secret: decryptedSecret, // Return decrypted secret
|
|
558
|
-
createdAt: row.created_at,
|
|
559
|
-
expiresAt: row.expires_at,
|
|
560
|
-
lastUsed: row.last_used,
|
|
561
|
-
};
|
|
562
|
-
} catch (error) {
|
|
563
|
-
console.error(`Failed to decrypt secret for credential '${name}':`, error);
|
|
564
|
-
return null; // Treat as credential not found (fail-safe behavior)
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
async updateCredentialUsage(name: string, lastUsed: number, expiresAt: number): Promise<void> {
|
|
569
|
-
const stmt = this.db.prepare(`
|
|
570
|
-
UPDATE credentials
|
|
571
|
-
SET last_used = ?, expires_at = ?
|
|
572
|
-
WHERE name = ?
|
|
573
|
-
`);
|
|
574
|
-
|
|
575
|
-
stmt.run(lastUsed, expiresAt, name);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async deleteExpiredCredentials(now: number): Promise<number> {
|
|
579
|
-
const stmt = this.db.prepare('DELETE FROM credentials WHERE expires_at < ?');
|
|
580
|
-
const result = stmt.run(now);
|
|
581
|
-
return result.changes;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
460
|
// ===== Rate Limiting =====
|
|
585
461
|
|
|
586
462
|
async checkRateLimit(identifier: string, limit: number, windowMs: number): Promise<boolean> {
|
|
@@ -652,13 +528,8 @@ export class SQLiteStorage implements Storage {
|
|
|
652
528
|
return result.count;
|
|
653
529
|
}
|
|
654
530
|
|
|
655
|
-
async
|
|
656
|
-
const result = this.db.prepare('SELECT COUNT(*) as count FROM offers WHERE
|
|
657
|
-
return result.count;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
async getCredentialCount(): Promise<number> {
|
|
661
|
-
const result = this.db.prepare('SELECT COUNT(*) as count FROM credentials').get() as { count: number };
|
|
531
|
+
async getOfferCountByPublicKey(publicKey: string): Promise<number> {
|
|
532
|
+
const result = this.db.prepare('SELECT COUNT(*) as count FROM offers WHERE public_key = ?').get(publicKey) as { count: number };
|
|
662
533
|
return result.count;
|
|
663
534
|
}
|
|
664
535
|
|
|
@@ -675,13 +546,13 @@ export class SQLiteStorage implements Storage {
|
|
|
675
546
|
private rowToOffer(row: any): Offer {
|
|
676
547
|
return {
|
|
677
548
|
id: row.id,
|
|
678
|
-
|
|
549
|
+
publicKey: row.public_key,
|
|
679
550
|
tags: JSON.parse(row.tags),
|
|
680
551
|
sdp: row.sdp,
|
|
681
552
|
createdAt: row.created_at,
|
|
682
553
|
expiresAt: row.expires_at,
|
|
683
554
|
lastSeen: row.last_seen,
|
|
684
|
-
|
|
555
|
+
answererPublicKey: row.answerer_public_key || undefined,
|
|
685
556
|
answerSdp: row.answer_sdp || undefined,
|
|
686
557
|
answeredAt: row.answered_at || undefined,
|
|
687
558
|
matchedTags: row.matched_tags ? JSON.parse(row.matched_tags) : undefined,
|