@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/README.md +121 -78
- package/dist/index.js +318 -220
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/src/app.ts +314 -272
- package/src/config.ts +1 -3
- package/src/crypto.ts +54 -0
- package/src/index.ts +0 -1
- package/src/storage/d1.ts +54 -7
- package/src/storage/hash-id.ts +4 -9
- package/src/storage/sqlite.ts +66 -15
- package/src/storage/types.ts +43 -10
- package/src/worker.ts +1 -3
- package/src/bloom.ts +0 -66
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.
|
|
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 {
|
|
764
|
-
if (!
|
|
765
|
-
return c.json({ error: "Missing required parameters:
|
|
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
|
-
},
|
|
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("/
|
|
847
|
+
app.get("/users/:username/services/:fqn", async (c) => {
|
|
795
848
|
try {
|
|
796
849
|
const username = c.req.param("username");
|
|
797
|
-
const
|
|
798
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
},
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
|
900
|
+
console.error("Error getting service:", err2);
|
|
813
901
|
return c.json({ error: "Internal server error" }, 500);
|
|
814
902
|
}
|
|
815
903
|
});
|
|
816
|
-
app.
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
852
|
-
|
|
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
|
-
|
|
855
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
996
|
+
app.delete("/users/:username/services/:fqn", authMiddleware, async (c) => {
|
|
892
997
|
try {
|
|
893
|
-
const
|
|
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
|
|
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.
|
|
1018
|
+
app.get("/services/:uuid", async (c) => {
|
|
937
1019
|
try {
|
|
938
|
-
const
|
|
939
|
-
const
|
|
940
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
965
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
|
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("/
|
|
1053
|
+
app.post("/services/:uuid/answer", authMiddleware, async (c) => {
|
|
1038
1054
|
try {
|
|
1039
|
-
const
|
|
1055
|
+
const uuid = c.req.param("uuid");
|
|
1040
1056
|
const body = await c.req.json();
|
|
1041
|
-
const { sdp
|
|
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(
|
|
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({
|
|
1081
|
+
return c.json({
|
|
1082
|
+
success: true,
|
|
1083
|
+
offerId: availableOffer.id
|
|
1084
|
+
}, 200);
|
|
1057
1085
|
} catch (err2) {
|
|
1058
|
-
console.error("Error answering
|
|
1086
|
+
console.error("Error answering service:", err2);
|
|
1059
1087
|
return c.json({ error: "Internal server error" }, 500);
|
|
1060
1088
|
}
|
|
1061
1089
|
});
|
|
1062
|
-
app.get("/
|
|
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
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
|
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("/
|
|
1114
|
+
app.post("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
|
|
1080
1115
|
try {
|
|
1081
|
-
const
|
|
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
|
|
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(
|
|
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("/
|
|
1150
|
+
app.get("/services/:uuid/ice-candidates", authMiddleware, async (c) => {
|
|
1101
1151
|
try {
|
|
1102
|
-
const
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
--
|
|
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,
|
|
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
|
});
|