@xtr-dev/rondevu-server 0.4.0 → 0.5.1

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
@@ -474,101 +474,35 @@ var wNAF = (n) => {
474
474
  };
475
475
 
476
476
  // src/crypto.ts
477
+ var import_node_buffer = require("node:buffer");
477
478
  hashes.sha512Async = async (message) => {
478
479
  return new Uint8Array(await crypto.subtle.digest("SHA-512", message));
479
480
  };
480
- var ALGORITHM = "AES-GCM";
481
- var IV_LENGTH = 12;
482
- var KEY_LENGTH = 32;
483
481
  var USERNAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
484
482
  var USERNAME_MIN_LENGTH = 3;
485
483
  var USERNAME_MAX_LENGTH = 32;
486
484
  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
485
  function base64ToBytes(base64) {
510
- const binString = atob(base64);
511
- return Uint8Array.from(binString, (char) => char.codePointAt(0));
486
+ return new Uint8Array(import_node_buffer.Buffer.from(base64, "base64"));
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" };
497
+ }
498
+ if (isNaN(timestamp)) {
499
+ return { valid: false, error: "Invalid timestamp in message" };
571
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,22 +523,28 @@ 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
  }
610
550
  function parseVersion(version) {
@@ -630,11 +570,25 @@ function isVersionCompatible(requested, available) {
630
570
  return true;
631
571
  }
632
572
  function parseServiceFqn(fqn) {
633
- const parts = fqn.split("@");
634
- if (parts.length !== 2) return null;
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;
635
588
  return {
636
- serviceName: parts[0],
637
- version: parts[1]
589
+ serviceName,
590
+ version,
591
+ username
638
592
  };
639
593
  }
640
594
  function validateTimestamp(timestamp) {
@@ -661,89 +615,516 @@ async function verifyEd25519Signature(publicKey, signature, message) {
661
615
  return false;
662
616
  }
663
617
  }
664
- async function validateUsernameClaim(username, publicKey, signature, message) {
665
- const usernameCheck = validateUsername(username);
666
- if (!usernameCheck.valid) {
667
- return usernameCheck;
668
- }
669
- const parts = message.split(":");
670
- if (parts.length !== 3 || parts[0] !== "claim" || parts[1] !== username) {
671
- return { valid: false, error: "Invalid message format (expected: claim:{username}:{timestamp})" };
672
- }
673
- const timestamp = parseInt(parts[2], 10);
674
- if (isNaN(timestamp)) {
675
- return { valid: false, error: "Invalid timestamp in message" };
676
- }
677
- const timestampCheck = validateTimestamp(timestamp);
678
- if (!timestampCheck.valid) {
679
- 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
+ }
680
648
  }
681
- const signatureValid = await verifyEd25519Signature(publicKey, signature, message);
682
- if (!signatureValid) {
649
+ const isValid = await verifyEd25519Signature(
650
+ usernameRecord.publicKey,
651
+ signature,
652
+ message
653
+ );
654
+ if (!isValid) {
683
655
  return { valid: false, error: "Invalid signature" };
684
656
  }
685
- return { valid: true };
686
- }
687
- async function validateServicePublish(username, serviceFqn, publicKey, signature, message) {
688
- const usernameCheck = validateUsername(username);
689
- if (!usernameCheck.valid) {
690
- return usernameCheck;
691
- }
692
- const parts = message.split(":");
693
- if (parts.length !== 4 || parts[0] !== "publish" || parts[1] !== username || parts[2] !== serviceFqn) {
694
- return { valid: false, error: "Invalid message format (expected: publish:{username}:{serviceFqn}:{timestamp})" };
695
- }
696
- const timestamp = parseInt(parts[3], 10);
697
- if (isNaN(timestamp)) {
698
- return { valid: false, error: "Invalid timestamp in message" };
699
- }
700
- const timestampCheck = validateTimestamp(timestamp);
701
- if (!timestampCheck.valid) {
702
- return timestampCheck;
703
- }
704
- const signatureValid = await verifyEd25519Signature(publicKey, signature, message);
705
- if (!signatureValid) {
706
- return { valid: false, error: "Invalid signature" };
657
+ const validation = validateAuthMessage(username, message);
658
+ if (!validation.valid) {
659
+ return { valid: false, error: validation.error };
707
660
  }
708
661
  return { valid: true };
709
662
  }
710
-
711
- // src/middleware/auth.ts
712
- function createAuthMiddleware(authSecret) {
713
- return async (c, next) => {
714
- const authHeader = c.req.header("Authorization");
715
- if (!authHeader) {
716
- return c.json({ error: "Missing Authorization header" }, 401);
717
- }
718
- const parts = authHeader.split(" ");
719
- if (parts.length !== 2 || parts[0] !== "Bearer") {
720
- return c.json({ error: "Invalid Authorization header format. Expected: Bearer {peerId}:{secret}" }, 401);
721
- }
722
- const credentials = parts[1].split(":");
723
- if (credentials.length !== 2) {
724
- return c.json({ error: "Invalid credentials format. Expected: {peerId}:{secret}" }, 401);
725
- }
726
- const [peerId, encryptedSecret] = credentials;
727
- const isValid = await validateCredentials(peerId, encryptedSecret, authSecret);
728
- if (!isValid) {
729
- return c.json({ error: "Invalid credentials" }, 401);
730
- }
731
- c.set("peerId", peerId);
732
- await next();
733
- };
663
+ function extractUsername(message) {
664
+ const parts = message.split(":");
665
+ if (parts.length < 2) return null;
666
+ return parts[1];
734
667
  }
735
- function getAuthenticatedPeerId(c) {
736
- const peerId = c.get("peerId");
737
- if (!peerId) {
738
- throw new Error("No authenticated peer ID in context");
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
+ };
680
+ }
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);
702
+ }
703
+ }
704
+ const fqnValidation = validateServiceFqn(serviceFqn);
705
+ if (!fqnValidation.valid) {
706
+ throw new Error(fqnValidation.error || "Invalid service FQN");
707
+ }
708
+ const parsed = parseServiceFqn(serviceFqn);
709
+ if (!parsed) {
710
+ throw new Error("Failed to parse service FQN");
711
+ }
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
+ }
746
+ }
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);
757
+ if (!service) {
758
+ throw new Error("Service not found");
759
+ }
760
+ const availableOffer2 = await findAvailableOffer(service);
761
+ if (!availableOffer2) {
762
+ throw new Error("Service has no available offers");
763
+ }
764
+ return buildServiceResponse(service, availableOffer2);
765
+ }
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`);
813
+ }
814
+ if (!offer.sdp || typeof offer.sdp !== "string") {
815
+ throw new Error(`Invalid offer at index ${index}: missing or invalid SDP`);
816
+ }
817
+ if (!offer.sdp.trim()) {
818
+ throw new Error(`Invalid offer at index ${index}: SDP cannot be empty`);
819
+ }
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) => ({
843
+ offerId: offer.id,
844
+ sdp: offer.sdp,
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");
860
+ }
861
+ const auth = await verifyAuth(username, message, signature, publicKey, storage);
862
+ if (!auth.valid) {
863
+ throw new Error(auth.error);
864
+ }
865
+ const parsed = parseServiceFqn(serviceFqn);
866
+ if (!parsed || !parsed.username) {
867
+ throw new Error("Service FQN must include username");
868
+ }
869
+ const service = await storage.getServiceByFqn(serviceFqn);
870
+ if (!service) {
871
+ throw new Error("Service not found");
872
+ }
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");
876
+ }
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
+ }
987
+ }
988
+ }
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`);
1019
+ }
1020
+ });
1021
+ const offer = await storage.getOfferById(offerId);
1022
+ if (!offer) {
1023
+ throw new Error("Offer not found");
1024
+ }
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
+ };
739
1066
  }
