@xtr-dev/rondevu-server 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -24
- package/dist/index.js +170 -186
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/src/app.ts +100 -186
package/README.md
CHANGED
|
@@ -240,10 +240,10 @@ Unpublish a service (requires authentication and ownership)
|
|
|
240
240
|
}
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
-
###
|
|
243
|
+
### WebRTC Signaling (Service-Based)
|
|
244
244
|
|
|
245
|
-
#### `POST /
|
|
246
|
-
|
|
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
|
-
"
|
|
255
|
-
{
|
|
256
|
-
"sdp": "v=0...",
|
|
257
|
-
"ttl": 300000
|
|
258
|
-
}
|
|
259
|
-
]
|
|
254
|
+
"sdp": "v=0..."
|
|
260
255
|
}
|
|
261
256
|
```
|
|
262
257
|
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
**Response:**
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"success": true,
|
|
262
|
+
"offerId": "offer-hash"
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
265
|
|
|
266
|
-
#### `
|
|
267
|
-
|
|
266
|
+
#### `GET /services/:uuid/answer`
|
|
267
|
+
Get answer for a service (offerer polls this)
|
|
268
268
|
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
**Headers:**
|
|
270
|
+
- `Authorization: Bearer {peerId}:{secret}`
|
|
271
271
|
|
|
272
|
-
**
|
|
272
|
+
**Response:**
|
|
273
273
|
```json
|
|
274
274
|
{
|
|
275
|
-
"
|
|
275
|
+
"offerId": "offer-hash",
|
|
276
|
+
"answererId": "answerer-peer-id",
|
|
277
|
+
"sdp": "v=0...",
|
|
278
|
+
"answeredAt": 1733404800000
|
|
276
279
|
}
|
|
277
280
|
```
|
|
278
281
|
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
|
835
|
-
if (!
|
|
836
|
-
return c.json({ error: "
|
|
851
|
+
const parsed = parseServiceFqn(serviceFqn);
|
|
852
|
+
if (!parsed) {
|
|
853
|
+
return c.json({ error: "Invalid service FQN format" }, 400);
|
|
837
854
|
}
|
|
838
|
-
const
|
|
839
|
-
|
|
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
|
|
843
|
-
|
|
844
|
-
|
|
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
|
|
847
|
-
const
|
|
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
|
|
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 {
|
|
879
|
-
if (!serviceFqn || !
|
|
880
|
-
return c.json({ error: "Missing required parameters: 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);
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
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
|
-
|
|
939
|
-
|
|
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
|
-
|
|
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
|
|
990
|
-
if (
|
|
991
|
-
return c.json({ error: "
|
|
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
|
|
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("/
|
|
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 {
|
|
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(
|
|
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({
|
|
1081
|
+
return c.json({
|
|
1082
|
+
success: true,
|
|
1083
|
+
offerId: availableOffer.id
|
|
1084
|
+
}, 200);
|
|
1140
1085
|
} catch (err2) {
|
|
1141
|
-
console.error("Error answering
|
|
1086
|
+
console.error("Error answering service:", err2);
|
|
1142
1087
|
return c.json({ error: "Internal server error" }, 500);
|
|
1143
1088
|
}
|
|
1144
1089
|
});
|
|
1145
|
-
app.get("/
|
|
1090
|
+
app.get("/services/:uuid/answer", authMiddleware, async (c) => {
|
|
1146
1091
|
try {
|
|
1147
|
-
const
|
|
1092
|
+
const uuid = c.req.param("uuid");
|
|
1148
1093
|
const peerId = getAuthenticatedPeerId(c);
|
|
1149
|
-
const
|
|
1150
|
-
if (!
|
|
1151
|
-
return c.json({ error: "
|
|
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
|
-
|
|
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:
|
|
1161
|
-
answererId:
|
|
1162
|
-
sdp:
|
|
1163
|
-
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("/
|
|
1114
|
+
app.post("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
|
|
1171
1115
|
try {
|
|
1172
|
-
const
|
|
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
|
|
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(
|
|
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("/
|
|
1150
|
+
app.get("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
|
|
1192
1151
|
try {
|
|
1193
|
-
const
|
|
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
|
|
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(
|
|
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
|