@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/dist/index.js CHANGED
@@ -477,98 +477,32 @@ var wNAF = (n) => {
477
477
  hashes.sha512Async = async (message) => {
478
478
  return new Uint8Array(await crypto.subtle.digest("SHA-512", message));
479
479
  };
480
- var ALGORITHM = "AES-GCM";
481
- var IV_LENGTH = 12;
482
- var KEY_LENGTH = 32;
483
480
  var USERNAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
484
481
  var USERNAME_MIN_LENGTH = 3;
485
482
  var USERNAME_MAX_LENGTH = 32;
486
483
  var TIMESTAMP_TOLERANCE_MS = 5 * 60 * 1e3;
487
- function generatePeerId() {
488
- const bytes = crypto.getRandomValues(new Uint8Array(16));
489
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
490
- }
491
- function generateSecretKey() {
492
- const bytes = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
493
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
494
- }
495
- function hexToBytes2(hex) {
496
- const bytes = new Uint8Array(hex.length / 2);
497
- for (let i = 0; i < hex.length; i += 2) {
498
- bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
499
- }
500
- return bytes;
501
- }
502
- function bytesToBase64(bytes) {
503
- const binString = Array.from(
504
- bytes,
505
- (byte) => String.fromCodePoint(byte)
506
- ).join("");
507
- return btoa(binString);
508
- }
509
484
  function base64ToBytes(base64) {
510
485
  const binString = atob(base64);
511
486
  return Uint8Array.from(binString, (char) => char.codePointAt(0));
512
487
  }
513
- async function encryptPeerId(peerId, secretKeyHex) {
514
- const keyBytes = hexToBytes2(secretKeyHex);
515
- if (keyBytes.length !== KEY_LENGTH) {
516
- throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);
517
- }
518
- const key = await crypto.subtle.importKey(
519
- "raw",
520
- keyBytes,
521
- { name: ALGORITHM, length: 256 },
522
- false,
523
- ["encrypt"]
524
- );
525
- const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
526
- const encoder = new TextEncoder();
527
- const data = encoder.encode(peerId);
528
- const encrypted = await crypto.subtle.encrypt(
529
- { name: ALGORITHM, iv },
530
- key,
531
- data
532
- );
533
- const combined = new Uint8Array(iv.length + encrypted.byteLength);
534
- combined.set(iv, 0);
535
- combined.set(new Uint8Array(encrypted), iv.length);
536
- return bytesToBase64(combined);
537
- }
538
- async function decryptPeerId(encryptedSecret, secretKeyHex) {
539
- try {
540
- const keyBytes = hexToBytes2(secretKeyHex);
541
- if (keyBytes.length !== KEY_LENGTH) {
542
- throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);
543
- }
544
- const combined = base64ToBytes(encryptedSecret);
545
- const iv = combined.slice(0, IV_LENGTH);
546
- const ciphertext = combined.slice(IV_LENGTH);
547
- const key = await crypto.subtle.importKey(
548
- "raw",
549
- keyBytes,
550
- { name: ALGORITHM, length: 256 },
551
- false,
552
- ["decrypt"]
553
- );
554
- const decrypted = await crypto.subtle.decrypt(
555
- { name: ALGORITHM, iv },
556
- key,
557
- ciphertext
558
- );
559
- const decoder = new TextDecoder();
560
- return decoder.decode(decrypted);
561
- } catch (err2) {
562
- throw new Error("Failed to decrypt peer ID: invalid secret or secret key");
488
+ function validateAuthMessage(expectedUsername, message) {
489
+ const parts = message.split(":");
490
+ if (parts.length < 3) {
491
+ return { valid: false, error: "Invalid message format: must have at least action:username:timestamp" };
563
492
  }
564
- }
565
- async function validateCredentials(peerId, encryptedSecret, secretKey) {
566
- try {
567
- const decryptedPeerId = await decryptPeerId(encryptedSecret, secretKey);
568
- return decryptedPeerId === peerId;
569
- } catch {
570
- return false;
493
+ const messageUsername = parts[1];
494
+ const timestamp = parseInt(parts[parts.length - 1], 10);
495
+ if (messageUsername !== expectedUsername) {
496
+ return { valid: false, error: "Username in message does not match authenticated username" };
571
497
  }
498
+ if (isNaN(timestamp)) {
499
+ return { valid: false, error: "Invalid timestamp in message" };
500
+ }
501
+ const timestampCheck = validateTimestamp(timestamp);
502
+ if (!timestampCheck.valid) {
503
+ return timestampCheck;
504
+ }
505
+ return { valid: true };
572
506
  }
573
507
  function validateUsername(username) {
574
508
  if (typeof username !== "string") {
@@ -589,24 +523,74 @@ function validateServiceFqn(fqn) {
589
523
  if (typeof fqn !== "string") {
590
524
  return { valid: false, error: "Service FQN must be a string" };
591
525
  }
592
- const parts = fqn.split("@");
593
- if (parts.length !== 2) {
594
- return { valid: false, error: "Service FQN must be in format: service-name@version" };
526
+ const parsed = parseServiceFqn(fqn);
527
+ if (!parsed) {
528
+ return { valid: false, error: "Service FQN must be in format: service:version[@username]" };
595
529
  }
596
- const [serviceName, version] = parts;
597
- const serviceNameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
530
+ const { serviceName, version, username } = parsed;
531
+ const serviceNameRegex = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/;
598
532
  if (!serviceNameRegex.test(serviceName)) {
599
- return { valid: false, error: "Service name must be reverse domain notation (e.g., com.example.service)" };
533
+ return { valid: false, error: "Service name must be lowercase alphanumeric with optional dots/dashes" };
600
534
  }
601
- if (serviceName.length < 3 || serviceName.length > 128) {
602
- return { valid: false, error: "Service name must be 3-128 characters" };
535
+ if (serviceName.length < 1 || serviceName.length > 128) {
536
+ return { valid: false, error: "Service name must be 1-128 characters" };
603
537
  }
604
538
  const versionRegex = /^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.-]+)?$/;
605
539
  if (!versionRegex.test(version)) {
606
540
  return { valid: false, error: "Version must be semantic versioning (e.g., 1.0.0, 2.1.3-beta)" };
607
541
  }
542
+ if (username) {
543
+ const usernameCheck = validateUsername(username);
544
+ if (!usernameCheck.valid) {
545
+ return usernameCheck;
546
+ }
547
+ }
608
548
  return { valid: true };
609
549
  }
550
+ function parseVersion(version) {
551
+ const match = version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-z0-9.-]+)?$/);
552
+ if (!match) return null;
553
+ return {
554
+ major: parseInt(match[1], 10),
555
+ minor: parseInt(match[2], 10),
556
+ patch: parseInt(match[3], 10),
557
+ prerelease: match[4]?.substring(1)
558
+ // Remove leading dash
559
+ };
560
+ }
561
+ function isVersionCompatible(requested, available) {
562
+ const req = parseVersion(requested);
563
+ const avail = parseVersion(available);
564
+ if (!req || !avail) return false;
565
+ if (req.major !== avail.major) return false;
566
+ if (req.major === 0 && req.minor !== avail.minor) return false;
567
+ if (avail.minor < req.minor) return false;
568
+ if (avail.minor === req.minor && avail.patch < req.patch) return false;
569
+ if (req.prerelease && req.prerelease !== avail.prerelease) return false;
570
+ return true;
571
+ }
572
+ function parseServiceFqn(fqn) {
573
+ if (!fqn || typeof fqn !== "string") return null;
574
+ const atIndex = fqn.lastIndexOf("@");
575
+ let serviceVersion;
576
+ let username = null;
577
+ if (atIndex > 0) {
578
+ serviceVersion = fqn.substring(0, atIndex);
579
+ username = fqn.substring(atIndex + 1);
580
+ } else {
581
+ serviceVersion = fqn;
582
+ }
583
+ const colonIndex = serviceVersion.indexOf(":");
584
+ if (colonIndex <= 0) return null;
585
+ const serviceName = serviceVersion.substring(0, colonIndex);
586
+ const version = serviceVersion.substring(colonIndex + 1);
587
+ if (!serviceName || !version) return null;
588
+ return {
589
+ serviceName,
590
+ version,
591
+ username
592
+ };
593
+ }
610
594
  function validateTimestamp(timestamp) {
611
595
  if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
612
596
  return { valid: false, error: "Timestamp must be a finite number" };
@@ -631,605 +615,582 @@ async function verifyEd25519Signature(publicKey, signature, message) {
631
615
  return false;
632
616
  }
633
617
  }
634
- async function validateUsernameClaim(username, publicKey, signature, message) {
635
- const usernameCheck = validateUsername(username);
636
- if (!usernameCheck.valid) {
637
- return usernameCheck;
638
- }
639
- const parts = message.split(":");
640
- if (parts.length !== 3 || parts[0] !== "claim" || parts[1] !== username) {
641
- return { valid: false, error: "Invalid message format (expected: claim:{username}:{timestamp})" };
642
- }
643
- const timestamp = parseInt(parts[2], 10);
644
- if (isNaN(timestamp)) {
645
- return { valid: false, error: "Invalid timestamp in message" };
646
- }
647
- const timestampCheck = validateTimestamp(timestamp);
648
- if (!timestampCheck.valid) {
649
- return timestampCheck;
618
+
619
+ // src/rpc.ts
620
+ var MAX_PAGE_SIZE = 100;
621
+ async function verifyAuth(username, message, signature, publicKey, storage) {
622
+ let usernameRecord = await storage.getUsername(username);
623
+ if (!usernameRecord) {
624
+ if (!publicKey) {
625
+ return {
626
+ valid: false,
627
+ error: `Username "${username}" is not claimed and no public key provided for auto-claim.`
628
+ };
629
+ }
630
+ const usernameValidation = validateUsername(username);
631
+ if (!usernameValidation.valid) {
632
+ return usernameValidation;
633
+ }
634
+ const signatureValid = await verifyEd25519Signature(publicKey, signature, message);
635
+ if (!signatureValid) {
636
+ return { valid: false, error: "Invalid signature for auto-claim" };
637
+ }
638
+ const expiresAt = Date.now() + 365 * 24 * 60 * 60 * 1e3;
639
+ await storage.claimUsername({
640
+ username,
641
+ publicKey,
642
+ expiresAt
643
+ });
644
+ usernameRecord = await storage.getUsername(username);
645
+ if (!usernameRecord) {
646
+ return { valid: false, error: "Failed to claim username" };
647
+ }
650
648
  }
651
- const signatureValid = await verifyEd25519Signature(publicKey, signature, message);
652
- if (!signatureValid) {
649
+ const isValid = await verifyEd25519Signature(
650
+ usernameRecord.publicKey,
651
+ signature,
652
+ message
653
+ );
654
+ if (!isValid) {
653
655
  return { valid: false, error: "Invalid signature" };
654
656
  }
655
- return { valid: true };
656
- }
657
- async function validateServicePublish(username, serviceFqn, publicKey, signature, message) {
658
- const usernameCheck = validateUsername(username);
659
- if (!usernameCheck.valid) {
660
- return usernameCheck;
661
- }
662
- const parts = message.split(":");
663
- if (parts.length !== 4 || parts[0] !== "publish" || parts[1] !== username || parts[2] !== serviceFqn) {
664
- return { valid: false, error: "Invalid message format (expected: publish:{username}:{serviceFqn}:{timestamp})" };
665
- }
666
- const timestamp = parseInt(parts[3], 10);
667
- if (isNaN(timestamp)) {
668
- return { valid: false, error: "Invalid timestamp in message" };
669
- }
670
- const timestampCheck = validateTimestamp(timestamp);
671
- if (!timestampCheck.valid) {
672
- return timestampCheck;
673
- }
674
- const signatureValid = await verifyEd25519Signature(publicKey, signature, message);
675
- if (!signatureValid) {
676
- return { valid: false, error: "Invalid signature" };
657
+ const validation = validateAuthMessage(username, message);
658
+ if (!validation.valid) {
659
+ return { valid: false, error: validation.error };
677
660
  }
678
661
  return { valid: true };
679
662
  }
680
-
681
- // src/middleware/auth.ts
682
- function createAuthMiddleware(authSecret) {
683
- return async (c, next) => {
684
- const authHeader = c.req.header("Authorization");
685
- if (!authHeader) {
686
- return c.json({ error: "Missing Authorization header" }, 401);
687
- }
688
- const parts = authHeader.split(" ");
689
- if (parts.length !== 2 || parts[0] !== "Bearer") {
690
- return c.json({ error: "Invalid Authorization header format. Expected: Bearer {peerId}:{secret}" }, 401);
691
- }
692
- const credentials = parts[1].split(":");
693
- if (credentials.length !== 2) {
694
- return c.json({ error: "Invalid credentials format. Expected: {peerId}:{secret}" }, 401);
695
- }
696
- const [peerId, encryptedSecret] = credentials;
697
- const isValid = await validateCredentials(peerId, encryptedSecret, authSecret);
698
- if (!isValid) {
699
- return c.json({ error: "Invalid credentials" }, 401);
700
- }
701
- c.set("peerId", peerId);
702
- await next();
703
- };
704
- }
705
- function getAuthenticatedPeerId(c) {
706
- const peerId = c.get("peerId");
707
- if (!peerId) {
708
- throw new Error("No authenticated peer ID in context");
709
- }
710
- return peerId;
663
+ function extractUsername(message) {
664
+ const parts = message.split(":");
665
+ if (parts.length < 2) return null;
666
+ return parts[1];
711
667
  }
712
-
713
- // src/app.ts
714
- function createApp(storage, config) {
715
- const app = new import_hono.Hono();
716
- const authMiddleware = createAuthMiddleware(config.authSecret);
717
- app.use("/*", (0, import_cors.cors)({
718
- origin: (origin) => {
719
- if (config.corsOrigins.length === 1 && config.corsOrigins[0] === "*") {
720
- return origin;
721
- }
722
- if (config.corsOrigins.includes(origin)) {
723
- return origin;
724
- }
725
- return config.corsOrigins[0];
726
- },
727
- allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
728
- allowHeaders: ["Content-Type", "Origin", "Authorization"],
729
- exposeHeaders: ["Content-Type"],
730
- maxAge: 600,
731
- credentials: true
732
- }));
733
- app.get("/", (c) => {
734
- return c.json({
735
- version: config.version,
736
- name: "Rondevu",
737
- description: "DNS-like WebRTC signaling with username claiming and service discovery"
738
- });
739
- });
740
- app.get("/health", (c) => {
741
- return c.json({
742
- status: "ok",
743
- timestamp: Date.now(),
744
- version: config.version
745
- });
746
- });
747
- app.post("/register", async (c) => {
748
- try {
749
- const peerId = generatePeerId();
750
- const secret = await encryptPeerId(peerId, config.authSecret);
751
- return c.json({
752
- peerId,
753
- secret
754
- }, 200);
755
- } catch (err2) {
756
- console.error("Error registering peer:", err2);
757
- return c.json({ error: "Internal server error" }, 500);
668
+ var handlers = {
669
+ /**
670
+ * Check if username is available
671
+ */
672
+ async getUser(params, message, signature, publicKey, storage, config) {
673
+ const { username } = params;
674
+ const claimed = await storage.getUsername(username);
675
+ if (!claimed) {
676
+ return {
677
+ username,
678
+ available: true
679
+ };
758
680
  }
759
- });
760
- app.get("/users/:username", async (c) => {
761
- try {
762
- const username = c.req.param("username");
763
- const claimed = await storage.getUsername(username);
764
- if (!claimed) {
765
- return c.json({
766
- username,
767
- available: true
768
- }, 200);
681
+ return {
682
+ username: claimed.username,
683
+ available: false,
684
+ claimedAt: claimed.claimedAt,
685
+ expiresAt: claimed.expiresAt,
686
+ publicKey: claimed.publicKey
687
+ };
688
+ },
689
+ /**
690
+ * Get service by FQN - Supports 3 modes:
691
+ * 1. Direct lookup: FQN includes @username
692
+ * 2. Paginated discovery: FQN without @username, with limit/offset
693
+ * 3. Random discovery: FQN without @username, no limit
694
+ */
695
+ async getService(params, message, signature, publicKey, storage, config) {
696
+ const { serviceFqn, limit, offset } = params;
697
+ const username = extractUsername(message);
698
+ if (username) {
699
+ const auth = await verifyAuth(username, message, signature, publicKey, storage);
700
+ if (!auth.valid) {
701
+ throw new Error(auth.error);
769
702
  }
770
- return c.json({
771
- username: claimed.username,
772
- available: false,
773
- claimedAt: claimed.claimedAt,
774
- expiresAt: claimed.expiresAt,
775
- publicKey: claimed.publicKey
776
- }, 200);
777
- } catch (err2) {
778
- console.error("Error checking username:", err2);
779
- return c.json({ error: "Internal server error" }, 500);
780
703
  }
781
- });
782
- app.post("/users/:username", async (c) => {
783
- try {
784
- const username = c.req.param("username");
785
- const body = await c.req.json();
786
- const { publicKey, signature, message } = body;
787
- if (!publicKey || !signature || !message) {
788
- return c.json({ error: "Missing required parameters: publicKey, signature, message" }, 400);
789
- }
790
- const validation = await validateUsernameClaim(username, publicKey, signature, message);
791
- if (!validation.valid) {
792
- return c.json({ error: validation.error }, 400);
793
- }
794
- try {
795
- const claimed = await storage.claimUsername({
796
- username,
797
- publicKey,
798
- signature,
799
- message
800
- });
801
- return c.json({
802
- username: claimed.username,
803
- claimedAt: claimed.claimedAt,
804
- expiresAt: claimed.expiresAt
805
- }, 201);
806
- } catch (err2) {
807
- if (err2.message?.includes("already claimed")) {
808
- return c.json({ error: "Username already claimed by different public key" }, 409);
809
- }
810
- throw err2;
811
- }
812
- } catch (err2) {
813
- console.error("Error claiming username:", err2);
814
- return c.json({ error: "Internal server error" }, 500);
704
+ const fqnValidation = validateServiceFqn(serviceFqn);
705
+ if (!fqnValidation.valid) {
706
+ throw new Error(fqnValidation.error || "Invalid service FQN");
815
707
  }
816
- });
817
- app.get("/users/:username/services", async (c) => {
818
- try {
819
- const username = c.req.param("username");
820
- const services = await storage.listServicesForUsername(username);
821
- return c.json({
822
- username,
823
- services
824
- }, 200);
825
- } catch (err2) {
826
- console.error("Error listing services:", err2);
827
- return c.json({ error: "Internal server error" }, 500);
708
+ const parsed = parseServiceFqn(serviceFqn);
709
+ if (!parsed) {
710
+ throw new Error("Failed to parse service FQN");
828
711
  }
829
- });
830
- app.get("/users/:username/services/:fqn", async (c) => {
831
- try {
832
- const username = c.req.param("username");
833
- const serviceFqn = decodeURIComponent(c.req.param("fqn"));
834
- const uuid = await storage.queryService(username, serviceFqn);
835
- if (!uuid) {
836
- return c.json({ error: "Service not found" }, 404);
712
+ const filterCompatibleServices = (services) => {
713
+ return services.filter((s) => {
714
+ const serviceVersion = parseServiceFqn(s.serviceFqn);
715
+ return serviceVersion && isVersionCompatible(parsed.version, serviceVersion.version);
716
+ });
717
+ };
718
+ const findAvailableOffer = async (service) => {
719
+ const offers = await storage.getOffersForService(service.id);
720
+ return offers.find((o) => !o.answererUsername);
721
+ };
722
+ const buildServiceResponse = (service, offer) => ({
723
+ serviceId: service.id,
724
+ username: service.username,
725
+ serviceFqn: service.serviceFqn,
726
+ offerId: offer.id,
727
+ sdp: offer.sdp,
728
+ createdAt: service.createdAt,
729
+ expiresAt: service.expiresAt
730
+ });
731
+ if (limit !== void 0) {
732
+ const pageLimit = Math.min(Math.max(1, limit), MAX_PAGE_SIZE);
733
+ const pageOffset = Math.max(0, offset || 0);
734
+ const allServices2 = await storage.getServicesByName(parsed.service, parsed.version);
735
+ const compatibleServices2 = filterCompatibleServices(allServices2);
736
+ const usernameSet = /* @__PURE__ */ new Set();
737
+ const uniqueServices = [];
738
+ for (const service of compatibleServices2) {
739
+ if (!usernameSet.has(service.username)) {
740
+ usernameSet.add(service.username);
741
+ const availableOffer2 = await findAvailableOffer(service);
742
+ if (availableOffer2) {
743
+ uniqueServices.push(buildServiceResponse(service, availableOffer2));
744
+ }
745
+ }
837
746
  }
838
- const service = await storage.getServiceByUuid(uuid);
747
+ const paginatedServices = uniqueServices.slice(pageOffset, pageOffset + pageLimit);
748
+ return {
749
+ services: paginatedServices,
750
+ count: paginatedServices.length,
751
+ limit: pageLimit,
752
+ offset: pageOffset
753
+ };
754
+ }
755
+ if (parsed.username) {
756
+ const service = await storage.getServiceByFqn(serviceFqn);
839
757
  if (!service) {
840
- return c.json({ error: "Service not found" }, 404);
758
+ throw new Error("Service not found");
841
759
  }
842
- const initialOffer = await storage.getOfferById(service.offerId);
843
- if (!initialOffer) {
844
- return c.json({ error: "Associated offer not found" }, 404);
760
+ const availableOffer2 = await findAvailableOffer(service);
761
+ if (!availableOffer2) {
762
+ throw new Error("Service has no available offers");
845
763
  }
846
- const peerOffers = await storage.getOffersByPeerId(initialOffer.peerId);
847
- const availableOffer = peerOffers.find((offer) => !offer.answererPeerId);
848
- if (!availableOffer) {
849
- return c.json({
850
- error: "No available offers",
851
- message: "All offers from this service are currently in use. Please try again later."
852
- }, 503);
853
- }
854
- return c.json({
855
- uuid,
856
- serviceId: service.id,
857
- username: service.username,
858
- serviceFqn: service.serviceFqn,
859
- offerId: availableOffer.id,
860
- sdp: availableOffer.sdp,
861
- isPublic: service.isPublic,
862
- metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
863
- createdAt: service.createdAt,
864
- expiresAt: service.expiresAt
865
- }, 200);
866
- } catch (err2) {
867
- console.error("Error getting service:", err2);
868
- return c.json({ error: "Internal server error" }, 500);
764
+ return buildServiceResponse(service, availableOffer2);
869
765
  }
870
- });
871
- app.post("/users/:username/services", authMiddleware, async (c) => {
872
- let serviceFqn;
873
- let offers = [];
874
- try {
875
- const username = c.req.param("username");
876
- const body = await c.req.json();
877
- serviceFqn = body.serviceFqn;
878
- const { sdp, ttl, isPublic, metadata, signature, message } = body;
879
- if (!serviceFqn || !sdp) {
880
- return c.json({ error: "Missing required parameters: serviceFqn, sdp" }, 400);
881
- }
882
- const fqnValidation = validateServiceFqn(serviceFqn);
883
- if (!fqnValidation.valid) {
884
- return c.json({ error: fqnValidation.error }, 400);
885
- }
886
- if (!signature || !message) {
887
- return c.json({ error: "Missing signature or message for username verification" }, 400);
888
- }
889
- const usernameRecord = await storage.getUsername(username);
890
- if (!usernameRecord) {
891
- return c.json({ error: "Username not claimed" }, 404);
892
- }
893
- const signatureValidation = await validateServicePublish(username, serviceFqn, usernameRecord.publicKey, signature, message);
894
- if (!signatureValidation.valid) {
895
- return c.json({ error: "Invalid signature for username" }, 403);
896
- }
897
- const existingUuid = await storage.queryService(username, serviceFqn);
898
- if (existingUuid) {
899
- const existingService = await storage.getServiceByUuid(existingUuid);
900
- if (existingService) {
901
- await storage.deleteService(existingService.id, username);
902
- }
903
- }
904
- if (typeof sdp !== "string" || sdp.length === 0) {
905
- return c.json({ error: "Invalid SDP" }, 400);
766
+ const allServices = await storage.getServicesByName(parsed.service, parsed.version);
767
+ const compatibleServices = filterCompatibleServices(allServices);
768
+ if (compatibleServices.length === 0) {
769
+ throw new Error("No services found");
770
+ }
771
+ const randomService = compatibleServices[Math.floor(Math.random() * compatibleServices.length)];
772
+ const availableOffer = await findAvailableOffer(randomService);
773
+ if (!availableOffer) {
774
+ throw new Error("Service has no available offers");
775
+ }
776
+ return buildServiceResponse(randomService, availableOffer);
777
+ },
778
+ /**
779
+ * Publish a service
780
+ */
781
+ async publishService(params, message, signature, publicKey, storage, config) {
782
+ const { serviceFqn, offers, ttl } = params;
783
+ const username = extractUsername(message);
784
+ if (!username) {
785
+ throw new Error("Username required for service publishing");
786
+ }
787
+ const auth = await verifyAuth(username, message, signature, publicKey, storage);
788
+ if (!auth.valid) {
789
+ throw new Error(auth.error);
790
+ }
791
+ const fqnValidation = validateServiceFqn(serviceFqn);
792
+ if (!fqnValidation.valid) {
793
+ throw new Error(fqnValidation.error || "Invalid service FQN");
794
+ }
795
+ const parsed = parseServiceFqn(serviceFqn);
796
+ if (!parsed || !parsed.username) {
797
+ throw new Error("Service FQN must include username");
798
+ }
799
+ if (parsed.username !== username) {
800
+ throw new Error("Service FQN username must match authenticated username");
801
+ }
802
+ if (!offers || !Array.isArray(offers) || offers.length === 0) {
803
+ throw new Error("Must provide at least one offer");
804
+ }
805
+ if (offers.length > config.maxOffersPerRequest) {
806
+ throw new Error(
807
+ `Too many offers (max ${config.maxOffersPerRequest})`
808
+ );
809
+ }
810
+ offers.forEach((offer, index) => {
811
+ if (!offer || typeof offer !== "object") {
812
+ throw new Error(`Invalid offer at index ${index}: must be an object`);
906
813
  }
907
- if (sdp.length > 64 * 1024) {
908
- return c.json({ error: "SDP too large (max 64KB)" }, 400);
814
+ if (!offer.sdp || typeof offer.sdp !== "string") {
815
+ throw new Error(`Invalid offer at index ${index}: missing or invalid SDP`);
909
816
  }
910
- const peerId = getAuthenticatedPeerId(c);
911
- const offerTtl = Math.min(
912
- Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
913
- config.offerMaxTtl
914
- );
915
- const expiresAt = Date.now() + offerTtl;
916
- offers = await storage.createOffers([{
917
- peerId,
918
- sdp,
919
- expiresAt
920
- }]);
921
- if (offers.length === 0) {
922
- return c.json({ error: "Failed to create offer" }, 500);
817
+ if (!offer.sdp.trim()) {
818
+ throw new Error(`Invalid offer at index ${index}: SDP cannot be empty`);
923
819
  }
924
- const offer = offers[0];
925
- const result = await storage.createService({
926
- username,
927
- serviceFqn,
928
- offerId: offer.id,
929
- expiresAt,
930
- isPublic: isPublic || false,
931
- metadata: metadata ? JSON.stringify(metadata) : void 0
932
- });
933
- return c.json({
934
- uuid: result.indexUuid,
935
- serviceFqn,
936
- username,
937
- serviceId: result.service.id,
820
+ });
821
+ const now = Date.now();
822
+ const offerTtl = ttl !== void 0 ? Math.min(
823
+ Math.max(ttl, config.offerMinTtl),
824
+ config.offerMaxTtl
825
+ ) : config.offerDefaultTtl;
826
+ const expiresAt = now + offerTtl;
827
+ const offerRequests = offers.map((offer) => ({
828
+ username,
829
+ serviceFqn,
830
+ sdp: offer.sdp,
831
+ expiresAt
832
+ }));
833
+ const result = await storage.createService({
834
+ serviceFqn,
835
+ expiresAt,
836
+ offers: offerRequests
837
+ });
838
+ return {
839
+ serviceId: result.service.id,
840
+ username: result.service.username,
841
+ serviceFqn: result.service.serviceFqn,
842
+ offers: result.offers.map((offer) => ({
938
843
  offerId: offer.id,
939
844
  sdp: offer.sdp,
940
- isPublic: result.service.isPublic,
941
- metadata,
942
- createdAt: result.service.createdAt,
943
- expiresAt: result.service.expiresAt
944
- }, 201);
945
- } catch (err2) {
946
- console.error("Error creating service:", err2);
947
- console.error("Error details:", {
948
- message: err2.message,
949
- stack: err2.stack,
950
- username: c.req.param("username"),
951
- serviceFqn,
952
- offerId: offers[0]?.id
953
- });
954
- return c.json({
955
- error: "Internal server error",
956
- details: err2.message
957
- }, 500);
845
+ createdAt: offer.createdAt,
846
+ expiresAt: offer.expiresAt
847
+ })),
848
+ createdAt: result.service.createdAt,
849
+ expiresAt: result.service.expiresAt
850
+ };
851
+ },
852
+ /**
853
+ * Delete a service
854
+ */
855
+ async deleteService(params, message, signature, publicKey, storage, config) {
856
+ const { serviceFqn } = params;
857
+ const username = extractUsername(message);
858
+ if (!username) {
859
+ throw new Error("Username required");
958
860
  }
959
- });
960
- app.delete("/users/:username/services/:fqn", authMiddleware, async (c) => {
961
- try {
962
- const username = c.req.param("username");
963
- const serviceFqn = decodeURIComponent(c.req.param("fqn"));
964
- const uuid = await storage.queryService(username, serviceFqn);
965
- if (!uuid) {
966
- return c.json({ error: "Service not found" }, 404);
967
- }
968
- const service = await storage.getServiceByUuid(uuid);
969
- if (!service) {
970
- return c.json({ error: "Service not found" }, 404);
971
- }
972
- const deleted = await storage.deleteService(service.id, username);
973
- if (!deleted) {
974
- return c.json({ error: "Service not found or not owned by this username" }, 404);
975
- }
976
- return c.json({ success: true }, 200);
977
- } catch (err2) {
978
- console.error("Error deleting service:", err2);
979
- return c.json({ error: "Internal server error" }, 500);
861
+ const auth = await verifyAuth(username, message, signature, publicKey, storage);
862
+ if (!auth.valid) {
863
+ throw new Error(auth.error);
980
864
  }
981
- });
982
- app.get("/services/:uuid", async (c) => {
983
- try {
984
- const uuid = c.req.param("uuid");
985
- const service = await storage.getServiceByUuid(uuid);
986
- if (!service) {
987
- return c.json({ error: "Service not found" }, 404);
988
- }
989
- const initialOffer = await storage.getOfferById(service.offerId);
990
- if (!initialOffer) {
991
- return c.json({ error: "Associated offer not found" }, 404);
992
- }
993
- const peerOffers = await storage.getOffersByPeerId(initialOffer.peerId);
994
- const availableOffer = peerOffers.find((offer) => !offer.answererPeerId);
995
- if (!availableOffer) {
996
- return c.json({
997
- error: "No available offers",
998
- message: "All offers from this service are currently in use. Please try again later."
999
- }, 503);
1000
- }
1001
- return c.json({
1002
- uuid,
1003
- serviceId: service.id,
1004
- username: service.username,
1005
- serviceFqn: service.serviceFqn,
1006
- offerId: availableOffer.id,
1007
- sdp: availableOffer.sdp,
1008
- isPublic: service.isPublic,
1009
- metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
1010
- createdAt: service.createdAt,
1011
- expiresAt: service.expiresAt
1012
- }, 200);
1013
- } catch (err2) {
1014
- console.error("Error getting service:", err2);
1015
- return c.json({ error: "Internal server error" }, 500);
865
+ const parsed = parseServiceFqn(serviceFqn);
866
+ if (!parsed || !parsed.username) {
867
+ throw new Error("Service FQN must include username");
1016
868
  }
1017
- });
1018
- app.post("/offers", authMiddleware, async (c) => {
1019
- try {
1020
- const body = await c.req.json();
1021
- const { offers } = body;
1022
- if (!Array.isArray(offers) || offers.length === 0) {
1023
- return c.json({ error: "Missing or invalid required parameter: offers (must be non-empty array)" }, 400);
1024
- }
1025
- if (offers.length > config.maxOffersPerRequest) {
1026
- return c.json({ error: `Too many offers (max ${config.maxOffersPerRequest})` }, 400);
1027
- }
1028
- const peerId = getAuthenticatedPeerId(c);
1029
- const validated = offers.map((offer) => {
1030
- const { sdp, ttl, secret } = offer;
1031
- if (typeof sdp !== "string" || sdp.length === 0) {
1032
- throw new Error("Invalid SDP in offer");
1033
- }
1034
- if (sdp.length > 64 * 1024) {
1035
- throw new Error("SDP too large (max 64KB)");
1036
- }
1037
- const offerTtl = Math.min(
1038
- Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
1039
- config.offerMaxTtl
1040
- );
1041
- return {
1042
- peerId,
1043
- sdp,
1044
- expiresAt: Date.now() + offerTtl,
1045
- secret: secret ? String(secret).substring(0, 128) : void 0
1046
- };
1047
- });
1048
- const created = await storage.createOffers(validated);
1049
- return c.json({
1050
- offers: created.map((offer) => ({
1051
- id: offer.id,
1052
- peerId: offer.peerId,
1053
- expiresAt: offer.expiresAt,
1054
- createdAt: offer.createdAt,
1055
- hasSecret: !!offer.secret
1056
- }))
1057
- }, 201);
1058
- } catch (err2) {
1059
- console.error("Error creating offers:", err2);
1060
- return c.json({ error: err2.message || "Internal server error" }, 500);
869
+ const service = await storage.getServiceByFqn(serviceFqn);
870
+ if (!service) {
871
+ throw new Error("Service not found");
1061
872
  }
1062
- });
1063
- app.get("/offers/mine", authMiddleware, async (c) => {
1064
- try {
1065
- const peerId = getAuthenticatedPeerId(c);
1066
- const offers = await storage.getOffersByPeerId(peerId);
1067
- return c.json({
1068
- offers: offers.map((offer) => ({
1069
- id: offer.id,
1070
- sdp: offer.sdp,
1071
- createdAt: offer.createdAt,
1072
- expiresAt: offer.expiresAt,
1073
- lastSeen: offer.lastSeen,
1074
- hasSecret: !!offer.secret,
1075
- answererPeerId: offer.answererPeerId,
1076
- answered: !!offer.answererPeerId
1077
- }))
1078
- }, 200);
1079
- } catch (err2) {
1080
- console.error("Error getting offers:", err2);
1081
- return c.json({ error: "Internal server error" }, 500);
873
+ const deleted = await storage.deleteService(service.id, username);
874
+ if (!deleted) {
875
+ throw new Error("Service not found or not owned by this username");
1082
876
  }
1083
- });
1084
- app.get("/offers/:offerId", authMiddleware, async (c) => {
1085
- try {
1086
- const offerId = c.req.param("offerId");
1087
- const offer = await storage.getOfferById(offerId);
1088
- if (!offer) {
1089
- return c.json({ error: "Offer not found" }, 404);
877
+ return { success: true };
878
+ },
879
+ /**
880
+ * Answer an offer
881
+ */
882
+ async answerOffer(params, message, signature, publicKey, storage, config) {
883
+ const { serviceFqn, offerId, sdp } = params;
884
+ const username = extractUsername(message);
885
+ if (!username) {
886
+ throw new Error("Username required");
887
+ }
888
+ const auth = await verifyAuth(username, message, signature, publicKey, storage);
889
+ if (!auth.valid) {
890
+ throw new Error(auth.error);
891
+ }
892
+ if (!sdp || typeof sdp !== "string" || sdp.length === 0) {
893
+ throw new Error("Invalid SDP");
894
+ }
895
+ if (sdp.length > 64 * 1024) {
896
+ throw new Error("SDP too large (max 64KB)");
897
+ }
898
+ const offer = await storage.getOfferById(offerId);
899
+ if (!offer) {
900
+ throw new Error("Offer not found");
901
+ }
902
+ if (offer.answererUsername) {
903
+ throw new Error("Offer already answered");
904
+ }
905
+ await storage.answerOffer(offerId, username, sdp);
906
+ return { success: true, offerId };
907
+ },
908
+ /**
909
+ * Get answer for an offer
910
+ */
911
+ async getOfferAnswer(params, message, signature, publicKey, storage, config) {
912
+ const { serviceFqn, offerId } = params;
913
+ const username = extractUsername(message);
914
+ if (!username) {
915
+ throw new Error("Username required");
916
+ }
917
+ const auth = await verifyAuth(username, message, signature, publicKey, storage);
918
+ if (!auth.valid) {
919
+ throw new Error(auth.error);
920
+ }
921
+ const offer = await storage.getOfferById(offerId);
922
+ if (!offer) {
923
+ throw new Error("Offer not found");
924
+ }
925
+ if (offer.username !== username) {
926
+ throw new Error("Not authorized to access this offer");
927
+ }
928
+ if (!offer.answererUsername || !offer.answerSdp) {
929
+ throw new Error("Offer not yet answered");
930
+ }
931
+ return {
932
+ sdp: offer.answerSdp,
933
+ offerId: offer.id,
934
+ answererId: offer.answererUsername,
935
+ answeredAt: offer.answeredAt
936
+ };
937
+ },
938
+ /**
939
+ * Combined polling for answers and ICE candidates
940
+ */
941
+ async poll(params, message, signature, publicKey, storage, config) {
942
+ const { since } = params;
943
+ const username = extractUsername(message);
944
+ if (!username) {
945
+ throw new Error("Username required");
946
+ }
947
+ const auth = await verifyAuth(username, message, signature, publicKey, storage);
948
+ if (!auth.valid) {
949
+ throw new Error(auth.error);
950
+ }
951
+ const sinceTimestamp = since || 0;
952
+ const answeredOffers = await storage.getAnsweredOffers(username);
953
+ const filteredAnswers = answeredOffers.filter(
954
+ (offer) => offer.answeredAt && offer.answeredAt > sinceTimestamp
955
+ );
956
+ const allOffers = await storage.getOffersByUsername(username);
957
+ const iceCandidatesByOffer = {};
958
+ for (const offer of allOffers) {
959
+ const offererCandidates = await storage.getIceCandidates(
960
+ offer.id,
961
+ "offerer",
962
+ sinceTimestamp
963
+ );
964
+ const answererCandidates = await storage.getIceCandidates(
965
+ offer.id,
966
+ "answerer",
967
+ sinceTimestamp
968
+ );
969
+ const allCandidates = [
970
+ ...offererCandidates.map((c) => ({
971
+ ...c,
972
+ role: "offerer"
973
+ })),
974
+ ...answererCandidates.map((c) => ({
975
+ ...c,
976
+ role: "answerer"
977
+ }))
978
+ ];
979
+ if (allCandidates.length > 0) {
980
+ const isOfferer = offer.username === username;
981
+ const filtered = allCandidates.filter(
982
+ (c) => isOfferer ? c.role === "answerer" : c.role === "offerer"
983
+ );
984
+ if (filtered.length > 0) {
985
+ iceCandidatesByOffer[offer.id] = filtered;
986
+ }
1090
987
  }
1091
- return c.json({
1092
- id: offer.id,
1093
- peerId: offer.peerId,
1094
- sdp: offer.sdp,
1095
- createdAt: offer.createdAt,
1096
- expiresAt: offer.expiresAt,
1097
- answererPeerId: offer.answererPeerId,
1098
- answered: !!offer.answererPeerId,
1099
- answerSdp: offer.answerSdp
1100
- }, 200);
1101
- } catch (err2) {
1102
- console.error("Error getting offer:", err2);
1103
- return c.json({ error: "Internal server error" }, 500);
1104
988
  }
1105
- });
1106
- app.delete("/offers/:offerId", authMiddleware, async (c) => {
1107
- try {
1108
- const offerId = c.req.param("offerId");
1109
- const peerId = getAuthenticatedPeerId(c);
1110
- const deleted = await storage.deleteOffer(offerId, peerId);
1111
- if (!deleted) {
1112
- return c.json({ error: "Offer not found or not owned by this peer" }, 404);
989
+ return {
990
+ answers: filteredAnswers.map((offer) => ({
991
+ offerId: offer.id,
992
+ serviceId: offer.serviceId,
993
+ answererId: offer.answererUsername,
994
+ sdp: offer.answerSdp,
995
+ answeredAt: offer.answeredAt
996
+ })),
997
+ iceCandidates: iceCandidatesByOffer
998
+ };
999
+ },
1000
+ /**
1001
+ * Add ICE candidates
1002
+ */
1003
+ async addIceCandidates(params, message, signature, publicKey, storage, config) {
1004
+ const { serviceFqn, offerId, candidates } = params;
1005
+ const username = extractUsername(message);
1006
+ if (!username) {
1007
+ throw new Error("Username required");
1008
+ }
1009
+ const auth = await verifyAuth(username, message, signature, publicKey, storage);
1010
+ if (!auth.valid) {
1011
+ throw new Error(auth.error);
1012
+ }
1013
+ if (!Array.isArray(candidates) || candidates.length === 0) {
1014
+ throw new Error("Missing or invalid required parameter: candidates");
1015
+ }
1016
+ candidates.forEach((candidate, index) => {
1017
+ if (!candidate || typeof candidate !== "object") {
1018
+ throw new Error(`Invalid candidate at index ${index}: must be an object`);
1113
1019
  }
1114
- return c.json({ success: true }, 200);
1115
- } catch (err2) {
1116
- console.error("Error deleting offer:", err2);
1117
- return c.json({ error: "Internal server error" }, 500);
1020
+ });
1021
+ const offer = await storage.getOfferById(offerId);
1022
+ if (!offer) {
1023
+ throw new Error("Offer not found");
1118
1024
  }
1119
- });
1120
- app.post("/offers/:offerId/answer", authMiddleware, async (c) => {
1025
+ const role = offer.username === username ? "offerer" : "answerer";
1026
+ const count = await storage.addIceCandidates(
1027
+ offerId,
1028
+ username,
1029
+ role,
1030
+ candidates
1031
+ );
1032
+ return { count, offerId };
1033
+ },
1034
+ /**
1035
+ * Get ICE candidates
1036
+ */
1037
+ async getIceCandidates(params, message, signature, publicKey, storage, config) {
1038
+ const { serviceFqn, offerId, since } = params;
1039
+ const username = extractUsername(message);
1040
+ if (!username) {
1041
+ throw new Error("Username required");
1042
+ }
1043
+ const auth = await verifyAuth(username, message, signature, publicKey, storage);
1044
+ if (!auth.valid) {
1045
+ throw new Error(auth.error);
1046
+ }
1047
+ const sinceTimestamp = since || 0;
1048
+ const offer = await storage.getOfferById(offerId);
1049
+ if (!offer) {
1050
+ throw new Error("Offer not found");
1051
+ }
1052
+ const isOfferer = offer.username === username;
1053
+ const role = isOfferer ? "answerer" : "offerer";
1054
+ const candidates = await storage.getIceCandidates(
1055
+ offerId,
1056
+ role,
1057
+ sinceTimestamp
1058
+ );
1059
+ return {
1060
+ candidates: candidates.map((c) => ({
1061
+ candidate: c.candidate,
1062
+ createdAt: c.createdAt
1063
+ })),
1064
+ offerId
1065
+ };
1066
+ }
1067
+ };
1068
+ async function handleRpc(requests, storage, config) {
1069
+ const responses = [];
1070
+ for (const request of requests) {
1121
1071
  try {
1122
- const offerId = c.req.param("offerId");
1123
- const body = await c.req.json();
1124
- const { sdp, secret } = body;
1125
- if (!sdp) {
1126
- return c.json({ error: "Missing required parameter: sdp" }, 400);
1072
+ const { method, message, signature, publicKey, params } = request;
1073
+ if (!method || typeof method !== "string") {
1074
+ responses.push({
1075
+ success: false,
1076
+ error: "Missing or invalid method"
1077
+ });
1078
+ continue;
1127
1079
  }
1128
- if (typeof sdp !== "string" || sdp.length === 0) {
1129
- return c.json({ error: "Invalid SDP" }, 400);
1080
+ if (!message || typeof message !== "string") {
1081
+ responses.push({
1082
+ success: false,
1083
+ error: "Missing or invalid message"
1084
+ });
1085
+ continue;
1130
1086
  }
1131
- if (sdp.length > 64 * 1024) {
1132
- return c.json({ error: "SDP too large (max 64KB)" }, 400);
1087
+ if (!signature || typeof signature !== "string") {
1088
+ responses.push({
1089
+ success: false,
1090
+ error: "Missing or invalid signature"
1091
+ });
1092
+ continue;
1133
1093
  }
1134
- const answererPeerId = getAuthenticatedPeerId(c);
1135
- const result = await storage.answerOffer(offerId, answererPeerId, sdp, secret);
1136
- if (!result.success) {
1137
- return c.json({ error: result.error }, 400);
1094
+ const handler = handlers[method];
1095
+ if (!handler) {
1096
+ responses.push({
1097
+ success: false,
1098
+ error: `Unknown method: ${method}`
1099
+ });
1100
+ continue;
1138
1101
  }
1139
- return c.json({ success: true }, 200);
1102
+ const result = await handler(
1103
+ params || {},
1104
+ message,
1105
+ signature,
1106
+ publicKey,
1107
+ storage,
1108
+ config
1109
+ );
1110
+ responses.push({
1111
+ success: true,
1112
+ result
1113
+ });
1140
1114
  } catch (err2) {
1141
- console.error("Error answering offer:", err2);
1142
- return c.json({ error: "Internal server error" }, 500);
1115
+ responses.push({
1116
+ success: false,
1117
+ error: err2.message || "Internal server error"
1118
+ });
1143
1119
  }
1144
- });
1145
- app.get("/offers/:offerId/answer", authMiddleware, async (c) => {
1146
- try {
1147
- const offerId = c.req.param("offerId");
1148
- const peerId = getAuthenticatedPeerId(c);
1149
- const offer = await storage.getOfferById(offerId);
1150
- if (!offer) {
1151
- return c.json({ error: "Offer not found" }, 404);
1152
- }
1153
- if (offer.peerId !== peerId) {
1154
- return c.json({ error: "Not authorized to view this answer" }, 403);
1120
+ }
1121
+ return responses;
1122
+ }
1123
+
1124
+ // src/app.ts
1125
+ var MAX_BATCH_SIZE = 100;
1126
+ function createApp(storage, config) {
1127
+ const app = new import_hono.Hono();
1128
+ app.use("/*", (0, import_cors.cors)({
1129
+ origin: (origin) => {
1130
+ if (config.corsOrigins.length === 1 && config.corsOrigins[0] === "*") {
1131
+ return origin;
1155
1132
  }
1156
- if (!offer.answererPeerId || !offer.answerSdp) {
1157
- return c.json({ error: "Offer not yet answered" }, 404);
1133
+ if (config.corsOrigins.includes(origin)) {
1134
+ return origin;
1158
1135
  }
1159
- return c.json({
1160
- offerId: offer.id,
1161
- answererId: offer.answererPeerId,
1162
- sdp: offer.answerSdp,
1163
- answeredAt: offer.answeredAt
1164
- }, 200);
1165
- } catch (err2) {
1166
- console.error("Error getting answer:", err2);
1167
- return c.json({ error: "Internal server error" }, 500);
1168
- }
1136
+ return config.corsOrigins[0];
1137
+ },
1138
+ allowMethods: ["GET", "POST", "OPTIONS"],
1139
+ allowHeaders: ["Content-Type", "Origin"],
1140
+ exposeHeaders: ["Content-Type"],
1141
+ credentials: false,
1142
+ maxAge: 86400
1143
+ }));
1144
+ app.get("/", (c) => {
1145
+ return c.json({
1146
+ version: config.version,
1147
+ name: "Rondevu",
1148
+ description: "WebRTC signaling with RPC interface and Ed25519 authentication"
1149
+ }, 200);
1169
1150
  });
1170
- app.post("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
1151
+ app.get("/health", (c) => {
1152
+ return c.json({
1153
+ status: "ok",
1154
+ timestamp: Date.now(),
1155
+ version: config.version
1156
+ }, 200);
1157
+ });
1158
+ app.post("/rpc", async (c) => {
1171
1159
  try {
1172
- const offerId = c.req.param("offerId");
1173
1160
  const body = await c.req.json();
1174
- const { candidates } = body;
1175
- if (!Array.isArray(candidates) || candidates.length === 0) {
1176
- return c.json({ error: "Missing or invalid required parameter: candidates" }, 400);
1161
+ const requests = Array.isArray(body) ? body : [body];
1162
+ if (requests.length === 0) {
1163
+ return c.json({ error: "Empty request array" }, 400);
1177
1164
  }
1178
- const peerId = getAuthenticatedPeerId(c);
1179
- const offer = await storage.getOfferById(offerId);
1180
- if (!offer) {
1181
- return c.json({ error: "Offer not found" }, 404);
1165
+ if (requests.length > MAX_BATCH_SIZE) {
1166
+ return c.json({ error: `Too many requests in batch (max ${MAX_BATCH_SIZE})` }, 400);
1182
1167
  }
1183
- const role = offer.peerId === peerId ? "offerer" : "answerer";
1184
- const count = await storage.addIceCandidates(offerId, peerId, role, candidates);
1185
- return c.json({ count }, 200);
1168
+ const responses = await handleRpc(requests, storage, config);
1169
+ return c.json(Array.isArray(body) ? responses : responses[0], 200);
1186
1170
  } catch (err2) {
1187
- console.error("Error adding ICE candidates:", err2);
1188
- return c.json({ error: "Internal server error" }, 500);
1189
- }
1190
- });
1191
- app.get("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
1192
- try {
1193
- const offerId = c.req.param("offerId");
1194
- const since = c.req.query("since");
1195
- const peerId = getAuthenticatedPeerId(c);
1196
- const offer = await storage.getOfferById(offerId);
1197
- if (!offer) {
1198
- return c.json({ error: "Offer not found" }, 404);
1199
- }
1200
- const targetRole = offer.peerId === peerId ? "answerer" : "offerer";
1201
- const sinceTimestamp = since ? parseInt(since, 10) : void 0;
1202
- const candidates = await storage.getIceCandidates(offerId, targetRole, sinceTimestamp);
1171
+ console.error("RPC error:", err2);
1203
1172
  return c.json({
1204
- candidates: candidates.map((c2) => ({
1205
- candidate: c2.candidate,
1206
- createdAt: c2.createdAt
1207
- }))
1208
- }, 200);
1209
- } catch (err2) {
1210
- console.error("Error getting ICE candidates:", err2);
1211
- return c.json({ error: "Internal server error" }, 500);
1173
+ success: false,
1174
+ error: "Invalid request format"
1175
+ }, 400);
1212
1176
  }
1213
1177
  });
1178
+ app.all("*", (c) => {
1179
+ return c.json({
1180
+ error: "Not found. Use POST /rpc for all API calls."
1181
+ }, 404);
1182
+ });
1214
1183
  return app;
1215
1184
  }
1216
1185
 
1217
1186
  // src/config.ts
1218
1187
  function loadConfig() {
1219
- let authSecret = process.env.AUTH_SECRET;
1220
- if (!authSecret) {
1221
- authSecret = generateSecretKey();
1222
- console.warn("WARNING: No AUTH_SECRET provided. Generated temporary secret:", authSecret);
1223
- console.warn("All peer credentials will be invalidated on server restart.");
1224
- console.warn("Set AUTH_SECRET environment variable to persist credentials across restarts.");
1225
- }
1226
1188
  return {
1227
1189
  port: parseInt(process.env.PORT || "3000", 10),
1228
1190
  storageType: process.env.STORAGE_TYPE || "sqlite",
1229
1191
  storagePath: process.env.STORAGE_PATH || ":memory:",
1230
1192
  corsOrigins: process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(",").map((o) => o.trim()) : ["*"],
1231
1193
  version: process.env.VERSION || "unknown",
1232
- authSecret,
1233
1194
  offerDefaultTtl: parseInt(process.env.OFFER_DEFAULT_TTL || "60000", 10),
1234
1195
  offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || "86400000", 10),
1235
1196
  offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || "60000", 10),
@@ -1275,30 +1236,29 @@ var SQLiteStorage = class {
1275
1236
  -- WebRTC signaling offers
1276
1237
  CREATE TABLE IF NOT EXISTS offers (
1277
1238
  id TEXT PRIMARY KEY,
1278
- peer_id TEXT NOT NULL,
1239
+ username TEXT NOT NULL,
1279
1240
  service_id TEXT,
1280
1241
  sdp TEXT NOT NULL,
1281
1242
  created_at INTEGER NOT NULL,
1282
1243
  expires_at INTEGER NOT NULL,
1283
1244
  last_seen INTEGER NOT NULL,
1284
- secret TEXT,
1285
- answerer_peer_id TEXT,
1245
+ answerer_username TEXT,
1286
1246
  answer_sdp TEXT,
1287
1247
  answered_at INTEGER,
1288
1248
  FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
1289
1249
  );
1290
1250
 
1291
- CREATE INDEX IF NOT EXISTS idx_offers_peer ON offers(peer_id);
1251
+ CREATE INDEX IF NOT EXISTS idx_offers_username ON offers(username);
1292
1252
  CREATE INDEX IF NOT EXISTS idx_offers_service ON offers(service_id);
1293
1253
  CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
1294
1254
  CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
1295
- CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
1255
+ CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_username);
1296
1256
 
1297
1257
  -- ICE candidates table
1298
1258
  CREATE TABLE IF NOT EXISTS ice_candidates (
1299
1259
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1300
1260
  offer_id TEXT NOT NULL,
1301
- peer_id TEXT NOT NULL,
1261
+ username TEXT NOT NULL,
1302
1262
  role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
1303
1263
  candidate TEXT NOT NULL,
1304
1264
  created_at INTEGER NOT NULL,
@@ -1306,7 +1266,7 @@ var SQLiteStorage = class {
1306
1266
  );
1307
1267
 
1308
1268
  CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
1309
- CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);
1269
+ CREATE INDEX IF NOT EXISTS idx_ice_username ON ice_candidates(username);
1310
1270
  CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
1311
1271
 
1312
1272
  -- Usernames table
@@ -1323,36 +1283,23 @@ var SQLiteStorage = class {
1323
1283
  CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
1324
1284
  CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
1325
1285
 
1326
- -- Services table (one service can have multiple offers)
1286
+ -- Services table (new schema with extracted fields for discovery)
1327
1287
  CREATE TABLE IF NOT EXISTS services (
1328
1288
  id TEXT PRIMARY KEY,
1329
- username TEXT NOT NULL,
1330
1289
  service_fqn TEXT NOT NULL,
1290
+ service_name TEXT NOT NULL,
1291
+ version TEXT NOT NULL,
1292
+ username TEXT NOT NULL,
1331
1293
  created_at INTEGER NOT NULL,
1332
1294
  expires_at INTEGER NOT NULL,
1333
- is_public INTEGER NOT NULL DEFAULT 0,
1334
- metadata TEXT,
1335
1295
  FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
1336
- UNIQUE(username, service_fqn)
1296
+ UNIQUE(service_fqn)
1337
1297
  );
1338
1298
 
1339
- CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
1340
1299
  CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
1300
+ CREATE INDEX IF NOT EXISTS idx_services_discovery ON services(service_name, version);
1301
+ CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
1341
1302
  CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
1342
-
1343
- -- Service index table (privacy layer)
1344
- CREATE TABLE IF NOT EXISTS service_index (
1345
- uuid TEXT PRIMARY KEY,
1346
- service_id TEXT NOT NULL,
1347
- username TEXT NOT NULL,
1348
- service_fqn TEXT NOT NULL,
1349
- created_at INTEGER NOT NULL,
1350
- expires_at INTEGER NOT NULL,
1351
- FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
1352
- );
1353
-
1354
- CREATE INDEX IF NOT EXISTS idx_service_index_username ON service_index(username);
1355
- CREATE INDEX IF NOT EXISTS idx_service_index_expires ON service_index(expires_at);
1356
1303
  `);
1357
1304
  this.db.pragma("foreign_keys = ON");
1358
1305
  }
@@ -1362,49 +1309,47 @@ var SQLiteStorage = class {
1362
1309
  const offersWithIds = await Promise.all(
1363
1310
  offers.map(async (offer) => ({
1364
1311
  ...offer,
1365
- serviceId: offer.serviceId || null,
1366
1312
  id: offer.id || await generateOfferHash(offer.sdp)
1367
1313
  }))
1368
1314
  );
1369
1315
  const transaction = this.db.transaction((offersWithIds2) => {
1370
1316
  const offerStmt = this.db.prepare(`
1371
- INSERT INTO offers (id, peer_id, service_id, sdp, created_at, expires_at, last_seen, secret)
1372
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1317
+ INSERT INTO offers (id, username, service_id, sdp, created_at, expires_at, last_seen)
1318
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1373
1319
  `);
1374
1320
  for (const offer of offersWithIds2) {
1375
1321
  const now = Date.now();
1376
1322
  offerStmt.run(
1377
1323
  offer.id,
1378
- offer.peerId,
1324
+ offer.username,
1379
1325
  offer.serviceId || null,
1380
1326
  offer.sdp,
1381
1327
  now,
1382
1328
  offer.expiresAt,
1383
- now,
1384
- offer.secret || null
1329
+ now
1385
1330
  );
1386
1331
  created.push({
1387
1332
  id: offer.id,
1388
- peerId: offer.peerId,
1389
- serviceId: offer.serviceId,
1333
+ username: offer.username,
1334
+ serviceId: offer.serviceId || void 0,
1335
+ serviceFqn: offer.serviceFqn,
1390
1336
  sdp: offer.sdp,
1391
1337
  createdAt: now,
1392
1338
  expiresAt: offer.expiresAt,
1393
- lastSeen: now,
1394
- secret: offer.secret
1339
+ lastSeen: now
1395
1340
  });
1396
1341
  }
1397
1342
  });
1398
1343
  transaction(offersWithIds);
1399
1344
  return created;
1400
1345
  }
1401
- async getOffersByPeerId(peerId) {
1346
+ async getOffersByUsername(username) {
1402
1347
  const stmt = this.db.prepare(`
1403
1348
  SELECT * FROM offers
1404
- WHERE peer_id = ? AND expires_at > ?
1349
+ WHERE username = ? AND expires_at > ?
1405
1350
  ORDER BY last_seen DESC
1406
1351
  `);
1407
- const rows = stmt.all(peerId, Date.now());
1352
+ const rows = stmt.all(username, Date.now());
1408
1353
  return rows.map((row) => this.rowToOffer(row));
1409
1354
  }
1410
1355
  async getOfferById(offerId) {
@@ -1418,12 +1363,12 @@ var SQLiteStorage = class {
1418
1363
  }
1419
1364
  return this.rowToOffer(row);
1420
1365
  }
1421
- async deleteOffer(offerId, ownerPeerId) {
1366
+ async deleteOffer(offerId, ownerUsername) {
1422
1367
  const stmt = this.db.prepare(`
1423
1368
  DELETE FROM offers
1424
- WHERE id = ? AND peer_id = ?
1369
+ WHERE id = ? AND username = ?
1425
1370
  `);
1426
- const result = stmt.run(offerId, ownerPeerId);
1371
+ const result = stmt.run(offerId, ownerUsername);
1427
1372
  return result.changes > 0;
1428
1373
  }
1429
1374
  async deleteExpiredOffers(now) {
@@ -1431,7 +1376,7 @@ var SQLiteStorage = class {
1431
1376
  const result = stmt.run(now);
1432
1377
  return result.changes;
1433
1378
  }
1434
- async answerOffer(offerId, answererPeerId, answerSdp, secret) {
1379
+ async answerOffer(offerId, answererUsername, answerSdp) {
1435
1380
  const offer = await this.getOfferById(offerId);
1436
1381
  if (!offer) {
1437
1382
  return {
@@ -1439,13 +1384,7 @@ var SQLiteStorage = class {
1439
1384
  error: "Offer not found or expired"
1440
1385
  };
1441
1386
  }
1442
- if (offer.secret && offer.secret !== secret) {
1443
- return {
1444
- success: false,
1445
- error: "Invalid or missing secret"
1446
- };
1447
- }
1448
- if (offer.answererPeerId) {
1387
+ if (offer.answererUsername) {
1449
1388
  return {
1450
1389
  success: false,
1451
1390
  error: "Offer already answered"
@@ -1453,10 +1392,10 @@ var SQLiteStorage = class {
1453
1392
  }
1454
1393
  const stmt = this.db.prepare(`
1455
1394
  UPDATE offers
1456
- SET answerer_peer_id = ?, answer_sdp = ?, answered_at = ?
1457
- WHERE id = ? AND answerer_peer_id IS NULL
1395
+ SET answerer_username = ?, answer_sdp = ?, answered_at = ?
1396
+ WHERE id = ? AND answerer_username IS NULL
1458
1397
  `);
1459
- const result = stmt.run(answererPeerId, answerSdp, Date.now(), offerId);
1398
+ const result = stmt.run(answererUsername, answerSdp, Date.now(), offerId);
1460
1399
  if (result.changes === 0) {
1461
1400
  return {
1462
1401
  success: false,
@@ -1465,19 +1404,19 @@ var SQLiteStorage = class {
1465
1404
  }
1466
1405
  return { success: true };
1467
1406
  }
1468
- async getAnsweredOffers(offererPeerId) {
1407
+ async getAnsweredOffers(offererUsername) {
1469
1408
  const stmt = this.db.prepare(`
1470
1409
  SELECT * FROM offers
1471
- WHERE peer_id = ? AND answerer_peer_id IS NOT NULL AND expires_at > ?
1410
+ WHERE username = ? AND answerer_username IS NOT NULL AND expires_at > ?
1472
1411
  ORDER BY answered_at DESC
1473
1412
  `);
1474
- const rows = stmt.all(offererPeerId, Date.now());
1413
+ const rows = stmt.all(offererUsername, Date.now());
1475
1414
  return rows.map((row) => this.rowToOffer(row));
1476
1415
  }
1477
1416
  // ===== ICE Candidate Management =====
1478
- async addIceCandidates(offerId, peerId, role, candidates) {
1417
+ async addIceCandidates(offerId, username, role, candidates) {
1479
1418
  const stmt = this.db.prepare(`
1480
- INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)
1419
+ INSERT INTO ice_candidates (offer_id, username, role, candidate, created_at)
1481
1420
  VALUES (?, ?, ?, ?, ?)
1482
1421
  `);
1483
1422
  const baseTimestamp = Date.now();
@@ -1485,7 +1424,7 @@ var SQLiteStorage = class {
1485
1424
  for (let i = 0; i < candidates2.length; i++) {
1486
1425
  stmt.run(
1487
1426
  offerId,
1488
- peerId,
1427
+ username,
1489
1428
  role,
1490
1429
  JSON.stringify(candidates2[i]),
1491
1430
  baseTimestamp + i
@@ -1511,7 +1450,7 @@ var SQLiteStorage = class {
1511
1450
  return rows.map((row) => ({
1512
1451
  id: row.id,
1513
1452
  offerId: row.offer_id,
1514
- peerId: row.peer_id,
1453
+ username: row.username,
1515
1454
  role: row.role,
1516
1455
  candidate: JSON.parse(row.candidate),
1517
1456
  createdAt: row.created_at
@@ -1587,63 +1526,74 @@ var SQLiteStorage = class {
1587
1526
  // ===== Service Management =====
1588
1527
  async createService(request) {
1589
1528
  const serviceId = (0, import_node_crypto.randomUUID)();
1590
- const indexUuid = (0, import_node_crypto.randomUUID)();
1591
1529
  const now = Date.now();
1592
- const offerRequests = request.offers.map((offer) => ({
1593
- ...offer,
1594
- serviceId
1595
- }));
1596
- const offers = await this.createOffers(offerRequests);
1530
+ const parsed = parseServiceFqn(request.serviceFqn);
1531
+ if (!parsed) {
1532
+ throw new Error(`Invalid service FQN: ${request.serviceFqn}`);
1533
+ }
1534
+ if (!parsed.username) {
1535
+ throw new Error(`Service FQN must include username: ${request.serviceFqn}`);
1536
+ }
1537
+ const { serviceName, version, username } = parsed;
1597
1538
  const transaction = this.db.transaction(() => {
1598
- const serviceStmt = this.db.prepare(`
1599
- INSERT INTO services (id, username, service_fqn, created_at, expires_at, is_public, metadata)
1539
+ const existingService = this.db.prepare(`
1540
+ SELECT id FROM services
1541
+ WHERE service_name = ? AND version = ? AND username = ?
1542
+ `).get(serviceName, version, username);
1543
+ if (existingService) {
1544
+ this.db.prepare(`
1545
+ DELETE FROM offers WHERE service_id = ?
1546
+ `).run(existingService.id);
1547
+ this.db.prepare(`
1548
+ DELETE FROM services WHERE id = ?
1549
+ `).run(existingService.id);
1550
+ }
1551
+ this.db.prepare(`
1552
+ INSERT INTO services (id, service_fqn, service_name, version, username, created_at, expires_at)
1600
1553
  VALUES (?, ?, ?, ?, ?, ?, ?)
1601
- `);
1602
- serviceStmt.run(
1554
+ `).run(
1603
1555
  serviceId,
1604
- request.username,
1605
- request.serviceFqn,
1606
- now,
1607
- request.expiresAt,
1608
- request.isPublic ? 1 : 0,
1609
- request.metadata || null
1610
- );
1611
- const indexStmt = this.db.prepare(`
1612
- INSERT INTO service_index (uuid, service_id, username, service_fqn, created_at, expires_at)
1613
- VALUES (?, ?, ?, ?, ?, ?)
1614
- `);
1615
- indexStmt.run(
1616
- indexUuid,
1617
- serviceId,
1618
- request.username,
1619
1556
  request.serviceFqn,
1557
+ serviceName,
1558
+ version,
1559
+ username,
1620
1560
  now,
1621
1561
  request.expiresAt
1622
1562
  );
1623
- this.touchUsername(request.username);
1563
+ const expiresAt = now + YEAR_IN_MS;
1564
+ this.db.prepare(`
1565
+ UPDATE usernames
1566
+ SET last_used = ?, expires_at = ?
1567
+ WHERE username = ? AND expires_at > ?
1568
+ `).run(now, expiresAt, username, now);
1624
1569
  });
1625
1570
  transaction();
1571
+ const offerRequests = request.offers.map((offer) => ({
1572
+ ...offer,
1573
+ serviceId
1574
+ }));
1575
+ const offers = await this.createOffers(offerRequests);
1626
1576
  return {
1627
1577
  service: {
1628
1578
  id: serviceId,
1629
- username: request.username,
1630
1579
  serviceFqn: request.serviceFqn,
1580
+ serviceName,
1581
+ version,
1582
+ username,
1631
1583
  createdAt: now,
1632
- expiresAt: request.expiresAt,
1633
- isPublic: request.isPublic || false,
1634
- metadata: request.metadata
1584
+ expiresAt: request.expiresAt
1635
1585
  },
1636
- indexUuid,
1637
1586
  offers
1638
1587
  };
1639
1588
  }
1640
- async batchCreateServices(requests) {
1641
- const results = [];
1642
- for (const request of requests) {
1643
- const result = await this.createService(request);
1644
- results.push(result);
1645
- }
1646
- return results;
1589
+ async getOffersForService(serviceId) {
1590
+ const stmt = this.db.prepare(`
1591
+ SELECT * FROM offers
1592
+ WHERE service_id = ? AND expires_at > ?
1593
+ ORDER BY created_at ASC
1594
+ `);
1595
+ const rows = stmt.all(serviceId, Date.now());
1596
+ return rows.map((row) => this.rowToOffer(row));
1647
1597
  }
1648
1598
  async getServiceById(serviceId) {
1649
1599
  const stmt = this.db.prepare(`
@@ -1656,42 +1606,49 @@ var SQLiteStorage = class {
1656
1606
  }
1657
1607
  return this.rowToService(row);
1658
1608
  }
1659
- async getServiceByUuid(uuid) {
1609
+ async getServiceByFqn(serviceFqn) {
1660
1610
  const stmt = this.db.prepare(`
1661
- SELECT s.* FROM services s
1662
- INNER JOIN service_index si ON s.id = si.service_id
1663
- WHERE si.uuid = ? AND s.expires_at > ?
1611
+ SELECT * FROM services
1612
+ WHERE service_fqn = ? AND expires_at > ?
1664
1613
  `);
1665
- const row = stmt.get(uuid, Date.now());
1614
+ const row = stmt.get(serviceFqn, Date.now());
1666
1615
  if (!row) {
1667
1616
  return null;
1668
1617
  }
1669
1618
  return this.rowToService(row);
1670
1619
  }
1671
- async listServicesForUsername(username) {
1620
+ async discoverServices(serviceName, version, limit, offset) {
1672
1621
  const stmt = this.db.prepare(`
1673
- SELECT si.uuid, s.is_public, s.service_fqn, s.metadata
1674
- FROM service_index si
1675
- INNER JOIN services s ON si.service_id = s.id
1676
- WHERE si.username = ? AND si.expires_at > ?
1622
+ SELECT DISTINCT s.* FROM services s
1623
+ INNER JOIN offers o ON o.service_id = s.id
1624
+ WHERE s.service_name = ?
1625
+ AND s.version = ?
1626
+ AND s.expires_at > ?
1627
+ AND o.answerer_username IS NULL
1628
+ AND o.expires_at > ?
1677
1629
  ORDER BY s.created_at DESC
1630
+ LIMIT ? OFFSET ?
1678
1631
  `);
1679
- const rows = stmt.all(username, Date.now());
1680
- return rows.map((row) => ({
1681
- uuid: row.uuid,
1682
- isPublic: row.is_public === 1,
1683
- serviceFqn: row.is_public === 1 ? row.service_fqn : void 0,
1684
- metadata: row.is_public === 1 ? row.metadata || void 0 : void 0
1685
- }));
1632
+ const rows = stmt.all(serviceName, version, Date.now(), Date.now(), limit, offset);
1633
+ return rows.map((row) => this.rowToService(row));
1686
1634
  }
1687
- async queryService(username, serviceFqn) {
1635
+ async getRandomService(serviceName, version) {
1688
1636
  const stmt = this.db.prepare(`
1689
- SELECT si.uuid FROM service_index si
1690
- INNER JOIN services s ON si.service_id = s.id
1691
- WHERE si.username = ? AND si.service_fqn = ? AND si.expires_at > ?
1637
+ SELECT s.* FROM services s
1638
+ INNER JOIN offers o ON o.service_id = s.id
1639
+ WHERE s.service_name = ?
1640
+ AND s.version = ?
1641
+ AND s.expires_at > ?
1642
+ AND o.answerer_username IS NULL
1643
+ AND o.expires_at > ?
1644
+ ORDER BY RANDOM()
1645
+ LIMIT 1
1692
1646
  `);
1693
- const row = stmt.get(username, serviceFqn, Date.now());
1694
- return row ? row.uuid : null;
1647
+ const row = stmt.get(serviceName, version, Date.now(), Date.now());
1648
+ if (!row) {
1649
+ return null;
1650
+ }
1651
+ return this.rowToService(row);
1695
1652
  }
1696
1653
  async deleteService(serviceId, username) {
1697
1654
  const stmt = this.db.prepare(`
@@ -1716,14 +1673,14 @@ var SQLiteStorage = class {
1716
1673
  rowToOffer(row) {
1717
1674
  return {
1718
1675
  id: row.id,
1719
- peerId: row.peer_id,
1676
+ username: row.username,
1720
1677
  serviceId: row.service_id || void 0,
1678
+ serviceFqn: row.service_fqn || void 0,
1721
1679
  sdp: row.sdp,
1722
1680
  createdAt: row.created_at,
1723
1681
  expiresAt: row.expires_at,
1724
1682
  lastSeen: row.last_seen,
1725
- secret: row.secret || void 0,
1726
- answererPeerId: row.answerer_peer_id || void 0,
1683
+ answererUsername: row.answerer_username || void 0,
1727
1684
  answerSdp: row.answer_sdp || void 0,
1728
1685
  answeredAt: row.answered_at || void 0
1729
1686
  };
@@ -1734,26 +1691,14 @@ var SQLiteStorage = class {
1734
1691
  rowToService(row) {
1735
1692
  return {
1736
1693
  id: row.id,
1737
- username: row.username,
1738
1694
  serviceFqn: row.service_fqn,
1695
+ serviceName: row.service_name,
1696
+ version: row.version,
1697
+ username: row.username,
1739
1698
  createdAt: row.created_at,
1740
- expiresAt: row.expires_at,
1741
- isPublic: row.is_public === 1,
1742
- metadata: row.metadata || void 0
1699
+ expiresAt: row.expires_at
1743
1700
  };
1744
1701
  }
1745
- /**
1746
- * Get all offers for a service
1747
- */
1748
- async getOffersForService(serviceId) {
1749
- const stmt = this.db.prepare(`
1750
- SELECT * FROM offers
1751
- WHERE service_id = ? AND expires_at > ?
1752
- ORDER BY created_at ASC
1753
- `);
1754
- const rows = stmt.all(serviceId, Date.now());
1755
- return rows.map((row) => this.rowToOffer(row));
1756
- }
1757
1702
  };
1758
1703
 
1759
1704
  // src/index.ts