@xtr-dev/rondevu-server 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -607,6 +607,36 @@ function validateServiceFqn(fqn) {
607
607
  }
608
608
  return { valid: true };
609
609
  }
610
+ function parseVersion(version) {
611
+ const match = version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-z0-9.-]+)?$/);
612
+ if (!match) return null;
613
+ return {
614
+ major: parseInt(match[1], 10),
615
+ minor: parseInt(match[2], 10),
616
+ patch: parseInt(match[3], 10),
617
+ prerelease: match[4]?.substring(1)
618
+ // Remove leading dash
619
+ };
620
+ }
621
+ function isVersionCompatible(requested, available) {
622
+ const req = parseVersion(requested);
623
+ const avail = parseVersion(available);
624
+ if (!req || !avail) return false;
625
+ if (req.major !== avail.major) return false;
626
+ if (req.major === 0 && req.minor !== avail.minor) return false;
627
+ if (avail.minor < req.minor) return false;
628
+ if (avail.minor === req.minor && avail.patch < req.patch) return false;
629
+ if (req.prerelease && req.prerelease !== avail.prerelease) return false;
630
+ return true;
631
+ }
632
+ function parseServiceFqn(fqn) {
633
+ const parts = fqn.split("@");
634
+ if (parts.length !== 2) return null;
635
+ return {
636
+ serviceName: parts[0],
637
+ version: parts[1]
638
+ };
639
+ }
610
640
  function validateTimestamp(timestamp) {
611
641
  if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
612
642
  return { valid: false, error: "Timestamp must be a finite number" };
@@ -757,12 +787,35 @@ function createApp(storage, config) {
757
787
  return c.json({ error: "Internal server error" }, 500);
758
788
  }
759
789
  });