740
- return peerId;
1067
+ };
1068
+ async function handleRpc(requests, storage, config) {
1069
+ const responses = [];
1070
+ for (const request of requests) {
1071
+ try {
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;
1079
+ }
1080
+ if (!message || typeof message !== "string") {
1081
+ responses.push({
1082
+ success: false,
1083
+ error: "Missing or invalid message"
1084
+ });
1085
+ continue;
1086
+ }
1087
+ if (!signature || typeof signature !== "string") {
1088
+ responses.push({
1089
+ success: false,
1090
+ error: "Missing or invalid signature"
1091
+ });
1092
+ continue;
1093
+ }
1094
+ const handler = handlers[method];
1095
+ if (!handler) {
1096
+ responses.push({
1097
+ success: false,
1098
+ error: `Unknown method: ${method}`
1099
+ });
1100
+ continue;
1101
+ }
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
+ });
1114
+ } catch (err2) {
1115
+ responses.push({
1116
+ success: false,
1117
+ error: err2.message || "Internal server error"
1118
+ });
1119
+ }
1120
+ }
1121
+ return responses;
741
1122
  }
742
1123
 
743
1124
  // src/app.ts
1125
+ var MAX_BATCH_SIZE = 100;
744
1126
  function createApp(storage, config) {
745
1127
  const app = new import_hono.Hono();
746
- const authMiddleware = createAuthMiddleware(config.authSecret);
747
1128
  app.use("/*", (0, import_cors.cors)({
748
1129
  origin: (origin) => {
749
1130
  if (config.corsOrigins.length === 1 && config.corsOrigins[0] === "*") {
@@ -754,458 +1135,62 @@ function createApp(storage, config) {
754
1135
  }
755
1136
  return config.corsOrigins[0];
756
1137
  },
757
- allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
758
- allowHeaders: ["Content-Type", "Origin", "Authorization"],
1138
+ allowMethods: ["GET", "POST", "OPTIONS"],
1139
+ allowHeaders: ["Content-Type", "Origin"],
759
1140
  exposeHeaders: ["Content-Type"],
760
- maxAge: 600,
761
- credentials: true
1141
+ credentials: false,
1142
+ maxAge: 86400
762
1143
  }));
763
1144
  app.get("/", (c) => {
764
1145
  return c.json({
765
1146
  version: config.version,
766
1147
  name: "Rondevu",
767
- description: "DNS-like WebRTC signaling with username claiming and service discovery"
768
- });
1148
+ description: "WebRTC signaling with RPC interface and Ed25519 authentication"
1149
+ }, 200);
769
1150
  });
770
1151
  app.get("/health", (c) => {
771
1152
  return c.json({
772
1153
  status: "ok",
773
1154
  timestamp: Date.now(),
774
1155
  version: config.version
775
- });
1156
+ }, 200);
776
1157
  });
