@xtr-dev/rondevu-server 0.1.5 → 0.2.1
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 +1070 -368
- package/dist/index.js.map +4 -4
- package/package.json +3 -2
- package/src/app.ts +339 -271
- package/src/crypto.ts +170 -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/d1.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
Storage,
|
|
4
|
+
Offer,
|
|
5
|
+
IceCandidate,
|
|
6
|
+
CreateOfferRequest,
|
|
7
|
+
Username,
|
|
8
|
+
ClaimUsernameRequest,
|
|
9
|
+
Service,
|
|
10
|
+
CreateServiceRequest,
|
|
11
|
+
ServiceInfo,
|
|
12
|
+
} from './types.ts';
|
|
2
13
|
import { generateOfferHash } from './hash-id.ts';
|
|
3
14
|
|
|
15
|
+
const YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000; // 365 days
|
|
16
|
+
|
|
4
17
|
/**
|
|
5
|
-
* D1 storage adapter for
|
|
6
|
-
* NOTE: This implementation is a placeholder and needs to be fully tested
|
|
18
|
+
* D1 storage adapter for rondevu DNS-like system using Cloudflare D1
|
|
7
19
|
*/
|
|
8
20
|
export class D1Storage implements Storage {
|
|
9
21
|
private db: D1Database;
|
|
@@ -17,11 +29,12 @@ export class D1Storage implements Storage {
|
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
/**
|
|
20
|
-
* Initializes database schema with
|
|
32
|
+
* Initializes database schema with username and service-based structure
|
|
21
33
|
* This should be run once during setup, not on every request
|
|
22
34
|
*/
|
|
23
35
|
async initializeDatabase(): Promise<void> {
|
|
24
36
|
await this.db.exec(`
|
|
37
|
+
-- Offers table (no topics)
|
|
25
38
|
CREATE TABLE IF NOT EXISTS offers (
|
|
26
39
|
id TEXT PRIMARY KEY,
|
|
27
40
|
peer_id TEXT NOT NULL,
|
|
@@ -40,22 +53,13 @@ export class D1Storage implements Storage {
|
|
|
40
53
|
CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
|
|
41
54
|
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
offer_id TEXT NOT NULL,
|
|
45
|
-
topic TEXT NOT NULL,
|
|
46
|
-
PRIMARY KEY (offer_id, topic),
|
|
47
|
-
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
CREATE INDEX IF NOT EXISTS idx_topics_topic ON offer_topics(topic);
|
|
51
|
-
CREATE INDEX IF NOT EXISTS idx_topics_offer ON offer_topics(offer_id);
|
|
52
|
-
|
|
56
|
+
-- ICE candidates table
|
|
53
57
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
54
58
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
59
|
offer_id TEXT NOT NULL,
|
|
56
60
|
peer_id TEXT NOT NULL,
|
|
57
61
|
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
|
|
58
|
-
candidate TEXT NOT NULL,
|
|
62
|
+
candidate TEXT NOT NULL,
|
|
59
63
|
created_at INTEGER NOT NULL,
|
|
60
64
|
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
|
|
61
65
|
);
|
|
@@ -63,36 +67,76 @@ export class D1Storage implements Storage {
|
|
|
63
67
|
CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
|
|
64
68
|
CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);
|
|
65
69
|
CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
|
|
70
|
+
|
|
71
|
+
-- Usernames table
|
|
72
|
+
CREATE TABLE IF NOT EXISTS usernames (
|
|
73
|
+
username TEXT PRIMARY KEY,
|
|
74
|
+
public_key TEXT NOT NULL UNIQUE,
|
|
75
|
+
claimed_at INTEGER NOT NULL,
|
|
76
|
+
expires_at INTEGER NOT NULL,
|
|
77
|
+
last_used INTEGER NOT NULL,
|
|
78
|
+
metadata TEXT,
|
|
79
|
+
CHECK(length(username) >= 3 AND length(username) <= 32)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
|
|
84
|
+
|
|
85
|
+
-- Services table
|
|
86
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
87
|
+
id TEXT PRIMARY KEY,
|
|
88
|
+
username TEXT NOT NULL,
|
|
89
|
+
service_fqn TEXT NOT NULL,
|
|
90
|
+
offer_id TEXT NOT NULL,
|
|
91
|
+
created_at INTEGER NOT NULL,
|
|
92
|
+
expires_at INTEGER NOT NULL,
|
|
93
|
+
is_public INTEGER NOT NULL DEFAULT 0,
|
|
94
|
+
metadata TEXT,
|
|
95
|
+
FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
|
|
96
|
+
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE,
|
|
97
|
+
UNIQUE(username, service_fqn)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_services_offer ON services(offer_id);
|
|
104
|
+
|
|
105
|
+
-- Service index table (privacy layer)
|
|
106
|
+
CREATE TABLE IF NOT EXISTS service_index (
|
|
107
|
+
uuid TEXT PRIMARY KEY,
|
|
108
|
+
service_id TEXT NOT NULL,
|
|
109
|
+
username TEXT NOT NULL,
|
|
110
|
+
service_fqn TEXT NOT NULL,
|
|
111
|
+
created_at INTEGER NOT NULL,
|
|
112
|
+
expires_at INTEGER NOT NULL,
|
|
113
|
+
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_service_index_username ON service_index(username);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_service_index_expires ON service_index(expires_at);
|
|
66
118
|
`);
|
|
67
119
|
}
|
|
68
120
|
|
|
121
|
+
// ===== Offer Management =====
|
|
122
|
+
|
|
69
123
|
async createOffers(offers: CreateOfferRequest[]): Promise<Offer[]> {
|
|
70
124
|
const created: Offer[] = [];
|
|
71
125
|
|
|
72
126
|
// D1 doesn't support true transactions yet, so we do this sequentially
|
|
73
127
|
for (const offer of offers) {
|
|
74
|
-
const id = offer.id || await generateOfferHash(offer.sdp,
|
|
128
|
+
const id = offer.id || await generateOfferHash(offer.sdp, []);
|
|
75
129
|
const now = Date.now();
|
|
76
130
|
|
|
77
|
-
// Insert offer
|
|
78
131
|
await this.db.prepare(`
|
|
79
132
|
INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
|
|
80
133
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
81
134
|
`).bind(id, offer.peerId, offer.sdp, now, offer.expiresAt, now, offer.secret || null).run();
|
|
82
135
|
|
|
83
|
-
// Insert topics
|
|
84
|
-
for (const topic of offer.topics) {
|
|
85
|
-
await this.db.prepare(`
|
|
86
|
-
INSERT INTO offer_topics (offer_id, topic)
|
|
87
|
-
VALUES (?, ?)
|
|
88
|
-
`).bind(id, topic).run();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
136
|
created.push({
|
|
92
137
|
id,
|
|
93
138
|
peerId: offer.peerId,
|
|
94
139
|
sdp: offer.sdp,
|
|
95
|
-
topics: offer.topics,
|
|
96
140
|
createdAt: now,
|
|
97
141
|
expiresAt: offer.expiresAt,
|
|
98
142
|
lastSeen: now,
|
|
@@ -103,33 +147,6 @@ export class D1Storage implements Storage {
|
|
|
103
147
|
return created;
|
|
104
148
|
}
|
|
105
149
|
|
|
106
|
-
async getOffersByTopic(topic: string, excludePeerIds?: string[]): Promise<Offer[]> {
|
|
107
|
-
let query = `
|
|
108
|
-
SELECT DISTINCT o.*
|
|
109
|
-
FROM offers o
|
|
110
|
-
INNER JOIN offer_topics ot ON o.id = ot.offer_id
|
|
111
|
-
WHERE ot.topic = ? AND o.expires_at > ?
|
|
112
|
-
`;
|
|
113
|
-
|
|
114
|
-
const params: any[] = [topic, Date.now()];
|
|
115
|
-
|
|
116
|
-
if (excludePeerIds && excludePeerIds.length > 0) {
|
|
117
|
-
const placeholders = excludePeerIds.map(() => '?').join(',');
|
|
118
|
-
query += ` AND o.peer_id NOT IN (${placeholders})`;
|
|
119
|
-
params.push(...excludePeerIds);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
query += ' ORDER BY o.last_seen DESC';
|
|
123
|
-
|
|
124
|
-
const result = await this.db.prepare(query).bind(...params).all();
|
|
125
|
-
|
|
126
|
-
if (!result.results) {
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return Promise.all(result.results.map(row => this.rowToOffer(row as any)));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
150
|
async getOffersByPeerId(peerId: string): Promise<Offer[]> {
|
|
134
151
|
const result = await this.db.prepare(`
|
|
135
152
|
SELECT * FROM offers
|
|
@@ -141,7 +158,7 @@ export class D1Storage implements Storage {
|
|
|
141
158
|
return [];
|
|
142
159
|
}
|
|
143
160
|
|
|
144
|
-
return
|
|
161
|
+
return result.results.map(row => this.rowToOffer(row as any));
|
|
145
162
|
}
|
|
146
163
|
|
|
147
164
|
async getOfferById(offerId: string): Promise<Offer | null> {
|
|
@@ -234,21 +251,20 @@ export class D1Storage implements Storage {
|
|
|
234
251
|
return [];
|
|
235
252
|
}
|
|
236
253
|
|
|
237
|
-
return
|
|
254
|
+
return result.results.map(row => this.rowToOffer(row as any));
|
|
238
255
|
}
|
|
239
256
|
|
|
257
|
+
// ===== ICE Candidate Management =====
|
|
258
|
+
|
|
240
259
|
async addIceCandidates(
|
|
241
260
|
offerId: string,
|
|
242
261
|
peerId: string,
|
|
243
262
|
role: 'offerer' | 'answerer',
|
|
244
263
|
candidates: any[]
|
|
245
264
|
): Promise<number> {
|
|
246
|
-
console.log(`[D1] addIceCandidates: offerId=${offerId}, peerId=${peerId}, role=${role}, count=${candidates.length}`);
|
|
247
|
-
|
|
248
|
-
// Give each candidate a unique timestamp to avoid "since" filtering issues
|
|
249
265
|
// D1 doesn't have transactions, so insert one by one
|
|
250
266
|
for (let i = 0; i < candidates.length; i++) {
|
|
251
|
-
const timestamp = Date.now() + i;
|
|
267
|
+
const timestamp = Date.now() + i;
|
|
252
268
|
await this.db.prepare(`
|
|
253
269
|
INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)
|
|
254
270
|
VALUES (?, ?, ?, ?, ?)
|
|
@@ -256,7 +272,7 @@ export class D1Storage implements Storage {
|
|
|
256
272
|
offerId,
|
|
257
273
|
peerId,
|
|
258
274
|
role,
|
|
259
|
-
JSON.stringify(candidates[i]),
|
|
275
|
+
JSON.stringify(candidates[i]),
|
|
260
276
|
timestamp
|
|
261
277
|
).run();
|
|
262
278
|
}
|
|
@@ -283,82 +299,232 @@ export class D1Storage implements Storage {
|
|
|
283
299
|
|
|
284
300
|
query += ' ORDER BY created_at ASC';
|
|
285
301
|
|
|
286
|
-
console.log(`[D1] getIceCandidates query: offerId=${offerId}, targetRole=${targetRole}, since=${since}`);
|
|
287
302
|
const result = await this.db.prepare(query).bind(...params).all();
|
|
288
|
-
console.log(`[D1] getIceCandidates result: ${result.results?.length || 0} rows`);
|
|
289
303
|
|
|
290
304
|
if (!result.results) {
|
|
291
305
|
return [];
|
|
292
306
|
}
|
|
293
307
|
|
|
294
|
-
|
|
308
|
+
return result.results.map((row: any) => ({
|
|
295
309
|
id: row.id,
|
|
296
310
|
offerId: row.offer_id,
|
|
297
311
|
peerId: row.peer_id,
|
|
298
312
|
role: row.role,
|
|
299
|
-
candidate: JSON.parse(row.candidate),
|
|
313
|
+
candidate: JSON.parse(row.candidate),
|
|
300
314
|
createdAt: row.created_at,
|
|
301
315
|
}));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ===== Username Management =====
|
|
319
|
+
|
|
320
|
+
async claimUsername(request: ClaimUsernameRequest): Promise<Username> {
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
const expiresAt = now + YEAR_IN_MS;
|
|
323
|
+
|
|
324
|
+
// Try to insert or update
|
|
325
|
+
const result = await this.db.prepare(`
|
|
326
|
+
INSERT INTO usernames (username, public_key, claimed_at, expires_at, last_used, metadata)
|
|
327
|
+
VALUES (?, ?, ?, ?, ?, NULL)
|
|
328
|
+
ON CONFLICT(username) DO UPDATE SET
|
|
329
|
+
expires_at = ?,
|
|
330
|
+
last_used = ?
|
|
331
|
+
WHERE public_key = ?
|
|
332
|
+
`).bind(
|
|
333
|
+
request.username,
|
|
334
|
+
request.publicKey,
|
|
335
|
+
now,
|
|
336
|
+
expiresAt,
|
|
337
|
+
now,
|
|
338
|
+
expiresAt,
|
|
339
|
+
now,
|
|
340
|
+
request.publicKey
|
|
341
|
+
).run();
|
|
342
|
+
|
|
343
|
+
if ((result.meta.changes || 0) === 0) {
|
|
344
|
+
throw new Error('Username already claimed by different public key');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
username: request.username,
|
|
349
|
+
publicKey: request.publicKey,
|
|
350
|
+
claimedAt: now,
|
|
351
|
+
expiresAt,
|
|
352
|
+
lastUsed: now,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async getUsername(username: string): Promise<Username | null> {
|
|
357
|
+
const result = await this.db.prepare(`
|
|
358
|
+
SELECT * FROM usernames
|
|
359
|
+
WHERE username = ? AND expires_at > ?
|
|
360
|
+
`).bind(username, Date.now()).first();
|
|
302
361
|
|
|
303
|
-
if (
|
|
304
|
-
|
|
362
|
+
if (!result) {
|
|
363
|
+
return null;
|
|
305
364
|
}
|
|
306
365
|
|
|
307
|
-
|
|
366
|
+
const row = result as any;
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
username: row.username,
|
|
370
|
+
publicKey: row.public_key,
|
|
371
|
+
claimedAt: row.claimed_at,
|
|
372
|
+
expiresAt: row.expires_at,
|
|
373
|
+
lastUsed: row.last_used,
|
|
374
|
+
metadata: row.metadata || undefined,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async touchUsername(username: string): Promise<boolean> {
|
|
379
|
+
const now = Date.now();
|
|
380
|
+
const expiresAt = now + YEAR_IN_MS;
|
|
381
|
+
|
|
382
|
+
const result = await this.db.prepare(`
|
|
383
|
+
UPDATE usernames
|
|
384
|
+
SET last_used = ?, expires_at = ?
|
|
385
|
+
WHERE username = ? AND expires_at > ?
|
|
386
|
+
`).bind(now, expiresAt, username, now).run();
|
|
387
|
+
|
|
388
|
+
return (result.meta.changes || 0) > 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async deleteExpiredUsernames(now: number): Promise<number> {
|
|
392
|
+
const result = await this.db.prepare(`
|
|
393
|
+
DELETE FROM usernames WHERE expires_at < ?
|
|
394
|
+
`).bind(now).run();
|
|
395
|
+
|
|
396
|
+
return result.meta.changes || 0;
|
|
308
397
|
}
|
|
309
398
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
399
|
+
// ===== Service Management =====
|
|
400
|
+
|
|
401
|
+
async createService(request: CreateServiceRequest): Promise<{
|
|
402
|
+
service: Service;
|
|
403
|
+
indexUuid: string;
|
|
313
404
|
}> {
|
|
405
|
+
const serviceId = randomUUID();
|
|
406
|
+
const indexUuid = randomUUID();
|
|
314
407
|
const now = Date.now();
|
|
315
408
|
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
409
|
+
// Insert service
|
|
410
|
+
await this.db.prepare(`
|
|
411
|
+
INSERT INTO services (id, username, service_fqn, offer_id, created_at, expires_at, is_public, metadata)
|
|
412
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
413
|
+
`).bind(
|
|
414
|
+
serviceId,
|
|
415
|
+
request.username,
|
|
416
|
+
request.serviceFqn,
|
|
417
|
+
request.offerId,
|
|
418
|
+
now,
|
|
419
|
+
request.expiresAt,
|
|
420
|
+
request.isPublic ? 1 : 0,
|
|
421
|
+
request.metadata || null
|
|
422
|
+
).run();
|
|
423
|
+
|
|
424
|
+
// Insert service index
|
|
425
|
+
await this.db.prepare(`
|
|
426
|
+
INSERT INTO service_index (uuid, service_id, username, service_fqn, created_at, expires_at)
|
|
427
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
428
|
+
`).bind(
|
|
429
|
+
indexUuid,
|
|
430
|
+
serviceId,
|
|
431
|
+
request.username,
|
|
432
|
+
request.serviceFqn,
|
|
433
|
+
now,
|
|
434
|
+
request.expiresAt
|
|
435
|
+
).run();
|
|
436
|
+
|
|
437
|
+
// Touch username to extend expiry
|
|
438
|
+
await this.touchUsername(request.username);
|
|
320
439
|
|
|
321
|
-
|
|
440
|
+
return {
|
|
441
|
+
service: {
|
|
442
|
+
id: serviceId,
|
|
443
|
+
username: request.username,
|
|
444
|
+
serviceFqn: request.serviceFqn,
|
|
445
|
+
offerId: request.offerId,
|
|
446
|
+
createdAt: now,
|
|
447
|
+
expiresAt: request.expiresAt,
|
|
448
|
+
isPublic: request.isPublic || false,
|
|
449
|
+
metadata: request.metadata,
|
|
450
|
+
},
|
|
451
|
+
indexUuid,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
322
454
|
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
SELECT
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
WHERE ${whereClause}
|
|
329
|
-
`;
|
|
455
|
+
async getServiceById(serviceId: string): Promise<Service | null> {
|
|
456
|
+
const result = await this.db.prepare(`
|
|
457
|
+
SELECT * FROM services
|
|
458
|
+
WHERE id = ? AND expires_at > ?
|
|
459
|
+
`).bind(serviceId, Date.now()).first();
|
|
330
460
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
SELECT
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
461
|
+
if (!result) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return this.rowToService(result as any);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async getServiceByUuid(uuid: string): Promise<Service | null> {
|
|
469
|
+
const result = await this.db.prepare(`
|
|
470
|
+
SELECT s.* FROM services s
|
|
471
|
+
INNER JOIN service_index si ON s.id = si.service_id
|
|
472
|
+
WHERE si.uuid = ? AND s.expires_at > ?
|
|
473
|
+
`).bind(uuid, Date.now()).first();
|
|
474
|
+
|
|
475
|
+
if (!result) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return this.rowToService(result as any);
|
|
480
|
+
}
|
|
350
481
|
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
482
|
+
async listServicesForUsername(username: string): Promise<ServiceInfo[]> {
|
|
483
|
+
const result = await this.db.prepare(`
|
|
484
|
+
SELECT si.uuid, s.is_public, s.service_fqn, s.metadata
|
|
485
|
+
FROM service_index si
|
|
486
|
+
INNER JOIN services s ON si.service_id = s.id
|
|
487
|
+
WHERE si.username = ? AND si.expires_at > ?
|
|
488
|
+
ORDER BY s.created_at DESC
|
|
489
|
+
`).bind(username, Date.now()).all();
|
|
490
|
+
|
|
491
|
+
if (!result.results) {
|
|
492
|
+
return [];
|
|
493
|
+
}
|
|
355
494
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
495
|
+
return result.results.map((row: any) => ({
|
|
496
|
+
uuid: row.uuid,
|
|
497
|
+
isPublic: row.is_public === 1,
|
|
498
|
+
serviceFqn: row.is_public === 1 ? row.service_fqn : undefined,
|
|
499
|
+
metadata: row.is_public === 1 ? row.metadata || undefined : undefined,
|
|
359
500
|
}));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async queryService(username: string, serviceFqn: string): Promise<string | null> {
|
|
504
|
+
const result = await this.db.prepare(`
|
|
505
|
+
SELECT si.uuid FROM service_index si
|
|
506
|
+
INNER JOIN services s ON si.service_id = s.id
|
|
507
|
+
WHERE si.username = ? AND si.service_fqn = ? AND si.expires_at > ?
|
|
508
|
+
`).bind(username, serviceFqn, Date.now()).first();
|
|
509
|
+
|
|
510
|
+
return result ? (result as any).uuid : null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async deleteService(serviceId: string, username: string): Promise<boolean> {
|
|
514
|
+
const result = await this.db.prepare(`
|
|
515
|
+
DELETE FROM services
|
|
516
|
+
WHERE id = ? AND username = ?
|
|
517
|
+
`).bind(serviceId, username).run();
|
|
518
|
+
|
|
519
|
+
return (result.meta.changes || 0) > 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async deleteExpiredServices(now: number): Promise<number> {
|
|
523
|
+
const result = await this.db.prepare(`
|
|
524
|
+
DELETE FROM services WHERE expires_at < ?
|
|
525
|
+
`).bind(now).run();
|
|
360
526
|
|
|
361
|
-
return
|
|
527
|
+
return result.meta.changes || 0;
|
|
362
528
|
}
|
|
363
529
|
|
|
364
530
|
async close(): Promise<void> {
|
|
@@ -366,22 +532,16 @@ export class D1Storage implements Storage {
|
|
|
366
532
|
// Connections are managed by the Cloudflare Workers runtime
|
|
367
533
|
}
|
|
368
534
|
|
|
535
|
+
// ===== Helper Methods =====
|
|
536
|
+
|
|
369
537
|
/**
|
|
370
|
-
* Helper method to convert database row to Offer object
|
|
538
|
+
* Helper method to convert database row to Offer object
|
|
371
539
|
*/
|
|
372
|
-
private
|
|
373
|
-
// Get topics for this offer
|
|
374
|
-
const topicResult = await this.db.prepare(`
|
|
375
|
-
SELECT topic FROM offer_topics WHERE offer_id = ?
|
|
376
|
-
`).bind(row.id).all();
|
|
377
|
-
|
|
378
|
-
const topics = topicResult.results?.map((t: any) => t.topic) || [];
|
|
379
|
-
|
|
540
|
+
private rowToOffer(row: any): Offer {
|
|
380
541
|
return {
|
|
381
542
|
id: row.id,
|
|
382
543
|
peerId: row.peer_id,
|
|
383
544
|
sdp: row.sdp,
|
|
384
|
-
topics,
|
|
385
545
|
createdAt: row.created_at,
|
|
386
546
|
expiresAt: row.expires_at,
|
|
387
547
|
lastSeen: row.last_seen,
|
|
@@ -391,4 +551,20 @@ export class D1Storage implements Storage {
|
|
|
391
551
|
answeredAt: row.answered_at || undefined,
|
|
392
552
|
};
|
|
393
553
|
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Helper method to convert database row to Service object
|
|
557
|
+
*/
|
|
558
|
+
private rowToService(row: any): Service {
|
|
559
|
+
return {
|
|
560
|
+
id: row.id,
|
|
561
|
+
username: row.username,
|
|
562
|
+
serviceFqn: row.service_fqn,
|
|
563
|
+
offerId: row.offer_id,
|
|
564
|
+
createdAt: row.created_at,
|
|
565
|
+
expiresAt: row.expires_at,
|
|
566
|
+
isPublic: row.is_public === 1,
|
|
567
|
+
metadata: row.metadata || undefined,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
394
570
|
}
|