@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/README.md +58 -24
- package/dist/index.js +170 -186
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/src/app.ts +100 -186
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -444,156 +444,17 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
444
444
|
}
|
|
445
445
|
});
|
|
446
446
|
|
|
447
|
-
// =====
|
|
447
|
+
// ===== Service-Based WebRTC Signaling =====
|
|
448
448
|
|
|
449
449
|
/**
|
|
450
|
-
* POST /
|
|
451
|
-
*
|
|
450
|
+
* POST /services/:uuid/answer
|
|
451
|
+
* Answer a service offer
|
|
452
452
|
*/
|
|
453
|
-
app.post('/
|
|
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 {
|
|
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(
|
|
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({
|
|
493
|
+
return c.json({
|
|
494
|
+
success: true,
|
|
495
|
+
offerId: availableOffer.id
|
|
496
|
+
}, 200);
|
|
619
497
|
} catch (err) {
|
|
620
|
-
console.error('Error answering
|
|
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 /
|
|
627
|
-
* Get answer for a
|
|
504
|
+
* GET /services/:uuid/answer
|
|
505
|
+
* Get answer for a service (offerer polls this)
|
|
628
506
|
*/
|
|
629
|
-
app.get('/
|
|
507
|
+
app.get('/services/:uuid/answer', authMiddleware, async (c) => {
|
|
630
508
|
try {
|
|
631
|
-
const
|
|
509
|
+
const uuid = c.req.param('uuid');
|
|
632
510
|
const peerId = getAuthenticatedPeerId(c);
|
|
633
511
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (!
|
|
637
|
-
return c.json({ error: '
|
|
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
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
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:
|
|
652
|
-
answererId:
|
|
653
|
-
sdp:
|
|
654
|
-
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 /
|
|
666
|
-
* Add ICE candidates for
|
|
539
|
+
* POST /services/:uuid/ice-candidates
|
|
540
|
+
* Add ICE candidates for a service
|
|
667
541
|
*/
|
|
668
|
-
app.post('/
|
|
542
|
+
app.post('/services/:uuid/ice-candidates', authMiddleware, async (c) => {
|
|
669
543
|
try {
|
|
670
|
-
const
|
|
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(
|
|
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(
|
|
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 /
|
|
700
|
-
* Get ICE candidates for
|
|
592
|
+
* GET /services/:uuid/ice-candidates
|
|
593
|
+
* Get ICE candidates for a service
|
|
701
594
|
*/
|
|
702
|
-
app.get('/
|
|
595
|
+
app.get('/services/:uuid/ice-candidates', authMiddleware, async (c) => {
|
|
703
596
|
try {
|
|
704
|
-
const
|
|
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(
|
|
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(
|
|
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
|
});
|