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