mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
Compare commits
12 Commits
feat/image
...
feat/home
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5b3043436 | ||
|
|
623a0db97f | ||
|
|
9827100d19 | ||
|
|
88f499ba5e | ||
|
|
0a1a4cd9b6 | ||
|
|
d7cb47fdd6 | ||
|
|
8b07adb0fc | ||
|
|
661d9d2e56 | ||
|
|
6eee40fcbe | ||
|
|
69d728c4a9 | ||
|
|
be85594bdc | ||
|
|
6e82eff9e2 |
@@ -72,7 +72,7 @@ export default component$(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const lockPlay = $(async () => {
|
const lockPlay = $(async () => {
|
||||||
if (!canvas.value || !playState.hasStream) return;
|
if (!canvas.value || !playState.hasStream || playState.nestriLock) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await canvas.value.requestPointerLock();
|
await canvas.value.requestPointerLock();
|
||||||
@@ -156,18 +156,22 @@ export default component$(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line qwik/no-use-visible-task
|
// eslint-disable-next-line qwik/no-use-visible-task
|
||||||
useVisibleTask$(({ track }) => {
|
useVisibleTask$(({ track }) => {
|
||||||
track(() => canvas.value);
|
track(() => canvas.value);
|
||||||
if (!canvas.value) return; // Ensure canvas is available
|
if (!canvas.value) return; // Ensure canvas is available
|
||||||
|
// Get query parameter "peerURL" from the URL
|
||||||
|
let peerURL = new URLSearchParams(window.location.search).get("peerURL");
|
||||||
|
if (!peerURL || peerURL.length <= 0) {
|
||||||
|
peerURL = "/dnsaddr/relay.dathorse.com/p2p/12D3KooWPK4v5wKYNYx9oXWjqLM8Xix6nm13o91j1Feqq98fLBsw";
|
||||||
|
}
|
||||||
|
|
||||||
setupPointerLockListener();
|
setupPointerLockListener();
|
||||||
try {
|
try {
|
||||||
if (!playState.video) {
|
if (!playState.video) {
|
||||||
playState.video = document.createElement("video") as HTMLVideoElement
|
playState.video = document.createElement("video") as HTMLVideoElement;
|
||||||
playState.video.style.visibility = "hidden";
|
playState.video.style.visibility = "hidden";
|
||||||
playState.webrtc = noSerialize(new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => {
|
playState.webrtc = noSerialize(new WebRTCStream(peerURL, id, async (mediaStream) => {
|
||||||
if (playState.video && mediaStream && playState.video.srcObject === null) {
|
if (playState.video && mediaStream && playState.video.srcObject === null) {
|
||||||
console.log("Setting mediastream");
|
console.log("Setting mediastream");
|
||||||
playState.video.srcObject = mediaStream;
|
playState.video.srcObject = mediaStream;
|
||||||
|
|||||||
285
bun.lock
285
bun.lock
@@ -4,7 +4,6 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "nestri",
|
"name": "nestri",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sharp": "^0.34.2",
|
|
||||||
"sst": "^3.11.21",
|
"sst": "^3.11.21",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -125,9 +124,11 @@
|
|||||||
"@aws-sdk/client-sqs": "^3.806.0",
|
"@aws-sdk/client-sqs": "^3.806.0",
|
||||||
"@nestri/core": "workspace:",
|
"@nestri/core": "workspace:",
|
||||||
"actor-core": "^0.8.0",
|
"actor-core": "^0.8.0",
|
||||||
"aws4fetch": "^1.0.20",
|
|
||||||
"hono": "^4.7.8",
|
"hono": "^4.7.8",
|
||||||
"hono-openapi": "^0.4.8",
|
"hono-openapi": "^0.4.8",
|
||||||
|
"steam-session": "*",
|
||||||
|
"steamcommunity": "^3.48.6",
|
||||||
|
"steamid": "^2.1.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
@@ -520,6 +521,8 @@
|
|||||||
|
|
||||||
"@databases/validate-unicode": ["@databases/validate-unicode@1.0.0", "", {}, "sha512-dLKqxGcymeVwEb/6c44KjOnzaAafFf0Wxa8xcfEjx/qOl3rdijsKYBAtIGhtVtOlpPf/PFKfgTuFurSPn/3B/g=="],
|
"@databases/validate-unicode": ["@databases/validate-unicode@1.0.0", "", {}, "sha512-dLKqxGcymeVwEb/6c44KjOnzaAafFf0Wxa8xcfEjx/qOl3rdijsKYBAtIGhtVtOlpPf/PFKfgTuFurSPn/3B/g=="],
|
||||||
|
|
||||||
|
"@doctormckay/stats-reporter": ["@doctormckay/stats-reporter@1.0.5", "", {}, "sha512-lCAuKW053zz91sKZZcGfOHxigBqn0Lo+/JvHBQq3XqzLJxn0YeZ5mJ96+PZto+PDCkgg+c/BX2Xo8DvAN44xLg=="],
|
||||||
|
|
||||||
"@doctormckay/stdlib": ["@doctormckay/stdlib@2.10.0", "", { "dependencies": { "psl": "^1.9.0" } }, "sha512-bwy+gPn6oa2KTpfxJKX3leZoV/wHDVtO0/gq3usPvqPswG//dcf3jVB8LcbRRsKO3BXCt5DqctOQ+Xb07ivxnw=="],
|
"@doctormckay/stdlib": ["@doctormckay/stdlib@2.10.0", "", { "dependencies": { "psl": "^1.9.0" } }, "sha512-bwy+gPn6oa2KTpfxJKX3leZoV/wHDVtO0/gq3usPvqPswG//dcf3jVB8LcbRRsKO3BXCt5DqctOQ+Xb07ivxnw=="],
|
||||||
|
|
||||||
"@doctormckay/user-agents": ["@doctormckay/user-agents@1.0.0", "", {}, "sha512-F+sL1YmebZTY2CnjoR9BXFEULpq7y8dxyLx48LZVa0BSDseXdLG/DtPISfM1iNv1XKCeiBzVNfAT/MOQ69v1Zw=="],
|
"@doctormckay/user-agents": ["@doctormckay/user-agents@1.0.0", "", {}, "sha512-F+sL1YmebZTY2CnjoR9BXFEULpq7y8dxyLx48LZVa0BSDseXdLG/DtPISfM1iNv1XKCeiBzVNfAT/MOQ69v1Zw=="],
|
||||||
@@ -704,9 +707,9 @@
|
|||||||
|
|
||||||
"@iconify/vue": ["@iconify/vue@4.3.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "vue": ">=3" } }, "sha512-Xq0h6zMrHBbrW8jXJ9fISi+x8oDQllg5hTDkDuxnWiskJ63rpJu9CvJshj8VniHVTbsxCg9fVoPAaNp3RQI5OQ=="],
|
"@iconify/vue": ["@iconify/vue@4.3.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "vue": ">=3" } }, "sha512-Xq0h6zMrHBbrW8jXJ9fISi+x8oDQllg5hTDkDuxnWiskJ63rpJu9CvJshj8VniHVTbsxCg9fVoPAaNp3RQI5OQ=="],
|
||||||
|
|
||||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg=="],
|
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A=="],
|
||||||
|
|
||||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g=="],
|
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q=="],
|
||||||
|
|
||||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="],
|
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="],
|
||||||
|
|
||||||
@@ -726,25 +729,23 @@
|
|||||||
|
|
||||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="],
|
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="],
|
||||||
|
|
||||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ=="],
|
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA=="],
|
||||||
|
|
||||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q=="],
|
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ=="],
|
||||||
|
|
||||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw=="],
|
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA=="],
|
||||||
|
|
||||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ=="],
|
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA=="],
|
||||||
|
|
||||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA=="],
|
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ=="],
|
||||||
|
|
||||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA=="],
|
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg=="],
|
||||||
|
|
||||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.2", "", { "dependencies": { "@emnapi/runtime": "^1.4.3" }, "cpu": "none" }, "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ=="],
|
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.1", "", { "dependencies": { "@emnapi/runtime": "^1.4.0" }, "cpu": "none" }, "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg=="],
|
||||||
|
|
||||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ=="],
|
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw=="],
|
||||||
|
|
||||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw=="],
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="],
|
||||||
|
|
||||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="],
|
|
||||||
|
|
||||||
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
|
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
|
||||||
|
|
||||||
@@ -1848,8 +1849,12 @@
|
|||||||
|
|
||||||
"as-table": ["as-table@1.0.55", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="],
|
"as-table": ["as-table@1.0.55", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="],
|
||||||
|
|
||||||
|
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
|
||||||
|
|
||||||
"assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="],
|
"assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="],
|
||||||
|
|
||||||
|
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
|
||||||
|
|
||||||
"ast-kit": ["ast-kit@1.4.2", "", { "dependencies": { "@babel/parser": "^7.26.9", "pathe": "^2.0.3" } }, "sha512-lvGehj1XsrIoQrD5CfPduIzQbcpuX2EPjlk/vDMDQF9U9HLRB6WwMTdighj5n52hdhh8xg9VgPTU7Q25MuJ/rw=="],
|
"ast-kit": ["ast-kit@1.4.2", "", { "dependencies": { "@babel/parser": "^7.26.9", "pathe": "^2.0.3" } }, "sha512-lvGehj1XsrIoQrD5CfPduIzQbcpuX2EPjlk/vDMDQF9U9HLRB6WwMTdighj5n52hdhh8xg9VgPTU7Q25MuJ/rw=="],
|
||||||
|
|
||||||
"ast-types": ["ast-types@0.15.2", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg=="],
|
"ast-types": ["ast-types@0.15.2", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg=="],
|
||||||
@@ -1858,7 +1863,7 @@
|
|||||||
|
|
||||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||||
|
|
||||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
"async": ["async@2.6.4", "", { "dependencies": { "lodash": "^4.17.14" } }, "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA=="],
|
||||||
|
|
||||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||||
|
|
||||||
@@ -1882,6 +1887,10 @@
|
|||||||
|
|
||||||
"aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="],
|
"aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="],
|
||||||
|
|
||||||
|
"aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="],
|
||||||
|
|
||||||
|
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
|
||||||
|
|
||||||
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||||
|
|
||||||
"axios": ["axios@1.8.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg=="],
|
"axios": ["axios@1.8.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg=="],
|
||||||
@@ -1902,6 +1911,8 @@
|
|||||||
|
|
||||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||||
|
|
||||||
|
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||||
|
|
||||||
"bignumber.js": ["bignumber.js@9.3.0", "", {}, "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA=="],
|
"bignumber.js": ["bignumber.js@9.3.0", "", {}, "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA=="],
|
||||||
|
|
||||||
"bin-links": ["bin-links@4.0.4", "", { "dependencies": { "cmd-shim": "^6.0.0", "npm-normalize-package-bin": "^3.0.0", "read-cmd-shim": "^4.0.0", "write-file-atomic": "^5.0.0" } }, "sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA=="],
|
"bin-links": ["bin-links@4.0.4", "", { "dependencies": { "cmd-shim": "^6.0.0", "npm-normalize-package-bin": "^3.0.0", "read-cmd-shim": "^4.0.0", "write-file-atomic": "^5.0.0" } }, "sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA=="],
|
||||||
@@ -1978,6 +1989,8 @@
|
|||||||
|
|
||||||
"capital-case": ["capital-case@1.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A=="],
|
"capital-case": ["capital-case@1.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A=="],
|
||||||
|
|
||||||
|
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
|
||||||
|
|
||||||
"cbor-extract": ["cbor-extract@2.2.0", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", "@cbor-extract/cbor-extract-linux-arm": "2.2.0", "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", "@cbor-extract/cbor-extract-linux-x64": "2.2.0", "@cbor-extract/cbor-extract-win32-x64": "2.2.0" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA=="],
|
"cbor-extract": ["cbor-extract@2.2.0", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", "@cbor-extract/cbor-extract-linux-arm": "2.2.0", "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", "@cbor-extract/cbor-extract-linux-x64": "2.2.0", "@cbor-extract/cbor-extract-win32-x64": "2.2.0" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA=="],
|
||||||
|
|
||||||
"cbor-x": ["cbor-x@1.6.0", "", { "optionalDependencies": { "cbor-extract": "^2.2.0" } }, "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg=="],
|
"cbor-x": ["cbor-x@1.6.0", "", { "optionalDependencies": { "cbor-extract": "^2.2.0" } }, "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg=="],
|
||||||
@@ -2000,6 +2013,8 @@
|
|||||||
|
|
||||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||||
|
|
||||||
|
"cheerio": ["cheerio@0.22.0", "", { "dependencies": { "css-select": "~1.2.0", "dom-serializer": "~0.1.0", "entities": "~1.1.1", "htmlparser2": "^3.9.1", "lodash.assignin": "^4.0.9", "lodash.bind": "^4.1.4", "lodash.defaults": "^4.0.1", "lodash.filter": "^4.4.0", "lodash.flatten": "^4.2.0", "lodash.foreach": "^4.3.0", "lodash.map": "^4.4.0", "lodash.merge": "^4.4.0", "lodash.pick": "^4.2.1", "lodash.reduce": "^4.4.0", "lodash.reject": "^4.4.0", "lodash.some": "^4.4.0" } }, "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||||
|
|
||||||
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
|
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
|
||||||
@@ -2124,7 +2139,7 @@
|
|||||||
|
|
||||||
"css-declaration-sorter": ["css-declaration-sorter@7.2.0", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow=="],
|
"css-declaration-sorter": ["css-declaration-sorter@7.2.0", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow=="],
|
||||||
|
|
||||||
"css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
|
"css-select": ["css-select@1.2.0", "", { "dependencies": { "boolbase": "~1.0.0", "css-what": "2.1", "domutils": "1.5.1", "nth-check": "~1.0.1" } }, "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA=="],
|
||||||
|
|
||||||
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
|
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
|
||||||
|
|
||||||
@@ -2142,6 +2157,10 @@
|
|||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"cuint": ["cuint@0.2.2", "", {}, "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw=="],
|
||||||
|
|
||||||
|
"dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="],
|
||||||
|
|
||||||
"data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],
|
"data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],
|
||||||
|
|
||||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||||
@@ -2208,7 +2227,7 @@
|
|||||||
|
|
||||||
"detab": ["detab@3.0.2", "", {}, "sha512-7Bp16Bk8sk0Y6gdXiCtnpGbghn8atnTJdd/82aWvS5ESnlcNvgUc10U2NYS0PAiDSGjWiI8qs/Cv1b2uSGdQ8w=="],
|
"detab": ["detab@3.0.2", "", {}, "sha512-7Bp16Bk8sk0Y6gdXiCtnpGbghn8atnTJdd/82aWvS5ESnlcNvgUc10U2NYS0PAiDSGjWiI8qs/Cv1b2uSGdQ8w=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
"detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
@@ -2230,7 +2249,7 @@
|
|||||||
|
|
||||||
"docus-starter": ["docus-starter@workspace:apps/docs"],
|
"docus-starter": ["docus-starter@workspace:apps/docs"],
|
||||||
|
|
||||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
"dom-serializer": ["dom-serializer@0.1.1", "", { "dependencies": { "domelementtype": "^1.3.0", "entities": "^1.1.1" } }, "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA=="],
|
||||||
|
|
||||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||||
|
|
||||||
@@ -2258,6 +2277,8 @@
|
|||||||
|
|
||||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||||
|
|
||||||
|
"ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="],
|
||||||
|
|
||||||
"eciesjs": ["eciesjs@0.4.14", "", { "dependencies": { "@ecies/ciphers": "^0.2.2", "@noble/ciphers": "^1.0.0", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0" } }, "sha512-eJAgf9pdv214Hn98FlUzclRMYWF7WfoLlkS9nWMTm1qcCwn6Ad4EGD9lr9HXMBfSrZhYQujRE+p0adPRkctC6A=="],
|
"eciesjs": ["eciesjs@0.4.14", "", { "dependencies": { "@ecies/ciphers": "^0.2.2", "@noble/ciphers": "^1.0.0", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0" } }, "sha512-eJAgf9pdv214Hn98FlUzclRMYWF7WfoLlkS9nWMTm1qcCwn6Ad4EGD9lr9HXMBfSrZhYQujRE+p0adPRkctC6A=="],
|
||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
@@ -2452,6 +2473,8 @@
|
|||||||
|
|
||||||
"externality": ["externality@1.0.2", "", { "dependencies": { "enhanced-resolve": "^5.14.1", "mlly": "^1.3.0", "pathe": "^1.1.1", "ufo": "^1.1.2" } }, "sha512-LyExtJWKxtgVzmgtEHyQtLFpw1KFhQphF9nTG8TpAIVkiI/xQ3FJh75tRFLYl4hkn7BNIIdLJInuDAavX35pMw=="],
|
"externality": ["externality@1.0.2", "", { "dependencies": { "enhanced-resolve": "^5.14.1", "mlly": "^1.3.0", "pathe": "^1.1.1", "ufo": "^1.1.2" } }, "sha512-LyExtJWKxtgVzmgtEHyQtLFpw1KFhQphF9nTG8TpAIVkiI/xQ3FJh75tRFLYl4hkn7BNIIdLJInuDAavX35pMw=="],
|
||||||
|
|
||||||
|
"extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="],
|
||||||
|
|
||||||
"fast-average-color": ["fast-average-color@9.5.0", "", {}, "sha512-nC6x2YIlJ9xxgkMFMd1BNoM1ctMjNoRKfRliPmiEWW3S6rLTHiQcy9g3pt/xiKv/D0NAAkhb9VyV+WJFvTqMGg=="],
|
"fast-average-color": ["fast-average-color@9.5.0", "", {}, "sha512-nC6x2YIlJ9xxgkMFMd1BNoM1ctMjNoRKfRliPmiEWW3S6rLTHiQcy9g3pt/xiKv/D0NAAkhb9VyV+WJFvTqMGg=="],
|
||||||
|
|
||||||
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
||||||
@@ -2524,6 +2547,8 @@
|
|||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
|
"forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="],
|
||||||
|
|
||||||
"form-data": ["form-data@2.5.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ=="],
|
"form-data": ["form-data@2.5.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ=="],
|
||||||
|
|
||||||
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||||
@@ -2584,6 +2609,8 @@
|
|||||||
|
|
||||||
"get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="],
|
"get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="],
|
||||||
|
|
||||||
|
"getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="],
|
||||||
|
|
||||||
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||||
|
|
||||||
"git-config-path": ["git-config-path@2.0.0", "", {}, "sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA=="],
|
"git-config-path": ["git-config-path@2.0.0", "", {}, "sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA=="],
|
||||||
@@ -2628,6 +2655,10 @@
|
|||||||
|
|
||||||
"h3": ["h3@1.15.1", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.3", "defu": "^6.1.4", "destr": "^2.0.3", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.5.4", "uncrypto": "^0.1.3" } }, "sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA=="],
|
"h3": ["h3@1.15.1", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.3", "defu": "^6.1.4", "destr": "^2.0.3", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.5.4", "uncrypto": "^0.1.3" } }, "sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA=="],
|
||||||
|
|
||||||
|
"har-schema": ["har-schema@2.0.0", "", {}, "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q=="],
|
||||||
|
|
||||||
|
"har-validator": ["har-validator@5.1.5", "", { "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w=="],
|
||||||
|
|
||||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
@@ -2698,6 +2729,8 @@
|
|||||||
|
|
||||||
"http-shutdown": ["http-shutdown@1.2.2", "", {}, "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw=="],
|
"http-shutdown": ["http-shutdown@1.2.2", "", {}, "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw=="],
|
||||||
|
|
||||||
|
"http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="],
|
||||||
|
|
||||||
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
|
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
|
||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
@@ -2718,6 +2751,8 @@
|
|||||||
|
|
||||||
"image-meta": ["image-meta@0.2.1", "", {}, "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw=="],
|
"image-meta": ["image-meta@0.2.1", "", {}, "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw=="],
|
||||||
|
|
||||||
|
"image-size": ["image-size@0.8.3", "", { "dependencies": { "queue": "6.0.1" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-SMtq1AJ+aqHB45c3FsB4ERK0UCiA2d3H1uq8s+8T0Pf8A3W4teyBQyaFaktH6xvZqh+npwlKU7i4fJo0r7TYTg=="],
|
||||||
|
|
||||||
"imagetools-core": ["imagetools-core@7.0.2", "", {}, "sha512-nrLdKLJHHXd8MitwlXK6/h1TSwGaH3X1DZ3z6yMv/tX7dJ12ecLxZ6P5jgKetfIFh8IJwH9fCWMoTA8ixg0VVA=="],
|
"imagetools-core": ["imagetools-core@7.0.2", "", {}, "sha512-nrLdKLJHHXd8MitwlXK6/h1TSwGaH3X1DZ3z6yMv/tX7dJ12ecLxZ6P5jgKetfIFh8IJwH9fCWMoTA8ixg0VVA=="],
|
||||||
|
|
||||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
@@ -2842,6 +2877,8 @@
|
|||||||
|
|
||||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||||
|
|
||||||
|
"is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="],
|
||||||
|
|
||||||
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||||
|
|
||||||
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||||
@@ -2860,6 +2897,8 @@
|
|||||||
|
|
||||||
"isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="],
|
"isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="],
|
||||||
|
|
||||||
|
"isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="],
|
||||||
|
|
||||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
||||||
"javascript-stringify": ["javascript-stringify@2.1.0", "", {}, "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="],
|
"javascript-stringify": ["javascript-stringify@2.1.0", "", {}, "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="],
|
||||||
@@ -2892,6 +2931,8 @@
|
|||||||
|
|
||||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="],
|
"json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="],
|
||||||
|
|
||||||
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
"json-schema-ref-resolver": ["json-schema-ref-resolver@2.0.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q=="],
|
"json-schema-ref-resolver": ["json-schema-ref-resolver@2.0.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q=="],
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
@@ -2902,6 +2943,8 @@
|
|||||||
|
|
||||||
"json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="],
|
"json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="],
|
||||||
|
|
||||||
|
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||||
@@ -2910,6 +2953,8 @@
|
|||||||
|
|
||||||
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
|
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
|
||||||
|
|
||||||
|
"jsprim": ["jsprim@1.4.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw=="],
|
||||||
|
|
||||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||||
|
|
||||||
"just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="],
|
"just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="],
|
||||||
@@ -2968,20 +3013,40 @@
|
|||||||
|
|
||||||
"lodash._reinterpolate": ["lodash._reinterpolate@3.0.0", "", {}, "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA=="],
|
"lodash._reinterpolate": ["lodash._reinterpolate@3.0.0", "", {}, "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA=="],
|
||||||
|
|
||||||
|
"lodash.assignin": ["lodash.assignin@4.2.0", "", {}, "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg=="],
|
||||||
|
|
||||||
|
"lodash.bind": ["lodash.bind@4.2.1", "", {}, "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA=="],
|
||||||
|
|
||||||
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||||
|
|
||||||
"lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="],
|
"lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="],
|
||||||
|
|
||||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||||
|
|
||||||
|
"lodash.filter": ["lodash.filter@4.6.0", "", {}, "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ=="],
|
||||||
|
|
||||||
|
"lodash.flatten": ["lodash.flatten@4.4.0", "", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="],
|
||||||
|
|
||||||
|
"lodash.foreach": ["lodash.foreach@4.5.0", "", {}, "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ=="],
|
||||||
|
|
||||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||||
|
|
||||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||||
|
|
||||||
|
"lodash.map": ["lodash.map@4.6.0", "", {}, "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q=="],
|
||||||
|
|
||||||
"lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="],
|
"lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="],
|
||||||
|
|
||||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||||
|
|
||||||
|
"lodash.pick": ["lodash.pick@4.4.0", "", {}, "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q=="],
|
||||||
|
|
||||||
|
"lodash.reduce": ["lodash.reduce@4.6.0", "", {}, "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw=="],
|
||||||
|
|
||||||
|
"lodash.reject": ["lodash.reject@4.6.0", "", {}, "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ=="],
|
||||||
|
|
||||||
|
"lodash.some": ["lodash.some@4.6.0", "", {}, "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ=="],
|
||||||
|
|
||||||
"lodash.template": ["lodash.template@4.5.0", "", { "dependencies": { "lodash._reinterpolate": "^3.0.0", "lodash.templatesettings": "^4.0.0" } }, "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A=="],
|
"lodash.template": ["lodash.template@4.5.0", "", { "dependencies": { "lodash._reinterpolate": "^3.0.0", "lodash.templatesettings": "^4.0.0" } }, "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A=="],
|
||||||
|
|
||||||
"lodash.templatesettings": ["lodash.templatesettings@4.2.0", "", { "dependencies": { "lodash._reinterpolate": "^3.0.0" } }, "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ=="],
|
"lodash.templatesettings": ["lodash.templatesettings@4.2.0", "", { "dependencies": { "lodash._reinterpolate": "^3.0.0" } }, "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ=="],
|
||||||
@@ -3306,6 +3371,8 @@
|
|||||||
|
|
||||||
"nypm": ["nypm@0.6.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^2.0.0", "tinyexec": "^0.3.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg=="],
|
"nypm": ["nypm@0.6.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^2.0.0", "tinyexec": "^0.3.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg=="],
|
||||||
|
|
||||||
|
"oauth-sign": ["oauth-sign@0.9.0", "", {}, "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||||
@@ -3434,6 +3501,8 @@
|
|||||||
|
|
||||||
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||||
|
|
||||||
|
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
|
||||||
|
|
||||||
"permessage-deflate": ["permessage-deflate@0.1.7", "", { "dependencies": { "safe-buffer": "*" } }, "sha512-EUNi/RIsyJ1P1u9QHFwMOUWMYetqlE22ZgGbad7YP856WF4BFF0B7DuNy6vEGsgNNud6c/SkdWzkne71hH8MjA=="],
|
"permessage-deflate": ["permessage-deflate@0.1.7", "", { "dependencies": { "safe-buffer": "*" } }, "sha512-EUNi/RIsyJ1P1u9QHFwMOUWMYetqlE22ZgGbad7YP856WF4BFF0B7DuNy6vEGsgNNud6c/SkdWzkne71hH8MjA=="],
|
||||||
|
|
||||||
"pg": ["pg@8.14.0", "", { "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.8.0", "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-nXbVpyoaXVmdqlKEzToFf37qzyeeh7mbiXsnoWvstSqohj88yaa/I/Rq/HEVn2QPSZEuLIJa/jSpRDyzjEx4FQ=="],
|
"pg": ["pg@8.14.0", "", { "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.8.0", "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-nXbVpyoaXVmdqlKEzToFf37qzyeeh7mbiXsnoWvstSqohj88yaa/I/Rq/HEVn2QPSZEuLIJa/jSpRDyzjEx4FQ=="],
|
||||||
@@ -3628,12 +3697,14 @@
|
|||||||
|
|
||||||
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
||||||
|
|
||||||
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
"qs": ["qs@6.5.3", "", {}, "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="],
|
||||||
|
|
||||||
"quansync": ["quansync@0.2.8", "", {}, "sha512-4+saucphJMazjt7iOM27mbFCk+D9dd/zmgMDCzRZ8MEoBfYp7lAvoN38et/phRQF6wOPMy/OROBGgoWeSKyluA=="],
|
"quansync": ["quansync@0.2.8", "", {}, "sha512-4+saucphJMazjt7iOM27mbFCk+D9dd/zmgMDCzRZ8MEoBfYp7lAvoN38et/phRQF6wOPMy/OROBGgoWeSKyluA=="],
|
||||||
|
|
||||||
"querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="],
|
"querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="],
|
||||||
|
|
||||||
|
"queue": ["queue@6.0.1", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||||
@@ -3756,6 +3827,8 @@
|
|||||||
|
|
||||||
"replace-in-file": ["replace-in-file@6.3.5", "", { "dependencies": { "chalk": "^4.1.2", "glob": "^7.2.0", "yargs": "^17.2.1" }, "bin": { "replace-in-file": "bin/cli.js" } }, "sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg=="],
|
"replace-in-file": ["replace-in-file@6.3.5", "", { "dependencies": { "chalk": "^4.1.2", "glob": "^7.2.0", "yargs": "^17.2.1" }, "bin": { "replace-in-file": "bin/cli.js" } }, "sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg=="],
|
||||||
|
|
||||||
|
"request": ["request@2.88.2", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } }, "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw=="],
|
||||||
|
|
||||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
@@ -3832,7 +3905,7 @@
|
|||||||
|
|
||||||
"secure-json-parse": ["secure-json-parse@3.0.2", "", {}, "sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w=="],
|
"secure-json-parse": ["secure-json-parse@3.0.2", "", {}, "sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||||
|
|
||||||
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||||
|
|
||||||
@@ -3862,7 +3935,7 @@
|
|||||||
|
|
||||||
"shallow-equal": ["shallow-equal@1.2.1", "", {}, "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="],
|
"shallow-equal": ["shallow-equal@1.2.1", "", {}, "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="],
|
"sharp": ["sharp@0.34.1", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.7.1" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.1", "@img/sharp-darwin-x64": "0.34.1", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.1", "@img/sharp-linux-arm64": "0.34.1", "@img/sharp-linux-s390x": "0.34.1", "@img/sharp-linux-x64": "0.34.1", "@img/sharp-linuxmusl-arm64": "0.34.1", "@img/sharp-linuxmusl-x64": "0.34.1", "@img/sharp-wasm32": "0.34.1", "@img/sharp-win32-ia32": "0.34.1", "@img/sharp-win32-x64": "0.34.1" } }, "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg=="],
|
||||||
|
|
||||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
@@ -3950,6 +4023,8 @@
|
|||||||
|
|
||||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||||
|
|
||||||
|
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
|
||||||
|
|
||||||
"ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="],
|
"ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="],
|
||||||
|
|
||||||
"sst": ["sst@3.11.21", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.11.21", "sst-darwin-x64": "3.11.21", "sst-linux-arm64": "3.11.21", "sst-linux-x64": "3.11.21", "sst-linux-x86": "3.11.21" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-q0zeVplSv4mvfWaruru83f2y9LcHQlpFjF7+KhsmSs5VheKMbIGExa35dHl00bYxOsGSbECx/W3S6fXMtC18rg=="],
|
"sst": ["sst@3.11.21", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.11.21", "sst-darwin-x64": "3.11.21", "sst-linux-arm64": "3.11.21", "sst-linux-x64": "3.11.21", "sst-linux-x86": "3.11.21" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-q0zeVplSv4mvfWaruru83f2y9LcHQlpFjF7+KhsmSs5VheKMbIGExa35dHl00bYxOsGSbECx/W3S6fXMtC18rg=="],
|
||||||
@@ -3978,6 +4053,10 @@
|
|||||||
|
|
||||||
"steam-session": ["steam-session@1.9.3", "", { "dependencies": { "@doctormckay/stdlib": "^2.9.0", "@doctormckay/user-agents": "^1.0.0", "debug": "^4.3.4", "kvparser": "^1.0.1", "node-bignumber": "^1.2.2", "protobufjs": "^7.1.0", "socks-proxy-agent": "^7.0.0", "steamid": "^2.0.0", "tiny-typed-emitter": "^2.1.0", "websocket13": "^4.0.0" } }, "sha512-leunJ8205CpEg/QW98S3LS+18qWHGb6wfe1oh2y4v/EChW0gq7Hd/buKb4R/TVh7VbFvb4taMUbwPvMkNaOPyg=="],
|
"steam-session": ["steam-session@1.9.3", "", { "dependencies": { "@doctormckay/stdlib": "^2.9.0", "@doctormckay/user-agents": "^1.0.0", "debug": "^4.3.4", "kvparser": "^1.0.1", "node-bignumber": "^1.2.2", "protobufjs": "^7.1.0", "socks-proxy-agent": "^7.0.0", "steamid": "^2.0.0", "tiny-typed-emitter": "^2.1.0", "websocket13": "^4.0.0" } }, "sha512-leunJ8205CpEg/QW98S3LS+18qWHGb6wfe1oh2y4v/EChW0gq7Hd/buKb4R/TVh7VbFvb4taMUbwPvMkNaOPyg=="],
|
||||||
|
|
||||||
|
"steam-totp": ["steam-totp@1.5.0", "", { "dependencies": { "@doctormckay/stats-reporter": "^1.0.0" } }, "sha512-RMlBK5dFtgplDMYYGg/k80RqEntzBcl7C/0RF18fQh9+XPe/iEMsfKmIE+xj8I3hqJW1akANAC6gf+YpfZq52w=="],
|
||||||
|
|
||||||
|
"steamcommunity": ["steamcommunity@3.48.6", "", { "dependencies": { "@doctormckay/user-agents": "^1.0.0", "async": "^2.6.3", "cheerio": "0.22.0", "image-size": "^0.8.2", "request": "^2.88.0", "steam-session": "^1.9.1", "steam-totp": "^1.5.0", "steamid": "^1.1.3", "xml2js": "^0.6.2" } }, "sha512-hl/WTUKpn0Rb5+OrHM2UvXG97EHLnEUqwENyD+aCuln7IsY3NridaPNQJxQHFPrlgI0vWVqw45ibIXhy3XPcjA=="],
|
||||||
|
|
||||||
"steamid": ["steamid@2.1.0", "", {}, "sha512-ndt1cvuuSC+i8fcxVsmeyRlgGsR1QsoAuIXz+eabj8/Y4GIWE2+mgHA7Hys61JDHOxttfWtXHtN2m5TNYTlORg=="],
|
"steamid": ["steamid@2.1.0", "", {}, "sha512-ndt1cvuuSC+i8fcxVsmeyRlgGsR1QsoAuIXz+eabj8/Y4GIWE2+mgHA7Hys61JDHOxttfWtXHtN2m5TNYTlORg=="],
|
||||||
|
|
||||||
"stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="],
|
"stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="],
|
||||||
@@ -4092,6 +4171,8 @@
|
|||||||
|
|
||||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||||
|
|
||||||
|
"tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="],
|
||||||
|
|
||||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
"treeverse": ["treeverse@3.0.0", "", {}, "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ=="],
|
"treeverse": ["treeverse@3.0.0", "", {}, "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ=="],
|
||||||
@@ -4118,6 +4199,8 @@
|
|||||||
|
|
||||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||||
|
|
||||||
|
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
|
||||||
|
|
||||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@4.37.0", "", {}, "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg=="],
|
"type-fest": ["type-fest@4.37.0", "", {}, "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg=="],
|
||||||
@@ -4252,6 +4335,8 @@
|
|||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
|
"verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="],
|
||||||
|
|
||||||
"vfile": ["vfile@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg=="],
|
"vfile": ["vfile@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg=="],
|
||||||
|
|
||||||
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||||
@@ -4824,12 +4909,8 @@
|
|||||||
|
|
||||||
"@mapbox/node-pre-gyp/consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
|
"@mapbox/node-pre-gyp/consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
|
||||||
|
|
||||||
"@mapbox/node-pre-gyp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
|
||||||
|
|
||||||
"@mapbox/node-pre-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
|
"@mapbox/node-pre-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
|
||||||
|
|
||||||
"@mapbox/node-pre-gyp/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@mapbox/node-pre-gyp/tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
"@mapbox/node-pre-gyp/tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/eventsource": ["eventsource@3.0.5", "", { "dependencies": { "eventsource-parser": "^3.0.0" } }, "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw=="],
|
"@modelcontextprotocol/sdk/eventsource": ["eventsource@3.0.5", "", { "dependencies": { "eventsource-parser": "^3.0.0" } }, "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw=="],
|
||||||
@@ -4870,24 +4951,14 @@
|
|||||||
|
|
||||||
"@npmcli/arborist/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@npmcli/arborist/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@npmcli/arborist/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@npmcli/fs/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@npmcli/git/ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="],
|
"@npmcli/git/ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="],
|
||||||
|
|
||||||
"@npmcli/git/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"@npmcli/git/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"@npmcli/git/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@npmcli/git/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"@npmcli/git/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||||
|
|
||||||
"@npmcli/map-workspaces/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@npmcli/map-workspaces/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@npmcli/metavuln-calculator/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@npmcli/package-json/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@npmcli/promise-spawn/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"@npmcli/promise-spawn/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||||
|
|
||||||
"@npmcli/run-script/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"@npmcli/run-script/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||||
@@ -4902,8 +4973,6 @@
|
|||||||
|
|
||||||
"@nuxt/cli/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
"@nuxt/cli/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||||
|
|
||||||
"@nuxt/cli/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/content/@vueuse/core": ["@vueuse/core@11.3.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "11.3.0", "@vueuse/shared": "11.3.0", "vue-demi": ">=0.14.10" } }, "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA=="],
|
"@nuxt/content/@vueuse/core": ["@vueuse/core@11.3.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "11.3.0", "@vueuse/shared": "11.3.0", "vue-demi": ">=0.14.10" } }, "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA=="],
|
||||||
|
|
||||||
"@nuxt/content/consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
|
"@nuxt/content/consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
|
||||||
@@ -4916,8 +4985,6 @@
|
|||||||
|
|
||||||
"@nuxt/devtools/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
"@nuxt/devtools/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
||||||
|
|
||||||
"@nuxt/devtools/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/devtools-kit/@nuxt/kit": ["@nuxt/kit@3.16.1", "", { "dependencies": { "c12": "^3.0.2", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.3", "errx": "^0.1.0", "exsolve": "^1.0.4", "globby": "^14.1.0", "ignore": "^7.0.3", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.1.0", "scule": "^1.3.0", "semver": "^7.7.1", "std-env": "^3.8.1", "ufo": "^1.5.4", "unctx": "^2.4.1", "unimport": "^4.1.2", "untyped": "^2.0.0" } }, "sha512-Perby8hJGUeCWad5oTVXb/Ibvp18ZCUC5PxHHu+acMDmVfnxSo48yqk7qNd09VkTF3LEzoEjNZpmW2ZWN0ry7A=="],
|
"@nuxt/devtools-kit/@nuxt/kit": ["@nuxt/kit@3.16.1", "", { "dependencies": { "c12": "^3.0.2", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.3", "errx": "^0.1.0", "exsolve": "^1.0.4", "globby": "^14.1.0", "ignore": "^7.0.3", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.1.0", "scule": "^1.3.0", "semver": "^7.7.1", "std-env": "^3.8.1", "ufo": "^1.5.4", "unctx": "^2.4.1", "unimport": "^4.1.2", "untyped": "^2.0.0" } }, "sha512-Perby8hJGUeCWad5oTVXb/Ibvp18ZCUC5PxHHu+acMDmVfnxSo48yqk7qNd09VkTF3LEzoEjNZpmW2ZWN0ry7A=="],
|
||||||
|
|
||||||
"@nuxt/devtools-kit/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
"@nuxt/devtools-kit/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
||||||
@@ -4926,8 +4993,6 @@
|
|||||||
|
|
||||||
"@nuxt/devtools-wizard/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
"@nuxt/devtools-wizard/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
||||||
|
|
||||||
"@nuxt/devtools-wizard/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/eslint-config/@eslint/js": ["@eslint/js@9.22.0", "", {}, "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ=="],
|
"@nuxt/eslint-config/@eslint/js": ["@eslint/js@9.22.0", "", {}, "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ=="],
|
||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.26.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.26.0", "@typescript-eslint/type-utils": "8.26.0", "@typescript-eslint/utils": "8.26.0", "@typescript-eslint/visitor-keys": "8.26.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q=="],
|
"@nuxt/eslint-config/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.26.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.26.0", "@typescript-eslint/type-utils": "8.26.0", "@typescript-eslint/utils": "8.26.0", "@typescript-eslint/visitor-keys": "8.26.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q=="],
|
||||||
@@ -4960,8 +5025,6 @@
|
|||||||
|
|
||||||
"@nuxt/kit/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
"@nuxt/kit/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||||
|
|
||||||
"@nuxt/kit/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/telemetry/@nuxt/kit": ["@nuxt/kit@3.16.1", "", { "dependencies": { "c12": "^3.0.2", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.3", "errx": "^0.1.0", "exsolve": "^1.0.4", "globby": "^14.1.0", "ignore": "^7.0.3", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.1.0", "scule": "^1.3.0", "semver": "^7.7.1", "std-env": "^3.8.1", "ufo": "^1.5.4", "unctx": "^2.4.1", "unimport": "^4.1.2", "untyped": "^2.0.0" } }, "sha512-Perby8hJGUeCWad5oTVXb/Ibvp18ZCUC5PxHHu+acMDmVfnxSo48yqk7qNd09VkTF3LEzoEjNZpmW2ZWN0ry7A=="],
|
"@nuxt/telemetry/@nuxt/kit": ["@nuxt/kit@3.16.1", "", { "dependencies": { "c12": "^3.0.2", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.3", "errx": "^0.1.0", "exsolve": "^1.0.4", "globby": "^14.1.0", "ignore": "^7.0.3", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.1.0", "scule": "^1.3.0", "semver": "^7.7.1", "std-env": "^3.8.1", "ufo": "^1.5.4", "unctx": "^2.4.1", "unimport": "^4.1.2", "untyped": "^2.0.0" } }, "sha512-Perby8hJGUeCWad5oTVXb/Ibvp18ZCUC5PxHHu+acMDmVfnxSo48yqk7qNd09VkTF3LEzoEjNZpmW2ZWN0ry7A=="],
|
||||||
|
|
||||||
"@nuxt/telemetry/git-url-parse": ["git-url-parse@16.0.1", "", { "dependencies": { "git-up": "^8.0.0" } }, "sha512-mcD36GrhAzX5JVOsIO52qNpgRyFzYWRbU1VSRFCvJt1IJvqfvH427wWw/CFqkWvjVPtdG5VTx4MKUeC5GeFPDQ=="],
|
"@nuxt/telemetry/git-url-parse": ["git-url-parse@16.0.1", "", { "dependencies": { "git-up": "^8.0.0" } }, "sha512-mcD36GrhAzX5JVOsIO52qNpgRyFzYWRbU1VSRFCvJt1IJvqfvH427wWw/CFqkWvjVPtdG5VTx4MKUeC5GeFPDQ=="],
|
||||||
@@ -4992,8 +5055,6 @@
|
|||||||
|
|
||||||
"@nuxtjs/color-mode/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"@nuxtjs/color-mode/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
"@nuxtjs/color-mode/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxtjs/mdc/consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
|
"@nuxtjs/mdc/consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
|
||||||
|
|
||||||
"@nuxtjs/mdc/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="],
|
"@nuxtjs/mdc/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="],
|
||||||
@@ -5066,8 +5127,6 @@
|
|||||||
|
|
||||||
"@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.55.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg=="],
|
"@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.55.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg=="],
|
||||||
|
|
||||||
"@opentelemetry/instrumentation/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@opentelemetry/instrumentation-amqplib/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
|
"@opentelemetry/instrumentation-amqplib/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
|
||||||
|
|
||||||
"@opentelemetry/instrumentation-amqplib/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="],
|
"@opentelemetry/instrumentation-amqplib/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="],
|
||||||
@@ -5298,8 +5357,6 @@
|
|||||||
|
|
||||||
"@opentelemetry/sdk-node/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.33.0", "", {}, "sha512-TIpZvE8fiEILFfTlfPnltpBaD3d9/+uQHVCyC3vfdh6WfCXKhNFzoP5RyDDIndfvZC5GrA4pyEDNyjPloJud+w=="],
|
"@opentelemetry/sdk-node/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.33.0", "", {}, "sha512-TIpZvE8fiEILFfTlfPnltpBaD3d9/+uQHVCyC3vfdh6WfCXKhNFzoP5RyDDIndfvZC5GrA4pyEDNyjPloJud+w=="],
|
||||||
|
|
||||||
"@opentelemetry/sdk-trace-node/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
|
"@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
|
||||||
|
|
||||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||||
@@ -5314,8 +5371,6 @@
|
|||||||
|
|
||||||
"@poppinss/dumper/supports-color": ["supports-color@10.0.0", "", {}, "sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ=="],
|
"@poppinss/dumper/supports-color": ["supports-color@10.0.0", "", {}, "sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ=="],
|
||||||
|
|
||||||
"@pulumi/pulumi/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@qwik-ui/headless/focus-trap": ["focus-trap@7.5.4", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w=="],
|
"@qwik-ui/headless/focus-trap": ["focus-trap@7.5.4", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w=="],
|
||||||
|
|
||||||
"@rocicorp/zero/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
|
"@rocicorp/zero/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
|
||||||
@@ -5330,8 +5385,6 @@
|
|||||||
|
|
||||||
"@rocicorp/zero/nanoid": ["nanoid@5.1.3", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ=="],
|
"@rocicorp/zero/nanoid": ["nanoid@5.1.3", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ=="],
|
||||||
|
|
||||||
"@rocicorp/zero/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"@rollup/plugin-commonjs/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
"@rollup/plugin-commonjs/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||||
@@ -5420,8 +5473,6 @@
|
|||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
"@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||||
@@ -5482,6 +5533,8 @@
|
|||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"archiver/async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
"aws-crt/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
"aws-crt/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
|
|
||||||
"aws-crt/mqtt": ["mqtt@4.3.8", "", { "dependencies": { "commist": "^1.0.0", "concat-stream": "^2.0.0", "debug": "^4.1.1", "duplexify": "^4.1.1", "help-me": "^3.0.0", "inherits": "^2.0.3", "lru-cache": "^6.0.0", "minimist": "^1.2.5", "mqtt-packet": "^6.8.0", "number-allocator": "^1.0.9", "pump": "^3.0.0", "readable-stream": "^3.6.0", "reinterval": "^1.1.0", "rfdc": "^1.3.0", "split2": "^3.1.0", "ws": "^7.5.5", "xtend": "^4.0.2" }, "bin": { "mqtt": "bin/mqtt.js", "mqtt_pub": "bin/pub.js", "mqtt_sub": "bin/sub.js" } }, "sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw=="],
|
"aws-crt/mqtt": ["mqtt@4.3.8", "", { "dependencies": { "commist": "^1.0.0", "concat-stream": "^2.0.0", "debug": "^4.1.1", "duplexify": "^4.1.1", "help-me": "^3.0.0", "inherits": "^2.0.3", "lru-cache": "^6.0.0", "minimist": "^1.2.5", "mqtt-packet": "^6.8.0", "number-allocator": "^1.0.9", "pump": "^3.0.0", "readable-stream": "^3.6.0", "reinterval": "^1.1.0", "rfdc": "^1.3.0", "split2": "^3.1.0", "ws": "^7.5.5", "xtend": "^4.0.2" }, "bin": { "mqtt": "bin/mqtt.js", "mqtt_pub": "bin/pub.js", "mqtt_sub": "bin/sub.js" } }, "sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw=="],
|
||||||
@@ -5516,6 +5569,10 @@
|
|||||||
|
|
||||||
"chalk-template/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
"chalk-template/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||||
|
|
||||||
|
"cheerio/entities": ["entities@1.1.2", "", {}, "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="],
|
||||||
|
|
||||||
|
"cheerio/htmlparser2": ["htmlparser2@3.10.1", "", { "dependencies": { "domelementtype": "^1.3.1", "domhandler": "^2.3.0", "domutils": "^1.5.1", "entities": "^1.1.1", "inherits": "^2.0.1", "readable-stream": "^3.1.1" } }, "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ=="],
|
||||||
|
|
||||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"citty/consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
|
"citty/consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
|
||||||
@@ -5534,14 +5591,28 @@
|
|||||||
|
|
||||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"css-select/css-what": ["css-what@2.1.3", "", {}, "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg=="],
|
||||||
|
|
||||||
|
"css-select/domutils": ["domutils@1.5.1", "", { "dependencies": { "dom-serializer": "0", "domelementtype": "1" } }, "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw=="],
|
||||||
|
|
||||||
|
"css-select/nth-check": ["nth-check@1.0.2", "", { "dependencies": { "boolbase": "~1.0.0" } }, "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg=="],
|
||||||
|
|
||||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||||
|
|
||||||
"dir-glob/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
|
"dir-glob/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
|
||||||
|
|
||||||
"docus-starter/eslint": ["eslint@9.22.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/config-helpers": "^0.1.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.22.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ=="],
|
"docus-starter/eslint": ["eslint@9.22.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/config-helpers": "^0.1.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.22.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ=="],
|
||||||
|
|
||||||
|
"dom-serializer/domelementtype": ["domelementtype@1.3.1", "", {}, "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="],
|
||||||
|
|
||||||
|
"dom-serializer/entities": ["entities@1.1.2", "", {}, "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="],
|
||||||
|
|
||||||
|
"domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||||
|
|
||||||
"duplexify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"duplexify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
|
"ecc-jsbn/jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="],
|
||||||
|
|
||||||
"effect/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
"effect/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||||
|
|
||||||
"engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
"engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||||
@@ -5570,20 +5641,12 @@
|
|||||||
|
|
||||||
"eslint-plugin-import-x/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"eslint-plugin-import-x/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"eslint-plugin-import-x/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsdoc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
"eslint-plugin-jsdoc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
||||||
|
|
||||||
"eslint-plugin-jsdoc/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-qwik/@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw=="],
|
"eslint-plugin-qwik/@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw=="],
|
||||||
|
|
||||||
"eslint-plugin-unicorn/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
"eslint-plugin-unicorn/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
||||||
|
|
||||||
"eslint-plugin-unicorn/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-vue/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"espree/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
"espree/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||||
|
|
||||||
"eval/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
"eval/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
||||||
@@ -5594,6 +5657,8 @@
|
|||||||
|
|
||||||
"express/mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="],
|
"express/mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="],
|
||||||
|
|
||||||
|
"express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||||
|
|
||||||
"express/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"express/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"express/send": ["send@1.1.0", "", { "dependencies": { "debug": "^4.3.5", "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^0.5.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA=="],
|
"express/send": ["send@1.1.0", "", { "dependencies": { "debug": "^4.3.5", "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^0.5.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA=="],
|
||||||
@@ -5604,16 +5669,12 @@
|
|||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"fastify/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"form-data/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"form-data/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||||
|
|
||||||
"gel/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"gel/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"gel/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||||
|
|
||||||
"get-source/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"get-source/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
@@ -5628,6 +5689,8 @@
|
|||||||
|
|
||||||
"h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
|
"h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
|
||||||
|
|
||||||
|
"har-validator/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
"hast-util-from-parse5/vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"hast-util-from-parse5/vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
"hast-util-raw/vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"hast-util-raw/vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
@@ -5714,8 +5777,6 @@
|
|||||||
|
|
||||||
"mkdist/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"mkdist/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
"mkdist/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"mlly/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
"mlly/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||||
|
|
||||||
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
@@ -5736,30 +5797,14 @@
|
|||||||
|
|
||||||
"nitropack/mime": ["mime@4.0.6", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A=="],
|
"nitropack/mime": ["mime@4.0.6", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A=="],
|
||||||
|
|
||||||
"nitropack/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"nitropack/unenv": ["unenv@2.0.0-rc.15", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA=="],
|
"nitropack/unenv": ["unenv@2.0.0-rc.15", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA=="],
|
||||||
|
|
||||||
"nitropack/youch": ["youch@4.1.0-beta.6", "", { "dependencies": { "@poppinss/dumper": "^0.6.3", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.1" } }, "sha512-y1aNsEeoLXnWb6pI9TvfNPIxySyo4Un3OGxKn7rsNj8+tgSquzXEWkzfA5y6gU0fvzmQgvx3JBn/p51qQ8Xg9A=="],
|
"nitropack/youch": ["youch@4.1.0-beta.6", "", { "dependencies": { "@poppinss/dumper": "^0.6.3", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.1" } }, "sha512-y1aNsEeoLXnWb6pI9TvfNPIxySyo4Un3OGxKn7rsNj8+tgSquzXEWkzfA5y6gU0fvzmQgvx3JBn/p51qQ8Xg9A=="],
|
||||||
|
|
||||||
"node-abi/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
"node-gyp/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"node-gyp/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"node-gyp/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||||
|
|
||||||
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
|
||||||
|
|
||||||
"normalize-package-data/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"npm-install-checks/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"npm-package-arg/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"npm-pick-manifest/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"nuxt/@nuxt/kit": ["@nuxt/kit@3.16.1", "", { "dependencies": { "c12": "^3.0.2", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.3", "errx": "^0.1.0", "exsolve": "^1.0.4", "globby": "^14.1.0", "ignore": "^7.0.3", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.1.0", "scule": "^1.3.0", "semver": "^7.7.1", "std-env": "^3.8.1", "ufo": "^1.5.4", "unctx": "^2.4.1", "unimport": "^4.1.2", "untyped": "^2.0.0" } }, "sha512-Perby8hJGUeCWad5oTVXb/Ibvp18ZCUC5PxHHu+acMDmVfnxSo48yqk7qNd09VkTF3LEzoEjNZpmW2ZWN0ry7A=="],
|
"nuxt/@nuxt/kit": ["@nuxt/kit@3.16.1", "", { "dependencies": { "c12": "^3.0.2", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.3", "errx": "^0.1.0", "exsolve": "^1.0.4", "globby": "^14.1.0", "ignore": "^7.0.3", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.1.0", "scule": "^1.3.0", "semver": "^7.7.1", "std-env": "^3.8.1", "ufo": "^1.5.4", "unctx": "^2.4.1", "unimport": "^4.1.2", "untyped": "^2.0.0" } }, "sha512-Perby8hJGUeCWad5oTVXb/Ibvp18ZCUC5PxHHu+acMDmVfnxSo48yqk7qNd09VkTF3LEzoEjNZpmW2ZWN0ry7A=="],
|
||||||
|
|
||||||
"nuxt/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"nuxt/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
@@ -5770,8 +5815,6 @@
|
|||||||
|
|
||||||
"nuxt/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
"nuxt/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||||
|
|
||||||
"nuxt/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"nuxt-config-schema/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
"nuxt-config-schema/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||||
|
|
||||||
"nuxt-config-schema/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
"nuxt-config-schema/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||||
@@ -5816,8 +5859,6 @@
|
|||||||
|
|
||||||
"pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="],
|
"pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="],
|
||||||
|
|
||||||
"portfinder/async": ["async@2.6.4", "", { "dependencies": { "lodash": "^4.17.14" } }, "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA=="],
|
|
||||||
|
|
||||||
"portfinder/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"portfinder/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
"portfinder/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
"portfinder/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||||
@@ -5826,8 +5867,6 @@
|
|||||||
|
|
||||||
"postcss-nesting/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
"postcss-nesting/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
||||||
|
|
||||||
"prebuild-install/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
|
||||||
|
|
||||||
"promise-retry/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
"promise-retry/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||||
|
|
||||||
"protobufjs/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
"protobufjs/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
||||||
@@ -5872,6 +5911,12 @@
|
|||||||
|
|
||||||
"replace-in-file/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
"replace-in-file/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
|
"request/form-data": ["form-data@2.3.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ=="],
|
||||||
|
|
||||||
|
"request/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"request/uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="],
|
||||||
|
|
||||||
"resolve-path/http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="],
|
"resolve-path/http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="],
|
||||||
|
|
||||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
@@ -5922,8 +5967,12 @@
|
|||||||
|
|
||||||
"spdx-correct/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="],
|
"spdx-correct/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="],
|
||||||
|
|
||||||
|
"sshpk/jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="],
|
||||||
|
|
||||||
"sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="],
|
"sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="],
|
||||||
|
|
||||||
|
"steamcommunity/steamid": ["steamid@1.1.3", "", { "dependencies": { "cuint": "^0.2.1" } }, "sha512-t86YjtP1LtPt8D+TaIARm6PtC9tBnF1FhxQeLFs6ohG7vDUfQuy/M8II14rx1TTUkVuYoWHP/7DlvTtoCGULcw=="],
|
||||||
|
|
||||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||||
@@ -5938,6 +5987,8 @@
|
|||||||
|
|
||||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||||
|
|
||||||
|
"svgo/css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
|
||||||
|
|
||||||
"tailwind-config-viewer/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="],
|
"tailwind-config-viewer/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="],
|
||||||
|
|
||||||
"tailwind-config-viewer/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
"tailwind-config-viewer/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
||||||
@@ -5956,6 +6007,8 @@
|
|||||||
|
|
||||||
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||||
|
|
||||||
|
"tough-cookie/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"ts-node/arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
|
"ts-node/arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
|
||||||
|
|
||||||
"tsx/esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="],
|
"tsx/esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="],
|
||||||
@@ -6030,6 +6083,8 @@
|
|||||||
|
|
||||||
"validate-npm-package-license/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="],
|
"validate-npm-package-license/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="],
|
||||||
|
|
||||||
|
"verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||||
|
|
||||||
"vfile-location/vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"vfile-location/vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
"vite/esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="],
|
"vite/esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="],
|
||||||
@@ -6052,8 +6107,6 @@
|
|||||||
|
|
||||||
"vite-plugin-checker/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
"vite-plugin-checker/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||||
|
|
||||||
"vue-eslint-parser/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
"wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="],
|
"wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="],
|
||||||
@@ -6454,8 +6507,6 @@
|
|||||||
|
|
||||||
"@nuxt/devtools-kit/@nuxt/kit/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
"@nuxt/devtools-kit/@nuxt/kit/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||||
|
|
||||||
"@nuxt/devtools-kit/@nuxt/kit/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/devtools-kit/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
"@nuxt/devtools-kit/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
||||||
|
|
||||||
"@nuxt/devtools-kit/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
|
"@nuxt/devtools-kit/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
|
||||||
@@ -6534,14 +6585,10 @@
|
|||||||
|
|
||||||
"@nuxt/telemetry/@nuxt/kit/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
"@nuxt/telemetry/@nuxt/kit/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||||
|
|
||||||
"@nuxt/telemetry/@nuxt/kit/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/telemetry/git-url-parse/git-up": ["git-up@8.0.1", "", { "dependencies": { "is-ssh": "^1.4.0", "parse-url": "^9.2.0" } }, "sha512-2XFu1uNZMSjkyetaF+8rqn6P0XqpMq/C+2ycjI6YwrIKcszZ5/WR4UubxjN0lILOKqLkLaHDaCr2B6fP1cke6g=="],
|
"@nuxt/telemetry/git-url-parse/git-up": ["git-up@8.0.1", "", { "dependencies": { "is-ssh": "^1.4.0", "parse-url": "^9.2.0" } }, "sha512-2XFu1uNZMSjkyetaF+8rqn6P0XqpMq/C+2ycjI6YwrIKcszZ5/WR4UubxjN0lILOKqLkLaHDaCr2B6fP1cke6g=="],
|
||||||
|
|
||||||
"@nuxt/ui/@vueuse/integrations/@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="],
|
"@nuxt/ui/@vueuse/integrations/@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="],
|
||||||
|
|
||||||
"@nuxt/vite-builder/@nuxt/kit/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/vite-builder/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
|
"@nuxt/vite-builder/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
|
||||||
|
|
||||||
"@nuxt/vite-builder/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
|
"@nuxt/vite-builder/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
|
||||||
@@ -6796,6 +6843,14 @@
|
|||||||
|
|
||||||
"c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"cheerio/htmlparser2/domelementtype": ["domelementtype@1.3.1", "", {}, "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="],
|
||||||
|
|
||||||
|
"cheerio/htmlparser2/domhandler": ["domhandler@2.4.2", "", { "dependencies": { "domelementtype": "1" } }, "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA=="],
|
||||||
|
|
||||||
|
"cheerio/htmlparser2/domutils": ["domutils@1.7.0", "", { "dependencies": { "dom-serializer": "0", "domelementtype": "1" } }, "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg=="],
|
||||||
|
|
||||||
|
"cheerio/htmlparser2/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"clipboardy/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
"clipboardy/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
||||||
|
|
||||||
"clipboardy/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
|
"clipboardy/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
|
||||||
@@ -6812,6 +6867,8 @@
|
|||||||
|
|
||||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"css-select/domutils/domelementtype": ["domelementtype@1.3.1", "", {}, "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="],
|
||||||
|
|
||||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||||
|
|
||||||
"docus-starter/eslint/@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="],
|
"docus-starter/eslint/@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="],
|
||||||
@@ -6886,6 +6943,8 @@
|
|||||||
|
|
||||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
|
||||||
|
"har-validator/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"http-assert/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
"http-assert/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||||
|
|
||||||
"http-assert/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
|
"http-assert/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
|
||||||
@@ -7294,10 +7353,6 @@
|
|||||||
|
|
||||||
"vite-imagetools/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
"vite-imagetools/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||||
|
|
||||||
"vite-imagetools/sharp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
|
||||||
|
|
||||||
"vite-imagetools/sharp/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"vite-node/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
"vite-node/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||||
|
|
||||||
"vite-node/vite/rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],
|
"vite-node/vite/rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],
|
||||||
@@ -7436,10 +7491,6 @@
|
|||||||
|
|
||||||
"wrangler/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
"wrangler/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||||
|
|
||||||
"wrangler/sharp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
|
||||||
|
|
||||||
"wrangler/sharp/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
"yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||||
|
|
||||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
@@ -7664,8 +7715,6 @@
|
|||||||
|
|
||||||
"@nestri/web/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@nestri/web/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@nestri/web/@typescript-eslint/parser/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nestri/web/@typescript-eslint/parser/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
|
"@nestri/web/@typescript-eslint/parser/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
|
||||||
|
|
||||||
"@nestri/web/eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"@nestri/web/eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
@@ -7698,8 +7747,6 @@
|
|||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@nuxt/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
|
"@nuxt/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
|
||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/parser/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
"@nuxt/eslint-config/@typescript-eslint/parser/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||||
@@ -7714,8 +7761,6 @@
|
|||||||
|
|
||||||
"@nuxt/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@nuxt/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@nuxt/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
|
"@nuxt/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
|
||||||
|
|
||||||
"@nuxt/icon/@nuxt/devtools-kit/@nuxt/schema/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"@nuxt/icon/@nuxt/devtools-kit/@nuxt/schema/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
@@ -7750,8 +7795,6 @@
|
|||||||
|
|
||||||
"@stylistic/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@stylistic/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@stylistic/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@stylistic/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
|
"@stylistic/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
|
||||||
|
|
||||||
"@types/bun/bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
"@types/bun/bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||||
@@ -7932,8 +7975,6 @@
|
|||||||
|
|
||||||
"eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],
|
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],
|
||||||
|
|
||||||
"read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
"read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||||
@@ -8064,14 +8105,10 @@
|
|||||||
|
|
||||||
"@nestri/web/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@nestri/web/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@nestri/web/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nestri/web/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
|
"@nestri/web/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
|
||||||
|
|
||||||
"@nestri/web/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@nestri/web/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@nestri/web/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nestri/web/@typescript-eslint/parser/@typescript-eslint/typescript-estree/globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"@nestri/web/@typescript-eslint/parser/@typescript-eslint/typescript-estree/globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
"@nestri/web/@typescript-eslint/parser/@typescript-eslint/typescript-estree/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
"@nestri/web/@typescript-eslint/parser/@typescript-eslint/typescript-estree/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||||
@@ -8082,12 +8119,8 @@
|
|||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@nuxt/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@nuxt/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
|
||||||
|
|
||||||
"@nuxt/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
"@nuxt/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
|
||||||
"@nuxt/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
"@nuxt/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||||
|
|||||||
@@ -10,21 +10,19 @@ WORKDIR /relay
|
|||||||
# TODO: Switch running layer to just alpine (doesn't need golang dev stack)
|
# TODO: Switch running layer to just alpine (doesn't need golang dev stack)
|
||||||
|
|
||||||
# ENV flags
|
# ENV flags
|
||||||
|
ENV REGEN_IDENTITY=false
|
||||||
ENV VERBOSE=false
|
ENV VERBOSE=false
|
||||||
ENV DEBUG=false
|
ENV DEBUG=false
|
||||||
ENV ENDPOINT_PORT=8088
|
ENV ENDPOINT_PORT=8088
|
||||||
ENV MESH_PORT=8089
|
ENV WEBRTC_UDP_START=0
|
||||||
ENV WEBRTC_UDP_START=10000
|
ENV WEBRTC_UDP_END=0
|
||||||
ENV WEBRTC_UDP_END=20000
|
|
||||||
ENV STUN_SERVER="stun.l.google.com:19302"
|
ENV STUN_SERVER="stun.l.google.com:19302"
|
||||||
ENV WEBRTC_UDP_MUX=8088
|
ENV WEBRTC_UDP_MUX=8088
|
||||||
ENV WEBRTC_NAT_IPS=""
|
ENV WEBRTC_NAT_IPS=""
|
||||||
ENV AUTO_ADD_LOCAL_IP=true
|
ENV AUTO_ADD_LOCAL_IP=true
|
||||||
ENV TLS_CERT=""
|
ENV PERSIST_DIR="./persist-data"
|
||||||
ENV TLS_KEY=""
|
|
||||||
|
|
||||||
EXPOSE $ENDPOINT_PORT
|
EXPOSE $ENDPOINT_PORT
|
||||||
EXPOSE $MESH_PORT
|
|
||||||
EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp
|
EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp
|
||||||
EXPOSE $WEBRTC_UDP_MUX/udp
|
EXPOSE $WEBRTC_UDP_MUX/udp
|
||||||
|
|
||||||
|
|||||||
152
infra/api.ts
152
infra/api.ts
@@ -1,16 +1,25 @@
|
|||||||
import { bus } from "./bus";
|
import { bus } from "./bus";
|
||||||
|
import { vpc } from "./vpc";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { domain } from "./dns";
|
import { domain } from "./dns";
|
||||||
import { secret } from "./secret";
|
import { secret } from "./secret";
|
||||||
import { cluster } from "./cluster";
|
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
|
|
||||||
export const apiService = new sst.aws.Service("Api", {
|
const urls = new sst.Linkable("Urls", {
|
||||||
cluster,
|
properties: {
|
||||||
cpu: $app.stage === "production" ? "2 vCPU" : undefined,
|
api: `https://api.${domain}`,
|
||||||
memory: $app.stage === "production" ? "4 GB" : undefined,
|
auth: `https://auth.${domain}`,
|
||||||
|
site: $dev ? "http://localhost:3000" : `https://console.${domain}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiFn = new sst.aws.Function("ApiFn", {
|
||||||
|
vpc,
|
||||||
|
handler: "packages/functions/src/api/index.handler",
|
||||||
|
streaming: !$dev,
|
||||||
link: [
|
link: [
|
||||||
bus,
|
bus,
|
||||||
|
urls,
|
||||||
auth,
|
auth,
|
||||||
postgres,
|
postgres,
|
||||||
secret.SteamApiKey,
|
secret.SteamApiKey,
|
||||||
@@ -22,73 +31,90 @@ export const apiService = new sst.aws.Service("Api", {
|
|||||||
secret.NestriProMonthly,
|
secret.NestriProMonthly,
|
||||||
secret.NestriProYearly,
|
secret.NestriProYearly,
|
||||||
],
|
],
|
||||||
command: ["bun", "run", "./src/api/index.ts"],
|
url: true,
|
||||||
image: {
|
});
|
||||||
dockerfile: "packages/functions/Containerfile",
|
|
||||||
},
|
const provider = new aws.Provider("UsEast1", { region: "us-east-1" });
|
||||||
loadBalancer: {
|
|
||||||
|
const webAcl = new aws.wafv2.WebAcl(
|
||||||
|
"ApiWaf",
|
||||||
|
{
|
||||||
|
scope: "CLOUDFRONT",
|
||||||
|
defaultAction: {
|
||||||
|
allow: {},
|
||||||
|
},
|
||||||
|
visibilityConfig: {
|
||||||
|
cloudwatchMetricsEnabled: true,
|
||||||
|
metricName: "api-rate-limit-metric",
|
||||||
|
sampledRequestsEnabled: true,
|
||||||
|
},
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
listen: "80/http",
|
name: "rate-limit-rule",
|
||||||
forward: "3001/http",
|
priority: 1,
|
||||||
|
action: {
|
||||||
|
block: {
|
||||||
|
customResponse: {
|
||||||
|
responseCode: 429,
|
||||||
|
customResponseBodyKey: "rate-limit-response",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statement: {
|
||||||
|
rateBasedStatement: {
|
||||||
|
limit: 2 * 60, // 2 rps per authorization header
|
||||||
|
evaluationWindowSec: 60,
|
||||||
|
aggregateKeyType: "CUSTOM_KEYS",
|
||||||
|
customKeys: [
|
||||||
|
{
|
||||||
|
header: {
|
||||||
|
name: "Authorization",
|
||||||
|
textTransformations: [{ priority: 0, type: "NONE" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visibilityConfig: {
|
||||||
|
cloudwatchMetricsEnabled: true,
|
||||||
|
metricName: "rate-limit-rule-metric",
|
||||||
|
sampledRequestsEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customResponseBodies: [
|
||||||
|
{
|
||||||
|
key: "rate-limit-response",
|
||||||
|
content: JSON.stringify({
|
||||||
|
type: "rate_limit",
|
||||||
|
code: "too_many_requests",
|
||||||
|
message: "Rate limit exceeded. Please try again later.",
|
||||||
|
}),
|
||||||
|
contentType: "APPLICATION_JSON",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
dev: {
|
{ provider },
|
||||||
url: "http://localhost:3001",
|
);
|
||||||
command: "bun dev:api",
|
|
||||||
directory: "packages/functions",
|
|
||||||
},
|
|
||||||
scaling:
|
|
||||||
$app.stage === "production"
|
|
||||||
? {
|
|
||||||
min: 2,
|
|
||||||
max: 10,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
// For persisting actor state
|
|
||||||
transform: {
|
|
||||||
taskDefinition: (args) => {
|
|
||||||
const volumes = $output(args.volumes).apply(v => {
|
|
||||||
const next = [...(v || []), {
|
|
||||||
name: "shared-tmp",
|
|
||||||
dockerVolumeConfiguration: {
|
|
||||||
scope: "shared",
|
|
||||||
driver: "local"
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
return next;
|
export const api = new sst.aws.Router("Api", {
|
||||||
})
|
|
||||||
|
|
||||||
// "containerDefinitions" is a JSON string, parse first
|
|
||||||
let containers = $jsonParse(args.containerDefinitions);
|
|
||||||
|
|
||||||
containers = containers.apply((containerDefinitions) => {
|
|
||||||
containerDefinitions[0].mountPoints = [
|
|
||||||
...(containerDefinitions[0].mountPoints ?? []),
|
|
||||||
{
|
|
||||||
sourceVolume: "shared-tmp",
|
|
||||||
containerPath: "/tmp"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
return containerDefinitions;
|
|
||||||
});
|
|
||||||
|
|
||||||
args.volumes = volumes
|
|
||||||
args.containerDefinitions = $jsonStringify(containers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const api = !$dev ? new sst.aws.Router("ApiRoute", {
|
|
||||||
routes: {
|
routes: {
|
||||||
// I think api.url should work all the same
|
"/*": apiFn.url,
|
||||||
"/*": apiService.nodes.loadBalancer.dnsName,
|
|
||||||
},
|
},
|
||||||
domain: {
|
domain: {
|
||||||
name: "api." + domain,
|
name: "api." + domain,
|
||||||
dns: sst.cloudflare.dns(),
|
dns: sst.cloudflare.dns(),
|
||||||
},
|
},
|
||||||
}) : apiService
|
transform: {
|
||||||
|
cdn(args) {
|
||||||
|
if (!args.transform) {
|
||||||
|
args.transform = {
|
||||||
|
distribution: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
args.transform!.distribution = {
|
||||||
|
webAclId: webAcl.arn,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
104
infra/auth.ts
104
infra/auth.ts
@@ -1,98 +1,32 @@
|
|||||||
import { bus } from "./bus";
|
import { bus } from "./bus";
|
||||||
|
import { vpc } from "./vpc";
|
||||||
import { domain } from "./dns";
|
import { domain } from "./dns";
|
||||||
import { secret } from "./secret";
|
import { secret } from "./secret";
|
||||||
import { cluster } from "./cluster";
|
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
|
|
||||||
export const authService = new sst.aws.Service("Auth", {
|
export const auth = new sst.aws.Auth("Auth", {
|
||||||
cluster,
|
authorizer: {
|
||||||
cpu: $app.stage === "production" ? "1 vCPU" : undefined,
|
vpc,
|
||||||
memory: $app.stage === "production" ? "2 GB" : undefined,
|
link: [
|
||||||
command: ["bun", "run", "./src/auth/index.ts"],
|
bus,
|
||||||
link: [
|
postgres,
|
||||||
bus,
|
secret.PolarSecret,
|
||||||
postgres,
|
secret.GithubClientID,
|
||||||
secret.PolarSecret,
|
secret.DiscordClientID,
|
||||||
secret.GithubClientID,
|
secret.GithubClientSecret,
|
||||||
secret.DiscordClientID,
|
secret.DiscordClientSecret,
|
||||||
secret.GithubClientSecret,
|
],
|
||||||
secret.DiscordClientSecret,
|
permissions: [
|
||||||
],
|
|
||||||
image: {
|
|
||||||
dockerfile: "packages/functions/Containerfile",
|
|
||||||
},
|
|
||||||
environment: {
|
|
||||||
NO_COLOR: "1",
|
|
||||||
STORAGE: "/tmp/persist.json"
|
|
||||||
},
|
|
||||||
loadBalancer: {
|
|
||||||
rules: [
|
|
||||||
{
|
{
|
||||||
listen: "80/http",
|
actions: ["ses:SendEmail"],
|
||||||
forward: "3002/http",
|
resources: ["*"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
handler: "packages/functions/src/auth/index.handler",
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
actions: ["ses:SendEmail"],
|
|
||||||
resources: ["*"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dev: {
|
|
||||||
command: "bun dev:auth",
|
|
||||||
directory: "packages/functions",
|
|
||||||
url: "http://localhost:3002",
|
|
||||||
},
|
|
||||||
scaling:
|
|
||||||
$app.stage === "production"
|
|
||||||
? {
|
|
||||||
min: 2,
|
|
||||||
max: 10,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
//For temporarily persisting the persist.json
|
|
||||||
transform: {
|
|
||||||
taskDefinition: (args) => {
|
|
||||||
const volumes = $output(args.volumes).apply(v => {
|
|
||||||
const next = [...(v || []), {
|
|
||||||
name: "shared-tmp",
|
|
||||||
dockerVolumeConfiguration: {
|
|
||||||
scope: "shared",
|
|
||||||
driver: "local"
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
return next;
|
|
||||||
})
|
|
||||||
|
|
||||||
// "containerDefinitions" is a JSON string, parse first
|
|
||||||
let containers = $jsonParse(args.containerDefinitions);
|
|
||||||
|
|
||||||
containers = containers.apply((containerDefinitions) => {
|
|
||||||
containerDefinitions[0].mountPoints = [
|
|
||||||
...(containerDefinitions[0].mountPoints ?? []),
|
|
||||||
{
|
|
||||||
sourceVolume: "shared-tmp",
|
|
||||||
containerPath: "/tmp"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
return containerDefinitions;
|
|
||||||
});
|
|
||||||
|
|
||||||
args.volumes = volumes
|
|
||||||
args.containerDefinitions = $jsonStringify(containers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const auth = !$dev ? new sst.aws.Router("AuthRoute", {
|
|
||||||
routes: {
|
|
||||||
// I think auth.url should work all the same
|
|
||||||
"/*": authService.nodes.loadBalancer.dnsName,
|
|
||||||
},
|
},
|
||||||
domain: {
|
domain: {
|
||||||
name: "auth." + domain,
|
name: "auth." + domain,
|
||||||
dns: sst.cloudflare.dns(),
|
dns: sst.cloudflare.dns(),
|
||||||
},
|
},
|
||||||
}) : authService
|
forceUpgrade: "v2",
|
||||||
|
});
|
||||||
18
infra/cdn.ts
Normal file
18
infra/cdn.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { domain } from "./dns";
|
||||||
|
import { storage } from "./storage";
|
||||||
|
|
||||||
|
export const cdn = new sst.aws.Router("CDNRouter", {
|
||||||
|
routes: {
|
||||||
|
"/*": {
|
||||||
|
bucket: storage,
|
||||||
|
rewrite: {
|
||||||
|
regex: "^/([a-zA-Z0-9_-]+)$",
|
||||||
|
to: "/public/$1"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
name: "cdn." + domain,
|
||||||
|
dns: sst.cloudflare.dns()
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { domain } from "./dns";
|
|
||||||
import { storage } from "./storage";
|
|
||||||
|
|
||||||
sst.Linkable.wrap(aws.iam.AccessKey, (resource) => ({
|
|
||||||
properties: {
|
|
||||||
key: resource.id,
|
|
||||||
secret: resource.secret,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const cache = new sst.cloudflare.Kv("ImageCache");
|
|
||||||
|
|
||||||
const bucket = new sst.cloudflare.Bucket("ImageBucket");
|
|
||||||
|
|
||||||
const lambdaInvokerUser = new aws.iam.User("ImageIAMUser", {
|
|
||||||
name: `${$app.name}-${$app.stage}-ImageIAMUser`,
|
|
||||||
forceDestroy: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const imageProcessorFunction = new sst.aws.Function("ImageProcessor",
|
|
||||||
{
|
|
||||||
memory: "1024 MB",
|
|
||||||
link: [storage],
|
|
||||||
timeout: "30 seconds",
|
|
||||||
nodejs: { install: ["sharp"] },
|
|
||||||
handler: "packages/functions/src/images/processor.handler",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
new aws.iam.UserPolicy("InvokeLambdaPolicy", {
|
|
||||||
user: lambdaInvokerUser.name,
|
|
||||||
policy: $output({
|
|
||||||
Version: "2012-10-17",
|
|
||||||
Statement: [
|
|
||||||
{
|
|
||||||
Effect: "Allow",
|
|
||||||
Action: ["lambda:InvokeFunction"],
|
|
||||||
Resource: imageProcessorFunction.arn,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).apply(JSON.stringify),
|
|
||||||
});
|
|
||||||
|
|
||||||
const accessKey = new aws.iam.AccessKey("ImageInvokerAccessKey", {
|
|
||||||
user: lambdaInvokerUser.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const imageCdn = new sst.cloudflare.Worker("ImageCDN", {
|
|
||||||
url: true,
|
|
||||||
domain: "cdn." + domain,
|
|
||||||
link: [bucket, cache, imageProcessorFunction, accessKey],
|
|
||||||
handler: "packages/functions/src/images/index.ts",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const outputs = {
|
|
||||||
cdn: imageCdn.url
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { auth } from "./auth";
|
// import { auth } from "./auth";
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
|
|
||||||
export const device = new sst.aws.Realtime("Realtime", {
|
export const device = new sst.aws.Realtime("Realtime", {
|
||||||
authorizer: {
|
authorizer: {
|
||||||
link: [auth, postgres],
|
link: [ postgres],
|
||||||
handler: "packages/functions/src/realtime/authorizer.handler"
|
handler: "packages/functions/src/realtime/authorizer.handler"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1 +1,5 @@
|
|||||||
export const storage = new sst.aws.Bucket("Storage");
|
export const storage = new sst.aws.Bucket("Storage", {
|
||||||
|
access: "cloudfront"
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zeroStorage = new sst.aws.Bucket("ZeroStorage");
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// This is the website part where people play and connect
|
// This is the website part where people play and connect
|
||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
|
import { cdn } from "./cdn";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { zero } from "./zero";
|
import { zero } from "./zero";
|
||||||
import { domain } from "./dns";
|
import { domain } from "./dns";
|
||||||
@@ -16,6 +17,7 @@ new sst.aws.StaticSite("Web", {
|
|||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
VITE_API_URL: api.url,
|
VITE_API_URL: api.url,
|
||||||
|
VITE_CDN_URL: cdn.url,
|
||||||
VITE_STAGE: $app.stage,
|
VITE_STAGE: $app.stage,
|
||||||
VITE_AUTH_URL: auth.url,
|
VITE_AUTH_URL: auth.url,
|
||||||
VITE_ZERO_URL: zero.url,
|
VITE_ZERO_URL: zero.url,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { vpc } from "./vpc";
|
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { domain } from "./dns";
|
import { domain } from "./dns";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { cluster } from "./cluster";
|
import { cluster } from "./cluster";
|
||||||
import { storage } from "./storage";
|
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
|
import { zeroStorage } from "./storage";
|
||||||
|
|
||||||
// const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}/${postgres.database}`
|
|
||||||
const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}:${postgres.port}/${postgres.database}`;
|
const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}:${postgres.port}/${postgres.database}`;
|
||||||
|
|
||||||
const tag = $dev
|
const tag = $dev
|
||||||
@@ -34,7 +32,7 @@ const zeroEnv = {
|
|||||||
? {
|
? {
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
ZERO_LITESTREAM_BACKUP_URL: $interpolate`s3://${storage.name}/zero/0`,
|
ZERO_LITESTREAM_BACKUP_URL: $interpolate`s3://${zeroStorage.name}/zero/0`,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,15 +41,12 @@ const replicationManager = !$dev
|
|||||||
? new sst.aws.Service(`ZeroReplication`, {
|
? new sst.aws.Service(`ZeroReplication`, {
|
||||||
cluster,
|
cluster,
|
||||||
wait: true,
|
wait: true,
|
||||||
...($app.stage === "production"
|
cpu: "0.5 vCPU",
|
||||||
? {
|
memory: "1 GB",
|
||||||
cpu: "2 vCPU",
|
capacity: "spot",
|
||||||
memory: "4 GB",
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
architecture: "arm64",
|
architecture: "arm64",
|
||||||
image: zeroEnv.ZERO_IMAGE_URL,
|
image: zeroEnv.ZERO_IMAGE_URL,
|
||||||
link: [storage, postgres],
|
link: [zeroStorage, postgres],
|
||||||
health: {
|
health: {
|
||||||
command: ["CMD-SHELL", "curl -f http://localhost:4849/ || exit 1"],
|
command: ["CMD-SHELL", "curl -f http://localhost:4849/ || exit 1"],
|
||||||
interval: "5 seconds",
|
interval: "5 seconds",
|
||||||
@@ -128,17 +123,11 @@ const replicationManager = !$dev
|
|||||||
export const zero = new sst.aws.Service("Zero", {
|
export const zero = new sst.aws.Service("Zero", {
|
||||||
cluster,
|
cluster,
|
||||||
image: zeroEnv.ZERO_IMAGE_URL,
|
image: zeroEnv.ZERO_IMAGE_URL,
|
||||||
link: [storage, postgres],
|
link: [zeroStorage, postgres],
|
||||||
architecture: "arm64",
|
architecture: "arm64",
|
||||||
...($app.stage === "production"
|
cpu: "0.5 vCPU",
|
||||||
? {
|
memory: "1 GB",
|
||||||
cpu: "2 vCPU",
|
capacity: "spot",
|
||||||
memory: "4 GB",
|
|
||||||
capacity: "spot"
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
capacity: "spot"
|
|
||||||
}),
|
|
||||||
environment: {
|
environment: {
|
||||||
...zeroEnv,
|
...zeroEnv,
|
||||||
...($dev
|
...($dev
|
||||||
@@ -146,9 +135,9 @@ export const zero = new sst.aws.Service("Zero", {
|
|||||||
ZERO_NUM_SYNC_WORKERS: "1",
|
ZERO_NUM_SYNC_WORKERS: "1",
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
ZERO_CHANGE_STREAMER_URI: replicationManager?.url.apply((val) =>
|
ZERO_CHANGE_STREAMER_URI: replicationManager.url.apply((val) =>
|
||||||
val.replace("http://", "ws://"),
|
val.replace("http://", "ws://"),
|
||||||
) ?? "",
|
),
|
||||||
ZERO_UPSTREAM_MAX_CONNS: "15",
|
ZERO_UPSTREAM_MAX_CONNS: "15",
|
||||||
ZERO_CVR_MAX_CONNS: "160",
|
ZERO_CVR_MAX_CONNS: "160",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sharp": "^0.34.2",
|
|
||||||
"sst": "^3.11.21"
|
"sst": "^3.11.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fn } from "../utils";
|
import { fn } from "../utils";
|
||||||
|
import { Resource } from "sst";
|
||||||
import { Actor } from "../actor";
|
import { Actor } from "../actor";
|
||||||
|
import { bus } from "sst/aws/bus";
|
||||||
import { Common } from "../common";
|
import { Common } from "../common";
|
||||||
import { Examples } from "../examples";
|
import { Examples } from "../examples";
|
||||||
import { createEvent } from "../event";
|
import { createEvent } from "../event";
|
||||||
import { eq, and, isNull, desc } from "drizzle-orm";
|
import { eq, and, isNull, desc } from "drizzle-orm";
|
||||||
import { steamTable, StatusEnum, Limitations } from "./steam.sql";
|
import { steamTable, StatusEnum, Limitations } from "./steam.sql";
|
||||||
import { createTransaction, useTransaction } from "../drizzle/transaction";
|
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
||||||
|
|
||||||
export namespace Steam {
|
export namespace Steam {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
@@ -122,9 +124,9 @@ export namespace Steam {
|
|||||||
lastSyncedAt: input.lastSyncedAt ?? Common.utc(),
|
lastSyncedAt: input.lastSyncedAt ?? Common.utc(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// await afterTx(async () =>
|
await afterTx(async () =>
|
||||||
// bus.publish(Resource.Bus, Events.Created, { userID, steamID: input.id })
|
bus.publish(Resource.Bus, Events.Created, { userID, steamID: input.id })
|
||||||
// );
|
);
|
||||||
|
|
||||||
return input.id
|
return input.id
|
||||||
}),
|
}),
|
||||||
@@ -149,9 +151,9 @@ export namespace Steam {
|
|||||||
})
|
})
|
||||||
.where(eq(steamTable.id, input.steamID));
|
.where(eq(steamTable.id, input.steamID));
|
||||||
|
|
||||||
// await afterTx(async () =>
|
await afterTx(async () =>
|
||||||
// bus.publish(Resource.Bus, Events.Updated, { userID, steamID: input.steamID })
|
bus.publish(Resource.Bus, Events.Updated, { userID, steamID: input.steamID })
|
||||||
// );
|
);
|
||||||
|
|
||||||
return input.steamID
|
return input.steamID
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
FROM mirror.gcr.io/oven/bun:1.2
|
|
||||||
|
|
||||||
# TODO: Add a way to build C# Steam.exe and start it to run in the container before the API
|
|
||||||
|
|
||||||
ADD ./package.json .
|
|
||||||
ADD ./bun.lock .
|
|
||||||
ADD ./packages/core/package.json ./packages/core/package.json
|
|
||||||
ADD ./packages/functions/package.json ./packages/functions/package.json
|
|
||||||
ADD ./patches ./patches
|
|
||||||
RUN bun install --ignore-scripts
|
|
||||||
|
|
||||||
ADD ./packages/functions ./packages/functions
|
|
||||||
ADD ./packages/core ./packages/core
|
|
||||||
|
|
||||||
WORKDIR ./packages/functions
|
|
||||||
|
|
||||||
CMD ["bun", "run", "./src/api/index.ts"]
|
|
||||||
@@ -7,10 +7,6 @@
|
|||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/steamcommunity": "^3.43.8"
|
"@types/steamcommunity": "^3.43.8"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"dev:auth": "bun run --watch ./src/auth/index.ts",
|
|
||||||
"dev:api": "bun run --watch ./src/api/index.ts"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
@@ -25,8 +21,10 @@
|
|||||||
"@aws-sdk/client-sqs": "^3.806.0",
|
"@aws-sdk/client-sqs": "^3.806.0",
|
||||||
"@nestri/core": "workspace:",
|
"@nestri/core": "workspace:",
|
||||||
"actor-core": "^0.8.0",
|
"actor-core": "^0.8.0",
|
||||||
"aws4fetch": "^1.0.20",
|
|
||||||
"hono": "^4.7.8",
|
"hono": "^4.7.8",
|
||||||
"hono-openapi": "^0.4.8"
|
"hono-openapi": "^0.4.8",
|
||||||
|
"steam-session": "*",
|
||||||
|
"steamcommunity": "^3.48.6",
|
||||||
|
"steamid": "^2.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { z } from "zod"
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import {
|
|
||||||
S3Client,
|
|
||||||
GetObjectCommand,
|
|
||||||
} from "@aws-sdk/client-s3";
|
|
||||||
import Sharp from "sharp";
|
|
||||||
import { Resource } from "sst";
|
|
||||||
import { validator } from "hono-openapi/zod";
|
|
||||||
import { HTTPException } from "hono/http-exception";
|
|
||||||
|
|
||||||
const s3 = new S3Client();
|
|
||||||
|
|
||||||
interface TimingMetrics {
|
|
||||||
download: number;
|
|
||||||
transform: number;
|
|
||||||
upload?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTimingHeader = (metrics: TimingMetrics): string => {
|
|
||||||
const timings = [
|
|
||||||
`img-download;dur=${Math.round(metrics.download)}`,
|
|
||||||
`img-transform;dur=${Math.round(metrics.transform)}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (metrics.upload !== undefined) {
|
|
||||||
timings.push(`img-upload;dur=${Math.round(metrics.upload)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return timings.join(",");
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export namespace ImageApi {
|
|
||||||
export const route = new Hono()
|
|
||||||
.post("/:hash",
|
|
||||||
validator("json",
|
|
||||||
z.object({
|
|
||||||
dpr: z.number().optional(),
|
|
||||||
width: z.number().optional(),
|
|
||||||
height: z.number().optional(),
|
|
||||||
quality: z.number().optional(),
|
|
||||||
format: z.enum(["avif", "webp", "jpeg"]),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
// validator("header",
|
|
||||||
// z.object({
|
|
||||||
// secretKey: z.string(),
|
|
||||||
// })
|
|
||||||
// ),
|
|
||||||
validator("param",
|
|
||||||
z.object({
|
|
||||||
hash: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
async (c) => {
|
|
||||||
const input = c.req.valid("json");
|
|
||||||
const { hash } = c.req.valid("param");
|
|
||||||
// const secret = c.req.valid("header").secretKey
|
|
||||||
|
|
||||||
const metrics: TimingMetrics = {
|
|
||||||
download: 0,
|
|
||||||
transform: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadStart = performance.now();
|
|
||||||
let originalImage: Buffer;
|
|
||||||
let contentType: string;
|
|
||||||
try {
|
|
||||||
const getCommand = new GetObjectCommand({
|
|
||||||
Bucket: Resource.Storage.name,
|
|
||||||
Key: hash,
|
|
||||||
});
|
|
||||||
const response = await s3.send(getCommand);
|
|
||||||
|
|
||||||
originalImage = Buffer.from(await response.Body!.transformToByteArray());
|
|
||||||
contentType = response.ContentType || "image/jpeg";
|
|
||||||
metrics.download = performance.now() - downloadStart;
|
|
||||||
} catch (error) {
|
|
||||||
throw new HTTPException(500, { message: `Error downloading original image:${error}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const transformStart = performance.now();
|
|
||||||
let transformedImage: Buffer;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let sharpInstance = Sharp(originalImage, {
|
|
||||||
failOn: "none",
|
|
||||||
animated: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const metadata = await sharpInstance.metadata();
|
|
||||||
|
|
||||||
// Apply transformations
|
|
||||||
if (input.width || input.height) {
|
|
||||||
sharpInstance = sharpInstance.resize({
|
|
||||||
width: input.width,
|
|
||||||
height: input.height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.orientation) {
|
|
||||||
sharpInstance = sharpInstance.rotate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.format) {
|
|
||||||
const isLossy = ["jpeg", "webp", "avif"].includes(input.format);
|
|
||||||
|
|
||||||
if (isLossy && input.quality) {
|
|
||||||
sharpInstance = sharpInstance.toFormat(input.format, {
|
|
||||||
quality: input.quality,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sharpInstance = sharpInstance.toFormat(input.format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transformedImage = await sharpInstance.toBuffer();
|
|
||||||
metrics.transform = performance.now() - transformStart;
|
|
||||||
|
|
||||||
contentType = `image/${input.format}`;
|
|
||||||
} catch (error) {
|
|
||||||
throw new HTTPException(500, { message: `Error transforming image:${error}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.newResponse(transformedImage,
|
|
||||||
200,
|
|
||||||
{
|
|
||||||
"Content-Type": contentType,
|
|
||||||
"Cache-Control": "max-age=31536000",
|
|
||||||
"Server-Timing": formatTimingHeader(metrics),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import "zod-openapi/extend";
|
import "zod-openapi/extend";
|
||||||
import { cors } from "hono/cors";
|
import { Hono } from "hono";
|
||||||
import { GameApi } from "./game";
|
import { GameApi } from "./game";
|
||||||
import { SteamApi } from "./steam";
|
import { SteamApi } from "./steam";
|
||||||
import { auth } from "./utils/auth";
|
import { auth } from "./utils/auth";
|
||||||
import { FriendApi } from "./friend";
|
import { FriendApi } from "./friend";
|
||||||
import { logger } from "hono/logger";
|
import { logger } from "hono/logger";
|
||||||
import { type Env, Hono } from "hono";
|
|
||||||
import { Realtime } from "./realtime";
|
|
||||||
import { AccountApi } from "./account";
|
import { AccountApi } from "./account";
|
||||||
import { openAPISpecs } from "hono-openapi";
|
import { openAPISpecs } from "hono-openapi";
|
||||||
import { patchLogger } from "../utils/patch-logger";
|
import { patchLogger } from "../utils/patch-logger";
|
||||||
import { HTTPException } from "hono/http-exception";
|
import { HTTPException } from "hono/http-exception";
|
||||||
|
import { handle, streamHandle } from "hono/aws-lambda";
|
||||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||||
|
|
||||||
patchLogger();
|
patchLogger();
|
||||||
@@ -18,18 +17,16 @@ patchLogger();
|
|||||||
export const app = new Hono();
|
export const app = new Hono();
|
||||||
app
|
app
|
||||||
.use(logger())
|
.use(logger())
|
||||||
.use(cors())
|
|
||||||
.use(async (c, next) => {
|
.use(async (c, next) => {
|
||||||
c.header("Cache-Control", "no-store");
|
c.header("Cache-Control", "no-store");
|
||||||
return next();
|
return next();
|
||||||
})
|
})
|
||||||
.use(auth)
|
.use(auth)
|
||||||
|
|
||||||
const routes = app
|
const routes = app
|
||||||
.get("/", (c) => c.text("Hello World!"))
|
.get("/", (c) => c.text("Hello World!"))
|
||||||
.route("/games", GameApi.route)
|
.route("/games", GameApi.route)
|
||||||
.route("/steam", SteamApi.route)
|
.route("/steam", SteamApi.route)
|
||||||
.route("/realtime", Realtime.route)
|
|
||||||
.route("/friends", FriendApi.route)
|
.route("/friends", FriendApi.route)
|
||||||
.route("/account", AccountApi.route)
|
.route("/account", AccountApi.route)
|
||||||
.onError((error, c) => {
|
.onError((error, c) => {
|
||||||
@@ -77,7 +74,7 @@ app.get(
|
|||||||
scheme: "bearer",
|
scheme: "bearer",
|
||||||
bearerFormat: "JWT",
|
bearerFormat: "JWT",
|
||||||
},
|
},
|
||||||
SteamID: {
|
TeamID: {
|
||||||
type: "apiKey",
|
type: "apiKey",
|
||||||
description: "The steam ID to use for this query",
|
description: "The steam ID to use for this query",
|
||||||
in: "header",
|
in: "header",
|
||||||
@@ -85,7 +82,7 @@ app.get(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
security: [{ Bearer: [], SteamID: [] }],
|
security: [{ Bearer: [], TeamID: [] }],
|
||||||
servers: [
|
servers: [
|
||||||
{ description: "Production", url: "https://api.nestri.io" },
|
{ description: "Production", url: "https://api.nestri.io" },
|
||||||
{ description: "Sandbox", url: "https://api.dev.nestri.io" },
|
{ description: "Sandbox", url: "https://api.dev.nestri.io" },
|
||||||
@@ -94,13 +91,6 @@ app.get(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export default {
|
export type Routes = typeof routes;
|
||||||
port: 3001,
|
|
||||||
idleTimeout: 255,
|
export const handler = process.env.SST_LIVE ? handle(app) : streamHandle(app);
|
||||||
webSocketHandler: Realtime.webSocketHandler,
|
|
||||||
fetch: (req: Request, env: Env) =>
|
|
||||||
app.fetch(req, env, {
|
|
||||||
waitUntil: (fn) => fn,
|
|
||||||
passThroughOnException: () => { },
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Resource } from "sst";
|
import { Resource } from "sst";
|
||||||
import { bus } from "sst/aws/bus";
|
|
||||||
import { Actor } from "@nestri/core/actor";
|
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { User } from "@nestri/core/user/index";
|
import { User } from "@nestri/core/user/index";
|
||||||
import { Examples } from "@nestri/core/examples";
|
import { Examples } from "@nestri/core/examples";
|
||||||
import { Steam } from "@nestri/core/steam/index";
|
import { Steam } from "@nestri/core/steam/index";
|
||||||
import { getCookie, setCookie } from "hono/cookie";
|
import { getCookie, setCookie } from "hono/cookie";
|
||||||
import { Client } from "@nestri/core/client/index";
|
import { Client } from "@nestri/core/client/index";
|
||||||
import { Friend } from "@nestri/core/friend/index";
|
|
||||||
import { Library } from "@nestri/core/library/index";
|
|
||||||
import { chunkArray } from "@nestri/core/utils/helper";
|
|
||||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||||
import { ErrorResponses, validator, Result, notPublic } from "./utils";
|
import { ErrorResponses, validator, Result, notPublic } from "./utils";
|
||||||
|
|
||||||
|
|
||||||
export namespace SteamApi {
|
export namespace SteamApi {
|
||||||
export const route = new Hono()
|
export const route = new Hono()
|
||||||
.get("/",
|
.get("/",
|
||||||
@@ -110,89 +104,6 @@ export namespace SteamApi {
|
|||||||
await Steam.updateOwner({ userID, steamID })
|
await Steam.updateOwner({ userID, steamID })
|
||||||
}
|
}
|
||||||
|
|
||||||
c.executionCtx.waitUntil((async () => {
|
|
||||||
try {
|
|
||||||
// Get friends info
|
|
||||||
const friends = await Client.getFriendsList(steamID);
|
|
||||||
|
|
||||||
const friendSteamIDs = friends.friendslist.friends.map(f => f.steamid);
|
|
||||||
|
|
||||||
// Steam API has a limit of requesting 100 friends at a go
|
|
||||||
const friendChunks = chunkArray(friendSteamIDs, 100);
|
|
||||||
|
|
||||||
const settled = await Promise.allSettled(
|
|
||||||
friendChunks.map(async (friendIDs) => {
|
|
||||||
const friendsInfo = await Client.getUserInfo(friendIDs)
|
|
||||||
|
|
||||||
return await Promise.all(
|
|
||||||
friendsInfo.map(async (friend) => {
|
|
||||||
const wasAdded = await Steam.create(friend);
|
|
||||||
|
|
||||||
if (!wasAdded) {
|
|
||||||
console.log(`Friend ${friend.id} already exists`)
|
|
||||||
}
|
|
||||||
|
|
||||||
await Friend.add({ friendSteamID: friend.id, steamID })
|
|
||||||
|
|
||||||
return friend.id
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
settled
|
|
||||||
.filter(result => result.status === 'rejected')
|
|
||||||
.forEach(result => console.warn('[putFriends] failed:', (result as PromiseRejectedResult).reason))
|
|
||||||
|
|
||||||
const prod = (Resource.App.stage === "production" || Resource.App.stage === "dev")
|
|
||||||
|
|
||||||
const friendIDs = [
|
|
||||||
steamID,
|
|
||||||
...(prod ? settled
|
|
||||||
.filter(result => result.status === "fulfilled")
|
|
||||||
.map(f => f.value)
|
|
||||||
.flat() : [])
|
|
||||||
]
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
friendIDs.map(async (currentSteamID) => {
|
|
||||||
// Get user library
|
|
||||||
const gameLibrary = await Client.getUserLibrary(currentSteamID);
|
|
||||||
|
|
||||||
const queryLib = await Promise.allSettled(
|
|
||||||
gameLibrary.response.games.map(async (game) => {
|
|
||||||
await Actor.provide(
|
|
||||||
"steam",
|
|
||||||
{
|
|
||||||
steamID: currentSteamID,
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
|
|
||||||
await bus.publish(
|
|
||||||
Resource.Bus,
|
|
||||||
Library.Events.Add,
|
|
||||||
{
|
|
||||||
appID: game.appid,
|
|
||||||
totalPlaytime: game.playtime_forever,
|
|
||||||
lastPlayed: game.rtime_last_played ? new Date(game.rtime_last_played * 1000) : null,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
queryLib
|
|
||||||
.filter(i => i.status === "rejected")
|
|
||||||
.forEach(e => console.warn(`[pushUserLib]: Failed to push user library to queue: ${e.reason}`))
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Failed to process Steam data for user ${userID}:`, error);
|
|
||||||
}
|
|
||||||
})())
|
|
||||||
|
|
||||||
return c.html(
|
return c.html(
|
||||||
`
|
`
|
||||||
<script>
|
<script>
|
||||||
@@ -236,7 +147,7 @@ export namespace SteamApi {
|
|||||||
|
|
||||||
setCookie(c, "user_id", user.id);
|
setCookie(c, "user_id", user.id);
|
||||||
|
|
||||||
const returnUrl = `${new URL(c.req.url).origin}/steam/callback/${userID}`
|
const returnUrl = `${new URL(Resource.Urls.api).origin}/steam/callback/${userID}`
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
'openid.ns': 'http://specs.openid.net/auth/2.0',
|
'openid.ns': 'http://specs.openid.net/auth/2.0',
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
import { Resource } from "sst";
|
import { Resource } from "sst";
|
||||||
import { type Env } from "hono";
|
|
||||||
import { logger } from "hono/logger";
|
import { logger } from "hono/logger";
|
||||||
import { subjects } from "../subjects";
|
import { subjects } from "../subjects";
|
||||||
|
import { handle } from "hono/aws-lambda";
|
||||||
import { PasswordUI, Select } from "./ui";
|
import { PasswordUI, Select } from "./ui";
|
||||||
import { issuer } from "@openauthjs/openauth";
|
import { issuer } from "@openauthjs/openauth";
|
||||||
import { User } from "@nestri/core/user/index";
|
import { User } from "@nestri/core/user/index";
|
||||||
import { Email } from "@nestri/core/email/index";
|
import { Email } from "@nestri/core/email/index";
|
||||||
import { patchLogger } from "../utils/patch-logger";
|
import { patchLogger } from "../utils/patch-logger";
|
||||||
import { handleDiscord, handleGithub } from "./utils";
|
import { handleDiscord, handleGithub } from "./utils";
|
||||||
import { MemoryStorage } from "@openauthjs/openauth/storage/memory";
|
|
||||||
import { DiscordAdapter, PasswordAdapter, GithubAdapter } from "./adapters";
|
import { DiscordAdapter, PasswordAdapter, GithubAdapter } from "./adapters";
|
||||||
|
|
||||||
patchLogger();
|
patchLogger();
|
||||||
|
|
||||||
const app = issuer({
|
const app = issuer({
|
||||||
//TODO: Create our own Storage (?)
|
|
||||||
select: Select(),
|
select: Select(),
|
||||||
storage: MemoryStorage({
|
|
||||||
persist: process.env.STORAGE
|
|
||||||
}),
|
|
||||||
theme: {
|
theme: {
|
||||||
title: "Nestri | Auth",
|
title: "Nestri | Auth",
|
||||||
primary: "#FF4F01",
|
primary: "#FF4F01",
|
||||||
@@ -161,13 +156,4 @@ const app = issuer({
|
|||||||
},
|
},
|
||||||
}).use(logger())
|
}).use(logger())
|
||||||
|
|
||||||
|
export const handler = handle(app);
|
||||||
export default {
|
|
||||||
port: 3002,
|
|
||||||
idleTimeout: 255,
|
|
||||||
fetch: (req: Request, env: Env) =>
|
|
||||||
app.fetch(req, env, {
|
|
||||||
waitUntil: (fn) => fn,
|
|
||||||
passThroughOnException: () => { },
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@@ -5,8 +5,10 @@ import { Actor } from "@nestri/core/actor";
|
|||||||
import { Game } from "@nestri/core/game/index";
|
import { Game } from "@nestri/core/game/index";
|
||||||
import { Steam } from "@nestri/core/steam/index";
|
import { Steam } from "@nestri/core/steam/index";
|
||||||
import { Client } from "@nestri/core/client/index";
|
import { Client } from "@nestri/core/client/index";
|
||||||
|
import { Friend } from "@nestri/core/friend/index";
|
||||||
import { Images } from "@nestri/core/images/index";
|
import { Images } from "@nestri/core/images/index";
|
||||||
import { Library } from "@nestri/core/library/index";
|
import { Library } from "@nestri/core/library/index";
|
||||||
|
import { chunkArray } from "@nestri/core/utils/index";
|
||||||
import { BaseGame } from "@nestri/core/base-game/index";
|
import { BaseGame } from "@nestri/core/base-game/index";
|
||||||
import { Categories } from "@nestri/core/categories/index";
|
import { Categories } from "@nestri/core/categories/index";
|
||||||
import { ImageTypeEnum } from "@nestri/core/images/images.sql";
|
import { ImageTypeEnum } from "@nestri/core/images/images.sql";
|
||||||
@@ -46,7 +48,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new HeadObjectCommand({
|
new HeadObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
Body: image.buffer,
|
Body: image.buffer,
|
||||||
...(image.format && { ContentType: `image/${image.format}` }),
|
...(image.format && { ContentType: `image/${image.format}` }),
|
||||||
Metadata: {
|
Metadata: {
|
||||||
@@ -89,7 +91,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new HeadObjectCommand({
|
new HeadObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
Body: image.buffer,
|
Body: image.buffer,
|
||||||
...(image.format && { ContentType: `image/${image.format}` }),
|
...(image.format && { ContentType: `image/${image.format}` }),
|
||||||
Metadata: {
|
Metadata: {
|
||||||
@@ -134,7 +136,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new HeadObjectCommand({
|
new HeadObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -143,7 +145,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
Body: image.buffer,
|
Body: image.buffer,
|
||||||
...(image.format && { ContentType: `image/${image.format}` }),
|
...(image.format && { ContentType: `image/${image.format}` }),
|
||||||
Metadata: {
|
Metadata: {
|
||||||
@@ -262,7 +264,94 @@ export const handler = bus.subscriber(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case "steam_account.created":
|
||||||
|
case "steam_account.updated": {
|
||||||
|
const userID = event.properties.userID;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const steamID = event.properties.steamID;
|
||||||
|
// Get friends info
|
||||||
|
const friends = await Client.getFriendsList(steamID);
|
||||||
|
|
||||||
|
const friendSteamIDs = friends.friendslist.friends.map(f => f.steamid);
|
||||||
|
|
||||||
|
// Steam API has a limit of requesting 100 friends at a go
|
||||||
|
const friendChunks = chunkArray(friendSteamIDs, 100);
|
||||||
|
|
||||||
|
const settled = await Promise.allSettled(
|
||||||
|
friendChunks.map(async (friendIDs) => {
|
||||||
|
const friendsInfo = await Client.getUserInfo(friendIDs)
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
friendsInfo.map(async (friend) => {
|
||||||
|
const wasAdded = await Steam.create(friend);
|
||||||
|
|
||||||
|
if (!wasAdded) {
|
||||||
|
console.log(`Friend ${friend.id} already exists`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Friend.add({ friendSteamID: friend.id, steamID })
|
||||||
|
|
||||||
|
return friend.id
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
settled
|
||||||
|
.filter(result => result.status === 'rejected')
|
||||||
|
.forEach(result => console.warn('[putFriends] failed:', (result as PromiseRejectedResult).reason))
|
||||||
|
|
||||||
|
const prod = (Resource.App.stage === "production" || Resource.App.stage === "dev")
|
||||||
|
|
||||||
|
const friendIDs = [
|
||||||
|
steamID,
|
||||||
|
...(prod ? settled
|
||||||
|
.filter(result => result.status === "fulfilled")
|
||||||
|
.map(f => f.value)
|
||||||
|
.flat() : [])
|
||||||
|
]
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
friendIDs.map(async (currentSteamID) => {
|
||||||
|
// Get user library
|
||||||
|
const gameLibrary = await Client.getUserLibrary(currentSteamID);
|
||||||
|
|
||||||
|
const queryLib = await Promise.allSettled(
|
||||||
|
gameLibrary.response.games.map(async (game) => {
|
||||||
|
await Actor.provide(
|
||||||
|
"steam",
|
||||||
|
{
|
||||||
|
steamID: currentSteamID,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
|
||||||
|
await bus.publish(
|
||||||
|
Resource.Bus,
|
||||||
|
Library.Events.Add,
|
||||||
|
{
|
||||||
|
appID: game.appid,
|
||||||
|
totalPlaytime: game.playtime_forever,
|
||||||
|
lastPlayed: game.rtime_last_played ? new Date(game.rtime_last_played * 1000) : null,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
queryLib
|
||||||
|
.filter(i => i.status === "rejected")
|
||||||
|
.forEach(e => console.warn(`[pushUserLib]: Failed to push user library to queue: ${e.reason}`))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Failed to process Steam data for user ${userID}:`, error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import { Hono } from "hono";
|
|
||||||
import { AwsClient } from 'aws4fetch'
|
|
||||||
import { Resource } from "sst";
|
|
||||||
import { HTTPException } from "hono/http-exception";
|
|
||||||
|
|
||||||
|
|
||||||
export namespace ImageRoute {
|
|
||||||
export const route = new Hono()
|
|
||||||
.get(
|
|
||||||
"/:hashWithExt",
|
|
||||||
async (c) => {
|
|
||||||
// const { hashWithExt } = c.req.param();
|
|
||||||
|
|
||||||
const client = new AwsClient({
|
|
||||||
accessKeyId: Resource.ImageInvokerAccessKey.key,
|
|
||||||
secretAccessKey: Resource.ImageInvokerAccessKey.secret,
|
|
||||||
})
|
|
||||||
|
|
||||||
const LAMBDA_URL = `https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/${Resource.ImageProcessor.name}/invocations`
|
|
||||||
|
|
||||||
const lambdaResponse = await client.fetch(LAMBDA_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ world: "hello" }),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!lambdaResponse.ok) {
|
|
||||||
console.error(await lambdaResponse.text())
|
|
||||||
return c.json({ error: `Lambda API returned ${lambdaResponse.status}` }, { status: 500 })
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(await lambdaResponse.json())
|
|
||||||
|
|
||||||
// // Validate format
|
|
||||||
// // Split hash and extension
|
|
||||||
// const match = hashWithExt.match(/^([a-zA-Z0-9_-]+)\.(avif|webp)$/);
|
|
||||||
// if (!match) {
|
|
||||||
// throw new HTTPException(400, { message: "Invalid image hash or format" });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const [, hash, format] = match;
|
|
||||||
|
|
||||||
// const query = c.req.query();
|
|
||||||
// // Parse dimensions
|
|
||||||
// const width = parseInt(query.w || query.width || "");
|
|
||||||
// const height = parseInt(query.h || query.height || "");
|
|
||||||
// const dpr = parseFloat(query.dpr || "1");
|
|
||||||
|
|
||||||
// if (isNaN(width) || width <= 0) {
|
|
||||||
// throw new HTTPException(400, { message: "Invalid width" });
|
|
||||||
// }
|
|
||||||
// if (!isNaN(height) && height < 0) {
|
|
||||||
// throw new HTTPException(400, { message: "Invalid height" });
|
|
||||||
// }
|
|
||||||
// if (dpr < 1 || dpr > 4) {
|
|
||||||
// throw new HTTPException(400, { message: "Invalid dpr" });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log("url",Resource.Api.url)
|
|
||||||
|
|
||||||
// const imageBytes = await fetch(`${Resource.Api.url}/image/${hash}`,{
|
|
||||||
// method:"POST",
|
|
||||||
// body:JSON.stringify({
|
|
||||||
// dpr,
|
|
||||||
// width,
|
|
||||||
// height,
|
|
||||||
// format
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
// console.log("imahe",imageBytes.headers)
|
|
||||||
|
|
||||||
// // Normalize and build cache key
|
|
||||||
// // const cacheKey = `${hash}_${format}_w${width}${height ? `_h${height}` : ""}_dpr${dpr}`;
|
|
||||||
|
|
||||||
// // Add aggressive caching
|
|
||||||
// // c.header("Cache-Control", "public, max-age=315360000, immutable");
|
|
||||||
|
|
||||||
// // Placeholder image response (to be replaced by real logic)
|
|
||||||
// return c.newResponse(await imageBytes.arrayBuffer(),
|
|
||||||
// // {
|
|
||||||
// // headers: {
|
|
||||||
// // ...imageBytes.headers
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// );
|
|
||||||
|
|
||||||
return c.text("success")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Hono } from "hono";
|
|
||||||
import { logger } from "hono/logger";
|
|
||||||
import { ImageRoute } from "./image";
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
app
|
|
||||||
.use(logger(), async (c, next) => {
|
|
||||||
// c.header("Cache-Control", "public, max-age=315360000, immutable");
|
|
||||||
return next();
|
|
||||||
})
|
|
||||||
|
|
||||||
const routes = app
|
|
||||||
.get("/", (c) => c.text("Hello World 👋🏾"))
|
|
||||||
.route("/image", ImageRoute.route)
|
|
||||||
|
|
||||||
|
|
||||||
export type Routes = typeof routes;
|
|
||||||
export default app;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export async function handler(event: any) {
|
|
||||||
console.log('Task completion event received:', JSON.stringify(event, null, 2));
|
|
||||||
|
|
||||||
return JSON.stringify({ hello: "world" })
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,18 @@
|
|||||||
"@bufbuild/protoc-gen-es": "^2.2.3"
|
"@bufbuild/protoc-gen-es": "^2.2.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^2.2.3"
|
"@bufbuild/protobuf": "^2.2.3",
|
||||||
|
"@chainsafe/libp2p-noise": "^16.1.3",
|
||||||
|
"@chainsafe/libp2p-yamux": "^7.0.1",
|
||||||
|
"@libp2p/identify": "^3.0.32",
|
||||||
|
"@libp2p/interface": "^2.10.2",
|
||||||
|
"@libp2p/ping": "^2.0.32",
|
||||||
|
"@libp2p/websockets": "^9.2.13",
|
||||||
|
"@multiformats/multiaddr": "^12.4.0",
|
||||||
|
"it-length-prefixed": "^10.0.1",
|
||||||
|
"it-pipe": "^3.0.1",
|
||||||
|
"libp2p": "^2.8.8",
|
||||||
|
"uint8arraylist": "^2.4.8",
|
||||||
|
"uint8arrays": "^5.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,305 @@
|
|||||||
import {LatencyTracker} from "./latency";
|
import { LatencyTracker } from "./latency";
|
||||||
|
import { Uint8ArrayList } from "uint8arraylist";
|
||||||
|
import { allocUnsafe } from "uint8arrays/alloc";
|
||||||
|
import { pipe } from "it-pipe";
|
||||||
|
import { decode, encode } from "it-length-prefixed";
|
||||||
|
import { Stream } from "@libp2p/interface";
|
||||||
|
|
||||||
export interface MessageBase {
|
export interface MessageBase {
|
||||||
payload_type: string;
|
payload_type: string;
|
||||||
latency?: LatencyTracker;
|
latency?: LatencyTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MessageRaw extends MessageBase {
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewMessageRaw(type: string, data: any): Uint8Array {
|
||||||
|
const msg = {
|
||||||
|
payload_type: type,
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
return new TextEncoder().encode(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessageICE extends MessageBase {
|
export interface MessageICE extends MessageBase {
|
||||||
payload_type: "ice";
|
|
||||||
candidate: RTCIceCandidateInit;
|
candidate: RTCIceCandidateInit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function NewMessageICE(
|
||||||
|
type: string,
|
||||||
|
candidate: RTCIceCandidateInit,
|
||||||
|
): Uint8Array {
|
||||||
|
const msg = {
|
||||||
|
payload_type: type,
|
||||||
|
candidate: candidate,
|
||||||
|
};
|
||||||
|
return new TextEncoder().encode(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessageSDP extends MessageBase {
|
export interface MessageSDP extends MessageBase {
|
||||||
payload_type: "sdp";
|
|
||||||
sdp: RTCSessionDescriptionInit;
|
sdp: RTCSessionDescriptionInit;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum JoinerType {
|
export function NewMessageSDP(
|
||||||
JoinerNode = 0,
|
type: string,
|
||||||
JoinerClient = 1,
|
sdp: RTCSessionDescriptionInit,
|
||||||
|
): Uint8Array {
|
||||||
|
const msg = {
|
||||||
|
payload_type: type,
|
||||||
|
sdp: sdp,
|
||||||
|
};
|
||||||
|
return new TextEncoder().encode(JSON.stringify(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageJoin extends MessageBase {
|
const MAX_SIZE = 1024 * 1024; // 1MB
|
||||||
payload_type: "join";
|
const MAX_QUEUE_SIZE = 1000; // Maximum number of messages in the queue
|
||||||
joiner_type: JoinerType;
|
|
||||||
|
// Custom 4-byte length encoder
|
||||||
|
export const length4ByteEncoder = (length: number) => {
|
||||||
|
const buf = allocUnsafe(4);
|
||||||
|
|
||||||
|
// Write the length as a 32-bit unsigned integer (4 bytes)
|
||||||
|
buf[0] = length >>> 24;
|
||||||
|
buf[1] = (length >>> 16) & 0xff;
|
||||||
|
buf[2] = (length >>> 8) & 0xff;
|
||||||
|
buf[3] = length & 0xff;
|
||||||
|
|
||||||
|
// Set the bytes property to 4
|
||||||
|
length4ByteEncoder.bytes = 4;
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
};
|
||||||
|
length4ByteEncoder.bytes = 4;
|
||||||
|
|
||||||
|
// Custom 4-byte length decoder
|
||||||
|
export const length4ByteDecoder = (data: Uint8ArrayList) => {
|
||||||
|
if (data.byteLength < 4) {
|
||||||
|
// Not enough bytes to read the length
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the length from the first 4 bytes
|
||||||
|
let length = 0;
|
||||||
|
length =
|
||||||
|
(data.subarray(0, 1)[0] >>> 0) * 0x1000000 +
|
||||||
|
(data.subarray(1, 2)[0] >>> 0) * 0x10000 +
|
||||||
|
(data.subarray(2, 3)[0] >>> 0) * 0x100 +
|
||||||
|
(data.subarray(3, 4)[0] >>> 0);
|
||||||
|
|
||||||
|
// Set bytes read to 4
|
||||||
|
length4ByteDecoder.bytes = 4;
|
||||||
|
|
||||||
|
return length;
|
||||||
|
};
|
||||||
|
length4ByteDecoder.bytes = 4;
|
||||||
|
|
||||||
|
interface PromiseMessage {
|
||||||
|
data: Uint8Array;
|
||||||
|
resolve: () => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AnswerType {
|
export class SafeStream {
|
||||||
AnswerOffline = 0,
|
private stream: Stream;
|
||||||
AnswerInUse,
|
private callbacks: Map<string, ((data: any) => void)[]> = new Map();
|
||||||
AnswerOK
|
private isReading: boolean = false;
|
||||||
}
|
private isWriting: boolean = false;
|
||||||
|
private closed: boolean = false;
|
||||||
|
private messageQueue: PromiseMessage[] = [];
|
||||||
|
private writeLock = false;
|
||||||
|
private readRetries = 0;
|
||||||
|
private writeRetries = 0;
|
||||||
|
private readonly MAX_RETRIES = 5;
|
||||||
|
|
||||||
export interface MessageAnswer extends MessageBase {
|
constructor(stream: Stream) {
|
||||||
payload_type: "answer";
|
this.stream = stream;
|
||||||
answer_type: AnswerType;
|
this.startReading();
|
||||||
|
this.startWriting();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startReading(): Promise<void> {
|
||||||
|
if (this.isReading || this.closed) return;
|
||||||
|
|
||||||
|
this.isReading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const source = this.stream.source;
|
||||||
|
const decodedSource = decode(source, {
|
||||||
|
maxDataLength: MAX_SIZE,
|
||||||
|
lengthDecoder: length4ByteDecoder,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of decodedSource) {
|
||||||
|
if (this.closed) break;
|
||||||
|
|
||||||
|
this.readRetries = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = chunk.slice();
|
||||||
|
const message = JSON.parse(
|
||||||
|
new TextDecoder().decode(data),
|
||||||
|
) as MessageBase;
|
||||||
|
const msgType = message.payload_type;
|
||||||
|
|
||||||
|
if (this.callbacks.has(msgType)) {
|
||||||
|
const handlers = this.callbacks.get(msgType)!;
|
||||||
|
for (const handler of handlers) {
|
||||||
|
try {
|
||||||
|
handler(message);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error in message handler for ${msgType}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error processing message:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Stream reading error:", err);
|
||||||
|
} finally {
|
||||||
|
this.isReading = false;
|
||||||
|
this.readRetries++;
|
||||||
|
|
||||||
|
// If not closed, try to restart reading
|
||||||
|
if (!this.closed && this.readRetries < this.MAX_RETRIES)
|
||||||
|
setTimeout(() => this.startReading(), 100);
|
||||||
|
else if (this.readRetries >= this.MAX_RETRIES)
|
||||||
|
console.error(
|
||||||
|
"Max retries reached for reading stream, stopping attempts",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerCallback(
|
||||||
|
msgType: string,
|
||||||
|
callback: (data: any) => void,
|
||||||
|
): void {
|
||||||
|
if (!this.callbacks.has(msgType)) {
|
||||||
|
this.callbacks.set(msgType, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbacks.get(msgType)!.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeCallback(msgType: string, callback: (data: any) => void): void {
|
||||||
|
if (this.callbacks.has(msgType)) {
|
||||||
|
const callbacks = this.callbacks.get(msgType)!;
|
||||||
|
const index = callbacks.indexOf(callback);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
callbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callbacks.length === 0) {
|
||||||
|
this.callbacks.delete(msgType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startWriting(): Promise<void> {
|
||||||
|
if (this.isWriting || this.closed) return;
|
||||||
|
|
||||||
|
this.isWriting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create an async generator for real-time message processing
|
||||||
|
const messageSource = async function* (this: SafeStream) {
|
||||||
|
while (!this.closed) {
|
||||||
|
// Check if we have messages to send
|
||||||
|
if (this.messageQueue.length > 0) {
|
||||||
|
this.writeLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = this.messageQueue[0];
|
||||||
|
|
||||||
|
// Encode the message
|
||||||
|
const encoded = encode([message.data], {
|
||||||
|
maxDataLength: MAX_SIZE,
|
||||||
|
lengthEncoder: length4ByteEncoder,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of encoded) {
|
||||||
|
yield chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove message after successful sending
|
||||||
|
this.writeRetries = 0;
|
||||||
|
const sentMessage = this.messageQueue.shift();
|
||||||
|
if (sentMessage)
|
||||||
|
sentMessage.resolve();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error encoding or sending message:", err);
|
||||||
|
const failedMessage = this.messageQueue.shift();
|
||||||
|
if (failedMessage)
|
||||||
|
failedMessage.reject(new Error(`Failed to send message: ${err}`));
|
||||||
|
} finally {
|
||||||
|
this.writeLock = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No messages to send, wait for a short period
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
await pipe(messageSource(), this.stream.sink).catch((err) => {
|
||||||
|
console.error("Sink error:", err);
|
||||||
|
this.isWriting = false;
|
||||||
|
this.writeRetries++;
|
||||||
|
|
||||||
|
// Try to restart if not closed
|
||||||
|
if (!this.closed && this.writeRetries < this.MAX_RETRIES) {
|
||||||
|
setTimeout(() => this.startWriting(), 1000);
|
||||||
|
} else if (this.writeRetries >= this.MAX_RETRIES) {
|
||||||
|
console.error("Max retries reached for writing to stream sink, stopping attempts");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Stream writing error:", err);
|
||||||
|
this.isWriting = false;
|
||||||
|
this.writeRetries++;
|
||||||
|
|
||||||
|
// Try to restart if not closed
|
||||||
|
if (!this.closed && this.writeRetries < this.MAX_RETRIES) {
|
||||||
|
setTimeout(() => this.startWriting(), 1000);
|
||||||
|
} else if (this.writeRetries >= this.MAX_RETRIES) {
|
||||||
|
console.error("Max retries reached for writing stream, stopping attempts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async writeMessage(message: Uint8Array): Promise<void> {
|
||||||
|
if (this.closed) {
|
||||||
|
throw new Error("Cannot write to closed stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate message size before queuing
|
||||||
|
if (message.length > MAX_SIZE) {
|
||||||
|
throw new Error("Message size exceeds maximum size limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the message queue is too large
|
||||||
|
if (this.messageQueue.length >= MAX_QUEUE_SIZE) {
|
||||||
|
throw new Error("Message queue is full, cannot write message");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a promise to resolve when the message is sent
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.messageQueue.push({ data: message, resolve, reject } as PromiseMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.closed = true;
|
||||||
|
this.callbacks.clear();
|
||||||
|
// Reject pending messages
|
||||||
|
for (const msg of this.messageQueue)
|
||||||
|
msg.reject(new Error("Stream closed"));
|
||||||
|
|
||||||
|
this.messageQueue = [];
|
||||||
|
this.readRetries = 0;
|
||||||
|
this.writeRetries = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import {
|
import {
|
||||||
MessageBase,
|
NewMessageRaw,
|
||||||
MessageICE,
|
NewMessageSDP,
|
||||||
MessageJoin,
|
NewMessageICE,
|
||||||
MessageSDP,
|
SafeStream,
|
||||||
MessageAnswer,
|
|
||||||
JoinerType,
|
|
||||||
AnswerType,
|
|
||||||
} from "./messages";
|
} from "./messages";
|
||||||
|
import { webSockets } from "@libp2p/websockets";
|
||||||
|
import { createLibp2p, Libp2p } from "libp2p";
|
||||||
|
import { noise } from "@chainsafe/libp2p-noise";
|
||||||
|
import { yamux } from "@chainsafe/libp2p-yamux";
|
||||||
|
import { identify } from "@libp2p/identify";
|
||||||
|
import { multiaddr } from "@multiformats/multiaddr";
|
||||||
|
import { Connection } from "@libp2p/interface";
|
||||||
|
import { ping } from "@libp2p/ping";
|
||||||
|
|
||||||
//FIXME: Sometimes the room will wait to say offline, then appear to be online after retrying :D
|
//FIXME: Sometimes the room will wait to say offline, then appear to be online after retrying :D
|
||||||
// This works for me, with my trashy internet, does it work for you as well?
|
// This works for me, with my trashy internet, does it work for you as well?
|
||||||
|
|
||||||
|
const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0";
|
||||||
|
|
||||||
export class WebRTCStream {
|
export class WebRTCStream {
|
||||||
private _ws: WebSocket | undefined = undefined;
|
private _p2p: Libp2p | undefined = undefined;
|
||||||
|
private _p2pConn: Connection | undefined = undefined;
|
||||||
|
private _p2pSafeStream: SafeStream | undefined = undefined;
|
||||||
private _pc: RTCPeerConnection | undefined = undefined;
|
private _pc: RTCPeerConnection | undefined = undefined;
|
||||||
private _audioTrack: MediaStreamTrack | undefined = undefined;
|
private _audioTrack: MediaStreamTrack | undefined = undefined;
|
||||||
private _videoTrack: MediaStreamTrack | undefined = undefined;
|
private _videoTrack: MediaStreamTrack | undefined = undefined;
|
||||||
@@ -24,7 +33,11 @@ export class WebRTCStream {
|
|||||||
private _isConnected: boolean = false; // Add flag to track connection state
|
private _isConnected: boolean = false; // Add flag to track connection state
|
||||||
currentFrameRate: number = 60;
|
currentFrameRate: number = 60;
|
||||||
|
|
||||||
constructor(serverURL: string, roomName: string, connectedCallback: (stream: MediaStream | null) => void) {
|
constructor(
|
||||||
|
serverURL: string,
|
||||||
|
roomName: string,
|
||||||
|
connectedCallback: (stream: MediaStream | null) => void,
|
||||||
|
) {
|
||||||
if (roomName.length <= 0) {
|
if (roomName.length <= 0) {
|
||||||
console.error("Room name not provided");
|
console.error("Room name not provided");
|
||||||
return;
|
return;
|
||||||
@@ -33,120 +46,114 @@ export class WebRTCStream {
|
|||||||
this._onConnected = connectedCallback;
|
this._onConnected = connectedCallback;
|
||||||
this._serverURL = serverURL;
|
this._serverURL = serverURL;
|
||||||
this._roomName = roomName;
|
this._roomName = roomName;
|
||||||
this._setup(serverURL, roomName);
|
this._setup(serverURL, roomName).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setup(serverURL: string, roomName: string) {
|
private async _setup(serverURL: string, roomName: string) {
|
||||||
// Don't setup new connection if already connected
|
// Don't setup new connection if already connected
|
||||||
if (this._isConnected) {
|
if (this._isConnected) {
|
||||||
console.log("Already connected, skipping setup");
|
console.log("Already connected, skipping setup");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Setting up WebSocket");
|
console.log("Setting up libp2p");
|
||||||
const wsURL = serverURL.replace(/^http/, "ws");
|
|
||||||
this._ws = new WebSocket(`${wsURL}/api/ws/${roomName}`);
|
|
||||||
this._ws.onopen = async () => {
|
|
||||||
console.log("WebSocket opened");
|
|
||||||
// Send join message
|
|
||||||
const joinMessage: MessageJoin = {
|
|
||||||
payload_type: "join",
|
|
||||||
joiner_type: JoinerType.JoinerClient
|
|
||||||
};
|
|
||||||
this._ws!.send(JSON.stringify(joinMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
let iceHolder: RTCIceCandidateInit[] = [];
|
this._p2p = await createLibp2p({
|
||||||
|
transports: [webSockets()],
|
||||||
|
connectionEncrypters: [noise()],
|
||||||
|
streamMuxers: [yamux()],
|
||||||
|
connectionGater: {
|
||||||
|
denyDialMultiaddr: () => {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
identify: identify(),
|
||||||
|
ping: ping(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this._ws.onmessage = async (e) => {
|
this._p2p.addEventListener("peer:connect", async (e) => {
|
||||||
// allow only JSON
|
console.debug("Peer connected:", e.detail);
|
||||||
if (typeof e.data === "object") return;
|
});
|
||||||
if (!e.data) return;
|
this._p2p.addEventListener("peer:disconnect", (e) => {
|
||||||
const message = JSON.parse(e.data) as MessageBase;
|
console.debug("Peer disconnected:", e.detail);
|
||||||
switch (message.payload_type) {
|
});
|
||||||
case "sdp":
|
|
||||||
|
const ma = multiaddr(serverURL);
|
||||||
|
console.debug("Dialing peer at:", ma.toString());
|
||||||
|
this._p2pConn = await this._p2p.dial(ma);
|
||||||
|
|
||||||
|
if (this._p2pConn) {
|
||||||
|
console.log("Stream is being established");
|
||||||
|
let stream = await this._p2pConn
|
||||||
|
.newStream(NESTRI_PROTOCOL_STREAM_REQUEST)
|
||||||
|
.catch(console.error);
|
||||||
|
if (stream) {
|
||||||
|
this._p2pSafeStream = new SafeStream(stream);
|
||||||
|
console.log("Stream opened with peer");
|
||||||
|
|
||||||
|
let iceHolder: RTCIceCandidateInit[] = [];
|
||||||
|
this._p2pSafeStream.registerCallback("ice-candidate", (data) => {
|
||||||
|
if (this._pc) {
|
||||||
|
if (this._pc.remoteDescription) {
|
||||||
|
this._pc.addIceCandidate(data.candidate).catch((err) => {
|
||||||
|
console.error("Error adding ICE candidate:", err);
|
||||||
|
});
|
||||||
|
// Add held candidates
|
||||||
|
iceHolder.forEach((candidate) => {
|
||||||
|
this._pc!.addIceCandidate(candidate).catch((err) => {
|
||||||
|
console.error("Error adding held ICE candidate:", err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
iceHolder = [];
|
||||||
|
} else {
|
||||||
|
iceHolder.push(data.candidate);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
iceHolder.push(data.candidate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._p2pSafeStream.registerCallback("offer", async (data) => {
|
||||||
if (!this._pc) {
|
if (!this._pc) {
|
||||||
// Setup peer connection now
|
// Setup peer connection now
|
||||||
this._setupPeerConnection();
|
this._setupPeerConnection();
|
||||||
}
|
}
|
||||||
console.log("Received SDP: ", (message as MessageSDP).sdp);
|
await this._pc!.setRemoteDescription(data.sdp);
|
||||||
await this._pc!.setRemoteDescription((message as MessageSDP).sdp);
|
|
||||||
// Create our answer
|
// Create our answer
|
||||||
const answer = await this._pc!.createAnswer();
|
const answer = await this._pc!.createAnswer();
|
||||||
// Force stereo in Chromium browsers
|
// Force stereo in Chromium browsers
|
||||||
answer.sdp = this.forceOpusStereo(answer.sdp!);
|
answer.sdp = this.forceOpusStereo(answer.sdp!);
|
||||||
await this._pc!.setLocalDescription(answer);
|
await this._pc!.setLocalDescription(answer);
|
||||||
this._ws!.send(JSON.stringify({
|
// Send answer back
|
||||||
payload_type: "sdp",
|
const answerMsg = NewMessageSDP("answer", answer);
|
||||||
sdp: answer
|
await this._p2pSafeStream?.writeMessage(answerMsg);
|
||||||
}));
|
});
|
||||||
break;
|
|
||||||
case "ice":
|
|
||||||
if (!this._pc) break;
|
|
||||||
if (this._pc.remoteDescription) {
|
|
||||||
try {
|
|
||||||
await this._pc.addIceCandidate((message as MessageICE).candidate);
|
|
||||||
// Add held ICE candidates
|
|
||||||
for (const ice of iceHolder) {
|
|
||||||
try {
|
|
||||||
await this._pc.addIceCandidate(ice);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error adding held ICE candidate: ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
iceHolder = [];
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error adding ICE candidate: ", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
iceHolder.push((message as MessageICE).candidate);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "answer":
|
|
||||||
switch ((message as MessageAnswer).answer_type) {
|
|
||||||
case AnswerType.AnswerOffline:
|
|
||||||
console.log("Room is offline");
|
|
||||||
// Call callback with null stream
|
|
||||||
if (this._onConnected)
|
|
||||||
this._onConnected(null);
|
|
||||||
|
|
||||||
break;
|
this._p2pSafeStream.registerCallback("request-stream-offline", (data) => {
|
||||||
case AnswerType.AnswerInUse:
|
console.warn("Stream is offline for room:", data.roomName);
|
||||||
console.warn("Room is in use, we shouldn't even be getting this message");
|
this._onConnected?.(null);
|
||||||
break;
|
});
|
||||||
case AnswerType.AnswerOK:
|
|
||||||
console.log("Joining Room was successful");
|
// Send stream request
|
||||||
break;
|
// marshal room name into json
|
||||||
}
|
const request = NewMessageRaw(
|
||||||
break;
|
"request-stream-room",
|
||||||
default:
|
roomName,
|
||||||
console.error("Unknown message type: ", message);
|
);
|
||||||
|
await this._p2pSafeStream.writeMessage(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._ws.onclose = () => {
|
|
||||||
console.log("WebSocket closed, reconnecting in 3 seconds");
|
|
||||||
if (this._onConnected)
|
|
||||||
this._onConnected(null);
|
|
||||||
|
|
||||||
// Clear PeerConnection
|
|
||||||
this._cleanupPeerConnection()
|
|
||||||
|
|
||||||
this._handleConnectionFailure()
|
|
||||||
// setTimeout(() => {
|
|
||||||
// this._setup(serverURL, roomName);
|
|
||||||
// }, this._connectionTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._ws.onerror = (e) => {
|
|
||||||
console.error("WebSocket error: ", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forces opus to stereo in Chromium browsers, because of course
|
// Forces opus to stereo in Chromium browsers, because of course
|
||||||
private forceOpusStereo(SDP: string): string {
|
private forceOpusStereo(SDP: string): string {
|
||||||
// Look for "minptime=10;useinbandfec=1" and replace with "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1;"
|
// Look for "minptime=10;useinbandfec=1" and replace with "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1;"
|
||||||
return SDP.replace(/(minptime=10;useinbandfec=1)/, "$1;stereo=1;sprop-stereo=1;");
|
return SDP.replace(
|
||||||
|
/(minptime=10;useinbandfec=1)/,
|
||||||
|
"$1;stereo=1;sprop-stereo=1;",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setupPeerConnection() {
|
private _setupPeerConnection() {
|
||||||
@@ -158,43 +165,50 @@ export class WebRTCStream {
|
|||||||
this._pc = new RTCPeerConnection({
|
this._pc = new RTCPeerConnection({
|
||||||
iceServers: [
|
iceServers: [
|
||||||
{
|
{
|
||||||
urls: "stun:stun.l.google.com:19302"
|
urls: "stun:stun.l.google.com:19302",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
this._pc.ontrack = (e) => {
|
this._pc.ontrack = (e) => {
|
||||||
console.log("Track received: ", e.track);
|
console.debug("Track received: ", e.track);
|
||||||
if (e.track.kind === "audio")
|
if (e.track.kind === "audio") this._audioTrack = e.track;
|
||||||
this._audioTrack = e.track;
|
else if (e.track.kind === "video") this._videoTrack = e.track;
|
||||||
else if (e.track.kind === "video")
|
|
||||||
this._videoTrack = e.track;
|
|
||||||
|
|
||||||
this._checkConnectionState();
|
this._checkConnectionState();
|
||||||
};
|
};
|
||||||
|
|
||||||
this._pc.onconnectionstatechange = () => {
|
this._pc.onconnectionstatechange = () => {
|
||||||
console.log("Connection state changed to: ", this._pc!.connectionState);
|
console.debug("Connection state changed to: ", this._pc!.connectionState);
|
||||||
this._checkConnectionState();
|
this._checkConnectionState();
|
||||||
};
|
};
|
||||||
|
|
||||||
this._pc.oniceconnectionstatechange = () => {
|
this._pc.oniceconnectionstatechange = () => {
|
||||||
console.log("ICE connection state changed to: ", this._pc!.iceConnectionState);
|
console.debug(
|
||||||
|
"ICE connection state changed to: ",
|
||||||
|
this._pc!.iceConnectionState,
|
||||||
|
);
|
||||||
this._checkConnectionState();
|
this._checkConnectionState();
|
||||||
};
|
};
|
||||||
|
|
||||||
this._pc.onicegatheringstatechange = () => {
|
this._pc.onicegatheringstatechange = () => {
|
||||||
console.log("ICE gathering state changed to: ", this._pc!.iceGatheringState);
|
console.debug(
|
||||||
|
"ICE gathering state changed to: ",
|
||||||
|
this._pc!.iceGatheringState,
|
||||||
|
);
|
||||||
this._checkConnectionState();
|
this._checkConnectionState();
|
||||||
};
|
};
|
||||||
|
|
||||||
this._pc.onicecandidate = (e) => {
|
this._pc.onicecandidate = (e) => {
|
||||||
if (e.candidate) {
|
if (e.candidate) {
|
||||||
const message: MessageICE = {
|
const iceMsg = NewMessageICE("ice-candidate", e.candidate);
|
||||||
payload_type: "ice",
|
if (this._p2pSafeStream) {
|
||||||
candidate: e.candidate
|
this._p2pSafeStream.writeMessage(iceMsg).catch((err) =>
|
||||||
};
|
console.error("Error sending ICE candidate:", err),
|
||||||
this._ws!.send(JSON.stringify(message));
|
);
|
||||||
|
} else {
|
||||||
|
console.warn("P2P stream not established, cannot send ICE candidate");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -207,26 +221,35 @@ export class WebRTCStream {
|
|||||||
private _checkConnectionState() {
|
private _checkConnectionState() {
|
||||||
if (!this._pc) return;
|
if (!this._pc) return;
|
||||||
|
|
||||||
console.log("Checking connection state:", {
|
console.debug("Checking connection state:", {
|
||||||
connectionState: this._pc.connectionState,
|
connectionState: this._pc.connectionState,
|
||||||
iceConnectionState: this._pc.iceConnectionState,
|
iceConnectionState: this._pc.iceConnectionState,
|
||||||
hasAudioTrack: !!this._audioTrack,
|
hasAudioTrack: !!this._audioTrack,
|
||||||
hasVideoTrack: !!this._videoTrack,
|
hasVideoTrack: !!this._videoTrack,
|
||||||
isConnected: this._isConnected
|
isConnected: this._isConnected,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this._pc.connectionState === "connected" && this._audioTrack !== undefined && this._videoTrack !== undefined) {
|
if (
|
||||||
|
this._pc.connectionState === "connected" &&
|
||||||
|
this._audioTrack !== undefined &&
|
||||||
|
this._videoTrack !== undefined
|
||||||
|
) {
|
||||||
this._clearConnectionTimer();
|
this._clearConnectionTimer();
|
||||||
if (!this._isConnected) {
|
if (!this._isConnected) {
|
||||||
// Only trigger callback if not already connected
|
// Only trigger callback if not already connected
|
||||||
this._isConnected = true;
|
this._isConnected = true;
|
||||||
if (this._onConnected !== undefined) {
|
if (this._onConnected !== undefined) {
|
||||||
this._onConnected(new MediaStream([this._audioTrack, this._videoTrack]));
|
this._onConnected(
|
||||||
|
new MediaStream([this._audioTrack, this._videoTrack]),
|
||||||
|
);
|
||||||
|
|
||||||
// Continuously set low-latency target
|
// Continuously set low-latency target
|
||||||
this._pc.getReceivers().forEach((receiver: RTCRtpReceiver) => {
|
this._pc.getReceivers().forEach((receiver: RTCRtpReceiver) => {
|
||||||
let intervalLoop = setInterval(async () => {
|
let intervalLoop = setInterval(async () => {
|
||||||
if (receiver.track.readyState !== "live" || (receiver.transport && receiver.transport.state !== "connected")) {
|
if (
|
||||||
|
receiver.track.readyState !== "live" ||
|
||||||
|
(receiver.transport && receiver.transport.state !== "connected")
|
||||||
|
) {
|
||||||
clearInterval(intervalLoop);
|
clearInterval(intervalLoop);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -239,9 +262,11 @@ export class WebRTCStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._gatherFrameRate();
|
this._gatherFrameRate();
|
||||||
} else if (this._pc.connectionState === "failed" ||
|
} else if (
|
||||||
|
this._pc.connectionState === "failed" ||
|
||||||
this._pc.connectionState === "closed" ||
|
this._pc.connectionState === "closed" ||
|
||||||
this._pc.iceConnectionState === "failed") {
|
this._pc.iceConnectionState === "failed"
|
||||||
|
) {
|
||||||
console.log("Connection failed or closed, attempting reconnect");
|
console.log("Connection failed or closed, attempting reconnect");
|
||||||
this._isConnected = false; // Reset connected state
|
this._isConnected = false; // Reset connected state
|
||||||
this._handleConnectionFailure();
|
this._handleConnectionFailure();
|
||||||
@@ -250,7 +275,8 @@ export class WebRTCStream {
|
|||||||
|
|
||||||
private _handleConnectionFailure() {
|
private _handleConnectionFailure() {
|
||||||
this._clearConnectionTimer();
|
this._clearConnectionTimer();
|
||||||
if (this._isConnected) { // Only notify if previously connected
|
if (this._isConnected) {
|
||||||
|
// Only notify if previously connected
|
||||||
this._isConnected = false;
|
this._isConnected = false;
|
||||||
if (this._onConnected) {
|
if (this._onConnected) {
|
||||||
this._onConnected(null);
|
this._onConnected(null);
|
||||||
@@ -260,7 +286,7 @@ export class WebRTCStream {
|
|||||||
|
|
||||||
// Attempt to reconnect only if not already connected
|
// Attempt to reconnect only if not already connected
|
||||||
if (!this._isConnected && this._serverURL && this._roomName) {
|
if (!this._isConnected && this._serverURL && this._roomName) {
|
||||||
this._setup(this._serverURL, this._roomName);
|
this._setup(this._serverURL, this._roomName).catch((err) => console.error("Reconnection failed:", err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,10 +302,8 @@ export class WebRTCStream {
|
|||||||
|
|
||||||
if (this._audioTrack || this._videoTrack) {
|
if (this._audioTrack || this._videoTrack) {
|
||||||
try {
|
try {
|
||||||
if (this._audioTrack)
|
if (this._audioTrack) this._audioTrack.stop();
|
||||||
this._audioTrack.stop();
|
if (this._videoTrack) this._videoTrack.stop();
|
||||||
if (this._videoTrack)
|
|
||||||
this._videoTrack.stop();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error stopping media tracks:", err);
|
console.error("Error stopping media tracks:", err);
|
||||||
}
|
}
|
||||||
@@ -308,16 +332,18 @@ export class WebRTCStream {
|
|||||||
private _setupDataChannelEvents() {
|
private _setupDataChannelEvents() {
|
||||||
if (!this._dataChannel) return;
|
if (!this._dataChannel) return;
|
||||||
|
|
||||||
this._dataChannel.onclose = () => console.log('sendChannel has closed')
|
this._dataChannel.onclose = () => console.log("sendChannel has closed");
|
||||||
this._dataChannel.onopen = () => console.log('sendChannel has opened')
|
this._dataChannel.onopen = () => console.log("sendChannel has opened");
|
||||||
this._dataChannel.onmessage = e => console.log(`Message from DataChannel '${this._dataChannel?.label}' payload '${e.data}'`)
|
this._dataChannel.onmessage = (e) =>
|
||||||
|
console.log(
|
||||||
|
`Message from DataChannel '${this._dataChannel?.label}' payload '${e.data}'`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _gatherFrameRate() {
|
private _gatherFrameRate() {
|
||||||
if (this._pc === undefined || this._videoTrack === undefined)
|
if (this._pc === undefined || this._videoTrack === undefined) return;
|
||||||
return;
|
|
||||||
|
|
||||||
const videoInfoPromise = new Promise<{ fps: number}>((resolve) => {
|
const videoInfoPromise = new Promise<{ fps: number }>((resolve) => {
|
||||||
// Keep trying to get fps until it's found
|
// Keep trying to get fps until it's found
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
if (this._pc === undefined) {
|
if (this._pc === undefined) {
|
||||||
@@ -329,7 +355,7 @@ export class WebRTCStream {
|
|||||||
stats.forEach((report) => {
|
stats.forEach((report) => {
|
||||||
if (report.type === "inbound-rtp") {
|
if (report.type === "inbound-rtp") {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|
||||||
resolve({ fps: report.framesPerSecond });
|
resolve({ fps: report.framesPerSecond });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -337,25 +363,26 @@ export class WebRTCStream {
|
|||||||
});
|
});
|
||||||
|
|
||||||
videoInfoPromise.then((value) => {
|
videoInfoPromise.then((value) => {
|
||||||
this.currentFrameRate = value.fps
|
this.currentFrameRate = value.fps;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send binary message through the data channel
|
// Send binary message through the data channel
|
||||||
public sendBinary(data: Uint8Array) {
|
public sendBinary(data: Uint8Array) {
|
||||||
if (this._dataChannel && this._dataChannel.readyState === "open")
|
if (this._dataChannel && this._dataChannel.readyState === "open")
|
||||||
this._dataChannel.send(data);
|
this._dataChannel.send(data);
|
||||||
else
|
else console.log("Data channel not open or not established.");
|
||||||
console.log("Data channel not open or not established.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnect() {
|
public disconnect() {
|
||||||
this._clearConnectionTimer();
|
this._clearConnectionTimer();
|
||||||
this._cleanupPeerConnection();
|
this._cleanupPeerConnection();
|
||||||
if (this._ws) {
|
if (this._p2pConn) {
|
||||||
this._ws.close();
|
this._p2pConn
|
||||||
this._ws = undefined;
|
.close()
|
||||||
|
.catch((err) => console.error("Error closing P2P connection:", err));
|
||||||
|
this._p2pConn = undefined;
|
||||||
}
|
}
|
||||||
this._isConnected = false;
|
this._isConnected = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ type Resource struct {
|
|||||||
Auth struct {
|
Auth struct {
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
}
|
}
|
||||||
AuthFingerprintKey struct {
|
/*AuthFingerprintKey struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}*/
|
||||||
Realtime struct {
|
Realtime struct {
|
||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint"`
|
||||||
Authorizer string `json:"authorizer"`
|
Authorizer string `json:"authorizer"`
|
||||||
|
|||||||
1
packages/relay/.gitignore
vendored
Normal file
1
packages/relay/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
persist-data/
|
||||||
@@ -3,16 +3,15 @@ module relay
|
|||||||
go 1.24
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/websocket v1.5.3
|
|
||||||
github.com/libp2p/go-libp2p v0.41.1
|
github.com/libp2p/go-libp2p v0.41.1
|
||||||
github.com/libp2p/go-libp2p-pubsub v0.13.1
|
github.com/libp2p/go-libp2p-pubsub v0.13.1
|
||||||
github.com/libp2p/go-reuseport v0.4.0
|
github.com/libp2p/go-reuseport v0.4.0
|
||||||
github.com/multiformats/go-multiaddr v0.15.0
|
github.com/multiformats/go-multiaddr v0.15.0
|
||||||
github.com/oklog/ulid/v2 v2.1.0
|
github.com/oklog/ulid/v2 v2.1.1
|
||||||
github.com/pion/ice/v4 v4.0.9
|
github.com/pion/ice/v4 v4.0.10
|
||||||
github.com/pion/interceptor v0.1.37
|
github.com/pion/interceptor v0.1.38
|
||||||
github.com/pion/rtp v1.8.13
|
github.com/pion/rtp v1.8.15
|
||||||
github.com/pion/webrtc/v4 v4.0.14
|
github.com/pion/webrtc/v4 v4.1.1
|
||||||
google.golang.org/protobuf v1.36.6
|
google.golang.org/protobuf v1.36.6
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,32 +25,35 @@ require (
|
|||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/elastic/gosigar v0.14.3 // indirect
|
github.com/elastic/gosigar v0.14.3 // indirect
|
||||||
|
github.com/filecoin-project/go-clock v0.1.0 // indirect
|
||||||
github.com/flynn/noise v1.1.0 // indirect
|
github.com/flynn/noise v1.1.0 // indirect
|
||||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/google/gopacket v1.1.19 // indirect
|
github.com/google/gopacket v1.1.19 // indirect
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/huin/goupnp v1.3.0 // indirect
|
github.com/huin/goupnp v1.3.0 // indirect
|
||||||
github.com/ipfs/go-cid v0.5.0 // indirect
|
github.com/ipfs/go-cid v0.5.0 // indirect
|
||||||
github.com/ipfs/go-log/v2 v2.5.1 // indirect
|
github.com/ipfs/go-log/v2 v2.6.0 // indirect
|
||||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/koron/go-ssdp v0.0.5 // indirect
|
github.com/koron/go-ssdp v0.0.6 // indirect
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||||
github.com/libp2p/go-flow-metrics v0.2.0 // indirect
|
github.com/libp2p/go-flow-metrics v0.3.0 // indirect
|
||||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
|
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
|
||||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||||
github.com/libp2p/go-netroute v0.2.2 // indirect
|
github.com/libp2p/go-netroute v0.2.2 // indirect
|
||||||
github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
|
github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
|
||||||
|
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
|
||||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/miekg/dns v1.1.64 // indirect
|
github.com/miekg/dns v1.1.66 // indirect
|
||||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
@@ -66,7 +68,7 @@ require (
|
|||||||
github.com/multiformats/go-multistream v0.6.0 // indirect
|
github.com/multiformats/go-multistream v0.6.0 // indirect
|
||||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.23.3 // indirect
|
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||||
github.com/pion/datachannel v1.5.10 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
@@ -76,37 +78,39 @@ require (
|
|||||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.15 // indirect
|
github.com/pion/rtcp v1.2.15 // indirect
|
||||||
github.com/pion/sctp v1.8.37 // indirect
|
github.com/pion/sctp v1.8.39 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.11 // indirect
|
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||||
github.com/pion/stun v0.6.1 // indirect
|
github.com/pion/stun v0.6.1 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.63.0 // indirect
|
github.com/prometheus/common v0.64.0 // indirect
|
||||||
github.com/prometheus/procfs v0.16.0 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.50.1 // indirect
|
github.com/quic-go/quic-go v0.52.0 // indirect
|
||||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
|
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
|
||||||
github.com/raulk/go-watchdog v1.3.0 // indirect
|
github.com/raulk/go-watchdog v1.3.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
go.uber.org/dig v1.18.1 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
go.uber.org/fx v1.23.0 // indirect
|
go.uber.org/dig v1.19.0 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
go.uber.org/fx v1.24.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.2 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/net v0.38.0 // indirect
|
golang.org/x/net v0.40.0 // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
golang.org/x/tools v0.31.0 // indirect
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
lukechampine.com/blake3 v1.4.0 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D
|
|||||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
|
||||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
@@ -47,6 +46,8 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
|
|||||||
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||||
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
|
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
|
||||||
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||||
|
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
|
||||||
|
github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs=
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
||||||
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||||
@@ -85,8 +86,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
|
|||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||||
@@ -102,8 +103,8 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
|||||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||||
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
|
github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg=
|
||||||
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
|
github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8=
|
||||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||||
@@ -118,8 +119,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
|
|||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk=
|
github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU=
|
||||||
github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w=
|
github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -131,8 +132,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||||
github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw=
|
github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784=
|
||||||
github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc=
|
github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo=
|
||||||
github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE=
|
github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE=
|
||||||
github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI=
|
github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI=
|
||||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
|
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
|
||||||
@@ -149,17 +150,19 @@ github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQsc
|
|||||||
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
||||||
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
|
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
|
||||||
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
|
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
|
||||||
|
github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
|
||||||
|
github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
|
||||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
|
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
|
||||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
|
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||||
github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ=
|
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||||
github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||||
|
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
|
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
|
||||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
|
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
|
||||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
|
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
|
||||||
@@ -201,12 +204,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||||
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
|
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||||
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
|
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
|
||||||
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||||
@@ -221,10 +224,10 @@ github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
|||||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||||
github.com/pion/ice/v4 v4.0.9 h1:VKgU4MwA2LUDVLq+WBkpEHTcAb8c5iCvFMECeuPOZNk=
|
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||||
github.com/pion/ice/v4 v4.0.9/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
github.com/pion/interceptor v0.1.38 h1:Mgt3XIIq47uR5vcLLahfRucE6tFPjxHak+z5ZZFEzLU=
|
||||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
github.com/pion/interceptor v0.1.38/go.mod h1:HS9X+Ue5LDE6q2C2tuvOuO83XkBdJFgn6MBDtfoJX4Q=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||||
@@ -234,12 +237,12 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
|||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||||
github.com/pion/rtp v1.8.13 h1:8uSUPpjSL4OlwZI8Ygqu7+h2p9NPFB+yAZ461Xn5sNg=
|
github.com/pion/rtp v1.8.15 h1:MuhuGn1cxpVCPLNY1lI7F1tQ8Spntpgf12ob+pOYT8s=
|
||||||
github.com/pion/rtp v1.8.13/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
github.com/pion/rtp v1.8.15/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||||
github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs=
|
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||||
github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||||
github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI=
|
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||||
github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
||||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
||||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||||
@@ -252,37 +255,39 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp
|
|||||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||||
github.com/pion/webrtc/v4 v4.0.14 h1:nyds/sFRR+HvmWoBa6wrL46sSfpArE0qR883MBW96lg=
|
github.com/pion/webrtc/v4 v4.1.1 h1:PMFPtLg1kpD2pVtun+LGUzA3k54JdFl87WO0Z1+HKug=
|
||||||
github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnCXB821Kk=
|
github.com/pion/webrtc/v4 v4.1.1/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
|
||||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
|
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
|
||||||
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
|
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
|
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
|
||||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
|
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
|
||||||
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
||||||
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
@@ -318,9 +323,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
@@ -336,23 +339,20 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
|||||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/dig v1.18.1 h1:rLww6NuajVjeQn+49u5NcezUJEGwd5uXmyoCKW2g5Es=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/dig v1.18.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||||
go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
|
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||||
go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
|
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
|
||||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||||
@@ -369,20 +369,18 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
@@ -394,7 +392,6 @@ golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73r
|
|||||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -402,15 +399,15 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
@@ -426,8 +423,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -440,10 +437,10 @@ golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -452,8 +449,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -464,33 +461,30 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
|||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -520,16 +514,14 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w=
|
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||||
lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0=
|
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/libp2p/go-reuseport"
|
"github.com/libp2p/go-reuseport"
|
||||||
"github.com/pion/ice/v4"
|
"github.com/pion/ice/v4"
|
||||||
"github.com/pion/interceptor"
|
"github.com/pion/interceptor"
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
"log/slog"
|
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalWebRTCAPI *webrtc.API
|
var globalWebRTCAPI *webrtc.API
|
||||||
@@ -24,17 +25,9 @@ func InitWebRTCAPI() error {
|
|||||||
// Media engine
|
// Media engine
|
||||||
mediaEngine := &webrtc.MediaEngine{}
|
mediaEngine := &webrtc.MediaEngine{}
|
||||||
|
|
||||||
// Register additional header extensions to reduce latency
|
// Register our extensions
|
||||||
// Playout Delay
|
if err := RegisterExtensions(mediaEngine); err != nil {
|
||||||
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
return fmt.Errorf("failed to register extensions: %w", err)
|
||||||
URI: ExtensionPlayoutDelay,
|
|
||||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
|
||||||
URI: ExtensionPlayoutDelay,
|
|
||||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default codecs cover most of our needs
|
// Default codecs cover most of our needs
|
||||||
@@ -75,9 +68,10 @@ func InitWebRTCAPI() error {
|
|||||||
// New in v4, reduces CPU usage and latency when enabled
|
// New in v4, reduces CPU usage and latency when enabled
|
||||||
settingEngine.EnableSCTPZeroChecksum(true)
|
settingEngine.EnableSCTPZeroChecksum(true)
|
||||||
|
|
||||||
nat11IPs := GetFlags().NAT11IPs
|
nat11IP := GetFlags().NAT11IP
|
||||||
if len(nat11IPs) > 0 {
|
if len(nat11IP) > 0 {
|
||||||
settingEngine.SetNAT1To1IPs(nat11IPs, webrtc.ICECandidateTypeHost)
|
settingEngine.SetNAT1To1IPs([]string{nat11IP}, webrtc.ICECandidateTypeSrflx)
|
||||||
|
slog.Info("Using NAT 1:1 IP for WebRTC", "nat11_ip", nat11IP)
|
||||||
}
|
}
|
||||||
|
|
||||||
muxPort := GetFlags().UDPMuxPort
|
muxPort := GetFlags().UDPMuxPort
|
||||||
@@ -85,7 +79,7 @@ func InitWebRTCAPI() error {
|
|||||||
// Use reuseport to allow multiple listeners on the same port
|
// Use reuseport to allow multiple listeners on the same port
|
||||||
pktListener, err := reuseport.ListenPacket("udp", ":"+strconv.Itoa(muxPort))
|
pktListener, err := reuseport.ListenPacket("udp", ":"+strconv.Itoa(muxPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create UDP listener: %w", err)
|
return fmt.Errorf("failed to create WebRTC muxed UDP listener: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := ice.NewMultiUDPMuxDefault(ice.NewUDPMuxDefault(ice.UDPMuxParams{
|
mux := ice.NewMultiUDPMuxDefault(ice.NewUDPMuxDefault(ice.UDPMuxParams{
|
||||||
@@ -95,10 +89,13 @@ func InitWebRTCAPI() error {
|
|||||||
settingEngine.SetICEUDPMux(mux)
|
settingEngine.SetICEUDPMux(mux)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the UDP port range used by WebRTC
|
if flags.WebRTCUDPStart > 0 && flags.WebRTCUDPEnd > 0 && flags.WebRTCUDPStart < flags.WebRTCUDPEnd {
|
||||||
err = settingEngine.SetEphemeralUDPPortRange(uint16(flags.WebRTCUDPStart), uint16(flags.WebRTCUDPEnd))
|
// Set the UDP port range used by WebRTC
|
||||||
if err != nil {
|
err = settingEngine.SetEphemeralUDPPortRange(uint16(flags.WebRTCUDPStart), uint16(flags.WebRTCUDPEnd))
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Info("Using WebRTC UDP Port Range", "start", flags.WebRTCUDPStart, "end", flags.WebRTCUDPEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
settingEngine.SetIncludeLoopbackCandidate(true) // Just in case
|
settingEngine.SetIncludeLoopbackCandidate(true) // Just in case
|
||||||
@@ -109,11 +106,6 @@ func InitWebRTCAPI() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebRTCAPI returns the global WebRTC API
|
|
||||||
func GetWebRTCAPI() *webrtc.API {
|
|
||||||
return globalWebRTCAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatePeerConnection sets up a new peer connection
|
// CreatePeerConnection sets up a new peer connection
|
||||||
func CreatePeerConnection(onClose func()) (*webrtc.PeerConnection, error) {
|
func CreatePeerConnection(onClose func()) (*webrtc.PeerConnection, error) {
|
||||||
pc, err := globalWebRTCAPI.NewPeerConnection(globalWebRTCConfig)
|
pc, err := globalWebRTCAPI.NewPeerConnection(globalWebRTCConfig)
|
||||||
|
|||||||
@@ -1,19 +1,51 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"errors"
|
||||||
"github.com/oklog/ulid/v2"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewULID() (ulid.ULID, error) {
|
func NewULID() (ulid.ULID, error) {
|
||||||
return ulid.New(ulid.Timestamp(time.Now()), ulid.Monotonic(rand.Reader, 0))
|
return ulid.New(ulid.Timestamp(time.Now()), ulid.Monotonic(rand.Reader, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to generate PSK from token
|
// GenerateED25519Key generates a new ED25519 key
|
||||||
func GeneratePSKFromToken(token string) ([]byte, error) {
|
func GenerateED25519Key() (ed25519.PrivateKey, error) {
|
||||||
// Simple hash-based PSK generation (32 bytes for libp2p)
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
hash := sha256.Sum256([]byte(token))
|
if err != nil {
|
||||||
return hash[:], nil
|
return nil, fmt.Errorf("failed to generate ED25519 key pair: %w", err)
|
||||||
|
}
|
||||||
|
return priv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveED25519Key saves an ED25519 private key to a path as a binary file
|
||||||
|
func SaveED25519Key(privateKey ed25519.PrivateKey, filePath string) error {
|
||||||
|
if privateKey == nil {
|
||||||
|
return errors.New("private key cannot be nil")
|
||||||
|
}
|
||||||
|
if len(privateKey) != ed25519.PrivateKeySize {
|
||||||
|
return errors.New("private key must be exactly 64 bytes for ED25519")
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filePath, privateKey, 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to save ED25519 key to %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadED25519Key loads an ED25519 private key binary file from a path
|
||||||
|
func LoadED25519Key(filePath string) (ed25519.PrivateKey, error) {
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read ED25519 key from %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
if len(data) != ed25519.PrivateKeySize {
|
||||||
|
return nil, fmt.Errorf("ED25519 key must be exactly %d bytes, got %d", ed25519.PrivateKeySize, len(data))
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,45 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
|
import "github.com/pion/webrtc/v4"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ExtensionPlayoutDelay string = "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"
|
ExtensionPlayoutDelay string = "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExtensionMap maps URIs to their IDs based on registration order
|
// ExtensionMap maps audio/video extension URIs to their IDs based on registration order
|
||||||
// IMPORTANT: This must match the order in which extensions are registered in common.go!
|
var ExtensionMap = map[webrtc.RTPCodecType]map[string]uint8{}
|
||||||
var ExtensionMap = map[string]uint8{
|
|
||||||
ExtensionPlayoutDelay: 1,
|
func RegisterExtensions(mediaEngine *webrtc.MediaEngine) error {
|
||||||
|
// Register additional header extensions to reduce latency
|
||||||
|
// Playout Delay (Video)
|
||||||
|
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
||||||
|
URI: ExtensionPlayoutDelay,
|
||||||
|
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Playout Delay (Audio)
|
||||||
|
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
||||||
|
URI: ExtensionPlayoutDelay,
|
||||||
|
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the extension IDs for both audio and video
|
||||||
|
ExtensionMap[webrtc.RTPCodecTypeAudio] = map[string]uint8{
|
||||||
|
ExtensionPlayoutDelay: 1,
|
||||||
|
}
|
||||||
|
ExtensionMap[webrtc.RTPCodecTypeVideo] = map[string]uint8{
|
||||||
|
ExtensionPlayoutDelay: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetExtension(codecType webrtc.RTPCodecType, extURI string) (uint8, bool) {
|
||||||
|
cType, ok := ExtensionMap[codecType]
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
extID, ok := cType[extURI]
|
||||||
|
return extID, ok
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,47 +2,43 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"github.com/pion/webrtc/v4"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalFlags *Flags
|
var globalFlags *Flags
|
||||||
|
|
||||||
type Flags struct {
|
type Flags struct {
|
||||||
Verbose bool // Log everything to console
|
RegenIdentity bool // Remove old identity on startup and regenerate it
|
||||||
Debug bool // Enable debug mode, implies Verbose
|
Verbose bool // Log everything to console
|
||||||
EndpointPort int // Port for HTTP/S and WS/S endpoint (TCP)
|
Debug bool // Enable debug mode, implies Verbose
|
||||||
MeshPort int // Port for Mesh connections (TCP)
|
EndpointPort int // Port for HTTP/S and WS/S endpoint (TCP)
|
||||||
WebRTCUDPStart int // WebRTC UDP port range start - ignored if UDPMuxPort is set
|
WebRTCUDPStart int // WebRTC UDP port range start - ignored if UDPMuxPort is set
|
||||||
WebRTCUDPEnd int // WebRTC UDP port range end - ignored if UDPMuxPort is set
|
WebRTCUDPEnd int // WebRTC UDP port range end - ignored if UDPMuxPort is set
|
||||||
STUNServer string // WebRTC STUN server
|
STUNServer string // WebRTC STUN server
|
||||||
UDPMuxPort int // WebRTC UDP mux port - if set, overrides UDP port range
|
UDPMuxPort int // WebRTC UDP mux port - if set, overrides UDP port range
|
||||||
AutoAddLocalIP bool // Automatically add local IP to NAT 1 to 1 IPs
|
AutoAddLocalIP bool // Automatically add local IP to NAT 1 to 1 IPs
|
||||||
NAT11IPs []string // WebRTC NAT 1 to 1 IP(s) - allows specifying host IP(s) if behind NAT
|
NAT11IP string // WebRTC NAT 1 to 1 IP - allows specifying IP of relay if behind NAT
|
||||||
TLSCert string // Path to TLS certificate
|
PersistDir string // Directory to save persistent data to
|
||||||
TLSKey string // Path to TLS key
|
|
||||||
ControlSecret string // Shared secret for this relay's control endpoint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (flags *Flags) DebugLog() {
|
func (flags *Flags) DebugLog() {
|
||||||
slog.Info("Relay flags",
|
slog.Debug("Relay flags",
|
||||||
|
"regenIdentity", flags.RegenIdentity,
|
||||||
"verbose", flags.Verbose,
|
"verbose", flags.Verbose,
|
||||||
"debug", flags.Debug,
|
"debug", flags.Debug,
|
||||||
"endpointPort", flags.EndpointPort,
|
"endpointPort", flags.EndpointPort,
|
||||||
"meshPort", flags.MeshPort,
|
|
||||||
"webrtcUDPStart", flags.WebRTCUDPStart,
|
"webrtcUDPStart", flags.WebRTCUDPStart,
|
||||||
"webrtcUDPEnd", flags.WebRTCUDPEnd,
|
"webrtcUDPEnd", flags.WebRTCUDPEnd,
|
||||||
"stunServer", flags.STUNServer,
|
"stunServer", flags.STUNServer,
|
||||||
"webrtcUDPMux", flags.UDPMuxPort,
|
"webrtcUDPMux", flags.UDPMuxPort,
|
||||||
"autoAddLocalIP", flags.AutoAddLocalIP,
|
"autoAddLocalIP", flags.AutoAddLocalIP,
|
||||||
"webrtcNAT11IPs", strings.Join(flags.NAT11IPs, ","),
|
"webrtcNAT11IPs", flags.NAT11IP,
|
||||||
"tlsCert", flags.TLSCert,
|
"persistDir", flags.PersistDir,
|
||||||
"tlsKey", flags.TLSKey,
|
|
||||||
"controlSecret", flags.ControlSecret,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,29 +72,25 @@ func InitFlags() {
|
|||||||
// Create Flags struct
|
// Create Flags struct
|
||||||
globalFlags = &Flags{}
|
globalFlags = &Flags{}
|
||||||
// Get flags
|
// Get flags
|
||||||
|
flag.BoolVar(&globalFlags.RegenIdentity, "regenIdentity", getEnvAsBool("REGEN_IDENTITY", false), "Regenerate identity on startup")
|
||||||
flag.BoolVar(&globalFlags.Verbose, "verbose", getEnvAsBool("VERBOSE", false), "Verbose mode")
|
flag.BoolVar(&globalFlags.Verbose, "verbose", getEnvAsBool("VERBOSE", false), "Verbose mode")
|
||||||
flag.BoolVar(&globalFlags.Debug, "debug", getEnvAsBool("DEBUG", false), "Debug mode")
|
flag.BoolVar(&globalFlags.Debug, "debug", getEnvAsBool("DEBUG", false), "Debug mode")
|
||||||
flag.IntVar(&globalFlags.EndpointPort, "endpointPort", getEnvAsInt("ENDPOINT_PORT", 8088), "HTTP endpoint port")
|
flag.IntVar(&globalFlags.EndpointPort, "endpointPort", getEnvAsInt("ENDPOINT_PORT", 8088), "HTTP endpoint port")
|
||||||
flag.IntVar(&globalFlags.MeshPort, "meshPort", getEnvAsInt("MESH_PORT", 8089), "Mesh connections TCP port")
|
flag.IntVar(&globalFlags.WebRTCUDPStart, "webrtcUDPStart", getEnvAsInt("WEBRTC_UDP_START", 0), "WebRTC UDP port range start")
|
||||||
flag.IntVar(&globalFlags.WebRTCUDPStart, "webrtcUDPStart", getEnvAsInt("WEBRTC_UDP_START", 10000), "WebRTC UDP port range start")
|
flag.IntVar(&globalFlags.WebRTCUDPEnd, "webrtcUDPEnd", getEnvAsInt("WEBRTC_UDP_END", 0), "WebRTC UDP port range end")
|
||||||
flag.IntVar(&globalFlags.WebRTCUDPEnd, "webrtcUDPEnd", getEnvAsInt("WEBRTC_UDP_END", 20000), "WebRTC UDP port range end")
|
|
||||||
flag.StringVar(&globalFlags.STUNServer, "stunServer", getEnvAsString("STUN_SERVER", "stun.l.google.com:19302"), "WebRTC STUN server")
|
flag.StringVar(&globalFlags.STUNServer, "stunServer", getEnvAsString("STUN_SERVER", "stun.l.google.com:19302"), "WebRTC STUN server")
|
||||||
flag.IntVar(&globalFlags.UDPMuxPort, "webrtcUDPMux", getEnvAsInt("WEBRTC_UDP_MUX", 8088), "WebRTC UDP mux port")
|
flag.IntVar(&globalFlags.UDPMuxPort, "webrtcUDPMux", getEnvAsInt("WEBRTC_UDP_MUX", 8088), "WebRTC UDP mux port")
|
||||||
flag.BoolVar(&globalFlags.AutoAddLocalIP, "autoAddLocalIP", getEnvAsBool("AUTO_ADD_LOCAL_IP", true), "Automatically add local IP to NAT 1 to 1 IPs")
|
flag.BoolVar(&globalFlags.AutoAddLocalIP, "autoAddLocalIP", getEnvAsBool("AUTO_ADD_LOCAL_IP", true), "Automatically add local IP to NAT 1 to 1 IPs")
|
||||||
// String with comma separated IPs
|
// String with comma separated IPs
|
||||||
nat11IPs := ""
|
nat11IP := ""
|
||||||
flag.StringVar(&nat11IPs, "webrtcNAT11IPs", getEnvAsString("WEBRTC_NAT_IPS", ""), "WebRTC NAT 1 to 1 IP(s), comma delimited")
|
flag.StringVar(&nat11IP, "webrtcNAT11IP", getEnvAsString("WEBRTC_NAT_IP", ""), "WebRTC NAT 1 to 1 IP")
|
||||||
flag.StringVar(&globalFlags.TLSCert, "tlsCert", getEnvAsString("TLS_CERT", ""), "Path to TLS certificate")
|
flag.StringVar(&globalFlags.PersistDir, "persistDir", getEnvAsString("PERSIST_DIR", "./persist-data"), "Directory to save persistent data to")
|
||||||
flag.StringVar(&globalFlags.TLSKey, "tlsKey", getEnvAsString("TLS_KEY", ""), "Path to TLS key")
|
|
||||||
flag.StringVar(&globalFlags.ControlSecret, "controlSecret", getEnvAsString("CONTROL_SECRET", ""), "Shared secret for control endpoint")
|
|
||||||
// Parse flags
|
// Parse flags
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// If debug is enabled, verbose is also enabled
|
// If debug is enabled, verbose is also enabled
|
||||||
if globalFlags.Debug {
|
if globalFlags.Debug {
|
||||||
globalFlags.Verbose = true
|
globalFlags.Verbose = true
|
||||||
// If Debug is enabled, set ControlSecret to 1234
|
|
||||||
globalFlags.ControlSecret = "1234"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ICE STUN servers
|
// ICE STUN servers
|
||||||
@@ -108,24 +100,11 @@ func InitFlags() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize NAT 1 to 1 IPs
|
|
||||||
globalFlags.NAT11IPs = []string{}
|
|
||||||
|
|
||||||
// Get local IP
|
|
||||||
if globalFlags.AutoAddLocalIP {
|
|
||||||
globalFlags.NAT11IPs = append(globalFlags.NAT11IPs, getLocalIP())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse NAT 1 to 1 IPs from string
|
// Parse NAT 1 to 1 IPs from string
|
||||||
if len(nat11IPs) > 0 {
|
if len(nat11IP) > 0 {
|
||||||
split := strings.Split(nat11IPs, ",")
|
globalFlags.NAT11IP = nat11IP
|
||||||
if len(split) > 0 {
|
} else if globalFlags.AutoAddLocalIP {
|
||||||
for _, ip := range split {
|
globalFlags.NAT11IP = getLocalIP()
|
||||||
globalFlags.NAT11IPs = append(globalFlags.NAT11IPs, ip)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
globalFlags.NAT11IPs = append(globalFlags.NAT11IPs, nat11IPs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
gen "relay/internal/proto"
|
gen "relay/internal/proto"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TimestampEntry struct {
|
type TimestampEntry struct {
|
||||||
|
|||||||
175
packages/relay/internal/common/safebufio.go
Normal file
175
packages/relay/internal/common/safebufio.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxSize is the maximum allowed data size (1MB)
|
||||||
|
const MaxSize = 1024 * 1024
|
||||||
|
|
||||||
|
// SafeBufioRW wraps a bufio.ReadWriter for sending and receiving JSON and protobufs safely
|
||||||
|
type SafeBufioRW struct {
|
||||||
|
brw *bufio.ReadWriter
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSafeBufioRW(brw *bufio.ReadWriter) *SafeBufioRW {
|
||||||
|
return &SafeBufioRW{brw: brw}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendJSON serializes the given data as JSON and sends it with a 4-byte length prefix
|
||||||
|
func (bu *SafeBufioRW) SendJSON(data interface{}) error {
|
||||||
|
bu.mutex.Lock()
|
||||||
|
defer bu.mutex.Unlock()
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(jsonData) > MaxSize {
|
||||||
|
return errors.New("JSON data exceeds maximum size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the 4-byte length prefix
|
||||||
|
if err = binary.Write(bu.brw, binary.BigEndian, uint32(len(jsonData))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the JSON data
|
||||||
|
if _, err = bu.brw.Write(jsonData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the writer to ensure data is sent
|
||||||
|
return bu.brw.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReceiveJSON reads a 4-byte length prefix, then reads and unmarshals the JSON
|
||||||
|
func (bu *SafeBufioRW) ReceiveJSON(dest interface{}) error {
|
||||||
|
bu.mutex.RLock()
|
||||||
|
defer bu.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Read the 4-byte length prefix
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(bu.brw, binary.BigEndian, &length); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if length > MaxSize {
|
||||||
|
return errors.New("received JSON data exceeds maximum size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the JSON data
|
||||||
|
data := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(bu.brw, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(data, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive reads a 4-byte length prefix, then reads the raw data
|
||||||
|
func (bu *SafeBufioRW) Receive() ([]byte, error) {
|
||||||
|
bu.mutex.RLock()
|
||||||
|
defer bu.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Read the 4-byte length prefix
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(bu.brw, binary.BigEndian, &length); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if length > MaxSize {
|
||||||
|
return nil, errors.New("received data exceeds maximum size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the raw data
|
||||||
|
data := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(bu.brw, data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendProto serializes the given protobuf message and sends it with a 4-byte length prefix
|
||||||
|
func (bu *SafeBufioRW) SendProto(msg proto.Message) error {
|
||||||
|
bu.mutex.Lock()
|
||||||
|
defer bu.mutex.Unlock()
|
||||||
|
|
||||||
|
protoData, err := proto.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(protoData) > MaxSize {
|
||||||
|
return errors.New("protobuf data exceeds maximum size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the 4-byte length prefix
|
||||||
|
if err = binary.Write(bu.brw, binary.BigEndian, uint32(len(protoData))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the Protobuf data
|
||||||
|
if _, err := bu.brw.Write(protoData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the writer to ensure data is sent
|
||||||
|
return bu.brw.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReceiveProto reads a 4-byte length prefix, then reads and unmarshals the protobuf
|
||||||
|
func (bu *SafeBufioRW) ReceiveProto(msg proto.Message) error {
|
||||||
|
bu.mutex.RLock()
|
||||||
|
defer bu.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Read the 4-byte length prefix
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(bu.brw, binary.BigEndian, &length); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if length > MaxSize {
|
||||||
|
return errors.New("received Protobuf data exceeds maximum size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the Protobuf data
|
||||||
|
data := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(bu.brw, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return proto.Unmarshal(data, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes raw data to the underlying buffer
|
||||||
|
func (bu *SafeBufioRW) Write(data []byte) (int, error) {
|
||||||
|
bu.mutex.Lock()
|
||||||
|
defer bu.mutex.Unlock()
|
||||||
|
|
||||||
|
if len(data) > MaxSize {
|
||||||
|
return 0, errors.New("data exceeds maximum size")
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := bu.brw.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the writer to ensure data is sent
|
||||||
|
if err = bu.brw.Flush(); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"encoding/json"
|
||||||
"reflect"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrKeyNotFound = errors.New("key not found")
|
|
||||||
ErrValueNotPointer = errors.New("value is not a pointer")
|
|
||||||
ErrFieldNotFound = errors.New("field not found")
|
|
||||||
ErrTypeMismatch = errors.New("type mismatch")
|
|
||||||
)
|
|
||||||
|
|
||||||
// SafeMap is a generic thread-safe map with its own mutex
|
// SafeMap is a generic thread-safe map with its own mutex
|
||||||
type SafeMap[K comparable, V any] struct {
|
type SafeMap[K comparable, V any] struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -34,6 +27,14 @@ func (sm *SafeMap[K, V]) Get(key K) (V, bool) {
|
|||||||
return v, ok
|
return v, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Has checks if a key exists in the map
|
||||||
|
func (sm *SafeMap[K, V]) Has(key K) bool {
|
||||||
|
sm.mu.RLock()
|
||||||
|
defer sm.mu.RUnlock()
|
||||||
|
_, ok := sm.m[key]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// Set adds or updates a value in the map
|
// Set adds or updates a value in the map
|
||||||
func (sm *SafeMap[K, V]) Set(key K, value V) {
|
func (sm *SafeMap[K, V]) Set(key K, value V) {
|
||||||
sm.mu.Lock()
|
sm.mu.Lock()
|
||||||
@@ -66,36 +67,31 @@ func (sm *SafeMap[K, V]) Copy() map[K]V {
|
|||||||
return copied
|
return copied
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates a specific field in the value data
|
// Range iterates over the map and applies a function to each key-value pair
|
||||||
func (sm *SafeMap[K, V]) Update(key K, fieldName string, newValue any) error {
|
func (sm *SafeMap[K, V]) Range(f func(K, V) bool) {
|
||||||
|
sm.mu.RLock()
|
||||||
|
defer sm.mu.RUnlock()
|
||||||
|
for k, v := range sm.m {
|
||||||
|
if !f(k, v) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SafeMap[K, V]) MarshalJSON() ([]byte, error) {
|
||||||
|
sm.mu.RLock()
|
||||||
|
defer sm.mu.RUnlock()
|
||||||
|
return json.Marshal(sm.m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SafeMap[K, V]) UnmarshalJSON(data []byte) error {
|
||||||
sm.mu.Lock()
|
sm.mu.Lock()
|
||||||
defer sm.mu.Unlock()
|
defer sm.mu.Unlock()
|
||||||
|
return json.Unmarshal(data, &sm.m)
|
||||||
v, ok := sm.m[key]
|
}
|
||||||
if !ok {
|
|
||||||
return ErrKeyNotFound
|
func (sm *SafeMap[K, V]) String() string {
|
||||||
}
|
sm.mu.RLock()
|
||||||
|
defer sm.mu.RUnlock()
|
||||||
// Use reflect to update the field
|
return fmt.Sprintf("%+v", sm.m)
|
||||||
rv := reflect.ValueOf(v)
|
|
||||||
if rv.Kind() != reflect.Ptr {
|
|
||||||
return ErrValueNotPointer
|
|
||||||
}
|
|
||||||
|
|
||||||
rv = rv.Elem()
|
|
||||||
// Check if the field exists
|
|
||||||
field := rv.FieldByName(fieldName)
|
|
||||||
if !field.IsValid() || !field.CanSet() {
|
|
||||||
return ErrFieldNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
newRV := reflect.ValueOf(newValue)
|
|
||||||
if newRV.Type() != field.Type() {
|
|
||||||
return ErrTypeMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
field.Set(newRV)
|
|
||||||
sm.m[key] = v
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
package connections
|
package connections
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pion/webrtc/v4"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
gen "relay/internal/proto"
|
gen "relay/internal/proto"
|
||||||
|
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type OnMessageCallback func(data []byte)
|
||||||
|
|
||||||
// NestriDataChannel is a custom data channel with callbacks
|
// NestriDataChannel is a custom data channel with callbacks
|
||||||
type NestriDataChannel struct {
|
type NestriDataChannel struct {
|
||||||
*webrtc.DataChannel
|
*webrtc.DataChannel
|
||||||
@@ -37,7 +40,7 @@ func NewNestriDataChannel(dc *webrtc.DataChannel) *NestriDataChannel {
|
|||||||
// Handle message type callback
|
// Handle message type callback
|
||||||
if callback, ok := ndc.callbacks["input"]; ok {
|
if callback, ok := ndc.callbacks["input"]; ok {
|
||||||
go callback(msg.Data)
|
go callback(msg.Data)
|
||||||
} // TODO: Log unknown message type?
|
} // We don't care about unhandled messages
|
||||||
})
|
})
|
||||||
|
|
||||||
return ndc
|
return ndc
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
package connections
|
package connections
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pion/webrtc/v4"
|
"encoding/json"
|
||||||
"relay/internal/common"
|
"relay/internal/common"
|
||||||
"time"
|
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MessageBase is the base type for WS/DC messages.
|
// MessageBase is the base type for any JSON message
|
||||||
type MessageBase struct {
|
type MessageBase struct {
|
||||||
PayloadType string `json:"payload_type"`
|
Type string `json:"payload_type"`
|
||||||
Latency *common.LatencyTracker `json:"latency,omitempty"`
|
Latency *common.LatencyTracker `json:"latency,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageRaw struct {
|
||||||
|
MessageBase
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessageRaw(t string, data json.RawMessage) *MessageRaw {
|
||||||
|
return &MessageRaw{
|
||||||
|
MessageBase: MessageBase{
|
||||||
|
Type: t,
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageLog represents a log message.
|
|
||||||
type MessageLog struct {
|
type MessageLog struct {
|
||||||
MessageBase
|
MessageBase
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
@@ -20,7 +34,17 @@ type MessageLog struct {
|
|||||||
Time string `json:"time"`
|
Time string `json:"time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageMetrics represents a metrics/heartbeat message.
|
func NewMessageLog(t string, level, message, time string) *MessageLog {
|
||||||
|
return &MessageLog{
|
||||||
|
MessageBase: MessageBase{
|
||||||
|
Type: t,
|
||||||
|
},
|
||||||
|
Level: level,
|
||||||
|
Message: message,
|
||||||
|
Time: time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type MessageMetrics struct {
|
type MessageMetrics struct {
|
||||||
MessageBase
|
MessageBase
|
||||||
UsageCPU float64 `json:"usage_cpu"`
|
UsageCPU float64 `json:"usage_cpu"`
|
||||||
@@ -29,104 +53,42 @@ type MessageMetrics struct {
|
|||||||
PipelineLatency float64 `json:"pipeline_latency"`
|
PipelineLatency float64 `json:"pipeline_latency"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageICECandidate represents an ICE candidate message.
|
func NewMessageMetrics(t string, usageCPU, usageMemory float64, uptime uint64, pipelineLatency float64) *MessageMetrics {
|
||||||
type MessageICECandidate struct {
|
return &MessageMetrics{
|
||||||
MessageBase
|
MessageBase: MessageBase{
|
||||||
Candidate webrtc.ICECandidateInit `json:"candidate"`
|
Type: t,
|
||||||
}
|
},
|
||||||
|
|
||||||
// MessageSDP represents an SDP message.
|
|
||||||
type MessageSDP struct {
|
|
||||||
MessageBase
|
|
||||||
SDP webrtc.SessionDescription `json:"sdp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoinerType is an enum for the type of incoming room joiner
|
|
||||||
type JoinerType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
JoinerNode JoinerType = iota
|
|
||||||
JoinerClient
|
|
||||||
)
|
|
||||||
|
|
||||||
func (jt *JoinerType) String() string {
|
|
||||||
switch *jt {
|
|
||||||
case JoinerNode:
|
|
||||||
return "node"
|
|
||||||
case JoinerClient:
|
|
||||||
return "client"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageJoin is used to tell us that either participant or ingest wants to join the room
|
|
||||||
type MessageJoin struct {
|
|
||||||
MessageBase
|
|
||||||
JoinerType JoinerType `json:"joiner_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnswerType is an enum for the type of answer, signaling Room state for a joiner
|
|
||||||
type AnswerType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
AnswerOffline AnswerType = iota // For participant/client, when the room is offline without stream
|
|
||||||
AnswerInUse // For ingest/node joiner, when the room is already in use by another ingest/node
|
|
||||||
AnswerOK // For both, when the join request is handled successfully
|
|
||||||
)
|
|
||||||
|
|
||||||
// MessageAnswer is used to send the answer to a join request
|
|
||||||
type MessageAnswer struct {
|
|
||||||
MessageBase
|
|
||||||
AnswerType AnswerType `json:"answer_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendLogMessageWS sends a log message to the given WebSocket connection.
|
|
||||||
func (ws *SafeWebSocket) SendLogMessageWS(level, message string) error {
|
|
||||||
msg := MessageLog{
|
|
||||||
MessageBase: MessageBase{PayloadType: "log"},
|
|
||||||
Level: level,
|
|
||||||
Message: message,
|
|
||||||
Time: time.Now().Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
return ws.SendJSON(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMetricsMessageWS sends a metrics message to the given WebSocket connection.
|
|
||||||
func (ws *SafeWebSocket) SendMetricsMessageWS(usageCPU, usageMemory float64, uptime uint64, pipelineLatency float64) error {
|
|
||||||
msg := MessageMetrics{
|
|
||||||
MessageBase: MessageBase{PayloadType: "metrics"},
|
|
||||||
UsageCPU: usageCPU,
|
UsageCPU: usageCPU,
|
||||||
UsageMemory: usageMemory,
|
UsageMemory: usageMemory,
|
||||||
Uptime: uptime,
|
Uptime: uptime,
|
||||||
PipelineLatency: pipelineLatency,
|
PipelineLatency: pipelineLatency,
|
||||||
}
|
}
|
||||||
return ws.SendJSON(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendICECandidateMessageWS sends an ICE candidate message to the given WebSocket connection.
|
type MessageICE struct {
|
||||||
func (ws *SafeWebSocket) SendICECandidateMessageWS(candidate webrtc.ICECandidateInit) error {
|
MessageBase
|
||||||
msg := MessageICECandidate{
|
Candidate webrtc.ICECandidateInit `json:"candidate"`
|
||||||
MessageBase: MessageBase{PayloadType: "ice"},
|
|
||||||
Candidate: candidate,
|
|
||||||
}
|
|
||||||
return ws.SendJSON(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendSDPMessageWS sends an SDP message to the given WebSocket connection.
|
func NewMessageICE(t string, candidate webrtc.ICECandidateInit) *MessageICE {
|
||||||
func (ws *SafeWebSocket) SendSDPMessageWS(sdp webrtc.SessionDescription) error {
|
return &MessageICE{
|
||||||
msg := MessageSDP{
|
MessageBase: MessageBase{
|
||||||
MessageBase: MessageBase{PayloadType: "sdp"},
|
Type: t,
|
||||||
SDP: sdp,
|
},
|
||||||
|
Candidate: candidate,
|
||||||
}
|
}
|
||||||
return ws.SendJSON(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendAnswerMessageWS sends an answer message to the given WebSocket connection.
|
type MessageSDP struct {
|
||||||
func (ws *SafeWebSocket) SendAnswerMessageWS(answer AnswerType) error {
|
MessageBase
|
||||||
msg := MessageAnswer{
|
SDP webrtc.SessionDescription `json:"sdp"`
|
||||||
MessageBase: MessageBase{PayloadType: "answer"},
|
}
|
||||||
AnswerType: answer,
|
|
||||||
}
|
func NewMessageSDP(t string, sdp webrtc.SessionDescription) *MessageSDP {
|
||||||
return ws.SendJSON(msg)
|
return &MessageSDP{
|
||||||
|
MessageBase: MessageBase{
|
||||||
|
Type: t,
|
||||||
|
},
|
||||||
|
SDP: sdp,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
package connections
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pion/webrtc/v4"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
gen "relay/internal/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SendMeshHandshake sends a handshake message to another relay.
|
|
||||||
func (ws *SafeWebSocket) SendMeshHandshake(relayID, publicKey string) error {
|
|
||||||
msg := &gen.MeshMessage{
|
|
||||||
Type: &gen.MeshMessage_Handshake{
|
|
||||||
Handshake: &gen.Handshake{
|
|
||||||
RelayId: relayID,
|
|
||||||
DhPublicKey: publicKey,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data, err := proto.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ws.SendBinary(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMeshHandshakeResponse sends a handshake response to a relay.
|
|
||||||
func (ws *SafeWebSocket) SendMeshHandshakeResponse(relayID, dhPublicKey string, approvals map[string]string) error {
|
|
||||||
msg := &gen.MeshMessage{
|
|
||||||
Type: &gen.MeshMessage_HandshakeResponse{
|
|
||||||
HandshakeResponse: &gen.HandshakeResponse{
|
|
||||||
RelayId: relayID,
|
|
||||||
DhPublicKey: dhPublicKey,
|
|
||||||
Approvals: approvals,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data, err := proto.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ws.SendBinary(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMeshForwardSDP sends a forwarded SDP message to another relay
|
|
||||||
func (ws *SafeWebSocket) SendMeshForwardSDP(roomName, participantID string, sdp webrtc.SessionDescription) error {
|
|
||||||
msg := &gen.MeshMessage{
|
|
||||||
Type: &gen.MeshMessage_ForwardSdp{
|
|
||||||
ForwardSdp: &gen.ForwardSDP{
|
|
||||||
RoomName: roomName,
|
|
||||||
ParticipantId: participantID,
|
|
||||||
Sdp: sdp.SDP,
|
|
||||||
Type: sdp.Type.String(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data, err := proto.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ws.SendBinary(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMeshForwardICE sends a forwarded ICE candidate to another relay
|
|
||||||
func (ws *SafeWebSocket) SendMeshForwardICE(roomName, participantID string, candidate webrtc.ICECandidateInit) error {
|
|
||||||
var sdpMLineIndex uint32
|
|
||||||
if candidate.SDPMLineIndex != nil {
|
|
||||||
sdpMLineIndex = uint32(*candidate.SDPMLineIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := &gen.MeshMessage{
|
|
||||||
Type: &gen.MeshMessage_ForwardIce{
|
|
||||||
ForwardIce: &gen.ForwardICE{
|
|
||||||
RoomName: roomName,
|
|
||||||
ParticipantId: participantID,
|
|
||||||
Candidate: &gen.ICECandidateInit{
|
|
||||||
Candidate: candidate.Candidate,
|
|
||||||
SdpMid: candidate.SDPMid,
|
|
||||||
SdpMLineIndex: &sdpMLineIndex,
|
|
||||||
UsernameFragment: candidate.UsernameFragment,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data, err := proto.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ws.SendBinary(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *SafeWebSocket) SendMeshForwardIngest(roomName string) error {
|
|
||||||
msg := &gen.MeshMessage{
|
|
||||||
Type: &gen.MeshMessage_ForwardIngest{
|
|
||||||
ForwardIngest: &gen.ForwardIngest{
|
|
||||||
RoomName: roomName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data, err := proto.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ws.SendBinary(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *SafeWebSocket) SendMeshStreamRequest(roomName string) error {
|
|
||||||
msg := &gen.MeshMessage{
|
|
||||||
Type: &gen.MeshMessage_StreamRequest{
|
|
||||||
StreamRequest: &gen.StreamRequest{
|
|
||||||
RoomName: roomName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data, err := proto.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ws.SendBinary(data)
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
package connections
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"log/slog"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OnMessageCallback is a callback for messages of given type
|
|
||||||
type OnMessageCallback func(data []byte)
|
|
||||||
|
|
||||||
// SafeWebSocket is a websocket with a mutex
|
|
||||||
type SafeWebSocket struct {
|
|
||||||
*websocket.Conn
|
|
||||||
sync.Mutex
|
|
||||||
closed bool
|
|
||||||
closeCallback func() // Callback to call on close
|
|
||||||
closeChan chan struct{} // Channel to signal closure
|
|
||||||
callbacks map[string]OnMessageCallback // MessageBase type -> callback
|
|
||||||
binaryCallback OnMessageCallback // Binary message callback
|
|
||||||
sharedSecret []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSafeWebSocket creates a new SafeWebSocket from *websocket.Conn
|
|
||||||
func NewSafeWebSocket(conn *websocket.Conn) *SafeWebSocket {
|
|
||||||
ws := &SafeWebSocket{
|
|
||||||
Conn: conn,
|
|
||||||
closed: false,
|
|
||||||
closeCallback: nil,
|
|
||||||
closeChan: make(chan struct{}),
|
|
||||||
callbacks: make(map[string]OnMessageCallback),
|
|
||||||
binaryCallback: nil,
|
|
||||||
sharedSecret: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launch a goroutine to handle messages
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
// Read message
|
|
||||||
kind, data, err := ws.Conn.ReadMessage()
|
|
||||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNoStatusReceived) {
|
|
||||||
// If unexpected close error, break
|
|
||||||
slog.Debug("WebSocket closed unexpectedly", "err", err)
|
|
||||||
break
|
|
||||||
} else if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNoStatusReceived) {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
slog.Error("Failed reading WebSocket message", "err", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
switch kind {
|
|
||||||
case websocket.TextMessage:
|
|
||||||
// Decode message
|
|
||||||
var msg MessageBase
|
|
||||||
if err = json.Unmarshal(data, &msg); err != nil {
|
|
||||||
slog.Error("Failed decoding WebSocket message", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle message type callback
|
|
||||||
if callback, ok := ws.callbacks[msg.PayloadType]; ok {
|
|
||||||
callback(data)
|
|
||||||
} // TODO: Log unknown message payload type?
|
|
||||||
break
|
|
||||||
case websocket.BinaryMessage:
|
|
||||||
// Handle binary message callback
|
|
||||||
if ws.binaryCallback != nil {
|
|
||||||
ws.binaryCallback(data)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
slog.Warn("Unknown WebSocket message type", "type", kind)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal closure to callback first
|
|
||||||
if ws.closeCallback != nil {
|
|
||||||
ws.closeCallback()
|
|
||||||
}
|
|
||||||
close(ws.closeChan)
|
|
||||||
ws.closed = true
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ws
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSharedSecret sets the shared secret for the websocket
|
|
||||||
func (ws *SafeWebSocket) SetSharedSecret(secret []byte) {
|
|
||||||
ws.sharedSecret = secret
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSharedSecret returns the shared secret for the websocket
|
|
||||||
func (ws *SafeWebSocket) GetSharedSecret() []byte {
|
|
||||||
return ws.sharedSecret
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendJSON writes JSON to a websocket with a mutex
|
|
||||||
func (ws *SafeWebSocket) SendJSON(v interface{}) error {
|
|
||||||
ws.Lock()
|
|
||||||
defer ws.Unlock()
|
|
||||||
return ws.Conn.WriteJSON(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendBinary writes binary to a websocket with a mutex
|
|
||||||
func (ws *SafeWebSocket) SendBinary(data []byte) error {
|
|
||||||
ws.Lock()
|
|
||||||
defer ws.Unlock()
|
|
||||||
return ws.Conn.WriteMessage(websocket.BinaryMessage, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterMessageCallback sets the callback for binary message of given type
|
|
||||||
func (ws *SafeWebSocket) RegisterMessageCallback(msgType string, callback OnMessageCallback) {
|
|
||||||
if ws.callbacks == nil {
|
|
||||||
ws.callbacks = make(map[string]OnMessageCallback)
|
|
||||||
}
|
|
||||||
ws.callbacks[msgType] = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterBinaryMessageCallback sets the callback for all binary messages
|
|
||||||
func (ws *SafeWebSocket) RegisterBinaryMessageCallback(callback OnMessageCallback) {
|
|
||||||
ws.binaryCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnregisterMessageCallback removes the callback for binary message of given type
|
|
||||||
func (ws *SafeWebSocket) UnregisterMessageCallback(msgType string) {
|
|
||||||
if ws.callbacks != nil {
|
|
||||||
delete(ws.callbacks, msgType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnregisterBinaryMessageCallback removes the callback for all binary messages
|
|
||||||
func (ws *SafeWebSocket) UnregisterBinaryMessageCallback() {
|
|
||||||
ws.binaryCallback = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterOnClose sets the callback for websocket closing
|
|
||||||
func (ws *SafeWebSocket) RegisterOnClose(callback func()) {
|
|
||||||
ws.closeCallback = func() {
|
|
||||||
// Clear our callbacks
|
|
||||||
ws.callbacks = nil
|
|
||||||
ws.binaryCallback = nil
|
|
||||||
// Call the callback
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closed returns a channel that closes when the WebSocket connection is terminated
|
|
||||||
func (ws *SafeWebSocket) Closed() <-chan struct{} {
|
|
||||||
return ws.closeChan
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsClosed returns true if the WebSocket connection is closed
|
|
||||||
func (ws *SafeWebSocket) IsClosed() bool {
|
|
||||||
return ws.closed
|
|
||||||
}
|
|
||||||
13
packages/relay/internal/core/consts.go
Normal file
13
packages/relay/internal/core/consts.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// --- Constants ---
|
||||||
|
const (
|
||||||
|
// PubSub Topics
|
||||||
|
roomStateTopicName = "room-states"
|
||||||
|
relayMetricsTopicName = "relay-metrics"
|
||||||
|
|
||||||
|
// Timers and Intervals
|
||||||
|
metricsPublishInterval = 15 * time.Second // How often to publish own metrics
|
||||||
|
)
|
||||||
214
packages/relay/internal/core/core.go
Normal file
214
packages/relay/internal/core/core.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"relay/internal/common"
|
||||||
|
"relay/internal/shared"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p"
|
||||||
|
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/libp2p/go-libp2p/core/host"
|
||||||
|
"github.com/libp2p/go-libp2p/core/peer"
|
||||||
|
"github.com/libp2p/go-libp2p/p2p/protocol/ping"
|
||||||
|
"github.com/libp2p/go-libp2p/p2p/security/noise"
|
||||||
|
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
|
||||||
|
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
|
||||||
|
"github.com/multiformats/go-multiaddr"
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// -- Variables --
|
||||||
|
|
||||||
|
var globalRelay *Relay
|
||||||
|
|
||||||
|
// -- Structs --
|
||||||
|
|
||||||
|
// RelayInfo contains light information of Relay, in mesh-friendly format
|
||||||
|
type RelayInfo struct {
|
||||||
|
ID peer.ID
|
||||||
|
MeshAddrs []string // Addresses of this relay
|
||||||
|
MeshRooms *common.SafeMap[string, shared.RoomInfo] // Rooms hosted by this relay
|
||||||
|
MeshLatencies *common.SafeMap[string, time.Duration] // Latencies to other peers from this relay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relay structure enhanced with metrics and state
|
||||||
|
type Relay struct {
|
||||||
|
RelayInfo
|
||||||
|
|
||||||
|
Host host.Host // libp2p host for peer-to-peer networking
|
||||||
|
PubSub *pubsub.PubSub // PubSub for state synchronization
|
||||||
|
PingService *ping.PingService
|
||||||
|
|
||||||
|
// Local
|
||||||
|
LocalRooms *common.SafeMap[ulid.ULID, *shared.Room] // room ID -> local Room struct (hosted by this relay)
|
||||||
|
LocalMeshPeers *common.SafeMap[peer.ID, *RelayInfo] // peer ID -> mesh peer relay info (connected to this relay)
|
||||||
|
LocalMeshConnections *common.SafeMap[peer.ID, *webrtc.PeerConnection] // peer ID -> PeerConnection (connected to this relay)
|
||||||
|
|
||||||
|
// Protocols
|
||||||
|
ProtocolRegistry
|
||||||
|
|
||||||
|
// PubSub Topics
|
||||||
|
pubTopicState *pubsub.Topic // topic for room states
|
||||||
|
pubTopicRelayMetrics *pubsub.Topic // topic for relay metrics/status
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay, error) {
|
||||||
|
listenAddrs := []string{
|
||||||
|
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4 - Raw TCP
|
||||||
|
fmt.Sprintf("/ip6/::/tcp/%d", port), // IPv6 - Raw TCP
|
||||||
|
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d/ws", port), // IPv4 - TCP WebSocket
|
||||||
|
fmt.Sprintf("/ip6/::/tcp/%d/ws", port), // IPv6 - TCP WebSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
var muAddrs []multiaddr.Multiaddr
|
||||||
|
for _, addr := range listenAddrs {
|
||||||
|
multiAddr, err := multiaddr.NewMultiaddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse multiaddr '%s': %w", addr, err)
|
||||||
|
}
|
||||||
|
muAddrs = append(muAddrs, multiAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize libp2p host
|
||||||
|
p2pHost, err := libp2p.New(
|
||||||
|
// TODO: Currently static identity
|
||||||
|
libp2p.Identity(identityKey),
|
||||||
|
// Enable required transports
|
||||||
|
libp2p.Transport(tcp.NewTCPTransport),
|
||||||
|
libp2p.Transport(ws.New),
|
||||||
|
// Other options
|
||||||
|
libp2p.ListenAddrs(muAddrs...),
|
||||||
|
libp2p.Security(noise.ID, noise.New),
|
||||||
|
libp2p.EnableRelay(),
|
||||||
|
libp2p.EnableHolePunching(),
|
||||||
|
libp2p.EnableNATService(),
|
||||||
|
libp2p.EnableAutoNATv2(),
|
||||||
|
libp2p.ShareTCPListener(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create libp2p host for relay: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up pubsub
|
||||||
|
p2pPubsub, err := pubsub.NewGossipSub(ctx, p2pHost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create pubsub: %w, addrs: %v", err, p2pHost.Addrs())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Ping Service
|
||||||
|
pingSvc := ping.NewPingService(p2pHost)
|
||||||
|
|
||||||
|
var addresses []string
|
||||||
|
for _, addr := range p2pHost.Addrs() {
|
||||||
|
addresses = append(addresses, addr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &Relay{
|
||||||
|
RelayInfo: RelayInfo{
|
||||||
|
ID: p2pHost.ID(),
|
||||||
|
MeshAddrs: addresses,
|
||||||
|
MeshRooms: common.NewSafeMap[string, shared.RoomInfo](),
|
||||||
|
MeshLatencies: common.NewSafeMap[string, time.Duration](),
|
||||||
|
},
|
||||||
|
Host: p2pHost,
|
||||||
|
PubSub: p2pPubsub,
|
||||||
|
PingService: pingSvc,
|
||||||
|
LocalRooms: common.NewSafeMap[ulid.ULID, *shared.Room](),
|
||||||
|
LocalMeshPeers: common.NewSafeMap[peer.ID, *RelayInfo](),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add network notifier after relay is initialized
|
||||||
|
p2pHost.Network().Notify(&networkNotifier{relay: r})
|
||||||
|
|
||||||
|
// Set up PubSub topics and handlers
|
||||||
|
if err = r.setupPubSub(ctx); err != nil {
|
||||||
|
err = p2pHost.Close()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to close host after PubSub setup failure", "err", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to setup PubSub: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Protocol Registry
|
||||||
|
r.ProtocolRegistry = NewProtocolRegistry(r)
|
||||||
|
|
||||||
|
// Start discovery features
|
||||||
|
if err = startMDNSDiscovery(r); err != nil {
|
||||||
|
slog.Warn("Failed to initialize mDNS discovery, continuing without..", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start background tasks
|
||||||
|
go r.periodicMetricsPublisher(ctx)
|
||||||
|
|
||||||
|
printConnectInstructions(p2pHost)
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error {
|
||||||
|
var err error
|
||||||
|
persistentDir := common.GetFlags().PersistDir
|
||||||
|
|
||||||
|
// Load or generate identity key
|
||||||
|
var identityKey crypto.PrivKey
|
||||||
|
var privKey ed25519.PrivateKey
|
||||||
|
// First check if we need to generate identity
|
||||||
|
hasIdentity := len(persistentDir) > 0 && common.GetFlags().RegenIdentity == false
|
||||||
|
if hasIdentity {
|
||||||
|
_, err = os.Stat(persistentDir + "/identity.key")
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to check identity key file: %w", err)
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
hasIdentity = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasIdentity {
|
||||||
|
// Make sure the persistent directory exists
|
||||||
|
if err = os.MkdirAll(persistentDir, 0700); err != nil {
|
||||||
|
return fmt.Errorf("failed to create persistent data directory: %w", err)
|
||||||
|
}
|
||||||
|
// Generate
|
||||||
|
slog.Info("Generating new identity for relay")
|
||||||
|
privKey, err = common.GenerateED25519Key()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate new identity: %w", err)
|
||||||
|
}
|
||||||
|
// Save the key
|
||||||
|
if err = common.SaveED25519Key(privKey, persistentDir+"/identity.key"); err != nil {
|
||||||
|
return fmt.Errorf("failed to save identity key: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("New identity generated and saved", "path", persistentDir+"/identity.key")
|
||||||
|
} else {
|
||||||
|
slog.Info("Loading existing identity for relay", "path", persistentDir+"/identity.key")
|
||||||
|
// Load the key
|
||||||
|
privKey, err = common.LoadED25519Key(persistentDir + "/identity.key")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load identity key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to libp2p crypto.PrivKey
|
||||||
|
identityKey, err = crypto.UnmarshalEd25519PrivateKey(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal ED25519 private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalRelay, err = NewRelay(ctx, common.GetFlags().EndpointPort, identityKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create relay: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = common.InitWebRTCAPI(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Relay initialized", "id", globalRelay.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
38
packages/relay/internal/core/mdns.go
Normal file
38
packages/relay/internal/core/mdns.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/peer"
|
||||||
|
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mdnsDiscoveryRendezvous = "/nestri-relay/mdns-discovery/1.0.0" // Shared string for mDNS discovery
|
||||||
|
)
|
||||||
|
|
||||||
|
type discoveryNotifee struct {
|
||||||
|
relay *Relay
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *discoveryNotifee) HandlePeerFound(pi peer.AddrInfo) {
|
||||||
|
if d.relay != nil {
|
||||||
|
if err := d.relay.connectToRelay(context.Background(), &pi); err != nil {
|
||||||
|
slog.Error("failed to connect to discovered relay", "peer", pi.ID, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMDNSDiscovery(relay *Relay) error {
|
||||||
|
d := &discoveryNotifee{
|
||||||
|
relay: relay,
|
||||||
|
}
|
||||||
|
|
||||||
|
service := mdns.NewMdnsService(relay.Host, mdnsDiscoveryRendezvous, d)
|
||||||
|
if err := service.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start mDNS discovery: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
128
packages/relay/internal/core/metrics.go
Normal file
128
packages/relay/internal/core/metrics.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/peer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Metrics Collection and Publishing ---
|
||||||
|
|
||||||
|
// periodicMetricsPublisher periodically gathers local metrics and publishes them.
|
||||||
|
func (r *Relay) periodicMetricsPublisher(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(metricsPublishInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// Publish immediately on start
|
||||||
|
if err := r.publishRelayMetrics(ctx); err != nil {
|
||||||
|
slog.Error("Failed to publish initial relay metrics", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
slog.Info("Stopping metrics publisher")
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := r.publishRelayMetrics(ctx); err != nil {
|
||||||
|
slog.Error("Failed to publish relay metrics", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// publishRelayMetrics sends the current relay status to the mesh.
|
||||||
|
func (r *Relay) publishRelayMetrics(ctx context.Context) error {
|
||||||
|
if r.pubTopicRelayMetrics == nil {
|
||||||
|
slog.Warn("Cannot publish relay metrics: topic is nil")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all peer latencies
|
||||||
|
r.checkAllPeerLatencies(ctx)
|
||||||
|
|
||||||
|
data, err := json.Marshal(r.RelayInfo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal relay status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubErr := r.pubTopicRelayMetrics.Publish(ctx, data); pubErr != nil {
|
||||||
|
// Don't return error on publish failure, just log
|
||||||
|
slog.Error("Failed to publish relay metrics message", "err", pubErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAllPeerLatencies measures latency to all currently connected peers.
|
||||||
|
func (r *Relay) checkAllPeerLatencies(ctx context.Context) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, p := range r.Host.Network().Peers() {
|
||||||
|
if p == r.ID {
|
||||||
|
continue // Skip self
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
// Run checks concurrently
|
||||||
|
go func(peerID peer.ID) {
|
||||||
|
defer wg.Done()
|
||||||
|
go r.measureLatencyToPeer(ctx, peerID)
|
||||||
|
}(p)
|
||||||
|
}
|
||||||
|
wg.Wait() // Wait for all latency checks to complete
|
||||||
|
}
|
||||||
|
|
||||||
|
// measureLatencyToPeer pings a specific peer and updates the local latency map.
|
||||||
|
func (r *Relay) measureLatencyToPeer(ctx context.Context, peerID peer.ID) {
|
||||||
|
// Check peer status first
|
||||||
|
if !r.hasConnectedPeer(peerID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a context for the ping operation
|
||||||
|
pingCtx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Use the PingService instance stored in the Relay struct
|
||||||
|
if r.PingService == nil {
|
||||||
|
slog.Error("PingService is nil, cannot measure latency", "peer", peerID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resultsCh := r.PingService.Ping(pingCtx, peerID)
|
||||||
|
|
||||||
|
// Wait for the result (or timeout)
|
||||||
|
select {
|
||||||
|
case <-pingCtx.Done():
|
||||||
|
// Ping timed out
|
||||||
|
slog.Warn("Latency check canceled", "peer", peerID, "err", pingCtx.Err())
|
||||||
|
case result, ok := <-resultsCh:
|
||||||
|
if !ok {
|
||||||
|
// Channel closed unexpectedly
|
||||||
|
slog.Warn("Ping service channel closed unexpectedly", "peer", peerID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Received ping result
|
||||||
|
if result.Error != nil {
|
||||||
|
slog.Warn("Latency check failed, removing peer from local peers map", "peer", peerID, "err", result.Error)
|
||||||
|
// Remove from MeshPeers if ping failed
|
||||||
|
if r.LocalMeshPeers.Has(peerID) {
|
||||||
|
r.LocalMeshPeers.Delete(peerID)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping successful, update latency
|
||||||
|
latency := result.RTT
|
||||||
|
// Ensure latency is not zero if successful, assign a minimal value if so.
|
||||||
|
// Sometimes RTT can be reported as 0 for very fast local connections.
|
||||||
|
if latency <= 0 {
|
||||||
|
latency = 1 * time.Microsecond
|
||||||
|
}
|
||||||
|
|
||||||
|
r.RelayInfo.MeshLatencies.Set(peerID.String(), latency)
|
||||||
|
}
|
||||||
|
}
|
||||||
128
packages/relay/internal/core/p2p.go
Normal file
128
packages/relay/internal/core/p2p.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/host"
|
||||||
|
"github.com/libp2p/go-libp2p/core/network"
|
||||||
|
"github.com/libp2p/go-libp2p/core/peer"
|
||||||
|
"github.com/multiformats/go-multiaddr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Structs ---
|
||||||
|
|
||||||
|
// networkNotifier logs connection events and updates relay state
|
||||||
|
type networkNotifier struct {
|
||||||
|
relay *Relay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connected is called when a connection is established
|
||||||
|
func (n *networkNotifier) Connected(net network.Network, conn network.Conn) {
|
||||||
|
if n.relay == nil {
|
||||||
|
n.relay.onPeerConnected(conn.RemotePeer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnected is called when a connection is terminated
|
||||||
|
func (n *networkNotifier) Disconnected(net network.Network, conn network.Conn) {
|
||||||
|
// Update the status of the disconnected peer
|
||||||
|
if n.relay != nil {
|
||||||
|
n.relay.onPeerDisconnected(conn.RemotePeer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen is called when the node starts listening on an address
|
||||||
|
func (n *networkNotifier) Listen(net network.Network, addr multiaddr.Multiaddr) {}
|
||||||
|
|
||||||
|
// ListenClose is called when the node stops listening on an address
|
||||||
|
func (n *networkNotifier) ListenClose(net network.Network, addr multiaddr.Multiaddr) {}
|
||||||
|
|
||||||
|
// --- PubSub Setup ---
|
||||||
|
|
||||||
|
// setupPubSub initializes PubSub topics and subscriptions.
|
||||||
|
func (r *Relay) setupPubSub(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Room State Topic
|
||||||
|
r.pubTopicState, err = r.PubSub.Join(roomStateTopicName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to join room state topic '%s': %w", roomStateTopicName, err)
|
||||||
|
}
|
||||||
|
stateSub, err := r.pubTopicState.Subscribe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to subscribe to room state topic '%s': %w", roomStateTopicName, err)
|
||||||
|
}
|
||||||
|
go r.handleRoomStateMessages(ctx, stateSub) // Handler in relay_state.go
|
||||||
|
|
||||||
|
// Relay Metrics Topic
|
||||||
|
r.pubTopicRelayMetrics, err = r.PubSub.Join(relayMetricsTopicName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to join relay metrics topic '%s': %w", relayMetricsTopicName, err)
|
||||||
|
}
|
||||||
|
metricsSub, err := r.pubTopicRelayMetrics.Subscribe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to subscribe to relay metrics topic '%s': %w", relayMetricsTopicName, err)
|
||||||
|
}
|
||||||
|
go r.handleRelayMetricsMessages(ctx, metricsSub) // Handler in relay_state.go
|
||||||
|
|
||||||
|
slog.Info("PubSub topics joined and subscriptions started")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Connection Management ---
|
||||||
|
|
||||||
|
// connectToRelay is internal method to connect to a relay peer using multiaddresses
|
||||||
|
func (r *Relay) connectToRelay(ctx context.Context, peerInfo *peer.AddrInfo) error {
|
||||||
|
if peerInfo.ID == r.ID {
|
||||||
|
return errors.New("cannot connect to self")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a timeout for the connection attempt
|
||||||
|
connectCtx, cancel := context.WithTimeout(ctx, 15*time.Second) // 15s timeout
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
slog.Info("Attempting to connect to peer", "peer", peerInfo.ID, "addrs", peerInfo.Addrs)
|
||||||
|
if err := r.Host.Connect(connectCtx, *peerInfo); err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to %s: %w", peerInfo.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Successfully connected to peer", "peer", peerInfo.ID, "addrs", peerInfo.Addrs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectToRelay connects to another relay by its multiaddress.
|
||||||
|
func (r *Relay) ConnectToRelay(ctx context.Context, addr string) error {
|
||||||
|
ma, err := multiaddr.NewMultiaddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid multiaddress: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to extract peer info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.connectToRelay(ctx, peerInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printConnectInstructions logs the multiaddresses for connecting to this relay.
|
||||||
|
func printConnectInstructions(p2pHost host.Host) {
|
||||||
|
peerInfo := peer.AddrInfo{
|
||||||
|
ID: p2pHost.ID(),
|
||||||
|
Addrs: p2pHost.Addrs(),
|
||||||
|
}
|
||||||
|
addrs, err := peer.AddrInfoToP2pAddrs(&peerInfo)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to convert peer info to addresses", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Mesh connection addresses:")
|
||||||
|
for _, addr := range addrs {
|
||||||
|
slog.Info(fmt.Sprintf("> %s", addr.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
694
packages/relay/internal/core/protocol_stream.go
Normal file
694
packages/relay/internal/core/protocol_stream.go
Normal file
@@ -0,0 +1,694 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"relay/internal/common"
|
||||||
|
"relay/internal/connections"
|
||||||
|
"relay/internal/shared"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/network"
|
||||||
|
"github.com/libp2p/go-libp2p/core/peer"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO:s
|
||||||
|
// TODO: When disconnecting with stream open, causes crash on requester
|
||||||
|
// TODO: Need to trigger stream request if remote room is online and there are participants in local waiting
|
||||||
|
// TODO: Cleanup local room state when stream is closed upstream
|
||||||
|
|
||||||
|
// --- Protocol IDs ---
|
||||||
|
const (
|
||||||
|
protocolStreamRequest = "/nestri-relay/stream-request/1.0.0" // For requesting a stream from relay
|
||||||
|
protocolStreamPush = "/nestri-relay/stream-push/1.0.0" // For pushing a stream to relay
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Protocol Types ---
|
||||||
|
|
||||||
|
// StreamConnection is a connection between two relays for stream protocol
|
||||||
|
type StreamConnection struct {
|
||||||
|
pc *webrtc.PeerConnection
|
||||||
|
ndc *connections.NestriDataChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamProtocol deals with meshed stream forwarding
|
||||||
|
type StreamProtocol struct {
|
||||||
|
relay *Relay
|
||||||
|
servedConns *common.SafeMap[peer.ID, *StreamConnection] // peer ID -> StreamConnection (for served streams)
|
||||||
|
incomingConns *common.SafeMap[string, *StreamConnection] // room name -> StreamConnection (for incoming pushed streams)
|
||||||
|
requestedConns *common.SafeMap[string, *StreamConnection] // room name -> StreamConnection (for requested streams from other relays)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStreamProtocol(relay *Relay) *StreamProtocol {
|
||||||
|
protocol := &StreamProtocol{
|
||||||
|
relay: relay,
|
||||||
|
servedConns: common.NewSafeMap[peer.ID, *StreamConnection](),
|
||||||
|
incomingConns: common.NewSafeMap[string, *StreamConnection](),
|
||||||
|
requestedConns: common.NewSafeMap[string, *StreamConnection](),
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol.relay.Host.SetStreamHandler(protocolStreamRequest, protocol.handleStreamRequest)
|
||||||
|
protocol.relay.Host.SetStreamHandler(protocolStreamPush, protocol.handleStreamPush)
|
||||||
|
|
||||||
|
return protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Protocol Stream Handlers ---
|
||||||
|
|
||||||
|
// handleStreamRequest manages a request from another relay for a stream hosted locally
|
||||||
|
func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
|
||||||
|
brw := bufio.NewReadWriter(bufio.NewReader(stream), bufio.NewWriter(stream))
|
||||||
|
safeBRW := common.NewSafeBufioRW(brw)
|
||||||
|
|
||||||
|
iceHolder := make([]webrtc.ICECandidateInit, 0)
|
||||||
|
for {
|
||||||
|
data, err := safeBRW.Receive()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, network.ErrReset) {
|
||||||
|
slog.Debug("Stream request connection closed by peer", "peer", stream.Conn().RemotePeer())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Error("Failed to receive data", "err", err)
|
||||||
|
_ = stream.Reset()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseMsg connections.MessageBase
|
||||||
|
if err = json.Unmarshal(data, &baseMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal base message", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch baseMsg.Type {
|
||||||
|
case "request-stream-room":
|
||||||
|
var rawMsg connections.MessageRaw
|
||||||
|
if err = json.Unmarshal(data, &rawMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal raw message for room stream request", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomName string
|
||||||
|
if err = json.Unmarshal(rawMsg.Data, &roomName); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal room name from raw message", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Received stream request for room", "room", roomName)
|
||||||
|
room := sp.relay.GetRoomByName(roomName)
|
||||||
|
if room == nil || !room.IsOnline() || room.OwnerID != sp.relay.ID {
|
||||||
|
// TODO: Allow forward requests to other relays from here?
|
||||||
|
slog.Debug("Cannot provide stream for nil, offline or non-owned room", "room", roomName, "is_online", room != nil && room.IsOnline(), "is_owner", room != nil && room.OwnerID == sp.relay.ID)
|
||||||
|
// Respond with "request-stream-offline" message with room name
|
||||||
|
// TODO: Store the peer and send "online" message when the room comes online
|
||||||
|
roomNameData, err := json.Marshal(roomName)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to marshal room name for request stream offline", "room", roomName, "err", err)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if err = safeBRW.SendJSON(connections.NewMessageRaw(
|
||||||
|
"request-stream-offline",
|
||||||
|
roomNameData,
|
||||||
|
)); err != nil {
|
||||||
|
slog.Error("Failed to send request stream offline message", "room", roomName, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pc, err := common.CreatePeerConnection(func() {
|
||||||
|
slog.Info("PeerConnection closed for requested stream", "room", roomName)
|
||||||
|
// Cleanup the stream connection
|
||||||
|
if ok := sp.servedConns.Has(stream.Conn().RemotePeer()); ok {
|
||||||
|
sp.servedConns.Delete(stream.Conn().RemotePeer())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create PeerConnection for requested stream", "room", roomName, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tracks
|
||||||
|
if room.AudioTrack != nil {
|
||||||
|
if _, err = pc.AddTrack(room.AudioTrack); err != nil {
|
||||||
|
slog.Error("Failed to add audio track for requested stream", "room", roomName, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if room.VideoTrack != nil {
|
||||||
|
if _, err = pc.AddTrack(room.VideoTrack); err != nil {
|
||||||
|
slog.Error("Failed to add video track for requested stream", "room", roomName, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataChannel setup
|
||||||
|
settingOrdered := true
|
||||||
|
settingMaxRetransmits := uint16(2)
|
||||||
|
dc, err := pc.CreateDataChannel("relay-data", &webrtc.DataChannelInit{
|
||||||
|
Ordered: &settingOrdered,
|
||||||
|
MaxRetransmits: &settingMaxRetransmits,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create DataChannel for requested stream", "room", roomName, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ndc := connections.NewNestriDataChannel(dc)
|
||||||
|
|
||||||
|
ndc.RegisterOnOpen(func() {
|
||||||
|
slog.Debug("Relay DataChannel opened for requested stream", "room", roomName)
|
||||||
|
})
|
||||||
|
ndc.RegisterOnClose(func() {
|
||||||
|
slog.Debug("Relay DataChannel closed for requested stream", "room", roomName)
|
||||||
|
})
|
||||||
|
ndc.RegisterMessageCallback("input", func(data []byte) {
|
||||||
|
if room.DataChannel != nil {
|
||||||
|
if err = room.DataChannel.SendBinary(data); err != nil {
|
||||||
|
slog.Error("Failed to forward input message from mesh to upstream room", "room", roomName, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ICE Candidate handling
|
||||||
|
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||||
|
if candidate == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = safeBRW.SendJSON(connections.NewMessageICE("ice-candidate", candidate.ToJSON())); err != nil {
|
||||||
|
slog.Error("Failed to send ICE candidate message for requested stream", "room", roomName, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create offer
|
||||||
|
offer, err := pc.CreateOffer(nil)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create offer for requested stream", "room", roomName, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = pc.SetLocalDescription(offer); err != nil {
|
||||||
|
slog.Error("Failed to set local description for requested stream", "room", roomName, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = safeBRW.SendJSON(connections.NewMessageSDP("offer", offer)); err != nil {
|
||||||
|
slog.Error("Failed to send offer for requested stream", "room", roomName, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the connection
|
||||||
|
sp.servedConns.Set(stream.Conn().RemotePeer(), &StreamConnection{
|
||||||
|
pc: pc,
|
||||||
|
ndc: ndc,
|
||||||
|
})
|
||||||
|
|
||||||
|
slog.Debug("Sent offer for requested stream")
|
||||||
|
case "ice-candidate":
|
||||||
|
var iceMsg connections.MessageICE
|
||||||
|
if err := json.Unmarshal(data, &iceMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal ICE message", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if conn, ok := sp.servedConns.Get(stream.Conn().RemotePeer()); ok && conn.pc.RemoteDescription() != nil {
|
||||||
|
if err := conn.pc.AddICECandidate(iceMsg.Candidate); err != nil {
|
||||||
|
slog.Error("Failed to add ICE candidate", "err", err)
|
||||||
|
}
|
||||||
|
for _, heldIce := range iceHolder {
|
||||||
|
if err := conn.pc.AddICECandidate(heldIce); err != nil {
|
||||||
|
slog.Error("Failed to add held ICE candidate", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear the held candidates
|
||||||
|
iceHolder = make([]webrtc.ICECandidateInit, 0)
|
||||||
|
} else {
|
||||||
|
// Hold the candidate until remote description is set
|
||||||
|
iceHolder = append(iceHolder, iceMsg.Candidate)
|
||||||
|
}
|
||||||
|
case "answer":
|
||||||
|
var answerMsg connections.MessageSDP
|
||||||
|
if err := json.Unmarshal(data, &answerMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal answer from signaling message", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if conn, ok := sp.servedConns.Get(stream.Conn().RemotePeer()); ok {
|
||||||
|
if err := conn.pc.SetRemoteDescription(answerMsg.SDP); err != nil {
|
||||||
|
slog.Error("Failed to set remote description for answer", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slog.Debug("Set remote description for answer")
|
||||||
|
} else {
|
||||||
|
slog.Warn("Received answer without active PeerConnection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestStream manages the internals of the stream request
|
||||||
|
func (sp *StreamProtocol) requestStream(stream network.Stream, room *shared.Room) error {
|
||||||
|
brw := bufio.NewReadWriter(bufio.NewReader(stream), bufio.NewWriter(stream))
|
||||||
|
safeBRW := common.NewSafeBufioRW(brw)
|
||||||
|
|
||||||
|
slog.Debug("Requesting room stream from peer", "room", room.Name, "peer", stream.Conn().RemotePeer())
|
||||||
|
|
||||||
|
// Send room name to the remote peer
|
||||||
|
roomData, err := json.Marshal(room.Name)
|
||||||
|
if err != nil {
|
||||||
|
_ = stream.Close()
|
||||||
|
return fmt.Errorf("failed to marshal room name: %w", err)
|
||||||
|
}
|
||||||
|
if err = safeBRW.SendJSON(connections.NewMessageRaw(
|
||||||
|
"request-stream-room",
|
||||||
|
roomData,
|
||||||
|
)); err != nil {
|
||||||
|
_ = stream.Close()
|
||||||
|
return fmt.Errorf("failed to send room request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc, err := common.CreatePeerConnection(func() {
|
||||||
|
slog.Info("Relay PeerConnection closed for requested stream", "room", room.Name)
|
||||||
|
_ = stream.Close() // ignore error as may be closed already
|
||||||
|
// Cleanup the stream connection
|
||||||
|
if ok := sp.requestedConns.Has(room.Name); ok {
|
||||||
|
sp.requestedConns.Delete(room.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
_ = stream.Close()
|
||||||
|
return fmt.Errorf("failed to create PeerConnection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||||
|
localTrack, _ := webrtc.NewTrackLocalStaticRTP(track.Codec().RTPCodecCapability, track.ID(), "relay-"+room.Name+"-"+track.Kind().String())
|
||||||
|
slog.Debug("Received track for requested stream", "room", room.Name, "track_kind", track.Kind().String())
|
||||||
|
|
||||||
|
room.SetTrack(track.Kind(), localTrack)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
rtpPacket, _, err := track.ReadRTP()
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
slog.Error("Failed to read RTP packet for requested stream room", "room", room.Name, "err", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
err = localTrack.WriteRTP(rtpPacket)
|
||||||
|
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||||
|
slog.Error("Failed to write RTP to local track for requested stream room", "room", room.Name, "err", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
pc.OnDataChannel(func(dc *webrtc.DataChannel) {
|
||||||
|
ndc := connections.NewNestriDataChannel(dc)
|
||||||
|
ndc.RegisterOnOpen(func() {
|
||||||
|
slog.Debug("Relay DataChannel opened for requested stream", "room", room.Name)
|
||||||
|
})
|
||||||
|
ndc.RegisterOnClose(func() {
|
||||||
|
slog.Debug("Relay DataChannel closed for requested stream", "room", room.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set the DataChannel in the requestedConns map
|
||||||
|
if conn, ok := sp.requestedConns.Get(room.Name); ok {
|
||||||
|
conn.ndc = ndc
|
||||||
|
} else {
|
||||||
|
sp.requestedConns.Set(room.Name, &StreamConnection{
|
||||||
|
pc: pc,
|
||||||
|
ndc: ndc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not handle any messages from upstream here
|
||||||
|
})
|
||||||
|
|
||||||
|
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||||
|
if candidate == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = safeBRW.SendJSON(connections.NewMessageICE(
|
||||||
|
"ice-candidate",
|
||||||
|
candidate.ToJSON(),
|
||||||
|
)); err != nil {
|
||||||
|
slog.Error("Failed to send ICE candidate message for requested stream", "room", room.Name, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle incoming messages (offer and candidates)
|
||||||
|
go func() {
|
||||||
|
iceHolder := make([]webrtc.ICECandidateInit, 0)
|
||||||
|
|
||||||
|
for {
|
||||||
|
data, err := safeBRW.Receive()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, network.ErrReset) {
|
||||||
|
slog.Debug("Connection for requested stream closed by peer", "room", room.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Error("Failed to receive data for requested stream", "room", room.Name, "err", err)
|
||||||
|
_ = stream.Reset()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseMsg connections.MessageBase
|
||||||
|
if err = json.Unmarshal(data, &baseMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal base message for requested stream", "room", room.Name, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch baseMsg.Type {
|
||||||
|
case "ice-candidate":
|
||||||
|
var iceMsg connections.MessageICE
|
||||||
|
if err = json.Unmarshal(data, &iceMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal ICE candidate for requested stream", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if conn, ok := sp.requestedConns.Get(room.Name); ok && conn.pc.RemoteDescription() != nil {
|
||||||
|
if err = conn.pc.AddICECandidate(iceMsg.Candidate); err != nil {
|
||||||
|
slog.Error("Failed to add ICE candidate for requested stream", "room", room.Name, "err", err)
|
||||||
|
}
|
||||||
|
// Add held candidates
|
||||||
|
for _, heldCandidate := range iceHolder {
|
||||||
|
if err = conn.pc.AddICECandidate(heldCandidate); err != nil {
|
||||||
|
slog.Error("Failed to add held ICE candidate for requested stream", "room", room.Name, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear the held candidates
|
||||||
|
iceHolder = make([]webrtc.ICECandidateInit, 0)
|
||||||
|
} else {
|
||||||
|
// Hold the candidate until remote description is set
|
||||||
|
iceHolder = append(iceHolder, iceMsg.Candidate)
|
||||||
|
}
|
||||||
|
case "offer":
|
||||||
|
var offerMsg connections.MessageSDP
|
||||||
|
if err = json.Unmarshal(data, &offerMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal offer for requested stream", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = pc.SetRemoteDescription(offerMsg.SDP); err != nil {
|
||||||
|
slog.Error("Failed to set remote description for requested stream", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
answer, err := pc.CreateAnswer(nil)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create answer for requested stream", "room", room.Name, "err", err)
|
||||||
|
if err = stream.Reset(); err != nil {
|
||||||
|
slog.Error("Failed to reset stream for requested stream", "err", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = pc.SetLocalDescription(answer); err != nil {
|
||||||
|
slog.Error("Failed to set local description for requested stream", "room", room.Name, "err", err)
|
||||||
|
if err = stream.Reset(); err != nil {
|
||||||
|
slog.Error("Failed to reset stream for requested stream", "err", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = safeBRW.SendJSON(connections.NewMessageSDP(
|
||||||
|
"answer",
|
||||||
|
answer,
|
||||||
|
)); err != nil {
|
||||||
|
slog.Error("Failed to send answer for requested stream", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the connection
|
||||||
|
sp.requestedConns.Set(room.Name, &StreamConnection{
|
||||||
|
pc: pc,
|
||||||
|
ndc: nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
slog.Debug("Sent answer for requested stream", "room", room.Name)
|
||||||
|
default:
|
||||||
|
slog.Warn("Unknown signaling message type", "room", room.Name, "type", baseMsg.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleStreamPush manages a stream push from a node (nestri-server)
|
||||||
|
func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
|
||||||
|
brw := bufio.NewReadWriter(bufio.NewReader(stream), bufio.NewWriter(stream))
|
||||||
|
safeBRW := common.NewSafeBufioRW(brw)
|
||||||
|
|
||||||
|
var room *shared.Room
|
||||||
|
iceHolder := make([]webrtc.ICECandidateInit, 0)
|
||||||
|
for {
|
||||||
|
data, err := safeBRW.Receive()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, network.ErrReset) {
|
||||||
|
slog.Debug("Stream push connection closed by peer", "peer", stream.Conn().RemotePeer())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Error("Failed to receive data for stream push", "err", err)
|
||||||
|
_ = stream.Reset()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseMsg connections.MessageBase
|
||||||
|
if err = json.Unmarshal(data, &baseMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal base message from base message", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch baseMsg.Type {
|
||||||
|
case "push-stream-room":
|
||||||
|
var rawMsg connections.MessageRaw
|
||||||
|
if err = json.Unmarshal(data, &rawMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal room name from data", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomName string
|
||||||
|
if err = json.Unmarshal(rawMsg.Data, &roomName); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal room name from raw message", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Received stream push request for room", "room", roomName)
|
||||||
|
|
||||||
|
room = sp.relay.GetRoomByName(roomName)
|
||||||
|
if room != nil {
|
||||||
|
if room.OwnerID != sp.relay.ID {
|
||||||
|
slog.Error("Cannot push a stream to non-owned room", "room", room.Name, "owner_id", room.OwnerID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if room.IsOnline() {
|
||||||
|
slog.Error("Cannot push a stream to already online room", "room", room.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create a new room if it doesn't exist
|
||||||
|
room = sp.relay.CreateRoom(roomName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond with an OK with the room name
|
||||||
|
roomData, err := json.Marshal(room.Name)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to marshal room name for push stream response", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = safeBRW.SendJSON(connections.NewMessageRaw(
|
||||||
|
"push-stream-ok",
|
||||||
|
roomData,
|
||||||
|
)); err != nil {
|
||||||
|
slog.Error("Failed to send push stream OK response", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case "ice-candidate":
|
||||||
|
var iceMsg connections.MessageICE
|
||||||
|
if err = json.Unmarshal(data, &iceMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal ICE candidate from data", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if conn, ok := sp.incomingConns.Get(room.Name); ok && conn.pc.RemoteDescription() != nil {
|
||||||
|
if err = conn.pc.AddICECandidate(iceMsg.Candidate); err != nil {
|
||||||
|
slog.Error("Failed to add ICE candidate for pushed stream", "err", err)
|
||||||
|
}
|
||||||
|
for _, heldIce := range iceHolder {
|
||||||
|
if err := conn.pc.AddICECandidate(heldIce); err != nil {
|
||||||
|
slog.Error("Failed to add held ICE candidate for pushed stream", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear the held candidates
|
||||||
|
iceHolder = make([]webrtc.ICECandidateInit, 0)
|
||||||
|
} else {
|
||||||
|
// Hold the candidate until remote description is set
|
||||||
|
iceHolder = append(iceHolder, iceMsg.Candidate)
|
||||||
|
}
|
||||||
|
case "offer":
|
||||||
|
// Make sure we have room set to push to (set by "push-stream-room")
|
||||||
|
if room == nil {
|
||||||
|
slog.Error("Received offer without room set for stream push")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var offerMsg connections.MessageSDP
|
||||||
|
if err = json.Unmarshal(data, &offerMsg); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal offer from data", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create PeerConnection for the incoming stream
|
||||||
|
pc, err := common.CreatePeerConnection(func() {
|
||||||
|
slog.Info("PeerConnection closed for pushed stream", "room", room.Name)
|
||||||
|
// Cleanup the stream connection
|
||||||
|
if ok := sp.incomingConns.Has(room.Name); ok {
|
||||||
|
sp.incomingConns.Delete(room.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create PeerConnection for pushed stream", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.OnDataChannel(func(dc *webrtc.DataChannel) {
|
||||||
|
// TODO: Is this the best way to handle DataChannel? Should we just use the map directly?
|
||||||
|
room.DataChannel = connections.NewNestriDataChannel(dc)
|
||||||
|
room.DataChannel.RegisterOnOpen(func() {
|
||||||
|
slog.Debug("DataChannel opened for pushed stream", "room", room.Name)
|
||||||
|
})
|
||||||
|
room.DataChannel.RegisterOnClose(func() {
|
||||||
|
slog.Debug("DataChannel closed for pushed stream", "room", room.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set the DataChannel in the incomingConns map
|
||||||
|
if conn, ok := sp.incomingConns.Get(room.Name); ok {
|
||||||
|
conn.ndc = room.DataChannel
|
||||||
|
} else {
|
||||||
|
sp.incomingConns.Set(room.Name, &StreamConnection{
|
||||||
|
pc: pc,
|
||||||
|
ndc: room.DataChannel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||||
|
if candidate == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = safeBRW.SendJSON(connections.NewMessageICE(
|
||||||
|
"ice-candidate",
|
||||||
|
candidate.ToJSON(),
|
||||||
|
)); err != nil {
|
||||||
|
slog.Error("Failed to send ICE candidate message for pushed stream", "room", room.Name, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
pc.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||||
|
localTrack, err := webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, remoteTrack.Kind().String(), fmt.Sprintf("nestri-%s-%s", room.Name, remoteTrack.Kind().String()))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create local track for pushed stream", "room", room.Name, "track_kind", remoteTrack.Kind().String(), "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Received track for pushed stream", "room", room.Name, "track_kind", remoteTrack.Kind().String())
|
||||||
|
|
||||||
|
// Set track for Room
|
||||||
|
room.SetTrack(remoteTrack.Kind(), localTrack)
|
||||||
|
|
||||||
|
// Prepare PlayoutDelayExtension so we don't need to recreate it for each packet
|
||||||
|
playoutExt := &rtp.PlayoutDelayExtension{
|
||||||
|
MinDelay: 0,
|
||||||
|
MaxDelay: 0,
|
||||||
|
}
|
||||||
|
playoutPayload, err := playoutExt.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to marshal PlayoutDelayExtension for room", "room", room.Name, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
rtpPacket, _, err := remoteTrack.ReadRTP()
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
slog.Error("Failed to read RTP from remote track for room", "room", room.Name, "err", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use PlayoutDelayExtension for low latency, if set for this track kind
|
||||||
|
if extID, ok := common.GetExtension(remoteTrack.Kind(), common.ExtensionPlayoutDelay); ok {
|
||||||
|
if err := rtpPacket.SetExtension(extID, playoutPayload); err != nil {
|
||||||
|
slog.Error("Failed to set PlayoutDelayExtension for room", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = localTrack.WriteRTP(rtpPacket)
|
||||||
|
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||||
|
slog.Error("Failed to write RTP to local track for room", "room", room.Name, "err", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Track closed for room", "room", room.Name, "track_kind", remoteTrack.Kind().String())
|
||||||
|
|
||||||
|
// Cleanup the track from the room
|
||||||
|
room.SetTrack(remoteTrack.Kind(), nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set the remote description
|
||||||
|
if err = pc.SetRemoteDescription(offerMsg.SDP); err != nil {
|
||||||
|
slog.Error("Failed to set remote description for pushed stream", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slog.Debug("Set remote description for pushed stream", "room", room.Name)
|
||||||
|
|
||||||
|
// Create an answer
|
||||||
|
answer, err := pc.CreateAnswer(nil)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create answer for pushed stream", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = pc.SetLocalDescription(answer); err != nil {
|
||||||
|
slog.Error("Failed to set local description for pushed stream", "room", room.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = safeBRW.SendJSON(connections.NewMessageSDP(
|
||||||
|
"answer",
|
||||||
|
answer,
|
||||||
|
)); err != nil {
|
||||||
|
slog.Error("Failed to send answer for pushed stream", "room", room.Name, "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the connection
|
||||||
|
sp.incomingConns.Set(room.Name, &StreamConnection{
|
||||||
|
pc: pc,
|
||||||
|
ndc: room.DataChannel, // if it exists, if not it will be set later
|
||||||
|
})
|
||||||
|
slog.Debug("Sent answer for pushed stream", "room", room.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Public Usable Methods ---
|
||||||
|
|
||||||
|
// RequestStream sends a request to get room stream from another relay
|
||||||
|
func (sp *StreamProtocol) RequestStream(ctx context.Context, room *shared.Room, peerID peer.ID) error {
|
||||||
|
stream, err := sp.relay.Host.NewStream(ctx, peerID, protocolStreamRequest)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stream request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sp.requestStream(stream, room)
|
||||||
|
}
|
||||||
13
packages/relay/internal/core/protocols.go
Normal file
13
packages/relay/internal/core/protocols.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// ProtocolRegistry is a type holding all protocols to split away the bloat
|
||||||
|
type ProtocolRegistry struct {
|
||||||
|
StreamProtocol *StreamProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProtocolRegistry initializes and returns a new protocol registry
|
||||||
|
func NewProtocolRegistry(relay *Relay) ProtocolRegistry {
|
||||||
|
return ProtocolRegistry{
|
||||||
|
StreamProtocol: NewStreamProtocol(relay),
|
||||||
|
}
|
||||||
|
}
|
||||||
108
packages/relay/internal/core/room.go
Normal file
108
packages/relay/internal/core/room.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"relay/internal/shared"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/network"
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Room Management ---
|
||||||
|
|
||||||
|
// GetRoomByID retrieves a local Room struct by its ULID
|
||||||
|
func (r *Relay) GetRoomByID(id ulid.ULID) *shared.Room {
|
||||||
|
if room, ok := r.LocalRooms.Get(id); ok {
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoomByName retrieves a local Room struct by its name
|
||||||
|
func (r *Relay) GetRoomByName(name string) *shared.Room {
|
||||||
|
for _, room := range r.LocalRooms.Copy() {
|
||||||
|
if room.Name == name {
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRoom creates a new local Room struct with the given name
|
||||||
|
func (r *Relay) CreateRoom(name string) *shared.Room {
|
||||||
|
roomID := ulid.Make()
|
||||||
|
room := shared.NewRoom(name, roomID, r.ID)
|
||||||
|
r.LocalRooms.Set(room.ID, room)
|
||||||
|
slog.Debug("Created new local room", "room", name, "id", room.ID)
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRoomIfEmpty checks if a local room struct is inactive and can be removed
|
||||||
|
func (r *Relay) DeleteRoomIfEmpty(room *shared.Room) {
|
||||||
|
if room == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if room.Participants.Len() == 0 && r.LocalRooms.Has(room.ID) {
|
||||||
|
slog.Debug("Deleting empty room without participants", "room", room.Name)
|
||||||
|
r.LocalRooms.Delete(room.ID)
|
||||||
|
err := room.PeerConnection.Close()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to close Room PeerConnection", "room", room.Name, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRemoteRoomByName returns room from mesh by name
|
||||||
|
func (r *Relay) GetRemoteRoomByName(roomName string) *shared.RoomInfo {
|
||||||
|
for _, room := range r.MeshRooms.Copy() {
|
||||||
|
if room.Name == roomName && room.OwnerID != r.ID {
|
||||||
|
// Make sure connection is alive
|
||||||
|
if r.Host.Network().Connectedness(room.OwnerID) == network.Connected {
|
||||||
|
return &room
|
||||||
|
} else {
|
||||||
|
slog.Debug("Removing stale peer, owns a room without connection", "room", roomName, "peer", room.OwnerID)
|
||||||
|
r.onPeerDisconnected(room.OwnerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- State Publishing ---
|
||||||
|
|
||||||
|
// publishRoomStates publishes the state of all rooms currently owned by *this* relay
|
||||||
|
func (r *Relay) publishRoomStates(ctx context.Context) error {
|
||||||
|
if r.pubTopicState == nil {
|
||||||
|
slog.Warn("Cannot publish room states: topic is nil")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var statesToPublish []shared.RoomInfo
|
||||||
|
r.LocalRooms.Range(func(id ulid.ULID, room *shared.Room) bool {
|
||||||
|
// Only publish state for rooms owned by this relay
|
||||||
|
if room.OwnerID == r.ID {
|
||||||
|
statesToPublish = append(statesToPublish, shared.RoomInfo{
|
||||||
|
ID: room.ID,
|
||||||
|
Name: room.Name,
|
||||||
|
OwnerID: r.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true // Continue iteration
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(statesToPublish) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(statesToPublish)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal local room states: %w", err)
|
||||||
|
}
|
||||||
|
if pubErr := r.pubTopicState.Publish(ctx, data); pubErr != nil {
|
||||||
|
slog.Error("Failed to publish room states message", "err", pubErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
173
packages/relay/internal/core/state.go
Normal file
173
packages/relay/internal/core/state.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"relay/internal/shared"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||||
|
"github.com/libp2p/go-libp2p/core/network"
|
||||||
|
"github.com/libp2p/go-libp2p/core/peer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- PubSub Message Handlers ---
|
||||||
|
|
||||||
|
// handleRoomStateMessages processes incoming room state updates from peers.
|
||||||
|
func (r *Relay) handleRoomStateMessages(ctx context.Context, sub *pubsub.Subscription) {
|
||||||
|
slog.Debug("Starting room state message handler...")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
slog.Info("Stopping room state message handler")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
msg, err := sub.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, pubsub.ErrSubscriptionCancelled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
slog.Info("Room state subscription ended", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Error("Error receiving room state message", "err", err)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if msg.GetFrom() == r.Host.ID() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var states []shared.RoomInfo
|
||||||
|
if err := json.Unmarshal(msg.Data, &states); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal room states", "from", msg.GetFrom(), "data_len", len(msg.Data), "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.updateMeshRoomStates(msg.GetFrom(), states)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRelayMetricsMessages processes incoming status updates from peers.
|
||||||
|
func (r *Relay) handleRelayMetricsMessages(ctx context.Context, sub *pubsub.Subscription) {
|
||||||
|
slog.Debug("Starting relay metrics message handler...")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
slog.Info("Stopping relay metrics message handler")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
msg, err := sub.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, pubsub.ErrSubscriptionCancelled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
slog.Info("Relay metrics subscription ended", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Error("Error receiving relay metrics message", "err", err)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if msg.GetFrom() == r.Host.ID() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var info RelayInfo
|
||||||
|
if err := json.Unmarshal(msg.Data, &info); err != nil {
|
||||||
|
slog.Error("Failed to unmarshal relay status", "from", msg.GetFrom(), "data_len", len(msg.Data), "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info.ID != msg.GetFrom() {
|
||||||
|
slog.Error("Peer ID mismatch in relay status", "expected", info.ID, "actual", msg.GetFrom())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.onPeerStatus(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- State Check Functions ---
|
||||||
|
// hasConnectedPeer checks if peer is in map and has a valid connection
|
||||||
|
func (r *Relay) hasConnectedPeer(peerID peer.ID) bool {
|
||||||
|
if _, ok := r.LocalMeshPeers.Get(peerID); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.Host.Network().Connectedness(peerID) != network.Connected {
|
||||||
|
slog.Debug("Peer not connected", "peer", peerID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- State Change Functions ---
|
||||||
|
|
||||||
|
// onPeerStatus updates the status of a peer based on received metrics, adding local perspective
|
||||||
|
func (r *Relay) onPeerStatus(recvInfo RelayInfo) {
|
||||||
|
r.LocalMeshPeers.Set(recvInfo.ID, &recvInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// onPeerConnected is called when a new peer connects to the relay
|
||||||
|
func (r *Relay) onPeerConnected(peerID peer.ID) {
|
||||||
|
// Add to local peer map
|
||||||
|
r.LocalMeshPeers.Set(peerID, &RelayInfo{
|
||||||
|
ID: peerID,
|
||||||
|
})
|
||||||
|
|
||||||
|
slog.Info("Peer connected", "peer", peerID)
|
||||||
|
|
||||||
|
// Trigger immediate state exchange
|
||||||
|
go func() {
|
||||||
|
if err := r.publishRelayMetrics(context.Background()); err != nil {
|
||||||
|
slog.Error("Failed to publish relay metrics on connect", "err", err)
|
||||||
|
} else {
|
||||||
|
if err = r.publishRoomStates(context.Background()); err != nil {
|
||||||
|
slog.Error("Failed to publish room states on connect", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// onPeerDisconnected marks a peer as disconnected in our status view and removes latency info
|
||||||
|
func (r *Relay) onPeerDisconnected(peerID peer.ID) {
|
||||||
|
slog.Info("Mesh peer disconnected, deleting from local peer map", "peer", peerID)
|
||||||
|
// Remove peer from local mesh peers
|
||||||
|
if r.LocalMeshPeers.Has(peerID) {
|
||||||
|
r.LocalMeshPeers.Delete(peerID)
|
||||||
|
}
|
||||||
|
// Remove any rooms associated with this peer
|
||||||
|
if r.MeshRooms.Has(peerID.String()) {
|
||||||
|
r.MeshRooms.Delete(peerID.String())
|
||||||
|
}
|
||||||
|
// Remove any latencies associated with this peer
|
||||||
|
if r.LocalMeshPeers.Has(peerID) {
|
||||||
|
r.LocalMeshPeers.Delete(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: If any rooms were routed through this peer, handle that case
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateMeshRoomStates merges received room states into the MeshRooms map
|
||||||
|
// TODO: Wrap in another type with timestamp or another mechanism to avoid conflicts
|
||||||
|
func (r *Relay) updateMeshRoomStates(peerID peer.ID, states []shared.RoomInfo) {
|
||||||
|
for _, state := range states {
|
||||||
|
if state.OwnerID == r.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If previously did not exist, but does now, request a connection if participants exist for our room
|
||||||
|
existed := r.MeshRooms.Has(state.ID.String())
|
||||||
|
if !existed {
|
||||||
|
// Request connection to this peer if we have participants in our local room
|
||||||
|
if room, ok := r.LocalRooms.Get(state.ID); ok {
|
||||||
|
if room.Participants.Len() > 0 {
|
||||||
|
slog.Debug("Got new remote room state, we locally have participants for, requesting stream", "room_name", room.Name, "peer", peerID)
|
||||||
|
if err := r.StreamProtocol.RequestStream(context.Background(), room, peerID); err != nil {
|
||||||
|
slog.Error("Failed to request stream for new remote room state", "room_name", room.Name, "peer", peerID, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.MeshRooms.Set(state.ID.String(), state)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/pion/webrtc/v4"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"log/slog"
|
|
||||||
"relay/internal/common"
|
|
||||||
"relay/internal/connections"
|
|
||||||
gen "relay/internal/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParticipantHandler(participant *Participant, room *Room, relay *Relay) {
|
|
||||||
onPCClose := func() {
|
|
||||||
slog.Debug("Participant PeerConnection closed", "participant", participant.ID, "room", room.Name)
|
|
||||||
room.removeParticipantByID(participant.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
participant.PeerConnection, err = common.CreatePeerConnection(onPCClose)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create participant PeerConnection", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data channel settings
|
|
||||||
settingOrdered := true
|
|
||||||
settingMaxRetransmits := uint16(0)
|
|
||||||
dc, err := participant.PeerConnection.CreateDataChannel("data", &webrtc.DataChannelInit{
|
|
||||||
Ordered: &settingOrdered,
|
|
||||||
MaxRetransmits: &settingMaxRetransmits,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create data channel for participant", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
participant.DataChannel = connections.NewNestriDataChannel(dc)
|
|
||||||
|
|
||||||
// Register channel opening handling
|
|
||||||
participant.DataChannel.RegisterOnOpen(func() {
|
|
||||||
slog.Debug("DataChannel opened for participant", "participant", participant.ID, "room", room.Name)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register channel closing handling
|
|
||||||
participant.DataChannel.RegisterOnClose(func() {
|
|
||||||
slog.Debug("DataChannel closed for participant", "participant", participant.ID, "room", room.Name)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register text message handling
|
|
||||||
participant.DataChannel.RegisterMessageCallback("input", func(data []byte) {
|
|
||||||
ForwardParticipantDataChannelMessage(participant, room, data)
|
|
||||||
})
|
|
||||||
|
|
||||||
participant.PeerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
|
||||||
if candidate == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := participant.WebSocket.SendICECandidateMessageWS(candidate.ToJSON()); err != nil {
|
|
||||||
slog.Error("Failed to send ICE candidate to participant", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
iceHolder := make([]webrtc.ICECandidateInit, 0)
|
|
||||||
|
|
||||||
// ICE callback
|
|
||||||
participant.WebSocket.RegisterMessageCallback("ice", func(data []byte) {
|
|
||||||
var iceMsg connections.MessageICECandidate
|
|
||||||
if err = json.Unmarshal(data, &iceMsg); err != nil {
|
|
||||||
slog.Error("Failed to decode ICE candidate message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if participant.PeerConnection.RemoteDescription() != nil {
|
|
||||||
if err = participant.PeerConnection.AddICECandidate(iceMsg.Candidate); err != nil {
|
|
||||||
slog.Error("Failed to add ICE candidate for participant", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
// Add held ICE candidates
|
|
||||||
for _, heldCandidate := range iceHolder {
|
|
||||||
if err = participant.PeerConnection.AddICECandidate(heldCandidate); err != nil {
|
|
||||||
slog.Error("Failed to add held ICE candidate for participant", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
iceHolder = nil
|
|
||||||
} else {
|
|
||||||
iceHolder = append(iceHolder, iceMsg.Candidate)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// SDP answer callback
|
|
||||||
participant.WebSocket.RegisterMessageCallback("sdp", func(data []byte) {
|
|
||||||
var sdpMsg connections.MessageSDP
|
|
||||||
if err = json.Unmarshal(data, &sdpMsg); err != nil {
|
|
||||||
slog.Error("Failed to decode SDP message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handleParticipantSDP(participant, sdpMsg)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Log callback
|
|
||||||
participant.WebSocket.RegisterMessageCallback("log", func(data []byte) {
|
|
||||||
var logMsg connections.MessageLog
|
|
||||||
if err = json.Unmarshal(data, &logMsg); err != nil {
|
|
||||||
slog.Error("Failed to decode log message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: Handle log message sending to metrics server
|
|
||||||
})
|
|
||||||
|
|
||||||
// Metrics callback
|
|
||||||
participant.WebSocket.RegisterMessageCallback("metrics", func(data []byte) {
|
|
||||||
// Ignore for now
|
|
||||||
})
|
|
||||||
|
|
||||||
participant.WebSocket.RegisterOnClose(func() {
|
|
||||||
slog.Debug("WebSocket closed for participant", "participant", participant.ID, "room", room.Name)
|
|
||||||
// Remove from Room
|
|
||||||
room.removeParticipantByID(participant.ID)
|
|
||||||
})
|
|
||||||
|
|
||||||
slog.Info("Participant ready, sending OK answer", "participant", participant.ID, "room", room.Name)
|
|
||||||
if err := participant.WebSocket.SendAnswerMessageWS(connections.AnswerOK); err != nil {
|
|
||||||
slog.Error("Failed to send OK answer", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If room is online, also send offer
|
|
||||||
if room.Online {
|
|
||||||
if err = room.signalParticipantWithTracks(participant); err != nil {
|
|
||||||
slog.Error("Failed to signal participant with tracks", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
active, provider := relay.IsRoomActive(room.ID)
|
|
||||||
if active {
|
|
||||||
slog.Debug("Room active remotely, requesting stream", "room", room.Name, "provider", provider)
|
|
||||||
if _, err := relay.requestStream(context.Background(), room.Name, room.ID, provider); err != nil {
|
|
||||||
slog.Error("Failed to request stream", "room", room.Name, "err", err)
|
|
||||||
} else {
|
|
||||||
slog.Debug("Stream requested successfully", "room", room.Name, "provider", provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SDP answer handler for participants
|
|
||||||
func handleParticipantSDP(participant *Participant, answerMsg connections.MessageSDP) {
|
|
||||||
// Get SDP offer
|
|
||||||
sdpAnswer := answerMsg.SDP.SDP
|
|
||||||
|
|
||||||
// Set remote description
|
|
||||||
err := participant.PeerConnection.SetRemoteDescription(webrtc.SessionDescription{
|
|
||||||
Type: webrtc.SDPTypeAnswer,
|
|
||||||
SDP: sdpAnswer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to set remote SDP answer for participant", "participant", participant.ID, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ForwardParticipantDataChannelMessage(participant *Participant, room *Room, data []byte) {
|
|
||||||
// Debug mode: Add latency timestamp
|
|
||||||
if common.GetFlags().Debug {
|
|
||||||
var inputMsg gen.ProtoMessageInput
|
|
||||||
if err := proto.Unmarshal(data, &inputMsg); err != nil {
|
|
||||||
slog.Error("Failed to decode input message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
protoLat := inputMsg.GetMessageBase().GetLatency()
|
|
||||||
if protoLat != nil {
|
|
||||||
lat := common.LatencyTrackerFromProto(protoLat)
|
|
||||||
lat.AddTimestamp("relay_to_node")
|
|
||||||
protoLat = lat.ToProto()
|
|
||||||
}
|
|
||||||
if newData, err := proto.Marshal(&inputMsg); err != nil {
|
|
||||||
slog.Error("Failed to marshal input message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// Update data with the modified message
|
|
||||||
data = newData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward to local room DataChannel if it exists (e.g., local ingest)
|
|
||||||
if room.DataChannel != nil {
|
|
||||||
if err := room.DataChannel.SendBinary(data); err != nil {
|
|
||||||
slog.Error("Failed to send input message to room", "participant", participant.ID, "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/libp2p/go-reuseport"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"relay/internal/common"
|
|
||||||
"relay/internal/connections"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var httpMux *http.ServeMux
|
|
||||||
|
|
||||||
func InitHTTPEndpoint(_ context.Context, ctxCancel context.CancelFunc) error {
|
|
||||||
// Create HTTP mux which serves our WS endpoint
|
|
||||||
httpMux = http.NewServeMux()
|
|
||||||
|
|
||||||
// Endpoints themselves
|
|
||||||
httpMux.Handle("/", http.NotFoundHandler())
|
|
||||||
// If control endpoint secret is set, enable the control endpoint
|
|
||||||
if len(common.GetFlags().ControlSecret) > 0 {
|
|
||||||
httpMux.HandleFunc("/api/control", corsAnyHandler(controlHandler))
|
|
||||||
}
|
|
||||||
// WS endpoint
|
|
||||||
httpMux.HandleFunc("/api/ws/{roomName}", corsAnyHandler(wsHandler))
|
|
||||||
|
|
||||||
// Get our serving port
|
|
||||||
port := common.GetFlags().EndpointPort
|
|
||||||
tlsCert := common.GetFlags().TLSCert
|
|
||||||
tlsKey := common.GetFlags().TLSKey
|
|
||||||
|
|
||||||
// Create re-usable listener port
|
|
||||||
httpListener, err := reuseport.Listen("tcp", ":"+strconv.Itoa(port))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create TCP listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log and start the endpoint server
|
|
||||||
if len(tlsCert) <= 0 && len(tlsKey) <= 0 {
|
|
||||||
slog.Info("Starting HTTP endpoint server", "port", port)
|
|
||||||
go func() {
|
|
||||||
if err := http.Serve(httpListener, httpMux); err != nil {
|
|
||||||
slog.Error("Failed to start HTTP server", "err", err)
|
|
||||||
ctxCancel()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else if len(tlsCert) > 0 && len(tlsKey) > 0 {
|
|
||||||
slog.Info("Starting HTTPS endpoint server", "port", port)
|
|
||||||
go func() {
|
|
||||||
if err := http.ServeTLS(httpListener, httpMux, tlsCert, tlsKey); err != nil {
|
|
||||||
slog.Error("Failed to start HTTPS server", "err", err)
|
|
||||||
ctxCancel()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
return errors.New("no TLS certificate or TLS key provided")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// logHTTPError logs (if verbose) and sends an error code to requester
|
|
||||||
func logHTTPError(w http.ResponseWriter, err string, code int) {
|
|
||||||
if common.GetFlags().Verbose {
|
|
||||||
slog.Error("HTTP error", "code", code, "message", err)
|
|
||||||
}
|
|
||||||
http.Error(w, err, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// corsAnyHandler allows any origin to access the endpoint
|
|
||||||
func corsAnyHandler(next func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
|
||||||
return func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
// Allow all origins
|
|
||||||
res.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
res.Header().Set("Access-Control-Allow-Methods", "*")
|
|
||||||
res.Header().Set("Access-Control-Allow-Headers", "*")
|
|
||||||
|
|
||||||
if req.Method != http.MethodOptions {
|
|
||||||
next(res, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wsHandler is the handler for the /api/ws/{roomName} endpoint
|
|
||||||
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Get given room name now
|
|
||||||
roomName := r.PathValue("roomName")
|
|
||||||
if len(roomName) <= 0 {
|
|
||||||
logHTTPError(w, "no room name given", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rel := GetRelay()
|
|
||||||
// Get or create room in any case
|
|
||||||
room := rel.GetOrCreateRoom(roomName)
|
|
||||||
|
|
||||||
// Upgrade to WebSocket
|
|
||||||
upgrader := websocket.Upgrader{
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
wsConn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
logHTTPError(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create SafeWebSocket
|
|
||||||
ws := connections.NewSafeWebSocket(wsConn)
|
|
||||||
// Assign message handler for join request
|
|
||||||
ws.RegisterMessageCallback("join", func(data []byte) {
|
|
||||||
var joinMsg connections.MessageJoin
|
|
||||||
if err = json.Unmarshal(data, &joinMsg); err != nil {
|
|
||||||
slog.Error("Failed to unmarshal join message", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("Join message", "room", room.Name, "joinerType", joinMsg.JoinerType)
|
|
||||||
|
|
||||||
// Handle join request, depending if it's from ingest/node or participant/client
|
|
||||||
switch joinMsg.JoinerType {
|
|
||||||
case connections.JoinerNode:
|
|
||||||
// If room already online, send InUse answer
|
|
||||||
if room.Online {
|
|
||||||
if err = ws.SendAnswerMessageWS(connections.AnswerInUse); err != nil {
|
|
||||||
slog.Error("Failed to send InUse answer to node", "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
room.AssignWebSocket(ws)
|
|
||||||
go IngestHandler(room)
|
|
||||||
case connections.JoinerClient:
|
|
||||||
// Create participant and add to room regardless of online status
|
|
||||||
participant := NewParticipant(ws)
|
|
||||||
room.AddParticipant(participant)
|
|
||||||
// If room not online, send Offline answer
|
|
||||||
if !room.Online {
|
|
||||||
if err = ws.SendAnswerMessageWS(connections.AnswerOffline); err != nil {
|
|
||||||
slog.Error("Failed to send offline answer to participant", "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go ParticipantHandler(participant, room, rel)
|
|
||||||
default:
|
|
||||||
slog.Error("Unknown joiner type", "joinerType", joinMsg.JoinerType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unregister ourselves, if something happens on the other side they should just reconnect?
|
|
||||||
ws.UnregisterMessageCallback("join")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// controlMessage is the JSON struct for the control messages
|
|
||||||
type controlMessage struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// controlHandler is the handler for the /api/control endpoint, for controlling this relay
|
|
||||||
func controlHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Check for control secret in Authorization header
|
|
||||||
authHeader := r.Header.Get("Authorization")
|
|
||||||
if len(authHeader) <= 0 || authHeader != common.GetFlags().ControlSecret {
|
|
||||||
logHTTPError(w, "missing or invalid Authorization header", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle CORS preflight request
|
|
||||||
if r.Method == http.MethodOptions {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode the control message
|
|
||||||
var msg controlMessage
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
|
||||||
logHTTPError(w, "failed to decode control message", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//relay := GetRelay()
|
|
||||||
switch msg.Type {
|
|
||||||
case "join_mesh":
|
|
||||||
// Join the mesh network, get relay address from msg.Value
|
|
||||||
if len(msg.Value) <= 0 {
|
|
||||||
logHTTPError(w, "missing relay address", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx := r.Context()
|
|
||||||
if err := GetRelay().ConnectToRelay(ctx, msg.Value); err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("Failed to connect: %v", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write([]byte("Successfully connected to relay"))
|
|
||||||
default:
|
|
||||||
logHTTPError(w, "unknown control message type", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/pion/rtp"
|
|
||||||
"github.com/pion/webrtc/v4"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"relay/internal/common"
|
|
||||||
"relay/internal/connections"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IngestHandler(room *Room) {
|
|
||||||
relay := GetRelay()
|
|
||||||
|
|
||||||
// Callback for closing PeerConnection
|
|
||||||
onPCClose := func() {
|
|
||||||
slog.Debug("ingest PeerConnection closed", "room", room.Name)
|
|
||||||
room.Online = false
|
|
||||||
room.signalParticipantsOffline()
|
|
||||||
relay.DeleteRoomIfEmpty(room)
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
room.PeerConnection, err = common.CreatePeerConnection(onPCClose)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create ingest PeerConnection", "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
room.PeerConnection.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
|
||||||
localTrack, err := webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, remoteTrack.Kind().String(), fmt.Sprintf("nestri-%s-%s", room.Name, remoteTrack.Kind().String()))
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create local track for room", "room", room.Name, "kind", remoteTrack.Kind(), "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
slog.Debug("Received track for room", "room", room.Name, "kind", remoteTrack.Kind())
|
|
||||||
|
|
||||||
// Set track and let Room handle state
|
|
||||||
room.SetTrack(remoteTrack.Kind(), localTrack)
|
|
||||||
|
|
||||||
// Prepare PlayoutDelayExtension so we don't need to recreate it for each packet
|
|
||||||
playoutExt := &rtp.PlayoutDelayExtension{
|
|
||||||
MinDelay: 0,
|
|
||||||
MaxDelay: 0,
|
|
||||||
}
|
|
||||||
playoutPayload, err := playoutExt.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to marshal PlayoutDelayExtension for room", "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
rtpPacket, _, err := remoteTrack.ReadRTP()
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, io.EOF) {
|
|
||||||
slog.Error("Failed to read RTP from remote track for room", "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use PlayoutDelayExtension for low latency, only for video tracks
|
|
||||||
if err := rtpPacket.SetExtension(common.ExtensionMap[common.ExtensionPlayoutDelay], playoutPayload); err != nil {
|
|
||||||
slog.Error("Failed to set PlayoutDelayExtension for room", "room", room.Name, "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = localTrack.WriteRTP(rtpPacket)
|
|
||||||
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
|
||||||
slog.Error("Failed to write RTP to local track for room", "room", room.Name, "err", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("Track closed for room", "room", room.Name, "kind", remoteTrack.Kind())
|
|
||||||
|
|
||||||
// Clear track when done
|
|
||||||
room.SetTrack(remoteTrack.Kind(), nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
room.PeerConnection.OnDataChannel(func(dc *webrtc.DataChannel) {
|
|
||||||
room.DataChannel = connections.NewNestriDataChannel(dc)
|
|
||||||
slog.Debug("Ingest received DataChannel for room", "room", room.Name)
|
|
||||||
|
|
||||||
room.DataChannel.RegisterOnOpen(func() {
|
|
||||||
slog.Debug("ingest DataChannel opened for room", "room", room.Name)
|
|
||||||
})
|
|
||||||
|
|
||||||
room.DataChannel.OnClose(func() {
|
|
||||||
slog.Debug("ingest DataChannel closed for room", "room", room.Name)
|
|
||||||
})
|
|
||||||
|
|
||||||
// We do not handle any messages from ingest via DataChannel yet
|
|
||||||
})
|
|
||||||
|
|
||||||
room.PeerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
|
||||||
if candidate == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
slog.Debug("ingest received ICECandidate for room", "room", room.Name)
|
|
||||||
err = room.WebSocket.SendICECandidateMessageWS(candidate.ToJSON())
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to send ICE candidate message to ingest for room", "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
iceHolder := make([]webrtc.ICECandidateInit, 0)
|
|
||||||
|
|
||||||
// ICE callback
|
|
||||||
room.WebSocket.RegisterMessageCallback("ice", func(data []byte) {
|
|
||||||
var iceMsg connections.MessageICECandidate
|
|
||||||
if err = json.Unmarshal(data, &iceMsg); err != nil {
|
|
||||||
slog.Error("Failed to decode ICE candidate message from ingest for room", "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if room.PeerConnection != nil {
|
|
||||||
if room.PeerConnection.RemoteDescription() != nil {
|
|
||||||
if err = room.PeerConnection.AddICECandidate(iceMsg.Candidate); err != nil {
|
|
||||||
slog.Error("Failed to add ICE candidate for room", "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
for _, heldCandidate := range iceHolder {
|
|
||||||
if err = room.PeerConnection.AddICECandidate(heldCandidate); err != nil {
|
|
||||||
slog.Error("Failed to add held ICE candidate for room", "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
iceHolder = make([]webrtc.ICECandidateInit, 0)
|
|
||||||
} else {
|
|
||||||
iceHolder = append(iceHolder, iceMsg.Candidate)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
slog.Error("ICE candidate received but PeerConnection is nil for room", "room", room.Name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// SDP offer callback
|
|
||||||
room.WebSocket.RegisterMessageCallback("sdp", func(data []byte) {
|
|
||||||
var sdpMsg connections.MessageSDP
|
|
||||||
if err = json.Unmarshal(data, &sdpMsg); err != nil {
|
|
||||||
slog.Error("Failed to decode SDP message from ingest for room", "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
answer := handleIngestSDP(room, sdpMsg)
|
|
||||||
if answer != nil {
|
|
||||||
if err = room.WebSocket.SendSDPMessageWS(*answer); err != nil {
|
|
||||||
slog.Error("Failed to send SDP answer message to ingest for room", "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
slog.Error("Failed to handle ingest SDP message for room", "room", room.Name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Log callback
|
|
||||||
room.WebSocket.RegisterMessageCallback("log", func(data []byte) {
|
|
||||||
var logMsg connections.MessageLog
|
|
||||||
if err = json.Unmarshal(data, &logMsg); err != nil {
|
|
||||||
slog.Error("Failed to decode log message from ingest for room", "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: Handle log message sending to metrics server
|
|
||||||
})
|
|
||||||
|
|
||||||
// Metrics callback
|
|
||||||
room.WebSocket.RegisterMessageCallback("metrics", func(data []byte) {
|
|
||||||
var metricsMsg connections.MessageMetrics
|
|
||||||
if err = json.Unmarshal(data, &metricsMsg); err != nil {
|
|
||||||
slog.Error("Failed to decode metrics message from ingest for room", "room", room.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: Handle metrics message sending to metrics server
|
|
||||||
})
|
|
||||||
|
|
||||||
room.WebSocket.RegisterOnClose(func() {
|
|
||||||
slog.Debug("ingest WebSocket closed for room", "room", room.Name)
|
|
||||||
room.Online = false
|
|
||||||
room.signalParticipantsOffline()
|
|
||||||
relay.DeleteRoomIfEmpty(room)
|
|
||||||
})
|
|
||||||
|
|
||||||
slog.Info("Room is ready, sending OK answer to ingest", "room", room.Name)
|
|
||||||
if err = room.WebSocket.SendAnswerMessageWS(connections.AnswerOK); err != nil {
|
|
||||||
slog.Error("Failed to send OK answer message to ingest for room", "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SDP offer handler, returns SDP answer
|
|
||||||
func handleIngestSDP(room *Room, offerMsg connections.MessageSDP) *webrtc.SessionDescription {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
sdpOffer := offerMsg.SDP.SDP
|
|
||||||
sdpOffer = strings.Replace(sdpOffer, ";sprop-maxcapturerate=24000", "", -1)
|
|
||||||
|
|
||||||
err = room.PeerConnection.SetRemoteDescription(webrtc.SessionDescription{
|
|
||||||
Type: webrtc.SDPTypeOffer,
|
|
||||||
SDP: sdpOffer,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to set remote description for room", "room", room.Name, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
answer, err := room.PeerConnection.CreateAnswer(nil)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create SDP answer for room", "room", room.Name, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = room.PeerConnection.SetLocalDescription(answer)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to set local description for room", "room", room.Name, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &answer
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/pion/webrtc/v4"
|
|
||||||
"log/slog"
|
|
||||||
"math/rand"
|
|
||||||
"relay/internal/common"
|
|
||||||
"relay/internal/connections"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Participant struct {
|
|
||||||
ID ulid.ULID //< Internal IDs are useful to keeping unique internal track and not have conflicts later
|
|
||||||
Name string
|
|
||||||
WebSocket *connections.SafeWebSocket
|
|
||||||
PeerConnection *webrtc.PeerConnection
|
|
||||||
DataChannel *connections.NestriDataChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewParticipant(ws *connections.SafeWebSocket) *Participant {
|
|
||||||
id, err := common.NewULID()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create ULID for Participant", "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &Participant{
|
|
||||||
ID: id,
|
|
||||||
Name: createRandomName(),
|
|
||||||
WebSocket: ws,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Participant) addTrack(trackLocal *webrtc.TrackLocalStaticRTP) error {
|
|
||||||
rtpSender, err := p.PeerConnection.AddTrack(trackLocal)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
rtcpBuffer := make([]byte, 1400)
|
|
||||||
for {
|
|
||||||
if _, _, rtcpErr := rtpSender.Read(rtcpBuffer); rtcpErr != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Participant) signalOffer() error {
|
|
||||||
if p.PeerConnection == nil {
|
|
||||||
return fmt.Errorf("peer connection is nil for participant: '%s' - cannot signal offer", p.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
offer, err := p.PeerConnection.CreateOffer(nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.PeerConnection.SetLocalDescription(offer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.WebSocket.SendSDPMessageWS(offer)
|
|
||||||
}
|
|
||||||
|
|
||||||
var namesFirst = []string{"Happy", "Sad", "Angry", "Calm", "Excited", "Bored", "Confused", "Confident", "Curious", "Depressed", "Disappointed", "Embarrassed", "Energetic", "Fearful", "Frustrated", "Glad", "Guilty", "Hopeful", "Impatient", "Jealous", "Lonely", "Motivated", "Nervous", "Optimistic", "Pessimistic", "Proud", "Relaxed", "Shy", "Stressed", "Surprised", "Tired", "Worried"}
|
|
||||||
var namesSecond = []string{"Dragon", "Unicorn", "Troll", "Goblin", "Elf", "Dwarf", "Ogre", "Gnome", "Mermaid", "Siren", "Vampire", "Ghoul", "Werewolf", "Minotaur", "Centaur", "Griffin", "Phoenix", "Wyvern", "Hydra", "Kraken"}
|
|
||||||
|
|
||||||
func createRandomName() string {
|
|
||||||
randomFirst := namesFirst[rand.Intn(len(namesFirst))]
|
|
||||||
randomSecond := namesSecond[rand.Intn(len(namesSecond))]
|
|
||||||
return randomFirst + " " + randomSecond
|
|
||||||
}
|
|
||||||
@@ -1,702 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/libp2p/go-libp2p"
|
|
||||||
"github.com/libp2p/go-libp2p-pubsub"
|
|
||||||
"github.com/libp2p/go-libp2p/core/host"
|
|
||||||
"github.com/libp2p/go-libp2p/core/network"
|
|
||||||
"github.com/libp2p/go-libp2p/core/peer"
|
|
||||||
"github.com/libp2p/go-libp2p/core/pnet"
|
|
||||||
"github.com/libp2p/go-libp2p/p2p/security/noise"
|
|
||||||
"github.com/multiformats/go-multiaddr"
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/pion/webrtc/v4"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"relay/internal/common"
|
|
||||||
"relay/internal/connections"
|
|
||||||
)
|
|
||||||
|
|
||||||
var globalRelay *Relay
|
|
||||||
|
|
||||||
// networkNotifier logs connection events
|
|
||||||
type networkNotifier struct{}
|
|
||||||
|
|
||||||
func (n *networkNotifier) Connected(net network.Network, conn network.Conn) {
|
|
||||||
slog.Info("Peer connected", "local", conn.LocalPeer(), "remote", conn.RemotePeer())
|
|
||||||
}
|
|
||||||
func (n *networkNotifier) Disconnected(net network.Network, conn network.Conn) {
|
|
||||||
slog.Info("Peer disconnected", "local", conn.LocalPeer(), "remote", conn.RemotePeer())
|
|
||||||
}
|
|
||||||
func (n *networkNotifier) Listen(net network.Network, addr multiaddr.Multiaddr) {}
|
|
||||||
func (n *networkNotifier) ListenClose(net network.Network, addr multiaddr.Multiaddr) {}
|
|
||||||
|
|
||||||
type ICEMessage struct {
|
|
||||||
PeerID string
|
|
||||||
TargetID string
|
|
||||||
RoomID ulid.ULID
|
|
||||||
Candidate []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type Relay struct {
|
|
||||||
ID peer.ID
|
|
||||||
Rooms *common.SafeMap[ulid.ULID, *Room]
|
|
||||||
Host host.Host // libp2p host for peer-to-peer networking
|
|
||||||
PubSub *pubsub.PubSub // PubSub for state synchronization
|
|
||||||
MeshState *common.SafeMap[ulid.ULID, RoomInfo] // room ID -> state
|
|
||||||
RelayPCs *common.SafeMap[ulid.ULID, *webrtc.PeerConnection] // room ID -> relay PeerConnection
|
|
||||||
pubTopicState *pubsub.Topic // topic for room states
|
|
||||||
pubTopicICECandidate *pubsub.Topic // topic for ICE candidates aimed to this relay
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRelay(ctx context.Context, port int) (*Relay, error) {
|
|
||||||
listenAddrs := []string{
|
|
||||||
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4
|
|
||||||
fmt.Sprintf("/ip6/::/tcp/%d", port), // IPv6
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use "testToken" as the pre-shared token for authentication
|
|
||||||
// TODO: Give via flags, before PR commit
|
|
||||||
token := "testToken"
|
|
||||||
// Generate 32-byte PSK from the token using SHA-256
|
|
||||||
shaToken := sha256.Sum256([]byte(token))
|
|
||||||
tokenPSK := pnet.PSK(shaToken[:])
|
|
||||||
|
|
||||||
// Initialize libp2p host
|
|
||||||
p2pHost, err := libp2p.New(
|
|
||||||
libp2p.ListenAddrStrings(listenAddrs...),
|
|
||||||
libp2p.Security(noise.ID, noise.New),
|
|
||||||
libp2p.EnableRelay(),
|
|
||||||
libp2p.EnableHolePunching(),
|
|
||||||
libp2p.PrivateNetwork(tokenPSK),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create libp2p host for relay: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up pubsub
|
|
||||||
p2pPubsub, err := pubsub.NewGossipSub(ctx, p2pHost)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create pubsub: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add network notifier to log connections
|
|
||||||
p2pHost.Network().Notify(&networkNotifier{})
|
|
||||||
|
|
||||||
r := &Relay{
|
|
||||||
ID: p2pHost.ID(),
|
|
||||||
Host: p2pHost,
|
|
||||||
PubSub: p2pPubsub,
|
|
||||||
Rooms: common.NewSafeMap[ulid.ULID, *Room](),
|
|
||||||
MeshState: common.NewSafeMap[ulid.ULID, RoomInfo](),
|
|
||||||
RelayPCs: common.NewSafeMap[ulid.ULID, *webrtc.PeerConnection](),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up state synchronization and stream handling
|
|
||||||
r.setupStateSync(ctx)
|
|
||||||
r.setupStreamHandler()
|
|
||||||
|
|
||||||
slog.Info("Relay initialized", "id", r.ID, "addrs", p2pHost.Addrs())
|
|
||||||
|
|
||||||
peerInfo := peer.AddrInfo{
|
|
||||||
ID: p2pHost.ID(),
|
|
||||||
Addrs: p2pHost.Addrs(),
|
|
||||||
}
|
|
||||||
addrs, err := peer.AddrInfoToP2pAddrs(&peerInfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to convert peer info to addresses: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("Connect with one of the following URLs below:")
|
|
||||||
for _, addr := range addrs {
|
|
||||||
slog.Debug(fmt.Sprintf("- %s", addr.String()))
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitRelay(ctx context.Context, ctxCancel context.CancelFunc, port int) error {
|
|
||||||
var err error
|
|
||||||
globalRelay, err = NewRelay(ctx, port)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create relay: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := common.InitWebRTCAPI(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := InitHTTPEndpoint(ctx, ctxCancel); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("Relay initialized", "id", globalRelay.ID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRelay() *Relay {
|
|
||||||
return globalRelay
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) GetRoomByID(id ulid.ULID) *Room {
|
|
||||||
if room, ok := r.Rooms.Get(id); ok {
|
|
||||||
return room
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) GetOrCreateRoom(name string) *Room {
|
|
||||||
if room := r.GetRoomByName(name); room != nil {
|
|
||||||
return room
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := common.NewULID()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to generate new ULID for room", "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
room := NewRoom(name, id, r.ID)
|
|
||||||
room.Relay = r
|
|
||||||
r.Rooms.Set(room.ID, room)
|
|
||||||
|
|
||||||
slog.Debug("Created new room", "name", name, "id", room.ID)
|
|
||||||
return room
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) DeleteRoomIfEmpty(room *Room) {
|
|
||||||
participantCount := room.Participants.Len()
|
|
||||||
if participantCount > 0 {
|
|
||||||
slog.Debug("Room not empty, not deleting", "name", room.Name, "id", room.ID, "participants", participantCount)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a "tombstone" state for the room, this allows propagation of the room deletion
|
|
||||||
tombstoneState := RoomInfo{
|
|
||||||
ID: room.ID,
|
|
||||||
Name: room.Name,
|
|
||||||
Online: false,
|
|
||||||
OwnerID: room.OwnerID,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish updated state to mesh
|
|
||||||
if err := r.publishRoomState(context.Background(), tombstoneState); err != nil {
|
|
||||||
slog.Error("Failed to publish room states on change", "room", room.Name, "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("Deleting room since empty and offline", "name", room.Name, "id", room.ID)
|
|
||||||
r.Rooms.Delete(room.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) setupStateSync(ctx context.Context) {
|
|
||||||
var err error
|
|
||||||
r.pubTopicState, err = r.PubSub.Join("room-states")
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to join pubsub topic", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sub, err := r.pubTopicState.Subscribe()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to subscribe to topic", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.pubTopicICECandidate, err = r.PubSub.Join("ice-candidates")
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to join ICE candidates topic", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iceCandidateSub, err := r.pubTopicICECandidate.Subscribe()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to subscribe to ICE candidates topic", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle state updates only from authenticated peers
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
msg, err := sub.Next(ctx)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error receiving pubsub message", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if msg.GetFrom() == r.Host.ID() {
|
|
||||||
continue // Ignore own messages
|
|
||||||
}
|
|
||||||
var states []RoomInfo
|
|
||||||
if err := json.Unmarshal(msg.Data, &states); err != nil {
|
|
||||||
slog.Error("Failed to unmarshal room states", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r.updateMeshState(states)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Handle incoming ICE candidates for given room
|
|
||||||
go func() {
|
|
||||||
// Map of ICE candidate slices per room ID
|
|
||||||
iceHolder := make(map[ulid.ULID][]webrtc.ICECandidateInit)
|
|
||||||
|
|
||||||
for {
|
|
||||||
msg, err := iceCandidateSub.Next(ctx)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error receiving ICE candidate message", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if msg.GetFrom() == r.Host.ID() {
|
|
||||||
continue // Ignore own messages
|
|
||||||
}
|
|
||||||
|
|
||||||
var iceMsg ICEMessage
|
|
||||||
if err := json.Unmarshal(msg.Data, &iceMsg); err != nil {
|
|
||||||
slog.Error("Failed to unmarshal ICE candidate message", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if iceMsg.TargetID != r.ID.String() {
|
|
||||||
continue // Ignore messages not meant for this relay
|
|
||||||
}
|
|
||||||
|
|
||||||
if iceHolder[iceMsg.RoomID] == nil {
|
|
||||||
iceHolder[iceMsg.RoomID] = make([]webrtc.ICECandidateInit, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pc, ok := r.RelayPCs.Get(iceMsg.RoomID); ok {
|
|
||||||
// Unmarshal ice candidate
|
|
||||||
var candidate webrtc.ICECandidateInit
|
|
||||||
if err := json.Unmarshal(iceMsg.Candidate, &candidate); err != nil {
|
|
||||||
slog.Error("Failed to unmarshal ICE candidate", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pc.RemoteDescription() != nil {
|
|
||||||
if err := pc.AddICECandidate(candidate); err != nil {
|
|
||||||
slog.Error("Failed to add ICE candidate", "err", err)
|
|
||||||
}
|
|
||||||
// Add any held candidates
|
|
||||||
for _, heldCandidate := range iceHolder[iceMsg.RoomID] {
|
|
||||||
if err := pc.AddICECandidate(heldCandidate); err != nil {
|
|
||||||
slog.Error("Failed to add held ICE candidate", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
iceHolder[iceMsg.RoomID] = make([]webrtc.ICECandidateInit, 0)
|
|
||||||
} else {
|
|
||||||
iceHolder[iceMsg.RoomID] = append(iceHolder[iceMsg.RoomID], candidate)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
slog.Error("PeerConnection for room not found when adding ICE candidate", "roomID", iceMsg.RoomID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) publishRoomState(ctx context.Context, state RoomInfo) error {
|
|
||||||
data, err := json.Marshal([]RoomInfo{state})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.pubTopicState.Publish(ctx, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) publishRoomStates(ctx context.Context) error {
|
|
||||||
var states []RoomInfo
|
|
||||||
for _, room := range r.Rooms.Copy() {
|
|
||||||
states = append(states, RoomInfo{
|
|
||||||
ID: room.ID,
|
|
||||||
Name: room.Name,
|
|
||||||
Online: room.Online,
|
|
||||||
OwnerID: r.ID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(states)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.pubTopicState.Publish(ctx, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) updateMeshState(states []RoomInfo) {
|
|
||||||
for _, state := range states {
|
|
||||||
if state.OwnerID == r.ID {
|
|
||||||
continue // Skip own state
|
|
||||||
}
|
|
||||||
existing, exists := r.MeshState.Get(state.ID)
|
|
||||||
r.MeshState.Set(state.ID, state)
|
|
||||||
slog.Debug("Updated mesh state", "room", state.Name, "online", state.Online, "owner", state.OwnerID)
|
|
||||||
|
|
||||||
// React to state changes
|
|
||||||
if !exists || existing.Online != state.Online {
|
|
||||||
room := r.GetRoomByName(state.Name)
|
|
||||||
if state.Online {
|
|
||||||
if room == nil || !room.Online {
|
|
||||||
slog.Info("Room became active remotely, requesting stream", "room", state.Name, "owner", state.OwnerID)
|
|
||||||
go func() {
|
|
||||||
if _, err := r.requestStream(context.Background(), state.Name, state.ID, state.OwnerID); err != nil {
|
|
||||||
slog.Error("Failed to request stream", "room", state.Name, "err", err)
|
|
||||||
} else {
|
|
||||||
slog.Info("Successfully requested stream", "room", state.Name, "owner", state.OwnerID)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
} else if room != nil && room.Online {
|
|
||||||
slog.Info("Room became inactive remotely, stopping local stream", "room", state.Name)
|
|
||||||
if pc, ok := r.RelayPCs.Get(state.ID); ok {
|
|
||||||
_ = pc.Close()
|
|
||||||
r.RelayPCs.Delete(state.ID)
|
|
||||||
}
|
|
||||||
room.Online = false
|
|
||||||
room.signalParticipantsOffline()
|
|
||||||
} else if room == nil && !exists {
|
|
||||||
slog.Info("Received tombstone state for room", "name", state.Name, "id", state.ID)
|
|
||||||
if pc, ok := r.RelayPCs.Get(state.ID); ok {
|
|
||||||
_ = pc.Close()
|
|
||||||
r.RelayPCs.Delete(state.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) IsRoomActive(roomID ulid.ULID) (bool, peer.ID) {
|
|
||||||
if state, exists := r.MeshState.Get(roomID); exists && state.Online {
|
|
||||||
return true, state.OwnerID
|
|
||||||
}
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) GetRoomByName(name string) *Room {
|
|
||||||
for _, room := range r.Rooms.Copy() {
|
|
||||||
if room.Name == name {
|
|
||||||
return room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeMessage(stream network.Stream, data []byte) error {
|
|
||||||
length := uint32(len(data))
|
|
||||||
if err := binary.Write(stream, binary.BigEndian, length); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err := stream.Write(data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func readMessage(stream network.Stream) ([]byte, error) {
|
|
||||||
var length uint32
|
|
||||||
if err := binary.Read(stream, binary.BigEndian, &length); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data := make([]byte, length)
|
|
||||||
_, err := io.ReadFull(stream, data)
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) setupStreamHandler() {
|
|
||||||
r.Host.SetStreamHandler("/nestri-relay/stream/1.0.0", func(stream network.Stream) {
|
|
||||||
defer func(stream network.Stream) {
|
|
||||||
err := stream.Close()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to close stream", "err", err)
|
|
||||||
}
|
|
||||||
}(stream)
|
|
||||||
remotePeer := stream.Conn().RemotePeer()
|
|
||||||
|
|
||||||
roomNameData, err := readMessage(stream)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
slog.Error("Failed to read room name", "peer", remotePeer, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
roomName := string(roomNameData)
|
|
||||||
|
|
||||||
slog.Info("Stream request from peer", "peer", remotePeer, "room", roomName)
|
|
||||||
|
|
||||||
room := r.GetRoomByName(roomName)
|
|
||||||
if room == nil || !room.Online {
|
|
||||||
slog.Error("Cannot provide stream for inactive room", "room", roomName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pc, err := common.CreatePeerConnection(func() {
|
|
||||||
r.RelayPCs.Delete(room.ID)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create relay PeerConnection", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.RelayPCs.Set(room.ID, pc)
|
|
||||||
|
|
||||||
if room.AudioTrack != nil {
|
|
||||||
_, err := pc.AddTrack(room.AudioTrack)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to add audio track", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if room.VideoTrack != nil {
|
|
||||||
_, err := pc.AddTrack(room.VideoTrack)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to add video track", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settingOrdered := true
|
|
||||||
settingMaxRetransmits := uint16(0)
|
|
||||||
dc, err := pc.CreateDataChannel("relay-data", &webrtc.DataChannelInit{
|
|
||||||
Ordered: &settingOrdered,
|
|
||||||
MaxRetransmits: &settingMaxRetransmits,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create relay DataChannel", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
relayDC := connections.NewNestriDataChannel(dc)
|
|
||||||
|
|
||||||
relayDC.RegisterOnOpen(func() {
|
|
||||||
slog.Debug("Relay DataChannel opened", "room", roomName)
|
|
||||||
})
|
|
||||||
|
|
||||||
relayDC.RegisterOnClose(func() {
|
|
||||||
slog.Debug("Relay DataChannel closed", "room", roomName)
|
|
||||||
})
|
|
||||||
|
|
||||||
relayDC.RegisterMessageCallback("input", func(data []byte) {
|
|
||||||
if room.DataChannel != nil {
|
|
||||||
// Forward message to the room's data channel
|
|
||||||
if err := room.DataChannel.SendBinary(data); err != nil {
|
|
||||||
slog.Error("Failed to send DataChannel message", "room", roomName, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
offer, err := pc.CreateOffer(nil)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create offer", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := pc.SetLocalDescription(offer); err != nil {
|
|
||||||
slog.Error("Failed to set local description", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offerData, err := json.Marshal(offer)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to marshal offer", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := writeMessage(stream, offerData); err != nil {
|
|
||||||
slog.Error("Failed to send offer", "peer", remotePeer, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle our generated ICE candidates
|
|
||||||
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
|
||||||
if candidate == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
candidateData, err := json.Marshal(candidate.ToJSON())
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to marshal ICE candidate", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
iceMsg := ICEMessage{
|
|
||||||
PeerID: r.Host.ID().String(),
|
|
||||||
TargetID: remotePeer.String(),
|
|
||||||
RoomID: room.ID,
|
|
||||||
Candidate: candidateData,
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(iceMsg)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to marshal ICE message", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := r.pubTopicICECandidate.Publish(context.Background(), data); err != nil {
|
|
||||||
slog.Error("Failed to publish ICE candidate message", "err", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
answerData, err := readMessage(stream)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
slog.Error("Failed to read answer", "peer", remotePeer, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var answer webrtc.SessionDescription
|
|
||||||
if err := json.Unmarshal(answerData, &answer); err != nil {
|
|
||||||
slog.Error("Failed to unmarshal answer", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := pc.SetRemoteDescription(answer); err != nil {
|
|
||||||
slog.Error("Failed to set remote description", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Relay) requestStream(ctx context.Context, roomName string, roomID ulid.ULID, providerPeer peer.ID) (*webrtc.PeerConnection, error) {
|
|
||||||
stream, err := r.Host.NewStream(ctx, providerPeer, "/nestri-relay/stream/1.0.0")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create stream: %w", err)
|
|
||||||
}
|
|
||||||
defer func(stream network.Stream) {
|
|
||||||
err := stream.Close()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to close stream", "err", err)
|
|
||||||
}
|
|
||||||
}(stream)
|
|
||||||
|
|
||||||
if err := writeMessage(stream, []byte(roomName)); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to send room name: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
room := r.GetRoomByName(roomName)
|
|
||||||
if room == nil {
|
|
||||||
room = NewRoom(roomName, roomID, providerPeer)
|
|
||||||
r.Rooms.Set(roomID, room)
|
|
||||||
} else if room.ID != roomID {
|
|
||||||
// Mismatch, prefer the one from the provider
|
|
||||||
// TODO: When mesh is created, if there are mismatches, we should have relays negotiate common room IDs
|
|
||||||
room.ID = roomID
|
|
||||||
room.OwnerID = providerPeer
|
|
||||||
r.Rooms.Set(roomID, room)
|
|
||||||
}
|
|
||||||
|
|
||||||
pc, err := common.CreatePeerConnection(func() {
|
|
||||||
r.RelayPCs.Delete(roomID)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create PeerConnection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.RelayPCs.Set(roomID, pc)
|
|
||||||
|
|
||||||
offerData, err := readMessage(stream)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return nil, fmt.Errorf("failed to read offer: %w", err)
|
|
||||||
}
|
|
||||||
var offer webrtc.SessionDescription
|
|
||||||
if err := json.Unmarshal(offerData, &offer); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal offer: %w", err)
|
|
||||||
}
|
|
||||||
if err := pc.SetRemoteDescription(offer); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to set remote description: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
|
||||||
localTrack, _ := webrtc.NewTrackLocalStaticRTP(track.Codec().RTPCodecCapability, track.ID(), "relay-"+roomName+"-"+track.Kind().String())
|
|
||||||
slog.Debug("Received track for mesh relay room", "room", roomName, "kind", track.Kind())
|
|
||||||
|
|
||||||
room.SetTrack(track.Kind(), localTrack)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
rtpPacket, _, err := track.ReadRTP()
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, io.EOF) {
|
|
||||||
slog.Error("Failed to read RTP packet from remote track for room", "room", roomName, "err", err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
err = localTrack.WriteRTP(rtpPacket)
|
|
||||||
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
|
||||||
slog.Error("Failed to write RTP to local track for room", "room", room.Name, "err", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
|
|
||||||
// ICE candidate handling
|
|
||||||
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
|
||||||
if candidate == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
candidateData, err := json.Marshal(candidate.ToJSON())
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to marshal ICE candidate", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
iceMsg := ICEMessage{
|
|
||||||
PeerID: r.Host.ID().String(),
|
|
||||||
TargetID: providerPeer.String(),
|
|
||||||
RoomID: roomID,
|
|
||||||
Candidate: candidateData,
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(iceMsg)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to marshal ICE message", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := r.pubTopicICECandidate.Publish(ctx, data); err != nil {
|
|
||||||
slog.Error("Failed to publish ICE candidate message", "err", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
pc.OnDataChannel(func(dc *webrtc.DataChannel) {
|
|
||||||
relayDC := connections.NewNestriDataChannel(dc)
|
|
||||||
slog.Debug("Received DataChannel from peer", "room", roomName)
|
|
||||||
|
|
||||||
relayDC.RegisterOnOpen(func() {
|
|
||||||
slog.Debug("Relay DataChannel opened", "room", roomName)
|
|
||||||
})
|
|
||||||
|
|
||||||
relayDC.OnClose(func() {
|
|
||||||
slog.Debug("Relay DataChannel closed", "room", roomName)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Override room DataChannel with the mesh-relay one to forward messages
|
|
||||||
room.DataChannel = relayDC
|
|
||||||
})
|
|
||||||
|
|
||||||
answer, err := pc.CreateAnswer(nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create answer: %w", err)
|
|
||||||
}
|
|
||||||
if err := pc.SetLocalDescription(answer); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to set local description: %w", err)
|
|
||||||
}
|
|
||||||
answerData, err := json.Marshal(answer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal answer: %w", err)
|
|
||||||
}
|
|
||||||
if err := writeMessage(stream, answerData); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to send answer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectToRelay manually connects to another relay by its multiaddress
|
|
||||||
func (r *Relay) ConnectToRelay(ctx context.Context, addr string) error {
|
|
||||||
// Parse the multiaddress
|
|
||||||
ma, err := multiaddr.NewMultiaddr(addr)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Invalid multiaddress", "addr", addr, "err", err)
|
|
||||||
return fmt.Errorf("invalid multiaddress: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract peer ID from multiaddress
|
|
||||||
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to extract peer info", "addr", addr, "err", err)
|
|
||||||
return fmt.Errorf("failed to extract peer info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to the peer
|
|
||||||
if err := r.Host.Connect(ctx, *peerInfo); err != nil {
|
|
||||||
slog.Error("Failed to connect to peer", "peer", peerInfo.ID, "addr", addr, "err", err)
|
|
||||||
return fmt.Errorf("failed to connect: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish challenge on join
|
|
||||||
//go r.sendAuthChallenge(ctx)
|
|
||||||
|
|
||||||
slog.Info("Successfully connected to peer", "peer", peerInfo.ID, "addr", addr)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
44
packages/relay/internal/shared/participant.go
Normal file
44
packages/relay/internal/shared/participant.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"relay/internal/common"
|
||||||
|
"relay/internal/connections"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Participant struct {
|
||||||
|
ID ulid.ULID
|
||||||
|
PeerConnection *webrtc.PeerConnection
|
||||||
|
DataChannel *connections.NestriDataChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParticipant() (*Participant, error) {
|
||||||
|
id, err := common.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create ULID for Participant: %w", err)
|
||||||
|
}
|
||||||
|
return &Participant{
|
||||||
|
ID: id,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Participant) addTrack(trackLocal *webrtc.TrackLocalStaticRTP) error {
|
||||||
|
rtpSender, err := p.PeerConnection.AddTrack(trackLocal)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
rtcpBuffer := make([]byte, 1400)
|
||||||
|
for {
|
||||||
|
if _, _, rtcpErr := rtpSender.Read(rtcpBuffer); rtcpErr != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,32 +1,28 @@
|
|||||||
package internal
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/libp2p/go-libp2p/core/peer"
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/pion/webrtc/v4"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"relay/internal/common"
|
"relay/internal/common"
|
||||||
"relay/internal/connections"
|
"relay/internal/connections"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/peer"
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoomInfo struct {
|
type RoomInfo struct {
|
||||||
ID ulid.ULID `json:"id"`
|
ID ulid.ULID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Online bool `json:"online"`
|
|
||||||
OwnerID peer.ID `json:"owner_id"`
|
OwnerID peer.ID `json:"owner_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Room struct {
|
type Room struct {
|
||||||
RoomInfo
|
RoomInfo
|
||||||
WebSocket *connections.SafeWebSocket
|
|
||||||
PeerConnection *webrtc.PeerConnection
|
PeerConnection *webrtc.PeerConnection
|
||||||
AudioTrack *webrtc.TrackLocalStaticRTP
|
AudioTrack *webrtc.TrackLocalStaticRTP
|
||||||
VideoTrack *webrtc.TrackLocalStaticRTP
|
VideoTrack *webrtc.TrackLocalStaticRTP
|
||||||
DataChannel *connections.NestriDataChannel
|
DataChannel *connections.NestriDataChannel
|
||||||
Participants *common.SafeMap[ulid.ULID, *Participant]
|
Participants *common.SafeMap[ulid.ULID, *Participant]
|
||||||
Relay *Relay
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room {
|
func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room {
|
||||||
@@ -34,21 +30,12 @@ func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room {
|
|||||||
RoomInfo: RoomInfo{
|
RoomInfo: RoomInfo{
|
||||||
ID: roomID,
|
ID: roomID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Online: false,
|
|
||||||
OwnerID: ownerID,
|
OwnerID: ownerID,
|
||||||
},
|
},
|
||||||
Participants: common.NewSafeMap[ulid.ULID, *Participant](),
|
Participants: common.NewSafeMap[ulid.ULID, *Participant](),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssignWebSocket assigns a WebSocket connection to a Room
|
|
||||||
func (r *Room) AssignWebSocket(ws *connections.SafeWebSocket) {
|
|
||||||
if r.WebSocket != nil {
|
|
||||||
slog.Warn("WebSocket already assigned to room", "room", r.Name)
|
|
||||||
}
|
|
||||||
r.WebSocket = ws
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddParticipant adds a Participant to a Room
|
// AddParticipant adds a Participant to a Room
|
||||||
func (r *Room) AddParticipant(participant *Participant) {
|
func (r *Room) AddParticipant(participant *Participant) {
|
||||||
slog.Debug("Adding participant to room", "participant", participant.ID, "room", r.Name)
|
slog.Debug("Adding participant to room", "participant", participant.ID, "room", r.Name)
|
||||||
@@ -62,21 +49,8 @@ func (r *Room) removeParticipantByID(pID ulid.ULID) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes a Participant from a Room by participant's name
|
|
||||||
func (r *Room) removeParticipantByName(pName string) {
|
|
||||||
for id, participant := range r.Participants.Copy() {
|
|
||||||
if participant.Name == pName {
|
|
||||||
if err := r.signalParticipantOffline(participant); err != nil {
|
|
||||||
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
|
|
||||||
}
|
|
||||||
r.Participants.Delete(id)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes all participants from a Room
|
// Removes all participants from a Room
|
||||||
func (r *Room) removeAllParticipants() {
|
/*func (r *Room) removeAllParticipants() {
|
||||||
for id, participant := range r.Participants.Copy() {
|
for id, participant := range r.Participants.Copy() {
|
||||||
if err := r.signalParticipantOffline(participant); err != nil {
|
if err := r.signalParticipantOffline(participant); err != nil {
|
||||||
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
|
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
|
||||||
@@ -84,24 +58,28 @@ func (r *Room) removeAllParticipants() {
|
|||||||
r.Participants.Delete(id)
|
r.Participants.Delete(id)
|
||||||
slog.Debug("Removed participant from room", "participant", id, "room", r.Name)
|
slog.Debug("Removed participant from room", "participant", id, "room", r.Name)
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// IsOnline checks if the room is online (has both audio and video tracks)
|
||||||
|
func (r *Room) IsOnline() bool {
|
||||||
|
return r.AudioTrack != nil && r.VideoTrack != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
|
func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
|
||||||
|
//oldOnline := r.IsOnline()
|
||||||
|
|
||||||
switch trackType {
|
switch trackType {
|
||||||
case webrtc.RTPCodecTypeAudio:
|
case webrtc.RTPCodecTypeAudio:
|
||||||
r.AudioTrack = track
|
r.AudioTrack = track
|
||||||
slog.Debug("Audio track set", "room", r.Name, "track", track != nil)
|
|
||||||
case webrtc.RTPCodecTypeVideo:
|
case webrtc.RTPCodecTypeVideo:
|
||||||
r.VideoTrack = track
|
r.VideoTrack = track
|
||||||
slog.Debug("Video track set", "room", r.Name, "track", track != nil)
|
|
||||||
default:
|
default:
|
||||||
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType)
|
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType)
|
||||||
}
|
}
|
||||||
|
|
||||||
newOnline := r.AudioTrack != nil && r.VideoTrack != nil
|
/*newOnline := r.IsOnline()
|
||||||
if r.Online != newOnline {
|
if oldOnline != newOnline {
|
||||||
r.Online = newOnline
|
if newOnline {
|
||||||
if r.Online {
|
|
||||||
slog.Debug("Room online, participants will be signaled", "room", r.Name)
|
slog.Debug("Room online, participants will be signaled", "room", r.Name)
|
||||||
r.signalParticipantsWithTracks()
|
r.signalParticipantsWithTracks()
|
||||||
} else {
|
} else {
|
||||||
@@ -109,15 +87,16 @@ func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalS
|
|||||||
r.signalParticipantsOffline()
|
r.signalParticipantsOffline()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish updated state to mesh
|
// TODO: Publish updated state to mesh
|
||||||
go func() {
|
go func() {
|
||||||
if err := r.Relay.publishRoomStates(context.Background()); err != nil {
|
if err := r.Relay.publishRoomStates(context.Background()); err != nil {
|
||||||
slog.Error("Failed to publish room states on change", "room", r.Name, "err", err)
|
slog.Error("Failed to publish room states on change", "room", r.Name, "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TODO: libp2p'ify
|
||||||
func (r *Room) signalParticipantsWithTracks() {
|
func (r *Room) signalParticipantsWithTracks() {
|
||||||
for _, participant := range r.Participants.Copy() {
|
for _, participant := range r.Participants.Copy() {
|
||||||
if err := r.signalParticipantWithTracks(participant); err != nil {
|
if err := r.signalParticipantWithTracks(participant); err != nil {
|
||||||
@@ -162,3 +141,4 @@ func (r *Room) signalParticipantOffline(participant *Participant) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
@@ -2,12 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"relay/internal"
|
|
||||||
"relay/internal/common"
|
"relay/internal/common"
|
||||||
|
"relay/internal/core"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ func main() {
|
|||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
// Start relay
|
// Start relay
|
||||||
err := internal.InitRelay(mainCtx, mainStopper, common.GetFlags().MeshPort)
|
err := core.InitRelay(mainCtx, mainStopper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to initialize relay", "err", err)
|
slog.Error("Failed to initialize relay", "err", err)
|
||||||
mainStopper()
|
mainStopper()
|
||||||
@@ -42,5 +41,5 @@ func main() {
|
|||||||
|
|
||||||
// Wait for exit signal
|
// Wait for exit signal
|
||||||
<-mainCtx.Done()
|
<-mainCtx.Done()
|
||||||
log.Println("Shutting down gracefully by signal...")
|
slog.Info("Shutting down gracefully by signal...")
|
||||||
}
|
}
|
||||||
|
|||||||
1763
packages/server/Cargo.lock
generated
1763
packages/server/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -19,14 +19,14 @@ webrtc = "0.13"
|
|||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
rustls = { version = "0.23", features = ["ring"] }
|
rustls = { version = "0.23", features = ["ring"] }
|
||||||
tokio-tungstenite = { version = "0.26", features = ["native-tls"] }
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
num-derive = "0.4"
|
|
||||||
num-traits = "0.2"
|
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
prost-types = "0.13"
|
prost-types = "0.13"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
atomic_refcell = "0.1"
|
atomic_refcell = "0.1"
|
||||||
|
byteorder = "1.5"
|
||||||
|
libp2p = { version = "0.55", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros"] }
|
||||||
|
libp2p-stream = "0.3.0-alpha"
|
||||||
@@ -4,14 +4,14 @@ mod gpu;
|
|||||||
mod latency;
|
mod latency;
|
||||||
mod messages;
|
mod messages;
|
||||||
mod nestrisink;
|
mod nestrisink;
|
||||||
|
mod p2p;
|
||||||
mod proto;
|
mod proto;
|
||||||
mod websocket;
|
|
||||||
|
|
||||||
use crate::args::encoding_args;
|
use crate::args::encoding_args;
|
||||||
use crate::enc_helper::EncoderType;
|
use crate::enc_helper::EncoderType;
|
||||||
use crate::gpu::GPUVendor;
|
use crate::gpu::GPUVendor;
|
||||||
use crate::nestrisink::NestriSignaller;
|
use crate::nestrisink::NestriSignaller;
|
||||||
use crate::websocket::NestriWebSocket;
|
use crate::p2p::p2p::NestriP2P;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use gst::prelude::*;
|
use gst::prelude::*;
|
||||||
use gstrswebrtc::signaller::Signallable;
|
use gstrswebrtc::signaller::Signallable;
|
||||||
@@ -19,6 +19,8 @@ use gstrswebrtc::webrtcsink::BaseWebRTCSink;
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use tracing_subscriber::filter::LevelFilter;
|
||||||
|
|
||||||
// Handles gathering GPU information and selecting the most suitable GPU
|
// Handles gathering GPU information and selecting the most suitable GPU
|
||||||
fn handle_gpus(args: &args::Args) -> Result<gpu::GPUInfo, Box<dyn Error>> {
|
fn handle_gpus(args: &args::Args) -> Result<gpu::GPUInfo, Box<dyn Error>> {
|
||||||
@@ -165,32 +167,29 @@ fn handle_encoder_audio(args: &args::Args) -> String {
|
|||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
let mut args = args::Args::new();
|
let mut args = args::Args::new();
|
||||||
if args.app.verbose {
|
|
||||||
// Make sure tracing has INFO level
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_max_level(tracing::Level::INFO)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::builder()
|
||||||
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
|
.from_env()?,
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
if args.app.verbose {
|
||||||
args.debug_print();
|
args.debug_print();
|
||||||
} else {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rustls::crypto::ring::default_provider()
|
rustls::crypto::ring::default_provider()
|
||||||
.install_default()
|
.install_default()
|
||||||
.expect("Failed to install ring crypto provider");
|
.expect("Failed to install ring crypto provider");
|
||||||
|
|
||||||
// Begin connection attempt to the relay WebSocket endpoint
|
// Get relay URL from arguments
|
||||||
// replace any http/https with ws/wss
|
let relay_url = args.app.relay_url.trim();
|
||||||
let replaced_relay_url = args
|
|
||||||
.app
|
|
||||||
.relay_url
|
|
||||||
.replace("http://", "ws://")
|
|
||||||
.replace("https://", "wss://");
|
|
||||||
let ws_url = format!("{}/api/ws/{}", replaced_relay_url, args.app.room,);
|
|
||||||
|
|
||||||
// Setup our websocket
|
// Initialize libp2p (logically the sink should handle the connection to be independent)
|
||||||
let nestri_ws = Arc::new(NestriWebSocket::new(ws_url).await?);
|
let nestri_p2p = Arc::new(NestriP2P::new().await?);
|
||||||
|
let p2p_conn = nestri_p2p.connect(relay_url).await?;
|
||||||
|
|
||||||
gst::init()?;
|
gst::init()?;
|
||||||
gstrswebrtc::plugin_register_static()?;
|
gstrswebrtc::plugin_register_static()?;
|
||||||
@@ -328,7 +327,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
/* Output */
|
/* Output */
|
||||||
// WebRTC sink Element
|
// WebRTC sink Element
|
||||||
let signaller = NestriSignaller::new(nestri_ws.clone(), video_source.clone());
|
let signaller =
|
||||||
|
NestriSignaller::new(args.app.room, p2p_conn.clone(), video_source.clone()).await?;
|
||||||
let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone()));
|
let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone()));
|
||||||
webrtcsink.set_property_from_str("stun-server", "stun://stun.l.google.com:19302");
|
webrtcsink.set_property_from_str("stun-server", "stun://stun.l.google.com:19302");
|
||||||
webrtcsink.set_property_from_str("congestion-control", "disabled");
|
webrtcsink.set_property_from_str("congestion-control", "disabled");
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
use crate::latency::LatencyTracker;
|
use crate::latency::LatencyTracker;
|
||||||
use num_derive::{FromPrimitive, ToPrimitive};
|
|
||||||
use num_traits::{FromPrimitive, ToPrimitive};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::error::Error;
|
|
||||||
use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit;
|
use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit;
|
||||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||||
|
|
||||||
@@ -12,6 +9,13 @@ pub struct MessageBase {
|
|||||||
pub latency: Option<LatencyTracker>,
|
pub latency: Option<LatencyTracker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct MessageRaw {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub base: MessageBase,
|
||||||
|
pub data: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct MessageLog {
|
pub struct MessageLog {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@@ -44,76 +48,3 @@ pub struct MessageSDP {
|
|||||||
pub base: MessageBase,
|
pub base: MessageBase,
|
||||||
pub sdp: RTCSessionDescription,
|
pub sdp: RTCSessionDescription,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(i32)]
|
|
||||||
#[derive(Debug, FromPrimitive, ToPrimitive, Copy, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(try_from = "i32", into = "i32")]
|
|
||||||
pub enum JoinerType {
|
|
||||||
JoinerNode = 0,
|
|
||||||
JoinerClient = 1,
|
|
||||||
}
|
|
||||||
impl TryFrom<i32> for JoinerType {
|
|
||||||
type Error = &'static str;
|
|
||||||
|
|
||||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
|
||||||
JoinerType::from_i32(value).ok_or("Invalid value for JoinerType")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<JoinerType> for i32 {
|
|
||||||
fn from(joiner_type: JoinerType) -> Self {
|
|
||||||
joiner_type.to_i32().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct MessageJoin {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub base: MessageBase,
|
|
||||||
pub joiner_type: JoinerType,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(i32)]
|
|
||||||
#[derive(Debug, FromPrimitive, ToPrimitive, Copy, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(try_from = "i32", into = "i32")]
|
|
||||||
pub enum AnswerType {
|
|
||||||
AnswerOffline = 0,
|
|
||||||
AnswerInUse = 1,
|
|
||||||
AnswerOK = 2,
|
|
||||||
}
|
|
||||||
impl TryFrom<i32> for AnswerType {
|
|
||||||
type Error = &'static str;
|
|
||||||
|
|
||||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
|
||||||
AnswerType::from_i32(value).ok_or("Invalid value for AnswerType")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<AnswerType> for i32 {
|
|
||||||
fn from(answer_type: AnswerType) -> Self {
|
|
||||||
answer_type.to_i32().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct MessageAnswer {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub base: MessageBase,
|
|
||||||
pub answer_type: AnswerType,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encode_message<T: Serialize>(message: &T) -> Result<String, Box<dyn Error>> {
|
|
||||||
// Serialize the message to JSON
|
|
||||||
let json = serde_json::to_string(message)?;
|
|
||||||
Ok(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decode_message(data: String) -> Result<MessageBase, Box<dyn Error + Send + Sync>> {
|
|
||||||
let base_message: MessageBase = serde_json::from_str(&data)?;
|
|
||||||
Ok(base_message)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decode_message_as<T: for<'de> Deserialize<'de>>(
|
|
||||||
data: String,
|
|
||||||
) -> Result<T, Box<dyn Error + Send + Sync>> {
|
|
||||||
let message: T = serde_json::from_str(&data)?;
|
|
||||||
Ok(message)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
use crate::messages::{
|
use crate::messages::{MessageBase, MessageICE, MessageRaw, MessageSDP};
|
||||||
AnswerType, JoinerType, MessageAnswer, MessageBase, MessageICE, MessageJoin, MessageSDP,
|
use crate::p2p::p2p::NestriConnection;
|
||||||
decode_message_as, encode_message,
|
use crate::p2p::p2p_protocol_stream::NestriStreamProtocol;
|
||||||
};
|
|
||||||
use crate::proto::proto::proto_input::InputType::{
|
use crate::proto::proto::proto_input::InputType::{
|
||||||
KeyDown, KeyUp, MouseKeyDown, MouseKeyUp, MouseMove, MouseMoveAbs, MouseWheel,
|
KeyDown, KeyUp, MouseKeyDown, MouseKeyUp, MouseMove, MouseMoveAbs, MouseWheel,
|
||||||
};
|
};
|
||||||
use crate::proto::proto::{ProtoInput, ProtoMessageInput};
|
use crate::proto::proto::{ProtoInput, ProtoMessageInput};
|
||||||
use crate::websocket::NestriWebSocket;
|
|
||||||
use atomic_refcell::AtomicRefCell;
|
use atomic_refcell::AtomicRefCell;
|
||||||
use glib::subclass::prelude::*;
|
use glib::subclass::prelude::*;
|
||||||
use gst::glib;
|
use gst::glib;
|
||||||
@@ -20,22 +18,37 @@ use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit;
|
|||||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||||
|
|
||||||
pub struct Signaller {
|
pub struct Signaller {
|
||||||
nestri_ws: PLRwLock<Option<Arc<NestriWebSocket>>>,
|
stream_room: PLRwLock<Option<String>>,
|
||||||
|
stream_protocol: PLRwLock<Option<Arc<NestriStreamProtocol>>>,
|
||||||
wayland_src: PLRwLock<Option<Arc<gst::Element>>>,
|
wayland_src: PLRwLock<Option<Arc<gst::Element>>>,
|
||||||
data_channel: AtomicRefCell<Option<gst_webrtc::WebRTCDataChannel>>,
|
data_channel: AtomicRefCell<Option<gst_webrtc::WebRTCDataChannel>>,
|
||||||
}
|
}
|
||||||
impl Default for Signaller {
|
impl Default for Signaller {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
nestri_ws: PLRwLock::new(None),
|
stream_room: PLRwLock::new(None),
|
||||||
|
stream_protocol: PLRwLock::new(None),
|
||||||
wayland_src: PLRwLock::new(None),
|
wayland_src: PLRwLock::new(None),
|
||||||
data_channel: AtomicRefCell::new(None),
|
data_channel: AtomicRefCell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Signaller {
|
impl Signaller {
|
||||||
pub fn set_nestri_ws(&self, nestri_ws: Arc<NestriWebSocket>) {
|
pub async fn set_nestri_connection(
|
||||||
*self.nestri_ws.write() = Some(nestri_ws);
|
&self,
|
||||||
|
nestri_conn: NestriConnection,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let stream_protocol = NestriStreamProtocol::new(nestri_conn).await?;
|
||||||
|
*self.stream_protocol.write() = Some(Arc::new(stream_protocol));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_stream_room(&self, room: String) {
|
||||||
|
*self.stream_room.write() = Some(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_stream_protocol(&self) -> Option<Arc<NestriStreamProtocol>> {
|
||||||
|
self.stream_protocol.read().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_wayland_src(&self, wayland_src: Arc<gst::Element>) {
|
pub fn set_wayland_src(&self, wayland_src: Arc<gst::Element>) {
|
||||||
@@ -58,16 +71,14 @@ impl Signaller {
|
|||||||
|
|
||||||
/// Helper method to clean things up
|
/// Helper method to clean things up
|
||||||
fn register_callbacks(&self) {
|
fn register_callbacks(&self) {
|
||||||
let nestri_ws = {
|
let Some(stream_protocol) = self.get_stream_protocol() else {
|
||||||
self.nestri_ws
|
gst::error!(gst::CAT_DEFAULT, "Stream protocol not set");
|
||||||
.read()
|
return;
|
||||||
.clone()
|
|
||||||
.expect("NestriWebSocket not set")
|
|
||||||
};
|
};
|
||||||
{
|
{
|
||||||
let self_obj = self.obj().clone();
|
let self_obj = self.obj().clone();
|
||||||
let _ = nestri_ws.register_callback("sdp", move |data| {
|
stream_protocol.register_callback("answer", move |data| {
|
||||||
if let Ok(message) = decode_message_as::<MessageSDP>(data) {
|
if let Ok(message) = serde_json::from_slice::<MessageSDP>(&data) {
|
||||||
let sdp =
|
let sdp =
|
||||||
gst_sdp::SDPMessage::parse_buffer(message.sdp.sdp.as_bytes()).unwrap();
|
gst_sdp::SDPMessage::parse_buffer(message.sdp.sdp.as_bytes()).unwrap();
|
||||||
let answer = WebRTCSessionDescription::new(WebRTCSDPType::Answer, sdp);
|
let answer = WebRTCSessionDescription::new(WebRTCSDPType::Answer, sdp);
|
||||||
@@ -82,12 +93,11 @@ impl Signaller {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
let self_obj = self.obj().clone();
|
let self_obj = self.obj().clone();
|
||||||
let _ = nestri_ws.register_callback("ice", move |data| {
|
stream_protocol.register_callback("ice-candidate", move |data| {
|
||||||
if let Ok(message) = decode_message_as::<MessageICE>(data) {
|
if let Ok(message) = serde_json::from_slice::<MessageICE>(&data) {
|
||||||
let candidate = message.candidate;
|
let candidate = message.candidate;
|
||||||
let sdp_m_line_index = candidate.sdp_mline_index.unwrap_or(0) as u32;
|
let sdp_m_line_index = candidate.sdp_mline_index.unwrap_or(0) as u32;
|
||||||
let sdp_mid = candidate.sdp_mid;
|
let sdp_mid = candidate.sdp_mid;
|
||||||
|
|
||||||
self_obj.emit_by_name::<()>(
|
self_obj.emit_by_name::<()>(
|
||||||
"handle-ice",
|
"handle-ice",
|
||||||
&[
|
&[
|
||||||
@@ -104,29 +114,28 @@ impl Signaller {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
let self_obj = self.obj().clone();
|
let self_obj = self.obj().clone();
|
||||||
let _ = nestri_ws.register_callback("answer", move |data| {
|
stream_protocol.register_callback("push-stream-ok", move |data| {
|
||||||
if let Ok(answer) = decode_message_as::<MessageAnswer>(data) {
|
if let Ok(answer) = serde_json::from_slice::<MessageRaw>(&data) {
|
||||||
gst::info!(gst::CAT_DEFAULT, "Received answer: {:?}", answer);
|
// Decode room name string
|
||||||
match answer.answer_type {
|
if let Some(room_name) = answer.data.as_str() {
|
||||||
AnswerType::AnswerOK => {
|
gst::info!(
|
||||||
gst::info!(gst::CAT_DEFAULT, "Received OK answer");
|
gst::CAT_DEFAULT,
|
||||||
// Send our SDP offer
|
"Received OK answer for room: {}",
|
||||||
self_obj.emit_by_name::<()>(
|
room_name
|
||||||
"session-requested",
|
);
|
||||||
&[
|
} else {
|
||||||
&"unique-session-id",
|
gst::error!(gst::CAT_DEFAULT, "Failed to decode room name from answer");
|
||||||
&"consumer-identifier",
|
|
||||||
&None::<WebRTCSessionDescription>,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
AnswerType::AnswerInUse => {
|
|
||||||
gst::error!(gst::CAT_DEFAULT, "Room is in use by another node");
|
|
||||||
}
|
|
||||||
AnswerType::AnswerOffline => {
|
|
||||||
gst::warning!(gst::CAT_DEFAULT, "Room is offline");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send our SDP offer
|
||||||
|
self_obj.emit_by_name::<()>(
|
||||||
|
"session-requested",
|
||||||
|
&[
|
||||||
|
&"unique-session-id",
|
||||||
|
&"consumer-identifier",
|
||||||
|
&None::<WebRTCSessionDescription>,
|
||||||
|
],
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to decode answer");
|
gst::error!(gst::CAT_DEFAULT, "Failed to decode answer");
|
||||||
}
|
}
|
||||||
@@ -177,89 +186,32 @@ impl SignallableImpl for Signaller {
|
|||||||
fn start(&self) {
|
fn start(&self) {
|
||||||
gst::info!(gst::CAT_DEFAULT, "Signaller started");
|
gst::info!(gst::CAT_DEFAULT, "Signaller started");
|
||||||
|
|
||||||
// Get WebSocket connection
|
|
||||||
let nestri_ws = {
|
|
||||||
self.nestri_ws
|
|
||||||
.read()
|
|
||||||
.clone()
|
|
||||||
.expect("NestriWebSocket not set")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register message callbacks
|
// Register message callbacks
|
||||||
self.register_callbacks();
|
self.register_callbacks();
|
||||||
|
|
||||||
// Subscribe to reconnection notifications
|
// Subscribe to reconnection notifications
|
||||||
let reconnected_notify = nestri_ws.subscribe_reconnected();
|
// TODO: Re-implement reconnection handling
|
||||||
|
|
||||||
// Clone necessary references
|
let Some(stream_room) = self.stream_room.read().clone() else {
|
||||||
let self_clone = self.obj().clone();
|
gst::error!(gst::CAT_DEFAULT, "Stream room not set");
|
||||||
let nestri_ws_clone = nestri_ws.clone();
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// Spawn a task to handle actions upon reconnection
|
let push_msg = MessageRaw {
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
// Wait for a reconnection notification
|
|
||||||
reconnected_notify.notified().await;
|
|
||||||
|
|
||||||
tracing::warn!("Reconnected to relay, re-negotiating...");
|
|
||||||
gst::warning!(gst::CAT_DEFAULT, "Reconnected to relay, re-negotiating...");
|
|
||||||
|
|
||||||
// Emit "session-ended" first to make sure the element is cleaned up
|
|
||||||
self_clone.emit_by_name::<bool>("session-ended", &[&"unique-session-id"]);
|
|
||||||
|
|
||||||
// Send a new join message
|
|
||||||
let join_msg = MessageJoin {
|
|
||||||
base: MessageBase {
|
|
||||||
payload_type: "join".to_string(),
|
|
||||||
latency: None,
|
|
||||||
},
|
|
||||||
joiner_type: JoinerType::JoinerNode,
|
|
||||||
};
|
|
||||||
if let Ok(encoded) = encode_message(&join_msg) {
|
|
||||||
if let Err(e) = nestri_ws_clone.send_message(encoded) {
|
|
||||||
gst::error!(
|
|
||||||
gst::CAT_DEFAULT,
|
|
||||||
"Failed to send join message after reconnection: {:?}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
gst::error!(
|
|
||||||
gst::CAT_DEFAULT,
|
|
||||||
"Failed to encode join message after reconnection"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we need to interact with GStreamer or GLib, schedule it on the main thread
|
|
||||||
let self_clone_for_main = self_clone.clone();
|
|
||||||
glib::MainContext::default().invoke(move || {
|
|
||||||
// Emit the "session-requested" signal
|
|
||||||
self_clone_for_main.emit_by_name::<()>(
|
|
||||||
"session-requested",
|
|
||||||
&[
|
|
||||||
&"unique-session-id",
|
|
||||||
&"consumer-identifier",
|
|
||||||
&None::<WebRTCSessionDescription>,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let join_msg = MessageJoin {
|
|
||||||
base: MessageBase {
|
base: MessageBase {
|
||||||
payload_type: "join".to_string(),
|
payload_type: "push-stream-room".to_string(),
|
||||||
latency: None,
|
latency: None,
|
||||||
},
|
},
|
||||||
joiner_type: JoinerType::JoinerNode,
|
data: serde_json::Value::from(stream_room),
|
||||||
};
|
};
|
||||||
if let Ok(encoded) = encode_message(&join_msg) {
|
|
||||||
if let Err(e) = nestri_ws.send_message(encoded) {
|
let Some(stream_protocol) = self.get_stream_protocol() else {
|
||||||
tracing::error!("Failed to send join message: {:?}", e);
|
gst::error!(gst::CAT_DEFAULT, "Stream protocol not set");
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to send join message: {:?}", e);
|
return;
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to encode join message");
|
if let Err(e) = stream_protocol.send_message(&push_msg) {
|
||||||
|
tracing::error!("Failed to send push stream room message: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,26 +220,21 @@ impl SignallableImpl for Signaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn send_sdp(&self, _session_id: &str, sdp: &WebRTCSessionDescription) {
|
fn send_sdp(&self, _session_id: &str, sdp: &WebRTCSessionDescription) {
|
||||||
let nestri_ws = {
|
|
||||||
self.nestri_ws
|
|
||||||
.read()
|
|
||||||
.clone()
|
|
||||||
.expect("NestriWebSocket not set")
|
|
||||||
};
|
|
||||||
let sdp_message = MessageSDP {
|
let sdp_message = MessageSDP {
|
||||||
base: MessageBase {
|
base: MessageBase {
|
||||||
payload_type: "sdp".to_string(),
|
payload_type: "offer".to_string(),
|
||||||
latency: None,
|
latency: None,
|
||||||
},
|
},
|
||||||
sdp: RTCSessionDescription::offer(sdp.sdp().as_text().unwrap()).unwrap(),
|
sdp: RTCSessionDescription::offer(sdp.sdp().as_text().unwrap()).unwrap(),
|
||||||
};
|
};
|
||||||
if let Ok(encoded) = encode_message(&sdp_message) {
|
|
||||||
if let Err(e) = nestri_ws.send_message(encoded) {
|
let Some(stream_protocol) = self.get_stream_protocol() else {
|
||||||
tracing::error!("Failed to send SDP message: {:?}", e);
|
gst::error!(gst::CAT_DEFAULT, "Stream protocol not set");
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to send SDP message: {:?}", e);
|
return;
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to encode SDP message");
|
if let Err(e) = stream_protocol.send_message(&sdp_message) {
|
||||||
|
tracing::error!("Failed to send SDP message: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,12 +245,6 @@ impl SignallableImpl for Signaller {
|
|||||||
sdp_m_line_index: u32,
|
sdp_m_line_index: u32,
|
||||||
sdp_mid: Option<String>,
|
sdp_mid: Option<String>,
|
||||||
) {
|
) {
|
||||||
let nestri_ws = {
|
|
||||||
self.nestri_ws
|
|
||||||
.read()
|
|
||||||
.clone()
|
|
||||||
.expect("NestriWebSocket not set")
|
|
||||||
};
|
|
||||||
let candidate_init = RTCIceCandidateInit {
|
let candidate_init = RTCIceCandidateInit {
|
||||||
candidate: candidate.to_string(),
|
candidate: candidate.to_string(),
|
||||||
sdp_mid,
|
sdp_mid,
|
||||||
@@ -312,18 +253,19 @@ impl SignallableImpl for Signaller {
|
|||||||
};
|
};
|
||||||
let ice_message = MessageICE {
|
let ice_message = MessageICE {
|
||||||
base: MessageBase {
|
base: MessageBase {
|
||||||
payload_type: "ice".to_string(),
|
payload_type: "ice-candidate".to_string(),
|
||||||
latency: None,
|
latency: None,
|
||||||
},
|
},
|
||||||
candidate: candidate_init,
|
candidate: candidate_init,
|
||||||
};
|
};
|
||||||
if let Ok(encoded) = encode_message(&ice_message) {
|
|
||||||
if let Err(e) = nestri_ws.send_message(encoded) {
|
let Some(stream_protocol) = self.get_stream_protocol() else {
|
||||||
tracing::error!("Failed to send ICE message: {:?}", e);
|
gst::error!(gst::CAT_DEFAULT, "Stream protocol not set");
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to send ICE message: {:?}", e);
|
return;
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to encode ICE message");
|
if let Err(e) = stream_protocol.send_message(&ice_message) {
|
||||||
|
tracing::error!("Failed to send ICE candidate message: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::websocket::NestriWebSocket;
|
use crate::p2p::p2p::NestriConnection;
|
||||||
use gst::glib;
|
use gst::glib;
|
||||||
use gst::subclass::prelude::*;
|
use gst::subclass::prelude::*;
|
||||||
use gstrswebrtc::signaller::Signallable;
|
use gstrswebrtc::signaller::Signallable;
|
||||||
@@ -11,15 +11,20 @@ glib::wrapper! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NestriSignaller {
|
impl NestriSignaller {
|
||||||
pub fn new(nestri_ws: Arc<NestriWebSocket>, wayland_src: Arc<gst::Element>) -> Self {
|
pub async fn new(
|
||||||
|
room: String,
|
||||||
|
nestri_conn: NestriConnection,
|
||||||
|
wayland_src: Arc<gst::Element>,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let obj: Self = glib::Object::new();
|
let obj: Self = glib::Object::new();
|
||||||
obj.imp().set_nestri_ws(nestri_ws);
|
obj.imp().set_stream_room(room);
|
||||||
|
obj.imp().set_nestri_connection(nestri_conn).await?;
|
||||||
obj.imp().set_wayland_src(wayland_src);
|
obj.imp().set_wayland_src(wayland_src);
|
||||||
obj
|
Ok(obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Default for NestriSignaller {
|
impl Default for NestriSignaller {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
panic!("Cannot create NestriSignaller without NestriWebSocket");
|
panic!("Cannot create NestriSignaller without NestriConnection and WaylandSrc");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
packages/server/src/p2p.rs
Normal file
3
packages/server/src/p2p.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod p2p;
|
||||||
|
pub mod p2p_safestream;
|
||||||
|
pub mod p2p_protocol_stream;
|
||||||
131
packages/server/src/p2p/p2p.rs
Normal file
131
packages/server/src/p2p/p2p.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use futures_util::StreamExt;
|
||||||
|
use libp2p::multiaddr::Protocol;
|
||||||
|
use libp2p::{
|
||||||
|
Multiaddr, PeerId, Swarm, identify, noise, ping,
|
||||||
|
swarm::{NetworkBehaviour, SwarmEvent},
|
||||||
|
tcp, yamux,
|
||||||
|
};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct NestriConnection {
|
||||||
|
pub peer_id: PeerId,
|
||||||
|
pub control: libp2p_stream::Control,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
struct NestriBehaviour {
|
||||||
|
identify: identify::Behaviour,
|
||||||
|
ping: ping::Behaviour,
|
||||||
|
stream: libp2p_stream::Behaviour,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NestriP2P {
|
||||||
|
swarm: Arc<Mutex<Swarm<NestriBehaviour>>>,
|
||||||
|
}
|
||||||
|
impl NestriP2P {
|
||||||
|
pub async fn new() -> Result<Self, Box<dyn Error>> {
|
||||||
|
let swarm = Arc::new(Mutex::new(
|
||||||
|
libp2p::SwarmBuilder::with_new_identity()
|
||||||
|
.with_tokio()
|
||||||
|
.with_tcp(
|
||||||
|
tcp::Config::default(),
|
||||||
|
noise::Config::new,
|
||||||
|
yamux::Config::default,
|
||||||
|
)?
|
||||||
|
.with_dns()?
|
||||||
|
.with_behaviour(|key| {
|
||||||
|
let identify_behaviour = identify::Behaviour::new(identify::Config::new(
|
||||||
|
"/ipfs/id/1.0.0".to_string(),
|
||||||
|
key.public(),
|
||||||
|
));
|
||||||
|
let ping_behaviour = ping::Behaviour::default();
|
||||||
|
let stream_behaviour = libp2p_stream::Behaviour::default();
|
||||||
|
|
||||||
|
Ok(NestriBehaviour {
|
||||||
|
identify: identify_behaviour,
|
||||||
|
ping: ping_behaviour,
|
||||||
|
stream: stream_behaviour,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.build(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Spawn the swarm event loop
|
||||||
|
let swarm_clone = swarm.clone();
|
||||||
|
tokio::spawn(swarm_loop(swarm_clone));
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut swarm_lock = swarm.lock().await;
|
||||||
|
swarm_lock.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; // IPv4 - TCP Raw
|
||||||
|
swarm_lock.listen_on("/ip6/::/tcp/0".parse()?)?; // IPv6 - TCP Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(NestriP2P { swarm })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect(&self, conn_url: &str) -> Result<NestriConnection, Box<dyn Error>> {
|
||||||
|
let conn_addr: Multiaddr = conn_url.parse()?;
|
||||||
|
|
||||||
|
let mut swarm_lock = self.swarm.lock().await;
|
||||||
|
swarm_lock.dial(conn_addr.clone())?;
|
||||||
|
|
||||||
|
let Some(Protocol::P2p(peer_id)) = conn_addr.clone().iter().last() else {
|
||||||
|
return Err("Invalid connection URL: missing peer ID".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(NestriConnection {
|
||||||
|
peer_id,
|
||||||
|
control: swarm_lock.behaviour().stream.new_control(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn swarm_loop(swarm: Arc<Mutex<Swarm<NestriBehaviour>>>) {
|
||||||
|
loop {
|
||||||
|
let event = {
|
||||||
|
let mut swarm_lock = swarm.lock().await;
|
||||||
|
swarm_lock.select_next_some().await
|
||||||
|
};
|
||||||
|
match event {
|
||||||
|
SwarmEvent::NewListenAddr { address, .. } => {
|
||||||
|
tracing::info!("Listening on: '{}'", address);
|
||||||
|
}
|
||||||
|
SwarmEvent::ConnectionEstablished { peer_id, .. } => {
|
||||||
|
tracing::info!("Connection established with peer: {}", peer_id);
|
||||||
|
}
|
||||||
|
SwarmEvent::ConnectionClosed { peer_id, cause, .. } => {
|
||||||
|
if let Some(err) = cause {
|
||||||
|
tracing::error!(
|
||||||
|
"Connection with peer {} closed due to error: {}",
|
||||||
|
peer_id,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::info!("Connection with peer {} closed", peer_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SwarmEvent::IncomingConnection {
|
||||||
|
local_addr,
|
||||||
|
send_back_addr,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
tracing::info!(
|
||||||
|
"Incoming connection from: {} (send back to: {})",
|
||||||
|
local_addr,
|
||||||
|
send_back_addr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
|
||||||
|
if let Some(peer_id) = peer_id {
|
||||||
|
tracing::error!("Failed to connect to peer {}: {}", peer_id, error);
|
||||||
|
} else {
|
||||||
|
tracing::error!("Failed to connect: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
packages/server/src/p2p/p2p_protocol_stream.rs
Normal file
149
packages/server/src/p2p/p2p_protocol_stream.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use crate::p2p::p2p::NestriConnection;
|
||||||
|
use crate::p2p::p2p_safestream::SafeStream;
|
||||||
|
use libp2p::StreamProtocol;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
// Cloneable callback type
|
||||||
|
pub type CallbackInner = dyn Fn(Vec<u8>) + Send + Sync + 'static;
|
||||||
|
pub struct Callback(Arc<CallbackInner>);
|
||||||
|
impl Callback {
|
||||||
|
pub fn new<F>(f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Vec<u8>) + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Callback(Arc::new(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn call(&self, data: Vec<u8>) {
|
||||||
|
self.0(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Clone for Callback {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Callback(Arc::clone(&self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Box<CallbackInner>> for Callback {
|
||||||
|
fn from(boxed: Box<CallbackInner>) -> Self {
|
||||||
|
Callback(Arc::from(boxed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NestriStreamProtocol manages the stream protocol for Nestri connections.
|
||||||
|
pub struct NestriStreamProtocol {
|
||||||
|
tx: mpsc::Sender<Vec<u8>>,
|
||||||
|
safe_stream: Arc<SafeStream>,
|
||||||
|
callbacks: Arc<RwLock<HashMap<String, Callback>>>,
|
||||||
|
}
|
||||||
|
impl NestriStreamProtocol {
|
||||||
|
const NESTRI_PROTOCOL_STREAM_PUSH: StreamProtocol =
|
||||||
|
StreamProtocol::new("/nestri-relay/stream-push/1.0.0");
|
||||||
|
|
||||||
|
pub async fn new(
|
||||||
|
nestri_connection: NestriConnection,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let mut nestri_connection = nestri_connection.clone();
|
||||||
|
let push_stream = match nestri_connection
|
||||||
|
.control
|
||||||
|
.open_stream(nestri_connection.peer_id, Self::NESTRI_PROTOCOL_STREAM_PUSH)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(stream) => stream,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(Box::new(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel(1000);
|
||||||
|
|
||||||
|
let sp = NestriStreamProtocol {
|
||||||
|
tx,
|
||||||
|
safe_stream: Arc::new(SafeStream::new(push_stream)),
|
||||||
|
callbacks: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spawn the loops
|
||||||
|
sp.spawn_read_loop();
|
||||||
|
sp.spawn_write_loop(rx);
|
||||||
|
|
||||||
|
Ok(sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_read_loop(&self) -> tokio::task::JoinHandle<()> {
|
||||||
|
let safe_stream = self.safe_stream.clone();
|
||||||
|
let callbacks = self.callbacks.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let data = {
|
||||||
|
match safe_stream.receive_raw().await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error receiving data: {}", e);
|
||||||
|
break; // Exit the loop on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match serde_json::from_slice::<crate::messages::MessageBase>(&data) {
|
||||||
|
Ok(base_message) => {
|
||||||
|
let response_type = base_message.payload_type;
|
||||||
|
let callback = {
|
||||||
|
let callbacks_lock = callbacks.read().unwrap();
|
||||||
|
callbacks_lock.get(&response_type).cloned()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(callback) = callback {
|
||||||
|
// Call the registered callback with the raw data
|
||||||
|
callback.call(data);
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
"No callback registered for response type: {}",
|
||||||
|
response_type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to decode message: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_write_loop(&self, mut rx: mpsc::Receiver<Vec<u8>>) -> tokio::task::JoinHandle<()> {
|
||||||
|
let safe_stream = self.safe_stream.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
// Wait for a message from the channel
|
||||||
|
if let Some(tx_data) = rx.recv().await {
|
||||||
|
if let Err(e) = safe_stream.send_raw(&tx_data).await {
|
||||||
|
tracing::error!("Error sending data: {:?}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::info!("Receiver closed, exiting write loop");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_message<M: serde::Serialize>(
|
||||||
|
&self,
|
||||||
|
message: &M,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let json_data = serde_json::to_vec(message)?;
|
||||||
|
self.tx.try_send(json_data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a callback for a specific response type
|
||||||
|
pub fn register_callback<F>(&self, response_type: &str, callback: F)
|
||||||
|
where
|
||||||
|
F: Fn(Vec<u8>) + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let mut callbacks_lock = self.callbacks.write().unwrap();
|
||||||
|
callbacks_lock.insert(response_type.to_string(), Callback::new(callback));
|
||||||
|
}
|
||||||
|
}
|
||||||
105
packages/server/src/p2p/p2p_safestream.rs
Normal file
105
packages/server/src/p2p/p2p_safestream.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
use futures_util::io::{ReadHalf, WriteHalf};
|
||||||
|
use futures_util::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use prost::Message;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
const MAX_SIZE: usize = 1024 * 1024; // 1MB
|
||||||
|
|
||||||
|
pub struct SafeStream {
|
||||||
|
stream_read: Arc<Mutex<ReadHalf<libp2p::Stream>>>,
|
||||||
|
stream_write: Arc<Mutex<WriteHalf<libp2p::Stream>>>,
|
||||||
|
}
|
||||||
|
impl SafeStream {
|
||||||
|
pub fn new(stream: libp2p::Stream) -> Self {
|
||||||
|
let (read, write) = stream.split();
|
||||||
|
SafeStream {
|
||||||
|
stream_read: Arc::new(Mutex::new(read)),
|
||||||
|
stream_write: Arc::new(Mutex::new(write)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_json<T: Serialize>(
|
||||||
|
&self,
|
||||||
|
data: &T,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let json_data = serde_json::to_vec(data)?;
|
||||||
|
tracing::info!("Sending JSON");
|
||||||
|
let e = self.send_with_length_prefix(&json_data).await;
|
||||||
|
tracing::info!("Sent JSON");
|
||||||
|
e
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn receive_json<T: DeserializeOwned>(&self) -> Result<T, Box<dyn std::error::Error>> {
|
||||||
|
let data = self.receive_with_length_prefix().await?;
|
||||||
|
let msg = serde_json::from_slice(&data)?;
|
||||||
|
Ok(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_proto<M: Message>(&self, msg: &M) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut proto_data = Vec::new();
|
||||||
|
msg.encode(&mut proto_data)?;
|
||||||
|
self.send_with_length_prefix(&proto_data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn receive_proto<M: Message + Default>(
|
||||||
|
&self,
|
||||||
|
) -> Result<M, Box<dyn std::error::Error>> {
|
||||||
|
let data = self.receive_with_length_prefix().await?;
|
||||||
|
let msg = M::decode(&*data)?;
|
||||||
|
Ok(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_raw(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.send_with_length_prefix(data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn receive_raw(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||||
|
self.receive_with_length_prefix().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_with_length_prefix(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if data.len() > MAX_SIZE {
|
||||||
|
return Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"Data exceeds maximum size",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stream_write = self.stream_write.lock().await;
|
||||||
|
|
||||||
|
// Write the 4-byte length prefix
|
||||||
|
let mut length_prefix = [0u8; 4];
|
||||||
|
BigEndian::write_u32(&mut length_prefix, data.len() as u32);
|
||||||
|
stream_write.write_all(&length_prefix).await?;
|
||||||
|
|
||||||
|
// Write the actual data
|
||||||
|
stream_write.write_all(data).await?;
|
||||||
|
stream_write.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_with_length_prefix(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||||
|
let mut stream_read = self.stream_read.lock().await;
|
||||||
|
|
||||||
|
// Read the 4-byte length prefix
|
||||||
|
let mut length_prefix = [0u8; 4];
|
||||||
|
stream_read.read_exact(&mut length_prefix).await?;
|
||||||
|
let length = BigEndian::read_u32(&length_prefix) as usize;
|
||||||
|
|
||||||
|
if length > MAX_SIZE {
|
||||||
|
return Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"Data exceeds maximum size",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the actual data
|
||||||
|
let mut buffer = vec![0; length];
|
||||||
|
stream_read.read_exact(&mut buffer).await?;
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
use crate::messages::decode_message;
|
|
||||||
use futures_util::StreamExt;
|
|
||||||
use futures_util::sink::SinkExt;
|
|
||||||
use futures_util::stream::{SplitSink, SplitStream};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio::sync::{Mutex, Notify, mpsc};
|
|
||||||
use tokio::time::sleep;
|
|
||||||
use tokio_tungstenite::tungstenite::{Message, Utf8Bytes};
|
|
||||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async};
|
|
||||||
|
|
||||||
type Callback = Box<dyn Fn(String) + Send + Sync>;
|
|
||||||
type WSRead = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
|
|
||||||
type WSWrite = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct NestriWebSocket {
|
|
||||||
ws_url: String,
|
|
||||||
reader: Arc<Mutex<Option<WSRead>>>,
|
|
||||||
writer: Arc<Mutex<Option<WSWrite>>>,
|
|
||||||
callbacks: Arc<RwLock<HashMap<String, Callback>>>,
|
|
||||||
message_tx: mpsc::UnboundedSender<String>,
|
|
||||||
reconnected_notify: Arc<Notify>,
|
|
||||||
}
|
|
||||||
impl NestriWebSocket {
|
|
||||||
pub async fn new(ws_url: String) -> Result<NestriWebSocket, Box<dyn Error>> {
|
|
||||||
// Attempt to connect to the WebSocket
|
|
||||||
let ws_stream = NestriWebSocket::do_connect(&ws_url).await.unwrap();
|
|
||||||
|
|
||||||
// Split the stream into read and write halves
|
|
||||||
let (write, read) = ws_stream.split();
|
|
||||||
|
|
||||||
// Create the message channel
|
|
||||||
let (message_tx, message_rx) = mpsc::unbounded_channel();
|
|
||||||
|
|
||||||
let ws = NestriWebSocket {
|
|
||||||
ws_url,
|
|
||||||
reader: Arc::new(Mutex::new(Some(read))),
|
|
||||||
writer: Arc::new(Mutex::new(Some(write))),
|
|
||||||
callbacks: Arc::new(RwLock::new(HashMap::new())),
|
|
||||||
message_tx: message_tx.clone(),
|
|
||||||
reconnected_notify: Arc::new(Notify::new()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Spawn the read loop
|
|
||||||
ws.spawn_read_loop();
|
|
||||||
// Spawn the write loop
|
|
||||||
ws.spawn_write_loop(message_rx);
|
|
||||||
|
|
||||||
Ok(ws)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn do_connect(
|
|
||||||
ws_url: &str,
|
|
||||||
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>, Box<dyn Error + Send + Sync>> {
|
|
||||||
loop {
|
|
||||||
match connect_async(ws_url).await {
|
|
||||||
Ok((ws_stream, _)) => {
|
|
||||||
return Ok(ws_stream);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to connect to WebSocket, retrying: {:?}", e);
|
|
||||||
sleep(Duration::from_secs(3)).await; // Wait before retrying
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles message -> callback calls and reconnects on error/disconnect
|
|
||||||
fn spawn_read_loop(&self) {
|
|
||||||
let reader = self.reader.clone();
|
|
||||||
let callbacks = self.callbacks.clone();
|
|
||||||
let self_clone = self.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
// Lock the reader to get the WSRead, then drop the lock
|
|
||||||
let ws_read_option = {
|
|
||||||
let mut reader_lock = reader.lock().await;
|
|
||||||
reader_lock.take()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut ws_read = match ws_read_option {
|
|
||||||
Some(ws_read) => ws_read,
|
|
||||||
None => {
|
|
||||||
tracing::error!("Reader is None, cannot proceed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
while let Some(message_result) = ws_read.next().await {
|
|
||||||
match message_result {
|
|
||||||
Ok(message) => {
|
|
||||||
let data = message
|
|
||||||
.into_text()
|
|
||||||
.expect("failed to turn message into text");
|
|
||||||
let base_message = match decode_message(data.to_string()) {
|
|
||||||
Ok(base_message) => base_message,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to decode message: {:?}", e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let callbacks_lock = callbacks.read().unwrap();
|
|
||||||
if let Some(callback) = callbacks_lock.get(&base_message.payload_type) {
|
|
||||||
let data = data.clone();
|
|
||||||
callback(data.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(
|
|
||||||
"Error receiving message: {:?}, reconnecting in 3 seconds...",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
sleep(Duration::from_secs(3)).await;
|
|
||||||
self_clone.reconnect().await.unwrap();
|
|
||||||
break; // Break the inner loop to get a new ws_read
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// After reconnection, the loop continues, and we acquire a new ws_read
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_write_loop(&self, mut message_rx: mpsc::UnboundedReceiver<String>) {
|
|
||||||
let writer = self.writer.clone();
|
|
||||||
let self_clone = self.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
// Wait for a message from the channel
|
|
||||||
if let Some(message) = message_rx.recv().await {
|
|
||||||
loop {
|
|
||||||
// Acquire the writer lock
|
|
||||||
let mut writer_lock = writer.lock().await;
|
|
||||||
if let Some(writer) = writer_lock.as_mut() {
|
|
||||||
// Try to send the message over the WebSocket
|
|
||||||
match writer
|
|
||||||
.send(Message::Text(Utf8Bytes::from(message.clone())))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
// Message sent successfully
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Error sending message: {:?}", e);
|
|
||||||
// Attempt to reconnect
|
|
||||||
if let Err(e) = self_clone.reconnect().await {
|
|
||||||
tracing::error!("Error during reconnection: {:?}", e);
|
|
||||||
// Wait before retrying
|
|
||||||
sleep(Duration::from_secs(3)).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tracing::error!("Writer is None, cannot send message");
|
|
||||||
// Attempt to reconnect
|
|
||||||
if let Err(e) = self_clone.reconnect().await {
|
|
||||||
tracing::error!("Error during reconnection: {:?}", e);
|
|
||||||
// Wait before retrying
|
|
||||||
sleep(Duration::from_secs(3)).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reconnect(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
|
||||||
loop {
|
|
||||||
match NestriWebSocket::do_connect(&self.ws_url).await {
|
|
||||||
Ok(ws_stream) => {
|
|
||||||
let (write, read) = ws_stream.split();
|
|
||||||
{
|
|
||||||
let mut writer_lock = self.writer.lock().await;
|
|
||||||
*writer_lock = Some(write);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let mut reader_lock = self.reader.lock().await;
|
|
||||||
*reader_lock = Some(read);
|
|
||||||
}
|
|
||||||
// Notify subscribers of successful reconnection
|
|
||||||
self.reconnected_notify.notify_waiters();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to reconnect to WebSocket: {:?}", e);
|
|
||||||
sleep(Duration::from_secs(3)).await; // Wait before retrying
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a message through the WebSocket
|
|
||||||
pub fn send_message(&self, message: String) -> Result<(), Box<dyn Error>> {
|
|
||||||
self.message_tx
|
|
||||||
.send(message)
|
|
||||||
.map_err(|e| format!("Failed to send message: {:?}", e).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register a callback for a specific response type
|
|
||||||
pub fn register_callback<F>(&self, response_type: &str, callback: F)
|
|
||||||
where
|
|
||||||
F: Fn(String) + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
let mut callbacks_lock = self.callbacks.write().unwrap();
|
|
||||||
callbacks_lock.insert(response_type.to_string(), Box::new(callback));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscribe to event for reconnection
|
|
||||||
pub fn subscribe_reconnected(&self) -> Arc<Notify> {
|
|
||||||
self.reconnected_notify.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -82,7 +82,7 @@ export const App: Component = () => {
|
|||||||
: "light",
|
: "light",
|
||||||
);
|
);
|
||||||
|
|
||||||
const darkMode = window.matchMedia("(prefers-color-scheme: dark)");
|
const darkMode = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
const setColorScheme = (e: MediaQueryListEvent) => {
|
const setColorScheme = (e: MediaQueryListEvent) => {
|
||||||
setTheme(e.matches ? "dark" : "light");
|
setTheme(e.matches ? "dark" : "light");
|
||||||
};
|
};
|
||||||
@@ -94,26 +94,26 @@ export const App: Component = () => {
|
|||||||
const storage = useStorage();
|
const storage = useStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenAuthProvider
|
// <OpenAuthProvider
|
||||||
issuer={import.meta.env.VITE_AUTH_URL}
|
// issuer={import.meta.env.VITE_AUTH_URL}
|
||||||
clientID="web"
|
// clientID="web"
|
||||||
>
|
// >
|
||||||
<Root class={theme() === "light" ? lightClass : darkClass}>
|
<Root class={theme() === "light" ? lightClass : darkClass}>
|
||||||
<Router>
|
<Router>
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
component={(props) => (
|
component={(props) => (
|
||||||
<AccountProvider
|
// <AccountProvider
|
||||||
loadingUI={
|
// loadingUI={
|
||||||
<FullScreen>
|
// <FullScreen>
|
||||||
<Text weight='semibold' spacing='xs' size="3xl" font="heading" >Confirming your identity…</Text>
|
// <Text weight='semibold' spacing='xs' size="3xl" font="heading" >Confirming your identity…</Text>
|
||||||
</FullScreen>
|
// </FullScreen>
|
||||||
}>
|
// }>
|
||||||
<ZeroProvider>
|
// <ZeroProvider>
|
||||||
{/* props.children */}
|
props.children
|
||||||
{props.children}
|
// {props.children}
|
||||||
</ZeroProvider>
|
// </ZeroProvider>
|
||||||
</AccountProvider>
|
// </AccountProvider>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Route path=":steamID">{SteamRoute}</Route>
|
<Route path=":steamID">{SteamRoute}</Route>
|
||||||
@@ -145,6 +145,6 @@ export const App: Component = () => {
|
|||||||
</Route>
|
</Route>
|
||||||
</Router>
|
</Router>
|
||||||
</Root>
|
</Root>
|
||||||
</OpenAuthProvider>
|
// </OpenAuthProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
54
packages/www/src/assets/service-worker.js
Normal file
54
packages/www/src/assets/service-worker.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const CACHE_NAME = 'image-cache-v1';
|
||||||
|
const AUTH_TOKEN = 'Bearer YOUR_DYNAMIC_AUTH_TOKEN'; // Replace at runtime if needed
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
self.skipWaiting(); // Activate immediately
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
clients.claim(); // Take control of all clients
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const req = event.request;
|
||||||
|
|
||||||
|
// Only intercept image requests
|
||||||
|
if (req.destination !== 'image') return;
|
||||||
|
|
||||||
|
// Only intercept our image requests
|
||||||
|
const url = new URL(req.url);
|
||||||
|
if (import.meta.env.VITE_CDN_URL !== url.origin || url.pathname.includes("/public")) return;
|
||||||
|
|
||||||
|
event.respondWith(handleImageRequest(req));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleImageRequest(request) {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
|
||||||
|
const cachedResponse = await cache.match(request);
|
||||||
|
if (cachedResponse) return cachedResponse;
|
||||||
|
|
||||||
|
// Clone and modify the request with Authorization header
|
||||||
|
const modifiedRequest = new Request(request.url, {
|
||||||
|
method: request.method,
|
||||||
|
headers: {
|
||||||
|
...Object.fromEntries(request.headers.entries()),
|
||||||
|
Authorization: AUTH_TOKEN,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
mode: 'same-origin',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(modifiedRequest);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await cache.put(request, response.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
return new Response('Image load failed', { status: 503 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,19 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register('/src/assets/service-worker.js')
|
||||||
|
.then((reg) => {
|
||||||
|
console.log('[SW] Registered:', reg.scope);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('[SW] Registration failed:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<StorageProvider>
|
<StorageProvider>
|
||||||
|
|||||||
@@ -1,524 +0,0 @@
|
|||||||
import { FullScreen, theme } from "@nestri/www/ui";
|
|
||||||
import { styled } from "@macaron-css/solid";
|
|
||||||
import { Header } from "@nestri/www/pages/steam/header";
|
|
||||||
import { Modal } from "@nestri/www/ui/modal";
|
|
||||||
import { createEffect, createSignal, Match, onCleanup, Switch } from "solid-js";
|
|
||||||
import { Text } from "@nestri/www/ui/text"
|
|
||||||
import { globalStyle, keyframes } from "@macaron-css/core";
|
|
||||||
import { A } from "@solidjs/router";
|
|
||||||
import Avatar from "@nestri/www/ui/avatar";
|
|
||||||
import { Portal } from "@nestri/www/common/portal";
|
|
||||||
import { QrCodeComponent } from "@nestri/www/components"
|
|
||||||
|
|
||||||
|
|
||||||
const LastPlayedWrapper = styled("div", {
|
|
||||||
base: {
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
justifyContent: "center",
|
|
||||||
minHeight: 700,
|
|
||||||
height: "50vw",
|
|
||||||
maxHeight: 800,
|
|
||||||
WebkitBoxPack: "center",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
":after": {
|
|
||||||
content: "",
|
|
||||||
pointerEvents: "none",
|
|
||||||
userSelect: "none",
|
|
||||||
background: `linear-gradient(to bottom,transparent,${theme.color.background.d200})`,
|
|
||||||
width: "100%",
|
|
||||||
left: 0,
|
|
||||||
position: "absolute",
|
|
||||||
bottom: -1,
|
|
||||||
zIndex: 3,
|
|
||||||
height: 320,
|
|
||||||
backdropFilter: "blur(2px)",
|
|
||||||
WebkitBackdropFilter: "blur(1px)",
|
|
||||||
WebkitMaskImage: `linear-gradient(to top,${theme.color.background.d200} 25%,transparent)`,
|
|
||||||
maskImage: `linear-gradient(to top,${theme.color.background.d200} 25%,transparent)`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const LastPlayedFader = styled("div", {
|
|
||||||
base: {
|
|
||||||
position: "absolute",
|
|
||||||
width: "100%",
|
|
||||||
height: "3rem",
|
|
||||||
backgroundColor: "rgba(0,0,0,.08)",
|
|
||||||
mixBlendMode: "multiply",
|
|
||||||
backdropFilter: "saturate(160%) blur(60px)",
|
|
||||||
WebkitBackdropFilter: "saturate(160%) blur(60px)",
|
|
||||||
maskImage: "linear-gradient(to top,rgba(0,0,0,.15) 0%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)",
|
|
||||||
// background: "linear-gradient(rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0.3) 50%, rgba(10, 0, 0, 0.15) 65%, rgba(0, 0, 0, 0.075) 75.5%, rgba(0, 0, 0, 0.035) 82.85%, rgba(0, 0, 0, 0.02) 88%, rgba(0, 0, 0, 0) 100%)",
|
|
||||||
opacity: 0.6,
|
|
||||||
// backdropFilter: "blur(16px)",
|
|
||||||
pointerEvents: "none",
|
|
||||||
zIndex: 1,
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const BackgroundImage = styled("div", {
|
|
||||||
base: {
|
|
||||||
position: "fixed",
|
|
||||||
inset: 0,
|
|
||||||
backgroundColor: theme.color.background.d200,
|
|
||||||
backgroundSize: "cover",
|
|
||||||
zIndex: 0,
|
|
||||||
transitionDuration: "0.2s",
|
|
||||||
transitionTimingFunction: "cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
transitionProperty: "opacity",
|
|
||||||
backgroundImage: "url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/1203190/ss_97ea9b0b5a6adf3436b31d389cd18d3a647ee4bf.jpg)"
|
|
||||||
// backgroundImage: "url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/3373660/c4993923f605b608939536b5f2521913850b028a/ss_c4993923f605b608939536b5f2521913850b028a.jpg)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const LogoBackgroundImage = styled("div", {
|
|
||||||
base: {
|
|
||||||
position: "fixed",
|
|
||||||
top: "2rem",
|
|
||||||
height: 240,
|
|
||||||
// width: 320,
|
|
||||||
aspectRatio: "16 / 9",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%,0%)",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
zIndex: 1,
|
|
||||||
transitionDuration: "0.2s",
|
|
||||||
transitionTimingFunction: "cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
transitionProperty: "opacity",
|
|
||||||
backgroundImage: "url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/1203190/logo_2x.png)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const Material = styled("div", {
|
|
||||||
base: {
|
|
||||||
backdropFilter: "saturate(160%) blur(60px)",
|
|
||||||
WebkitBackdropFilter: "saturate(160%) blur(60px)",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
position: "absolute",
|
|
||||||
borderRadius: 6,
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
maskImage: "linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,0) 40.82%,rgba(0,0,0,.15) 50%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)",
|
|
||||||
WebkitMaskImage: "linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,0) 40.82%,rgba(0,0,0,.15) 50%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const JoeColor = styled("div", {
|
|
||||||
base: {
|
|
||||||
backgroundColor: "rgba(0,0,0,.08)",
|
|
||||||
mixBlendMode: "multiply",
|
|
||||||
position: "absolute",
|
|
||||||
borderRadius: 6,
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
maskImage: "linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,0) 40.82%,rgba(0,0,0,.15) 50%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)",
|
|
||||||
WebkitMaskImage: "linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,0) 40.82%,rgba(0,0,0,.15) 50%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GamesContainer = styled("div", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
flexDirection: "column",
|
|
||||||
zIndex: 10,
|
|
||||||
isolation: "isolate",
|
|
||||||
backgroundColor: theme.color.background.d200,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GamesWrapper = styled("div", {
|
|
||||||
base: {
|
|
||||||
maxWidth: "70vw",
|
|
||||||
width: "100%",
|
|
||||||
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
|
|
||||||
margin: "0 auto",
|
|
||||||
display: "grid",
|
|
||||||
marginTop: -80,
|
|
||||||
columnGap: 12,
|
|
||||||
rowGap: 10
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GameImage = styled("img", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
aspectRatio: "460/215",
|
|
||||||
borderRadius: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GameSquareImage = styled("img", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
borderRadius: 10,
|
|
||||||
transitionDuration: "0.2s",
|
|
||||||
transitionTimingFunction: "cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
transitionProperty: "all",
|
|
||||||
cursor: "pointer",
|
|
||||||
border: `2px solid transparent`,
|
|
||||||
":hover": {
|
|
||||||
transform: "scale(1.05)",
|
|
||||||
outline: `2px solid ${theme.color.brand}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GameImageCapsule = styled("img", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
aspectRatio: "374/448",
|
|
||||||
borderRadius: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamLibrary = styled("div", {
|
|
||||||
base: {
|
|
||||||
borderTop: `1px solid ${theme.color.gray.d400}`,
|
|
||||||
padding: "20px 0",
|
|
||||||
margin: "20px auto",
|
|
||||||
width: "100%",
|
|
||||||
display: "grid",
|
|
||||||
// backgroundColor: "red",
|
|
||||||
maxWidth: "70vw",
|
|
||||||
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
||||||
columnGap: 20,
|
|
||||||
rowGap: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const Title = styled("h3", {
|
|
||||||
base: {
|
|
||||||
textAlign: "left",
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
fontWeight: theme.font.weight.medium,
|
|
||||||
fontSize: theme.font.size["2xl"],
|
|
||||||
letterSpacing: -0.7,
|
|
||||||
gridColumn: "1/-1",
|
|
||||||
marginBottom: 20,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameTitle = styled("h3", {
|
|
||||||
base: {
|
|
||||||
textAlign: "left",
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
fontWeight: theme.font.weight.medium,
|
|
||||||
fontSize: theme.font.size["xl"],
|
|
||||||
letterSpacing: -0.7,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameSubTitle = styled("span", {
|
|
||||||
base: {
|
|
||||||
textAlign: "left",
|
|
||||||
fontWeight: theme.font.weight.regular,
|
|
||||||
color: theme.color.gray.d900,
|
|
||||||
fontSize: theme.font.size["base"],
|
|
||||||
letterSpacing: -0.4,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SubTitle = styled("span", {
|
|
||||||
base: {
|
|
||||||
textAlign: "left",
|
|
||||||
fontWeight: theme.font.weight.regular,
|
|
||||||
color: theme.color.gray.d900,
|
|
||||||
fontSize: theme.font.size["base"],
|
|
||||||
letterSpacing: -0.4,
|
|
||||||
gridColumn: "1/-1",
|
|
||||||
marginTop: -20,
|
|
||||||
marginBottom: 20,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const FriendsList = styled("div", {
|
|
||||||
base: {
|
|
||||||
borderTop: `1px solid ${theme.color.gray.d400}`,
|
|
||||||
padding: "20px 0",
|
|
||||||
margin: "20px auto",
|
|
||||||
width: "100%",
|
|
||||||
display: "grid",
|
|
||||||
maxWidth: "70vw",
|
|
||||||
gridTemplateColumns: "repeat(5, minmax(0, 1fr))",
|
|
||||||
columnGap: 12,
|
|
||||||
rowGap: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const FriendContainer = styled("div", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
minHeight: "calc(100% + 20px)",
|
|
||||||
aspectRatio: "300/380",
|
|
||||||
borderRadius: 15,
|
|
||||||
position: "relative",
|
|
||||||
padding: "35px 17px",
|
|
||||||
border: `1px solid ${theme.color.gray.d500}`,
|
|
||||||
backgroundColor: theme.color.background.d100,
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const FriendsSubText = styled("span", {
|
|
||||||
base: {
|
|
||||||
color: theme.color.gray.d900,
|
|
||||||
fontSize: theme.font.size.sm,
|
|
||||||
marginTop: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const FriendsText = styled("h3", {
|
|
||||||
base: {
|
|
||||||
fontSize: theme.font.size["lg"],
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
marginTop: 20,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const FriendsInviteButton = styled("button", {
|
|
||||||
base: {
|
|
||||||
minWidth: 48,
|
|
||||||
borderRadius: 9999,
|
|
||||||
textAlign: "center",
|
|
||||||
padding: "0px 24px",
|
|
||||||
fontSize: theme.font.size["base"],
|
|
||||||
lineHeight: "1.75",
|
|
||||||
marginTop: 20,
|
|
||||||
cursor: "pointer",
|
|
||||||
fontWeight: theme.font.weight.bold,
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
border: `1px solid ${theme.color.gray.d100}`,
|
|
||||||
backgroundColor: theme.color.blue.d700,
|
|
||||||
transition: "all 0.2s cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
":hover": {
|
|
||||||
transform: "scale(1.05)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameContainer = styled("div", {
|
|
||||||
base: {
|
|
||||||
padding: "20px 0",
|
|
||||||
width: "100%",
|
|
||||||
minHeight: 72,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
selectors: {
|
|
||||||
"&:not(:last-of-type)": {
|
|
||||||
borderBottom: `1px solid ${theme.color.gray.d400}`
|
|
||||||
},
|
|
||||||
"&:not(:first-of-type)": {
|
|
||||||
borderTop: `1px solid ${theme.color.gray.d400}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameImg = styled("img", {
|
|
||||||
base: {
|
|
||||||
border: "none",
|
|
||||||
outline: "none",
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
height: 80,
|
|
||||||
borderRadius: 8
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameText = styled("div", {
|
|
||||||
base: {
|
|
||||||
paddingRight: "3em",
|
|
||||||
marginLeft: 30,
|
|
||||||
display: "flex",
|
|
||||||
gap: 8,
|
|
||||||
flexDirection: "column",
|
|
||||||
alignSelf: "center",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const SteamGameBtn = styled("button", {
|
|
||||||
base: {
|
|
||||||
minWidth: 48,
|
|
||||||
borderRadius: 9999,
|
|
||||||
textAlign: "center",
|
|
||||||
padding: "0px 24px",
|
|
||||||
fontSize: theme.font.size["base"],
|
|
||||||
lineHeight: "1.75",
|
|
||||||
// marginTop: 20,
|
|
||||||
// marginRight: 1,
|
|
||||||
margin: "0 1px 0 auto",
|
|
||||||
cursor: "pointer",
|
|
||||||
alignSelf: "center",
|
|
||||||
fontWeight: theme.font.weight.bold,
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
color: theme.color.blue.d900,
|
|
||||||
border: `1px solid ${theme.color.gray.d100}`,
|
|
||||||
backgroundColor: theme.color.blue.d300,
|
|
||||||
transition: "all 0.2s cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
":hover": {
|
|
||||||
transform: "scale(1.05)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const PortalContainer = styled("div", {
|
|
||||||
base: {
|
|
||||||
zIndex: 4,
|
|
||||||
isolation: "isolate",
|
|
||||||
position: "fixed",
|
|
||||||
bottom: "20vh",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the home page layout for the gaming platform.
|
|
||||||
*
|
|
||||||
* This component wraps its content within a header and a full-screen container,
|
|
||||||
* currently displaying a QR code component. Commented sections indicate planned
|
|
||||||
* enhancements such as game previews, team mate suggestions, and a Steam library.
|
|
||||||
*/
|
|
||||||
export function HomeRoute() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Header>
|
|
||||||
<FullScreen >
|
|
||||||
{/* <LastPlayedWrapper>
|
|
||||||
<LastPlayedFader />
|
|
||||||
<LogoBackgroundImage />
|
|
||||||
<BackgroundImage />
|
|
||||||
<Material />
|
|
||||||
<JoeColor />
|
|
||||||
<PortalContainer>
|
|
||||||
<Portal />
|
|
||||||
</PortalContainer>
|
|
||||||
</LastPlayedWrapper>
|
|
||||||
*/}
|
|
||||||
<GamesContainer>
|
|
||||||
<GamesWrapper>
|
|
||||||
<GameSquareImage draggable={false} alt="Assasin's Creed Shadows" src="https://assets-prd.ignimgs.com/2024/05/15/acshadows-1715789601294.jpg" />
|
|
||||||
<GameSquareImage draggable={false} alt="Assasin's Creed Shadows" src="https://assets-prd.ignimgs.com/2022/09/22/slime-rancher-2-button-02-1663890048548.jpg" />
|
|
||||||
<GameSquareImage draggable={false} alt="Assasin's Creed Shadows" src="https://assets-prd.ignimgs.com/2023/05/19/cataclismo-button-1684532710313.jpg" />
|
|
||||||
<GameSquareImage draggable={false} alt="Assasin's Creed Shadows" src="https://assets-prd.ignimgs.com/2024/03/27/marvelrivals-1711557092104.jpg" />
|
|
||||||
</GamesWrapper>
|
|
||||||
<FriendsList>
|
|
||||||
<Title>Team Mate Suggestions</Title>
|
|
||||||
<SubTitle>Invite people to join your team and play together</SubTitle>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="Wanjohi Ryan" />
|
|
||||||
<FriendsText>Wanjohi Ryan</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="Tracy Jones" />
|
|
||||||
<FriendsText>Tracy Jones</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="The65th" />
|
|
||||||
<FriendsText>The65th</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="Menstral" />
|
|
||||||
<FriendsText>Menstral</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="AstroHot" />
|
|
||||||
<FriendsText>AstroHot</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
</FriendsList>
|
|
||||||
<SteamLibrary>
|
|
||||||
<Title>Your Steam library</Title>
|
|
||||||
<SubTitle>These titles from your Steam Library are fully functional on Nestri</SubTitle>
|
|
||||||
<div>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2023/05/27/alanwake2-1685200534966.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Alan Wake II</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action, Adventure, Horror</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2022/09/22/slime-rancher-2-button-02-1663890048548.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Slime Rancher 2</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action, Adventure, Casual, Indie</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets1.ignimgs.com/2019/07/17/doom-eternal---button-fin-1563400339680.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Doom Eternal</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2022/10/12/dead-space-2023-button-3-1665603079064.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Dead Space</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action, Adventure</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Update</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2023/01/25/hifirush-1674680068070.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Hi-Fi Rush</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2023/08/24/baldursg3-1692894717196.jpeg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Baldur's Gate 3</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Adventure, RPG, Strategy</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
</div>
|
|
||||||
</SteamLibrary>
|
|
||||||
</GamesContainer>
|
|
||||||
</FullScreen>
|
|
||||||
</Header>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
<GameImageCapsule alt="Assasin's Creed Shadows" src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/2625420/hero_capsule.jpg?t=1742853642" />
|
|
||||||
<GameImageCapsule alt="Assasin's Creed Shadows" src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/2486740/hero_capsule.jpg?t=1742596243" />
|
|
||||||
<GameImageCapsule alt="Assasin's Creed Shadows" src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/870780/hero_capsule.jpg?t=1737800535" />
|
|
||||||
<GameImageCapsule alt="Assasin's Creed Shadows" src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/2050650/hero_capsule.jpg?t=1737800535" />
|
|
||||||
*/
|
|
||||||
|
|
||||||
13
packages/www/src/pages/steam/home.tsx
Normal file
13
packages/www/src/pages/steam/home.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { FullScreen } from "@nestri/www/ui"
|
||||||
|
import { Header } from "@nestri/www/pages/steam/header";
|
||||||
|
|
||||||
|
|
||||||
|
export const HomeRoute = () => {
|
||||||
|
return (
|
||||||
|
<Header>
|
||||||
|
<FullScreen>
|
||||||
|
HOEM
|
||||||
|
</FullScreen>
|
||||||
|
</Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { SteamContext } from "@nestri/www/providers/context";
|
|||||||
import { createEffect, createMemo, Match, Switch } from "solid-js";
|
import { createEffect, createMemo, Match, Switch } from "solid-js";
|
||||||
import { NotAllowed, NotFound } from "@nestri/www/pages/not-found";
|
import { NotAllowed, NotFound } from "@nestri/www/pages/not-found";
|
||||||
import { useAccount, useStorage } from "@nestri/www/providers/account";
|
import { useAccount, useStorage } from "@nestri/www/providers/account";
|
||||||
|
import { HomeRoute } from "./home";
|
||||||
|
|
||||||
export const SteamRoute = (
|
export const SteamRoute = (
|
||||||
<Route
|
<Route
|
||||||
@@ -15,14 +16,14 @@ export const SteamRoute = (
|
|||||||
// const storage = useStorage();
|
// const storage = useStorage();
|
||||||
// const openauth = useOpenAuth();
|
// const openauth = useOpenAuth();
|
||||||
|
|
||||||
// const team = createMemo(() =>
|
// const steam = createMemo(() =>
|
||||||
// account.current.teams.find(
|
// account.current.profiles.find(
|
||||||
// (item) => item.id === params.steamID,
|
// (item) => item.id === params.steamID,
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// createEffect(() => {
|
// createEffect(() => {
|
||||||
// const t = team();
|
// const t = steam();
|
||||||
// if (!t) return;
|
// if (!t) return;
|
||||||
// storage.set("steam", t.id);
|
// storage.set("steam", t.id);
|
||||||
// });
|
// });
|
||||||
@@ -40,22 +41,22 @@ export const SteamRoute = (
|
|||||||
|
|
||||||
// return (
|
// return (
|
||||||
// <Switch>
|
// <Switch>
|
||||||
// <Match when={!team()}>
|
// <Match when={!steam()}>
|
||||||
// {/* TODO: Add a public page for (other) teams */}
|
|
||||||
// <NotAllowed header />
|
// <NotAllowed header />
|
||||||
// </Match>
|
// </Match>
|
||||||
// <Match when={team()}>
|
// <Match when={steam()}>
|
||||||
// <TeamContext.Provider value={() => team()!}>
|
// <SteamContext.Provider value={() => steam()!}>
|
||||||
// <ApiProvider>
|
// <ApiProvider>
|
||||||
// {props.children}
|
// {props.children}
|
||||||
// </ApiProvider>
|
// </ApiProvider>
|
||||||
// </TeamContext.Provider>
|
// </SteamContext.Provider>
|
||||||
// </Match>
|
// </Match>
|
||||||
// </Switch>
|
// </Switch>
|
||||||
// )
|
// )
|
||||||
// }}
|
// }}
|
||||||
>
|
>
|
||||||
<Route path="library" component={LibraryRoute} />
|
<Route path="library" component={LibraryRoute} />
|
||||||
|
<Route path="" component={HomeRoute} />
|
||||||
<Route path="*" component={() => <NotFound header />} />
|
<Route path="*" component={() => <NotFound header />} />
|
||||||
</Route>
|
</Route>
|
||||||
)
|
)
|
||||||
@@ -126,7 +126,7 @@ const Logo = styled("svg", {
|
|||||||
opacity: "70%",
|
opacity: "70%",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//MaRt@6563
|
|
||||||
export function LibraryRoute() {
|
export function LibraryRoute() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
1
packages/www/src/sst-env.d.ts
vendored
1
packages/www/src/sst-env.d.ts
vendored
@@ -4,6 +4,7 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_URL: string
|
readonly VITE_API_URL: string
|
||||||
|
readonly VITE_CDN_URL: string
|
||||||
readonly VITE_STAGE: string
|
readonly VITE_STAGE: string
|
||||||
readonly VITE_AUTH_URL: string
|
readonly VITE_AUTH_URL: string
|
||||||
readonly VITE_ZERO_URL: string
|
readonly VITE_ZERO_URL: string
|
||||||
|
|||||||
43
sst-env.d.ts
vendored
43
sst-env.d.ts
vendored
@@ -7,13 +7,16 @@ import "sst"
|
|||||||
declare module "sst" {
|
declare module "sst" {
|
||||||
export interface Resource {
|
export interface Resource {
|
||||||
"Api": {
|
"Api": {
|
||||||
"service": string
|
"type": "sst.aws.Router"
|
||||||
"type": "sst.aws.Service"
|
"url": string
|
||||||
|
}
|
||||||
|
"ApiFn": {
|
||||||
|
"name": string
|
||||||
|
"type": "sst.aws.Function"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"service": string
|
"type": "sst.aws.Auth"
|
||||||
"type": "sst.aws.Service"
|
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
"Bus": {
|
"Bus": {
|
||||||
@@ -21,6 +24,10 @@ declare module "sst" {
|
|||||||
"name": string
|
"name": string
|
||||||
"type": "sst.aws.Bus"
|
"type": "sst.aws.Bus"
|
||||||
}
|
}
|
||||||
|
"CDNRouter": {
|
||||||
|
"type": "sst.aws.Router"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
"Database": {
|
"Database": {
|
||||||
"clusterArn": string
|
"clusterArn": string
|
||||||
"database": string
|
"database": string
|
||||||
@@ -57,15 +64,6 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"ImageInvokerAccessKey": {
|
|
||||||
"key": string
|
|
||||||
"secret": string
|
|
||||||
"type": "aws.iam/accessKey.AccessKey"
|
|
||||||
}
|
|
||||||
"ImageProcessor": {
|
|
||||||
"name": string
|
|
||||||
"type": "sst.aws.Function"
|
|
||||||
}
|
|
||||||
"NestriFamilyMonthly": {
|
"NestriFamilyMonthly": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -111,6 +109,12 @@ declare module "sst" {
|
|||||||
"name": string
|
"name": string
|
||||||
"type": "sst.aws.Bucket"
|
"type": "sst.aws.Bucket"
|
||||||
}
|
}
|
||||||
|
"Urls": {
|
||||||
|
"api": string
|
||||||
|
"auth": string
|
||||||
|
"site": string
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
}
|
||||||
"VPC": {
|
"VPC": {
|
||||||
"bastion": string
|
"bastion": string
|
||||||
"type": "sst.aws.Vpc"
|
"type": "sst.aws.Vpc"
|
||||||
@@ -124,15 +128,10 @@ declare module "sst" {
|
|||||||
"type": "sst.aws.Service"
|
"type": "sst.aws.Service"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
}
|
"ZeroStorage": {
|
||||||
}
|
"name": string
|
||||||
// cloudflare
|
"type": "sst.aws.Bucket"
|
||||||
import * as cloudflare from "@cloudflare/workers-types";
|
}
|
||||||
declare module "sst" {
|
|
||||||
export interface Resource {
|
|
||||||
"ImageBucket": cloudflare.R2Bucket
|
|
||||||
"ImageCDN": cloudflare.Service
|
|
||||||
"ImageCache": cloudflare.KVNamespace
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1 @@
|
|||||||
{
|
{}
|
||||||
"compilerOptions": {
|
|
||||||
"lib": [
|
|
||||||
"ESNext"
|
|
||||||
],
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"allowJs": true,
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
/* Linting */
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"forceConsistentCasingInFileNames": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user