@xtr-dev/rondevu-server 0.3.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-server",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "DNS-like WebRTC signaling server with username claiming and service discovery",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
package/src/app.ts CHANGED
@@ -444,156 +444,17 @@ export function createApp(storage: Storage, config: Config) {
444
444
  }
445
445
  });
446
446
 
447
- // ===== Offer Management (Core WebRTC) =====
447
+ // ===== Service-Based WebRTC Signaling =====
448
448
 
449
449
  /**
450
- * POST /offers
451
- * Create offers (direct, no service - for testing/advanced users)
450
+ * POST /services/:uuid/answer
451
+ * Answer a service offer
452
452
  */
453
- app.post('/offers', authMiddleware, async (c) => {
453
+ app.post('/services/:uuid/answer', authMiddleware, async (c) => {
454
454
  try {
455
+ const uuid = c.req.param('uuid');
455
456
  const body = await c.req.json();
456
- const { offers } = body;
457
-
458
- if (!Array.isArray(offers) || offers.length === 0) {
459
- return c.json({ error: 'Missing or invalid required parameter: offers (must be non-empty array)' }, 400);
460
- }
461
-
462
- if (offers.length > config.maxOffersPerRequest) {
463
- return c.json({ error: `Too many offers (max ${config.maxOffersPerRequest})` }, 400);
464
- }
465
-
466
- const peerId = getAuthenticatedPeerId(c);
467
-
468
- // Validate and prepare offers
469
- const validated = offers.map((offer: any) => {
470
- const { sdp, ttl, secret } = offer;
471
-
472
- if (typeof sdp !== 'string' || sdp.length === 0) {
473
- throw new Error('Invalid SDP in offer');
474
- }
475
-
476
- if (sdp.length > 64 * 1024) {
477
- throw new Error('SDP too large (max 64KB)');
478
- }
479
-
480
- const offerTtl = Math.min(
481
- Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
482
- config.offerMaxTtl
483
- );
484
-
485
- return {
486
- peerId,
487
- sdp,
488
- expiresAt: Date.now() + offerTtl,
489
- secret: secret ? String(secret).substring(0, 128) : undefined
490
- };
491
- });
492
-
493
- const created = await storage.createOffers(validated);
494
-
495
- return c.json({
496
- offers: created.map(offer => ({
497
- id: offer.id,
498
- peerId: offer.peerId,
499
- expiresAt: offer.expiresAt,
500
- createdAt: offer.createdAt,
501
- hasSecret: !!offer.secret
502
- }))
503
- }, 201);
504
- } catch (err: any) {
505
- console.error('Error creating offers:', err);
506
- return c.json({ error: err.message || 'Internal server error' }, 500);
507
- }
508
- });
509
-
510
- /**
511
- * GET /offers/mine
512
- * Get authenticated peer's offers
513
- */
514
- app.get('/offers/mine', authMiddleware, async (c) => {
515
- try {
516
- const peerId = getAuthenticatedPeerId(c);
517
- const offers = await storage.getOffersByPeerId(peerId);
518
-
519
- return c.json({
520
- offers: offers.map(offer => ({
521
- id: offer.id,
522
- sdp: offer.sdp,
523
- createdAt: offer.createdAt,
524
- expiresAt: offer.expiresAt,
525
- lastSeen: offer.lastSeen,
526
- hasSecret: !!offer.secret,
527
- answererPeerId: offer.answererPeerId,
528
- answered: !!offer.answererPeerId
529
- }))
530
- }, 200);
531
- } catch (err) {
532
- console.error('Error getting offers:', err);
533
- return c.json({ error: 'Internal server error' }, 500);
534
- }
535
- });
536
-
537
- /**
538
- * GET /offers/:offerId
539
- * Get offer details (added for completeness)
540
- */
541
- app.get('/offers/:offerId', authMiddleware, async (c) => {
542
- try {
543
- const offerId = c.req.param('offerId');
544
- const offer = await storage.getOfferById(offerId);
545
-
546
- if (!offer) {
547
- return c.json({ error: 'Offer not found' }, 404);
548
- }
549
-
550
- return c.json({
551
- id: offer.id,
552
- peerId: offer.peerId,
553
- sdp: offer.sdp,
554
- createdAt: offer.createdAt,
555
- expiresAt: offer.expiresAt,
556
- answererPeerId: offer.answererPeerId,
557
- answered: !!offer.answererPeerId,
558
- answerSdp: offer.answerSdp
559
- }, 200);
560
- } catch (err) {
561
- console.error('Error getting offer:', err);
562
- return c.json({ error: 'Internal server error' }, 500);
563
- }
564
- });
565
-
566
- /**
567
- * DELETE /offers/:offerId
568
- * Delete an offer
569
- */
570
- app.delete('/offers/:offerId', authMiddleware, async (c) => {
571
- try {
572
- const offerId = c.req.param('offerId');
573
- const peerId = getAuthenticatedPeerId(c);
574
-
575
- const deleted = await storage.deleteOffer(offerId, peerId);
576
-
577
- if (!deleted) {
578
- return c.json({ error: 'Offer not found or not owned by this peer' }, 404);
579
- }
580
-
581
- return c.json({ success: true }, 200);
582
- } catch (err) {
583
- console.error('Error deleting offer:', err);
584
- return c.json({ error: 'Internal server error' }, 500);
585
- }
586
- });
587
-
588
- /**
589
- * POST /offers/:offerId/answer
590
- * Answer an offer
591
- */
592
- app.post('/offers/:offerId/answer', authMiddleware, async (c) => {
593
- try {
594
- const offerId = c.req.param('offerId');
595
- const body = await c.req.json();
596
- const { sdp, secret } = body;
457
+ const { sdp } = body;
597
458
 
598
459
  if (!sdp) {
599
460
  return c.json({ error: 'Missing required parameter: sdp' }, 400);
@@ -607,69 +468,82 @@ export function createApp(storage: Storage, config: Config) {
607
468
  return c.json({ error: 'SDP too large (max 64KB)' }, 400);
608
469
  }
609
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
+
610
485
  const answererPeerId = getAuthenticatedPeerId(c);
611
486
 
612
- const result = await storage.answerOffer(offerId, answererPeerId, sdp, secret);
487
+ const result = await storage.answerOffer(availableOffer.id, answererPeerId, sdp);
613
488
 
614
489
  if (!result.success) {
615
490
  return c.json({ error: result.error }, 400);
616
491
  }
617
492
 
618
- return c.json({ success: true }, 200);
493
+ return c.json({
494
+ success: true,
495
+ offerId: availableOffer.id
496
+ }, 200);
619
497
  } catch (err) {
620
- console.error('Error answering offer:', err);
498
+ console.error('Error answering service:', err);
621
499
  return c.json({ error: 'Internal server error' }, 500);
622
500
  }
623
501
  });
624
502
 
625
503
  /**
626
- * GET /offers/:offerId/answer
627
- * Get answer for a specific offer (RESTful endpoint)
504
+ * GET /services/:uuid/answer
505
+ * Get answer for a service (offerer polls this)
628
506
  */
629
- app.get('/offers/:offerId/answer', authMiddleware, async (c) => {
507
+ app.get('/services/:uuid/answer', authMiddleware, async (c) => {
630
508
  try {
631
- const offerId = c.req.param('offerId');
509
+ const uuid = c.req.param('uuid');
632
510
  const peerId = getAuthenticatedPeerId(c);
633
511
 
634
- const offer = await storage.getOfferById(offerId);
635
-
636
- if (!offer) {
637
- return c.json({ error: 'Offer not found' }, 404);
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);
638
516
  }
639
517
 
640
- // Verify ownership
641
- if (offer.peerId !== peerId) {
642
- return c.json({ error: 'Not authorized to view this answer' }, 403);
643
- }
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);
644
521
 
645
- // Check if answered
646
- if (!offer.answererPeerId || !offer.answerSdp) {
522
+ if (!myOffer || !myOffer.answerSdp) {
647
523
  return c.json({ error: 'Offer not yet answered' }, 404);
648
524
  }
649
525
 
650
526
  return c.json({
651
- offerId: offer.id,
652
- answererId: offer.answererPeerId,
653
- sdp: offer.answerSdp,
654
- answeredAt: offer.answeredAt
527
+ offerId: myOffer.id,
528
+ answererId: myOffer.answererPeerId,
529
+ sdp: myOffer.answerSdp,
530
+ answeredAt: myOffer.answeredAt
655
531
  }, 200);
656
532
  } catch (err) {
657
- console.error('Error getting answer:', err);
533
+ console.error('Error getting service answer:', err);
658
534
  return c.json({ error: 'Internal server error' }, 500);
659
535
  }
660
536
  });
661
537
 
662
- // ===== ICE Candidate Exchange =====
663
-
664
538
  /**
665
- * POST /offers/:offerId/ice-candidates
666
- * Add ICE candidates for an offer
539
+ * POST /services/:uuid/ice-candidates
540
+ * Add ICE candidates for a service
667
541
  */
668
- app.post('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {
542
+ app.post('/services/:uuid/ice-candidates', authMiddleware, async (c) => {
669
543
  try {
670
- const offerId = c.req.param('offerId');
544
+ const uuid = c.req.param('uuid');
671
545
  const body = await c.req.json();
672
- const { candidates } = body;
546
+ const { candidates, offerId } = body;
673
547
 
674
548
  if (!Array.isArray(candidates) || candidates.length === 0) {
675
549
  return c.json({ error: 'Missing or invalid required parameter: candidates' }, 400);
@@ -677,8 +551,27 @@ export function createApp(storage: Storage, config: Config) {
677
551
 
678
552
  const peerId = getAuthenticatedPeerId(c);
679
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
+
680
573
  // Get offer to determine role
681
- const offer = await storage.getOfferById(offerId);
574
+ const offer = await storage.getOfferById(targetOfferId);
682
575
  if (!offer) {
683
576
  return c.json({ error: 'Offer not found' }, 404);
684
577
  }
@@ -686,27 +579,47 @@ export function createApp(storage: Storage, config: Config) {
686
579
  // Determine role
687
580
  const role = offer.peerId === peerId ? 'offerer' : 'answerer';
688
581
 
689
- const count = await storage.addIceCandidates(offerId, peerId, role, candidates);
582
+ const count = await storage.addIceCandidates(targetOfferId, peerId, role, candidates);
690
583
 
691
- return c.json({ count }, 200);
584
+ return c.json({ count, offerId: targetOfferId }, 200);
692
585
  } catch (err) {
693
- console.error('Error adding ICE candidates:', err);
586
+ console.error('Error adding ICE candidates to service:', err);
694
587
  return c.json({ error: 'Internal server error' }, 500);
695
588
  }
696
589
  });
697
590
 
698
591
  /**
699
- * GET /offers/:offerId/ice-candidates
700
- * Get ICE candidates for an offer
592
+ * GET /services/:uuid/ice-candidates
593
+ * Get ICE candidates for a service
701
594
  */
702
- app.get('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {
595
+ app.get('/services/:uuid/ice-candidates', authMiddleware, async (c) => {
703
596
  try {
704
- const offerId = c.req.param('offerId');
597
+ const uuid = c.req.param('uuid');
705
598
  const since = c.req.query('since');
599
+ const offerId = c.req.query('offerId');
706
600
  const peerId = getAuthenticatedPeerId(c);
707
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
+
708
621
  // Get offer to determine role
709
- const offer = await storage.getOfferById(offerId);
622
+ const offer = await storage.getOfferById(targetOfferId);
710
623
  if (!offer) {
711
624
  return c.json({ error: 'Offer not found' }, 404);
712
625
  }
@@ -715,16 +628,17 @@ export function createApp(storage: Storage, config: Config) {
715
628
  const targetRole = offer.peerId === peerId ? 'answerer' : 'offerer';
716
629
  const sinceTimestamp = since ? parseInt(since, 10) : undefined;
717
630
 
718
- const candidates = await storage.getIceCandidates(offerId, targetRole, sinceTimestamp);
631
+ const candidates = await storage.getIceCandidates(targetOfferId, targetRole, sinceTimestamp);
719
632
 
720
633
  return c.json({
721
634
  candidates: candidates.map(c => ({
722
635
  candidate: c.candidate,
723
636
  createdAt: c.createdAt
724
- }))
637
+ })),
638
+ offerId: targetOfferId
725
639
  }, 200);
726
640
  } catch (err) {
727
- console.error('Error getting ICE candidates:', err);
641
+ console.error('Error getting ICE candidates for service:', err);
728
642
  return c.json({ error: 'Internal server error' }, 500);
729
643
  }
730
644
  });