@xtr-dev/rondevu-server 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -240,10 +240,10 @@ Unpublish a service (requires authentication and ownership)
240
240
  }
241
241
  ```
242
242
 
243
- ### Offer Management (Low-level)
243
+ ### WebRTC Signaling (Service-Based)
244
244
 
245
- #### `POST /offers`
246
- Create one or more offers (requires authentication)
245
+ #### `POST /services/:uuid/answer`
246
+ Answer a service offer (requires authentication)
247
247
 
248
248
  **Headers:**
249
249
  - `Authorization: Bearer {peerId}:{secret}`
@@ -251,46 +251,80 @@ Create one or more offers (requires authentication)
251
251
  **Request:**
252
252
  ```json
253
253
  {
254
- "offers": [
255
- {
256
- "sdp": "v=0...",
257
- "ttl": 300000
258
- }
259
- ]
254
+ "sdp": "v=0..."
260
255
  }
261
256
  ```
262
257
 
263
- #### `GET /offers/mine`
264
- List all offers owned by authenticated peer
258
+ **Response:**
259
+ ```json
260
+ {
261
+ "success": true,
262
+ "offerId": "offer-hash"
263
+ }
264
+ ```
265
265
 
266
- #### `DELETE /offers/:offerId`
267
- Delete a specific offer
266
+ #### `GET /services/:uuid/answer`
267
+ Get answer for a service (offerer polls this)
268
268
 
269
- #### `POST /offers/:offerId/answer`
270
- Answer an offer (locks it to answerer)
269
+ **Headers:**
270
+ - `Authorization: Bearer {peerId}:{secret}`
271
271
 
272
- **Request:**
272
+ **Response:**
273
273
  ```json
274
274
  {
275
- "sdp": "v=0..."
275
+ "offerId": "offer-hash",
276
+ "answererId": "answerer-peer-id",
277
+ "sdp": "v=0...",
278
+ "answeredAt": 1733404800000
276
279
  }
277
280
  ```
278
281
 
279
- #### `GET /offers/:offerId/answer`
280
- Get answer for a specific offer
282
+ **Note:** Returns 404 if not yet answered
283
+
284
+ #### `POST /services/:uuid/ice-candidates`
285
+ Post ICE candidates for a service (requires authentication)
281
286
 
282
- #### `POST /offers/:offerId/ice-candidates`
283
- Post ICE candidates for an offer
287
+ **Headers:**
288
+ - `Authorization: Bearer {peerId}:{secret}`
284
289
 
285
290
  **Request:**
286
291
  ```json
287
292
  {
288
- "candidates": ["candidate:1 1 UDP..."]
293
+ "candidates": ["candidate:1 1 UDP..."],
294
+ "offerId": "optional-offer-id"
295
+ }
296
+ ```
297
+
298
+ **Response:**
299
+ ```json
300
+ {
301
+ "count": 1,
302
+ "offerId": "offer-hash"
303
+ }
304
+ ```
305
+
306
+ **Note:** If `offerId` is omitted, the server will auto-detect the peer's offer
307
+
308
+ #### `GET /services/:uuid/ice-candidates?since=1234567890&offerId=optional-offer-id`
309
+ Get ICE candidates from the other peer (requires authentication)
310
+
311
+ **Headers:**
312
+ - `Authorization: Bearer {peerId}:{secret}`
313
+
314
+ **Response:**
315
+ ```json
316
+ {
317
+ "candidates": [
318
+ {
319
+ "candidate": "candidate:1 1 UDP...",
320
+ "createdAt": 1733404800000
321
+ }
322
+ ],
323
+ "offerId": "offer-hash"
289
324
  }