760
- app.post("/usernames/claim", async (c) => {
790
+ app.get("/users/:username", async (c) => {
761
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");
762
815
  const body = await c.req.json();
763
- const { username, publicKey, signature, message } = body;
764
- if (!username || !publicKey || !signature || !message) {
765
- return c.json({ error: "Missing required parameters: username, publicKey, signature, message" }, 400);
816
+ const { publicKey, signature, message } = body;
817
+ if (!publicKey || !signature || !message) {
818
+ return c.json({ error: "Missing required parameters: publicKey, signature, message" }, 400);
766
819
  }
767
820
  const validation = await validateUsernameClaim(username, publicKey, signature, message);
768
821
  if (!validation.valid) {
@@ -779,7 +832,7 @@ function createApp(storage, config) {
779
832
  username: claimed.username,
780
833
  claimedAt: claimed.claimedAt,
781
834
  expiresAt: claimed.expiresAt
782
- }, 200);
835
+ }, 201);
783
836
  } catch (err2) {
784
837
  if (err2.message?.includes("already claimed")) {
785
838
  return c.json({ error: "Username already claimed by different public key" }, 409);
@@ -791,47 +844,73 @@ function createApp(storage, config) {
791
844
  return c.json({ error: "Internal server error" }, 500);
792
845
  }
793
846
  });
794
- app.get("/usernames/:username", async (c) => {
847
+ app.get("/users/:username/services/:fqn", async (c) => {
795
848
  try {
796
849
  const username = c.req.param("username");
797
- const claimed = await storage.getUsername(username);
798
- if (!claimed) {
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) {
799
866
  return c.json({
800
- username,
801
- available: true
802
- }, 200);
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);
803
886
  }
804
887
  return c.json({
805
- username: claimed.username,
806
- available: false,
807
- claimedAt: claimed.claimedAt,
808
- expiresAt: claimed.expiresAt,
809
- publicKey: claimed.publicKey
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
810
898
  }, 200);
811
899
  } catch (err2) {
812
- console.error("Error checking username:", err2);
900
+ console.error("Error getting service:", err2);
813
901
  return c.json({ error: "Internal server error" }, 500);
814
902
  }
815
903
  });
816
- app.get("/usernames/:username/services", async (c) => {
904
+ app.post("/users/:username/services", authMiddleware, async (c) => {
905
+ let serviceFqn;
906
+ let createdOffers = [];
817
907
  try {
818
908
  const username = c.req.param("username");
819
- const services = await storage.listServicesForUsername(username);
820
- return c.json({
821
- username,
822
- services
823
- }, 200);
824
- } catch (err2) {
825
- console.error("Error listing services:", err2);
826
- return c.json({ error: "Internal server error" }, 500);
827
- }
828
- });
829
- app.post("/services", authMiddleware, async (c) => {
830
- try {
831
909
  const body = await c.req.json();
832
- const { username, serviceFqn, sdp, ttl, isPublic, metadata, signature, message } = body;
833
- if (!username || !serviceFqn || !sdp) {
834
- return c.json({ error: "Missing required parameters: username, serviceFqn, sdp" }, 400);
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);
835
914
  }
836
915
  const fqnValidation = validateServiceFqn(serviceFqn);
837
916
  if (!fqnValidation.valid) {
@@ -848,11 +927,20 @@ function createApp(storage, config) {
848
927
  if (!signatureValidation.valid) {
849
928
  return c.json({ error: "Invalid signature for username" }, 403);
850
929
  }
851
- if (typeof sdp !== "string" || sdp.length === 0) {
852
- return c.json({ error: "Invalid SDP" }, 400);
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
+ }
853
936
  }
854
- if (sdp.length > 64 * 1024) {
855
- return c.json({ error: "SDP too large (max 64KB)" }, 400);
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
+ }
856
944
  }
857
945
  const peerId = getAuthenticatedPeerId(c);
858
946
  const offerTtl = Math.min(
@@ -860,70 +948,64 @@ function createApp(storage, config) {
860
948
  config.offerMaxTtl
861
949
  );
862
950
  const expiresAt = Date.now() + offerTtl;
863
- const offers = await storage.createOffers([{
951
+ const offerRequests = offers.map((offer) => ({
864
952
  peerId,
865
- sdp,
953
+ sdp: offer.sdp,
866
954
  expiresAt
867
- }]);
868
- if (offers.length === 0) {
869
- return c.json({ error: "Failed to create offer" }, 500);
870
- }
871
- const offer = offers[0];
955
+ }));
872
956
  const result = await storage.createService({
873
957
  username,
874
958
  serviceFqn,
875
- offerId: offer.id,
876
959
  expiresAt,
877
960
  isPublic: isPublic || false,
878
- metadata: metadata ? JSON.stringify(metadata) : void 0
961
+ metadata: metadata ? JSON.stringify(metadata) : void 0,
962
+ offers: offerRequests
879
963
  });
964
+ createdOffers = result.offers;
880
965
  return c.json({
881
- serviceId: result.service.id,
882
966
  uuid: result.indexUuid,
883
- offerId: offer.id,
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,
884
979
  expiresAt: result.service.expiresAt
885
980
  }, 201);
886
981
  } catch (err2) {
887
982
  console.error("Error creating service:", err2);
888
- return c.json({ error: "Internal server error" }, 500);
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);
889
994
  }
890
995
  });
891
- app.get("/services/:uuid", async (c) => {
996
+ app.delete("/users/:username/services/:fqn", authMiddleware, async (c) => {
892
997
  try {
893
- const uuid = c.req.param("uuid");
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
+ }
894
1004
  const service = await storage.getServiceByUuid(uuid);
895
1005
  if (!service) {
896
1006
  return c.json({ error: "Service not found" }, 404);
897
1007
  }
898
- const offer = await storage.getOfferById(service.offerId);
899
- if (!offer) {
900
- return c.json({ error: "Associated offer not found" }, 404);
901
- }
902
- return c.json({
903
- serviceId: service.id,
904
- username: service.username,
905
- serviceFqn: service.serviceFqn,
906
- offerId: service.offerId,
907
- sdp: offer.sdp,
908
- isPublic: service.isPublic,
909
- metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
910
- createdAt: service.createdAt,
911
- expiresAt: service.expiresAt
912
- }, 200);
913
- } catch (err2) {
914
- console.error("Error getting service:", err2);
915
- return c.json({ error: "Internal server error" }, 500);
916
- }
917
- });
918
- app.delete("/services/:serviceId", authMiddleware, async (c) => {
919
- try {
920
- const serviceId = c.req.param("serviceId");
921
- const body = await c.req.json();
922
- const { username } = body;
923
- if (!username) {
924
- return c.json({ error: "Missing required parameter: username" }, 400);
925
- }
926
- const deleted = await storage.deleteService(serviceId, username);
1008
+ const deleted = await storage.deleteService(service.id, username);
927
1009
  if (!deleted) {
928
1010
  return c.json({ error: "Service not found or not owned by this username" }, 404);
929
1011
  }
@@ -933,112 +1015,46 @@ function createApp(storage, config) {
933
1015
  return c.json({ error: "Internal server error" }, 500);
934
1016
  }
935
1017
  });
936
- app.post("/index/:username/query", async (c) => {
1018
+ app.get("/services/:uuid", async (c) => {
937
1019
  try {
938
- const username = c.req.param("username");
939
- const body = await c.req.json();
940
- const { serviceFqn } = body;
941
- if (!serviceFqn) {
942
- return c.json({ error: "Missing required parameter: serviceFqn" }, 400);
943
- }
944
- const uuid = await storage.queryService(username, serviceFqn);
945
- if (!uuid) {
1020
+ const uuid = c.req.param("uuid");
1021
+ const service = await storage.getServiceByUuid(uuid);
1022
+ if (!service) {
946
1023
  return c.json({ error: "Service not found" }, 404);
947
1024
  }
948
- return c.json({
949
- uuid,
950
- allowed: true
951
- }, 200);
952
- } catch (err2) {
953
- console.error("Error querying service:", err2);
954
- return c.json({ error: "Internal server error" }, 500);
955
- }
956
- });
957
- app.post("/offers", authMiddleware, async (c) => {
958
- try {
959
- const body = await c.req.json();
960
- const { offers } = body;
961
- if (!Array.isArray(offers) || offers.length === 0) {
962
- return c.json({ error: "Missing or invalid required parameter: offers (must be non-empty array)" }, 400);
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);
963
1028
  }
964
- if (offers.length > config.maxOffersPerRequest) {
965
- return c.json({ error: `Too many offers (max ${config.maxOffersPerRequest})` }, 400);
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);
966
1035
  }
967
- const peerId = getAuthenticatedPeerId(c);
968
- const validated = offers.map((offer) => {
969
- const { sdp, ttl, secret } = offer;
970
- if (typeof sdp !== "string" || sdp.length === 0) {
971
- throw new Error("Invalid SDP in offer");
972
- }
973
- if (sdp.length > 64 * 1024) {
974
- throw new Error("SDP too large (max 64KB)");
975
- }
976
- const offerTtl = Math.min(
977
- Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
978
- config.offerMaxTtl
979
- );
980
- return {
981
- peerId,
982
- sdp,
983
- expiresAt: Date.now() + offerTtl,
984
- secret: secret ? String(secret).substring(0, 128) : void 0
985
- };
986
- });
987
- const created = await storage.createOffers(validated);
988
1036
  return c.json({
989
- offers: created.map((offer) => ({
990
- id: offer.id,
991
- peerId: offer.peerId,
992
- expiresAt: offer.expiresAt,
993
- createdAt: offer.createdAt,
994
- hasSecret: !!offer.secret
995
- }))
996
- }, 201);
997
- } catch (err2) {
998
- console.error("Error creating offers:", err2);
999
- return c.json({ error: err2.message || "Internal server error" }, 500);
1000
- }
1001
- });
1002
- app.get("/offers/mine", authMiddleware, async (c) => {
1003
- try {
1004
- const peerId = getAuthenticatedPeerId(c);
1005
- const offers = await storage.getOffersByPeerId(peerId);
1006
- return c.json({
1007
- offers: offers.map((offer) => ({
1008
- id: offer.id,
1009
- sdp: offer.sdp,
1010
- createdAt: offer.createdAt,
1011
- expiresAt: offer.expiresAt,
1012
- lastSeen: offer.lastSeen,
1013
- hasSecret: !!offer.secret,
1014
- answererPeerId: offer.answererPeerId,
1015
- answered: !!offer.answererPeerId
1016
- }))
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
1017
1047
  }, 200);
1018
1048
  } catch (err2) {
1019
- console.error("Error getting offers:", err2);
1020
- return c.json({ error: "Internal server error" }, 500);
1021
- }
1022
- });
1023
- app.delete("/offers/:offerId", authMiddleware, async (c) => {
1024
- try {
1025
- const offerId = c.req.param("offerId");
1026
- const peerId = getAuthenticatedPeerId(c);
1027
- const deleted = await storage.deleteOffer(offerId, peerId);
1028
- if (!deleted) {
1029
- return c.json({ error: "Offer not found or not owned by this peer" }, 404);
1030
- }
1031
- return c.json({ success: true }, 200);
1032
- } catch (err2) {
1033
- console.error("Error deleting offer:", err2);
1049
+ console.error("Error getting service:", err2);
1034
1050
  return c.json({ error: "Internal server error" }, 500);
1035
1051
  }
1036
1052
  });
1037
- app.post("/offers/:offerId/answer", authMiddleware, async (c) => {
1053
+ app.post("/services/:uuid/answer", authMiddleware, async (c) => {
1038
1054
  try {
1039
- const offerId = c.req.param("offerId");
1055
+ const uuid = c.req.param("uuid");
1040
1056
  const body = await c.req.json();
1041
- const { sdp, secret } = body;
1057
+ const { sdp } = body;
1042
1058
  if (!sdp) {
1043
1059
  return c.json({ error: "Missing required parameter: sdp" }, 400);
1044
1060
  }
@@ -1048,75 +1064,126 @@ function createApp(storage, config) {
1048
1064
  if (sdp.length > 64 * 1024) {
1049
1065
  return c.json({ error: "SDP too large (max 64KB)" }, 400);
1050
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
+ }
1051
1076
  const answererPeerId = getAuthenticatedPeerId(c);
1052
- const result = await storage.answerOffer(offerId, answererPeerId, sdp, secret);
1077
+ const result = await storage.answerOffer(availableOffer.id, answererPeerId, sdp);
1053
1078
  if (!result.success) {
1054
1079
  return c.json({ error: result.error }, 400);
1055
1080
  }
1056
- return c.json({ success: true }, 200);
1081
+ return c.json({
1082
+ success: true,
1083
+ offerId: availableOffer.id
1084
+ }, 200);
1057
1085
  } catch (err2) {
1058
- console.error("Error answering offer:", err2);
1086
+ console.error("Error answering service:", err2);
1059
1087
  return c.json({ error: "Internal server error" }, 500);
1060
1088
  }
1061
1089
  });
1062
- app.get("/offers/answers", authMiddleware, async (c) => {
1090
+ app.get("/services/:uuid/answer", authMiddleware, async (c) => {
1063
1091
  try {
1092
+ const uuid = c.req.param("uuid");
1064
1093
  const peerId = getAuthenticatedPeerId(c);
1065
- const offers = await storage.getAnsweredOffers(peerId);
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
+ }
1066
1103
  return c.json({
1067
- answers: offers.map((offer) => ({
1068
- offerId: offer.id,
1069
- answererPeerId: offer.answererPeerId,
1070
- answerSdp: offer.answerSdp,
1071
- answeredAt: offer.answeredAt
1072
- }))
1104
+ offerId: myOffer.id,
1105
+ answererId: myOffer.answererPeerId,
1106
+ sdp: myOffer.answerSdp,
1107
+ answeredAt: myOffer.answeredAt
1073
1108
  }, 200);
1074
1109
  } catch (err2) {
1075
- console.error("Error getting answers:", err2);
1110
+ console.error("Error getting service answer:", err2);
1076
1111
  return c.json({ error: "Internal server error" }, 500);
1077
1112
  }
1078
1113
  });
1079
- app.post("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
1114
+ app.post("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
1080
1115
  try {
1081
- const offerId = c.req.param("offerId");
1116
+ const uuid = c.req.param("uuid");
1082
1117
  const body = await c.req.json();
1083
- const { candidates } = body;
1118
+ const { candidates, offerId } = body;
1084
1119
  if (!Array.isArray(candidates) || candidates.length === 0) {
1085
1120
  return c.json({ error: "Missing or invalid required parameter: candidates" }, 400);
1086
1121
  }
1087
1122
  const peerId = getAuthenticatedPeerId(c);
1088
- const offer = await storage.getOfferById(offerId);
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);
1089
1139
  if (!offer) {
1090
1140
  return c.json({ error: "Offer not found" }, 404);
1091
1141
  }
1092
1142
  const role = offer.peerId === peerId ? "offerer" : "answerer";
1093
- const count = await storage.addIceCandidates(offerId, peerId, role, candidates);
1094
- return c.json({ count }, 200);
1143
+ const count = await storage.addIceCandidates(targetOfferId, peerId, role, candidates);
1144
+ return c.json({ count, offerId: targetOfferId }, 200);
1095
1145
  } catch (err2) {
1096
- console.error("Error adding ICE candidates:", err2);
1146
+ console.error("Error adding ICE candidates to service:", err2);
1097
1147
  return c.json({ error: "Internal server error" }, 500);
1098
1148
  }
1099
1149
  });
1100
- app.get("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
1150
+ app.get("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
1101
1151
  try {
1102
- const offerId = c.req.param("offerId");
1152
+ const uuid = c.req.param("uuid");
1103
1153
  const since = c.req.query("since");
1154
+ const offerId = c.req.query("offerId");
1104
1155
  const peerId = getAuthenticatedPeerId(c);
1105
- const offer = await storage.getOfferById(offerId);
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);
1106
1172
  if (!offer) {
1107
1173
  return c.json({ error: "Offer not found" }, 404);
1108
1174
  }
1109
1175
  const targetRole = offer.peerId === peerId ? "answerer" : "offerer";
1110
1176
  const sinceTimestamp = since ? parseInt(since, 10) : void 0;
1111
- const candidates = await storage.getIceCandidates(offerId, targetRole, sinceTimestamp);
1177
+ const candidates = await storage.getIceCandidates(targetOfferId, targetRole, sinceTimestamp);
1112
1178
  return c.json({
1113
1179
  candidates: candidates.map((c2) => ({
1114
1180
  candidate: c2.candidate,
1115
1181
  createdAt: c2.createdAt
1116
- }))
1182
+ })),
1183
+ offerId: targetOfferId
1117
1184
  }, 200);
1118
1185
  } catch (err2) {
1119
- console.error("Error getting ICE candidates:", err2);
1186
+ console.error("Error getting ICE candidates for service:", err2);
1120
1187
  return c.json({ error: "Internal server error" }, 500);
1121
1188
  }
1122
1189
  });
@@ -1143,8 +1210,7 @@ function loadConfig() {
1143
1210
  offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || "86400000", 10),
1144
1211
  offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || "60000", 10),
1145
1212
  cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL || "60000", 10),
1146
- maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || "100", 10),
1147
- maxTopicsPerOffer: parseInt(process.env.MAX_TOPICS_PER_OFFER || "50", 10)
1213
+ maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || "100", 10)
1148
1214
  };
1149
1215
  }