777
- app.post("/register", async (c) => {
1158
+ app.post("/rpc", async (c) => {
778
1159
  try {
779
- const peerId = generatePeerId();
780
- const secret = await encryptPeerId(peerId, config.authSecret);
781
- return c.json({
782
- peerId,
783
- secret
784
- }, 200);
785
- } catch (err2) {
786
- console.error("Error registering peer:", err2);
787
- return c.json({ error: "Internal server error" }, 500);
788
- }
789
- });
790
- app.get("/users/:username", async (c) => {
791
- try {
792
- const username = c.req.param("username");
793
- const claimed = await storage.getUsername(username);
794
- if (!claimed) {
795
- return c.json({
796
- username,
797
- available: true
798
- }, 200);
799
- }
800
- return c.json({
801
- username: claimed.username,
802
- available: false,
803
- claimedAt: claimed.claimedAt,
804
- expiresAt: claimed.expiresAt,
805
- publicKey: claimed.publicKey
806
- }, 200);
807
- } catch (err2) {
808
- console.error("Error checking username:", err2);
809
- return c.json({ error: "Internal server error" }, 500);
810
- }
811
- });
812
- app.post("/users/:username", async (c) => {
813
- try {
814
- const username = c.req.param("username");
815
1160
  const body = await c.req.json();
816
- const { publicKey, signature, message } = body;
817
- if (!publicKey || !signature || !message) {
818
- return c.json({ error: "Missing required parameters: publicKey, signature, message" }, 400);
1161
+ const requests = Array.isArray(body) ? body : [body];
1162
+ if (requests.length === 0) {
1163
+ return c.json({ error: "Empty request array" }, 400);
819
1164
  }
820
- const validation = await validateUsernameClaim(username, publicKey, signature, message);
821
- if (!validation.valid) {
822
- return c.json({ error: validation.error }, 400);
823
- }
824
- try {
825
- const claimed = await storage.claimUsername({
826
- username,
827
- publicKey,
828
- signature,
829
- message
830
- });
831
- return c.json({
832
- username: claimed.username,
833
- claimedAt: claimed.claimedAt,
834
- expiresAt: claimed.expiresAt
835
- }, 201);
836
- } catch (err2) {
837
- if (err2.message?.includes("already claimed")) {
838
- return c.json({ error: "Username already claimed by different public key" }, 409);
839
- }
840
- throw err2;
1165
+ if (requests.length > MAX_BATCH_SIZE) {
1166
+ return c.json({ error: `Too many requests in batch (max ${MAX_BATCH_SIZE})` }, 400);
841
1167
  }
1168
+ const responses = await handleRpc(requests, storage, config);
1169
+ return c.json(Array.isArray(body) ? responses : responses[0], 200);
842
1170
  } catch (err2) {
843
- console.error("Error claiming username:", err2);
844
- return c.json({ error: "Internal server error" }, 500);
845
- }
846
- });
847
- app.get("/users/:username/services/:fqn", async (c) => {
848
- try {
849
- const username = c.req.param("username");
850
- const serviceFqn = decodeURIComponent(c.req.param("fqn"));
851
- const parsed = parseServiceFqn(serviceFqn);
852
- if (!parsed) {
853
- return c.json({ error: "Invalid service FQN format" }, 400);
854
- }
855
- const { serviceName, version: requestedVersion } = parsed;
856
- const matchingServices = await storage.findServicesByName(username, serviceName);
857
- if (matchingServices.length === 0) {
858
- return c.json({ error: "Service not found" }, 404);
859
- }
860
- const compatibleServices = matchingServices.filter((service2) => {
861
- const serviceParsed = parseServiceFqn(service2.serviceFqn);
862
- if (!serviceParsed) return false;
863
- return isVersionCompatible(requestedVersion, serviceParsed.version);
864
- });
865
- if (compatibleServices.length === 0) {
866
- return c.json({
867
- error: "No compatible version found",
868
- message: `Requested ${serviceFqn}, but no compatible versions available`
869
- }, 404);
870
- }
871
- const service = compatibleServices[0];
872
- const uuid = await storage.queryService(username, service.serviceFqn);
873
- if (!uuid) {
874
- return c.json({ error: "Service index not found" }, 500);
875
- }
876
- const serviceOffers = await storage.getOffersForService(service.id);
877
- if (serviceOffers.length === 0) {
878
- return c.json({ error: "No offers found for this service" }, 404);
879
- }
880
- const availableOffer = serviceOffers.find((offer) => !offer.answererPeerId);
881
- if (!availableOffer) {
882
- return c.json({
883
- error: "No available offers",
884
- message: "All offers from this service are currently in use. Please try again later."
885
- }, 503);
886
- }
1171
+ console.error("RPC error:", err2);
887
1172
  return c.json({
888
- uuid,
889
- serviceId: service.id,
890
- username: service.username,
891
- serviceFqn: service.serviceFqn,
892
- offerId: availableOffer.id,
893
- sdp: availableOffer.sdp,
894
- isPublic: service.isPublic,
895
- metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
896
- createdAt: service.createdAt,
897
- expiresAt: service.expiresAt
898
- }, 200);
899
- } catch (err2) {
900
- console.error("Error getting service:", err2);
901
- return c.json({ error: "Internal server error" }, 500);
902
- }
903
- });
904
- app.post("/users/:username/services", authMiddleware, async (c) => {
905
- let serviceFqn;
906
- let createdOffers = [];
907
- try {
908
- const username = c.req.param("username");
909
- const body = await c.req.json();
910
- serviceFqn = body.serviceFqn;
911
- const { offers, ttl, isPublic, metadata, signature, message } = body;
912
- if (!serviceFqn || !offers || !Array.isArray(offers) || offers.length === 0) {
913
- return c.json({ error: "Missing required parameters: serviceFqn, offers (must be non-empty array)" }, 400);
914
- }
915
- const fqnValidation = validateServiceFqn(serviceFqn);
916
- if (!fqnValidation.valid) {
917
- return c.json({ error: fqnValidation.error }, 400);
918
- }
919
- if (!signature || !message) {
920
- return c.json({ error: "Missing signature or message for username verification" }, 400);
921
- }
922
- const usernameRecord = await storage.getUsername(username);
923
- if (!usernameRecord) {
924
- return c.json({ error: "Username not claimed" }, 404);
925
- }
926
- const signatureValidation = await validateServicePublish(username, serviceFqn, usernameRecord.publicKey, signature, message);
927
- if (!signatureValidation.valid) {
928
- return c.json({ error: "Invalid signature for username" }, 403);
929
- }
930
- const existingUuid = await storage.queryService(username, serviceFqn);
931
- if (existingUuid) {
932
- const existingService = await storage.getServiceByUuid(existingUuid);
933
- if (existingService) {
934
- await storage.deleteService(existingService.id, username);
935
- }
936
- }
937
- for (const offer of offers) {
938
- if (!offer.sdp || typeof offer.sdp !== "string" || offer.sdp.length === 0) {
939
- return c.json({ error: "Invalid SDP in offers array" }, 400);
940
- }
941
- if (offer.sdp.length > 64 * 1024) {
942
- return c.json({ error: "SDP too large (max 64KB)" }, 400);
943
- }
944
- }
945
- const peerId = getAuthenticatedPeerId(c);
946
- const offerTtl = Math.min(
947
- Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
948
- config.offerMaxTtl
949
- );
950
- const expiresAt = Date.now() + offerTtl;
951
- const offerRequests = offers.map((offer) => ({
952
- peerId,
953
- sdp: offer.sdp,
954
- expiresAt
955
- }));
956
- const result = await storage.createService({
957
- username,
958
- serviceFqn,
959
- expiresAt,
960
- isPublic: isPublic || false,
961
- metadata: metadata ? JSON.stringify(metadata) : void 0,
962
- offers: offerRequests
963
- });
964
- createdOffers = result.offers;
965
- return c.json({
966
- uuid: result.indexUuid,
967
- serviceFqn,
968
- username,
969
- serviceId: result.service.id,
970
- offers: result.offers.map((o) => ({
971
- offerId: o.id,
972
- sdp: o.sdp,
973
- createdAt: o.createdAt,
974
- expiresAt: o.expiresAt
975
- })),
976
- isPublic: result.service.isPublic,
977
- metadata,
978
- createdAt: result.service.createdAt,
979
- expiresAt: result.service.expiresAt
980
- }, 201);
981
- } catch (err2) {
982
- console.error("Error creating service:", err2);
983
- console.error("Error details:", {
984
- message: err2.message,
985
- stack: err2.stack,
986
- username: c.req.param("username"),
987
- serviceFqn,
988
- offerIds: createdOffers.map((o) => o.id)
989
- });
990
- return c.json({
991
- error: "Internal server error",
992
- details: err2.message
993
- }, 500);
994
- }
995
- });
996
- app.delete("/users/:username/services/:fqn", authMiddleware, async (c) => {
997
- try {
998
- const username = c.req.param("username");
999
- const serviceFqn = decodeURIComponent(c.req.param("fqn"));
1000
- const uuid = await storage.queryService(username, serviceFqn);
1001
- if (!uuid) {
1002
- return c.json({ error: "Service not found" }, 404);
1003
- }
1004
- const service = await storage.getServiceByUuid(uuid);
1005
- if (!service) {
1006
- return c.json({ error: "Service not found" }, 404);
1007
- }
1008
- const deleted = await storage.deleteService(service.id, username);
1009
- if (!deleted) {
1010
- return c.json({ error: "Service not found or not owned by this username" }, 404);
1011
- }
1012
- return c.json({ success: true }, 200);
1013
- } catch (err2) {
1014
- console.error("Error deleting service:", err2);
1015
- return c.json({ error: "Internal server error" }, 500);
1016
- }
1017
- });
1018
- app.get("/services/:uuid", async (c) => {
1019
- try {
1020
- const uuid = c.req.param("uuid");
1021
- const service = await storage.getServiceByUuid(uuid);
1022
- if (!service) {
1023
- return c.json({ error: "Service not found" }, 404);
1024
- }
1025
- const serviceOffers = await storage.getOffersForService(service.id);
1026
- if (serviceOffers.length === 0) {
1027
- return c.json({ error: "No offers found for this service" }, 404);
1028
- }
1029
- const availableOffer = serviceOffers.find((offer) => !offer.answererPeerId);
1030
- if (!availableOffer) {
1031
- return c.json({
1032
- error: "No available offers",
1033
- message: "All offers from this service are currently in use. Please try again later."
1034
- }, 503);
1035
- }
1036
- return c.json({
1037
- uuid,
1038
- serviceId: service.id,
1039
- username: service.username,
1040
- serviceFqn: service.serviceFqn,
1041
- offerId: availableOffer.id,
1042
- sdp: availableOffer.sdp,
1043
- isPublic: service.isPublic,
1044
- metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
1045
- createdAt: service.createdAt,
1046
- expiresAt: service.expiresAt
1047
- }, 200);
1048
- } catch (err2) {
1049
- console.error("Error getting service:", err2);
1050
- return c.json({ error: "Internal server error" }, 500);
1051
- }
1052
- });
1053
- app.post("/services/:uuid/answer", authMiddleware, async (c) => {
1054
- try {
1055
- const uuid = c.req.param("uuid");
1056
- const body = await c.req.json();
1057
- const { sdp } = body;
1058
- if (!sdp) {
1059
- return c.json({ error: "Missing required parameter: sdp" }, 400);
1060
- }
1061
- if (typeof sdp !== "string" || sdp.length === 0) {
1062
- return c.json({ error: "Invalid SDP" }, 400);
1063
- }
1064
- if (sdp.length > 64 * 1024) {
1065
- return c.json({ error: "SDP too large (max 64KB)" }, 400);
1066
- }
1067
- const service = await storage.getServiceByUuid(uuid);
1068
- if (!service) {
1069
- return c.json({ error: "Service not found" }, 404);
1070
- }
1071
- const serviceOffers = await storage.getOffersForService(service.id);
1072
- const availableOffer = serviceOffers.find((offer) => !offer.answererPeerId);
1073
- if (!availableOffer) {
1074
- return c.json({ error: "No available offers" }, 503);
1075
- }
1076
- const answererPeerId = getAuthenticatedPeerId(c);
1077
- const result = await storage.answerOffer(availableOffer.id, answererPeerId, sdp);
1078
- if (!result.success) {
1079
- return c.json({ error: result.error }, 400);
1080
- }
1081
- return c.json({
1082
- success: true,
1083
- offerId: availableOffer.id
1084
- }, 200);
1085
- } catch (err2) {
1086
- console.error("Error answering service:", err2);
1087
- return c.json({ error: "Internal server error" }, 500);
1088
- }
1089
- });
1090
- app.get("/services/:uuid/answer", authMiddleware, async (c) => {
1091
- try {
1092
- const uuid = c.req.param("uuid");
1093
- const peerId = getAuthenticatedPeerId(c);
1094
- const service = await storage.getServiceByUuid(uuid);
1095
- if (!service) {
1096
- return c.json({ error: "Service not found" }, 404);
1097
- }
1098
- const serviceOffers = await storage.getOffersForService(service.id);
1099
- const myOffer = serviceOffers.find((offer) => offer.peerId === peerId && offer.answererPeerId);
1100
- if (!myOffer || !myOffer.answerSdp) {
1101
- return c.json({ error: "Offer not yet answered" }, 404);
1102
- }
1103
- return c.json({
1104
- offerId: myOffer.id,
1105
- answererId: myOffer.answererPeerId,
1106
- sdp: myOffer.answerSdp,
1107
- answeredAt: myOffer.answeredAt
1108
- }, 200);
1109
- } catch (err2) {
1110
- console.error("Error getting service answer:", err2);
1111
- return c.json({ error: "Internal server error" }, 500);
1112
- }
1113
- });
1114
- app.post("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
1115
- try {
1116
- const uuid = c.req.param("uuid");
1117
- const body = await c.req.json();
1118
- const { candidates, offerId } = body;
1119
- if (!Array.isArray(candidates) || candidates.length === 0) {
1120
- return c.json({ error: "Missing or invalid required parameter: candidates" }, 400);
1121
- }
1122
- const peerId = getAuthenticatedPeerId(c);
1123
- const service = await storage.getServiceByUuid(uuid);
1124
- if (!service) {
1125
- return c.json({ error: "Service not found" }, 404);
1126
- }
1127
- let targetOfferId = offerId;
1128
- if (!targetOfferId) {
1129
- const serviceOffers = await storage.getOffersForService(service.id);
1130
- const myOffer = serviceOffers.find(
1131
- (offer2) => offer2.peerId === peerId || offer2.answererPeerId === peerId
1132
- );
1133
- if (!myOffer) {
1134
- return c.json({ error: "No offer found for this peer" }, 404);
1135
- }
1136
- targetOfferId = myOffer.id;
1137
- }
1138
- const offer = await storage.getOfferById(targetOfferId);
1139
- if (!offer) {
1140
- return c.json({ error: "Offer not found" }, 404);
1141
- }
1142
- const role = offer.peerId === peerId ? "offerer" : "answerer";
1143
- const count = await storage.addIceCandidates(targetOfferId, peerId, role, candidates);
1144
- return c.json({ count, offerId: targetOfferId }, 200);
1145
- } catch (err2) {
1146
- console.error("Error adding ICE candidates to service:", err2);
1147
- return c.json({ error: "Internal server error" }, 500);
1173
+ success: false,
1174
+ error: "Invalid request format"
1175
+ }, 400);
1148
1176
  }
1149
1177
  });
1150
- app.get("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
1151
- try {
1152
- const uuid = c.req.param("uuid");
1153
- const since = c.req.query("since");
1154
- const offerId = c.req.query("offerId");
1155
- const peerId = getAuthenticatedPeerId(c);
1156
- const service = await storage.getServiceByUuid(uuid);
1157
- if (!service) {
1158
- return c.json({ error: "Service not found" }, 404);
1159
- }
1160
- let targetOfferId = offerId;
1161
- if (!targetOfferId) {
1162
- const serviceOffers = await storage.getOffersForService(service.id);
1163
- const myOffer = serviceOffers.find(
1164
- (offer2) => offer2.peerId === peerId || offer2.answererPeerId === peerId
1165
- );
1166
- if (!myOffer) {
1167
- return c.json({ error: "No offer found for this peer" }, 404);
1168
- }
1169
- targetOfferId = myOffer.id;
1170
- }
1171
- const offer = await storage.getOfferById(targetOfferId);
1172
- if (!offer) {
1173
- return c.json({ error: "Offer not found" }, 404);
1174
- }
1175
- const targetRole = offer.peerId === peerId ? "answerer" : "offerer";
1176
- const sinceTimestamp = since ? parseInt(since, 10) : void 0;
1177
- const candidates = await storage.getIceCandidates(targetOfferId, targetRole, sinceTimestamp);
1178
- return c.json({
1179
- candidates: candidates.map((c2) => ({
1180
- candidate: c2.candidate,
1181
- createdAt: c2.createdAt
1182
- })),
1183
- offerId: targetOfferId
1184
- }, 200);
1185
- } catch (err2) {
1186
- console.error("Error getting ICE candidates for service:", err2);
1187
- return c.json({ error: "Internal server error" }, 500);
1188
- }
1178
+ app.all("*", (c) => {
1179
+ return c.json({
1180
+ error: "Not found. Use POST /rpc for all API calls."
1181
+ }, 404);
1189
1182
  });
1190
1183
  return app;
1191
1184
  }
1192
1185
 
1193
1186
  // src/config.ts
1194
1187
  function loadConfig() {
1195
- let authSecret = process.env.AUTH_SECRET;
1196
- if (!authSecret) {
1197
- authSecret = generateSecretKey();
1198
- console.warn("WARNING: No AUTH_SECRET provided. Generated temporary secret:", authSecret);
1199
- console.warn("All peer credentials will be invalidated on server restart.");
1200
- console.warn("Set AUTH_SECRET environment variable to persist credentials across restarts.");
1201
- }
1202
1188
  return {
1203
1189
  port: parseInt(process.env.PORT || "3000", 10),
1204
1190
  storageType: process.env.STORAGE_TYPE || "sqlite",
1205
1191
  storagePath: process.env.STORAGE_PATH || ":memory:",
1206
1192
  corsOrigins: process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(",").map((o) => o.trim()) : ["*"],
1207
1193
  version: process.env.VERSION || "unknown",
1208
- authSecret,
1209
1194
  offerDefaultTtl: parseInt(process.env.OFFER_DEFAULT_TTL || "60000", 10),
1210
1195
  offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || "86400000", 10),
1211
1196
  offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || "60000", 10),
@@ -1251,30 +1236,29 @@ var SQLiteStorage = class {
1251
1236
  -- WebRTC signaling offers
1252
1237
  CREATE TABLE IF NOT EXISTS offers (
1253
1238
  id TEXT PRIMARY KEY,
1254
- peer_id TEXT NOT NULL,
1239
+ username TEXT NOT NULL,
1255
1240
  service_id TEXT,
1256
1241
  sdp TEXT NOT NULL,
1257
1242
  created_at INTEGER NOT NULL,
1258
1243
  expires_at INTEGER NOT NULL,
1259
1244
  last_seen INTEGER NOT NULL,
1260
- secret TEXT,
1261
- answerer_peer_id TEXT,
1245
+ answerer_username TEXT,
1262
1246
  answer_sdp TEXT,
1263
1247
  answered_at INTEGER,
1264
1248
  FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
1265
1249
  );
1266
1250
 
1267
- CREATE INDEX IF NOT EXISTS idx_offers_peer ON offers(peer_id);
1251
+ CREATE INDEX IF NOT EXISTS idx_offers_username ON offers(username);
1268
1252
  CREATE INDEX IF NOT EXISTS idx_offers_service ON offers(service_id);
1269
1253
  CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
1270
1254
  CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
1271
- 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);
1272
1256
 
1273
1257
  -- ICE candidates table
1274
1258
  CREATE TABLE IF NOT EXISTS ice_candidates (
1275
1259
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1276
1260
  offer_id TEXT NOT NULL,
1277
- peer_id TEXT NOT NULL,
1261
+ username TEXT NOT NULL,
1278
1262
  role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
1279
1263
  candidate TEXT NOT NULL,
1280
1264
  created_at INTEGER NOT NULL,
@@ -1282,7 +1266,7 @@ var SQLiteStorage = class {
1282
1266
  );
1283
1267
 
1284
1268
  CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
1285
- 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);
1286
1270
  CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
1287
1271
 
1288
1272
  -- Usernames table
@@ -1299,36 +1283,23 @@ var SQLiteStorage = class {
1299
1283
  CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
1300
1284
  CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
1301
1285
 
1302
- -- Services table (one service can have multiple offers)
1286
+ -- Services table (new schema with extracted fields for discovery)
1303
1287
  CREATE TABLE IF NOT EXISTS services (
1304
1288
  id TEXT PRIMARY KEY,
1305
- username TEXT NOT NULL,
1306
1289
  service_fqn TEXT NOT NULL,
1290
+ service_name TEXT NOT NULL,
1291
+ version TEXT NOT NULL,
1292
+ username TEXT NOT NULL,
1307
1293
  created_at INTEGER NOT NULL,
1308
1294
  expires_at INTEGER NOT NULL,
1309
- is_public INTEGER NOT NULL DEFAULT 0,
1310
- metadata TEXT,
1311
1295
  FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
1312
- UNIQUE(username, service_fqn)
1296
+ UNIQUE(service_fqn)
1313
1297
  );
1314
1298
 
1315
- CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
1316
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);
1317
1302
  CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
1318
-
1319
- -- Service index table (privacy layer)
1320
- CREATE TABLE IF NOT EXISTS service_index (
1321
- uuid TEXT PRIMARY KEY,
1322
- service_id TEXT NOT NULL,
1323
- username TEXT NOT NULL,
1324
- service_fqn TEXT NOT NULL,
1325
- created_at INTEGER NOT NULL,
1326
- expires_at INTEGER NOT NULL,
1327
- FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
1328
- );
1329
-
1330
- CREATE INDEX IF NOT EXISTS idx_service_index_username ON service_index(username);
1331
- CREATE INDEX IF NOT EXISTS idx_service_index_expires ON service_index(expires_at);
1332
1303
  `);
1333
1304
  this.db.pragma("foreign_keys = ON");
1334
1305
  }
@@ -1343,43 +1314,42 @@ var SQLiteStorage = class {
1343
1314
  );
1344
1315
  const transaction = this.db.transaction((offersWithIds2) => {
1345
1316
  const offerStmt = this.db.prepare(`
1346
- INSERT INTO offers (id, peer_id, service_id, sdp, created_at, expires_at, last_seen, secret)
1347
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1317
+ INSERT INTO offers (id, username, service_id, sdp, created_at, expires_at, last_seen)
1318
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1348
1319
  `);
1349
1320
  for (const offer of offersWithIds2) {
1350
1321
  const now = Date.now();
1351
1322
  offerStmt.run(
1352
1323
  offer.id,
1353
- offer.peerId,
1324
+ offer.username,
1354
1325
  offer.serviceId || null,
1355
1326
  offer.sdp,
1356
1327
  now,
1357
1328
  offer.expiresAt,
1358
- now,
1359
- offer.secret || null
1329
+ now
1360
1330
  );
1361
1331
  created.push({
1362
1332
  id: offer.id,
1363
- peerId: offer.peerId,
1333
+ username: offer.username,
1364
1334
  serviceId: offer.serviceId || void 0,
1335
+ serviceFqn: offer.serviceFqn,
1365
1336
  sdp: offer.sdp,
1366
1337
  createdAt: now,
1367
1338
  expiresAt: offer.expiresAt,
1368
- lastSeen: now,
1369
- secret: offer.secret
1339
+ lastSeen: now
1370
1340
  });
1371
1341
  }
1372
1342
  });
1373
1343
  transaction(offersWithIds);
1374
1344
  return created;
1375
1345
  }
1376
- async getOffersByPeerId(peerId) {
1346
+ async getOffersByUsername(username) {
1377
1347
  const stmt = this.db.prepare(`
1378
1348
  SELECT * FROM offers
1379
- WHERE peer_id = ? AND expires_at > ?
1349
+ WHERE username = ? AND expires_at > ?
1380
1350
  ORDER BY last_seen DESC
1381
1351
  `);
1382
- const rows = stmt.all(peerId, Date.now());
1352
+ const rows = stmt.all(username, Date.now());
1383
1353
  return rows.map((row) => this.rowToOffer(row));
1384
1354
  }
1385
1355
  async getOfferById(offerId) {
@@ -1393,12 +1363,12 @@ var SQLiteStorage = class {
1393
1363
  }
1394
1364
  return this.rowToOffer(row);
1395
1365
  }
1396
- async deleteOffer(offerId, ownerPeerId) {
1366
+ async deleteOffer(offerId, ownerUsername) {
1397
1367
  const stmt = this.db.prepare(`
1398
1368
  DELETE FROM offers
1399
- WHERE id = ? AND peer_id = ?
1369
+ WHERE id = ? AND username = ?
1400
1370
  `);
1401
- const result = stmt.run(offerId, ownerPeerId);
1371
+ const result = stmt.run(offerId, ownerUsername);
1402
1372
  return result.changes > 0;
1403
1373
  }
1404
1374
  async deleteExpiredOffers(now) {
@@ -1406,7 +1376,7 @@ var SQLiteStorage = class {
1406
1376
  const result = stmt.run(now);
1407
1377
  return result.changes;
1408
1378
  }
1409
- async answerOffer(offerId, answererPeerId, answerSdp, secret) {
1379
+ async answerOffer(offerId, answererUsername, answerSdp) {
1410
1380
  const offer = await this.getOfferById(offerId);
1411
1381
  if (!offer) {
1412
1382
  return {
@@ -1414,13 +1384,7 @@ var SQLiteStorage = class {
1414
1384
  error: "Offer not found or expired"
1415
1385
  };
1416
1386
  }
1417
- if (offer.secret && offer.secret !== secret) {
1418
- return {
1419
- success: false,
1420
- error: "Invalid or missing secret"
1421
- };
1422
- }
1423
- if (offer.answererPeerId) {
1387
+ if (offer.answererUsername) {
1424
1388
  return {
1425
1389
  success: false,
1426
1390
  error: "Offer already answered"
@@ -1428,10 +1392,10 @@ var SQLiteStorage = class {
1428
1392
  }
1429
1393
  const stmt = this.db.prepare(`
1430
1394
  UPDATE offers
1431
- SET answerer_peer_id = ?, answer_sdp = ?, answered_at = ?
1432
- WHERE id = ? AND answerer_peer_id IS NULL
1395
+ SET answerer_username = ?, answer_sdp = ?, answered_at = ?
1396
+ WHERE id = ? AND answerer_username IS NULL
1433
1397
  `);
1434
- const result = stmt.run(answererPeerId, answerSdp, Date.now(), offerId);
1398
+ const result = stmt.run(answererUsername, answerSdp, Date.now(), offerId);
1435
1399
  if (result.changes === 0) {
1436
1400
  return {
1437
1401
  success: false,
@@ -1440,19 +1404,19 @@ var SQLiteStorage = class {
1440
1404
  }
1441
1405
  return { success: true };
1442
1406
  }
1443
- async getAnsweredOffers(offererPeerId) {
1407
+ async getAnsweredOffers(offererUsername) {
1444
1408
  const stmt = this.db.prepare(`
1445
1409
  SELECT * FROM offers
1446
- 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 > ?
1447
1411
  ORDER BY answered_at DESC
1448
1412
  `);
1449
- const rows = stmt.all(offererPeerId, Date.now());
1413
+ const rows = stmt.all(offererUsername, Date.now());
1450
1414
  return rows.map((row) => this.rowToOffer(row));
1451
1415
  }
1452
1416
  // ===== ICE Candidate Management =====
1453
- async addIceCandidates(offerId, peerId, role, candidates) {
1417
+ async addIceCandidates(offerId, username, role, candidates) {
1454
1418
  const stmt = this.db.prepare(`
1455
- INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)
1419
+ INSERT INTO ice_candidates (offer_id, username, role, candidate, created_at)
1456
1420
  VALUES (?, ?, ?, ?, ?)
