@xtr-dev/rondevu-server 0.2.4 → 0.4.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/app.ts CHANGED
@@ -3,11 +3,12 @@ import { cors } from 'hono/cors';
3
3
  import { Storage } from './storage/types.ts';
4
4
  import { Config } from './config.ts';
5
5
  import { createAuthMiddleware, getAuthenticatedPeerId } from './middleware/auth.ts';
6
- import { generatePeerId, encryptPeerId, validateUsernameClaim, validateServicePublish, validateServiceFqn } from './crypto.ts';
6
+ import { generatePeerId, encryptPeerId, validateUsernameClaim, validateServicePublish, validateServiceFqn, parseServiceFqn, isVersionCompatible } from './crypto.ts';
7
7
  import type { Context } from 'hono';
8
8
 
9
9
  /**
10
10
  * Creates the Hono application with username and service-based WebRTC signaling
11
+ * RESTful API design - v0.11.0
11
12
  */
12
13
  export function createApp(storage: Storage, config: Config) {
13
14
  const app = new Hono();
@@ -61,7 +62,7 @@ export function createApp(storage: Storage, config: Config) {
61
62
 
62
63
  /**
63
64
  * POST /register
64
- * Register a new peer (still needed for peer ID generation)
65
+ * Register a new peer
65
66
  */
66
67
  app.post('/register', async (c) => {
67
68
  try {
@@ -78,19 +79,50 @@ export function createApp(storage: Storage, config: Config) {
78
79
  }
79
80
  });
80
81
 
81
- // ===== Username Management =====
82
+ // ===== User Management (RESTful) =====
82
83
 
83
84
  /**
84
- * POST /usernames/claim
85
+ * GET /users/:username
86
+ * Check if username is available or get claim info
87
+ */
88
+ app.get('/users/:username', async (c) => {
89
+ try {
90
+ const username = c.req.param('username');
91
+
92
+ const claimed = await storage.getUsername(username);
93
+
94
+ if (!claimed) {
95
+ return c.json({
96
+ username,
97
+ available: true
98
+ }, 200);
99
+ }
100
+
101
+ return c.json({
102
+ username: claimed.username,
103
+ available: false,
104
+ claimedAt: claimed.claimedAt,
105
+ expiresAt: claimed.expiresAt,
106
+ publicKey: claimed.publicKey
107
+ }, 200);
108
+ } catch (err) {
109
+ console.error('Error checking username:', err);
110
+ return c.json({ error: 'Internal server error' }, 500);
111
+ }
112
+ });
113
+
114
+ /**
115
+ * POST /users/:username
85
116
  * Claim a username with cryptographic proof
86
117
  */
87
- app.post('/usernames/claim', async (c) => {
118
+ app.post('/users/:username', async (c) => {
88
119
  try {
120
+ const username = c.req.param('username');
89
121
  const body = await c.req.json();
90
- const { username, publicKey, signature, message } = body;
122
+ const { publicKey, signature, message } = body;
91
123
 
92
- if (!username || !publicKey || !signature || !message) {
93
- return c.json({ error: 'Missing required parameters: username, publicKey, signature, message' }, 400);
124
+ if (!publicKey || !signature || !message) {
125
+ return c.json({ error: 'Missing required parameters: publicKey, signature, message' }, 400);
94
126
  }
95
127
 
96
128
  // Validate claim
@@ -112,7 +144,7 @@ export function createApp(storage: Storage, config: Config) {
112
144
  username: claimed.username,
113
145
  claimedAt: claimed.claimedAt,
114
146
  expiresAt: claimed.expiresAt
115
- }, 200);
147
+ }, 201);
116
148
  } catch (err: any) {
117
149
  if (err.message?.includes('already claimed')) {
118
150
  return c.json({ error: 'Username already claimed by different public key' }, 409);
@@ -126,68 +158,104 @@ export function createApp(storage: Storage, config: Config) {
126
158
  });
127
159
 
128
160
  /**
129
- * GET /usernames/:username
130
- * Check if username is available or get claim info
161
+ * GET /users/:username/services/:fqn
162
+ * Get service by username and FQN with semver-compatible matching
131
163
  */
132
- app.get('/usernames/:username', async (c) => {
164
+ app.get('/users/:username/services/:fqn', async (c) => {
133
165
  try {
134
166
  const username = c.req.param('username');
167
+ const serviceFqn = decodeURIComponent(c.req.param('fqn'));
135
168
 
136
- const claimed = await storage.getUsername(username);
169
+ // Parse the requested FQN
170
+ const parsed = parseServiceFqn(serviceFqn);
171
+ if (!parsed) {
172
+ return c.json({ error: 'Invalid service FQN format' }, 400);
173
+ }
137
174
 
138
- if (!claimed) {
175
+ const { serviceName, version: requestedVersion } = parsed;
176
+
177
+ // Find all services with matching service name
178
+ const matchingServices = await storage.findServicesByName(username, serviceName);
179
+
180
+ if (matchingServices.length === 0) {
181
+ return c.json({ error: 'Service not found' }, 404);
182
+ }
183
+
184
+ // Filter to compatible versions
185
+ const compatibleServices = matchingServices.filter(service => {
186
+ const serviceParsed = parseServiceFqn(service.serviceFqn);
187
+ if (!serviceParsed) return false;
188
+ return isVersionCompatible(requestedVersion, serviceParsed.version);
189
+ });
190
+
191
+ if (compatibleServices.length === 0) {
139
192
  return c.json({
140
- username,
141
- available: true
142
- }, 200);
193
+ error: 'No compatible version found',
194
+ message: `Requested ${serviceFqn}, but no compatible versions available`
195
+ }, 404);
143
196
  }
144
197
 
145
- return c.json({
146
- username: claimed.username,
147
- available: false,
148
- claimedAt: claimed.claimedAt,
149
- expiresAt: claimed.expiresAt,
150
- publicKey: claimed.publicKey
151
- }, 200);
152
- } catch (err) {
153
- console.error('Error checking username:', err);
154
- return c.json({ error: 'Internal server error' }, 500);
155
- }
156
- });
198
+ // Use the first compatible service (most recently created)
199
+ const service = compatibleServices[0];
157
200
 
158
- /**
159
- * GET /usernames/:username/services
160
- * List services for a username (privacy-preserving)
161
- */
162
- app.get('/usernames/:username/services', async (c) => {
163
- try {
164
- const username = c.req.param('username');
201
+ // Get the UUID for this service
202
+ const uuid = await storage.queryService(username, service.serviceFqn);
203
+
204
+ if (!uuid) {
205
+ return c.json({ error: 'Service index not found' }, 500);
206
+ }
165
207
 
166
- const services = await storage.listServicesForUsername(username);
208
+ // Get all offers for this service
209
+ const serviceOffers = await storage.getOffersForService(service.id);
210
+
211
+ if (serviceOffers.length === 0) {
212
+ return c.json({ error: 'No offers found for this service' }, 404);
213
+ }
214
+
215
+ // Find an unanswered offer
216
+ const availableOffer = serviceOffers.find(offer => !offer.answererPeerId);
217
+
218
+ if (!availableOffer) {
219
+ return c.json({
220
+ error: 'No available offers',
221
+ message: 'All offers from this service are currently in use. Please try again later.'
222
+ }, 503);
223
+ }
167
224
 
168
225
  return c.json({
169
- username,
170
- services
226
+ uuid: uuid,
227
+ serviceId: service.id,
228
+ username: service.username,
229
+ serviceFqn: service.serviceFqn,
230
+ offerId: availableOffer.id,
231
+ sdp: availableOffer.sdp,
232
+ isPublic: service.isPublic,
233
+ metadata: service.metadata ? JSON.parse(service.metadata) : undefined,
234
+ createdAt: service.createdAt,
235
+ expiresAt: service.expiresAt
171
236
  }, 200);
172
237
  } catch (err) {
173
- console.error('Error listing services:', err);
238
+ console.error('Error getting service:', err);
174
239
  return c.json({ error: 'Internal server error' }, 500);
175
240
  }
176
241
  });
177
242
 
178
- // ===== Service Management =====
179
-
180
243
  /**
181
- * POST /services
182
- * Publish a service
244
+ * POST /users/:username/services
245
+ * Publish a service with one or more offers (RESTful endpoint)
183
246
  */
184
- app.post('/services', authMiddleware, async (c) => {
247
+ app.post('/users/:username/services', authMiddleware, async (c) => {
248
+ let serviceFqn: string | undefined;
249
+ let createdOffers: any[] = [];
250
+
185
251
  try {
252
+ const username = c.req.param('username');
186
253
  const body = await c.req.json();
187
- const { username, serviceFqn, sdp, ttl, isPublic, metadata, signature, message } = body;
254
+ serviceFqn = body.serviceFqn;
255
+ const { offers, ttl, isPublic, metadata, signature, message } = body;
188
256
 
189
- if (!username || !serviceFqn || !sdp) {
190
- return c.json({ error: 'Missing required parameters: username, serviceFqn, sdp' }, 400);
257
+ if (!serviceFqn || !offers || !Array.isArray(offers) || offers.length === 0) {
258
+ return c.json({ error: 'Missing required parameters: serviceFqn, offers (must be non-empty array)' }, 400);
191
259
  }
192
260
 
193
261
  // Validate service FQN
@@ -212,13 +280,24 @@ export function createApp(storage: Storage, config: Config) {
212
280
  return c.json({ error: 'Invalid signature for username' }, 403);
213
281
  }
214
282
 
215
- // Validate SDP
216
- if (typeof sdp !== 'string' || sdp.length === 0) {
217
- return c.json({ error: 'Invalid SDP' }, 400);
283
+ // Delete existing service if one exists (upsert behavior)
284
+ const existingUuid = await storage.queryService(username, serviceFqn);
285
+ if (existingUuid) {
286
+ const existingService = await storage.getServiceByUuid(existingUuid);
287
+ if (existingService) {
288
+ await storage.deleteService(existingService.id, username);
289
+ }
218
290
  }
219
291
 
220
- if (sdp.length > 64 * 1024) {
221
- return c.json({ error: 'SDP too large (max 64KB)' }, 400);
292
+ // Validate all offers
293
+ for (const offer of offers) {
294
+ if (!offer.sdp || typeof offer.sdp !== 'string' || offer.sdp.length === 0) {
295
+ return c.json({ error: 'Invalid SDP in offers array' }, 400);
296
+ }
297
+
298
+ if (offer.sdp.length > 64 * 1024) {
299
+ return c.json({ error: 'SDP too large (max 64KB)' }, 400);
300
+ }
222
301
  }
223
302
 
224
303
  // Calculate expiry
@@ -229,94 +308,79 @@ export function createApp(storage: Storage, config: Config) {
229
308
  );
230
309
  const expiresAt = Date.now() + offerTtl;
231
310
 
232
- // Create offer first
233
- const offers = await storage.createOffers([{
311
+ // Prepare offer requests
312
+ const offerRequests = offers.map(offer => ({
234
313
  peerId,
235
- sdp,
314
+ sdp: offer.sdp,
236
315
  expiresAt
237
- }]);
316
+ }));
238
317
 
239
- if (offers.length === 0) {
240
- return c.json({ error: 'Failed to create offer' }, 500);
241
- }
242
-
243
- const offer = offers[0];
244
-
245
- // Create service
318
+ // Create service with offers
246
319
  const result = await storage.createService({
247
320
  username,
248
321
  serviceFqn,
249
- offerId: offer.id,
250
322
  expiresAt,
251
323
  isPublic: isPublic || false,
252
- metadata: metadata ? JSON.stringify(metadata) : undefined
324
+ metadata: metadata ? JSON.stringify(metadata) : undefined,
325
+ offers: offerRequests
253
326
  });
254
327
 
328
+ createdOffers = result.offers;
329
+
330
+ // Return full service details with all offers
255
331
  return c.json({
256
- serviceId: result.service.id,
257
332
  uuid: result.indexUuid,
258
- offerId: offer.id,
333
+ serviceFqn: serviceFqn,
334
+ username: username,
335
+ serviceId: result.service.id,
336
+ offers: result.offers.map(o => ({
337
+ offerId: o.id,
338
+ sdp: o.sdp,
339
+ createdAt: o.createdAt,
340
+ expiresAt: o.expiresAt
341
+ })),
342
+ isPublic: result.service.isPublic,
343
+ metadata: metadata,
344
+ createdAt: result.service.createdAt,
259
345
  expiresAt: result.service.expiresAt
260
346
  }, 201);
261
347
  } catch (err) {
262
348
  console.error('Error creating service:', err);
263
- return c.json({ error: 'Internal server error' }, 500);
349
+ console.error('Error details:', {
350
+ message: (err as Error).message,
351
+ stack: (err as Error).stack,
352
+ username: c.req.param('username'),
353
+ serviceFqn,
354
+ offerIds: createdOffers.map(o => o.id)
355
+ });
356
+ return c.json({
357
+ error: 'Internal server error',
358
+ details: (err as Error).message
359
+ }, 500);
264
360
  }
265
361
  });
266
362
 
267
363
  /**
268
- * GET /services/:uuid
269
- * Get service details by index UUID
364
+ * DELETE /users/:username/services/:fqn
365
+ * Delete a service by username and FQN (RESTful)
270
366
  */
271
- app.get('/services/:uuid', async (c) => {
367
+ app.delete('/users/:username/services/:fqn', authMiddleware, async (c) => {
272
368
  try {
273
- const uuid = c.req.param('uuid');
274
-
275
- const service = await storage.getServiceByUuid(uuid);
369
+ const username = c.req.param('username');
370
+ const serviceFqn = decodeURIComponent(c.req.param('fqn'));
276
371
 
277
- if (!service) {
372
+ // Find service by username and FQN
373
+ const uuid = await storage.queryService(username, serviceFqn);
374
+ if (!uuid) {
278
375
  return c.json({ error: 'Service not found' }, 404);
279
376
  }
280
377
 
281
- // Get associated offer
282
- const offer = await storage.getOfferById(service.offerId);
283
-
284
- if (!offer) {
285
- return c.json({ error: 'Associated offer not found' }, 404);
286
- }
287
-
288
- return c.json({
289
- serviceId: service.id,
290
- username: service.username,
291
- serviceFqn: service.serviceFqn,
292
- offerId: service.offerId,
293
- sdp: offer.sdp,
294
- isPublic: service.isPublic,
295
- metadata: service.metadata ? JSON.parse(service.metadata) : undefined,
296
- createdAt: service.createdAt,
297
- expiresAt: service.expiresAt
298
- }, 200);
299
- } catch (err) {
300
- console.error('Error getting service:', err);
301
- return c.json({ error: 'Internal server error' }, 500);
302
- }
303
- });
304
-
305
- /**
306
- * DELETE /services/:serviceId
307
- * Delete a service (requires ownership)
308
- */
309
- app.delete('/services/:serviceId', authMiddleware, async (c) => {
310
- try {
311
- const serviceId = c.req.param('serviceId');
312
- const body = await c.req.json();
313
- const { username } = body;
314
-
315
- if (!username) {
316
- return c.json({ error: 'Missing required parameter: username' }, 400);
378
+ const service = await storage.getServiceByUuid(uuid);
379
+ if (!service) {
380
+ return c.json({ error: 'Service not found' }, 404);
317
381
  }
318
382
 
319
- const deleted = await storage.deleteService(serviceId, username);
383
+ const deleted = await storage.deleteService(service.id, username);
320
384
 
321
385
  if (!deleted) {
322
386
  return c.json({ error: 'Service not found or not owned by this username' }, 404);
@@ -329,157 +393,68 @@ export function createApp(storage: Storage, config: Config) {
329
393
  }
330
394
  });
331
395
 
396
+ // ===== Service Management (Legacy - for UUID-based access) =====
397
+
332
398
  /**
333
- * POST /index/:username/query
334
- * Query service by FQN (returns UUID)
399
+ * GET /services/:uuid
400
+ * Get service details by index UUID (kept for privacy)
335
401
  */
336
- app.post('/index/:username/query', async (c) => {
402
+ app.get('/services/:uuid', async (c) => {
337
403
  try {
338
- const username = c.req.param('username');
339
- const body = await c.req.json();
340
- const { serviceFqn } = body;
341
-
342
- if (!serviceFqn) {
343
- return c.json({ error: 'Missing required parameter: serviceFqn' }, 400);
344
- }
404
+ const uuid = c.req.param('uuid');
345
405
 
346
- const uuid = await storage.queryService(username, serviceFqn);
406
+ const service = await storage.getServiceByUuid(uuid);
347
407
 
348
- if (!uuid) {
408
+ if (!service) {
349
409
  return c.json({ error: 'Service not found' }, 404);
350
410
  }
351
411
 
352
- return c.json({
353
- uuid,
354
- allowed: true
355
- }, 200);
356
- } catch (err) {
357
- console.error('Error querying service:', err);
358
- return c.json({ error: 'Internal server error' }, 500);
359
- }
360
- });
361
-
362
- // ===== Offer Management (Core WebRTC) =====
412
+ // Get all offers for this service
413
+ const serviceOffers = await storage.getOffersForService(service.id);
363
414
 
364
- /**
365
- * POST /offers
366
- * Create offers (direct, no service - for testing/advanced users)
367
- */
368
- app.post('/offers', authMiddleware, async (c) => {
369
- try {
370
- const body = await c.req.json();
371
- const { offers } = body;
372
-
373
- if (!Array.isArray(offers) || offers.length === 0) {
374
- return c.json({ error: 'Missing or invalid required parameter: offers (must be non-empty array)' }, 400);
375
- }
376
-
377
- if (offers.length > config.maxOffersPerRequest) {
378
- return c.json({ error: `Too many offers (max ${config.maxOffersPerRequest})` }, 400);
415
+ if (serviceOffers.length === 0) {
416
+ return c.json({ error: 'No offers found for this service' }, 404);
379
417
  }
380
418
 
381
- const peerId = getAuthenticatedPeerId(c);
382
-
383
- // Validate and prepare offers
384
- const validated = offers.map((offer: any) => {
385
- const { sdp, ttl, secret } = offer;
386
-
387
- if (typeof sdp !== 'string' || sdp.length === 0) {
388
- throw new Error('Invalid SDP in offer');
389
- }
390
-
391
- if (sdp.length > 64 * 1024) {
392
- throw new Error('SDP too large (max 64KB)');
393
- }
394
-
395
- const offerTtl = Math.min(
396
- Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
397
- config.offerMaxTtl
398
- );
419
+ // Find an unanswered offer
420
+ const availableOffer = serviceOffers.find(offer => !offer.answererPeerId);
399
421
 
400
- return {
401
- peerId,
402
- sdp,
403
- expiresAt: Date.now() + offerTtl,
404
- secret: secret ? String(secret).substring(0, 128) : undefined
405
- };
406
- });
407
-
408
- const created = await storage.createOffers(validated);
409
-
410
- return c.json({
411
- offers: created.map(offer => ({
412
- id: offer.id,
413
- peerId: offer.peerId,
414
- expiresAt: offer.expiresAt,
415
- createdAt: offer.createdAt,
416
- hasSecret: !!offer.secret
417
- }))
418
- }, 201);
419
- } catch (err: any) {
420
- console.error('Error creating offers:', err);
421
- return c.json({ error: err.message || 'Internal server error' }, 500);
422
- }
423
- });
424
-
425
- /**
426
- * GET /offers/mine
427
- * Get authenticated peer's offers
428
- */
429
- app.get('/offers/mine', authMiddleware, async (c) => {
430
- try {
431
- const peerId = getAuthenticatedPeerId(c);
432
- const offers = await storage.getOffersByPeerId(peerId);
422
+ if (!availableOffer) {
423
+ return c.json({
424
+ error: 'No available offers',
425
+ message: 'All offers from this service are currently in use. Please try again later.'
426
+ }, 503);
427
+ }
433
428
 
434
429
  return c.json({
435
- offers: offers.map(offer => ({
436
- id: offer.id,
437
- sdp: offer.sdp,
438
- createdAt: offer.createdAt,
439
- expiresAt: offer.expiresAt,
440
- lastSeen: offer.lastSeen,
441
- hasSecret: !!offer.secret,
442
- answererPeerId: offer.answererPeerId,
443
- answered: !!offer.answererPeerId
444
- }))
430
+ uuid: uuid,
431
+ serviceId: service.id,
432
+ username: service.username,
433
+ serviceFqn: service.serviceFqn,
434
+ offerId: availableOffer.id,
435
+ sdp: availableOffer.sdp,
436
+ isPublic: service.isPublic,
437
+ metadata: service.metadata ? JSON.parse(service.metadata) : undefined,
438
+ createdAt: service.createdAt,
439
+ expiresAt: service.expiresAt
445
440
  }, 200);
446
441
  } catch (err) {
447
- console.error('Error getting offers:', err);
442
+ console.error('Error getting service:', err);
448
443
  return c.json({ error: 'Internal server error' }, 500);
449
444
  }
450
445
  });
451
446
 
452
- /**
453
- * DELETE /offers/:offerId
454
- * Delete an offer
455
- */
456
- app.delete('/offers/:offerId', authMiddleware, async (c) => {
457
- try {
458
- const offerId = c.req.param('offerId');
459
- const peerId = getAuthenticatedPeerId(c);
460
-
461
- const deleted = await storage.deleteOffer(offerId, peerId);
462
-
463
- if (!deleted) {
464
- return c.json({ error: 'Offer not found or not owned by this peer' }, 404);
465
- }
466
-
467
- return c.json({ success: true }, 200);
468
- } catch (err) {
469
- console.error('Error deleting offer:', err);
470
- return c.json({ error: 'Internal server error' }, 500);
471
- }
472
- });
447
+ // ===== Service-Based WebRTC Signaling =====
473
448
 
474
449
  /**
475
- * POST /offers/:offerId/answer
476
- * Answer an offer
450
+ * POST /services/:uuid/answer
451
+ * Answer a service offer
477
452
  */
478
- app.post('/offers/:offerId/answer', authMiddleware, async (c) => {
453
+ app.post('/services/:uuid/answer', authMiddleware, async (c) => {
479
454
  try {
480
- const offerId = c.req.param('offerId');
455
+ const uuid = c.req.param('uuid');
481
456
  const body = await c.req.json();
482
- const { sdp, secret } = body;
457
+ const { sdp } = body;
483
458
 
484
459
  if (!sdp) {
485
460
  return c.json({ error: 'Missing required parameter: sdp' }, 400);
@@ -493,55 +468,82 @@ export function createApp(storage: Storage, config: Config) {
493
468
  return c.json({ error: 'SDP too large (max 64KB)' }, 400);
494
469
  }
495
470
 
471
+ // Get the service by UUID
472
+ const service = await storage.getServiceByUuid(uuid);
473
+ if (!service) {
474
+ return c.json({ error: 'Service not found' }, 404);
475
+ }
476
+
477
+ // Get available offer from service
478
+ const serviceOffers = await storage.getOffersForService(service.id);
479
+ const availableOffer = serviceOffers.find(offer => !offer.answererPeerId);
480
+
481
+ if (!availableOffer) {
482
+ return c.json({ error: 'No available offers' }, 503);
483
+ }
484
+
496
485
  const answererPeerId = getAuthenticatedPeerId(c);
497
486
 
498
- const result = await storage.answerOffer(offerId, answererPeerId, sdp, secret);
487
+ const result = await storage.answerOffer(availableOffer.id, answererPeerId, sdp);
499
488
 
500
489
  if (!result.success) {
501
490
  return c.json({ error: result.error }, 400);
502
491
  }
503
492
 
504
- return c.json({ success: true }, 200);
493
+ return c.json({
494
+ success: true,
495
+ offerId: availableOffer.id
496
+ }, 200);
505
497
  } catch (err) {
506
- console.error('Error answering offer:', err);
498
+ console.error('Error answering service:', err);
507
499
  return c.json({ error: 'Internal server error' }, 500);
508
500
  }
509
501
  });
510
502
 
511
503
  /**
512
- * GET /offers/answers
513
- * Get answers for authenticated peer's offers
504
+ * GET /services/:uuid/answer
505
+ * Get answer for a service (offerer polls this)
514
506
  */
515
- app.get('/offers/answers', authMiddleware, async (c) => {
507
+ app.get('/services/:uuid/answer', authMiddleware, async (c) => {
516
508
  try {
509
+ const uuid = c.req.param('uuid');
517
510
  const peerId = getAuthenticatedPeerId(c);
518
- const offers = await storage.getAnsweredOffers(peerId);
511
+
512
+ // Get the service by UUID
513
+ const service = await storage.getServiceByUuid(uuid);
514
+ if (!service) {
515
+ return c.json({ error: 'Service not found' }, 404);
516
+ }
517
+
518
+ // Get offers for this service owned by the requesting peer
519
+ const serviceOffers = await storage.getOffersForService(service.id);
520
+ const myOffer = serviceOffers.find(offer => offer.peerId === peerId && offer.answererPeerId);
521
+
522
+ if (!myOffer || !myOffer.answerSdp) {
523
+ return c.json({ error: 'Offer not yet answered' }, 404);
524
+ }
519
525
 
520
526
  return c.json({
521
- answers: offers.map(offer => ({
522
- offerId: offer.id,
523
- answererPeerId: offer.answererPeerId,
524
- answerSdp: offer.answerSdp,
525
- answeredAt: offer.answeredAt
526
- }))
527
+ offerId: myOffer.id,
528
+ answererId: myOffer.answererPeerId,
529
+ sdp: myOffer.answerSdp,
530
+ answeredAt: myOffer.answeredAt
527
531
  }, 200);
528
532
  } catch (err) {
529
- console.error('Error getting answers:', err);
533
+ console.error('Error getting service answer:', err);
530
534
  return c.json({ error: 'Internal server error' }, 500);
531
535
  }
532
536
  });
533
537
 
534
- // ===== ICE Candidate Exchange =====
535
-
536
538
  /**
537
- * POST /offers/:offerId/ice-candidates
538
- * Add ICE candidates for an offer
539
+ * POST /services/:uuid/ice-candidates
540
+ * Add ICE candidates for a service
539
541
  */
540
- app.post('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {
542
+ app.post('/services/:uuid/ice-candidates', authMiddleware, async (c) => {
541
543
  try {
542
- const offerId = c.req.param('offerId');
544
+ const uuid = c.req.param('uuid');
543
545
  const body = await c.req.json();
544
- const { candidates } = body;
546
+ const { candidates, offerId } = body;
545
547
 
546
548
  if (!Array.isArray(candidates) || candidates.length === 0) {
547
549
  return c.json({ error: 'Missing or invalid required parameter: candidates' }, 400);
@@ -549,8 +551,27 @@ export function createApp(storage: Storage, config: Config) {
549
551
 
550
552
  const peerId = getAuthenticatedPeerId(c);
551
553
 
554
+ // Get the service by UUID
555
+ const service = await storage.getServiceByUuid(uuid);
556
+ if (!service) {
557
+ return c.json({ error: 'Service not found' }, 404);
558
+ }
559
+
560
+ // If offerId is provided, use it; otherwise find the peer's offer
561
+ let targetOfferId = offerId;
562
+ if (!targetOfferId) {
563
+ const serviceOffers = await storage.getOffersForService(service.id);
564
+ const myOffer = serviceOffers.find(offer =>
565
+ offer.peerId === peerId || offer.answererPeerId === peerId
566
+ );
567
+ if (!myOffer) {
568
+ return c.json({ error: 'No offer found for this peer' }, 404);
569
+ }
570
+ targetOfferId = myOffer.id;
571
+ }
572
+
552
573
  // Get offer to determine role
553
- const offer = await storage.getOfferById(offerId);
574
+ const offer = await storage.getOfferById(targetOfferId);
554
575
  if (!offer) {
555
576
  return c.json({ error: 'Offer not found' }, 404);
556
577
  }
@@ -558,27 +579,47 @@ export function createApp(storage: Storage, config: Config) {
558
579
  // Determine role
559
580
  const role = offer.peerId === peerId ? 'offerer' : 'answerer';
560
581
 
561
- const count = await storage.addIceCandidates(offerId, peerId, role, candidates);
582
+ const count = await storage.addIceCandidates(targetOfferId, peerId, role, candidates);
562
583
 
563
- return c.json({ count }, 200);
584
+ return c.json({ count, offerId: targetOfferId }, 200);
564
585
  } catch (err) {
565
- console.error('Error adding ICE candidates:', err);
586
+ console.error('Error adding ICE candidates to service:', err);
566
587
  return c.json({ error: 'Internal server error' }, 500);
567
588
  }
568
589
  });
569
590
 
570
591
  /**
571
- * GET /offers/:offerId/ice-candidates
572
- * Get ICE candidates for an offer
592
+ * GET /services/:uuid/ice-candidates
593
+ * Get ICE candidates for a service
573
594
  */
574
- app.get('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {
595
+ app.get('/services/:uuid/ice-candidates', authMiddleware, async (c) => {
575
596
  try {
576
- const offerId = c.req.param('offerId');
597
+ const uuid = c.req.param('uuid');
577
598
  const since = c.req.query('since');
599
+ const offerId = c.req.query('offerId');
578
600
  const peerId = getAuthenticatedPeerId(c);
579
601
 
602
+ // Get the service by UUID
603
+ const service = await storage.getServiceByUuid(uuid);
604
+ if (!service) {
605
+ return c.json({ error: 'Service not found' }, 404);
606
+ }
607
+
608
+ // If offerId is provided, use it; otherwise find the peer's offer
609
+ let targetOfferId = offerId;
610
+ if (!targetOfferId) {
611
+ const serviceOffers = await storage.getOffersForService(service.id);
612
+ const myOffer = serviceOffers.find(offer =>
613
+ offer.peerId === peerId || offer.answererPeerId === peerId
614
+ );
615
+ if (!myOffer) {
616
+ return c.json({ error: 'No offer found for this peer' }, 404);
617
+ }
618
+ targetOfferId = myOffer.id;
619
+ }
620
+
580
621
  // Get offer to determine role
581
- const offer = await storage.getOfferById(offerId);
622
+ const offer = await storage.getOfferById(targetOfferId);
582
623
  if (!offer) {
583
624
  return c.json({ error: 'Offer not found' }, 404);
584
625
  }
@@ -587,16 +628,17 @@ export function createApp(storage: Storage, config: Config) {
587
628
  const targetRole = offer.peerId === peerId ? 'answerer' : 'offerer';
588
629
  const sinceTimestamp = since ? parseInt(since, 10) : undefined;
589
630
 
590
- const candidates = await storage.getIceCandidates(offerId, targetRole, sinceTimestamp);
631
+ const candidates = await storage.getIceCandidates(targetOfferId, targetRole, sinceTimestamp);
591
632
 
592
633
  return c.json({
593
634
  candidates: candidates.map(c => ({
594
635
  candidate: c.candidate,
595
636
  createdAt: c.createdAt
596
- }))
637
+ })),
638
+ offerId: targetOfferId
597
639
  }, 200);
598
640
  } catch (err) {
599
- console.error('Error getting ICE candidates:', err);
641
+ console.error('Error getting ICE candidates for service:', err);
600
642
  return c.json({ error: 'Internal server error' }, 500);
601
643
  }
602
644
  });