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