1457
1421
  `);
1458
1422
  const baseTimestamp = Date.now();
@@ -1460,7 +1424,7 @@ var SQLiteStorage = class {
1460
1424
  for (let i = 0; i < candidates2.length; i++) {
1461
1425
  stmt.run(
1462
1426
  offerId,
1463
- peerId,
1427
+ username,
1464
1428
  role,
1465
1429
  JSON.stringify(candidates2[i]),
1466
1430
  baseTimestamp + i
@@ -1486,7 +1450,7 @@ var SQLiteStorage = class {
1486
1450
  return rows.map((row) => ({
1487
1451
  id: row.id,
1488
1452
  offerId: row.offer_id,
1489
- peerId: row.peer_id,
1453
+ username: row.username,
1490
1454
  role: row.role,
1491
1455
  candidate: JSON.parse(row.candidate),
1492
1456
  createdAt: row.created_at
@@ -1562,63 +1526,74 @@ var SQLiteStorage = class {
1562
1526
  // ===== Service Management =====
1563
1527
  async createService(request) {
1564
1528
  const serviceId = (0, import_node_crypto.randomUUID)();
1565
- const indexUuid = (0, import_node_crypto.randomUUID)();
1566
1529
  const now = Date.now();
1567
- const offerRequests = request.offers.map((offer) => ({
1568
- ...offer,
1569
- serviceId
1570
- }));
1571
- 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;
1572
1538
  const transaction = this.db.transaction(() => {
1573
- const serviceStmt = this.db.prepare(`
1574
- 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)
1575
1553
  VALUES (?, ?, ?, ?, ?, ?, ?)
