@xtr-dev/rondevu-server 0.3.0 → 0.5.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/src/storage/d1.ts CHANGED
@@ -8,9 +8,9 @@ import {
8
8
  ClaimUsernameRequest,
9
9
  Service,
10
10
  CreateServiceRequest,
11
- ServiceInfo,
12
11
  } from './types.ts';
13
12
  import { generateOfferHash } from './hash-id.ts';
13
+ import { parseServiceFqn } from '../crypto.ts';
14
14
 
15
15
  const YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000; // 365 days
16
16
 
@@ -37,27 +37,28 @@ export class D1Storage implements Storage {
37
37
  -- WebRTC signaling offers
38
38
  CREATE TABLE IF NOT EXISTS offers (
39
39
  id TEXT PRIMARY KEY,
40
- peer_id TEXT NOT NULL,
40
+ username TEXT NOT NULL,
41
+ service_id TEXT,
41
42
  sdp TEXT NOT NULL,
42
43
  created_at INTEGER NOT NULL,
43
44
  expires_at INTEGER NOT NULL,
44
45
  last_seen INTEGER NOT NULL,
45
- secret TEXT,
46
- answerer_peer_id TEXT,
46
+ answerer_username TEXT,
47
47
  answer_sdp TEXT,
48
48
  answered_at INTEGER
49
49
  );
50
50
 
51
- CREATE INDEX IF NOT EXISTS idx_offers_peer ON offers(peer_id);
51
+ CREATE INDEX IF NOT EXISTS idx_offers_username ON offers(username);
52
+ CREATE INDEX IF NOT EXISTS idx_offers_service ON offers(service_id);
52
53
  CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
53
54
  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_peer_id);
55
+ CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_username);
55
56
 
56
57
  -- ICE candidates table
57
58
  CREATE TABLE IF NOT EXISTS ice_candidates (
58
59
  id INTEGER PRIMARY KEY AUTOINCREMENT,
59
60
  offer_id TEXT NOT NULL,
60
- peer_id TEXT NOT NULL,
61
+ username TEXT NOT NULL,
61
62
  role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
62
63
  candidate TEXT NOT NULL,
63
64
  created_at INTEGER NOT NULL,
@@ -65,7 +66,7 @@ export class D1Storage implements Storage {
65
66
  );
66
67
 
67
68
  CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
68
- CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);
69
+ CREATE INDEX IF NOT EXISTS idx_ice_username ON ice_candidates(username);
69
70
  CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
70
71
 
71
72
  -- Usernames table
@@ -82,39 +83,23 @@ export class D1Storage implements Storage {
82
83
  CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
83
84
  CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
84
85
 
85
- -- Services table
86
+ -- Services table (new schema with extracted fields for discovery)
86
87
  CREATE TABLE IF NOT EXISTS services (
87
88
  id TEXT PRIMARY KEY,
88
- username TEXT NOT NULL,
89
89
  service_fqn TEXT NOT NULL,
90
- offer_id TEXT NOT NULL,
90
+ service_name TEXT NOT NULL,
91
+ version TEXT NOT NULL,
92
+ username TEXT NOT NULL,
91
93
  created_at INTEGER NOT NULL,
92
94
  expires_at INTEGER NOT NULL,
93
- is_public INTEGER NOT NULL DEFAULT 0,
94
- metadata TEXT,
95
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)
96
+ UNIQUE(service_fqn)
98
97
  );
99
98
 
100
- CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
101
99
  CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
100
+ CREATE INDEX IF NOT EXISTS idx_services_discovery ON services(service_name, version);
101
+ CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
102
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);
118
103
  `);
119
104
  }
120
105
 
@@ -129,30 +114,31 @@ export class D1Storage implements Storage {
129
114
  const now = Date.now();
130
115
 
131
116
  await this.db.prepare(`
132
- INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
117
+ INSERT INTO offers (id, username, service_id, sdp, created_at, expires_at, last_seen)
133
118
  VALUES (?, ?, ?, ?, ?, ?, ?)
134
- `).bind(id, offer.peerId, offer.sdp, now, offer.expiresAt, now, offer.secret || null).run();
119
+ `).bind(id, offer.username, offer.serviceId || null, offer.sdp, now, offer.expiresAt, now).run();
135
120
 
136
121
  created.push({
137
122
  id,
138
- peerId: offer.peerId,
123
+ username: offer.username,
124
+ serviceId: offer.serviceId,
125
+ serviceFqn: offer.serviceFqn,
139
126
  sdp: offer.sdp,
140
127
  createdAt: now,
141
128
  expiresAt: offer.expiresAt,
142
129
  lastSeen: now,
143
- secret: offer.secret,
144
130
  });
