@xtr-dev/rondevu-server 0.2.3 → 0.3.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 +77 -68
- package/dist/index.js +242 -105
- package/dist/index.js.map +2 -2
- package/migrations/0005_v2_schema.sql +83 -0
- package/package.json +1 -1
- package/src/app.ts +267 -139
- package/src/config.ts +1 -3
- package/src/crypto.ts +97 -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
|
@@ -654,6 +654,29 @@ async function validateUsernameClaim(username, publicKey, signature, message) {
|
|
|
654
654
|
}
|
|
655
655
|
return { valid: true };
|
|
656
656
|
}
|
|
657
|
+
async function validateServicePublish(username, serviceFqn, publicKey, signature, message) {
|
|
658
|
+
const usernameCheck = validateUsername(username);
|
|
659
|
+
if (!usernameCheck.valid) {
|
|
660
|
+
return usernameCheck;
|
|
661
|
+
}
|
|
662
|
+
const parts = message.split(":");
|
|
663
|
+
if (parts.length !== 4 || parts[0] !== "publish" || parts[1] !== username || parts[2] !== serviceFqn) {
|
|
664
|
+
return { valid: false, error: "Invalid message format (expected: publish:{username}:{serviceFqn}:{timestamp})" };
|
|
665
|
+
}
|
|
666
|
+
const timestamp = parseInt(parts[3], 10);
|
|
667
|
+
if (isNaN(timestamp)) {
|
|
668
|
+
return { valid: false, error: "Invalid timestamp in message" };
|
|
669
|
+
}
|
|
670
|
+
const timestampCheck = validateTimestamp(timestamp);
|
|
671
|
+
if (!timestampCheck.valid) {
|
|
672
|
+
return timestampCheck;
|
|
673
|
+
}
|
|
674
|
+
const signatureValid = await verifyEd25519Signature(publicKey, signature, message);
|
|
675
|
+
if (!signatureValid) {
|
|
676
|
+
return { valid: false, error: "Invalid signature" };
|
|
677
|
+
}
|
|
678
|
+
return { valid: true };
|
|
679
|
+
}
|
|
657
680
|
|
|
658
681
|
// src/middleware/auth.ts
|
|
659
682
|
function createAuthMiddleware(authSecret) {
|
|
@@ -734,12 +757,35 @@ function createApp(storage, config) {
|
|
|
734
757
|
return c.json({ error: "Internal server error" }, 500);
|
|
735
758
|
}
|
|
736
759
|
});
|
|
737
|
-
app.
|
|
760
|
+
app.get("/users/:username", async (c) => {
|
|
738
761
|
try {
|
|
762
|
+
const username = c.req.param("username");
|
|
763
|
+
const claimed = await storage.getUsername(username);
|
|
764
|
+
if (!claimed) {
|
|
765
|
+
return c.json({
|
|
766
|
+
username,
|
|
767
|
+
available: true
|
|
768
|
+
}, 200);
|
|
769
|
+
}
|
|
770
|
+
return c.json({
|
|
771
|
+
username: claimed.username,
|
|
772
|
+
available: false,
|
|
773
|
+
claimedAt: claimed.claimedAt,
|
|
774
|
+
expiresAt: claimed.expiresAt,
|
|
775
|
+
publicKey: claimed.publicKey
|
|
776
|
+
}, 200);
|
|
777
|
+
} catch (err2) {
|
|
778
|
+
console.error("Error checking username:", err2);
|
|
779
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
app.post("/users/:username", async (c) => {
|
|
783
|
+
try {
|
|
784
|
+
const username = c.req.param("username");
|
|
739
785
|
const body = await c.req.json();
|
|
740
|
-
const {
|
|
741
|
-
if (!
|
|
742
|
-
return c.json({ error: "Missing required parameters:
|
|
786
|
+
const { publicKey, signature, message } = body;
|
|
787
|
+
if (!publicKey || !signature || !message) {
|
|
788
|
+
return c.json({ error: "Missing required parameters: publicKey, signature, message" }, 400);
|
|
743
789
|
}
|
|
744
790
|
const validation = await validateUsernameClaim(username, publicKey, signature, message);
|
|
745
791
|
if (!validation.valid) {
|
|
@@ -756,7 +802,7 @@ function createApp(storage, config) {
|
|
|
756
802
|
username: claimed.username,
|
|
757
803
|
claimedAt: claimed.claimedAt,
|
|
758
804
|
expiresAt: claimed.expiresAt
|
|
759
|
-
},
|
|
805
|
+
}, 201);
|
|
760
806
|
} catch (err2) {
|
|
761
807
|
if (err2.message?.includes("already claimed")) {
|
|
762
808
|
return c.json({ error: "Username already claimed by different public key" }, 409);
|
|
@@ -768,47 +814,70 @@ function createApp(storage, config) {
|
|
|
768
814
|
return c.json({ error: "Internal server error" }, 500);
|
|
769
815
|
}
|
|
770
816
|
});
|
|
771
|
-
app.get("/
|
|
817
|
+
app.get("/users/:username/services", async (c) => {
|
|
772
818
|
try {
|
|
773
819
|
const username = c.req.param("username");
|
|
774
|
-
const
|
|
775
|
-
if (!claimed) {
|
|
776
|
-
return c.json({
|
|
777
|
-
username,
|
|
778
|
-
available: true
|
|
779
|
-
}, 200);
|
|
780
|
-
}
|
|
820
|
+
const services = await storage.listServicesForUsername(username);
|
|
781
821
|
return c.json({
|
|
782
|
-
username
|
|
783
|
-
|
|
784
|
-
claimedAt: claimed.claimedAt,
|
|
785
|
-
expiresAt: claimed.expiresAt,
|
|
786
|
-
publicKey: claimed.publicKey
|
|
822
|
+
username,
|
|
823
|
+
services
|
|
787
824
|
}, 200);
|
|
788
825
|
} catch (err2) {
|
|
789
|
-
console.error("Error
|
|
826
|
+
console.error("Error listing services:", err2);
|
|
790
827
|
return c.json({ error: "Internal server error" }, 500);
|
|
791
828
|
}
|
|
792
829
|
});
|
|
793
|
-
app.get("/
|
|
830
|
+
app.get("/users/:username/services/:fqn", async (c) => {
|
|
794
831
|
try {
|
|
795
832
|
const username = c.req.param("username");
|
|
796
|
-
const
|
|
833
|
+
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);
|
|
837
|
+
}
|
|
838
|
+
const service = await storage.getServiceByUuid(uuid);
|
|
839
|
+
if (!service) {
|
|
840
|
+
return c.json({ error: "Service not found" }, 404);
|
|
841
|
+
}
|
|
842
|
+
const initialOffer = await storage.getOfferById(service.offerId);
|
|
843
|
+
if (!initialOffer) {
|
|
844
|
+
return c.json({ error: "Associated offer not found" }, 404);
|
|
845
|
+
}
|
|
846
|
+
const peerOffers = await storage.getOffersByPeerId(initialOffer.peerId);
|
|
847
|
+
const availableOffer = peerOffers.find((offer) => !offer.answererPeerId);
|
|
848
|
+
if (!availableOffer) {
|
|
849
|
+
return c.json({
|
|
850
|
+
error: "No available offers",
|
|
851
|
+
message: "All offers from this service are currently in use. Please try again later."
|
|
852
|
+
}, 503);
|
|
853
|
+
}
|
|
797
854
|
return c.json({
|
|
798
|
-
|
|
799
|
-
|
|
855
|
+
uuid,
|
|
856
|
+
serviceId: service.id,
|
|
857
|
+
username: service.username,
|
|
858
|
+
serviceFqn: service.serviceFqn,
|
|
859
|
+
offerId: availableOffer.id,
|
|
860
|
+
sdp: availableOffer.sdp,
|
|
861
|
+
isPublic: service.isPublic,
|
|
862
|
+
metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
|
|
863
|
+
createdAt: service.createdAt,
|
|
864
|
+
expiresAt: service.expiresAt
|
|
800
865
|
}, 200);
|
|
801
866
|
} catch (err2) {
|
|
802
|
-
console.error("Error
|
|
867
|
+
console.error("Error getting service:", err2);
|
|
803
868
|
return c.json({ error: "Internal server error" }, 500);
|
|
804
869
|
}
|
|
805
870
|
});
|
|
806
|
-
app.post("/services", authMiddleware, async (c) => {
|
|
871
|
+
app.post("/users/:username/services", authMiddleware, async (c) => {
|
|
872
|
+
let serviceFqn;
|
|
873
|
+
let offers = [];
|
|
807
874
|
try {
|
|
875
|
+
const username = c.req.param("username");
|
|
808
876
|
const body = await c.req.json();
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
877
|
+
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);
|
|
812
881
|
}
|
|
813
882
|
const fqnValidation = validateServiceFqn(serviceFqn);
|
|
814
883
|
if (!fqnValidation.valid) {
|
|
@@ -821,10 +890,17 @@ function createApp(storage, config) {
|
|
|
821
890
|
if (!usernameRecord) {
|
|
822
891
|
return c.json({ error: "Username not claimed" }, 404);
|
|
823
892
|
}
|
|
824
|
-
const signatureValidation = await
|
|
893
|
+
const signatureValidation = await validateServicePublish(username, serviceFqn, usernameRecord.publicKey, signature, message);
|
|
825
894
|
if (!signatureValidation.valid) {
|
|
826
895
|
return c.json({ error: "Invalid signature for username" }, 403);
|
|
827
896
|
}
|
|
897
|
+
const existingUuid = await storage.queryService(username, serviceFqn);
|
|
898
|
+
if (existingUuid) {
|
|
899
|
+
const existingService = await storage.getServiceByUuid(existingUuid);
|
|
900
|
+
if (existingService) {
|
|
901
|
+
await storage.deleteService(existingService.id, username);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
828
904
|
if (typeof sdp !== "string" || sdp.length === 0) {
|
|
829
905
|
return c.json({ error: "Invalid SDP" }, 400);
|
|
830
906
|
}
|
|
@@ -837,7 +913,7 @@ function createApp(storage, config) {
|
|
|
837
913
|
config.offerMaxTtl
|
|
838
914
|
);
|
|
839
915
|
const expiresAt = Date.now() + offerTtl;
|
|
840
|
-
|
|
916
|
+
offers = await storage.createOffers([{
|
|
841
917
|
peerId,
|
|
842
918
|
sdp,
|
|
843
919
|
expiresAt
|
|
@@ -855,52 +931,45 @@ function createApp(storage, config) {
|
|
|
855
931
|
metadata: metadata ? JSON.stringify(metadata) : void 0
|
|
856
932
|
});
|
|
857
933
|
return c.json({
|
|
858
|
-
serviceId: result.service.id,
|
|
859
934
|
uuid: result.indexUuid,
|
|
935
|
+
serviceFqn,
|
|
936
|
+
username,
|
|
937
|
+
serviceId: result.service.id,
|
|
860
938
|
offerId: offer.id,
|
|
939
|
+
sdp: offer.sdp,
|
|
940
|
+
isPublic: result.service.isPublic,
|
|
941
|
+
metadata,
|
|
942
|
+
createdAt: result.service.createdAt,
|
|
861
943
|
expiresAt: result.service.expiresAt
|
|
862
944
|
}, 201);
|
|
863
945
|
} catch (err2) {
|
|
864
946
|
console.error("Error creating service:", err2);
|
|
865
|
-
|
|
947
|
+
console.error("Error details:", {
|
|
948
|
+
message: err2.message,
|
|
949
|
+
stack: err2.stack,
|
|
950
|
+
username: c.req.param("username"),
|
|
951
|
+
serviceFqn,
|
|
952
|
+
offerId: offers[0]?.id
|
|
953
|
+
});
|
|
954
|
+
return c.json({
|
|
955
|
+
error: "Internal server error",
|
|
956
|
+
details: err2.message
|
|
957
|
+
}, 500);
|
|
866
958
|
}
|
|
867
959
|
});
|
|
868
|
-
app.
|
|
960
|
+
app.delete("/users/:username/services/:fqn", authMiddleware, async (c) => {
|
|
869
961
|
try {
|
|
870
|
-
const
|
|
962
|
+
const username = c.req.param("username");
|
|
963
|
+
const serviceFqn = decodeURIComponent(c.req.param("fqn"));
|
|
964
|
+
const uuid = await storage.queryService(username, serviceFqn);
|
|
965
|
+
if (!uuid) {
|
|
966
|
+
return c.json({ error: "Service not found" }, 404);
|
|
967
|
+
}
|
|
871
968
|
const service = await storage.getServiceByUuid(uuid);
|
|
872
969
|
if (!service) {
|
|
873
970
|
return c.json({ error: "Service not found" }, 404);
|
|
874
971
|
}
|
|
875
|
-
const
|
|
876
|
-
if (!offer) {
|
|
877
|
-
return c.json({ error: "Associated offer not found" }, 404);
|
|
878
|
-
}
|
|
879
|
-
return c.json({
|
|
880
|
-
serviceId: service.id,
|
|
881
|
-
username: service.username,
|
|
882
|
-
serviceFqn: service.serviceFqn,
|
|
883
|
-
offerId: service.offerId,
|
|
884
|
-
sdp: offer.sdp,
|
|
885
|
-
isPublic: service.isPublic,
|
|
886
|
-
metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
|
|
887
|
-
createdAt: service.createdAt,
|
|
888
|
-
expiresAt: service.expiresAt
|
|
889
|
-
}, 200);
|
|
890
|
-
} catch (err2) {
|
|
891
|
-
console.error("Error getting service:", err2);
|
|
892
|
-
return c.json({ error: "Internal server error" }, 500);
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
app.delete("/services/:serviceId", authMiddleware, async (c) => {
|
|
896
|
-
try {
|
|
897
|
-
const serviceId = c.req.param("serviceId");
|
|
898
|
-
const body = await c.req.json();
|
|
899
|
-
const { username } = body;
|
|
900
|
-
if (!username) {
|
|
901
|
-
return c.json({ error: "Missing required parameter: username" }, 400);
|
|
902
|
-
}
|
|
903
|
-
const deleted = await storage.deleteService(serviceId, username);
|
|
972
|
+
const deleted = await storage.deleteService(service.id, username);
|
|
904
973
|
if (!deleted) {
|
|
905
974
|
return c.json({ error: "Service not found or not owned by this username" }, 404);
|
|
906
975
|
}
|
|
@@ -910,24 +979,39 @@ function createApp(storage, config) {
|
|
|
910
979
|
return c.json({ error: "Internal server error" }, 500);
|
|
911
980
|
}
|
|
912
981
|
});
|
|
913
|
-
app.
|
|
982
|
+
app.get("/services/:uuid", async (c) => {
|
|
914
983
|
try {
|
|
915
|
-
const
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
if (!serviceFqn) {
|
|
919
|
-
return c.json({ error: "Missing required parameter: serviceFqn" }, 400);
|
|
920
|
-
}
|
|
921
|
-
const uuid = await storage.queryService(username, serviceFqn);
|
|
922
|
-
if (!uuid) {
|
|
984
|
+
const uuid = c.req.param("uuid");
|
|
985
|
+
const service = await storage.getServiceByUuid(uuid);
|
|
986
|
+
if (!service) {
|
|
923
987
|
return c.json({ error: "Service not found" }, 404);
|
|
924
988
|
}
|
|
989
|
+
const initialOffer = await storage.getOfferById(service.offerId);
|
|
990
|
+
if (!initialOffer) {
|
|
991
|
+
return c.json({ error: "Associated offer not found" }, 404);
|
|
992
|
+
}
|
|
993
|
+
const peerOffers = await storage.getOffersByPeerId(initialOffer.peerId);
|
|
994
|
+
const availableOffer = peerOffers.find((offer) => !offer.answererPeerId);
|
|
995
|
+
if (!availableOffer) {
|
|
996
|
+
return c.json({
|
|
997
|
+
error: "No available offers",
|
|
998
|
+
message: "All offers from this service are currently in use. Please try again later."
|
|
999
|
+
}, 503);
|
|
1000
|
+
}
|
|
925
1001
|
return c.json({
|
|
926
1002
|
uuid,
|
|
927
|
-
|
|
1003
|
+
serviceId: service.id,
|
|
1004
|
+
username: service.username,
|
|
1005
|
+
serviceFqn: service.serviceFqn,
|
|
1006
|
+
offerId: availableOffer.id,
|
|
1007
|
+
sdp: availableOffer.sdp,
|
|
1008
|
+
isPublic: service.isPublic,
|
|
1009
|
+
metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
|
|
1010
|
+
createdAt: service.createdAt,
|
|
1011
|
+
expiresAt: service.expiresAt
|
|
928
1012
|
}, 200);
|
|
929
1013
|
} catch (err2) {
|
|
930
|
-
console.error("Error
|
|
1014
|
+
console.error("Error getting service:", err2);
|
|
931
1015
|
return c.json({ error: "Internal server error" }, 500);
|
|
932
1016
|
}
|
|
933
1017
|
});
|
|
@@ -997,6 +1081,28 @@ function createApp(storage, config) {
|
|
|
997
1081
|
return c.json({ error: "Internal server error" }, 500);
|
|
998
1082
|
}
|
|
999
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
|
+
});
|
|
1000
1106
|
app.delete("/offers/:offerId", authMiddleware, async (c) => {
|
|
1001
1107
|
try {
|
|
1002
1108
|
const offerId = c.req.param("offerId");
|
|
@@ -1036,20 +1142,28 @@ function createApp(storage, config) {
|
|
|
1036
1142
|
return c.json({ error: "Internal server error" }, 500);
|
|
1037
1143
|
}
|
|
1038
1144
|
});
|
|
1039
|
-
app.get("/offers/
|
|
1145
|
+
app.get("/offers/:offerId/answer", authMiddleware, async (c) => {
|
|
1040
1146
|
try {
|
|
1147
|
+
const offerId = c.req.param("offerId");
|
|
1041
1148
|
const peerId = getAuthenticatedPeerId(c);
|
|
1042
|
-
const
|
|
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);
|
|
1155
|
+
}
|
|
1156
|
+
if (!offer.answererPeerId || !offer.answerSdp) {
|
|
1157
|
+
return c.json({ error: "Offer not yet answered" }, 404);
|
|
1158
|
+
}
|
|
1043
1159
|
return c.json({
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
answeredAt: offer.answeredAt
|
|
1049
|
-
}))
|
|
1160
|
+
offerId: offer.id,
|
|
1161
|
+
answererId: offer.answererPeerId,
|
|
1162
|
+
sdp: offer.answerSdp,
|
|
1163
|
+
answeredAt: offer.answeredAt
|
|
1050
1164
|
}, 200);
|
|
1051
1165
|
} catch (err2) {
|
|
1052
|
-
console.error("Error getting
|
|
1166
|
+
console.error("Error getting answer:", err2);
|
|
1053
1167
|
return c.json({ error: "Internal server error" }, 500);
|
|
1054
1168
|
}
|
|
1055
1169
|
});
|
|
@@ -1120,8 +1234,7 @@ function loadConfig() {
|
|
|
1120
1234
|
offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || "86400000", 10),
|
|
1121
1235
|
offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || "60000", 10),
|
|
1122
1236
|
cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL || "60000", 10),
|
|
1123
|
-
maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || "100", 10)
|
|
1124
|
-
maxTopicsPerOffer: parseInt(process.env.MAX_TOPICS_PER_OFFER || "50", 10)
|
|
1237
|
+
maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || "100", 10)
|
|
1125
1238
|
};
|
|
1126
1239
|
}
|
|
1127
1240
|
|
|
@@ -1130,11 +1243,9 @@ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
|
1130
1243
|
var import_node_crypto = require("node:crypto");
|
|
1131
1244
|
|
|
1132
1245
|
// src/storage/hash-id.ts
|
|
1133
|
-
async function generateOfferHash(sdp
|
|
1246
|
+
async function generateOfferHash(sdp) {
|
|
1134
1247
|
const sanitizedOffer = {
|
|
1135
|
-
sdp
|
|
1136
|
-
topics: [...topics].sort()
|
|
1137
|
-
// Sort topics for consistency
|
|
1248
|
+
sdp
|
|
1138
1249
|
};
|
|
1139
1250
|
const jsonString = JSON.stringify(sanitizedOffer);
|
|
1140
1251
|
const encoder = new TextEncoder();
|
|
@@ -1161,10 +1272,11 @@ var SQLiteStorage = class {
|
|
|
1161
1272
|
*/
|
|
1162
1273
|
initializeDatabase() {
|
|
1163
1274
|
this.db.exec(`
|
|
1164
|
-
--
|
|
1275
|
+
-- WebRTC signaling offers
|
|
1165
1276
|
CREATE TABLE IF NOT EXISTS offers (
|
|
1166
1277
|
id TEXT PRIMARY KEY,
|
|
1167
1278
|
peer_id TEXT NOT NULL,
|
|
1279
|
+
service_id TEXT,
|
|
1168
1280
|
sdp TEXT NOT NULL,
|
|
1169
1281
|
created_at INTEGER NOT NULL,
|
|
1170
1282
|
expires_at INTEGER NOT NULL,
|
|
@@ -1172,10 +1284,12 @@ var SQLiteStorage = class {
|
|
|
1172
1284
|
secret TEXT,
|
|
1173
1285
|
answerer_peer_id TEXT,
|
|
1174
1286
|
answer_sdp TEXT,
|
|
1175
|
-
answered_at INTEGER
|
|
1287
|
+
answered_at INTEGER,
|
|
1288
|
+
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
|
1176
1289
|
);
|
|
1177
1290
|
|
|
1178
1291
|
CREATE INDEX IF NOT EXISTS idx_offers_peer ON offers(peer_id);
|
|
1292
|
+
CREATE INDEX IF NOT EXISTS idx_offers_service ON offers(service_id);
|
|
1179
1293
|
CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
|
|
1180
1294
|
CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
|
|
1181
1295
|
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
|
|
@@ -1209,25 +1323,22 @@ var SQLiteStorage = class {
|
|
|
1209
1323
|
CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
|
|
1210
1324
|
CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
|
|
1211
1325
|
|
|
1212
|
-
-- Services table
|
|
1326
|
+
-- Services table (one service can have multiple offers)
|
|
1213
1327
|
CREATE TABLE IF NOT EXISTS services (
|
|
1214
1328
|
id TEXT PRIMARY KEY,
|
|
1215
1329
|
username TEXT NOT NULL,
|
|
1216
1330
|
service_fqn TEXT NOT NULL,
|
|
1217
|
-
offer_id TEXT NOT NULL,
|
|
1218
1331
|
created_at INTEGER NOT NULL,
|
|
1219
1332
|
expires_at INTEGER NOT NULL,
|
|
1220
1333
|
is_public INTEGER NOT NULL DEFAULT 0,
|
|
1221
1334
|
metadata TEXT,
|
|
1222
1335
|
FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
|
|
1223
|
-
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE,
|
|
1224
1336
|
UNIQUE(username, service_fqn)
|
|
1225
1337
|
);
|
|
1226
1338
|
|
|
1227
1339
|
CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
|
|
1228
1340
|
CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
|
|
1229
1341
|
CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
|
|
1230
|
-
CREATE INDEX IF NOT EXISTS idx_services_offer ON services(offer_id);
|
|
1231
1342
|
|
|
1232
1343
|
-- Service index table (privacy layer)
|
|
1233
1344
|
CREATE TABLE IF NOT EXISTS service_index (
|
|
@@ -1251,19 +1362,21 @@ var SQLiteStorage = class {
|
|
|
1251
1362
|
const offersWithIds = await Promise.all(
|
|
1252
1363
|
offers.map(async (offer) => ({
|
|
1253
1364
|
...offer,
|
|
1254
|
-
|
|
1365
|
+
serviceId: offer.serviceId || null,
|
|
1366
|
+
id: offer.id || await generateOfferHash(offer.sdp)
|
|
1255
1367
|
}))
|
|
1256
1368
|
);
|
|
1257
1369
|
const transaction = this.db.transaction((offersWithIds2) => {
|
|
1258
1370
|
const offerStmt = this.db.prepare(`
|
|
1259
|
-
INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
|
|
1260
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1371
|
+
INSERT INTO offers (id, peer_id, service_id, sdp, created_at, expires_at, last_seen, secret)
|
|
1372
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1261
1373
|
`);
|
|
1262
1374
|
for (const offer of offersWithIds2) {
|
|
1263
1375
|
const now = Date.now();
|
|
1264
1376
|
offerStmt.run(
|
|
1265
1377
|
offer.id,
|
|
1266
1378
|
offer.peerId,
|
|
1379
|
+
offer.serviceId || null,
|
|
1267
1380
|
offer.sdp,
|
|
1268
1381
|
now,
|
|
1269
1382
|
offer.expiresAt,
|
|
@@ -1273,6 +1386,7 @@ var SQLiteStorage = class {
|
|
|
1273
1386
|
created.push({
|
|
1274
1387
|
id: offer.id,
|
|
1275
1388
|
peerId: offer.peerId,
|
|
1389
|
+
serviceId: offer.serviceId,
|
|
1276
1390
|
sdp: offer.sdp,
|
|
1277
1391
|
createdAt: now,
|
|
1278
1392
|
expiresAt: offer.expiresAt,
|
|
@@ -1475,16 +1589,20 @@ var SQLiteStorage = class {
|
|
|
1475
1589
|
const serviceId = (0, import_node_crypto.randomUUID)();
|
|
1476
1590
|
const indexUuid = (0, import_node_crypto.randomUUID)();
|
|
1477
1591
|
const now = Date.now();
|
|
1592
|
+
const offerRequests = request.offers.map((offer) => ({
|
|
1593
|
+
...offer,
|
|
1594
|
+
serviceId
|
|
1595
|
+
}));
|
|
1596
|
+
const offers = await this.createOffers(offerRequests);
|
|
1478
1597
|
const transaction = this.db.transaction(() => {
|
|
1479
1598
|
const serviceStmt = this.db.prepare(`
|
|
1480
|
-
INSERT INTO services (id, username, service_fqn,
|
|
1481
|
-
VALUES (?, ?, ?, ?, ?, ?,
|
|
1599
|
+
INSERT INTO services (id, username, service_fqn, created_at, expires_at, is_public, metadata)
|
|
1600
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1482
1601
|
`);
|
|
1483
1602
|
serviceStmt.run(
|
|
1484
1603
|
serviceId,
|
|
1485
1604
|
request.username,
|
|
1486
1605
|
request.serviceFqn,
|
|
1487
|
-
request.offerId,
|
|
1488
1606
|
now,
|
|
1489
1607
|
request.expiresAt,
|
|
1490
1608
|
request.isPublic ? 1 : 0,
|
|
@@ -1510,15 +1628,23 @@ var SQLiteStorage = class {
|
|
|
1510
1628
|
id: serviceId,
|
|
1511
1629
|
username: request.username,
|
|
1512
1630
|
serviceFqn: request.serviceFqn,
|
|
1513
|
-
offerId: request.offerId,
|
|
1514
1631
|
createdAt: now,
|
|
1515
1632
|
expiresAt: request.expiresAt,
|
|
1516
1633
|
isPublic: request.isPublic || false,
|
|
1517
1634
|
metadata: request.metadata
|
|
1518
1635
|
},
|
|
1519
|
-
indexUuid
|
|
1636
|
+
indexUuid,
|
|
1637
|
+
offers
|
|
1520
1638
|
};
|
|
1521
1639
|
}
|
|
1640
|
+
async batchCreateServices(requests) {
|
|
1641
|
+
const results = [];
|
|
1642
|
+
for (const request of requests) {
|
|
1643
|
+
const result = await this.createService(request);
|
|
1644
|
+
results.push(result);
|
|
1645
|
+
}
|
|
1646
|
+
return results;
|
|
1647
|
+
}
|
|
1522
1648
|
async getServiceById(serviceId) {
|
|
1523
1649
|
const stmt = this.db.prepare(`
|
|
1524
1650
|
SELECT * FROM services
|
|
@@ -1591,6 +1717,7 @@ var SQLiteStorage = class {
|
|
|
1591
1717
|
return {
|
|
1592
1718
|
id: row.id,
|
|
1593
1719
|
peerId: row.peer_id,
|
|
1720
|
+
serviceId: row.service_id || void 0,
|
|
1594
1721
|
sdp: row.sdp,
|
|
1595
1722
|
createdAt: row.created_at,
|
|
1596
1723
|
expiresAt: row.expires_at,
|
|
@@ -1609,13 +1736,24 @@ var SQLiteStorage = class {
|
|
|
1609
1736
|
id: row.id,
|
|
1610
1737
|
username: row.username,
|
|
1611
1738
|
serviceFqn: row.service_fqn,
|
|
1612
|
-
offerId: row.offer_id,
|
|
1613
1739
|
createdAt: row.created_at,
|
|
1614
1740
|
expiresAt: row.expires_at,
|
|
1615
1741
|
isPublic: row.is_public === 1,
|
|
1616
1742
|
metadata: row.metadata || void 0
|
|
1617
1743
|
};
|
|
1618
1744
|
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Get all offers for a service
|
|
1747
|
+
*/
|
|
1748
|
+
async getOffersForService(serviceId) {
|
|
1749
|
+
const stmt = this.db.prepare(`
|
|
1750
|
+
SELECT * FROM offers
|
|
1751
|
+
WHERE service_id = ? AND expires_at > ?
|
|
1752
|
+
ORDER BY created_at ASC
|
|
1753
|
+
`);
|
|
1754
|
+
const rows = stmt.all(serviceId, Date.now());
|
|
1755
|
+
return rows.map((row) => this.rowToOffer(row));
|
|
1756
|
+
}
|
|
1619
1757
|
};
|
|
1620
1758
|
|
|
1621
1759
|
// src/index.ts
|
|
@@ -1631,7 +1769,6 @@ async function main() {
|
|
|
1631
1769
|
offerMinTtl: `${config.offerMinTtl}ms`,
|
|
1632
1770
|
cleanupInterval: `${config.cleanupInterval}ms`,
|
|
1633
1771
|
maxOffersPerRequest: config.maxOffersPerRequest,
|
|
1634
|
-
maxTopicsPerOffer: config.maxTopicsPerOffer,
|
|
1635
1772
|
corsOrigins: config.corsOrigins,
|
|
1636
1773
|
version: config.version
|
|
1637
1774
|
});
|