1576
- `);
1577
- serviceStmt.run(
1554
+ `).run(
1578
1555
  serviceId,
1579
- request.username,
1580
- request.serviceFqn,
1581
- now,
1582
- request.expiresAt,
1583
- request.isPublic ? 1 : 0,
1584
- request.metadata || null
1585
- );
1586
- const indexStmt = this.db.prepare(`
1587
- INSERT INTO service_index (uuid, service_id, username, service_fqn, created_at, expires_at)
1588
- VALUES (?, ?, ?, ?, ?, ?)
1589
- `);
1590
- indexStmt.run(
1591
- indexUuid,
1592
- serviceId,
1593
- request.username,
1594
1556
  request.serviceFqn,
1557
+ serviceName,
1558
+ version,
1559
+ username,
1595
1560
  now,
1596
1561
  request.expiresAt
1597
1562
  );
1598
- 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);
1599
1569
  });
1600
1570
  transaction();
1571
+ const offerRequests = request.offers.map((offer) => ({
1572
+ ...offer,
1573
+ serviceId
1574
+ }));
1575
+ const offers = await this.createOffers(offerRequests);
1601
1576
  return {
1602
1577
  service: {
1603
1578
  id: serviceId,
1604
- username: request.username,
1605
1579
  serviceFqn: request.serviceFqn,
1580
+ serviceName,
1581
+ version,
1582
+ username,
1606
1583
  createdAt: now,
1607
- expiresAt: request.expiresAt,
1608
- isPublic: request.isPublic || false,
1609
- metadata: request.metadata
1584
+ expiresAt: request.expiresAt
1610
1585
  },
1611
- indexUuid,
1612
1586
  offers
1613
1587
  };