145
131
  }
146
132
 
147
133
  return created;
148
134
  }
149
135
 
150
- async getOffersByPeerId(peerId: string): Promise<Offer[]> {
136
+ async getOffersByUsername(username: string): Promise<Offer[]> {
151
137
  const result = await this.db.prepare(`
152
138
  SELECT * FROM offers
153
- WHERE peer_id = ? AND expires_at > ?
139
+ WHERE username = ? AND expires_at > ?
154
140
  ORDER BY last_seen DESC
155
- `).bind(peerId, Date.now()).all();
141
+ `).bind(username, Date.now()).all();
156
142
 
157
143
  if (!result.results) {
158
144
  return [];
@@ -174,11 +160,11 @@ export class D1Storage implements Storage {
174
160
  return this.rowToOffer(result as any);
175
161
  }
176
162
 
177
- async deleteOffer(offerId: string, ownerPeerId: string): Promise<boolean> {
163
+ async deleteOffer(offerId: string, ownerUsername: string): Promise<boolean> {
178
164
  const result = await this.db.prepare(`
179
165
  DELETE FROM offers
180
- WHERE id = ? AND peer_id = ?
181
- `).bind(offerId, ownerPeerId).run();
166
+ WHERE id = ? AND username = ?
167
+ `).bind(offerId, ownerUsername).run();
182
168
 
183
169
  return (result.meta.changes || 0) > 0;
184
170
  }