290
325
  ```
291
326
 
292
- #### `GET /offers/:offerId/ice-candidates?since=1234567890`
293
- Get ICE candidates from the other peer
327
+ **Note:** Returns candidates from the opposite role (offerer gets answerer candidates and vice versa)
294
328
 
295
329
  ## Configuration
296
330
 
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" };
@@ -814,37 +844,40 @@ function createApp(storage, config) {
814
844
  return c.json({ error: "Internal server error" }, 500);
815
845
  }
816
846
  });
817
- app.get("/users/:username/services", async (c) => {
818
- try {
819
- const username = c.req.param("username");
820
- const services = await storage.listServicesForUsername(username);
821
- return c.json({
822
- username,
823
- services
824
- }, 200);
825
- } catch (err2) {
826
- console.error("Error listing services:", err2);
827
- return c.json({ error: "Internal server error" }, 500);
828
- }
829
- });
830
847
  app.get("/users/:username/services/:fqn", async (c) => {
831
848
  try {
832
849
  const username = c.req.param("username");
833
850
  const serviceFqn = decodeURIComponent(c.req.param("fqn"));
834
- const uuid = await storage.queryService(username, serviceFqn);
835
- if (!uuid) {
836
- return c.json({ error: "Service not found" }, 404);
851
+ const parsed = parseServiceFqn(serviceFqn);
852
+ if (!parsed) {
853
+ return c.json({ error: "Invalid service FQN format" }, 400);
837
854
  }
838
- const service = await storage.getServiceByUuid(uuid);
839
- if (!service) {
855
+ const { serviceName, version: requestedVersion } = parsed;
856
+ const matchingServices = await storage.findServicesByName(username, serviceName);
857
+ if (matchingServices.length === 0) {
840
858
  return c.json({ error: "Service not found" }, 404);
841
859
  }
842
- const initialOffer = await storage.getOfferById(service.offerId);
843
- if (!initialOffer) {
844
- return c.json({ error: "Associated offer not found" }, 404);
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);
845
870
  }
846
- const peerOffers = await storage.getOffersByPeerId(initialOffer.peerId);
847
- const availableOffer = peerOffers.find((offer) => !offer.answererPeerId);
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);
848
881
  if (!availableOffer) {
849
882
  return c.json({
850
883
  error: "No available offers",
@@ -870,14 +903,14 @@ function createApp(storage, config) {
870
903
  });
871
904
  app.post("/users/:username/services", authMiddleware, async (c) => {
872
905
  let serviceFqn;
873
- let offers = [];
906
+ let createdOffers = [];
874
907
  try {
875
908
  const username = c.req.param("username");
876
909
  const body = await c.req.json();
877
910
  serviceFqn = body.serviceFqn;
878
- const { sdp, ttl, isPublic, metadata, signature, message } = body;
879
- if (!serviceFqn || !sdp) {
880
- return c.json({ error: "Missing required parameters: serviceFqn, sdp" }, 400);
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);
881
914
  }
882
915
  const fqnValidation = validateServiceFqn(serviceFqn);
883
916
  if (!fqnValidation.valid) {
@@ -901,11 +934,13 @@ function createApp(storage, config) {
901
934
  await storage.deleteService(existingService.id, username);
902
935
  }
903
936
  }
904
- if (typeof sdp !== "string" || sdp.length === 0) {
905
- return c.json({ error: "Invalid SDP" }, 400);
906
- }
907
- if (sdp.length > 64 * 1024) {
908
- 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
+ }
909
944
  }
910
945
  const peerId = getAuthenticatedPeerId(c);
911
946
  const offerTtl = Math.min(
@@ -913,30 +948,31 @@ function createApp(storage, config) {
913
948
  config.offerMaxTtl
914
949
  );
915
950
  const expiresAt = Date.now() + offerTtl;
916
- offers = await storage.createOffers([{
951
+ const offerRequests = offers.map((offer) => ({
917
952
  peerId,
918
- sdp,
953
+ sdp: offer.sdp,
919
954
  expiresAt
920
- }]);
921
- if (offers.length === 0) {
922
- return c.json({ error: "Failed to create offer" }, 500);
923
- }
924
- const offer = offers[0];
955
+ }));
925
956
  const result = await storage.createService({
926
957
  username,
927
958
  serviceFqn,
928
- offerId: offer.id,
929
959
  expiresAt,
930
960
  isPublic: isPublic || false,
931
- metadata: metadata ? JSON.stringify(metadata) : void 0
961
+ metadata: metadata ? JSON.stringify(metadata) : void 0,
962
+ offers: offerRequests
932
963
  });
964
+ createdOffers = result.offers;
933
965
  return c.json({
934
966
  uuid: result.indexUuid,
935
967
  serviceFqn,
936
968
  username,
937
969
  serviceId: result.service.id,
938
- offerId: offer.id,
939
- sdp: offer.sdp,
970
+ offers: result.offers.map((o) => ({
971
+ offerId: o.id,
972
+ sdp: o.sdp,
973
+ createdAt: o.createdAt,
974
+ expiresAt: o.expiresAt
975
+ })),
940
976
  isPublic: result.service.isPublic,
941
977
  metadata,
942
978
  createdAt: result.service.createdAt,
@@ -949,7 +985,7 @@ function createApp(storage, config) {
949
985
  stack: err2.stack,
950
986
  username: c.req.param("username"),
951
987
  serviceFqn,
952
- offerId: offers[0]?.id
988
+ offerIds: createdOffers.map((o) => o.id)
953
989
  });
954
990
  return c.json({
955
991
  error: "Internal server error",
@@ -986,12 +1022,11 @@ function createApp(storage, config) {
986
1022
  if (!service) {
987
1023
  return c.json({ error: "Service not found" }, 404);
988
1024
  }
989
- const initialOffer = await storage.getOfferById(service.offerId);
990
- if (!initialOffer) {
991
- return c.json({ error: "Associated offer not found" }, 404);
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);
992
1028
  }
993
- const peerOffers = await storage.getOffersByPeerId(initialOffer.peerId);
994
- const availableOffer = peerOffers.find((offer) => !offer.answererPeerId);
1029
+ const availableOffer = serviceOffers.find((offer) => !offer.answererPeerId);
995
1030
  if (!availableOffer) {
996
1031
  return c.json({
997
1032
  error: "No available offers",
@@ -1015,113 +1050,11 @@ function createApp(storage, config) {
1015
1050
  return c.json({ error: "Internal server error" }, 500);
1016
1051
  }
1017
1052
  });
1018
- app.post("/offers", authMiddleware, async (c) => {
1053
+ app.post("/services/:uuid/answer", authMiddleware, async (c) => {
1019
1054
  try {
1055
+ const uuid = c.req.param("uuid");
1020
1056
  const body = await c.req.json();
1021
- const { offers } = body;
1022
- if (!Array.isArray(offers) || offers.length === 0) {
1023
- return c.json({ error: "Missing or invalid required parameter: offers (must be non-empty array)" }, 400);
1024
- }
1025
- if (offers.length > config.maxOffersPerRequest) {
1026
- return c.json({ error: `Too many offers (max ${config.maxOffersPerRequest})` }, 400);
1027
- }
1028
- const peerId = getAuthenticatedPeerId(c);
1029
- const validated = offers.map((offer) => {
1030
- const { sdp, ttl, secret } = offer;
1031
- if (typeof sdp !== "string" || sdp.length === 0) {
1032
- throw new Error("Invalid SDP in offer");
1033
- }
1034
- if (sdp.length > 64 * 1024) {
1035
- throw new Error("SDP too large (max 64KB)");
1036
- }
1037
- const offerTtl = Math.min(
1038
- Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
1039
- config.offerMaxTtl
1040
- );
1041
- return {
1042
- peerId,
1043
- sdp,
1044
- expiresAt: Date.now() + offerTtl,
1045
- secret: secret ? String(secret).substring(0, 128) : void 0
1046
- };
1047
- });
1048
- const created = await storage.createOffers(validated);
1049
- return c.json({
1050
- offers: created.map((offer) => ({
1051
- id: offer.id,
1052
- peerId: offer.peerId,
1053
- expiresAt: offer.expiresAt,
1054
- createdAt: offer.createdAt,
1055
- hasSecret: !!offer.secret
1056
- }))
1057
- }, 201);
1058
- } catch (err2) {
1059
- console.error("Error creating offers:", err2);
1060
- return c.json({ error: err2.message || "Internal server error" }, 500);
1061
- }
1062
- });
1063
- app.get("/offers/mine", authMiddleware, async (c) => {
1064
- try {
1065
- const peerId = getAuthenticatedPeerId(c);
1066
- const offers = await storage.getOffersByPeerId(peerId);
1067
- return c.json({
1068
- offers: offers.map((offer) => ({
1069
- id: offer.id,
1070
- sdp: offer.sdp,
1071
- createdAt: offer.createdAt,
1072
- expiresAt: offer.expiresAt,
1073
- lastSeen: offer.lastSeen,
1074
- hasSecret: !!offer.secret,
1075
- answererPeerId: offer.answererPeerId,
1076
- answered: !!offer.answererPeerId
1077
- }))
1078
- }, 200);
1079
- } catch (err2) {
1080
- console.error("Error getting offers:", err2);
1081
- return c.json({ error: "Internal server error" }, 500);
1082
- }
1083
- });
1084
- app.get("/offers/:offerId", authMiddleware, async (c) => {
1085
- try {
1086
- const offerId = c.req.param("offerId");
1087
- const offer = await storage.getOfferById(offerId);
1088
- if (!offer) {
1089
- return c.json({ error: "Offer not found" }, 404);
1090
- }
1091
- return c.json({
1092
- id: offer.id,
1093
- peerId: offer.peerId,
1094
- sdp: offer.sdp,
1095
- createdAt: offer.createdAt,
1096
- expiresAt: offer.expiresAt,
1097
- answererPeerId: offer.answererPeerId,
1098
- answered: !!offer.answererPeerId,
1099
- answerSdp: offer.answerSdp
1100
- }, 200);
1101
- } catch (err2) {
1102
- console.error("Error getting offer:", err2);
1103
- return c.json({ error: "Internal server error" }, 500);
1104
- }
1105
- });
1106
- app.delete("/offers/:offerId", authMiddleware, async (c) => {
1107
- try {
1108
- const offerId = c.req.param("offerId");
1109
- const peerId = getAuthenticatedPeerId(c);
1110
- const deleted = await storage.deleteOffer(offerId, peerId);
1111
- if (!deleted) {
1112
- return c.json({ error: "Offer not found or not owned by this peer" }, 404);
1113
- }
1114
- return c.json({ success: true }, 200);
1115
- } catch (err2) {
1116
- console.error("Error deleting offer:", err2);
1117
- return c.json({ error: "Internal server error" }, 500);
1118
- }
1119
- });
1120
- app.post("/offers/:offerId/answer", authMiddleware, async (c) => {
1121
- try {
1122
- const offerId = c.req.param("offerId");
1123
- const body = await c.req.json();
1124
- const { sdp, secret } = body;
1057
+ const { sdp } = body;
1125
1058
  if (!sdp) {
1126
1059
  return c.json({ error: "Missing required parameter: sdp" }, 400);
1127
1060
  }
@@ -1131,83 +1064,126 @@ function createApp(storage, config) {
1131
1064
  if (sdp.length > 64 * 1024) {
1132
1065
  return c.json({ error: "SDP too large (max 64KB)" }, 400);
1133
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
+ }
1134
1076
  const answererPeerId = getAuthenticatedPeerId(c);
1135
- const result = await storage.answerOffer(offerId, answererPeerId, sdp, secret);
1077
+ const result = await storage.answerOffer(availableOffer.id, answererPeerId, sdp);
1136
1078
  if (!result.success) {
1137
1079
  return c.json({ error: result.error }, 400);
1138
1080
  }
1139
- return c.json({ success: true }, 200);
1081
+ return c.json({
1082
+ success: true,
1083
+ offerId: availableOffer.id
1084
+ }, 200);
1140
1085
  } catch (err2) {
1141
- console.error("Error answering offer:", err2);
1086
+ console.error("Error answering service:", err2);
1142
1087
  return c.json({ error: "Internal server error" }, 500);
1143
1088
  }
1144
1089
  });
1145
- app.get("/offers/:offerId/answer", authMiddleware, async (c) => {
1090
+ app.get("/services/:uuid/answer", authMiddleware, async (c) => {
1146
1091
  try {
1147
- const offerId = c.req.param("offerId");
1092
+ const uuid = c.req.param("uuid");
1148
1093
  const peerId = getAuthenticatedPeerId(c);
1149
- const offer = await storage.getOfferById(offerId);
1150
- if (!offer) {
1151
- return c.json({ error: "Offer not found" }, 404);
1152
- }
1153
- if (offer.peerId !== peerId) {
1154
- return c.json({ error: "Not authorized to view this answer" }, 403);
1094
+ const service = await storage.getServiceByUuid(uuid);
1095
+ if (!service) {
1096
+ return c.json({ error: "Service not found" }, 404);
1155
1097
  }
1156
- if (!offer.answererPeerId || !offer.answerSdp) {
1098
+ const serviceOffers = await storage.getOffersForService(service.id);
1099
+ const myOffer = serviceOffers.find((offer) => offer.peerId === peerId && offer.answererPeerId);
1100
+ if (!myOffer || !myOffer.answerSdp) {
1157
1101
  return c.json({ error: "Offer not yet answered" }, 404);
1158
1102
  }
1159
1103
  return c.json({
1160
- offerId: offer.id,
1161
- answererId: offer.answererPeerId,
1162
- sdp: offer.answerSdp,
1163
- answeredAt: offer.answeredAt
1104
+ offerId: myOffer.id,
1105
+ answererId: myOffer.answererPeerId,
1106
+ sdp: myOffer.answerSdp,
1107
+ answeredAt: myOffer.answeredAt
1164
1108
  }, 200);
1165
1109
  } catch (err2) {
1166
- console.error("Error getting answer:", err2);
1110
+ console.error("Error getting service answer:", err2);
1167
1111
  return c.json({ error: "Internal server error" }, 500);
1168
1112
  }
1169
1113
  });
1170
- app.post("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
1114
+ app.post("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
1171
1115
  try {
1172
- const offerId = c.req.param("offerId");
1116
+ const uuid = c.req.param("uuid");
1173
1117
  const body = await c.req.json();
1174
- const { candidates } = body;
1118
+ const { candidates, offerId } = body;
1175
1119
  if (!Array.isArray(candidates) || candidates.length === 0) {
1176
1120
  return c.json({ error: "Missing or invalid required parameter: candidates" }, 400);
1177
1121
  }
1178
1122
  const peerId = getAuthenticatedPeerId(c);
1179
- 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);
1180
1139
  if (!offer) {
1181
1140
  return c.json({ error: "Offer not found" }, 404);
1182
1141
  }
1183
1142
  const role = offer.peerId === peerId ? "offerer" : "answerer";
1184
- const count = await storage.addIceCandidates(offerId, peerId, role, candidates);
1185
- return c.json({ count }, 200);
1143
+ const count = await storage.addIceCandidates(targetOfferId, peerId, role, candidates);
1144
+ return c.json({ count, offerId: targetOfferId }, 200);
1186
1145
  } catch (err2) {
1187
- console.error("Error adding ICE candidates:", err2);
1146
+ console.error("Error adding ICE candidates to service:", err2);
1188
1147
  return c.json({ error: "Internal server error" }, 500);
1189
1148
  }
1190
1149
  });
1191
- app.get("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
1150
+ app.get("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
1192
1151
  try {
1193
- const offerId = c.req.param("offerId");
1152
+ const uuid = c.req.param("uuid");
1194
1153
  const since = c.req.query("since");
1154
+ const offerId = c.req.query("offerId");
1195
1155
  const peerId = getAuthenticatedPeerId(c);
1196
- 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);
1197
1172
  if (!offer) {
1198
1173
  return c.json({ error: "Offer not found" }, 404);
1199
1174
  }
1200
1175
  const targetRole = offer.peerId === peerId ? "answerer" : "offerer";
1201
1176
  const sinceTimestamp = since ? parseInt(since, 10) : void 0;
1202
- const candidates = await storage.getIceCandidates(offerId, targetRole, sinceTimestamp);
1177
+ const candidates = await storage.getIceCandidates(targetOfferId, targetRole, sinceTimestamp);
1203
1178
  return c.json({
1204
1179
  candidates: candidates.map((c2) => ({
1205
1180
  candidate: c2.candidate,
1206
1181
  createdAt: c2.createdAt
1207
- }))
1182
+ })),
1183
+ offerId: targetOfferId
1208
1184
  }, 200);
1209
1185
  } catch (err2) {
1210
- console.error("Error getting ICE candidates:", err2);
1186
+ console.error("Error getting ICE candidates for service:", err2);
1211
1187
  return c.json({ error: "Internal server error" }, 500);
1212
1188
  }
1213
1189
  });
@@ -1362,7 +1338,6 @@ var SQLiteStorage = class {
1362
1338
  const offersWithIds = await Promise.all(
1363
1339
  offers.map(async (offer) => ({
1364
1340
  ...offer,
1365
- serviceId: offer.serviceId || null,
1366
1341
  id: offer.id || await generateOfferHash(offer.sdp)
1367
1342
  }))
1368
1343
  );
@@ -1386,7 +1361,7 @@ var SQLiteStorage = class {
1386
1361
  created.push({
1387
1362
  id: offer.id,
1388
1363
  peerId: offer.peerId,
1389
- serviceId: offer.serviceId,
1364
+ serviceId: offer.serviceId || void 0,
1390
1365
  sdp: offer.sdp,
1391
1366
  createdAt: now,
1392
1367
  expiresAt: offer.expiresAt,
@@ -1693,6 +1668,15 @@ var SQLiteStorage = class {
1693
1668
  const row = stmt.get(username, serviceFqn, Date.now());
1694
1669
  return row ? row.uuid : null;
1695
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
+ }
1696
1680
  async deleteService(serviceId, username) {
1697
1681
  const stmt = this.db.prepare(`
1698
1682
  DELETE FROM services