1614
1588
  }
1615
- async batchCreateServices(requests) {
1616
- const results = [];
1617
- for (const request of requests) {
1618
- const result = await this.createService(request);
1619
- results.push(result);
1620
- }
1621
- 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));
1622
1597
  }
1623
1598
  async getServiceById(serviceId) {
1624
1599
  const stmt = this.db.prepare(`
@@ -1631,51 +1606,49 @@ var SQLiteStorage = class {
1631
1606
  }
1632
1607
  return this.rowToService(row);
1633
1608
  }
1634
- async getServiceByUuid(uuid) {
1609
+ async getServiceByFqn(serviceFqn) {
1635
1610
  const stmt = this.db.prepare(`
1636
- SELECT s.* FROM services s
1637
- INNER JOIN service_index si ON s.id = si.service_id
1638
- WHERE si.uuid = ? AND s.expires_at > ?
1611
+ SELECT * FROM services
1612
+ WHERE service_fqn = ? AND expires_at > ?
1639
1613
  `);
1640
- const row = stmt.get(uuid, Date.now());
1614
+ const row = stmt.get(serviceFqn, Date.now());
1641
1615
  if (!row) {
1642
1616
  return null;
1643
1617
  }
1644
1618
  return this.rowToService(row);
1645
1619
  }
1646
- async listServicesForUsername(username) {
1620
+ async discoverServices(serviceName, version, limit, offset) {
1647
1621
  const stmt = this.db.prepare(`
1648
- SELECT si.uuid, s.is_public, s.service_fqn, s.metadata
1649
- FROM service_index si
1650
- INNER JOIN services s ON si.service_id = s.id
1651
- 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 > ?
1652
1629
  ORDER BY s.created_at DESC
1630
+ LIMIT ? OFFSET ?
1653
1631
  `);
1654
- const rows = stmt.all(username, Date.now());
1655
- return rows.map((row) => ({
1656
- uuid: row.uuid,
1657
- isPublic: row.is_public === 1,
1658
- serviceFqn: row.is_public === 1 ? row.service_fqn : void 0,
1659
- metadata: row.is_public === 1 ? row.metadata || void 0 : void 0
1660
- }));
1661
- }
1662
- async queryService(username, serviceFqn) {
1663
- const stmt = this.db.prepare(`
1664
- SELECT si.uuid FROM service_index si
1665
- INNER JOIN services s ON si.service_id = s.id
1666
- WHERE si.username = ? AND si.service_fqn = ? AND si.expires_at > ?
1667
- `);
1668
- const row = stmt.get(username, serviceFqn, Date.now());
1669
- return row ? row.uuid : null;
1632
+ const rows = stmt.all(serviceName, version, Date.now(), Date.now(), limit, offset);
1633
+ return rows.map((row) => this.rowToService(row));
1670
1634
  }
1671
- async findServicesByName(username, serviceName) {
1635
+ async getRandomService(serviceName, version) {
1672
1636
  const stmt = this.db.prepare(`
1673
- SELECT * FROM services
1674
- WHERE username = ? AND service_fqn LIKE ? AND expires_at > ?
1675
- ORDER BY created_at DESC
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
1676
1646
  `);
1677
- const rows = stmt.all(username, `${serviceName}@%`, Date.now());
1678
- return rows.map((row) => this.rowToService(row));
1647
+ const row = stmt.get(serviceName, version, Date.now(), Date.now());
1648
+ if (!row) {
1649
+ return null;
1650
+ }
1651
+ return this.rowToService(row);
1679
1652
  }
1680
1653
  async deleteService(serviceId, username) {
1681
1654
  const stmt = this.db.prepare(`
@@ -1700,14 +1673,14 @@ var SQLiteStorage = class {
1700
1673
  rowToOffer(row) {
1701
1674
  return {
1702
1675
  id: row.id,
1703
- peerId: row.peer_id,
1676
+ username: row.username,
1704
1677
  serviceId: row.service_id || void 0,
1678
+ serviceFqn: row.service_fqn || void 0,
1705
1679
  sdp: row.sdp,
1706
1680
  createdAt: row.created_at,
1707
1681
  expiresAt: row.expires_at,
1708
1682
  lastSeen: row.last_seen,
1709
- secret: row.secret || void 0,
1710
- answererPeerId: row.answerer_peer_id || void 0,
1683
+ answererUsername: row.answerer_username || void 0,
1711
1684
  answerSdp: row.answer_sdp || void 0,
1712
1685
  answeredAt: row.answered_at || void 0
1713
1686
  };
@@ -1718,26 +1691,14 @@ var SQLiteStorage = class {
1718
1691
  rowToService(row) {
1719
1692
  return {
1720
1693
  id: row.id,
1721
- username: row.username,
1722
1694
  serviceFqn: row.service_fqn,
1695
+ serviceName: row.service_name,
1696
+ version: row.version,
1697
+ username: row.username,
1723
1698
  createdAt: row.created_at,
1724
- expiresAt: row.expires_at,
1725
- isPublic: row.is_public === 1,
1726
- metadata: row.metadata || void 0
1699
+ expiresAt: row.expires_at
1727
1700
  };
1728
1701
  }
1729
- /**
1730
- * Get all offers for a service
1731
- */
1732
- async getOffersForService(serviceId) {
1733
- const stmt = this.db.prepare(`
1734
- SELECT * FROM offers
1735
- WHERE service_id = ? AND expires_at > ?
1736
- ORDER BY created_at ASC
1737
- `);
1738
- const rows = stmt.all(serviceId, Date.now());
1739
- return rows.map((row) => this.rowToOffer(row));
1740
- }
1741
1702
  };
1742
1703
 
1743
1704
  // src/index.ts