@@ -193,9 +179,8 @@ export class D1Storage implements Storage {
193
179
 
194
180
  async answerOffer(
195
181
  offerId: string,
196
- answererPeerId: string,
197
- answerSdp: string,
198
- secret?: string
182
+ answererUsername: string,
183
+ answerSdp: string
199
184
  ): Promise<{ success: boolean; error?: string }> {
200
185
  // Check if offer exists and is not expired
201
186
  const offer = await this.getOfferById(offerId);
@@ -207,16 +192,8 @@ export class D1Storage implements Storage {
207
192
  };
208
193
  }
209
194
 
210
- // Verify secret if offer is protected
211
- if (offer.secret && offer.secret !== secret) {
212
- return {
213
- success: false,
214
- error: 'Invalid or missing secret'
215
- };
216
- }
217
-
218
195
  // Check if offer already has an answerer
219
- if (offer.answererPeerId) {
196
+ if (offer.answererUsername) {
220
197
  return {
221
198
  success: false,
222
199
  error: 'Offer already answered'
@@ -226,9 +203,9 @@ export class D1Storage implements Storage {
226
203
  // Update offer with answer
227
204
  const result = await this.db.prepare(`
228
205
  UPDATE offers
229
- SET answerer_peer_id = ?, answer_sdp = ?, answered_at = ?
230
- WHERE id = ? AND answerer_peer_id IS NULL
231
- `).bind(answererPeerId, answerSdp, Date.now(), offerId).run();
206
+ SET answerer_username = ?, answer_sdp = ?, answered_at = ?
207
+ WHERE id = ? AND answerer_username IS NULL
208
+ `).bind(answererUsername, answerSdp, Date.now(), offerId).run();
232
209
 
233
210
  if ((result.meta.changes || 0) === 0) {
234
211
  return {
@@ -240,12 +217,12 @@ export class D1Storage implements Storage {
240
217
  return { success: true };
241
218
  }
242
219
 
243
- async getAnsweredOffers(offererPeerId: string): Promise<Offer[]> {
220
+ async getAnsweredOffers(offererUsername: string): Promise<Offer[]> {
244
221
  const result = await this.db.prepare(`
245
222
  SELECT * FROM offers
246
- WHERE peer_id = ? AND answerer_peer_id IS NOT NULL AND expires_at > ?
223
+ WHERE username = ? AND answerer_username IS NOT NULL AND expires_at > ?
247
224
  ORDER BY answered_at DESC
248
- `).bind(offererPeerId, Date.now()).all();
225
+ `).bind(offererUsername, Date.now()).all();
249
226
 
250
227
  if (!result.results) {
251
228
  return [];
@@ -258,7 +235,7 @@ export class D1Storage implements Storage {
258
235
 
259
236
  async addIceCandidates(
260
237
  offerId: string,
261
- peerId: string,
238
+ username: string,
262
239
  role: 'offerer' | 'answerer',
263
240
  candidates: any[]
264
241
  ): Promise<number> {
@@ -266,11 +243,11 @@ export class D1Storage implements Storage {
266
243
  for (let i = 0; i < candidates.length; i++) {
267
244
  const timestamp = Date.now() + i;
268
245
  await this.db.prepare(`
269
- INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)
246
+ INSERT INTO ice_candidates (offer_id, username, role, candidate, created_at)
270
247
  VALUES (?, ?, ?, ?, ?)
271
248
  `).bind(
272
249
  offerId,
273
- peerId,
250
+ username,
274
251
  role,
275
252
  JSON.stringify(candidates[i]),
276
253
  timestamp
@@ -308,7 +285,7 @@ export class D1Storage implements Storage {
308
285
  return result.results.map((row: any) => ({
309
286
  id: row.id,
310
287
  offerId: row.offer_id,
311
- peerId: row.peer_id,
288
+ username: row.username,
312
289
  role: row.role,
313
290
  candidate: JSON.parse(row.candidate),
314
291
  createdAt: row.created_at,
@@ -321,36 +298,44 @@ export class D1Storage implements Storage {
321
298
  const now = Date.now();
322
299
  const expiresAt = now + YEAR_IN_MS;
323
300
 
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();
301
+ try {
302
+ // Try to insert or update
303
+ const result = await this.db.prepare(`
304
+ INSERT INTO usernames (username, public_key, claimed_at, expires_at, last_used, metadata)
305
+ VALUES (?, ?, ?, ?, ?, NULL)
306
+ ON CONFLICT(username) DO UPDATE SET
307
+ expires_at = ?,
308
+ last_used = ?
309
+ WHERE public_key = ?
310
+ `).bind(
311
+ request.username,
312
+ request.publicKey,
313
+ now,
314
+ expiresAt,
315
+ now,
316
+ expiresAt,
317
+ now,
318
+ request.publicKey
319
+ ).run();
342
320
 
343
- if ((result.meta.changes || 0) === 0) {
344
- throw new Error('Username already claimed by different public key');
345
- }
321
+ if ((result.meta.changes || 0) === 0) {
322
+ throw new Error('Username already claimed by different public key');
323
+ }
346
324
 
347
- return {
348
- username: request.username,
349
- publicKey: request.publicKey,
350
- claimedAt: now,
351
- expiresAt,
352
- lastUsed: now,
353
- };
325
+ return {
326
+ username: request.username,
327
+ publicKey: request.publicKey,
328
+ claimedAt: now,
329
+ expiresAt,
330
+ lastUsed: now,
331
+ };
332
+ } catch (err: any) {
333
+ // Handle UNIQUE constraint on public_key
334
+ if (err.message?.includes('UNIQUE constraint failed: usernames.public_key')) {
335
+ throw new Error('This public key has already claimed a different username');
336
+ }
337
+ throw err;
338
+ }
354
339
  }
355
340
 
356
341
  async getUsername(username: string): Promise<Username | null> {
@@ -375,18 +360,6 @@ export class D1Storage implements Storage {
375
360
  };
376
361
  }
377
362
 
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
363
 
391
364
  async deleteExpiredUsernames(now: number): Promise<number> {
392
365
  const result = await this.db.prepare(`
@@ -400,36 +373,51 @@ export class D1Storage implements Storage {
400
373
 
401
374
  async createService(request: CreateServiceRequest): Promise<{
402
375
  service: Service;
403
- indexUuid: string;
404
376
  offers: Offer[];
405
377
  }> {
406
378
  const serviceId = crypto.randomUUID();
407
- const indexUuid = crypto.randomUUID();
408
379
  const now = Date.now();
409
380
 
410
- // Insert service
411
- await this.db.prepare(`
412
- INSERT INTO services (id, username, service_fqn, created_at, expires_at, is_public, metadata)
413
- VALUES (?, ?, ?, ?, ?, ?, ?)
414
- `).bind(
415
- serviceId,
416
- request.username,
417
- request.serviceFqn,
418
- now,
419
- request.expiresAt,
420
- request.isPublic ? 1 : 0,
421
- request.metadata || null
422
- ).run();
381
+ // Parse FQN to extract components
382
+ const parsed = parseServiceFqn(request.serviceFqn);
383
+ if (!parsed) {
384
+ throw new Error(`Invalid service FQN: ${request.serviceFqn}`);
385
+ }
386
+ if (!parsed.username) {
387
+ throw new Error(`Service FQN must include username: ${request.serviceFqn}`);
388
+ }
389
+
390
+ const { serviceName, version, username } = parsed;
391
+
392
+ // Delete existing service with same (service_name, version, username) and its related offers (upsert behavior)
393
+ // First get the existing service
394
+ const existingService = await this.db.prepare(`
395
+ SELECT id FROM services
396
+ WHERE service_name = ? AND version = ? AND username = ?
397
+ `).bind(serviceName, version, username).first();
398
+
399
+ if (existingService) {
400
+ // Delete related offers first (no FK cascade from offers to services)
401
+ await this.db.prepare(`
402
+ DELETE FROM offers WHERE service_id = ?
403
+ `).bind(existingService.id).run();
404
+
405
+ // Delete the service
406
+ await this.db.prepare(`
407
+ DELETE FROM services WHERE id = ?
408
+ `).bind(existingService.id).run();
409
+ }
423
410
 
424
- // Insert service index
411
+ // Insert new service with extracted fields
425
412
  await this.db.prepare(`
426
- INSERT INTO service_index (uuid, service_id, username, service_fqn, created_at, expires_at)
427
- VALUES (?, ?, ?, ?, ?, ?)
413
+ INSERT INTO services (id, service_fqn, service_name, version, username, created_at, expires_at)
414
+ VALUES (?, ?, ?, ?, ?, ?, ?)
428
415
  `).bind(
429
- indexUuid,
430
416
  serviceId,
431
- request.username,
432
417
  request.serviceFqn,
418
+ serviceName,
419
+ version,
420
+ username,
433
421
  now,
434
422
  request.expiresAt
435
423
  ).run();
@@ -441,36 +429,28 @@ export class D1Storage implements Storage {
441
429
  }));
442
430
  const offers = await this.createOffers(offerRequests);
443
431
 
444
- // Touch username to extend expiry
445
- await this.touchUsername(request.username);
432
+ // Touch username to extend expiry (inline logic)
433
+ const expiresAt = now + YEAR_IN_MS;
434
+ await this.db.prepare(`
435
+ UPDATE usernames
436
+ SET last_used = ?, expires_at = ?
437
+ WHERE username = ? AND expires_at > ?
438
+ `).bind(now, expiresAt, username, now).run();
446
439
 
447
440
  return {
448
441
  service: {
449
442
  id: serviceId,
450
- username: request.username,
451
443
  serviceFqn: request.serviceFqn,
444
+ serviceName,
445
+ version,
446
+ username,
452
447
  createdAt: now,
453
448
  expiresAt: request.expiresAt,
454
- isPublic: request.isPublic || false,
455
- metadata: request.metadata,
456
449
  },
457
- indexUuid,
458
450
  offers,
459
451
  };
460
452
  }
461
453
 
462
- async batchCreateServices(requests: CreateServiceRequest[]): Promise<Array<{
463
- service: Service;
464
- indexUuid: string;
465
- offers: Offer[];
466
- }>> {
467
- const results = [];
468
- for (const request of requests) {
469
- const result = await this.createService(request);
470
- results.push(result);
471
- }
472
- return results;
473
- }
474
454
 
475
455
  async getOffersForService(serviceId: string): Promise<Offer[]> {
476
456
  const result = await this.db.prepare(`
@@ -499,12 +479,11 @@ export class D1Storage implements Storage {
499
479
  return this.rowToService(result as any);
500
480
  }
501
481
 
502
- async getServiceByUuid(uuid: string): Promise<Service | null> {
482
+ async getServiceByFqn(serviceFqn: string): Promise<Service | null> {
503
483
  const result = await this.db.prepare(`
504
- SELECT s.* FROM services s
505
- INNER JOIN service_index si ON s.id = si.service_id
506
- WHERE si.uuid = ? AND s.expires_at > ?
507
- `).bind(uuid, Date.now()).first();
484
+ SELECT * FROM services
485
+ WHERE service_fqn = ? AND expires_at > ?
486
+ `).bind(serviceFqn, Date.now()).first();
508
487
 
509
488
  if (!result) {
510
489
  return null;
@@ -513,49 +492,56 @@ export class D1Storage implements Storage {
513
492
  return this.rowToService(result as any);
514
493
  }
515
494
 
516
- async listServicesForUsername(username: string): Promise<ServiceInfo[]> {
495
+
496
+
497
+
498
+
499
+ async discoverServices(
500
+ serviceName: string,
501
+ version: string,
502
+ limit: number,
503
+ offset: number
504
+ ): Promise<Service[]> {
505
+ // Query for unique services with available offers
506
+ // We join with offers and filter for available ones (answerer_username IS NULL)
517
507
  const result = await this.db.prepare(`
518
- SELECT si.uuid, s.is_public, s.service_fqn, s.metadata
519
- FROM service_index si
520
- INNER JOIN services s ON si.service_id = s.id
521
- WHERE si.username = ? AND si.expires_at > ?
508
+ SELECT DISTINCT s.* FROM services s
509
+ INNER JOIN offers o ON o.service_id = s.id
510
+ WHERE s.service_name = ?
511
+ AND s.version = ?
512
+ AND s.expires_at > ?
513
+ AND o.answerer_username IS NULL
514
+ AND o.expires_at > ?
522
515
  ORDER BY s.created_at DESC
523
- `).bind(username, Date.now()).all();
516
+ LIMIT ? OFFSET ?
517
+ `).bind(serviceName, version, Date.now(), Date.now(), limit, offset).all();
524
518
 
525
519
  if (!result.results) {
526
520
  return [];
527
521
  }
528
522
 
529
- return result.results.map((row: any) => ({
530
- uuid: row.uuid,
531
- isPublic: row.is_public === 1,
532
- serviceFqn: row.is_public === 1 ? row.service_fqn : undefined,
533
- metadata: row.is_public === 1 ? row.metadata || undefined : undefined,
534
- }));
535
- }
536
-
537
- async queryService(username: string, serviceFqn: string): Promise<string | null> {
538
- const result = await this.db.prepare(`
539
- SELECT si.uuid FROM service_index si
540
- INNER JOIN services s ON si.service_id = s.id
541
- WHERE si.username = ? AND si.service_fqn = ? AND si.expires_at > ?
542
- `).bind(username, serviceFqn, Date.now()).first();
543
-
544
- return result ? (result as any).uuid : null;
523
+ return result.results.map(row => this.rowToService(row as any));
545
524
  }
546
525
 
547
- async findServicesByName(username: string, serviceName: string): Promise<Service[]> {
526
+ async getRandomService(serviceName: string, version: string): Promise<Service | null> {
527
+ // Get a random service with an available offer
548
528
  const result = await this.db.prepare(`
549
- SELECT * FROM services
550
- WHERE username = ? AND service_fqn LIKE ? AND expires_at > ?
551
- ORDER BY created_at DESC
552
- `).bind(username, `${serviceName}@%`, Date.now()).all();
529
+ SELECT s.* FROM services s
530
+ INNER JOIN offers o ON o.service_id = s.id
531
+ WHERE s.service_name = ?
532
+ AND s.version = ?
533
+ AND s.expires_at > ?
534
+ AND o.answerer_username IS NULL
535
+ AND o.expires_at > ?
536
+ ORDER BY RANDOM()
537
+ LIMIT 1
538
+ `).bind(serviceName, version, Date.now(), Date.now()).first();
553
539
 
554
- if (!result.results) {
555
- return [];
540
+ if (!result) {
541
+ return null;
556
542
  }
557
543
 
558
- return result.results.map(row => this.rowToService(row as any));
544
+ return this.rowToService(result as any);
559
545
  }
560
546
 
561
547
  async deleteService(serviceId: string, username: string): Promise<boolean> {
@@ -588,13 +574,14 @@ export class D1Storage implements Storage {
588
574
  private rowToOffer(row: any): Offer {
589
575
  return {
590
576
  id: row.id,
591
- peerId: row.peer_id,
577
+ username: row.username,
578
+ serviceId: row.service_id || undefined,
579
+ serviceFqn: row.service_fqn || undefined,
592
580
  sdp: row.sdp,
593
581
  createdAt: row.created_at,
594
582
  expiresAt: row.expires_at,
595
583
  lastSeen: row.last_seen,
596
- secret: row.secret || undefined,
597
- answererPeerId: row.answerer_peer_id || undefined,
584
+ answererUsername: row.answerer_username || undefined,
598
585
  answerSdp: row.answer_sdp || undefined,
599
586
  answeredAt: row.answered_at || undefined,
600
587
  };
@@ -606,12 +593,12 @@ export class D1Storage implements Storage {
606
593
  private rowToService(row: any): Service {
607
594
  return {
608
595
  id: row.id,
609
- username: row.username,
610
596
  serviceFqn: row.service_fqn,
597
+ serviceName: row.service_name,
598
+ version: row.version,
599
+ username: row.username,
611
600
  createdAt: row.created_at,
612
601
  expiresAt: row.expires_at,
613
- isPublic: row.is_public === 1,
614
- metadata: row.metadata || undefined,
615
602
  };
616
603
  }
617
604
  }