1150
1216
 
@@ -1153,11 +1219,9 @@ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
1153
1219
  var import_node_crypto = require("node:crypto");
1154
1220
 
1155
1221
  // src/storage/hash-id.ts
1156
- async function generateOfferHash(sdp, topics) {
1222
+ async function generateOfferHash(sdp) {
1157
1223
  const sanitizedOffer = {
1158
- sdp,
1159
- topics: [...topics].sort()
1160
- // Sort topics for consistency
1224
+ sdp
1161
1225
  };
1162
1226
  const jsonString = JSON.stringify(sanitizedOffer);
1163
1227
  const encoder = new TextEncoder();
@@ -1184,10 +1248,11 @@ var SQLiteStorage = class {
1184
1248
  */
1185
1249
  initializeDatabase() {
1186
1250
  this.db.exec(`
1187
- -- Offers table (no topics)
1251
+ -- WebRTC signaling offers
1188
1252
  CREATE TABLE IF NOT EXISTS offers (
1189
1253
  id TEXT PRIMARY KEY,
1190
1254
  peer_id TEXT NOT NULL,
1255
+ service_id TEXT,
1191
1256
  sdp TEXT NOT NULL,
1192
1257
  created_at INTEGER NOT NULL,
1193
1258
  expires_at INTEGER NOT NULL,
@@ -1195,10 +1260,12 @@ var SQLiteStorage = class {
1195
1260
  secret TEXT,
1196
1261
  answerer_peer_id TEXT,
1197
1262
  answer_sdp TEXT,
1198
- answered_at INTEGER
1263
+ answered_at INTEGER,
1264
+ FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
1199
1265
  );
1200
1266
 
1201
1267
  CREATE INDEX IF NOT EXISTS idx_offers_peer ON offers(peer_id);
1268
+ CREATE INDEX IF NOT EXISTS idx_offers_service ON offers(service_id);
1202
1269
  CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
1203
1270
  CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
1204
1271
  CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
@@ -1232,25 +1299,22 @@ var SQLiteStorage = class {
1232
1299
  CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
1233
1300
  CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
1234
1301
 
1235
- -- Services table
1302
+ -- Services table (one service can have multiple offers)
1236
1303
  CREATE TABLE IF NOT EXISTS services (
1237
1304
  id TEXT PRIMARY KEY,
1238
1305
  username TEXT NOT NULL,
1239
1306
  service_fqn TEXT NOT NULL,
1240
- offer_id TEXT NOT NULL,
1241
1307
  created_at INTEGER NOT NULL,
1242
1308
  expires_at INTEGER NOT NULL,
1243
1309
  is_public INTEGER NOT NULL DEFAULT 0,
1244
1310
  metadata TEXT,
1245
1311
  FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
1246
- FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE,
1247
1312
  UNIQUE(username, service_fqn)
1248
1313
  );
1249
1314
 
1250
1315
  CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
1251
1316
  CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
1252
1317
  CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
1253
- CREATE INDEX IF NOT EXISTS idx_services_offer ON services(offer_id);
1254
1318
 
1255
1319
  -- Service index table (privacy layer)
1256
1320
  CREATE TABLE IF NOT EXISTS service_index (
@@ -1274,19 +1338,20 @@ var SQLiteStorage = class {
1274
1338
  const offersWithIds = await Promise.all(
1275
1339
  offers.map(async (offer) => ({
1276
1340
  ...offer,
1277
- id: offer.id || await generateOfferHash(offer.sdp, [])
1341
+ id: offer.id || await generateOfferHash(offer.sdp)
1278
1342
  }))
1279
1343
  );
1280
1344
  const transaction = this.db.transaction((offersWithIds2) => {
1281
1345
  const offerStmt = this.db.prepare(`
1282
- INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
1283
- VALUES (?, ?, ?, ?, ?, ?, ?)
1346
+ INSERT INTO offers (id, peer_id, service_id, sdp, created_at, expires_at, last_seen, secret)
1347
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1284
1348
  `);
1285
1349
  for (const offer of offersWithIds2) {
1286
1350
  const now = Date.now();
1287
1351
  offerStmt.run(
1288
1352
  offer.id,
1289
1353
  offer.peerId,
1354
+ offer.serviceId || null,
1290
1355
  offer.sdp,
1291
1356
  now,
1292
1357
  offer.expiresAt,
@@ -1296,6 +1361,7 @@ var SQLiteStorage = class {
1296
1361
  created.push({
1297
1362
  id: offer.id,
1298
1363
  peerId: offer.peerId,
1364
+ serviceId: offer.serviceId || void 0,
1299
1365
  sdp: offer.sdp,
1300
1366
  createdAt: now,
1301
1367
  expiresAt: offer.expiresAt,
@@ -1498,16 +1564,20 @@ var SQLiteStorage = class {
1498
1564
  const serviceId = (0, import_node_crypto.randomUUID)();
1499
1565
  const indexUuid = (0, import_node_crypto.randomUUID)();
1500
1566
  const now = Date.now();
1567
+ const offerRequests = request.offers.map((offer) => ({
1568
+ ...offer,
1569
+ serviceId
1570
+ }));
1571
+ const offers = await this.createOffers(offerRequests);
1501
1572
  const transaction = this.db.transaction(() => {
1502
1573
  const serviceStmt = this.db.prepare(`
1503
- INSERT INTO services (id, username, service_fqn, offer_id, created_at, expires_at, is_public, metadata)
1504
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1574
+ INSERT INTO services (id, username, service_fqn, created_at, expires_at, is_public, metadata)
1575
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1505
1576
  `);
1506
1577
  serviceStmt.run(
1507
1578
  serviceId,
1508
1579
  request.username,
1509
1580
  request.serviceFqn,
1510
- request.offerId,
1511
1581
  now,
1512
1582
  request.expiresAt,
1513
1583
  request.isPublic ? 1 : 0,
@@ -1533,15 +1603,23 @@ var SQLiteStorage = class {
1533
1603
  id: serviceId,
1534
1604
  username: request.username,
1535
1605
  serviceFqn: request.serviceFqn,
1536
- offerId: request.offerId,
1537
1606
  createdAt: now,
1538
1607
  expiresAt: request.expiresAt,
1539
1608
  isPublic: request.isPublic || false,
1540
1609
  metadata: request.metadata
1541
1610
  },
1542
- indexUuid
1611
+ indexUuid,
1612
+ offers
1543
1613
  };
1544
1614
  }
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;
1622
+ }
1545
1623
  async getServiceById(serviceId) {
1546
1624
  const stmt = this.db.prepare(`
1547
1625
  SELECT * FROM services
@@ -1590,6 +1668,15 @@ var SQLiteStorage = class {
1590
1668
  const row = stmt.get(username, serviceFqn, Date.now());
1591
1669
  return row ? row.uuid : null;
1592
1670
  }
1671
+ async findServicesByName(username, serviceName) {
1672
+ 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
1676
+ `);
1677
+ const rows = stmt.all(username, `${serviceName}@%`, Date.now());
1678
+ return rows.map((row) => this.rowToService(row));
1679
+ }
1593
1680
  async deleteService(serviceId, username) {
1594
1681
  const stmt = this.db.prepare(`
1595
1682
  DELETE FROM services
@@ -1614,6 +1701,7 @@ var SQLiteStorage = class {
1614
1701
  return {
1615
1702
  id: row.id,
1616
1703
  peerId: row.peer_id,
1704
+ serviceId: row.service_id || void 0,
1617
1705
  sdp: row.sdp,
1618
1706
  createdAt: row.created_at,
1619
1707
  expiresAt: row.expires_at,
@@ -1632,13 +1720,24 @@ var SQLiteStorage = class {
1632
1720
  id: row.id,
1633
1721
  username: row.username,
1634
1722
  serviceFqn: row.service_fqn,
1635
- offerId: row.offer_id,
1636
1723
  createdAt: row.created_at,
1637
1724
  expiresAt: row.expires_at,
1638
1725
  isPublic: row.is_public === 1,
1639
1726
  metadata: row.metadata || void 0
1640
1727
  };
1641
1728
  }
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
+ }
1642
1741
  };
1643
1742
 
1644
1743
  // src/index.ts
@@ -1654,7 +1753,6 @@ async function main() {
1654
1753
  offerMinTtl: `${config.offerMinTtl}ms`,
1655
1754
  cleanupInterval: `${config.cleanupInterval}ms`,
1656
1755
  maxOffersPerRequest: config.maxOffersPerRequest,
1657
- maxTopicsPerOffer: config.maxTopicsPerOffer,
1658
1756
  corsOrigins: config.corsOrigins,
1659
1757
  version: config.version
1660
1758
  });