@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/src/storage/d1.ts CHANGED
@@ -1,63 +1,65 @@
1
- // Use Web Crypto API (available globally in Cloudflare Workers)
2
1
  import {
3
2
  Storage,
4
3
  Offer,
5
4
  IceCandidate,
6
5
  CreateOfferRequest,
7
- Credential,
8
- GenerateCredentialsRequest,
9
6
  } from './types.ts';
10
7
  import { generateOfferHash } from './hash-id.ts';
11
8
 
12
- const YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000; // 365 days
13
-
14
9
  /**
15
10
  * D1 storage adapter for rondevu signaling system using Cloudflare D1
11
+ * Uses Ed25519 public key as identity (no usernames, no secrets)
16
12
  */
17
13
  export class D1Storage implements Storage {
18
14
  private db: D1Database;
19
- private masterEncryptionKey: string;
20
15
 
21
- /**
22
- * Creates a new D1 storage instance
23
- * @param db D1Database instance from Cloudflare Workers environment
24
- * @param masterEncryptionKey 64-char hex string for encrypting secrets (32 bytes)
25
- */
26
- constructor(db: D1Database, masterEncryptionKey: string) {
16
+ constructor(db: D1Database) {
27
17
  this.db = db;
28
- this.masterEncryptionKey = masterEncryptionKey;
29
18
  }
30
19
 
31
20
  /**
32
- * Initializes database schema with tags-based offers
21
+ * Initializes database schema for Ed25519 public key identity system
33
22
  * This should be run once during setup, not on every request
34
23
  */
35
24
  async initializeDatabase(): Promise<void> {
36
25
  await this.db.exec(`
26
+ -- Identities table (Ed25519 public key as identity)
27
+ CREATE TABLE IF NOT EXISTS identities (
28
+ public_key TEXT PRIMARY KEY,
29
+ created_at INTEGER NOT NULL,
30
+ expires_at INTEGER NOT NULL,
31
+ last_used INTEGER NOT NULL,
32
+ CHECK(length(public_key) = 64)
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS idx_identities_expires ON identities(expires_at);
36
+
37
37
  -- WebRTC signaling offers with tags
38
38
  CREATE TABLE IF NOT EXISTS offers (
39
39
  id TEXT PRIMARY KEY,
40
- username TEXT NOT NULL,
40
+ public_key TEXT NOT NULL,
41
41
  tags TEXT NOT NULL,
42
42
  sdp TEXT NOT NULL,
43
43
  created_at INTEGER NOT NULL,
44
44
  expires_at INTEGER NOT NULL,
45
45
  last_seen INTEGER NOT NULL,
46
- answerer_username TEXT,
46
+ answerer_public_key TEXT,
47
47
  answer_sdp TEXT,
48
- answered_at INTEGER
48
+ answered_at INTEGER,
49
+ matched_tags TEXT,
50
+ FOREIGN KEY (public_key) REFERENCES identities(public_key) ON DELETE CASCADE
49
51
  );
50
52
 
51
- CREATE INDEX IF NOT EXISTS idx_offers_username ON offers(username);
53
+ CREATE INDEX IF NOT EXISTS idx_offers_public_key ON offers(public_key);
52
54
  CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
53
55
  CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
54
- CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_username);
56
+ CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_public_key);
55
57
 
56
58
  -- ICE candidates table
57
59
  CREATE TABLE IF NOT EXISTS ice_candidates (
58
60
  id INTEGER PRIMARY KEY AUTOINCREMENT,
59
61
  offer_id TEXT NOT NULL,
60
- username TEXT NOT NULL,
62
+ public_key TEXT NOT NULL,
61
63
  role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
62
64
  candidate TEXT NOT NULL,
63
65
  created_at INTEGER NOT NULL,
@@ -65,22 +67,10 @@ export class D1Storage implements Storage {
65
67
  );
66
68
 
67
69
  CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
68
- CREATE INDEX IF NOT EXISTS idx_ice_username ON ice_candidates(username);
70
+ CREATE INDEX IF NOT EXISTS idx_ice_public_key ON ice_candidates(public_key);
71
+ CREATE INDEX IF NOT EXISTS idx_ice_role ON ice_candidates(role);
69
72
  CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
70
73
 
71
- -- Credentials table (replaces usernames with simpler name + secret auth)
72
- CREATE TABLE IF NOT EXISTS credentials (
73
- name TEXT PRIMARY KEY,
74
- secret TEXT NOT NULL UNIQUE,
75
- created_at INTEGER NOT NULL,
76
- expires_at INTEGER NOT NULL,
77
- last_used INTEGER NOT NULL,
78
- CHECK(length(name) >= 3 AND length(name) <= 32)
79
- );
80
-
81
- CREATE INDEX IF NOT EXISTS idx_credentials_expires ON credentials(expires_at);
82
- CREATE INDEX IF NOT EXISTS idx_credentials_secret ON credentials(secret);
83
-
84
74
  -- Rate limits table (for distributed rate limiting)
85
75
  CREATE TABLE IF NOT EXISTS rate_limits (
86
76
  identifier TEXT PRIMARY KEY,
@@ -105,19 +95,18 @@ export class D1Storage implements Storage {
105
95
  async createOffers(offers: CreateOfferRequest[]): Promise<Offer[]> {
106
96
  const created: Offer[] = [];
107
97
 
108
- // D1 doesn't support true transactions yet, so we do this sequentially
109
98
  for (const offer of offers) {
110
99
  const id = offer.id || await generateOfferHash(offer.sdp);
111
100
  const now = Date.now();
112
101
 
113
102
  await this.db.prepare(`
114
- INSERT INTO offers (id, username, tags, sdp, created_at, expires_at, last_seen)
103
+ INSERT INTO offers (id, public_key, tags, sdp, created_at, expires_at, last_seen)
115
104
  VALUES (?, ?, ?, ?, ?, ?, ?)
116
- `).bind(id, offer.username, JSON.stringify(offer.tags), offer.sdp, now, offer.expiresAt, now).run();
105
+ `).bind(id, offer.publicKey, JSON.stringify(offer.tags), offer.sdp, now, offer.expiresAt, now).run();
117
106
 
118
107
  created.push({
119
108
  id,
120
- username: offer.username,
109
+ publicKey: offer.publicKey,
121
110
  tags: offer.tags,
122
111
  sdp: offer.sdp,
123
112
  createdAt: now,
@@ -129,12 +118,12 @@ export class D1Storage implements Storage {
129
118
  return created;
130
119
  }
131
120
 
132
- async getOffersByUsername(username: string): Promise<Offer[]> {
121
+ async getOffersByPublicKey(publicKey: string): Promise<Offer[]> {
133
122
  const result = await this.db.prepare(`
134
123
  SELECT * FROM offers
135
- WHERE username = ? AND expires_at > ?
124
+ WHERE public_key = ? AND expires_at > ?
136
125
  ORDER BY last_seen DESC
137
- `).bind(username, Date.now()).all();
126
+ `).bind(publicKey, Date.now()).all();
138
127
 
139
128
  if (!result.results) {
140
129
  return [];
@@ -156,11 +145,11 @@ export class D1Storage implements Storage {
156
145
  return this.rowToOffer(result as any);
157
146
  }
158
147
 
159
- async deleteOffer(offerId: string, ownerUsername: string): Promise<boolean> {
148
+ async deleteOffer(offerId: string, ownerPublicKey: string): Promise<boolean> {
160
149
  const result = await this.db.prepare(`
161
150
  DELETE FROM offers
162
- WHERE id = ? AND username = ?
163
- `).bind(offerId, ownerUsername).run();
151
+ WHERE id = ? AND public_key = ?
152
+ `).bind(offerId, ownerPublicKey).run();
164
153
 
165
154
  return (result.meta.changes || 0) > 0;
166
155
  }
@@ -175,50 +164,40 @@ export class D1Storage implements Storage {
175
164
 
176
165
  async answerOffer(
177
166
  offerId: string,
178
- answererUsername: string,
179
- answerSdp: string
167
+ answererPublicKey: string,
168
+ answerSdp: string,
169
+ matchedTags?: string[]
180
170
  ): Promise<{ success: boolean; error?: string }> {
181
- // Check if offer exists and is not expired
182
171
  const offer = await this.getOfferById(offerId);
183
172
 
184
173
  if (!offer) {
185
- return {
186
- success: false,
187
- error: 'Offer not found or expired'
188
- };
174
+ return { success: false, error: 'Offer not found or expired' };
189
175
  }
190
176
 
191
- // Check if offer already has an answerer
192
- if (offer.answererUsername) {
193
- return {
194
- success: false,
195
- error: 'Offer already answered'
196
- };
177
+ if (offer.answererPublicKey) {
178
+ return { success: false, error: 'Offer already answered' };
197
179
  }
198
180
 
199
- // Update offer with answer
181
+ const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
200
182
  const result = await this.db.prepare(`
201
183
  UPDATE offers
202
- SET answerer_username = ?, answer_sdp = ?, answered_at = ?
203
- WHERE id = ? AND answerer_username IS NULL
204
- `).bind(answererUsername, answerSdp, Date.now(), offerId).run();
184
+ SET answerer_public_key = ?, answer_sdp = ?, answered_at = ?, matched_tags = ?
185
+ WHERE id = ? AND answerer_public_key IS NULL
186
+ `).bind(answererPublicKey, answerSdp, Date.now(), matchedTagsJson, offerId).run();
205
187
 
206
188
  if ((result.meta.changes || 0) === 0) {
207
- return {
208
- success: false,
209
- error: 'Offer already answered (race condition)'
210
- };
189
+ return { success: false, error: 'Offer already answered (race condition)' };
211
190
  }
212
191
 
213
192
  return { success: true };
214
193
  }
215
194
 
216
- async getAnsweredOffers(offererUsername: string): Promise<Offer[]> {
195
+ async getAnsweredOffers(offererPublicKey: string): Promise<Offer[]> {
217
196
  const result = await this.db.prepare(`
218
197
  SELECT * FROM offers
219
- WHERE username = ? AND answerer_username IS NOT NULL AND expires_at > ?
198
+ WHERE public_key = ? AND answerer_public_key IS NOT NULL AND expires_at > ?
220
199
  ORDER BY answered_at DESC
221
- `).bind(offererUsername, Date.now()).all();
200
+ `).bind(offererPublicKey, Date.now()).all();
222
201
 
223
202
  if (!result.results) {
224
203
  return [];
@@ -227,12 +206,12 @@ export class D1Storage implements Storage {
227
206
  return result.results.map(row => this.rowToOffer(row as any));
228
207
  }
229
208
 
230
- async getOffersAnsweredBy(answererUsername: string): Promise<Offer[]> {
209
+ async getOffersAnsweredBy(answererPublicKey: string): Promise<Offer[]> {
231
210
  const result = await this.db.prepare(`
232
211
  SELECT * FROM offers
233
- WHERE answerer_username = ? AND expires_at > ?
212
+ WHERE answerer_public_key = ? AND expires_at > ?
234
213
  ORDER BY answered_at DESC
235
- `).bind(answererUsername, Date.now()).all();
214
+ `).bind(answererPublicKey, Date.now()).all();
236
215
 
237
216
  if (!result.results) {
238
217
  return [];
@@ -245,7 +224,7 @@ export class D1Storage implements Storage {
245
224
 
246
225
  async discoverOffers(
247
226
  tags: string[],
248
- excludeUsername: string | null,
227
+ excludePublicKey: string | null,
249
228
  limit: number,
250
229
  offset: number
251
230
  ): Promise<Offer[]> {
@@ -253,22 +232,20 @@ export class D1Storage implements Storage {
253
232
  return [];
254
233
  }
255
234
 
256
- // Build query with JSON tag matching (OR logic)
257
- // D1/SQLite: Use json_each() to expand tags array and check if any tag matches
258
235
  const placeholders = tags.map(() => '?').join(',');
259
236
 
260
237
  let query = `
261
238
  SELECT DISTINCT o.* FROM offers o, json_each(o.tags) as t
262
239
  WHERE t.value IN (${placeholders})
263
240
  AND o.expires_at > ?
264
- AND o.answerer_username IS NULL
241
+ AND o.answerer_public_key IS NULL
265
242
  `;
266
243
 
267
244
  const params: any[] = [...tags, Date.now()];
268
245
 
269
- if (excludeUsername) {
270
- query += ' AND o.username != ?';
271
- params.push(excludeUsername);
246
+ if (excludePublicKey) {
247
+ query += ' AND o.public_key != ?';
248
+ params.push(excludePublicKey);
272
249
  }
273
250
 
274
251
  query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
@@ -285,27 +262,26 @@ export class D1Storage implements Storage {
285
262
 
286
263
  async getRandomOffer(
287
264
  tags: string[],
288
- excludeUsername: string | null
265
+ excludePublicKey: string | null
289
266
  ): Promise<Offer | null> {
290
267
  if (tags.length === 0) {
291
268
  return null;
292
269
  }
293
270
 
294
- // Build query with JSON tag matching (OR logic)
295
271
  const placeholders = tags.map(() => '?').join(',');
296
272
 
297
273
  let query = `
298
274
  SELECT DISTINCT o.* FROM offers o, json_each(o.tags) as t
299
275
  WHERE t.value IN (${placeholders})
300
276
  AND o.expires_at > ?
301
- AND o.answerer_username IS NULL
277
+ AND o.answerer_public_key IS NULL
302
278
  `;
303
279
 
304
280
  const params: any[] = [...tags, Date.now()];
305
281
 
306
- if (excludeUsername) {
307
- query += ' AND o.username != ?';
308
- params.push(excludeUsername);
282
+ if (excludePublicKey) {
283
+ query += ' AND o.public_key != ?';
284
+ params.push(excludePublicKey);
309
285
  }
310
286
 
311
287
  query += ' ORDER BY RANDOM() LIMIT 1';
@@ -319,19 +295,18 @@ export class D1Storage implements Storage {
319
295
 
320
296
  async addIceCandidates(
321
297
  offerId: string,
322
- username: string,
298
+ publicKey: string,
323
299
  role: 'offerer' | 'answerer',
324
300
  candidates: any[]
325
301
  ): Promise<number> {
326
- // D1 doesn't have transactions, so insert one by one
327
302
  for (let i = 0; i < candidates.length; i++) {
328
303
  const timestamp = Date.now() + i;
329
304
  await this.db.prepare(`
330
- INSERT INTO ice_candidates (offer_id, username, role, candidate, created_at)
305
+ INSERT INTO ice_candidates (offer_id, public_key, role, candidate, created_at)
331
306
  VALUES (?, ?, ?, ?, ?)
332
307
  `).bind(
333
308
  offerId,
334
- username,
309
+ publicKey,
335
310
  role,
336
311
  JSON.stringify(candidates[i]),
337
312
  timestamp
@@ -369,7 +344,7 @@ export class D1Storage implements Storage {
369
344
  return result.results.map((row: any) => ({
370
345
  id: row.id,
371
346
  offerId: row.offer_id,
372
- username: row.username,
347
+ publicKey: row.public_key,
373
348
  role: row.role,
374
349
  candidate: JSON.parse(row.candidate),
375
350
  createdAt: row.created_at,
@@ -378,41 +353,37 @@ export class D1Storage implements Storage {
378
353
 
379
354
  async getIceCandidatesForMultipleOffers(
380
355
  offerIds: string[],
381
- username: string,
356
+ publicKey: string,
382
357
  since?: number
383
358
  ): Promise<Map<string, IceCandidate[]>> {
384
359
  const result = new Map<string, IceCandidate[]>();
385
360
 
386
- // Return empty map if no offer IDs provided
387
361
  if (offerIds.length === 0) {
388
362
  return result;
389
363
  }
390
364
 
391
- // Validate array contains only strings
392
365
  if (!Array.isArray(offerIds) || !offerIds.every(id => typeof id === 'string')) {
393
366
  throw new Error('Invalid offer IDs: must be array of strings');
394
367
  }
395
368
 
396
- // Prevent DoS attacks from extremely large IN clauses
397
369
  if (offerIds.length > 1000) {
398
370
  throw new Error('Too many offer IDs (max 1000)');
399
371
  }
400
372
 
401
- // Build query that fetches candidates from the OTHER peer only
402
373
  const placeholders = offerIds.map(() => '?').join(',');
403
374
 
404
375
  let query = `
405
- SELECT ic.*, o.username as offer_username
376
+ SELECT ic.*, o.public_key as offer_public_key
406
377
  FROM ice_candidates ic
407
378
  INNER JOIN offers o ON o.id = ic.offer_id
408
379
  WHERE ic.offer_id IN (${placeholders})
409
380
  AND (
410
- (o.username = ? AND ic.role = 'answerer')
411
- OR (o.answerer_username = ? AND ic.role = 'offerer')
381
+ (o.public_key = ? AND ic.role = 'answerer')
382
+ OR (o.answerer_public_key = ? AND ic.role = 'offerer')
412
383
  )
413
384
  `;
414
385
 
415
- const params: any[] = [...offerIds, username, username];
386
+ const params: any[] = [...offerIds, publicKey, publicKey];
416
387
 
417
388
  if (since !== undefined) {
418
389
  query += ' AND ic.created_at > ?';
@@ -427,12 +398,11 @@ export class D1Storage implements Storage {
427
398
  return result;
428
399
  }
429
400
 
430
- // Group candidates by offer_id
431
401
  for (const row of queryResult.results as any[]) {
432
402
  const candidate: IceCandidate = {
433
403
  id: row.id,
434
404
  offerId: row.offer_id,
435
- username: row.username,
405
+ publicKey: row.public_key,
436
406
  role: row.role,
437
407
  candidate: JSON.parse(row.candidate),
438
408
  createdAt: row.created_at,
@@ -447,128 +417,12 @@ export class D1Storage implements Storage {
447
417
  return result;
448
418
  }
449
419
 
450
- // ===== Credential Management =====
451
-
452
- async generateCredentials(request: GenerateCredentialsRequest): Promise<Credential> {
453
- const now = Date.now();
454
- const expiresAt = request.expiresAt || (now + YEAR_IN_MS);
455
-
456
- const { generateCredentialName, generateSecret } = await import('../crypto.ts');
457
-
458
- let name: string;
459
-
460
- if (request.name) {
461
- // User requested specific username - check if available
462
- const existing = await this.db.prepare(`
463
- SELECT name FROM credentials WHERE name = ?
464
- `).bind(request.name).first();
465
-
466
- if (existing) {
467
- throw new Error('Username already taken');
468
- }
469
-
470
- name = request.name;
471
- } else {
472
- // Generate random name - retry until unique
473
- let attempts = 0;
474
- const maxAttempts = 100;
475
-
476
- while (attempts < maxAttempts) {
477
- name = generateCredentialName();
478
-
479
- const existing = await this.db.prepare(`
480
- SELECT name FROM credentials WHERE name = ?
481
- `).bind(name).first();
482
-
483
- if (!existing) {
484
- break;
485
- }
486
-
487
- attempts++;
488
- }
489
-
490
- if (attempts >= maxAttempts) {
491
- throw new Error(`Failed to generate unique credential name after ${maxAttempts} attempts`);
492
- }
493
- }
494
-
495
- const secret = generateSecret();
496
-
497
- // Encrypt secret before storing (AES-256-GCM)
498
- const { encryptSecret } = await import('../crypto.ts');
499
- const encryptedSecret = await encryptSecret(secret, this.masterEncryptionKey);
500
-
501
- // Insert credential with encrypted secret
502
- await this.db.prepare(`
503
- INSERT INTO credentials (name, secret, created_at, expires_at, last_used)
504
- VALUES (?, ?, ?, ?, ?)
505
- `).bind(name!, encryptedSecret, now, expiresAt, now).run();
506
-
507
- // Return plaintext secret to user (only time they'll see it)
508
- return {
509
- name: name!,
510
- secret, // Return plaintext secret, not encrypted
511
- createdAt: now,
512
- expiresAt,
513
- lastUsed: now,
514
- };
515
- }
516
-
517
- async getCredential(name: string): Promise<Credential | null> {
518
- const result = await this.db.prepare(`
519
- SELECT * FROM credentials
520
- WHERE name = ? AND expires_at > ?
521
- `).bind(name, Date.now()).first();
522
-
523
- if (!result) {
524
- return null;
525
- }
526
-
527
- const row = result as any;
528
-
529
- // Decrypt secret before returning
530
- // If decryption fails (e.g., master key rotated), treat as credential not found
531
- try {
532
- const { decryptSecret } = await import('../crypto.ts');
533
- const decryptedSecret = await decryptSecret(row.secret, this.masterEncryptionKey);
534
-
535
- return {
536
- name: row.name,
537
- secret: decryptedSecret, // Return decrypted secret
538
- createdAt: row.created_at,
539
- expiresAt: row.expires_at,
540
- lastUsed: row.last_used,
541
- };
542
- } catch (error) {
543
- console.error(`Failed to decrypt secret for credential '${name}':`, error);
544
- return null; // Treat as credential not found (fail-safe behavior)
545
- }
546
- }
547
-
548
- async updateCredentialUsage(name: string, lastUsed: number, expiresAt: number): Promise<void> {
549
- await this.db.prepare(`
550
- UPDATE credentials
551
- SET last_used = ?, expires_at = ?
552
- WHERE name = ?
553
- `).bind(lastUsed, expiresAt, name).run();
554
- }
555
-
556
- async deleteExpiredCredentials(now: number): Promise<number> {
557
- const result = await this.db.prepare(`
558
- DELETE FROM credentials WHERE expires_at < ?
559
- `).bind(now).run();
560
-
561
- return result.meta.changes || 0;
562
- }
563
-
564
420
  // ===== Rate Limiting =====
565
421
 
566
422
  async checkRateLimit(identifier: string, limit: number, windowMs: number): Promise<boolean> {
567
423
  const now = Date.now();
568
424
  const resetTime = now + windowMs;
569
425
 
570
- // Atomic UPSERT: Insert or increment count, reset if expired
571
- // This prevents TOCTOU race conditions by doing check+increment in single operation
572
426
  const result = await this.db.prepare(`
573
427
  INSERT INTO rate_limits (identifier, count, reset_time)
574
428
  VALUES (?, 1, ?)
@@ -584,7 +438,6 @@ export class D1Storage implements Storage {
584
438
  RETURNING count
585
439
  `).bind(identifier, resetTime, now, now, resetTime).first() as { count: number } | null;
586
440
 
587
- // Check if limit exceeded
588
441
  return result ? result.count <= limit : false;
589
442
  }
590
443
 
@@ -600,21 +453,17 @@ export class D1Storage implements Storage {
600
453
 
601
454
  async checkAndMarkNonce(nonceKey: string, expiresAt: number): Promise<boolean> {
602
455
  try {
603
- // Atomic INSERT - if nonce already exists, this will fail with UNIQUE constraint
604
- // This prevents replay attacks by ensuring each nonce is only used once
605
456
  const result = await this.db.prepare(`
606
457
  INSERT INTO nonces (nonce_key, expires_at)
607
458
  VALUES (?, ?)
608
459
  `).bind(nonceKey, expiresAt).run();
609
460
 
610
- // D1 returns success=true if insert succeeded
611
461
  return result.success;
612
462
  } catch (error: any) {
613
- // UNIQUE constraint violation means nonce already used (replay attack)
614
463
  if (error?.message?.includes('UNIQUE constraint failed')) {
615
464
  return false;
616
465
  }
617
- throw error; // Other errors should propagate
466
+ throw error;
618
467
  }
619
468
  }
620
469
 
@@ -628,7 +477,6 @@ export class D1Storage implements Storage {
628
477
 
629
478
  async close(): Promise<void> {
630
479
  // D1 doesn't require explicit connection closing
631
- // Connections are managed by the Cloudflare Workers runtime
632
480
  }
633
481
 
634
482
  // ===== Count Methods (for resource limits) =====
@@ -638,18 +486,13 @@ export class D1Storage implements Storage {
638
486
  return result?.count ?? 0;
639
487
  }
640
488
 
641
- async getOfferCountByUsername(username: string): Promise<number> {
642
- const result = await this.db.prepare('SELECT COUNT(*) as count FROM offers WHERE username = ?')
643
- .bind(username)
489
+ async getOfferCountByPublicKey(publicKey: string): Promise<number> {
490
+ const result = await this.db.prepare('SELECT COUNT(*) as count FROM offers WHERE public_key = ?')
491
+ .bind(publicKey)
644
492
  .first() as { count: number } | null;
645
493
  return result?.count ?? 0;
646
494
  }
647
495
 
648
- async getCredentialCount(): Promise<number> {
649
- const result = await this.db.prepare('SELECT COUNT(*) as count FROM credentials').first() as { count: number } | null;
650
- return result?.count ?? 0;
651
- }
652
-
653
496
  async getIceCandidateCount(offerId: string): Promise<number> {
654
497
  const result = await this.db.prepare('SELECT COUNT(*) as count FROM ice_candidates WHERE offer_id = ?')
655
498
  .bind(offerId)
@@ -659,21 +502,19 @@ export class D1Storage implements Storage {
659
502
 
660
503
  // ===== Helper Methods =====
661
504
 
662
- /**
663
- * Helper method to convert database row to Offer object
664
- */
665
505
  private rowToOffer(row: any): Offer {
666
506
  return {
667
507
  id: row.id,
668
- username: row.username,
508
+ publicKey: row.public_key,
669
509
  tags: JSON.parse(row.tags),
670
510
  sdp: row.sdp,
671
511
  createdAt: row.created_at,
672
512
  expiresAt: row.expires_at,
673
513
  lastSeen: row.last_seen,
674
- answererUsername: row.answerer_username || undefined,
514
+ answererPublicKey: row.answerer_public_key || undefined,
675
515
  answerSdp: row.answer_sdp || undefined,
676
516
  answeredAt: row.answered_at || undefined,
517
+ matchedTags: row.matched_tags ? JSON.parse(row.matched_tags) : undefined,
677
518
  };
678
519
  }
679
520
  }
@@ -10,8 +10,6 @@ export type StorageType = 'memory' | 'sqlite' | 'mysql' | 'postgres';
10
10
  */
11
11
  export interface StorageConfig {
12
12
  type: StorageType;
13
- /** Master encryption key for secrets (64-char hex string) */
14
- masterEncryptionKey: string;
15
13
  /** SQLite database path (default: ':memory:') */
16
14
  sqlitePath?: string;
17
15
  /** Connection string for MySQL/PostgreSQL */
@@ -28,15 +26,12 @@ export async function createStorage(config: StorageConfig): Promise<Storage> {
28
26
  switch (config.type) {
29
27
  case 'memory': {
30
28
  const { MemoryStorage } = await import('./memory.ts');
31
- return new MemoryStorage(config.masterEncryptionKey);
29
+ return new MemoryStorage();
32
30
  }
33
31
 
34
32
  case 'sqlite': {
35
33
  const { SQLiteStorage } = await import('./sqlite.ts');
36
- return new SQLiteStorage(
37
- config.sqlitePath || ':memory:',
38
- config.masterEncryptionKey
39
- );
34
+ return new SQLiteStorage(config.sqlitePath || ':memory:');
40
35
  }
41
36
 
42
37
  case 'mysql': {
@@ -44,11 +39,7 @@ export async function createStorage(config: StorageConfig): Promise<Storage> {
44
39
  throw new Error('MySQL storage requires DATABASE_URL connection string');
45
40
  }
46
41
  const { MySQLStorage } = await import('./mysql.ts');
47
- return MySQLStorage.create(
48
- config.connectionString,
49
- config.masterEncryptionKey,
50
- config.poolSize || 10
51
- );
42
+ return MySQLStorage.create(config.connectionString, config.poolSize || 10);
52
43
  }
53
44
 
54
45
  case 'postgres': {
@@ -56,11 +47,7 @@ export async function createStorage(config: StorageConfig): Promise<Storage> {
56
47
  throw new Error('PostgreSQL storage requires DATABASE_URL connection string');
57
48
  }
58
49
  const { PostgreSQLStorage } = await import('./postgres.ts');
59
- return PostgreSQLStorage.create(
60
- config.connectionString,
61
- config.masterEncryptionKey,
62
- config.poolSize || 10
63
- );
50
+ return PostgreSQLStorage.create(config.connectionString, config.poolSize || 10);
64
51
  }
65
52
 
66
53
  default: