@xtr-dev/rondevu-server 0.1.4 → 0.2.0
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 +217 -69
- package/dist/index.js +1068 -386
- package/dist/index.js.map +4 -4
- package/package.json +3 -2
- package/src/app.ts +340 -297
- package/src/crypto.ts +164 -0
- package/src/storage/d1.ts +295 -119
- package/src/storage/sqlite.ts +309 -107
- package/src/storage/types.ts +159 -29
package/src/storage/sqlite.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import {
|
|
4
|
+
Storage,
|
|
5
|
+
Offer,
|
|
6
|
+
IceCandidate,
|
|
7
|
+
CreateOfferRequest,
|
|
8
|
+
Username,
|
|
9
|
+
ClaimUsernameRequest,
|
|
10
|
+
Service,
|
|
11
|
+
CreateServiceRequest,
|
|
12
|
+
ServiceInfo,
|
|
13
|
+
} from './types.ts';
|
|
3
14
|
import { generateOfferHash } from './hash-id.ts';
|
|
4
15
|
|
|
16
|
+
const YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000; // 365 days
|
|
17
|
+
|
|
5
18
|
/**
|
|
6
|
-
* SQLite storage adapter for
|
|
19
|
+
* SQLite storage adapter for rondevu DNS-like system
|
|
7
20
|
* Supports both file-based and in-memory databases
|
|
8
21
|
*/
|
|
9
22
|
export class SQLiteStorage implements Storage {
|
|
@@ -19,10 +32,11 @@ export class SQLiteStorage implements Storage {
|
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
/**
|
|
22
|
-
* Initializes database schema with
|
|
35
|
+
* Initializes database schema with username and service-based structure
|
|
23
36
|
*/
|
|
24
37
|
private initializeDatabase(): void {
|
|
25
38
|
this.db.exec(`
|
|
39
|
+
-- Offers table (no topics)
|
|
26
40
|
CREATE TABLE IF NOT EXISTS offers (
|
|
27
41
|
id TEXT PRIMARY KEY,
|
|
28
42
|
peer_id TEXT NOT NULL,
|
|
@@ -41,22 +55,13 @@ export class SQLiteStorage implements Storage {
|
|
|
41
55
|
CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
|
|
42
56
|
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
offer_id TEXT NOT NULL,
|
|
46
|
-
topic TEXT NOT NULL,
|
|
47
|
-
PRIMARY KEY (offer_id, topic),
|
|
48
|
-
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
CREATE INDEX IF NOT EXISTS idx_topics_topic ON offer_topics(topic);
|
|
52
|
-
CREATE INDEX IF NOT EXISTS idx_topics_offer ON offer_topics(offer_id);
|
|
53
|
-
|
|
58
|
+
-- ICE candidates table
|
|
54
59
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
55
60
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
61
|
offer_id TEXT NOT NULL,
|
|
57
62
|
peer_id TEXT NOT NULL,
|
|
58
63
|
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
|
|
59
|
-
candidate TEXT NOT NULL,
|
|
64
|
+
candidate TEXT NOT NULL,
|
|
60
65
|
created_at INTEGER NOT NULL,
|
|
61
66
|
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
|
|
62
67
|
);
|
|
@@ -64,12 +69,62 @@ export class SQLiteStorage implements Storage {
|
|
|
64
69
|
CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
|
|
65
70
|
CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);
|
|
66
71
|
CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
|
|
72
|
+
|
|
73
|
+
-- Usernames table
|
|
74
|
+
CREATE TABLE IF NOT EXISTS usernames (
|
|
75
|
+
username TEXT PRIMARY KEY,
|
|
76
|
+
public_key TEXT NOT NULL UNIQUE,
|
|
77
|
+
claimed_at INTEGER NOT NULL,
|
|
78
|
+
expires_at INTEGER NOT NULL,
|
|
79
|
+
last_used INTEGER NOT NULL,
|
|
80
|
+
metadata TEXT,
|
|
81
|
+
CHECK(length(username) >= 3 AND length(username) <= 32)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
|
|
86
|
+
|
|
87
|
+
-- Services table
|
|
88
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
username TEXT NOT NULL,
|
|
91
|
+
service_fqn TEXT NOT NULL,
|
|
92
|
+
offer_id TEXT NOT NULL,
|
|
93
|
+
created_at INTEGER NOT NULL,
|
|
94
|
+
expires_at INTEGER NOT NULL,
|
|
95
|
+
is_public INTEGER NOT NULL DEFAULT 0,
|
|
96
|
+
metadata TEXT,
|
|
97
|
+
FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
|
|
98
|
+
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE,
|
|
99
|
+
UNIQUE(username, service_fqn)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_services_offer ON services(offer_id);
|
|
106
|
+
|
|
107
|
+
-- Service index table (privacy layer)
|
|
108
|
+
CREATE TABLE IF NOT EXISTS service_index (
|
|
109
|
+
uuid TEXT PRIMARY KEY,
|
|
110
|
+
service_id TEXT NOT NULL,
|
|
111
|
+
username TEXT NOT NULL,
|
|
112
|
+
service_fqn TEXT NOT NULL,
|
|
113
|
+
created_at INTEGER NOT NULL,
|
|
114
|
+
expires_at INTEGER NOT NULL,
|
|
115
|
+
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_service_index_username ON service_index(username);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_service_index_expires ON service_index(expires_at);
|
|
67
120
|
`);
|
|
68
121
|
|
|
69
122
|
// Enable foreign keys
|
|
70
123
|
this.db.pragma('foreign_keys = ON');
|
|
71
124
|
}
|
|
72
125
|
|
|
126
|
+
// ===== Offer Management =====
|
|
127
|
+
|
|
73
128
|
async createOffers(offers: CreateOfferRequest[]): Promise<Offer[]> {
|
|
74
129
|
const created: Offer[] = [];
|
|
75
130
|
|
|
@@ -77,7 +132,7 @@ export class SQLiteStorage implements Storage {
|
|
|
77
132
|
const offersWithIds = await Promise.all(
|
|
78
133
|
offers.map(async (offer) => ({
|
|
79
134
|
...offer,
|
|
80
|
-
id: offer.id || await generateOfferHash(offer.sdp,
|
|
135
|
+
id: offer.id || await generateOfferHash(offer.sdp, []),
|
|
81
136
|
}))
|
|
82
137
|
);
|
|
83
138
|
|
|
@@ -88,11 +143,6 @@ export class SQLiteStorage implements Storage {
|
|
|
88
143
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
89
144
|
`);
|
|
90
145
|
|
|
91
|
-
const topicStmt = this.db.prepare(`
|
|
92
|
-
INSERT INTO offer_topics (offer_id, topic)
|
|
93
|
-
VALUES (?, ?)
|
|
94
|
-
`);
|
|
95
|
-
|
|
96
146
|
for (const offer of offersWithIds) {
|
|
97
147
|
const now = Date.now();
|
|
98
148
|
|
|
@@ -107,16 +157,10 @@ export class SQLiteStorage implements Storage {
|
|
|
107
157
|
offer.secret || null
|
|
108
158
|
);
|
|
109
159
|
|
|
110
|
-
// Insert topics
|
|
111
|
-
for (const topic of offer.topics) {
|
|
112
|
-
topicStmt.run(offer.id, topic);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
160
|
created.push({
|
|
116
161
|
id: offer.id,
|
|
117
162
|
peerId: offer.peerId,
|
|
118
163
|
sdp: offer.sdp,
|
|
119
|
-
topics: offer.topics,
|
|
120
164
|
createdAt: now,
|
|
121
165
|
expiresAt: offer.expiresAt,
|
|
122
166
|
lastSeen: now,
|
|
@@ -129,30 +173,6 @@ export class SQLiteStorage implements Storage {
|
|
|
129
173
|
return created;
|
|
130
174
|
}
|
|
131
175
|
|
|
132
|
-
async getOffersByTopic(topic: string, excludePeerIds?: string[]): Promise<Offer[]> {
|
|
133
|
-
let query = `
|
|
134
|
-
SELECT DISTINCT o.*
|
|
135
|
-
FROM offers o
|
|
136
|
-
INNER JOIN offer_topics ot ON o.id = ot.offer_id
|
|
137
|
-
WHERE ot.topic = ? AND o.expires_at > ?
|
|
138
|
-
`;
|
|
139
|
-
|
|
140
|
-
const params: any[] = [topic, Date.now()];
|
|
141
|
-
|
|
142
|
-
if (excludePeerIds && excludePeerIds.length > 0) {
|
|
143
|
-
const placeholders = excludePeerIds.map(() => '?').join(',');
|
|
144
|
-
query += ` AND o.peer_id NOT IN (${placeholders})`;
|
|
145
|
-
params.push(...excludePeerIds);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
query += ' ORDER BY o.last_seen DESC';
|
|
149
|
-
|
|
150
|
-
const stmt = this.db.prepare(query);
|
|
151
|
-
const rows = stmt.all(...params) as any[];
|
|
152
|
-
|
|
153
|
-
return Promise.all(rows.map(row => this.rowToOffer(row)));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
176
|
async getOffersByPeerId(peerId: string): Promise<Offer[]> {
|
|
157
177
|
const stmt = this.db.prepare(`
|
|
158
178
|
SELECT * FROM offers
|
|
@@ -161,7 +181,7 @@ export class SQLiteStorage implements Storage {
|
|
|
161
181
|
`);
|
|
162
182
|
|
|
163
183
|
const rows = stmt.all(peerId, Date.now()) as any[];
|
|
164
|
-
return
|
|
184
|
+
return rows.map(row => this.rowToOffer(row));
|
|
165
185
|
}
|
|
166
186
|
|
|
167
187
|
async getOfferById(offerId: string): Promise<Offer | null> {
|
|
@@ -254,9 +274,11 @@ export class SQLiteStorage implements Storage {
|
|
|
254
274
|
`);
|
|
255
275
|
|
|
256
276
|
const rows = stmt.all(offererPeerId, Date.now()) as any[];
|
|
257
|
-
return
|
|
277
|
+
return rows.map(row => this.rowToOffer(row));
|
|
258
278
|
}
|
|
259
279
|
|
|
280
|
+
// ===== ICE Candidate Management =====
|
|
281
|
+
|
|
260
282
|
async addIceCandidates(
|
|
261
283
|
offerId: string,
|
|
262
284
|
peerId: string,
|
|
@@ -275,8 +297,8 @@ export class SQLiteStorage implements Storage {
|
|
|
275
297
|
offerId,
|
|
276
298
|
peerId,
|
|
277
299
|
role,
|
|
278
|
-
JSON.stringify(candidates[i]),
|
|
279
|
-
baseTimestamp + i
|
|
300
|
+
JSON.stringify(candidates[i]),
|
|
301
|
+
baseTimestamp + i
|
|
280
302
|
);
|
|
281
303
|
}
|
|
282
304
|
});
|
|
@@ -312,85 +334,249 @@ export class SQLiteStorage implements Storage {
|
|
|
312
334
|
offerId: row.offer_id,
|
|
313
335
|
peerId: row.peer_id,
|
|
314
336
|
role: row.role,
|
|
315
|
-
candidate: JSON.parse(row.candidate),
|
|
337
|
+
candidate: JSON.parse(row.candidate),
|
|
316
338
|
createdAt: row.created_at,
|
|
317
339
|
}));
|
|
318
340
|
}
|
|
319
341
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
342
|
+
// ===== Username Management =====
|
|
343
|
+
|
|
344
|
+
async claimUsername(request: ClaimUsernameRequest): Promise<Username> {
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
const expiresAt = now + YEAR_IN_MS;
|
|
347
|
+
|
|
348
|
+
// Try to insert or update
|
|
349
|
+
const stmt = this.db.prepare(`
|
|
350
|
+
INSERT INTO usernames (username, public_key, claimed_at, expires_at, last_used, metadata)
|
|
351
|
+
VALUES (?, ?, ?, ?, ?, NULL)
|
|
352
|
+
ON CONFLICT(username) DO UPDATE SET
|
|
353
|
+
expires_at = ?,
|
|
354
|
+
last_used = ?
|
|
355
|
+
WHERE public_key = ?
|
|
356
|
+
`);
|
|
357
|
+
|
|
358
|
+
const result = stmt.run(
|
|
359
|
+
request.username,
|
|
360
|
+
request.publicKey,
|
|
361
|
+
now,
|
|
362
|
+
expiresAt,
|
|
363
|
+
now,
|
|
364
|
+
expiresAt,
|
|
365
|
+
now,
|
|
366
|
+
request.publicKey
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
if (result.changes === 0) {
|
|
370
|
+
throw new Error('Username already claimed by different public key');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
username: request.username,
|
|
375
|
+
publicKey: request.publicKey,
|
|
376
|
+
claimedAt: now,
|
|
377
|
+
expiresAt,
|
|
378
|
+
lastUsed: now,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async getUsername(username: string): Promise<Username | null> {
|
|
383
|
+
const stmt = this.db.prepare(`
|
|
384
|
+
SELECT * FROM usernames
|
|
385
|
+
WHERE username = ? AND expires_at > ?
|
|
386
|
+
`);
|
|
387
|
+
|
|
388
|
+
const row = stmt.get(username, Date.now()) as any;
|
|
389
|
+
|
|
390
|
+
if (!row) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
username: row.username,
|
|
396
|
+
publicKey: row.public_key,
|
|
397
|
+
claimedAt: row.claimed_at,
|
|
398
|
+
expiresAt: row.expires_at,
|
|
399
|
+
lastUsed: row.last_used,
|
|
400
|
+
metadata: row.metadata || undefined,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async touchUsername(username: string): Promise<boolean> {
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
const expiresAt = now + YEAR_IN_MS;
|
|
407
|
+
|
|
408
|
+
const stmt = this.db.prepare(`
|
|
409
|
+
UPDATE usernames
|
|
410
|
+
SET last_used = ?, expires_at = ?
|
|
411
|
+
WHERE username = ? AND expires_at > ?
|
|
412
|
+
`);
|
|
413
|
+
|
|
414
|
+
const result = stmt.run(now, expiresAt, username, now);
|
|
415
|
+
return result.changes > 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async deleteExpiredUsernames(now: number): Promise<number> {
|
|
419
|
+
const stmt = this.db.prepare('DELETE FROM usernames WHERE expires_at < ?');
|
|
420
|
+
const result = stmt.run(now);
|
|
421
|
+
return result.changes;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ===== Service Management =====
|
|
425
|
+
|
|
426
|
+
async createService(request: CreateServiceRequest): Promise<{
|
|
427
|
+
service: Service;
|
|
428
|
+
indexUuid: string;
|
|
323
429
|
}> {
|
|
430
|
+
const serviceId = randomUUID();
|
|
431
|
+
const indexUuid = randomUUID();
|
|
324
432
|
const now = Date.now();
|
|
325
433
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
434
|
+
const transaction = this.db.transaction(() => {
|
|
435
|
+
// Insert service
|
|
436
|
+
const serviceStmt = this.db.prepare(`
|
|
437
|
+
INSERT INTO services (id, username, service_fqn, offer_id, created_at, expires_at, is_public, metadata)
|
|
438
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
439
|
+
`);
|
|
330
440
|
|
|
331
|
-
|
|
441
|
+
serviceStmt.run(
|
|
442
|
+
serviceId,
|
|
443
|
+
request.username,
|
|
444
|
+
request.serviceFqn,
|
|
445
|
+
request.offerId,
|
|
446
|
+
now,
|
|
447
|
+
request.expiresAt,
|
|
448
|
+
request.isPublic ? 1 : 0,
|
|
449
|
+
request.metadata || null
|
|
450
|
+
);
|
|
332
451
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
WHERE ${whereClause}
|
|
339
|
-
`;
|
|
452
|
+
// Insert service index
|
|
453
|
+
const indexStmt = this.db.prepare(`
|
|
454
|
+
INSERT INTO service_index (uuid, service_id, username, service_fqn, created_at, expires_at)
|
|
455
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
456
|
+
`);
|
|
340
457
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
458
|
+
indexStmt.run(
|
|
459
|
+
indexUuid,
|
|
460
|
+
serviceId,
|
|
461
|
+
request.username,
|
|
462
|
+
request.serviceFqn,
|
|
463
|
+
now,
|
|
464
|
+
request.expiresAt
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// Touch username to extend expiry
|
|
468
|
+
this.touchUsername(request.username);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
transaction();
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
service: {
|
|
475
|
+
id: serviceId,
|
|
476
|
+
username: request.username,
|
|
477
|
+
serviceFqn: request.serviceFqn,
|
|
478
|
+
offerId: request.offerId,
|
|
479
|
+
createdAt: now,
|
|
480
|
+
expiresAt: request.expiresAt,
|
|
481
|
+
isPublic: request.isPublic || false,
|
|
482
|
+
metadata: request.metadata,
|
|
483
|
+
},
|
|
484
|
+
indexUuid,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
358
487
|
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
488
|
+
async getServiceById(serviceId: string): Promise<Service | null> {
|
|
489
|
+
const stmt = this.db.prepare(`
|
|
490
|
+
SELECT * FROM services
|
|
491
|
+
WHERE id = ? AND expires_at > ?
|
|
492
|
+
`);
|
|
493
|
+
|
|
494
|
+
const row = stmt.get(serviceId, Date.now()) as any;
|
|
495
|
+
|
|
496
|
+
if (!row) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return this.rowToService(row);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async getServiceByUuid(uuid: string): Promise<Service | null> {
|
|
504
|
+
const stmt = this.db.prepare(`
|
|
505
|
+
SELECT s.* FROM services s
|
|
506
|
+
INNER JOIN service_index si ON s.id = si.service_id
|
|
507
|
+
WHERE si.uuid = ? AND s.expires_at > ?
|
|
508
|
+
`);
|
|
509
|
+
|
|
510
|
+
const row = stmt.get(uuid, Date.now()) as any;
|
|
511
|
+
|
|
512
|
+
if (!row) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
364
515
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
516
|
+
return this.rowToService(row);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async listServicesForUsername(username: string): Promise<ServiceInfo[]> {
|
|
520
|
+
const stmt = this.db.prepare(`
|
|
521
|
+
SELECT si.uuid, s.is_public, s.service_fqn, s.metadata
|
|
522
|
+
FROM service_index si
|
|
523
|
+
INNER JOIN services s ON si.service_id = s.id
|
|
524
|
+
WHERE si.username = ? AND si.expires_at > ?
|
|
525
|
+
ORDER BY s.created_at DESC
|
|
526
|
+
`);
|
|
527
|
+
|
|
528
|
+
const rows = stmt.all(username, Date.now()) as any[];
|
|
529
|
+
|
|
530
|
+
return rows.map(row => ({
|
|
531
|
+
uuid: row.uuid,
|
|
532
|
+
isPublic: row.is_public === 1,
|
|
533
|
+
serviceFqn: row.is_public === 1 ? row.service_fqn : undefined,
|
|
534
|
+
metadata: row.is_public === 1 ? row.metadata || undefined : undefined,
|
|
368
535
|
}));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async queryService(username: string, serviceFqn: string): Promise<string | null> {
|
|
539
|
+
const stmt = this.db.prepare(`
|
|
540
|
+
SELECT si.uuid FROM service_index si
|
|
541
|
+
INNER JOIN services s ON si.service_id = s.id
|
|
542
|
+
WHERE si.username = ? AND si.service_fqn = ? AND si.expires_at > ?
|
|
543
|
+
`);
|
|
544
|
+
|
|
545
|
+
const row = stmt.get(username, serviceFqn, Date.now()) as any;
|
|
546
|
+
|
|
547
|
+
return row ? row.uuid : null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async deleteService(serviceId: string, username: string): Promise<boolean> {
|
|
551
|
+
const stmt = this.db.prepare(`
|
|
552
|
+
DELETE FROM services
|
|
553
|
+
WHERE id = ? AND username = ?
|
|
554
|
+
`);
|
|
555
|
+
|
|
556
|
+
const result = stmt.run(serviceId, username);
|
|
557
|
+
return result.changes > 0;
|
|
558
|
+
}
|
|
369
559
|
|
|
370
|
-
|
|
560
|
+
async deleteExpiredServices(now: number): Promise<number> {
|
|
561
|
+
const stmt = this.db.prepare('DELETE FROM services WHERE expires_at < ?');
|
|
562
|
+
const result = stmt.run(now);
|
|
563
|
+
return result.changes;
|
|
371
564
|
}
|
|
372
565
|
|
|
373
566
|
async close(): Promise<void> {
|
|
374
567
|
this.db.close();
|
|
375
568
|
}
|
|
376
569
|
|
|
570
|
+
// ===== Helper Methods =====
|
|
571
|
+
|
|
377
572
|
/**
|
|
378
|
-
* Helper method to convert database row to Offer object
|
|
573
|
+
* Helper method to convert database row to Offer object
|
|
379
574
|
*/
|
|
380
|
-
private
|
|
381
|
-
// Get topics for this offer
|
|
382
|
-
const topicStmt = this.db.prepare(`
|
|
383
|
-
SELECT topic FROM offer_topics WHERE offer_id = ?
|
|
384
|
-
`);
|
|
385
|
-
|
|
386
|
-
const topicRows = topicStmt.all(row.id) as any[];
|
|
387
|
-
const topics = topicRows.map(t => t.topic);
|
|
388
|
-
|
|
575
|
+
private rowToOffer(row: any): Offer {
|
|
389
576
|
return {
|
|
390
577
|
id: row.id,
|
|
391
578
|
peerId: row.peer_id,
|
|
392
579
|
sdp: row.sdp,
|
|
393
|
-
topics,
|
|
394
580
|
createdAt: row.created_at,
|
|
395
581
|
expiresAt: row.expires_at,
|
|
396
582
|
lastSeen: row.last_seen,
|
|
@@ -400,4 +586,20 @@ export class SQLiteStorage implements Storage {
|
|
|
400
586
|
answeredAt: row.answered_at || undefined,
|
|
401
587
|
};
|
|
402
588
|
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Helper method to convert database row to Service object
|
|
592
|
+
*/
|
|
593
|
+
private rowToService(row: any): Service {
|
|
594
|
+
return {
|
|
595
|
+
id: row.id,
|
|
596
|
+
username: row.username,
|
|
597
|
+
serviceFqn: row.service_fqn,
|
|
598
|
+
offerId: row.offer_id,
|
|
599
|
+
createdAt: row.created_at,
|
|
600
|
+
expiresAt: row.expires_at,
|
|
601
|
+
isPublic: row.is_public === 1,
|
|
602
|
+
metadata: row.metadata || undefined,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
403
605
|
}
|