diff --git a/.env.example b/.env.example index 1390efc..04a1dd8 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/chattz PORT=3000 -APP_BASE_URL=http://localhost:3000 # Authentik OIDC app values OIDC_CLIENT_ID=replace-me @@ -17,9 +16,6 @@ TURN_URLS=turn:turn.example.com:3478?transport=udp,turn:turn.example.com:3478?tr TURN_USERNAME=replace-me TURN_PASSWORD=replace-me -# Cloudflare R2 media uploads -R2_ACCOUNT_ID=replace-me -R2_ACCESS_KEY_ID=replace-me -R2_SECRET_ACCESS_KEY=replace-me -R2_BUCKET=chattz-media -MEDIA_BASE_URL=https://media.example.com +# 32+ random chars; used to sign session cookies +SESSION_SECRET=replace-with-long-random-secret +COOKIE_SECURE=false diff --git a/.forgejo/workflows/pipeline.yaml b/.forgejo/workflows/pipeline.yaml index c76ae86..4adede5 100644 --- a/.forgejo/workflows/pipeline.yaml +++ b/.forgejo/workflows/pipeline.yaml @@ -11,51 +11,6 @@ jobs: with: node-version: 24 - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Update version from tag - run: | - # Extract version from tag (e.g. v1.2.3 -> 1.2.3) - VERSION=${GITHUB_REF_NAME#v} - echo "Bumping version to $VERSION" - npm version $VERSION --no-git-tag-version - - run: npm ci - - run: npm run dist:linux - - name: Build Windows installer with 32-bit Wine prefix - run: | - export WINEARCH=win32 - export WINEPREFIX="$HOME/.wine32" - wineboot -u - npm run dist:win - - name: Copy installers into static folder - run: | - mkdir -p static/installers - find static/installers -mindepth 1 -maxdepth 1 -type f -delete - copy_first() { - pattern="$1" - out="$2" - file="$(find dist -maxdepth 1 -type f -name "$pattern" | head -n 1)" - if [ -n "$file" ]; then - cp "$file" "static/installers/$out" - fi - } - # Copy versioned artifacts for electron-updater - cp dist/*.AppImage static/installers/ || true - cp dist/*.exe static/installers/ || true - cp dist/*.rpm static/installers/ || true - cp dist/*.deb static/installers/ || true - cp dist/*.msi static/installers/ || true - - # Maintain generic names for stable website links - copy_first '*.rpm' 'chattz-linux.rpm' - copy_first '*.deb' 'chattz-linux.deb' - copy_first '*.AppImage' 'chattz-linux.AppImage' - copy_first '*.exe' 'chattz-windows.exe' - copy_first '*.msi' 'chattz-windows.msi' - - # Metadata files for electron-updater - copy_first 'latest-linux.yml' 'latest-linux.yml' - copy_first 'latest.yml' 'latest.yml' - name: Cache Cargo registry uses: actions/cache@v4 with: @@ -82,7 +37,7 @@ jobs: mkdir -p ~/discord cp target/release/chattz ~/discord/discord chmod +x ~/discord/discord - mkdir -p ~/discord/static/uploads - find ~/discord/static -mindepth 1 -maxdepth 1 ! -name 'uploads' -exec rm -rf {} + + mkdir -p ~/discord/static + rm -rf ~/discord/static/* cp -r static/* ~/discord/static/ - systemctl --user start discord + systemctl --user start discord \ No newline at end of file diff --git a/.gitignore b/.gitignore index a2feb2a..0b745e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ /target -.env -node_modules -dist +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c44764d..190bd84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,418 +92,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-config" -version = "1.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http 0.63.5", - "aws-smithy-json 0.62.4", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 1.4.0", - "time", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d203b0bf2626dcba8665f5cd0871d7c2c0930223d6b6be9097592fea21242d0" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-lc-rs" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.37.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "aws-runtime" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede2ddc593e6c8acc6ce3358c28d6677a6dc49b65ba4b37a2befe14a11297e75" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http 0.63.5", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "bytes-utils", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.119.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d65fddc3844f902dfe1864acb8494db5f9342015ee3ab7890270d36fbd2e01c" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http 0.62.6", - "aws-smithy-json 0.61.9", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes", - "fastrand", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "lru", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.99.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9acba7c62f3d4e2408fa998a3a8caacd8b9a5b5549cf36e2372fbdae329d5449" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http 0.63.5", - "aws-smithy-json 0.62.4", - "aws-smithy-observability", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37411f8e0f4bea0c3ca0958ce7f18f6439db24d555dbd809787262cd00926aa9" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http 0.63.5", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "percent-encoding", - "sha2", - "time", - "tracing", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc50d0f63e714784b84223abd7abbc8577de8c35d699e0edd19f0a88a08ae13" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.63.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae" -dependencies = [ - "aws-smithy-http 0.62.6", - "aws-smithy-types", - "bytes", - "crc-fast", - "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0b3e587fbaa5d7f7e870544508af8ce82ea47cd30376e69e1e37c4ac746f79" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.62.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http" -version = "0.63.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d619373d490ad70966994801bc126846afaa0d1ee920697a031f0cf63f2568e7" -dependencies = [ - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00ccbb08c10f6bcf912f398188e42ee2eab5f1767ce215a02a73bc5df1bbdd95" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.13", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.8.1", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", - "hyper-util", - "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.36", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.4", - "tower", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-json" -version = "0.62.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-observability" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ccf7f6eba8b2dcf8ce9b74806c6c185659c311665c4bf8d6e71ebd454db6bf" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http 0.63.5", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af6e5def28be846479bbeac55aa4603d6f7986fc5da4601ba324dd5d377516" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.4.0", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca2734c16913a45343b37313605d84e7d8b34a4611598ce1d25b35860a2bed3" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0470cc047657c6e286346bdf10a8719d26efd6a91626992e0e64481e44323e96" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - [[package]] name = "axum" version = "0.8.8" @@ -516,10 +104,10 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "itoa", "matchit", @@ -550,8 +138,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", @@ -578,16 +166,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "base64ct" version = "1.8.3" @@ -630,16 +208,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - [[package]] name = "cc" version = "1.2.56" @@ -647,8 +215,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -669,20 +235,17 @@ name = "chattz" version = "0.1.0" dependencies = [ "anyhow", - "aws-config", - "aws-sdk-s3", "axum", "chrono", "dotenvy", "futures-util", + "jsonwebtoken", "reqwest", "sea-orm", "sea-orm-migration", "serde", "serde_json", - "sha2", "tokio", - "tower", "tower-http", "tracing", "tracing-subscriber", @@ -704,15 +267,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -728,16 +282,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -768,28 +312,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crc-fast" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" -dependencies = [ - "crc", - "digest", - "rand 0.9.2", - "regex", - "rustversion", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -926,12 +448,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.15.0" @@ -978,12 +494,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1022,12 +532,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.31" @@ -1169,44 +673,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -1278,17 +744,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.0" @@ -1299,17 +754,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1317,7 +761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -1328,8 +772,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1351,30 +795,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.8.1" @@ -1385,9 +805,8 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1398,35 +817,19 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", - "hyper 1.8.1", + "http", + "hyper", "hyper-util", - "rustls 0.23.36", - "rustls-native-certs", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower-service", "webpki-roots 1.0.6", ] @@ -1441,14 +844,14 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "hyper 1.8.1", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2", "tokio", "tower-service", "tracing", @@ -1629,16 +1032,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.85" @@ -1649,6 +1042,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1712,15 +1120,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -1794,7 +1193,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.4.0", + "http", "httparse", "memchr", "mime", @@ -1811,6 +1210,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1869,12 +1278,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - [[package]] name = "ordered-float" version = "4.6.0" @@ -1908,12 +1311,6 @@ dependencies = [ "syn", ] -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - [[package]] name = "parking" version = "2.2.1" @@ -1943,6 +1340,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2077,8 +1484,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", - "socket2 0.6.2", + "rustls", + "socket2", "thiserror", "tokio", "tracing", @@ -2097,7 +1504,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "slab", "thiserror", @@ -2115,7 +1522,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -2235,12 +1642,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "regex-syntax" version = "0.8.9" @@ -2256,25 +1657,25 @@ dependencies = [ "base64", "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -2334,45 +1735,20 @@ dependencies = [ "semver", ] -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ - "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -2383,23 +1759,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2417,31 +1782,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sea-bae" version = "0.2.1" @@ -2589,29 +1935,6 @@ dependencies = [ "syn", ] -[[package]] -name = "security-framework" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.27" @@ -2731,6 +2054,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.12" @@ -2746,16 +2081,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.2" @@ -2822,7 +2147,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls 0.23.36", + "rustls", "serde", "serde_json", "sha2", @@ -3085,6 +2410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde_core", @@ -3143,7 +2469,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2 0.6.2", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3159,23 +2485,13 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls", "tokio", ] @@ -3241,8 +2557,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "http-range-header", "httpdate", @@ -3347,7 +2663,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http", "httparse", "log", "rand 0.9.2", @@ -3467,12 +2783,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - [[package]] name = "want" version = "0.3.1" @@ -3903,12 +3213,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 6dbd768..5ab0c14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,20 +5,17 @@ edition = "2024" [dependencies] anyhow = "1" -aws-config = { version = "1", default-features = false, features = ["behavior-version-latest", "rt-tokio", "rustls"] } -aws-sdk-s3 = { version = "1", default-features = false, features = ["rt-tokio", "rustls"] } axum = { version = "0.8", features = ["macros", "ws", "multipart"] } chrono = { version = "0.4", features = ["serde"] } dotenvy = "0.15" +jsonwebtoken = "9" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -sea-orm = { version = "1.1", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid", "mock"] } +sea-orm = { version = "1.1", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid"] } sea-orm-migration = { version = "1.1", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -sha2 = "0.10" futures-util = "0.3" -tokio = { version = "1", features = ["fs", "io-util", "macros", "rt-multi-thread"] } -tower = { version = "0.5", features = ["util"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tower-http = { version = "0.6", features = ["trace", "fs"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } diff --git a/README.md b/README.md index 2708a60..af5e8f0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A simple single-instance Discord-style monolith in Rust using: ## What this includes - OIDC login flow (`/auth/login`, `/auth/callback`, `/auth/logout`) -- HttpOnly session cookie auth +- Signed session cookie auth - Channel voice chat over WebRTC (P2P mesh) with server WebSocket signaling - Guild invite codes (create + join) - Direct messages (DM) between users @@ -41,11 +41,6 @@ For voice reliability on restrictive networks, configure TURN in `.env`: - `TURN_USERNAME` - `TURN_PASSWORD` -For production deployments: -- `APP_BASE_URL` must be your public app origin and should use `https` -- `MEDIA_BASE_URL` should be a separate media origin for user uploads -- uploads and soundboard require R2/object storage to be configured - 3. Run app: ```bash @@ -60,7 +55,7 @@ Web UI is available at `http://localhost:${PORT}/`. ## Authentik setup notes Create an Authentik OAuth2/OIDC provider + application and set: -- Redirect URI: `${APP_BASE_URL}/auth/callback` +- Redirect URI: `http://localhost:3000/auth/callback` - Scopes including at least: `openid profile email` If you change `PORT`, update `OIDC_REDIRECT_URL` and this redirect URI to match. @@ -96,7 +91,6 @@ For Authentik these are commonly under `/application/o/...` for the app slug. - `GET /channels/:channel_id/voice/ws` (WebSocket signaling) All endpoints except health and auth flow require the session cookie from successful login. -Authenticated WebSocket connections (`/ws`, `/channels/:channel_id/voice/ws`) also use the same cookie session. ## Notes @@ -104,7 +98,6 @@ This is intentionally minimal and monolithic (single process, single Postgres in Voice is implemented as browser-to-browser WebRTC audio with signaling in this server. For two users behind strict NAT/firewall, you may need TURN for reliable connectivity. The web UI remembers the last selected guild in browser local storage and auto-selects it on reload. -User uploads are served from the configured media origin, not from `/static`. Mic filter modes in the UI: - `NSNet2 (Compat)`: always-on denoising mode (implemented using DeepFilterNet3 with lighter suppression preset) diff --git a/build/icon.png b/build/icon.png deleted file mode 100644 index 8169e48..0000000 Binary files a/build/icon.png and /dev/null differ diff --git a/desktop/app.js b/desktop/app.js deleted file mode 100644 index e0b9fa7..0000000 --- a/desktop/app.js +++ /dev/null @@ -1 +0,0 @@ -import "../static/shared/app-core.js"; diff --git a/desktop/index.html b/desktop/index.html deleted file mode 100644 index 1684077..0000000 --- a/desktop/index.html +++ /dev/null @@ -1,282 +0,0 @@ - - - - - - - - Chattz - - - - - - -
-
-
C
-

Chattz

-

Sign in to open your servers.

- -

-
-
- - - - - - - - - - - - - - - - - - - diff --git a/desktop/preload.js b/desktop/preload.js deleted file mode 100644 index f43670e..0000000 --- a/desktop/preload.js +++ /dev/null @@ -1,20 +0,0 @@ -const { contextBridge, ipcRenderer } = require('electron'); - -function onIpc(channel, callback) { - const listener = (_event, payload) => callback(payload); - ipcRenderer.on(channel, listener); - return () => ipcRenderer.removeListener(channel, listener); -} - -contextBridge.exposeInMainWorld('electronAPI', { - copyToClipboard: (text) => ipcRenderer.invoke('clipboard-write', text), - getUpdateState: () => ipcRenderer.invoke('get-update-state'), - checkForUpdatesNow: () => ipcRenderer.invoke('check-for-updates-now'), - checkForUpdates: () => ipcRenderer.send('check-for-updates'), - downloadUpdate: () => ipcRenderer.send('download-update'), - quitAndInstall: () => ipcRenderer.send('quit-and-install'), - onUpdateState: (callback) => onIpc('update-state', callback), - onUpdateAvailable: (callback) => onIpc('update-available', callback), - onUpdateDownloaded: (callback) => onIpc('update-downloaded', callback), - onUpdateError: (callback) => onIpc('update-error', callback) -}); diff --git a/desktop/vendor/deepfilternet3-noise-filter.esm.js b/desktop/vendor/deepfilternet3-noise-filter.esm.js deleted file mode 100644 index 7ad3ee9..0000000 --- a/desktop/vendor/deepfilternet3-noise-filter.esm.js +++ /dev/null @@ -1,249 +0,0 @@ -class AssetLoader { - constructor(config = {}) { - this.cdnUrl = config.cdnUrl ?? 'https://cdn.mezon.ai/AI/models/datas/noise_suppression/deepfilternet3'; - } - getCdnUrl(relativePath) { - return `${this.cdnUrl}/${relativePath}`; - } - getAssetUrls() { - return { - wasm: this.getCdnUrl('v2/pkg/df_bg.wasm'), - model: this.getCdnUrl('v2/models/DeepFilterNet3_onnx.tar.gz') - }; - } - async fetchAsset(url) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch asset: ${response.statusText}`); - } - return response.arrayBuffer(); - } -} -let defaultLoader = null; -function getAssetLoader(config) { - if (!defaultLoader || config) { - defaultLoader = new AssetLoader(config); - } - return defaultLoader; -} - -/** - * Creates a worklet module URL from inline code string - * This approach works with all bundlers without special configuration - */ -async function createWorkletModule(audioContext, workletCode) { - const blob = new Blob([workletCode], { type: 'application/javascript' }); - const blobUrl = URL.createObjectURL(blob); - await audioContext.audioWorklet.addModule(blobUrl); -} - -const WorkletMessageTypes = { - SET_SUPPRESSION_LEVEL: 'SET_SUPPRESSION_LEVEL', - SET_BYPASS: 'SET_BYPASS' -}; - -var workletCode = "(function () {\n 'use strict';\n\n let wasm;\n\n const heap = new Array(128).fill(undefined);\n\n heap.push(undefined, null, true, false);\n\n function getObject(idx) { return heap[idx]; }\n\n let heap_next = heap.length;\n\n function dropObject(idx) {\n if (idx < 132) return;\n heap[idx] = heap_next;\n heap_next = idx;\n }\n\n function takeObject(idx) {\n const ret = getObject(idx);\n dropObject(idx);\n return ret;\n }\n\n const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );\n\n if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }\n let cachedUint8Memory0 = null;\n\n function getUint8Memory0() {\n if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {\n cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);\n }\n return cachedUint8Memory0;\n }\n\n function getStringFromWasm0(ptr, len) {\n ptr = ptr >>> 0;\n return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));\n }\n\n function addHeapObject(obj) {\n if (heap_next === heap.length) heap.push(heap.length + 1);\n const idx = heap_next;\n heap_next = heap[idx];\n\n heap[idx] = obj;\n return idx;\n }\n /**\n * Set DeepFilterNet attenuation limit.\n *\n * Args:\n * - lim_db: New attenuation limit in dB.\n * @param {number} st\n * @param {number} lim_db\n */\n function df_set_atten_lim(st, lim_db) {\n wasm.df_set_atten_lim(st, lim_db);\n }\n\n /**\n * Get DeepFilterNet frame size in samples.\n * @param {number} st\n * @returns {number}\n */\n function df_get_frame_length(st) {\n const ret = wasm.df_get_frame_length(st);\n return ret >>> 0;\n }\n\n let WASM_VECTOR_LEN = 0;\n\n function passArray8ToWasm0(arg, malloc) {\n const ptr = malloc(arg.length * 1, 1) >>> 0;\n getUint8Memory0().set(arg, ptr / 1);\n WASM_VECTOR_LEN = arg.length;\n return ptr;\n }\n /**\n * Create a DeepFilterNet Model\n *\n * Args:\n * - path: File path to a DeepFilterNet tar.gz onnx model\n * - atten_lim: Attenuation limit in dB.\n *\n * Returns:\n * - DF state doing the full processing: stft, DNN noise reduction, istft.\n * @param {Uint8Array} model_bytes\n * @param {number} atten_lim\n * @returns {number}\n */\n function df_create(model_bytes, atten_lim) {\n const ptr0 = passArray8ToWasm0(model_bytes, wasm.__wbindgen_malloc);\n const len0 = WASM_VECTOR_LEN;\n const ret = wasm.df_create(ptr0, len0, atten_lim);\n return ret >>> 0;\n }\n\n let cachedFloat32Memory0 = null;\n\n function getFloat32Memory0() {\n if (cachedFloat32Memory0 === null || cachedFloat32Memory0.byteLength === 0) {\n cachedFloat32Memory0 = new Float32Array(wasm.memory.buffer);\n }\n return cachedFloat32Memory0;\n }\n\n function passArrayF32ToWasm0(arg, malloc) {\n const ptr = malloc(arg.length * 4, 4) >>> 0;\n getFloat32Memory0().set(arg, ptr / 4);\n WASM_VECTOR_LEN = arg.length;\n return ptr;\n }\n /**\n * Processes a chunk of samples.\n *\n * Args:\n * - df_state: Created via df_create()\n * - input: Input buffer of length df_get_frame_length()\n * - output: Output buffer of length df_get_frame_length()\n *\n * Returns:\n * - Local SNR of the current frame.\n * @param {number} st\n * @param {Float32Array} input\n * @returns {Float32Array}\n */\n function df_process_frame(st, input) {\n const ptr0 = passArrayF32ToWasm0(input, wasm.__wbindgen_malloc);\n const len0 = WASM_VECTOR_LEN;\n const ret = wasm.df_process_frame(st, ptr0, len0);\n return takeObject(ret);\n }\n\n function handleError(f, args) {\n try {\n return f.apply(this, args);\n } catch (e) {\n wasm.__wbindgen_exn_store(addHeapObject(e));\n }\n }\n\n (typeof FinalizationRegistry === 'undefined')\n ? { }\n : new FinalizationRegistry(ptr => wasm.__wbg_dfstate_free(ptr >>> 0));\n\n function __wbg_get_imports() {\n const imports = {};\n imports.wbg = {};\n imports.wbg.__wbindgen_object_drop_ref = function(arg0) {\n takeObject(arg0);\n };\n imports.wbg.__wbg_crypto_566d7465cdbb6b7a = function(arg0) {\n const ret = getObject(arg0).crypto;\n return addHeapObject(ret);\n };\n imports.wbg.__wbindgen_is_object = function(arg0) {\n const val = getObject(arg0);\n const ret = typeof(val) === 'object' && val !== null;\n return ret;\n };\n imports.wbg.__wbg_process_dc09a8c7d59982f6 = function(arg0) {\n const ret = getObject(arg0).process;\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_versions_d98c6400c6ca2bd8 = function(arg0) {\n const ret = getObject(arg0).versions;\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_node_caaf83d002149bd5 = function(arg0) {\n const ret = getObject(arg0).node;\n return addHeapObject(ret);\n };\n imports.wbg.__wbindgen_is_string = function(arg0) {\n const ret = typeof(getObject(arg0)) === 'string';\n return ret;\n };\n imports.wbg.__wbg_require_94a9da52636aacbf = function() { return handleError(function () {\n const ret = module.require;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbindgen_is_function = function(arg0) {\n const ret = typeof(getObject(arg0)) === 'function';\n return ret;\n };\n imports.wbg.__wbindgen_string_new = function(arg0, arg1) {\n const ret = getStringFromWasm0(arg0, arg1);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_msCrypto_0b84745e9245cdf6 = function(arg0) {\n const ret = getObject(arg0).msCrypto;\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_randomFillSync_290977693942bf03 = function() { return handleError(function (arg0, arg1) {\n getObject(arg0).randomFillSync(takeObject(arg1));\n }, arguments) };\n imports.wbg.__wbg_getRandomValues_260cc23a41afad9a = function() { return handleError(function (arg0, arg1) {\n getObject(arg0).getRandomValues(getObject(arg1));\n }, arguments) };\n imports.wbg.__wbg_newnoargs_e258087cd0daa0ea = function(arg0, arg1) {\n const ret = new Function(getStringFromWasm0(arg0, arg1));\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_new_63b92bc8671ed464 = function(arg0) {\n const ret = new Uint8Array(getObject(arg0));\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_new_9efabd6b6d2ce46d = function(arg0) {\n const ret = new Float32Array(getObject(arg0));\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_buffer_12d079cc21e14bdb = function(arg0) {\n const ret = getObject(arg0).buffer;\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_newwithbyteoffsetandlength_aa4a17c33a06e5cb = function(arg0, arg1, arg2) {\n const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_newwithlength_e9b4878cebadb3d3 = function(arg0) {\n const ret = new Uint8Array(arg0 >>> 0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_set_a47bac70306a19a7 = function(arg0, arg1, arg2) {\n getObject(arg0).set(getObject(arg1), arg2 >>> 0);\n };\n imports.wbg.__wbg_subarray_a1f73cd4b5b42fe1 = function(arg0, arg1, arg2) {\n const ret = getObject(arg0).subarray(arg1 >>> 0, arg2 >>> 0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_newwithbyteoffsetandlength_4a659d079a1650e0 = function(arg0, arg1, arg2) {\n const ret = new Float32Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_self_ce0dbfc45cf2f5be = function() { return handleError(function () {\n const ret = self.self;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbg_window_c6fb939a7f436783 = function() { return handleError(function () {\n const ret = window.window;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbg_globalThis_d1e6af4856ba331b = function() { return handleError(function () {\n const ret = globalThis.globalThis;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbg_global_207b558942527489 = function() { return handleError(function () {\n const ret = global.global;\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbindgen_is_undefined = function(arg0) {\n const ret = getObject(arg0) === undefined;\n return ret;\n };\n imports.wbg.__wbg_call_27c0f87801dedf93 = function() { return handleError(function (arg0, arg1) {\n const ret = getObject(arg0).call(getObject(arg1));\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbindgen_object_clone_ref = function(arg0) {\n const ret = getObject(arg0);\n return addHeapObject(ret);\n };\n imports.wbg.__wbg_call_b3ca7c6051f9bec1 = function() { return handleError(function (arg0, arg1, arg2) {\n const ret = getObject(arg0).call(getObject(arg1), getObject(arg2));\n return addHeapObject(ret);\n }, arguments) };\n imports.wbg.__wbindgen_memory = function() {\n const ret = wasm.memory;\n return addHeapObject(ret);\n };\n imports.wbg.__wbindgen_throw = function(arg0, arg1) {\n throw new Error(getStringFromWasm0(arg0, arg1));\n };\n\n return imports;\n }\n\n function __wbg_finalize_init(instance, module) {\n wasm = instance.exports;\n cachedFloat32Memory0 = null;\n cachedUint8Memory0 = null;\n\n\n return wasm;\n }\n\n function initSync(module) {\n if (wasm !== undefined) return wasm;\n\n const imports = __wbg_get_imports();\n\n if (!(module instanceof WebAssembly.Module)) {\n module = new WebAssembly.Module(module);\n }\n\n const instance = new WebAssembly.Instance(module, imports);\n\n return __wbg_finalize_init(instance);\n }\n\n const WorkletMessageTypes = {\n SET_SUPPRESSION_LEVEL: 'SET_SUPPRESSION_LEVEL',\n SET_BYPASS: 'SET_BYPASS'\n };\n\n class DeepFilterAudioProcessor extends AudioWorkletProcessor {\n constructor(options) {\n super();\n this.dfModel = null;\n this.inputWritePos = 0;\n this.inputReadPos = 0;\n this.outputWritePos = 0;\n this.outputReadPos = 0;\n this.bypass = false;\n this.isInitialized = false;\n this.tempFrame = null;\n this.bufferSize = 8192;\n this.inputBuffer = new Float32Array(this.bufferSize);\n this.outputBuffer = new Float32Array(this.bufferSize);\n try {\n // Initialize WASM from pre-compiled module\n initSync(options.processorOptions.wasmModule);\n const modelBytes = new Uint8Array(options.processorOptions.modelBytes);\n const handle = df_create(modelBytes, options.processorOptions.suppressionLevel ?? 50);\n const frameLength = df_get_frame_length(handle);\n this.dfModel = { handle, frameLength };\n this.bufferSize = frameLength * 4;\n this.inputBuffer = new Float32Array(this.bufferSize);\n this.outputBuffer = new Float32Array(this.bufferSize);\n // Pre-allocate temp frame buffer for processing\n this.tempFrame = new Float32Array(frameLength);\n this.isInitialized = true;\n this.port.onmessage = (event) => {\n this.handleMessage(event.data);\n };\n }\n catch (error) {\n console.error('Failed to initialize DeepFilter in AudioWorklet:', error);\n this.isInitialized = false;\n }\n }\n handleMessage(data) {\n switch (data.type) {\n case WorkletMessageTypes.SET_SUPPRESSION_LEVEL:\n if (this.dfModel && typeof data.value === 'number') {\n const level = Math.max(0, Math.min(100, Math.floor(data.value)));\n df_set_atten_lim(this.dfModel.handle, level);\n }\n break;\n case WorkletMessageTypes.SET_BYPASS:\n this.bypass = Boolean(data.value);\n break;\n }\n }\n getInputAvailable() {\n return (this.inputWritePos - this.inputReadPos + this.bufferSize) % this.bufferSize;\n }\n getOutputAvailable() {\n return (this.outputWritePos - this.outputReadPos + this.bufferSize) % this.bufferSize;\n }\n process(inputList, outputList) {\n const sourceLimit = Math.min(inputList.length, outputList.length);\n const input = inputList[0]?.[0];\n if (!input) {\n return true;\n }\n // Passthrough mode - copy input to all output channels\n if (!this.isInitialized || !this.dfModel || this.bypass || !this.tempFrame) {\n for (let inputNum = 0; inputNum < sourceLimit; inputNum++) {\n const output = outputList[inputNum];\n const channelCount = output.length;\n for (let channelNum = 0; channelNum < channelCount; channelNum++) {\n output[channelNum].set(input);\n }\n }\n return true;\n }\n // Write input to ring buffer\n for (let i = 0; i < input.length; i++) {\n this.inputBuffer[this.inputWritePos] = input[i];\n this.inputWritePos = (this.inputWritePos + 1) % this.bufferSize;\n }\n const frameLength = this.dfModel.frameLength;\n while (this.getInputAvailable() >= frameLength) {\n // Extract frame from ring buffer\n for (let i = 0; i < frameLength; i++) {\n this.tempFrame[i] = this.inputBuffer[this.inputReadPos];\n this.inputReadPos = (this.inputReadPos + 1) % this.bufferSize;\n }\n const processed = df_process_frame(this.dfModel.handle, this.tempFrame);\n // Write to output ring buffer\n for (let i = 0; i < processed.length; i++) {\n this.outputBuffer[this.outputWritePos] = processed[i];\n this.outputWritePos = (this.outputWritePos + 1) % this.bufferSize;\n }\n }\n const outputAvailable = this.getOutputAvailable();\n if (outputAvailable >= 128) {\n for (let inputNum = 0; inputNum < sourceLimit; inputNum++) {\n const output = outputList[inputNum];\n const channelCount = output.length;\n for (let channelNum = 0; channelNum < channelCount; channelNum++) {\n const outputChannel = output[channelNum];\n let readPos = this.outputReadPos;\n for (let i = 0; i < 128; i++) {\n outputChannel[i] = this.outputBuffer[readPos];\n readPos = (readPos + 1) % this.bufferSize;\n }\n }\n }\n this.outputReadPos = (this.outputReadPos + 128) % this.bufferSize;\n }\n return true;\n }\n }\n registerProcessor('deepfilter-audio-processor', DeepFilterAudioProcessor);\n\n})();\n"; - -class DeepFilterNet3Core { - constructor(config = {}) { - this.assets = null; - this.workletNode = null; - this.isInitialized = false; - this.bypassEnabled = false; - this.config = { - sampleRate: config.sampleRate ?? 48000, - noiseReductionLevel: config.noiseReductionLevel ?? 50, - assetConfig: config.assetConfig - }; - this.assetLoader = getAssetLoader(config.assetConfig); - } - async initialize() { - if (this.isInitialized) - return; - // Fetch and compile WASM on main thread - const assetUrls = this.assetLoader.getAssetUrls(); - const [wasmBytes, modelBytes] = await Promise.all([ - this.assetLoader.fetchAsset(assetUrls.wasm), - this.assetLoader.fetchAsset(assetUrls.model) - ]); - // Compile WASM module - const wasmModule = await WebAssembly.compile(wasmBytes); - this.assets = { wasmModule, modelBytes }; - this.isInitialized = true; - } - async createAudioWorkletNode(audioContext) { - this.ensureInitialized(); - if (!this.assets) { - throw new Error('Assets not loaded'); - } - await createWorkletModule(audioContext, workletCode); - this.workletNode = new AudioWorkletNode(audioContext, 'deepfilter-audio-processor', { - processorOptions: { - wasmModule: this.assets.wasmModule, - modelBytes: this.assets.modelBytes, - suppressionLevel: this.config.noiseReductionLevel - } - }); - return this.workletNode; - } - setSuppressionLevel(level) { - if (!this.workletNode || typeof level !== 'number' || isNaN(level)) - return; - const clampedLevel = Math.max(0, Math.min(100, Math.floor(level))); - this.workletNode.port.postMessage({ - type: WorkletMessageTypes.SET_SUPPRESSION_LEVEL, - value: clampedLevel - }); - } - destroy() { - if (!this.isInitialized) - return; - if (this.workletNode) { - this.workletNode.disconnect(); - this.workletNode = null; - } - this.assets = null; - this.isInitialized = false; - } - isReady() { - return this.isInitialized && this.workletNode !== null; - } - setNoiseSuppressionEnabled(enabled) { - if (!this.workletNode) - return; - this.bypassEnabled = !enabled; - this.workletNode.port.postMessage({ - type: WorkletMessageTypes.SET_BYPASS, - value: !enabled - }); - } - isNoiseSuppressionEnabled() { - return !this.bypassEnabled; - } - ensureInitialized() { - if (!this.isInitialized) { - throw new Error('Processor not initialized. Call initialize() first.'); - } - } -} - -class DeepFilterNoiseFilterProcessor { - constructor(options = {}) { - this.name = 'deepfilternet3-noise-filter'; - this.audioContext = null; - this.sourceNode = null; - this.workletNode = null; - this.destination = null; - this.enabled = true; - this.init = async (opts) => { - const track = opts.track ?? opts.mediaStreamTrack; - if (!track) { - throw new Error('DeepFilterNoiseFilterProcessor.init: missing MediaStreamTrack'); - } - this.originalTrack = track; - await this.ensureGraph(); - }; - this.restart = async (opts) => { - const track = opts.track ?? opts.mediaStreamTrack; - if (track) { - this.originalTrack = track; - } - await this.ensureGraph(); - }; - this.setEnabled = async (enable) => { - this.enabled = enable; - this.processor.setNoiseSuppressionEnabled(enable); - return this.enabled; - }; - this.suspend = async () => { - if (this.audioContext && this.audioContext.state === 'running') { - await this.audioContext.suspend(); - } - }; - this.resume = async () => { - if (this.audioContext && this.audioContext.state === 'suspended') { - await this.audioContext.resume(); - } - }; - this.destroy = async () => { - await this.teardownGraph(); - this.processor.destroy(); - }; - const cfg = { - sampleRate: options.sampleRate ?? 48000, - noiseReductionLevel: options.noiseReductionLevel ?? 80, - assetConfig: options.assetConfig - }; - this.enabled = options.enabled ?? true; - this.processor = new DeepFilterNet3Core(cfg); - } - static isSupported() { - return typeof AudioContext !== 'undefined' && typeof WebAssembly !== 'undefined'; - } - setSuppressionLevel(level) { - this.processor.setSuppressionLevel(level); - } - isEnabled() { - return this.enabled; - } - isNoiseSuppressionEnabled() { - return this.processor.isNoiseSuppressionEnabled(); - } - async ensureGraph() { - if (!this.originalTrack) { - throw new Error('No source track'); - } - this.audioContext ?? (this.audioContext = new AudioContext({ sampleRate: 48000 })); - if (this.audioContext.state !== 'running') { - try { - await this.audioContext.resume(); - } - catch { - // Ignore resume errors - } - } - await this.processor.initialize(); - if (!this.workletNode) { - const node = await this.processor.createAudioWorkletNode(this.audioContext); - this.workletNode = node; - } - if (!this.destination) { - this.destination = this.audioContext.createMediaStreamDestination(); - this.processedTrack = this.destination.stream.getAudioTracks()[0]; - } - if (this.sourceNode) { - this.sourceNode.disconnect(); - } - this.sourceNode = this.audioContext.createMediaStreamSource(new MediaStream([this.originalTrack])); - this.sourceNode.connect(this.workletNode).connect(this.destination); - await this.setEnabled(this.enabled); - } - async teardownGraph() { - try { - if (this.workletNode) { - this.workletNode.disconnect(); - this.workletNode = null; - } - if (this.sourceNode) { - this.sourceNode.disconnect(); - this.sourceNode = null; - } - if (this.destination) { - this.destination.disconnect(); - this.destination = null; - } - if (this.audioContext) { - void this.audioContext.close(); - this.audioContext = null; - } - } - catch { - // Ignore disconnect errors - } - } -} -function DeepFilterNoiseFilter(options) { - return new DeepFilterNoiseFilterProcessor(options); -} - -export { AssetLoader, DeepFilterNet3Core, DeepFilterNoiseFilter, DeepFilterNoiseFilterProcessor, getAssetLoader }; diff --git a/desktop/vendor/deepfilternet3/v2/models/DeepFilterNet3_onnx.tar.gz b/desktop/vendor/deepfilternet3/v2/models/DeepFilterNet3_onnx.tar.gz deleted file mode 100644 index 1c4f4ff..0000000 Binary files a/desktop/vendor/deepfilternet3/v2/models/DeepFilterNet3_onnx.tar.gz and /dev/null differ diff --git a/desktop/vendor/deepfilternet3/v2/pkg/df_bg.wasm b/desktop/vendor/deepfilternet3/v2/pkg/df_bg.wasm deleted file mode 100644 index 0d02360..0000000 Binary files a/desktop/vendor/deepfilternet3/v2/pkg/df_bg.wasm and /dev/null differ diff --git a/main.js b/main.js deleted file mode 100644 index 030c413..0000000 --- a/main.js +++ /dev/null @@ -1,313 +0,0 @@ -const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron'); -const { autoUpdater } = require('electron-updater'); -const path = require('path'); -const packageJson = require('./package.json'); -const PERIODIC_UPDATE_CHECK_INTERVAL_MS = 1 * 60 * 1000; - -const updateState = { - status: 'idle', // idle | checking | available | downloading | downloaded | installing | not-available | error - info: null, - error: null, -}; - -function broadcastUpdateState() { - const wins = BrowserWindow.getAllWindows(); - for (const win of wins) { - if (!win.isDestroyed()) { - win.webContents.send('update-state', updateState); - } - } -} - -function isNewerVersionAvailable(info) { - const next = info && typeof info.version === 'string' ? info.version : ''; - return Boolean(next) && next !== app.getVersion(); -} - -let updateCheckInProgress = false; -let installInProgress = false; -let periodicUpdateTimer = null; - -function resolveBackendUrl() { - const configuredUrl = (process.env.CHATTZ_URL || process.env.APP_BASE_URL || '').trim(); - if (configuredUrl) { - return configuredUrl.replace(/\/$/, ''); - } - - const packagedFallback = typeof packageJson.homepage === 'string' - ? packageJson.homepage.trim() - : ''; - if (app.isPackaged && packagedFallback) { - return packagedFallback.replace(/\/$/, ''); - } - - return 'http://localhost:3000'; -} - -function webContentsOrigin(webContents) { - if (!webContents || typeof webContents.getURL !== 'function') { - return null; - } - return safeOrigin(webContents.getURL()); -} - -async function runUpdateCheck(reason = 'manual') { - if (installInProgress) { - console.log(`Update check skipped (${reason}): install already in progress`); - return; - } - if (updateCheckInProgress) { - console.log(`Update check skipped (${reason}): another check is already in progress`); - return; - } - updateCheckInProgress = true; - updateState.status = 'checking'; - updateState.error = null; - try { - const result = await autoUpdater.checkForUpdates(); - const info = result && result.updateInfo ? result.updateInfo : null; - if (isNewerVersionAvailable(info)) { - updateState.status = 'available'; - updateState.info = info; - updateState.error = null; - } else { - updateState.status = 'not-available'; - updateState.info = info; - updateState.error = null; - } - } catch (err) { - updateState.status = 'error'; - updateState.error = err && err.message ? err.message : String(err); - console.error(`Update check failed (${reason})`, err); - } finally { - updateCheckInProgress = false; - broadcastUpdateState(); - } -} - -function installDownloadedUpdate() { - if (installInProgress) { - console.log('Update install already in progress'); - return; - } - - installInProgress = true; - updateState.status = 'installing'; - updateState.error = null; - broadcastUpdateState(); - - const wins = BrowserWindow.getAllWindows(); - for (const win of wins) { - if (win.isDestroyed()) continue; - try { - win.removeAllListeners('close'); - win.destroy(); - } catch (err) { - console.warn('Failed to destroy window before update install', err); - } - } - - setImmediate(() => { - try { - autoUpdater.quitAndInstall(false, true); - } catch (err) { - installInProgress = false; - updateState.status = 'error'; - updateState.error = err && err.message ? err.message : String(err); - broadcastUpdateState(); - console.error('quitAndInstall failed', err); - } - }); - - setTimeout(() => { - if (installInProgress) { - console.warn('Update install is still waiting for app shutdown'); - } - }, 15000); -} - -function startPeriodicUpdateChecks() { - if (periodicUpdateTimer || !app.isPackaged) { - return; - } - - periodicUpdateTimer = setInterval(() => { - void runUpdateCheck('periodic'); - }, PERIODIC_UPDATE_CHECK_INTERVAL_MS); -} - -function createWindow() { - const sess = session.fromPartition('persist:chattz'); - const backendUrl = resolveBackendUrl(); - const backendOrigin = new URL(backendUrl).origin; - console.log(`Desktop backend URL: ${backendUrl}`); - - const win = new BrowserWindow({ - width: 1200, - height: 800, - title: "Chattz Desktop", - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - session: sess, - preload: path.join(__dirname, 'desktop', 'preload.js') - } - }); - - win.setAutoHideMenuBar(true); - win.setMenuBarVisibility(true); - - sess.setPermissionCheckHandler((webContents, permission) => { - const origin = webContentsOrigin(webContents); - if (origin !== backendOrigin) return false; - return permission === 'media' || permission === 'clipboard-write'; - }); - - sess.setPermissionRequestHandler((webContents, permission, callback) => { - const origin = webContentsOrigin(webContents); - if (origin === backendOrigin && (permission === 'media' || permission === 'clipboard-write')) { - callback(true); - } else { - callback(false); - } - }); - - sess.setDisplayMediaRequestHandler((request, callback) => { - const origin = safeOrigin(request.frame?.url || win.webContents.getURL()); - if (origin !== backendOrigin) { - callback(null); - return; - } - desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => { - if (sources && sources.length > 0) { - const screenSource = sources.find(s => s.id.startsWith('screen')) || sources[0]; - callback({ video: screenSource, audio: 'loopback' }); - } else { - callback(null); - } - }).catch(err => { - console.error("Failed to get desktop sources for screen share", err); - callback(null); - }); - }); - - win.loadURL(backendUrl).catch((err) => { - console.error(`Failed to load desktop app: ${err}`); - }); - - // Ensure renderer receives latest updater state after any (re)load. - // Delay broadcast by 300ms to give the renderer time to register its - // onUpdateState IPC listener before we push state. - win.webContents.on('did-finish-load', () => { - setTimeout(() => { - if (!win.isDestroyed()) broadcastUpdateState(); - }, 300); - }); - - // Uncomment to debug - // win.webContents.openDevTools(); -} - -app.commandLine.appendSwitch('disable-webrtc-hw-encoding'); // Sometime helps resolve codec mismatch behavior -app.commandLine.appendSwitch('log-level', '3'); // Silences standard Chromium warning logs (like SRTP transport unprotect logs which are benign timing issues) - -app.whenReady().then(() => { - ipcMain.handle('clipboard-write', (event, text) => { - clipboard.writeText(text); - return true; - }); - - ipcMain.handle('get-update-state', () => ({ ...updateState })); - ipcMain.handle('check-for-updates-now', async () => { - await runUpdateCheck('renderer-direct'); - return { ...updateState }; - }); - - createWindow(); - - // Configure Auto-Updater - autoUpdater.autoDownload = false; - autoUpdater.logger = console; - - autoUpdater.on('update-available', (info) => { - updateState.status = 'available'; - updateState.info = info; - updateState.error = null; - broadcastUpdateState(); - const wins = BrowserWindow.getAllWindows(); - if (wins.length > 0) wins[0].webContents.send('update-available', info); - }); - - autoUpdater.on('update-not-available', (info) => { - updateState.status = 'not-available'; - updateState.info = info || null; - updateState.error = null; - broadcastUpdateState(); - }); - - autoUpdater.on('update-downloaded', (info) => { - updateState.status = 'downloaded'; - updateState.info = info; - updateState.error = null; - broadcastUpdateState(); - const wins = BrowserWindow.getAllWindows(); - if (wins.length > 0) wins[0].webContents.send('update-downloaded', info); - }); - - autoUpdater.on('error', (err) => { - updateState.status = 'error'; - updateState.error = err.message; - broadcastUpdateState(); - const wins = BrowserWindow.getAllWindows(); - if (wins.length > 0) wins[0].webContents.send('update-error', err.message); - }); - - ipcMain.on('check-for-updates', () => { - void runUpdateCheck('manual'); - }); - - ipcMain.on('download-update', () => { - if (installInProgress) return; - updateState.status = 'downloading'; - updateState.error = null; - broadcastUpdateState(); - autoUpdater.downloadUpdate(); - }); - - ipcMain.on('quit-and-install', () => { - installDownloadedUpdate(); - }); - - // Check once on startup - setTimeout(() => { - void runUpdateCheck('startup'); - }, 5000); - startPeriodicUpdateChecks(); - - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } - }); -}); - -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); - } -}); - -app.on('before-quit', () => { - if (periodicUpdateTimer) { - clearInterval(periodicUpdateTimer); - periodicUpdateTimer = null; - } -}); - -function safeOrigin(value) { - try { - return new URL(value).origin; - } catch (_) { - return null; - } -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 686ca4b..0000000 --- a/package-lock.json +++ /dev/null @@ -1,5307 +0,0 @@ -{ - "name": "chattz-electron", - "version": "0.0.50", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "chattz-electron", - "version": "0.0.50", - "dependencies": { - "electron-updater": "^6.8.3" - }, - "devDependencies": { - "electron": "^34.5.8", - "electron-builder": "^25.1.8" - } - }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", - "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/rebuild": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz", - "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", - "got": "^11.7.0", - "node-abi": "^3.45.0", - "node-api-version": "^0.2.0", - "node-gyp": "^9.0.0", - "ora": "^5.1.0", - "read-binary-file-arch": "^1.0.6", - "semver": "^7.3.5", - "tar": "^6.0.5", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/@electron/rebuild/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/rebuild/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/rebuild/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", - "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.2.7", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz", - "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", - "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "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" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/app-builder-bin": { - "version": "5.0.0-alpha.10", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", - "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "25.1.8", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz", - "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.1", - "@electron/rebuild": "3.6.1", - "@electron/universal": "2.0.1", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "bluebird-lst": "^1.0.9", - "builder-util": "25.1.7", - "builder-util-runtime": "9.2.10", - "chromium-pickle-js": "^0.2.0", - "config-file-ts": "0.2.8-rc1", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.6", - "ejs": "^3.1.8", - "electron-publish": "25.1.7", - "form-data": "^4.0.0", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "is-ci": "^3.0.0", - "isbinaryfile": "^5.0.0", - "js-yaml": "^4.1.0", - "json5": "^2.2.3", - "lazy-val": "^1.0.5", - "minimatch": "^10.0.0", - "resedit": "^1.7.0", - "sanitize-filename": "^1.6.3", - "semver": "^7.3.8", - "tar": "^6.1.12", - "temp-file": "^3.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "25.1.8", - "electron-builder-squirrel-windows": "25.1.8" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/app-builder-lib/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "dev": true, - "license": "ISC" - }, - "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/bluebird-lst": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", - "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bluebird": "^3.5.5" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz", - "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.10", - "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.2.10", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "is-ci": "^3.0.0", - "js-yaml": "^4.1.0", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.2.10", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", - "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/builder-util/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/cacache/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", - "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/config-file-ts": { - "version": "0.2.8-rc1", - "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", - "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^10.3.12", - "typescript": "^5.4.3" - } - }, - "node_modules/config-file-ts/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minimatch": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz", - "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dir-compare": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " - } - }, - "node_modules/dir-compare/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dmg-builder": { - "version": "25.1.8", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", - "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "25.1.8", - "builder-util": "25.1.7", - "builder-util-runtime": "9.2.10", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/dmg-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "34.5.8", - "resolved": "https://registry.npmjs.org/electron/-/electron-34.5.8.tgz", - "integrity": "sha512-vxLD65mabTzYmEVa9KceMHM0+zO+vqgrhcyNVlmTd0IGV5J7XZ8v/qElm0o4YQ4wPeq7olZkUjZkBQQEdr23/g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^20.9.0", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "25.1.8", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz", - "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "25.1.8", - "builder-util": "25.1.7", - "builder-util-runtime": "9.2.10", - "chalk": "^4.1.2", - "dmg-builder": "25.1.8", - "fs-extra": "^10.1.0", - "is-ci": "^3.0.0", - "lazy-val": "^1.0.5", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder-squirrel-windows": { - "version": "25.1.8", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", - "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "25.1.8", - "archiver": "^5.3.1", - "builder-util": "25.1.7", - "fs-extra": "^10.1.0" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-publish": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz", - "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "25.1.7", - "builder-util-runtime": "9.2.10", - "chalk": "^4.1.2", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-updater": { - "version": "6.8.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", - "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", - "license": "MIT", - "dependencies": { - "builder-util-runtime": "9.5.1", - "fs-extra": "^10.1.0", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "lodash.escaperegexp": "^4.1.2", - "lodash.isequal": "^4.5.0", - "semver": "~7.7.3", - "tiny-typed-emitter": "^2.1.0" - } - }, - "node_modules/electron-updater/node_modules/builder-util-runtime": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/electron-updater/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-updater/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-updater/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-updater/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/filelist": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz", - "integrity": "sha512-ct/ckWBV/9Dg3MlvCXsLcSUyoWwv9mCKqlhLNB2DAuXR/NZolSXlQqP5dyy6guWlPXBhodZyZ5lGPQcbQDxrEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^10.2.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "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" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true, - "license": "ISC" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "license": "MIT" - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "license": "MIT" - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", - "dev": true, - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", - "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.87.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", - "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", - "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^12.13 || ^14.13 || >=16" - } - }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/pe-library": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", - "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resedit": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^0.4.1" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/tiny-typed-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index f5c75b8..0000000 --- a/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "chattz-electron", - "version": "0.0.50", - "description": "Electron frontend for Chattz", - "author": "Pavel Flegr ", - "homepage": "https://discord.flegr.me", - "repository": { - "type": "git", - "url": "https://git.flegr.me/pavel/discord.git" - }, - "main": "main.js", - "scripts": { - "build:html": "node scripts/generate-html.js", - "start": "npm run build:html && electron .", - "dev": "npm run build:html && electron .", - "dist": "npm run build:html && electron-builder --publish never", - "dist:linux": "npm run build:html && electron-builder --linux --publish never", - "dist:win": "npm run build:html && electron-builder --win --publish never" - }, - "build": { - "appId": "me.flegr.chattz", - "productName": "Chattz", - "linux": { - "target": [ - "AppImage", - "rpm" - ], - "category": "Chat", - "icon": "build/icon.png" - }, - "win": { - "target": [ - "nsis" - ] - }, - "publish": [ - { - "provider": "generic", - "url": "https://discord.flegr.me/static/installers", - "channel": "latest" - } - ], - "files": [ - "main.js", - "package.json", - "desktop/", - "static/", - "!dist" - ], - "directories": { - "output": "dist" - } - }, - "devDependencies": { - "electron": "^34.5.8", - "electron-builder": "^25.1.8" - }, - "dependencies": { - "electron-updater": "^6.8.3" - } -} diff --git a/scripts/generate-html.js b/scripts/generate-html.js deleted file mode 100644 index a773454..0000000 --- a/scripts/generate-html.js +++ /dev/null @@ -1,57 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const root = path.resolve(__dirname, '..'); -const templatePath = path.join(root, 'shared-html', 'index.template.html'); -const template = fs.readFileSync(templatePath, 'utf8'); - -const updaterMarkup = ``; - -const variants = { - web: { - styles_href: '/static/styles.css', - lucide_src: '/static/vendor/lucide.min.js', - app_src: '/static/app.js?v=20260227-shared-core-1', - web_downloads: `
- Desktop downloads: - Windows - Linux (RPM) -
`, - desktop_updater: updaterMarkup, - outPath: path.join(root, 'static', 'index.html'), - }, - desktop: { - styles_href: '../static/styles.css', - lucide_src: '../static/vendor/lucide.min.js', - app_src: 'app.js?v=20260227-shared-core-1', - web_downloads: '', - desktop_updater: updaterMarkup, - outPath: path.join(root, 'desktop', 'index.html'), - }, -}; - -for (const variant of Object.values(variants)) { - let output = template; - for (const [key, value] of Object.entries(variant)) { - if (key === 'outPath') continue; - const token = `{{${key}}}`; - if (value === '') { - output = output.replace(new RegExp(`^[ \\t]*\\{\\{${key}\\}\\}[ \\t]*\\n?`, 'm'), ''); - continue; - } - output = output.replaceAll(token, value); - } - - if (/\{\{[a-z_]+\}\}/i.test(output)) { - throw new Error(`Unresolved template placeholders remain in ${variant.outPath}`); - } - - fs.writeFileSync( - variant.outPath, - `\n${output}`, - 'utf8' - ); -} diff --git a/shared-html/index.template.html b/shared-html/index.template.html deleted file mode 100644 index 3425611..0000000 --- a/shared-html/index.template.html +++ /dev/null @@ -1,279 +0,0 @@ - - - - - - - Chattz - - - - - - -
-
-
C
-

Chattz

-

Sign in to open your servers.

- - {{web_downloads}} -

-
-
- - - - - - - - - - - - - - - - - - - diff --git a/src/auth.rs b/src/auth.rs index 535953e..66fd67a 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,25 +1,32 @@ -use anyhow::{Result, anyhow}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use anyhow::{Context, Result, anyhow}; use axum::{ Json, extract::{FromRef, FromRequestParts}, - http::{HeaderMap, StatusCode, request::Parts}, + http::{StatusCode, request::Parts}, response::{IntoResponse, Response}, }; -use chrono::{Duration, Utc}; -use serde::Serialize; -use sha2::{Digest, Sha256}; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{AppState, db}; -pub const OAUTH_STATE_COOKIE: &str = "chattz_oauth_state"; -pub const SESSION_COOKIE: &str = "chattz_session"; -const SESSION_TTL_DAYS: i64 = 30; +const SESSION_COOKIE: &str = "chattz_session"; +const OAUTH_STATE_COOKIE: &str = "chattz_oauth_state"; +const SESSION_TTL_SECS: u64 = 60 * 60 * 24 * 7; + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct SessionClaims { + sub: String, + exp: usize, + iat: usize, +} #[derive(Debug, Clone)] pub struct AuthUser { pub id: Uuid, - pub session_id: String, } #[derive(Debug)] @@ -36,13 +43,6 @@ impl ApiError { } } - pub fn forbidden(msg: &str) -> Self { - Self { - status: StatusCode::FORBIDDEN, - message: msg.to_string(), - } - } - pub fn bad_request(msg: &str) -> Self { Self { status: StatusCode::BAD_REQUEST, @@ -56,13 +56,6 @@ impl ApiError { message: msg.to_string(), } } - - pub fn service_unavailable(msg: &str) -> Self { - Self { - status: StatusCode::SERVICE_UNAVAILABLE, - message: msg.to_string(), - } - } } #[derive(Serialize)] @@ -72,13 +65,7 @@ struct ErrorBody<'a> { impl IntoResponse for ApiError { fn into_response(self) -> Response { - ( - self.status, - Json(ErrorBody { - error: &self.message, - }), - ) - .into_response() + (self.status, Json(ErrorBody { error: &self.message })).into_response() } } @@ -90,25 +77,25 @@ impl From for ApiError { impl FromRequestParts for AuthUser where - AppState: FromRef, + AppState: axum::extract::FromRef, S: Send + Sync, { type Rejection = ApiError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app = AppState::from_ref(state); - let session_id = read_cookie_from_headers(&parts.headers, SESSION_COOKIE) - .ok_or_else(|| ApiError::unauthorized("missing session cookie"))?; + let token = read_cookie(parts, SESSION_COOKIE).ok_or_else(|| ApiError::unauthorized("missing session"))?; + let user_id = verify_session(&token, &app.settings.session_secret) + .map_err(|_| ApiError::unauthorized("invalid session"))?; - let user_id = db::touch_active_session(&app.db, &session_id) + let exists = db::user_exists(&app.db, user_id) .await - .map_err(|_| ApiError::unauthorized("invalid or expired session"))? - .ok_or_else(|| ApiError::unauthorized("invalid or expired session"))?; + .map_err(|_| ApiError::unauthorized("session user not found"))?; + if !exists { + return Err(ApiError::unauthorized("session user not found")); + } - Ok(Self { - id: user_id, - session_id, - }) + Ok(Self { id: user_id }) } } @@ -116,14 +103,6 @@ pub fn new_oauth_state() -> String { Uuid::new_v4().to_string() } -pub fn new_session_id() -> String { - Uuid::new_v4().simple().to_string() -} - -pub fn session_expiry() -> chrono::DateTime { - (Utc::now() + Duration::days(SESSION_TTL_DAYS)).fixed_offset() -} - pub fn make_oauth_state_cookie(value: &str, secure: bool) -> String { format!( "{name}={value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600{secure_flag}", @@ -133,32 +112,69 @@ pub fn make_oauth_state_cookie(value: &str, secure: bool) -> String { } pub fn clear_oauth_state_cookie(secure: bool) -> String { - clear_cookie(OAUTH_STATE_COOKIE, secure, "Lax") -} - -pub fn make_session_cookie(value: &str, secure: bool) -> String { format!( - "{name}={value}; Path=/; HttpOnly; SameSite=Strict; Max-Age={ttl}{secure_flag}", - name = SESSION_COOKIE, - ttl = Duration::days(SESSION_TTL_DAYS).num_seconds(), + "{name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0{secure_flag}", + name = OAUTH_STATE_COOKIE, secure_flag = if secure { "; Secure" } else { "" } ) } -pub fn clear_session_cookie(secure: bool) -> String { - clear_cookie(SESSION_COOKIE, secure, "Strict") -} - -pub fn read_cookie_from_headers(headers: &HeaderMap, cookie_name: &str) -> Option { +pub fn read_oauth_state_from_headers(headers: &axum::http::HeaderMap) -> Option { let raw = headers.get(axum::http::header::COOKIE)?.to_str().ok()?; raw.split(';').find_map(|pair| { let mut kv = pair.trim().splitn(2, '='); let key = kv.next()?; let value = kv.next()?; - (key == cookie_name).then(|| value.to_string()) + (key == OAUTH_STATE_COOKIE).then(|| value.to_string()) }) } +pub fn new_session_cookie(user_id: Uuid, secret: &str, secure: bool) -> Result { + let now = now_ts(); + let claims = SessionClaims { + sub: user_id.to_string(), + iat: now as usize, + exp: (now + SESSION_TTL_SECS) as usize, + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret.as_bytes()), + ) + .context("failed to encode session token")?; + + Ok(format!( + "{name}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={ttl}{secure_flag}", + name = SESSION_COOKIE, + ttl = SESSION_TTL_SECS, + secure_flag = if secure { "; Secure" } else { "" } + )) +} + +pub fn clear_session_cookie(secure: bool) -> String { + format!( + "{name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0{secure_flag}", + name = SESSION_COOKIE, + secure_flag = if secure { "; Secure" } else { "" } + ) +} + +fn verify_session(token: &str, secret: &str) -> Result { + let mut validation = Validation::default(); + validation.validate_exp = true; + + let data = decode::( + token, + &DecodingKey::from_secret(secret.as_bytes()), + &validation, + ) + .context("failed to decode session token")?; + + let user_id = Uuid::parse_str(&data.claims.sub).context("invalid sub in session token")?; + Ok(user_id) +} + pub fn validate_oauth_state(expected_cookie: Option, query_state: &str) -> Result<()> { let expected = expected_cookie.ok_or_else(|| anyhow!("missing oauth state cookie"))?; if expected != query_state { @@ -167,113 +183,19 @@ pub fn validate_oauth_state(expected_cookie: Option, query_state: &str) Ok(()) } -pub fn user_agent_hash(headers: &HeaderMap) -> Option { - header_hash(headers, axum::http::header::USER_AGENT.as_str()) +fn read_cookie(parts: &Parts, name: &str) -> Option { + let raw = parts.headers.get(axum::http::header::COOKIE)?.to_str().ok()?; + raw.split(';').find_map(|pair| { + let mut kv = pair.trim().splitn(2, '='); + let key = kv.next()?; + let value = kv.next()?; + (key == name).then(|| value.to_string()) + }) } -pub fn ip_hash(headers: &HeaderMap) -> Option { - headers - .get("x-forwarded-for") - .and_then(|v| v.to_str().ok()) - .and_then(|raw| raw.split(',').next().map(str::trim)) - .filter(|value| !value.is_empty()) - .map(hash_string) - .or_else(|| header_hash(headers, "x-real-ip")) -} - -pub fn origin_matches(headers: &HeaderMap, expected_origin: &str) -> bool { - headers - .get(axum::http::header::ORIGIN) - .and_then(|value| value.to_str().ok()) - .map(|origin| origin == expected_origin) - .unwrap_or(false) -} - -fn header_hash(headers: &HeaderMap, header_name: &str) -> Option { - headers - .get(header_name) - .and_then(|v| v.to_str().ok()) - .filter(|value| !value.trim().is_empty()) - .map(hash_string) -} - -fn hash_string(value: &str) -> String { - let mut hasher = Sha256::new(); - hasher.update(value.as_bytes()); - let digest = hasher.finalize(); - let mut out = String::with_capacity(digest.len() * 2); - for byte in digest { - out.push(nibble_to_hex(byte >> 4)); - out.push(nibble_to_hex(byte & 0x0f)); - } - out -} - -fn nibble_to_hex(value: u8) -> char { - match value { - 0..=9 => (b'0' + value) as char, - 10..=15 => (b'a' + (value - 10)) as char, - _ => unreachable!(), - } -} - -fn clear_cookie(name: &str, secure: bool, same_site: &str) -> String { - format!( - "{name}=; Path=/; HttpOnly; SameSite={same_site}; Max-Age=0{secure_flag}", - secure_flag = if secure { "; Secure" } else { "" } - ) -} - -#[cfg(test)] -mod tests { - use super::{ - SESSION_COOKIE, clear_session_cookie, hash_string, make_session_cookie, origin_matches, - read_cookie_from_headers, - }; - use axum::http::{HeaderMap, header}; - - #[test] - fn hash_string_is_stable() { - assert_eq!( - hash_string("example"), - "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c" - ); - } - - #[test] - fn reads_named_cookie_from_header() { - let mut headers = HeaderMap::new(); - headers.insert( - header::COOKIE, - "other=value; chattz_session=session-123; another=ok" - .parse() - .unwrap(), - ); - - assert_eq!( - read_cookie_from_headers(&headers, SESSION_COOKIE), - Some("session-123".to_string()) - ); - } - - #[test] - fn origin_match_requires_exact_origin() { - let mut headers = HeaderMap::new(); - headers.insert(header::ORIGIN, "http://localhost:3000".parse().unwrap()); - - assert!(origin_matches(&headers, "http://localhost:3000")); - assert!(!origin_matches(&headers, "https://localhost:3000")); - } - - #[test] - fn session_cookie_has_strict_policy_and_clear_cookie_expires() { - let session_cookie = make_session_cookie("session-123", false); - let cleared_cookie = clear_session_cookie(false); - - assert!(session_cookie.contains("HttpOnly")); - assert!(session_cookie.contains("SameSite=Strict")); - assert!(session_cookie.contains("Max-Age=")); - assert!(cleared_cookie.contains("SameSite=Strict")); - assert!(cleared_cookie.contains("Max-Age=0")); - } +fn now_ts() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)) + .as_secs() } diff --git a/src/chat.rs b/src/chat.rs index 565f08b..10beca6 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,30 +1,15 @@ -use std::collections::HashMap; - +use crate::AppState; use axum::extract::ws::{Message, WebSocket}; use futures_util::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use tokio::sync::{RwLock, mpsc}; use uuid::Uuid; -use crate::{AppState, db}; - -#[derive(Clone)] -pub struct ChatClient { - tx: mpsc::UnboundedSender, - is_idle: bool, -} - -#[derive(Serialize, Clone, Debug, PartialEq, Eq)] -pub struct OnlineUser { - pub user_id: Uuid, - pub online: bool, - pub idle: bool, -} - #[derive(Default)] pub struct ChatHub { - // user_id -> connection_id -> client - clients: RwLock>>, + // user_id -> sender + clients: RwLock>>, } #[derive(Serialize, Clone)] @@ -41,120 +26,68 @@ pub enum ServerEvent { UserPresence { user_id: Uuid, online: bool, - idle: bool, }, } #[derive(Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] enum ClientEvent { + // Currently no interactive client events for the general chat WS Ping, - SetIdleStatus { is_idle: bool }, } impl ChatHub { - pub async fn add_client( - &self, - user_id: Uuid, - connection_id: Uuid, - tx: mpsc::UnboundedSender, - ) -> Option { + pub async fn add_client(&self, user_id: Uuid, tx: mpsc::UnboundedSender) { let mut clients = self.clients.write().await; - let previous = aggregate_presence(clients.get(&user_id), user_id); - - clients - .entry(user_id) - .or_default() - .insert(connection_id, ChatClient { tx, is_idle: false }); - - let current = aggregate_presence(clients.get(&user_id), user_id); - presence_delta(previous, current) + clients.insert(user_id, tx); } - pub async fn remove_client(&self, user_id: Uuid, connection_id: Uuid) -> Option { + pub async fn remove_client(&self, user_id: Uuid) { let mut clients = self.clients.write().await; - let previous = aggregate_presence(clients.get(&user_id), user_id); - - if let Some(connections) = clients.get_mut(&user_id) { - connections.remove(&connection_id); - if connections.is_empty() { - clients.remove(&user_id); - } - } - - let current = aggregate_presence(clients.get(&user_id), user_id); - presence_delta(previous, current) + clients.remove(&user_id); } - pub async fn get_online_users_for(&self, visible_user_ids: &[Uuid]) -> Vec { + pub async fn get_online_users(&self) -> Vec { let clients = self.clients.read().await; - visible_user_ids - .iter() - .filter_map(|user_id| aggregate_presence(clients.get(user_id), *user_id)) - .filter(|presence| presence.online) - .collect() + clients.keys().cloned().collect() } - pub async fn broadcast_to_many(&self, user_ids: Vec, event: ServerEvent) { + pub async fn broadcast_all(&self, event: ServerEvent) { let clients = self.clients.read().await; - for user_id in user_ids { - if let Some(connections) = clients.get(&user_id) { - for client in connections.values() { - let _ = client.tx.send(event.clone()); - } - } + for tx in clients.values() { + let _ = tx.send(event.clone()); } } pub async fn broadcast_to_user(&self, user_id: Uuid, event: ServerEvent) { let clients = self.clients.read().await; - if let Some(connections) = clients.get(&user_id) { - for client in connections.values() { - let _ = client.tx.send(event.clone()); - } + if let Some(tx) = clients.get(&user_id) { + let _ = tx.send(event); } } - pub async fn set_idle_status( - &self, - user_id: Uuid, - connection_id: Uuid, - is_idle: bool, - ) -> Option { - let mut clients = self.clients.write().await; - let previous = aggregate_presence(clients.get(&user_id), user_id); - - if let Some(connections) = clients.get_mut(&user_id) - && let Some(client) = connections.get_mut(&connection_id) - { - client.is_idle = is_idle; + pub async fn broadcast_to_many(&self, user_ids: Vec, event: ServerEvent) { + let clients = self.clients.read().await; + for user_id in user_ids { + if let Some(tx) = clients.get(&user_id) { + let _ = tx.send(event.clone()); + } } - - let current = aggregate_presence(clients.get(&user_id), user_id); - presence_delta(previous, current) } } pub async fn handle_socket(state: AppState, socket: WebSocket, user_id: Uuid) { let (mut ws_sender, mut ws_receiver) = socket.split(); let (tx, mut rx) = mpsc::unbounded_channel::(); - let connection_id = Uuid::new_v4(); - if let Some(presence) = state.chat.add_client(user_id, connection_id, tx).await { - if let Ok(visible_user_ids) = db::list_visible_user_ids(&state.db, user_id).await { - state - .chat - .broadcast_to_many( - visible_user_ids, - ServerEvent::UserPresence { - user_id: presence.user_id, - online: presence.online, - idle: presence.idle, - }, - ) - .await; - } - } + state.chat.add_client(user_id, tx).await; + state + .chat + .broadcast_all(ServerEvent::UserPresence { + user_id, + online: true, + }) + .await; let send_task = tokio::spawn(async move { while let Some(event) = rx.recv().await { @@ -168,187 +101,18 @@ pub async fn handle_socket(state: AppState, socket: WebSocket, user_id: Uuid) { }); while let Some(Ok(msg)) = ws_receiver.next().await { - match msg { - Message::Close(_) => break, - Message::Text(text) => { - if let Ok(ClientEvent::SetIdleStatus { is_idle }) = - serde_json::from_str::(&text) - && let Some(presence) = state - .chat - .set_idle_status(user_id, connection_id, is_idle) - .await - && let Ok(visible_user_ids) = - db::list_visible_user_ids(&state.db, user_id).await - { - state - .chat - .broadcast_to_many( - visible_user_ids, - ServerEvent::UserPresence { - user_id: presence.user_id, - online: presence.online, - idle: presence.idle, - }, - ) - .await; - } - } - _ => {} + if let Message::Close(_) = msg { + break; } } send_task.abort(); - if let Some(presence) = state.chat.remove_client(user_id, connection_id).await - && let Ok(visible_user_ids) = db::list_visible_user_ids(&state.db, user_id).await - { - state - .chat - .broadcast_to_many( - visible_user_ids, - ServerEvent::UserPresence { - user_id: presence.user_id, - online: presence.online, - idle: presence.idle, - }, - ) - .await; - } -} - -fn aggregate_presence( - connections: Option<&HashMap>, - user_id: Uuid, -) -> Option { - let connections = connections?; - if connections.is_empty() { - return None; - } - - let idle = connections.values().all(|client| client.is_idle); - Some(OnlineUser { - user_id, - online: true, - idle, - }) -} - -fn presence_delta(previous: Option, current: Option) -> Option { - match (previous, current) { - (None, None) => None, - (Some(prev), Some(curr)) if prev == curr => None, - (Some(prev), None) => Some(OnlineUser { - user_id: prev.user_id, - online: false, - idle: false, - }), - (_, Some(curr)) => Some(curr), - } -} - -#[cfg(test)] -mod tests { - use super::{ChatHub, OnlineUser, ServerEvent}; - use serde_json::json; - use tokio::sync::mpsc; - use uuid::Uuid; - - #[tokio::test] - async fn multiple_connections_keep_user_online_until_last_disconnect() { - let hub = ChatHub::default(); - let user_id = Uuid::new_v4(); - let first_connection = Uuid::new_v4(); - let second_connection = Uuid::new_v4(); - let (tx1, _rx1) = mpsc::unbounded_channel(); - let (tx2, _rx2) = mpsc::unbounded_channel(); - - let first_presence = hub.add_client(user_id, first_connection, tx1).await; - let second_presence = hub.add_client(user_id, second_connection, tx2).await; - let after_first_disconnect = hub.remove_client(user_id, first_connection).await; - let after_last_disconnect = hub.remove_client(user_id, second_connection).await; - - assert_eq!( - first_presence, - Some(OnlineUser { - user_id, - online: true, - idle: false, - }) - ); - assert_eq!(second_presence, None); - assert_eq!(after_first_disconnect, None); - assert_eq!( - after_last_disconnect, - Some(OnlineUser { - user_id, - online: false, - idle: false, - }) - ); - } - - #[tokio::test] - async fn idle_status_only_flips_when_all_connections_are_idle() { - let hub = ChatHub::default(); - let user_id = Uuid::new_v4(); - let first_connection = Uuid::new_v4(); - let second_connection = Uuid::new_v4(); - let (tx1, _rx1) = mpsc::unbounded_channel(); - let (tx2, _rx2) = mpsc::unbounded_channel(); - - hub.add_client(user_id, first_connection, tx1).await; - hub.add_client(user_id, second_connection, tx2).await; - - let first_idle = hub.set_idle_status(user_id, first_connection, true).await; - let second_idle = hub.set_idle_status(user_id, second_connection, true).await; - let active_again = hub.set_idle_status(user_id, first_connection, false).await; - - assert_eq!(first_idle, None); - assert_eq!( - second_idle, - Some(OnlineUser { - user_id, - online: true, - idle: true, - }) - ); - assert_eq!( - active_again, - Some(OnlineUser { - user_id, - online: true, - idle: false, - }) - ); - } - - #[tokio::test] - async fn broadcast_to_user_reaches_all_active_connections() { - let hub = ChatHub::default(); - let user_id = Uuid::new_v4(); - let first_connection = Uuid::new_v4(); - let second_connection = Uuid::new_v4(); - let (tx1, mut rx1) = mpsc::unbounded_channel(); - let (tx2, mut rx2) = mpsc::unbounded_channel(); - - hub.add_client(user_id, first_connection, tx1).await; - hub.add_client(user_id, second_connection, tx2).await; - - hub.broadcast_to_user( + state.chat.remove_client(user_id).await; + state + .chat + .broadcast_all(ServerEvent::UserPresence { user_id, - ServerEvent::DmCreated { - other_user_id: Uuid::new_v4(), - message: json!({ "body": "hello" }), - }, - ) + online: false, + }) .await; - - assert!(matches!( - rx1.try_recv(), - Ok(ServerEvent::DmCreated { message, .. }) if message["body"] == "hello" - )); - assert!(matches!( - rx2.try_recv(), - Ok(ServerEvent::DmCreated { message, .. }) if message["body"] == "hello" - )); - } } diff --git a/src/config.rs b/src/config.rs index fbe7ecb..0696d55 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,8 @@ -use anyhow::{Context, Result, anyhow}; -use reqwest::Url; +use anyhow::{Context, Result}; #[derive(Clone, Debug)] pub struct Settings { pub port: u16, - pub app_base_url: String, - pub app_origin: String, pub database_url: String, pub oidc_client_id: String, pub oidc_client_secret: String, @@ -14,36 +11,21 @@ pub struct Settings { pub oidc_userinfo_url: String, pub oidc_redirect_url: String, pub oidc_scopes: String, - pub session_cookie_secure: bool, + pub session_secret: String, + pub cookie_secure: bool, pub stun_urls: Vec, pub turn_urls: Vec, pub turn_username: Option, pub turn_password: Option, - pub media: Option, -} - -#[derive(Clone, Debug)] -pub struct MediaSettings { - pub account_id: String, - pub access_key_id: String, - pub secret_access_key: String, - pub bucket: String, - pub public_base_url: String, } impl Settings { pub fn from_env() -> Result { - let app_base_url = required("APP_BASE_URL")?; - let app_url = Url::parse(&app_base_url).context("APP_BASE_URL must be a valid URL")?; - let app_origin = origin_from_url(&app_url)?; - Ok(Self { port: std::env::var("PORT") .unwrap_or_else(|_| "3000".into()) .parse() .context("PORT must be a valid u16")?, - app_base_url: trim_trailing_slash(&app_base_url), - app_origin, database_url: required("DATABASE_URL")?, oidc_client_id: required("OIDC_CLIENT_ID")?, oidc_client_secret: required("OIDC_CLIENT_SECRET")?, @@ -51,51 +33,20 @@ impl Settings { oidc_token_url: required("OIDC_TOKEN_URL")?, oidc_userinfo_url: required("OIDC_USERINFO_URL")?, oidc_redirect_url: required("OIDC_REDIRECT_URL")?, - oidc_scopes: std::env::var("OIDC_SCOPES") - .unwrap_or_else(|_| "openid profile email".to_string()), - session_cookie_secure: requires_secure_cookie(&app_url)?, + oidc_scopes: std::env::var("OIDC_SCOPES").unwrap_or_else(|_| "openid profile email".to_string()), + session_secret: required("SESSION_SECRET")?, + cookie_secure: std::env::var("COOKIE_SECURE") + .unwrap_or_else(|_| "false".into()) + .parse() + .context("COOKIE_SECURE must be true/false")?, stun_urls: parse_csv_env("STUN_URLS", "stun:stun.l.google.com:19302"), turn_urls: parse_csv_env("TURN_URLS", ""), turn_username: optional("TURN_USERNAME"), turn_password: optional("TURN_PASSWORD"), - media: MediaSettings::from_env()?, }) } } -impl MediaSettings { - fn from_env() -> Result> { - let account_id = optional("R2_ACCOUNT_ID"); - let access_key_id = optional("R2_ACCESS_KEY_ID"); - let secret_access_key = optional("R2_SECRET_ACCESS_KEY"); - let bucket = optional("R2_BUCKET"); - let public_base_url = optional("MEDIA_BASE_URL").or_else(|| optional("R2_PUBLIC_BASE_URL")); - - if account_id.is_none() - && access_key_id.is_none() - && secret_access_key.is_none() - && bucket.is_none() - && public_base_url.is_none() - { - return Ok(None); - } - - Ok(Some(Self { - account_id: account_id.context("missing env var R2_ACCOUNT_ID")?, - access_key_id: access_key_id.context("missing env var R2_ACCESS_KEY_ID")?, - secret_access_key: secret_access_key.context("missing env var R2_SECRET_ACCESS_KEY")?, - bucket: bucket.context("missing env var R2_BUCKET")?, - public_base_url: trim_trailing_slash( - &public_base_url.context("missing env var MEDIA_BASE_URL")?, - ), - })) - } - - pub fn endpoint_url(&self) -> String { - format!("https://{}.r2.cloudflarestorage.com", self.account_id) - } -} - fn required(name: &str) -> Result { std::env::var(name).with_context(|| format!("missing env var {name}")) } @@ -115,105 +66,3 @@ fn parse_csv_env(name: &str, default_value: &str) -> Vec { .map(ToString::to_string) .collect() } - -fn origin_from_url(url: &Url) -> Result { - let host = url - .host_str() - .ok_or_else(|| anyhow!("APP_BASE_URL must include a host"))?; - let mut origin = format!("{}://{}", url.scheme(), host); - if let Some(port) = url.port() { - origin.push(':'); - origin.push_str(&port.to_string()); - } - Ok(origin) -} - -fn requires_secure_cookie(url: &Url) -> Result { - match url.scheme() { - "https" => Ok(true), - "http" => { - let host = url - .host_str() - .ok_or_else(|| anyhow!("APP_BASE_URL must include a host"))?; - if matches!(host, "localhost" | "127.0.0.1" | "::1") { - Ok(false) - } else { - Err(anyhow!( - "APP_BASE_URL must use https outside localhost when session cookies are enabled" - )) - } - } - other => Err(anyhow!("APP_BASE_URL scheme {other} is not supported")), - } -} - -fn trim_trailing_slash(value: &str) -> String { - value.trim_end_matches('/').to_string() -} - -#[cfg(test)] -mod tests { - use super::{MediaSettings, origin_from_url, requires_secure_cookie, trim_trailing_slash}; - use reqwest::Url; - - #[test] - fn origin_from_url_preserves_explicit_port() { - let url = Url::parse("https://chat.example.com:8443/app").unwrap(); - let origin = origin_from_url(&url).unwrap(); - - assert_eq!(origin, "https://chat.example.com:8443"); - } - - #[test] - fn secure_cookie_is_required_for_https_origins() { - let url = Url::parse("https://chat.example.com").unwrap(); - - assert_eq!(requires_secure_cookie(&url).unwrap(), true); - } - - #[test] - fn localhost_http_is_allowed_without_secure_cookie() { - let url = Url::parse("http://localhost:3000").unwrap(); - - assert_eq!(requires_secure_cookie(&url).unwrap(), false); - } - - #[test] - fn non_localhost_http_is_rejected() { - let url = Url::parse("http://chat.example.com").unwrap(); - let err = requires_secure_cookie(&url).unwrap_err(); - - assert!( - err.to_string() - .contains("APP_BASE_URL must use https outside localhost") - ); - } - - #[test] - fn trim_trailing_slash_removes_only_suffix_slashes() { - assert_eq!( - trim_trailing_slash("https://chat.example.com///"), - "https://chat.example.com" - ); - assert_eq!( - trim_trailing_slash("https://chat.example.com/app"), - "https://chat.example.com/app" - ); - } - - #[test] - fn media_endpoint_url_uses_account_id() { - let media = MediaSettings { - account_id: "acct123".to_string(), - access_key_id: "key".to_string(), - secret_access_key: "secret".to_string(), - bucket: "bucket".to_string(), - public_base_url: "https://media.example.com".to_string(), - }; - - assert_eq!( - media.endpoint_url(), - "https://acct123.r2.cloudflarestorage.com" - ); - } -} diff --git a/src/db.rs b/src/db.rs index c86dd8c..2e26c94 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,22 +1,20 @@ -use std::collections::HashSet; - use anyhow::{Result, anyhow}; use chrono::{Duration, Utc}; use sea_orm::{ ActiveModelTrait, ActiveValue::Set, ColumnTrait, Condition, DatabaseConnection, DatabaseTransaction, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, - Statement, TransactionTrait, sea_query::OnConflict, + TransactionTrait, sea_query::OnConflict, }; use uuid::Uuid; use crate::{ entity::{ - attachments, channels, direct_messages, guild_members, guilds, invites, messages, sessions, - soundboard_sounds, users, + channels, direct_messages, guild_members, guilds, invites, messages, soundboard_sounds, + users, }, models::{ - Attachment, BasicUser, Channel, DmConversation, DmMessageWithAuthor, Guild, Invite, - MessageWithAuthor, SoundboardSound, User, + BasicUser, Channel, DmConversation, DmMessageWithAuthor, Guild, Invite, MessageWithAuthor, + SoundboardSound, User, }, }; @@ -28,88 +26,6 @@ pub async fn user_exists(db: &DatabaseConnection, user_id: Uuid) -> Result Ok(count > 0) } -pub async fn create_session( - db: &DatabaseConnection, - session_id: &str, - user_id: Uuid, - expires_at: chrono::DateTime, - user_agent_hash: Option, - ip_hash: Option, -) -> Result<()> { - sessions::Entity::insert(sessions::ActiveModel { - id: Set(session_id.to_string()), - user_id: Set(user_id), - expires_at: Set(expires_at), - created_at: Set(Utc::now().fixed_offset()), - last_seen_at: Set(Utc::now().fixed_offset()), - revoked_at: Set(None), - user_agent_hash: Set(user_agent_hash), - ip_hash: Set(ip_hash), - }) - .exec(db) - .await?; - - Ok(()) -} - -pub async fn touch_active_session( - db: &DatabaseConnection, - session_id: &str, -) -> Result> { - let session = sessions::Entity::find_by_id(session_id.to_string()) - .one(db) - .await?; - - let Some(session) = session else { - return Ok(None); - }; - - if session.revoked_at.is_some() || session.expires_at <= Utc::now().fixed_offset() { - return Ok(None); - } - - let user_id = session.user_id; - sessions::Entity::update_many() - .col_expr( - sessions::Column::LastSeenAt, - sea_orm::sea_query::Expr::value(Utc::now().fixed_offset()), - ) - .filter(sessions::Column::Id.eq(session_id.to_string())) - .exec(db) - .await?; - Ok(Some(user_id)) -} - -pub async fn revoke_session(db: &DatabaseConnection, session_id: &str) -> Result<()> { - if let Some(session) = sessions::Entity::find_by_id(session_id.to_string()) - .one(db) - .await? - { - sessions::Entity::update_many() - .col_expr( - sessions::Column::RevokedAt, - sea_orm::sea_query::Expr::value(Some(Utc::now().fixed_offset())), - ) - .filter(sessions::Column::Id.eq(session.id)) - .exec(db) - .await?; - } - - Ok(()) -} - -pub async fn cleanup_sessions(db: &DatabaseConnection) -> Result<()> { - sessions::Entity::delete_many() - .filter( - Condition::any() - .add(sessions::Column::ExpiresAt.lte(Utc::now().fixed_offset())) - .add(sessions::Column::RevokedAt.is_not_null()), - ) - .exec(db) - .await?; - Ok(()) -} - pub async fn upsert_user_from_oidc( db: &DatabaseConnection, oidc_sub: &str, @@ -190,58 +106,18 @@ pub async fn list_guilds_for_user(db: &DatabaseConnection, user_id: Uuid) -> Res .collect()) } -pub async fn list_visible_user_ids(db: &DatabaseConnection, user_id: Uuid) -> Result> { - let guild_ids: Vec = guild_members::Entity::find() - .filter(guild_members::Column::UserId.eq(user_id)) - .select_only() - .column(guild_members::Column::GuildId) - .into_tuple() - .all(db) - .await?; - - let mut visible = HashSet::from([user_id]); - - if !guild_ids.is_empty() { - let guild_users = guild_members::Entity::find() - .filter(guild_members::Column::GuildId.is_in(guild_ids)) - .all(db) - .await?; - visible.extend(guild_users.into_iter().map(|membership| membership.user_id)); - } - - let dm_rows = direct_messages::Entity::find() - .filter( - Condition::any() - .add(direct_messages::Column::SenderUserId.eq(user_id)) - .add(direct_messages::Column::RecipientUserId.eq(user_id)), - ) - .all(db) - .await?; - - for row in dm_rows { - if row.sender_user_id == user_id { - visible.insert(row.recipient_user_id); - } else { - visible.insert(row.sender_user_id); - } - } - - Ok(visible.into_iter().collect()) -} - pub async fn create_guild( db: &DatabaseConnection, owner_user_id: Uuid, name: &str, ) -> Result { - let txn = db.begin().await?; let guild = guilds::Entity::insert(guilds::ActiveModel { id: Set(Uuid::new_v4()), name: Set(name.to_string()), owner_user_id: Set(owner_user_id), ..Default::default() }) - .exec_with_returning(&txn) + .exec_with_returning(db) .await?; guild_members::Entity::insert(guild_members::ActiveModel { @@ -257,25 +133,12 @@ pub async fn create_guild( .do_nothing() .to_owned(), ) - .exec(&txn) + .exec(db) .await?; - txn.commit().await?; Ok(map_guild(guild)) } -pub async fn is_guild_owner( - db: &DatabaseConnection, - guild_id: Uuid, - user_id: Uuid, -) -> Result { - let guild = guilds::Entity::find_by_id(guild_id).one(db).await?; - match guild { - Some(g) => Ok(g.owner_user_id == user_id), - None => Ok(false), - } -} - pub async fn create_invite( db: &DatabaseConnection, guild_id: Uuid, @@ -317,12 +180,7 @@ pub async fn create_invite( pub async fn join_invite(db: &DatabaseConnection, code: &str, user_id: Uuid) -> Result { let txn = db.begin().await?; - let invite = invites::Entity::find() - .from_raw_sql(Statement::from_sql_and_values( - sea_orm::DatabaseBackend::Postgres, - r#"SELECT * FROM invites WHERE code = $1 FOR UPDATE"#, - [code.into()], - )) + let invite = invites::Entity::find_by_id(code.to_string()) .one(&txn) .await? .ok_or_else(|| anyhow!("invite not found"))?; @@ -475,63 +333,6 @@ pub async fn create_message( author_user_id: model.author_user_id, author_display_name: user.display_name, body: model.body, - attachments: Vec::new(), - created_at: model.created_at, - }) -} - -pub async fn create_message_with_attachment( - db: &DatabaseConnection, - channel_id: Uuid, - author_user_id: Uuid, - body: &str, - object_key: &str, - media_url: &str, - mime_type: &str, - size_bytes: i64, - original_filename: &str, -) -> Result { - let txn = db.begin().await?; - - let model = messages::Entity::insert(messages::ActiveModel { - id: Set(Uuid::new_v4()), - channel_id: Set(channel_id), - author_user_id: Set(author_user_id), - body: Set(body.to_string()), - ..Default::default() - }) - .exec_with_returning(&txn) - .await?; - - let attachment = attachments::Entity::insert(attachments::ActiveModel { - id: Set(Uuid::new_v4()), - channel_message_id: Set(Some(model.id)), - direct_message_id: Set(None), - uploader_user_id: Set(author_user_id), - object_key: Set(object_key.to_string()), - media_url: Set(media_url.to_string()), - mime_type: Set(mime_type.to_string()), - size_bytes: Set(size_bytes), - original_filename: Set(original_filename.to_string()), - ..Default::default() - }) - .exec_with_returning(&txn) - .await?; - - let user = users::Entity::find_by_id(author_user_id) - .one(&txn) - .await? - .ok_or_else(|| anyhow!("author not found"))?; - - txn.commit().await?; - - Ok(MessageWithAuthor { - id: model.id, - channel_id: model.channel_id, - author_user_id: model.author_user_id, - author_display_name: user.display_name, - body: model.body, - attachments: vec![map_attachment(attachment)], created_at: model.created_at, }) } @@ -549,10 +350,6 @@ pub async fn list_messages( .all(db) .await?; - let attachment_map = - list_attachments_for_channel_messages(db, rows.iter().map(|(msg, _)| msg.id).collect()) - .await?; - Ok(rows .into_iter() .map(|(msg, user)| { @@ -565,7 +362,6 @@ pub async fn list_messages( author_user_id: msg.author_user_id, author_display_name, body: msg.body, - attachments: attachment_map.get(&msg.id).cloned().unwrap_or_default(), created_at: msg.created_at, } }) @@ -599,63 +395,6 @@ pub async fn create_direct_message( recipient_user_id: model.recipient_user_id, author_display_name: user.display_name, body: model.body, - attachments: Vec::new(), - created_at: model.created_at, - }) -} - -pub async fn create_direct_message_with_attachment( - db: &DatabaseConnection, - sender_user_id: Uuid, - recipient_user_id: Uuid, - body: &str, - object_key: &str, - media_url: &str, - mime_type: &str, - size_bytes: i64, - original_filename: &str, -) -> Result { - let txn = db.begin().await?; - - let model = direct_messages::Entity::insert(direct_messages::ActiveModel { - id: Set(Uuid::new_v4()), - sender_user_id: Set(sender_user_id), - recipient_user_id: Set(recipient_user_id), - body: Set(body.to_string()), - ..Default::default() - }) - .exec_with_returning(&txn) - .await?; - - let attachment = attachments::Entity::insert(attachments::ActiveModel { - id: Set(Uuid::new_v4()), - channel_message_id: Set(None), - direct_message_id: Set(Some(model.id)), - uploader_user_id: Set(sender_user_id), - object_key: Set(object_key.to_string()), - media_url: Set(media_url.to_string()), - mime_type: Set(mime_type.to_string()), - size_bytes: Set(size_bytes), - original_filename: Set(original_filename.to_string()), - ..Default::default() - }) - .exec_with_returning(&txn) - .await?; - - let user = users::Entity::find_by_id(sender_user_id) - .one(&txn) - .await? - .ok_or_else(|| anyhow!("sender not found"))?; - - txn.commit().await?; - - Ok(DmMessageWithAuthor { - id: model.id, - author_user_id: model.sender_user_id, - recipient_user_id: model.recipient_user_id, - author_display_name: user.display_name, - body: model.body, - attachments: vec![map_attachment(attachment)], created_at: model.created_at, }) } @@ -698,9 +437,6 @@ pub async fn list_direct_messages( .map(|u| (u.id, u.display_name)) .collect(); - let attachment_map = - list_attachments_for_direct_messages(db, rows.iter().map(|msg| msg.id).collect()).await?; - Ok(rows .into_iter() .map(|msg| DmMessageWithAuthor { @@ -712,64 +448,11 @@ pub async fn list_direct_messages( .cloned() .unwrap_or_else(|| "Unknown User".to_string()), body: msg.body, - attachments: attachment_map.get(&msg.id).cloned().unwrap_or_default(), created_at: msg.created_at, }) .collect()) } -async fn list_attachments_for_channel_messages( - db: &DatabaseConnection, - message_ids: Vec, -) -> Result>> { - if message_ids.is_empty() { - return Ok(std::collections::HashMap::new()); - } - - let rows = attachments::Entity::find() - .filter(attachments::Column::ChannelMessageId.is_in(message_ids)) - .order_by_asc(attachments::Column::CreatedAt) - .all(db) - .await?; - - let mut grouped = std::collections::HashMap::>::new(); - for row in rows { - if let Some(message_id) = row.channel_message_id { - grouped - .entry(message_id) - .or_default() - .push(map_attachment(row)); - } - } - Ok(grouped) -} - -async fn list_attachments_for_direct_messages( - db: &DatabaseConnection, - message_ids: Vec, -) -> Result>> { - if message_ids.is_empty() { - return Ok(std::collections::HashMap::new()); - } - - let rows = attachments::Entity::find() - .filter(attachments::Column::DirectMessageId.is_in(message_ids)) - .order_by_asc(attachments::Column::CreatedAt) - .all(db) - .await?; - - let mut grouped = std::collections::HashMap::>::new(); - for row in rows { - if let Some(message_id) = row.direct_message_id { - grouped - .entry(message_id) - .or_default() - .push(map_attachment(row)); - } - } - Ok(grouped) -} - pub async fn list_dm_conversations( db: &DatabaseConnection, current_user_id: Uuid, @@ -843,16 +526,6 @@ fn map_guild(model: guilds::Model) -> Guild { } } -fn map_attachment(model: attachments::Model) -> Attachment { - Attachment { - id: model.id, - media_url: model.media_url, - mime_type: model.mime_type, - size_bytes: model.size_bytes, - original_filename: model.original_filename, - } -} - fn map_channel(model: channels::Model) -> Channel { Channel { id: model.id, @@ -922,10 +595,7 @@ pub async fn create_sound( created_by_user_id: Uuid, name: &str, icon: &str, - object_key: &str, - media_url: &str, - mime_type: &str, - size_bytes: i64, + file_path: &str, ) -> Result { let model = soundboard_sounds::Entity::insert(soundboard_sounds::ActiveModel { id: Set(Uuid::new_v4()), @@ -933,13 +603,7 @@ pub async fn create_sound( created_by_user_id: Set(created_by_user_id), name: Set(name.to_string()), icon: Set(icon.to_string()), - object_key: Set(Some(object_key.to_string())), - media_url: Set(media_url.to_string()), - mime_type: Set(Some(mime_type.to_string())), - size_bytes: Set(Some(size_bytes)), - // Keep the legacy column populated until every deployment has applied - // the nullable migration and old fallback paths are fully removed. - file_path: Set(Some(media_url.to_string())), + file_path: Set(file_path.to_string()), ..Default::default() }) .exec_with_returning(db) @@ -948,342 +612,14 @@ pub async fn create_sound( Ok(map_sound(model)) } -pub async fn get_sound_by_id(db: &DatabaseConnection, id: Uuid) -> Result> { - let row = soundboard_sounds::Entity::find_by_id(id).one(db).await?; - Ok(row.map(map_sound)) -} - -pub async fn get_sound_object_key(db: &DatabaseConnection, id: Uuid) -> Result> { - let row = soundboard_sounds::Entity::find_by_id(id).one(db).await?; - Ok(row.and_then(|sound| sound.object_key)) -} - -pub async fn delete_sound(db: &DatabaseConnection, id: Uuid) -> Result<()> { - soundboard_sounds::Entity::delete_by_id(id).exec(db).await?; - Ok(()) -} - fn map_sound(model: soundboard_sounds::Model) -> SoundboardSound { SoundboardSound { id: model.id, guild_id: model.guild_id, name: model.name, icon: model.icon, - media_url: model.media_url, - mime_type: model.mime_type, - size_bytes: model.size_bytes, + file_path: model.file_path, created_by_user_id: model.created_by_user_id, created_at: model.created_at, } } - -#[cfg(test)] -mod tests { - use super::{create_guild, create_sound, join_invite, list_visible_user_ids, validate_invite}; - use crate::entity::{direct_messages, guild_members, guilds, invites, soundboard_sounds}; - use chrono::{Duration, Utc}; - use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Value}; - use std::collections::{BTreeMap, BTreeSet}; - use uuid::Uuid; - - #[tokio::test] - async fn visible_users_include_self_shared_guilds_and_dm_partners() { - let current_user_id = Uuid::new_v4(); - let guild_a = Uuid::new_v4(); - let guild_b = Uuid::new_v4(); - let guild_peer = Uuid::new_v4(); - let shared_dm_peer = Uuid::new_v4(); - - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![ - BTreeMap::from([("guild_id".to_string(), Value::from(guild_a))]), - BTreeMap::from([("guild_id".to_string(), Value::from(guild_b))]), - ]]) - .append_query_results([vec![ - guild_members::Model { - guild_id: guild_a, - user_id: current_user_id, - created_at: Utc::now().fixed_offset(), - }, - guild_members::Model { - guild_id: guild_b, - user_id: current_user_id, - created_at: Utc::now().fixed_offset(), - }, - guild_members::Model { - guild_id: guild_a, - user_id: guild_peer, - created_at: Utc::now().fixed_offset(), - }, - ]]) - .append_query_results([vec![ - direct_messages::Model { - id: Uuid::new_v4(), - sender_user_id: current_user_id, - recipient_user_id: shared_dm_peer, - body: "hello".to_string(), - created_at: Utc::now().fixed_offset(), - }, - direct_messages::Model { - id: Uuid::new_v4(), - sender_user_id: shared_dm_peer, - recipient_user_id: current_user_id, - body: "hi".to_string(), - created_at: Utc::now().fixed_offset(), - }, - ]]) - .into_connection(); - - let visible = list_visible_user_ids(&db, current_user_id).await.unwrap(); - let visible: BTreeSet<_> = visible.into_iter().collect(); - - assert_eq!( - visible, - BTreeSet::from([current_user_id, guild_peer, shared_dm_peer]) - ); - } - - #[tokio::test] - async fn visible_users_returns_self_when_no_relationships_exist() { - let current_user_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([Vec::>::new()]) - .append_query_results([Vec::::new()]) - .into_connection(); - - let visible = list_visible_user_ids(&db, current_user_id).await.unwrap(); - - assert_eq!(visible, vec![current_user_id]); - } - - #[test] - fn validate_invite_rejects_expired_invites() { - let invite = invites::Model { - code: "expired".to_string(), - guild_id: Uuid::new_v4(), - created_by_user_id: Uuid::new_v4(), - created_at: Utc::now().fixed_offset(), - expires_at: Some((Utc::now() - Duration::minutes(1)).fixed_offset()), - max_uses: Some(5), - use_count: 0, - }; - - let err = validate_invite(&invite).unwrap_err(); - assert!(err.to_string().contains("invite expired")); - } - - #[test] - fn validate_invite_rejects_exhausted_invites() { - let invite = invites::Model { - code: "used".to_string(), - guild_id: Uuid::new_v4(), - created_by_user_id: Uuid::new_v4(), - created_at: Utc::now().fixed_offset(), - expires_at: Some((Utc::now() + Duration::minutes(5)).fixed_offset()), - max_uses: Some(1), - use_count: 1, - }; - - let err = validate_invite(&invite).unwrap_err(); - assert!(err.to_string().contains("invite exhausted")); - } - - #[test] - fn validate_invite_accepts_active_invites() { - let invite = invites::Model { - code: "active".to_string(), - guild_id: Uuid::new_v4(), - created_by_user_id: Uuid::new_v4(), - created_at: Utc::now().fixed_offset(), - expires_at: Some((Utc::now() + Duration::minutes(5)).fixed_offset()), - max_uses: Some(3), - use_count: 1, - }; - - validate_invite(&invite).unwrap(); - } - - #[tokio::test] - async fn create_guild_runs_guild_and_membership_in_one_transaction() { - let owner_user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let created_at = Utc::now().fixed_offset(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![guilds::Model { - id: guild_id, - name: "Guild".to_string(), - owner_user_id, - created_at, - }]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .into_connection(); - - let guild = create_guild(&db, owner_user_id, "Guild").await.unwrap(); - let transaction_log = format!("{:?}", db.into_transaction_log()); - - assert_eq!(guild.id, guild_id); - assert!(transaction_log.contains("BEGIN"), "{transaction_log}"); - assert!(transaction_log.contains("guilds"), "{transaction_log}"); - assert!( - transaction_log.contains("guild_members"), - "{transaction_log}" - ); - assert!(transaction_log.contains("COMMIT"), "{transaction_log}"); - - let guild_insert = transaction_log.find("guilds"); - let membership_insert = transaction_log.find("guild_members"); - assert!(guild_insert.is_some() && membership_insert.is_some()); - assert!(guild_insert.unwrap() < membership_insert.unwrap()); - } - - #[tokio::test] - async fn join_invite_locks_invite_and_updates_use_count_in_one_transaction() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let created_by_user_id = Uuid::new_v4(); - let created_at = Utc::now().fixed_offset(); - let invite = invites::Model { - code: "invite123".to_string(), - guild_id, - created_by_user_id, - created_at, - expires_at: Some((Utc::now() + Duration::minutes(5)).fixed_offset()), - max_uses: Some(5), - use_count: 0, - }; - let updated_invite = invites::Model { - use_count: 1, - ..invite.clone() - }; - let guild = guilds::Model { - id: guild_id, - name: "Guild".to_string(), - owner_user_id: created_by_user_id, - created_at, - }; - - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![invite]]) - .append_query_results([Vec::::new()]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![updated_invite]]) - .append_query_results([vec![guild]]) - .into_connection(); - - let joined_guild = join_invite(&db, "invite123", user_id).await.unwrap(); - let transaction_log = format!("{:?}", db.into_transaction_log()); - - assert_eq!(joined_guild.id, guild_id); - assert!(transaction_log.contains("BEGIN"), "{transaction_log}"); - assert!(transaction_log.contains("FOR UPDATE"), "{transaction_log}"); - assert!( - transaction_log.contains("guild_members"), - "{transaction_log}" - ); - assert!(transaction_log.contains("UPDATE"), "{transaction_log}"); - assert!(transaction_log.contains("invites"), "{transaction_log}"); - assert!(transaction_log.contains("COMMIT"), "{transaction_log}"); - } - - #[tokio::test] - async fn join_invite_does_not_increment_use_count_for_existing_member() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let created_by_user_id = Uuid::new_v4(); - let created_at = Utc::now().fixed_offset(); - let invite = invites::Model { - code: "invite123".to_string(), - guild_id, - created_by_user_id, - created_at, - expires_at: Some((Utc::now() + Duration::minutes(5)).fixed_offset()), - max_uses: Some(5), - use_count: 3, - }; - let existing_member = guild_members::Model { - guild_id, - user_id, - created_at, - }; - let guild = guilds::Model { - id: guild_id, - name: "Guild".to_string(), - owner_user_id: created_by_user_id, - created_at, - }; - - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![invite]]) - .append_query_results([vec![existing_member]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![guild]]) - .into_connection(); - - let joined_guild = join_invite(&db, "invite123", user_id).await.unwrap(); - let transaction_log = format!("{:?}", db.into_transaction_log()); - - assert_eq!(joined_guild.id, guild_id); - assert!(transaction_log.contains("FOR UPDATE"), "{transaction_log}"); - assert!( - transaction_log.contains("guild_members"), - "{transaction_log}" - ); - assert!(transaction_log.contains("COMMIT"), "{transaction_log}"); - assert!( - !transaction_log.contains("UPDATE \"invites\""), - "{transaction_log}" - ); - } - - #[tokio::test] - async fn create_sound_keeps_legacy_file_path_populated() { - let guild_id = Uuid::new_v4(); - let sound_id = Uuid::new_v4(); - let created_by_user_id = Uuid::new_v4(); - let created_at = Utc::now().fixed_offset(); - let media_url = "https://media.example.com/soundboard/test.mp3"; - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![soundboard_sounds::Model { - id: sound_id, - guild_id, - name: "Airhorn".to_string(), - icon: "AH".to_string(), - object_key: Some("soundboard/test.mp3".to_string()), - media_url: media_url.to_string(), - mime_type: Some("audio/mpeg".to_string()), - size_bytes: Some(1234), - file_path: Some(media_url.to_string()), - created_by_user_id, - created_at, - }]]) - .into_connection(); - - let sound = create_sound( - &db, - guild_id, - created_by_user_id, - "Airhorn", - "AH", - "soundboard/test.mp3", - media_url, - "audio/mpeg", - 1234, - ) - .await - .unwrap(); - let transaction_log = format!("{:?}", db.into_transaction_log()); - - assert_eq!(sound.id, sound_id); - assert!(transaction_log.contains("file_path"), "{transaction_log}"); - assert!(transaction_log.contains(media_url), "{transaction_log}"); - } -} diff --git a/src/entity/attachments.rs b/src/entity/attachments.rs deleted file mode 100644 index f5581d1..0000000 --- a/src/entity/attachments.rs +++ /dev/null @@ -1,66 +0,0 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] -#[sea_orm(table_name = "attachments")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub id: Uuid, - pub channel_message_id: Option, - pub direct_message_id: Option, - pub uploader_user_id: Uuid, - pub object_key: String, - pub media_url: String, - pub mime_type: String, - pub size_bytes: i64, - pub original_filename: String, - pub created_at: DateTimeWithTimeZone, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::messages::Entity", - from = "Column::ChannelMessageId", - to = "super::messages::Column::Id", - on_update = "NoAction", - on_delete = "Cascade" - )] - Messages, - #[sea_orm( - belongs_to = "super::direct_messages::Entity", - from = "Column::DirectMessageId", - to = "super::direct_messages::Column::Id", - on_update = "NoAction", - on_delete = "Cascade" - )] - DirectMessages, - #[sea_orm( - belongs_to = "super::users::Entity", - from = "Column::UploaderUserId", - to = "super::users::Column::Id", - on_update = "NoAction", - on_delete = "NoAction" - )] - Users, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Messages.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::DirectMessages.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Users.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 0dcd3c5..d1e64f7 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -1,10 +1,8 @@ -pub mod attachments; pub mod channels; pub mod direct_messages; pub mod guild_members; pub mod guilds; pub mod invites; pub mod messages; -pub mod sessions; pub mod soundboard_sounds; pub mod users; diff --git a/src/entity/sessions.rs b/src/entity/sessions.rs deleted file mode 100644 index 7d66a24..0000000 --- a/src/entity/sessions.rs +++ /dev/null @@ -1,35 +0,0 @@ -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "sessions")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub id: String, - pub user_id: Uuid, - pub expires_at: DateTimeWithTimeZone, - pub created_at: DateTimeWithTimeZone, - pub last_seen_at: DateTimeWithTimeZone, - pub revoked_at: Option, - pub user_agent_hash: Option, - pub ip_hash: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::users::Entity", - from = "Column::UserId", - to = "super::users::Column::Id", - on_update = "NoAction", - on_delete = "Cascade" - )] - Users, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Users.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entity/soundboard_sounds.rs b/src/entity/soundboard_sounds.rs index bd96feb..6e5a23f 100644 --- a/src/entity/soundboard_sounds.rs +++ b/src/entity/soundboard_sounds.rs @@ -9,11 +9,7 @@ pub struct Model { pub guild_id: Uuid, pub name: String, pub icon: String, - pub object_key: Option, - pub media_url: String, - pub mime_type: Option, - pub size_bytes: Option, - pub file_path: Option, + pub file_path: String, pub created_by_user_id: Uuid, pub created_at: DateTimeWithTimeZone, } diff --git a/src/handlers.rs b/src/handlers.rs index f9675d9..788688d 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,12 +1,10 @@ use axum::{ Json, Router, - body::Bytes, - extract::{DefaultBodyLimit, Path, Query, State, WebSocketUpgrade}, + extract::{Path, Query, State, WebSocketUpgrade}, http::{HeaderMap, StatusCode, header}, response::{Html, IntoResponse, Redirect}, routing::{get, post}, }; -use chrono::Datelike; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -14,7 +12,7 @@ use crate::{ AppState, auth::{self, ApiError, AuthUser}, chat, db, - models::{DmMessageWithAuthor, Guild, MessageWithAuthor, SoundboardSound}, + models::{Guild, SoundboardSound}, voice, }; @@ -30,11 +28,6 @@ pub fn routes() -> Router { "/dms/{other_user_id}/messages", get(list_dm_messages).post(send_dm_message), ) - .route( - "/dms/{other_user_id}/attachments", - post(upload_dm_attachment), - ) - .layer(DefaultBodyLimit::max(MAX_MEDIA_UPLOAD_BYTES)) .route("/presence", get(presence_list)) .route("/rtc-config", get(rtc_config)) .route("/guilds", get(list_guilds).post(create_guild)) @@ -50,21 +43,11 @@ pub fn routes() -> Router { "/guilds/{guild_id}/sounds", get(list_sounds).post(upload_sound), ) - .layer(DefaultBodyLimit::max(MAX_MEDIA_UPLOAD_BYTES)) - .route( - "/guilds/{guild_id}/sounds/{sound_id}", - post(delete_sound_post).delete(delete_sound), - ) .route("/channels", post(create_channel)) .route( "/channels/{channel_id}/messages", get(list_messages).post(send_message), ) - .route( - "/channels/{channel_id}/attachments", - post(upload_channel_attachment), - ) - .layer(DefaultBodyLimit::max(MAX_MEDIA_UPLOAD_BYTES)) .route("/channels/{channel_id}/voice/ws", get(voice_ws)) .route("/ws", get(chat_ws)) } @@ -92,7 +75,7 @@ async fn auth_login(State(state): State) -> Result, headers: HeaderMap, ) -> Result { - auth::validate_oauth_state( - auth::read_cookie_from_headers(&headers, auth::OAUTH_STATE_COOKIE), - &query.state, - ) - .map_err(|e| ApiError::bad_request(&e.to_string()))?; + auth::validate_oauth_state(auth::read_oauth_state_from_headers(&headers), &query.state) + .map_err(|e| ApiError::bad_request(&e.to_string()))?; let token_res = state .http @@ -228,51 +208,40 @@ async fn auth_callback( .await .map_err(|e| ApiError::internal(&format!("failed to persist user: {e}")))?; - let session_id = auth::new_session_id(); - db::create_session( - &state.db, - &session_id, + let session_cookie = auth::new_session_cookie( user.id, - auth::session_expiry(), - auth::user_agent_hash(&headers), - auth::ip_hash(&headers), + &state.settings.session_secret, + state.settings.cookie_secure, ) - .await - .map_err(|e| ApiError::internal(&format!("failed to create session: {e}")))?; + .map_err(|e| ApiError::internal(&e.to_string()))?; let mut headers = HeaderMap::new(); headers.append( header::SET_COOKIE, - auth::clear_oauth_state_cookie(state.settings.session_cookie_secure) + session_cookie .parse() - .map_err(|_| ApiError::internal("failed to clear oauth state cookie"))?, + .map_err(|_| ApiError::internal("failed to set session cookie"))?, ); headers.append( header::SET_COOKIE, - auth::make_session_cookie(&session_id, state.settings.session_cookie_secure) + auth::clear_oauth_state_cookie(state.settings.cookie_secure) .parse() - .map_err(|_| ApiError::internal("failed to set session cookie"))?, + .map_err(|_| ApiError::internal("failed to clear oauth state cookie"))?, ); Ok((headers, Redirect::to("/"))) } -async fn auth_logout( - State(state): State, - user: AuthUser, -) -> Result { - db::revoke_session(&state.db, &user.session_id) - .await - .map_err(|e| ApiError::internal(&format!("failed to revoke session: {e}")))?; - +async fn auth_logout(State(state): State) -> Result { let mut headers = HeaderMap::new(); - headers.insert( + headers.append( header::SET_COOKIE, - auth::clear_session_cookie(state.settings.session_cookie_secure) + auth::clear_session_cookie(state.settings.cookie_secure) .parse() .map_err(|_| ApiError::internal("failed to clear session cookie"))?, ); - Ok((headers, StatusCode::NO_CONTENT)) + + Ok((StatusCode::NO_CONTENT, headers)) } #[derive(Serialize)] @@ -333,12 +302,9 @@ async fn me(State(state): State, user: AuthUser) -> Result, - user: AuthUser, + _user: AuthUser, ) -> Result { - let visible_user_ids = db::list_visible_user_ids(&state.db, user.id) - .await - .map_err(|e| ApiError::internal(&format!("failed to load visible users: {e}")))?; - let users = state.chat.get_online_users_for(&visible_user_ids).await; + let users = state.chat.get_online_users().await; Ok(Json(users)) } @@ -679,101 +645,11 @@ async fn send_dm_message( Ok((StatusCode::CREATED, Json(SendMessageResponse { ok: true }))) } -async fn upload_channel_attachment( - State(state): State, - user: AuthUser, - Path(channel_id): Path, - headers: HeaderMap, - body: Bytes, -) -> Result { - ensure_channel_member(&state, channel_id, user.id).await?; - let uploaded = upload_media_from_request( - &state, - &headers, - body, - channel_object_key_prefix(channel_id), - ) - .await?; - - let message = db::create_message_with_attachment( - &state.db, - channel_id, - user.id, - "", - &uploaded.object_key, - &uploaded.media_url, - &uploaded.mime_type, - uploaded.size_bytes, - &uploaded.original_filename, - ) - .await - .map_err(|e| ApiError::internal(&format!("failed to create message attachment: {e}")))?; - - let guild_id = db::guild_id_for_channel(&state.db, channel_id) - .await - .map_err(|e| ApiError::internal(&format!("channel lookup failed: {e}")))? - .ok_or_else(|| ApiError::internal("channel not found after attachment upload"))?; - - let members = db::list_guild_member_ids(&state.db, guild_id) - .await - .map_err(|e| ApiError::internal(&format!("failed to list guild members: {e}")))?; - - broadcast_channel_message(&state, members, channel_id, &message).await; - Ok((StatusCode::CREATED, Json(message))) -} - -async fn upload_dm_attachment( - State(state): State, - user: AuthUser, - Path(other_user_id): Path, - headers: HeaderMap, - body: Bytes, -) -> Result { - if other_user_id == user.id { - return Err(ApiError::bad_request("cannot send dm to yourself")); - } - - let other_user_exists = db::user_exists(&state.db, other_user_id) - .await - .map_err(|e| ApiError::internal(&format!("user check failed: {e}")))?; - if !other_user_exists { - return Err(ApiError { - status: StatusCode::NOT_FOUND, - message: "user not found".to_string(), - }); - } - - let uploaded = upload_media_from_request( - &state, - &headers, - body, - dm_object_key_prefix(user.id, other_user_id), - ) - .await?; - - let message = db::create_direct_message_with_attachment( - &state.db, - user.id, - other_user_id, - "", - &uploaded.object_key, - &uploaded.media_url, - &uploaded.mime_type, - uploaded.size_bytes, - &uploaded.original_filename, - ) - .await - .map_err(|e| ApiError::internal(&format!("failed to create dm attachment: {e}")))?; - - broadcast_dm_message(&state, user.id, other_user_id, &message).await; - Ok((StatusCode::CREATED, Json(message))) -} - async fn voice_ws( + ws: WebSocketUpgrade, State(state): State, user: AuthUser, Path(channel_id): Path, - ws: WebSocketUpgrade, ) -> Result { ensure_channel_member(&state, channel_id, user.id).await?; @@ -795,9 +671,9 @@ async fn voice_ws( } async fn chat_ws( + ws: WebSocketUpgrade, State(state): State, user: AuthUser, - ws: WebSocketUpgrade, ) -> Result { Ok(ws.on_upgrade(move |socket| chat::handle_socket(state, socket, user.id))) } @@ -861,402 +737,26 @@ async fn upload_sound( _ => return Err(ApiError::bad_request("missing fields")), }; - let validated = validate_sound_upload(&file_name, &file_data) - .map_err(|e| ApiError::bad_request(&e.to_string()))?; - - let storage = state - .media - .as_ref() - .ok_or_else(|| ApiError::service_unavailable("media storage is not configured"))?; - - let object_key = format!( - "soundboard/{guild_id}/{}-{}", - Uuid::new_v4(), - sanitize_file_name(&file_name) - ); - let media_url = storage - .upload_object( - &object_key, - file_data.to_vec(), - &validated.mime_type, - &file_name, - false, - ) - .await - .map_err(|e| ApiError::internal(&e.to_string()))?; - - let sound = db::create_sound( - &state.db, - guild_id, - user.id, - name.trim(), - icon.trim(), - &object_key, - &media_url, - &validated.mime_type, - validated.size_bytes, - ) - .await?; - Ok(Json(sound)) -} - -const MAX_MEDIA_UPLOAD_BYTES: usize = 50 * 1024 * 1024; - -struct UploadedMedia { - object_key: String, - media_url: String, - mime_type: String, - size_bytes: i64, - original_filename: String, -} - -#[derive(Debug)] -struct ValidatedUpload { - mime_type: String, - inline: bool, - size_bytes: i64, -} - -async fn upload_media_from_request( - state: &AppState, - headers: &HeaderMap, - body: Bytes, - object_key_prefix: String, -) -> Result { - let storage = state - .media - .as_ref() - .ok_or_else(|| ApiError::service_unavailable("media storage is not configured"))?; - - let original_filename = headers - .get("x-file-name") - .and_then(|v| v.to_str().ok()) - .map(str::trim) - .filter(|v| !v.is_empty()) - .map(ToString::to_string) - .ok_or_else(|| ApiError::bad_request("missing x-file-name header"))?; - - let raw_mime_type = headers - .get(header::CONTENT_TYPE) - .and_then(|v| v.to_str().ok()) - .map(str::trim) - .filter(|v| !v.is_empty()) - .unwrap_or("application/octet-stream") - .to_string(); - - if body.is_empty() { - return Err(ApiError::bad_request("missing file upload")); - } - if body.len() > MAX_MEDIA_UPLOAD_BYTES { - return Err(ApiError::bad_request("file exceeds 50MB upload limit")); - } - - let validated = validate_general_upload(&original_filename, &body, &raw_mime_type) - .map_err(|e| ApiError::bad_request(&e.to_string()))?; - let safe_name = sanitize_file_name(&original_filename); - let object_key = format!("{}/{}-{}", object_key_prefix, Uuid::new_v4(), safe_name); - let size_bytes = body.len() as i64; - let media_url = storage - .upload_object( - &object_key, - body.to_vec(), - &validated.mime_type, - &original_filename, - validated.inline, - ) - .await - .map_err(|e| ApiError::internal(&e.to_string()))?; - - Ok(UploadedMedia { - object_key, - media_url, - mime_type: validated.mime_type, - size_bytes, - original_filename, - }) -} - -async fn broadcast_channel_message( - state: &AppState, - members: Vec, - channel_id: Uuid, - message: &MessageWithAuthor, -) { - state - .chat - .broadcast_to_many( - members, - chat::ServerEvent::MessageCreated { - channel_id, - message: serde_json::to_value(message).unwrap_or_default(), - }, - ) - .await; -} - -async fn broadcast_dm_message( - state: &AppState, - current_user_id: Uuid, - other_user_id: Uuid, - message: &DmMessageWithAuthor, -) { - state - .chat - .broadcast_to_user( - current_user_id, - chat::ServerEvent::DmCreated { - other_user_id, - message: serde_json::to_value(message).unwrap_or_default(), - }, - ) - .await; - state - .chat - .broadcast_to_user( - other_user_id, - chat::ServerEvent::DmCreated { - other_user_id: current_user_id, - message: serde_json::to_value(message).unwrap_or_default(), - }, - ) - .await; -} - -fn sanitize_file_name(file_name: &str) -> String { - let sanitized: String = file_name - .chars() - .map(|ch| { - if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_') { - ch - } else { - '_' - } - }) - .collect(); - - let trimmed = sanitized.trim_matches(|ch| ch == '_' || ch == '.').trim(); - if trimmed.is_empty() { - "upload.bin".to_string() - } else { - trimmed.to_string() - } -} - -fn validate_general_upload( - file_name: &str, - bytes: &[u8], - claimed_mime: &str, -) -> anyhow::Result { - let extension = lower_file_extension(file_name) - .ok_or_else(|| anyhow::anyhow!("file extension is required"))?; - let normalized_mime = sniff_upload_type(bytes, &extension, claimed_mime) - .ok_or_else(|| anyhow::anyhow!("unsupported upload type"))?; - let inline = is_inline_media_type(&normalized_mime); - - Ok(ValidatedUpload { - mime_type: normalized_mime, - inline, - size_bytes: bytes.len() as i64, - }) -} - -fn validate_sound_upload(file_name: &str, bytes: &[u8]) -> anyhow::Result { - let extension = lower_file_extension(file_name) - .ok_or_else(|| anyhow::anyhow!("sound file extension is required"))?; - let Some(mime_type) = sniff_audio_type(bytes, &extension) else { - return Err(anyhow::anyhow!( - "only mp3, ogg, and wav sounds are supported" - )); - }; - - Ok(ValidatedUpload { - mime_type, - inline: false, - size_bytes: bytes.len() as i64, - }) -} - -fn lower_file_extension(file_name: &str) -> Option { - std::path::Path::new(file_name) + let extension = std::path::Path::new(&file_name) .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.to_ascii_lowercase()) -} + .and_then(|e| e.to_str()) + .unwrap_or("mp3"); -fn sniff_upload_type(bytes: &[u8], extension: &str, claimed_mime: &str) -> Option { - sniff_audio_type(bytes, extension) - .or_else(|| sniff_image_type(bytes, extension)) - .or_else(|| sniff_video_type(bytes, extension)) - .or_else(|| sniff_document_type(extension, claimed_mime)) -} - -fn sniff_audio_type(bytes: &[u8], extension: &str) -> Option { - match extension { - "mp3" if bytes.starts_with(b"ID3") || bytes.first().copied() == Some(0xff) => { - Some("audio/mpeg".to_string()) - } - "ogg" if bytes.starts_with(b"OggS") => Some("audio/ogg".to_string()), - "wav" if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WAVE") => { - Some("audio/wav".to_string()) - } - _ => None, - } -} - -fn sniff_image_type(bytes: &[u8], extension: &str) -> Option { - match extension { - "png" if bytes.starts_with(b"\x89PNG\r\n\x1a\n") => Some("image/png".to_string()), - "jpg" | "jpeg" if bytes.starts_with(&[0xff, 0xd8, 0xff]) => Some("image/jpeg".to_string()), - "gif" if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") => { - Some("image/gif".to_string()) - } - "webp" if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WEBP") => { - Some("image/webp".to_string()) - } - _ => None, - } -} - -fn sniff_video_type(bytes: &[u8], extension: &str) -> Option { - match extension { - "mp4" - if bytes - .windows(8) - .any(|window| window == b"ftypisom" || window == b"ftypmp42") => - { - Some("video/mp4".to_string()) - } - "webm" if bytes.starts_with(&[0x1a, 0x45, 0xdf, 0xa3]) => Some("video/webm".to_string()), - _ => None, - } -} - -fn sniff_document_type(extension: &str, claimed_mime: &str) -> Option { - match extension { - "pdf" => Some("application/pdf".to_string()), - "txt" => Some("text/plain".to_string()), - "json" => Some("application/json".to_string()), - "csv" => Some("text/csv".to_string()), - "md" => Some("text/markdown".to_string()), - "zip" => Some("application/zip".to_string()), - "gz" => Some("application/gzip".to_string()), - _ if matches!( - claimed_mime, - "application/pdf" - | "text/plain" - | "application/json" - | "text/csv" - | "text/markdown" - | "application/zip" - | "application/gzip" - ) => - { - Some(claimed_mime.to_string()) - } - _ => None, - } -} - -fn is_inline_media_type(mime_type: &str) -> bool { - matches!( - mime_type, - "image/png" - | "image/jpeg" - | "image/gif" - | "image/webp" - | "video/mp4" - | "video/webm" - | "audio/mpeg" - | "audio/ogg" - | "audio/wav" - | "application/pdf" - | "text/plain" - | "application/json" - | "text/csv" - | "text/markdown" - ) -} - -fn channel_object_key_prefix(channel_id: Uuid) -> String { - let now = chrono::Utc::now(); - format!( - "channels/{}/{:04}/{:02}", - channel_id, - now.year(), - now.month() - ) -} - -fn dm_object_key_prefix(user_a: Uuid, user_b: Uuid) -> String { - let now = chrono::Utc::now(); - let (left, right) = if user_a <= user_b { - (user_a, user_b) - } else { - (user_b, user_a) - }; - format!( - "dms/{}-{}/{:04}/{:02}", - left, - right, - now.year(), - now.month() - ) -} - -async fn delete_sound_post( - state: State, - user: AuthUser, - path: Path<(Uuid, Uuid)>, -) -> Result { - delete_sound(state, user, path).await -} - -async fn delete_sound( - State(state): State, - user: AuthUser, - Path((guild_id, sound_id)): Path<(Uuid, Uuid)>, -) -> Result { - ensure_guild_member(&state, guild_id, user.id).await?; - - let sound = db::get_sound_by_id(&state.db, sound_id) - .await - .map_err(|e| ApiError::internal(&e.to_string()))? - .ok_or_else(|| ApiError { - status: StatusCode::NOT_FOUND, - message: "sound not found".to_string(), - })?; - - if sound.guild_id != guild_id { - return Err(ApiError::bad_request("sound does not belong to this guild")); - } - - let is_owner = db::is_guild_owner(&state.db, guild_id, user.id) + let safe_file_name = format!("{}.{}", Uuid::new_v4(), extension); + let upload_dir = std::path::Path::new("static/uploads/soundboard"); + tokio::fs::create_dir_all(upload_dir) .await .map_err(|e| ApiError::internal(&e.to_string()))?; - if sound.created_by_user_id != user.id && !is_owner { - return Err(ApiError { - status: StatusCode::FORBIDDEN, - message: "you do not have permission to delete this sound".to_string(), - }); - } - - let object_key = db::get_sound_object_key(&state.db, sound_id) + let file_path = upload_dir.join(&safe_file_name); + tokio::fs::write(&file_path, file_data) .await - .map_err(|e| ApiError::internal(&format!("failed to load sound storage key: {e}")))?; + .map_err(|e| ApiError::internal(&e.to_string()))?; - if let (Some(storage), Some(object_key)) = (state.media.as_ref(), object_key.as_deref()) { - storage - .delete_object(object_key) - .await - .map_err(|e| ApiError::internal(&format!("failed to delete sound media: {e}")))?; - } + let web_path = format!("/static/uploads/soundboard/{}", safe_file_name); - db::delete_sound(&state.db, sound_id) - .await - .map_err(|e| ApiError::internal(&format!("failed to delete sound record: {e}")))?; - - Ok(StatusCode::NO_CONTENT) + let sound = db::create_sound(&state.db, guild_id, user.id, &name, &icon, &web_path).await?; + Ok(Json(sound)) } async fn ensure_guild_member( @@ -1293,100 +793,3 @@ async fn ensure_channel_member( ensure_guild_member(state, guild_id, user_id).await } - -#[cfg(test)] -mod tests { - use super::{ - sanitize_file_name, upload_media_from_request, validate_general_upload, - validate_sound_upload, - }; - use crate::{AppState, chat::ChatHub, config::Settings, voice::VoiceHub}; - use axum::{ - body::Bytes, - http::{HeaderMap, StatusCode, header}, - }; - use sea_orm::{DatabaseBackend, MockDatabase}; - use std::sync::Arc; - - fn test_state() -> AppState { - AppState { - db: Arc::new(MockDatabase::new(DatabaseBackend::Postgres).into_connection()), - settings: Arc::new(Settings { - port: 3000, - app_base_url: "http://localhost:3000".to_string(), - app_origin: "http://localhost:3000".to_string(), - database_url: "postgres://localhost/test".to_string(), - oidc_client_id: "client".to_string(), - oidc_client_secret: "secret".to_string(), - oidc_authorize_url: "http://localhost:3000/oidc/authorize".to_string(), - oidc_token_url: "http://localhost:3000/oidc/token".to_string(), - oidc_userinfo_url: "http://localhost:3000/oidc/userinfo".to_string(), - oidc_redirect_url: "http://localhost:3000/auth/callback".to_string(), - oidc_scopes: "openid profile email".to_string(), - session_cookie_secure: false, - stun_urls: vec!["stun:stun.l.google.com:19302".to_string()], - turn_urls: Vec::new(), - turn_username: None, - turn_password: None, - media: None, - }), - http: reqwest::Client::new(), - voice: Arc::new(VoiceHub::default()), - chat: Arc::new(ChatHub::default()), - media: None, - } - } - - #[test] - fn sanitize_file_name_replaces_unsafe_chars() { - assert_eq!( - sanitize_file_name("../../hello world?.mp3"), - "hello_world_.mp3" - ); - } - - #[test] - fn validates_png_uploads() { - let png = b"\x89PNG\r\n\x1a\nrest"; - let upload = validate_general_upload("image.png", png, "image/png").unwrap(); - assert_eq!(upload.mime_type, "image/png"); - assert!(upload.inline); - } - - #[test] - fn rejects_unknown_uploads() { - let err = validate_general_upload("payload.exe", b"MZ...", "application/octet-stream") - .unwrap_err(); - assert!(err.to_string().contains("unsupported upload type")); - } - - #[test] - fn validates_sound_uploads() { - let wav = b"RIFFdataWAVE"; - let upload = validate_sound_upload("sound.wav", wav).unwrap(); - assert_eq!(upload.mime_type, "audio/wav"); - } - - #[tokio::test] - async fn upload_helper_requires_configured_media_storage() { - let state = test_state(); - let mut headers = HeaderMap::new(); - headers.insert("x-file-name", "photo.png".parse().unwrap()); - headers.insert(header::CONTENT_TYPE, "image/png".parse().unwrap()); - - let err = match upload_media_from_request( - &state, - &headers, - Bytes::from_static(b"\x89PNG\r\n\x1a\nrest"), - "channels/test".to_string(), - ) - .await - { - Ok(_) => panic!("expected upload helper to reject missing media storage"), - Err(err) => err, - }; - - assert_eq!(err.status, StatusCode::SERVICE_UNAVAILABLE); - assert_eq!(err.message, "media storage is not configured"); - } -} diff --git a/src/main.rs b/src/main.rs index a6e3c34..53ea6f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ mod config; mod db; mod entity; mod handlers; -mod media; mod migration; mod models; mod voice; @@ -12,32 +11,21 @@ mod voice; use std::{net::SocketAddr, sync::Arc}; use anyhow::Context; -use axum::{ - Router, - extract::{Request, State}, - http::{HeaderValue, Method, header}, - middleware::{Next, from_fn_with_state}, - response::Response, - routing::get, -}; +use axum::{Router, routing::get}; use sea_orm::Database; use sea_orm_migration::MigratorTrait; use tower_http::{services::ServeDir, trace::TraceLayer}; use tracing::info; -use crate::{ - auth::ApiError, config::Settings, handlers::routes, media::MediaStorage, migration::Migrator, - voice::VoiceHub, -}; +use crate::{config::Settings, handlers::routes, migration::Migrator, voice::VoiceHub}; #[derive(Clone)] pub struct AppState { - pub db: Arc, + pub db: sea_orm::DatabaseConnection, pub settings: Arc, pub http: reqwest::Client, pub voice: Arc, pub chat: Arc, - pub media: Option>, } #[tokio::main] @@ -54,25 +42,22 @@ async fn main() -> anyhow::Result<()> { Migrator::up(&db, None) .await .with_context(|| "failed to run migrations")?; - db::cleanup_sessions(&db) - .await - .with_context(|| "failed to clean expired sessions")?; - - let media = match settings.media.as_ref() { - Some(media_settings) => Some(Arc::new(MediaStorage::new(media_settings).await?)), - None => None, - }; let state = AppState { - db: Arc::new(db), + db, settings, http: reqwest::Client::new(), voice: Arc::new(VoiceHub::default()), chat: Arc::new(chat::ChatHub::default()), - media, }; let port = state.settings.port; - let app = build_app(state); + + let app = Router::new() + .route("/health", get(handlers::health)) + .nest_service("/static", ServeDir::new("static")) + .merge(routes()) + .with_state(state) + .layer(TraceLayer::new_for_http()); let addr = SocketAddr::from(([0, 0, 0, 0], port)); info!(%addr, "server started"); @@ -88,1092 +73,3 @@ fn init_tracing() { tracing_subscriber::fmt().with_env_filter(filter).init(); } - -fn build_app(state: AppState) -> Router { - let settings = Arc::clone(&state.settings); - Router::new() - .route("/health", get(handlers::health)) - .nest_service("/static", ServeDir::new("static")) - .merge(routes()) - .with_state(state) - .layer(from_fn_with_state( - Arc::clone(&settings), - add_security_headers, - )) - .layer(from_fn_with_state( - Arc::clone(&settings), - require_same_origin, - )) - .layer(TraceLayer::new_for_http()) -} - -async fn require_same_origin( - State(settings): State>, - request: Request, - next: Next, -) -> Result { - let requires_origin = matches!(*request.method(), Method::POST | Method::DELETE) - || is_websocket_upgrade(&request); - - if requires_origin && !auth::origin_matches(request.headers(), &settings.app_origin) { - return Err(ApiError::forbidden("cross-origin request rejected")); - } - - Ok(next.run(request).await) -} - -async fn add_security_headers( - State(settings): State>, - request: Request, - next: Next, -) -> Response { - let mut response = next.run(request).await; - let csp = build_csp(&settings); - - if let Ok(value) = HeaderValue::from_str(&csp) { - response - .headers_mut() - .insert("content-security-policy", value); - } - response.headers_mut().insert( - header::X_CONTENT_TYPE_OPTIONS, - HeaderValue::from_static("nosniff"), - ); - response - .headers_mut() - .insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY")); - response.headers_mut().insert( - header::REFERRER_POLICY, - HeaderValue::from_static("same-origin"), - ); - response -} - -fn is_websocket_upgrade(request: &Request) -> bool { - request - .headers() - .get(header::UPGRADE) - .and_then(|value| value.to_str().ok()) - .map(|value| value.eq_ignore_ascii_case("websocket")) - .unwrap_or(false) -} - -fn build_csp(settings: &Settings) -> String { - let media_origin = settings - .media - .as_ref() - .map(|media| media.public_base_url.as_str()) - .unwrap_or("'self'"); - - format!( - "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: {media_origin}; media-src 'self' https: {media_origin}; connect-src 'self' ws: wss: https://api.klipy.com; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" - ) -} - -#[cfg(test)] -mod tests { - use super::{AppState, build_app, build_csp}; - use crate::{ - auth, chat, - config::{MediaSettings, Settings}, - entity::{guilds, sessions, soundboard_sounds, users}, - voice::VoiceHub, - }; - use axum::{ - body::Body, - http::{Method, Request, StatusCode, header}, - }; - use chrono::Utc; - use sea_orm::MockExecResult; - use sea_orm::{DatabaseBackend, MockDatabase, Value}; - use std::{collections::BTreeMap, sync::Arc}; - use tower::ServiceExt; - use uuid::Uuid; - - fn test_settings() -> Arc { - Arc::new(Settings { - port: 3000, - app_base_url: "http://localhost:3000".to_string(), - app_origin: "http://localhost:3000".to_string(), - database_url: "postgres://localhost/test".to_string(), - oidc_client_id: "client".to_string(), - oidc_client_secret: "secret".to_string(), - oidc_authorize_url: "http://localhost:3000/oidc/authorize".to_string(), - oidc_token_url: "http://localhost:3000/oidc/token".to_string(), - oidc_userinfo_url: "http://localhost:3000/oidc/userinfo".to_string(), - oidc_redirect_url: "http://localhost:3000/auth/callback".to_string(), - oidc_scopes: "openid profile email".to_string(), - session_cookie_secure: false, - stun_urls: vec!["stun:stun.l.google.com:19302".to_string()], - turn_urls: Vec::new(), - turn_username: None, - turn_password: None, - media: None, - }) - } - - fn build_state(db: sea_orm::DatabaseConnection) -> AppState { - AppState { - db: Arc::new(db), - settings: test_settings(), - http: reqwest::Client::new(), - voice: Arc::new(VoiceHub::default()), - chat: Arc::new(chat::ChatHub::default()), - media: None, - } - } - - fn build_state_with_settings( - db: sea_orm::DatabaseConnection, - settings: Arc, - ) -> AppState { - AppState { - db: Arc::new(db), - settings, - http: reqwest::Client::new(), - voice: Arc::new(VoiceHub::default()), - chat: Arc::new(chat::ChatHub::default()), - media: None, - } - } - - fn websocket_request(uri: &str) -> axum::http::request::Builder { - Request::builder() - .uri(uri) - .header(header::CONNECTION, "upgrade") - .header(header::UPGRADE, "websocket") - .header("sec-websocket-version", "13") - .header("sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ==") - } - - fn mock_session(session_id: &str, user_id: Uuid) -> sessions::Model { - let now = Utc::now().fixed_offset(); - sessions::Model { - id: session_id.to_string(), - user_id, - expires_at: (Utc::now() + chrono::Duration::days(1)).fixed_offset(), - created_at: now, - last_seen_at: now, - revoked_at: None, - user_agent_hash: None, - ip_hash: None, - } - } - - fn count_row(value: i64) -> BTreeMap { - BTreeMap::from([("num_items".to_string(), Value::from(value))]) - } - - fn guild_id_row(guild_id: Uuid) -> BTreeMap { - BTreeMap::from([("guild_id".to_string(), Value::from(guild_id))]) - } - - #[tokio::test] - async fn me_requires_valid_session_cookie() { - let user_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![sessions::Model { - id: "session-1".to_string(), - user_id, - expires_at: (Utc::now() + chrono::Duration::days(1)).fixed_offset(), - created_at: Utc::now().fixed_offset(), - last_seen_at: Utc::now().fixed_offset(), - revoked_at: None, - user_agent_hash: None, - ip_hash: None, - }]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![users::Model { - id: user_id, - oidc_sub: "oidc-sub".to_string(), - email: Some("user@example.com".to_string()), - display_name: "User".to_string(), - avatar_url: None, - created_at: Utc::now().fixed_offset(), - updated_at: Utc::now().fixed_offset(), - }]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .uri("/me") - .header( - header::COOKIE, - format!("{}=session-1", auth::SESSION_COOKIE), - ) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - } - - #[tokio::test] - async fn health_response_includes_security_headers() { - let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .uri("/health") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get("x-content-type-options").unwrap(), - "nosniff" - ); - assert_eq!(response.headers().get("x-frame-options").unwrap(), "DENY"); - assert_eq!( - response.headers().get("referrer-policy").unwrap(), - "same-origin" - ); - assert!(response.headers().contains_key("content-security-policy")); - } - - #[tokio::test] - async fn logout_revokes_session_and_clears_cookie() { - let user_id = Uuid::new_v4(); - let now = Utc::now().fixed_offset(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![sessions::Model { - id: "session-2".to_string(), - user_id, - expires_at: (Utc::now() + chrono::Duration::days(1)).fixed_offset(), - created_at: now, - last_seen_at: now, - revoked_at: None, - user_agent_hash: None, - ip_hash: None, - }]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![sessions::Model { - id: "session-2".to_string(), - user_id, - expires_at: (Utc::now() + chrono::Duration::days(1)).fixed_offset(), - created_at: now, - last_seen_at: now, - revoked_at: None, - user_agent_hash: None, - ip_hash: None, - }]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri("/auth/logout") - .header( - header::COOKIE, - format!("{}=session-2", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NO_CONTENT); - let cookie = response.headers().get(header::SET_COOKIE).unwrap(); - assert!(cookie.to_str().unwrap().contains("Max-Age=0")); - } - - #[tokio::test] - async fn rejects_cross_origin_post_before_handler_runs() { - let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri("/auth/logout") - .header(header::ORIGIN, "https://evil.example") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn websocket_requires_valid_same_origin() { - let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); - let app = build_app(build_state(db)); - let response = app - .oneshot( - websocket_request("/ws") - .header(header::ORIGIN, "https://evil.example") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn websocket_requires_session_cookie() { - let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); - let app = build_app(build_state(db)); - let response = app - .oneshot( - websocket_request("/ws") - .header(header::ORIGIN, "http://localhost:3000") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn voice_websocket_requires_same_origin() { - let channel_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); - let app = build_app(build_state(db)); - let response = app - .oneshot( - websocket_request(&format!("/channels/{channel_id}/voice/ws")) - .header(header::ORIGIN, "https://evil.example") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn voice_websocket_requires_session_cookie() { - let channel_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); - let app = build_app(build_state(db)); - let response = app - .oneshot( - websocket_request(&format!("/channels/{channel_id}/voice/ws")) - .header(header::ORIGIN, "http://localhost:3000") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn delete_sound_requires_guild_membership() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let sound_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-sound-1", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(0)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::DELETE) - .uri(format!("/guilds/{guild_id}/sounds/{sound_id}")) - .header( - header::COOKIE, - format!("{}=session-sound-1", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn delete_sound_forbids_non_owner_non_creator() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let sound_id = Uuid::new_v4(); - let created_at = Utc::now().fixed_offset(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-sound-2", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(1)]]) - .append_query_results([vec![soundboard_sounds::Model { - id: sound_id, - guild_id, - name: "Test".to_string(), - icon: "x".to_string(), - object_key: Some("soundboard/test.mp3".to_string()), - media_url: "https://media.example.com/test.mp3".to_string(), - mime_type: Some("audio/mpeg".to_string()), - size_bytes: Some(123), - file_path: None, - created_by_user_id: Uuid::new_v4(), - created_at, - }]]) - .append_query_results([vec![guilds::Model { - id: guild_id, - name: "Guild".to_string(), - owner_user_id: Uuid::new_v4(), - created_at, - }]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::DELETE) - .uri(format!("/guilds/{guild_id}/sounds/{sound_id}")) - .header( - header::COOKIE, - format!("{}=session-sound-2", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn delete_sound_returns_not_found_for_missing_sound() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let sound_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-sound-3", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(1)]]) - .append_query_results([Vec::::new()]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::DELETE) - .uri(format!("/guilds/{guild_id}/sounds/{sound_id}")) - .header( - header::COOKIE, - format!("{}=session-sound-3", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn delete_sound_rejects_wrong_guild_sound() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let other_guild_id = Uuid::new_v4(); - let sound_id = Uuid::new_v4(); - let created_at = Utc::now().fixed_offset(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-sound-4", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(1)]]) - .append_query_results([vec![soundboard_sounds::Model { - id: sound_id, - guild_id: other_guild_id, - name: "Test".to_string(), - icon: "x".to_string(), - object_key: Some("soundboard/test.mp3".to_string()), - media_url: "https://media.example.com/test.mp3".to_string(), - mime_type: Some("audio/mpeg".to_string()), - size_bytes: Some(123), - file_path: None, - created_by_user_id: user_id, - created_at, - }]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::DELETE) - .uri(format!("/guilds/{guild_id}/sounds/{sound_id}")) - .header( - header::COOKIE, - format!("{}=session-sound-4", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - } - - #[tokio::test] - async fn channel_attachment_requires_guild_membership() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let channel_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-channel-1", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![guild_id_row(guild_id)]]) - .append_query_results([vec![count_row(0)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri(format!("/channels/{channel_id}/attachments")) - .header( - header::COOKIE, - format!("{}=session-channel-1", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .header("x-file-name", "photo.png") - .header(header::CONTENT_TYPE, "image/png") - .body(Body::from(Vec::from(&b"\x89PNG\r\n\x1a\nrest"[..]))) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn channel_attachment_requires_media_storage_when_member() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let channel_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-channel-2", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![guild_id_row(guild_id)]]) - .append_query_results([vec![count_row(1)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri(format!("/channels/{channel_id}/attachments")) - .header( - header::COOKIE, - format!("{}=session-channel-2", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .header("x-file-name", "photo.png") - .header(header::CONTENT_TYPE, "image/png") - .body(Body::from(Vec::from(&b"\x89PNG\r\n\x1a\nrest"[..]))) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); - } - - #[tokio::test] - async fn dm_attachment_rejects_sending_to_self() { - let user_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-dm-1", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri(format!("/dms/{user_id}/attachments")) - .header( - header::COOKIE, - format!("{}=session-dm-1", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .header("x-file-name", "photo.png") - .header(header::CONTENT_TYPE, "image/png") - .body(Body::from(Vec::from(&b"\x89PNG\r\n\x1a\nrest"[..]))) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - } - - #[tokio::test] - async fn dm_attachment_returns_not_found_for_missing_recipient() { - let user_id = Uuid::new_v4(); - let other_user_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-dm-2", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(0)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri(format!("/dms/{other_user_id}/attachments")) - .header( - header::COOKIE, - format!("{}=session-dm-2", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .header("x-file-name", "photo.png") - .header(header::CONTENT_TYPE, "image/png") - .body(Body::from(Vec::from(&b"\x89PNG\r\n\x1a\nrest"[..]))) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn dm_attachment_requires_media_storage_for_existing_user() { - let user_id = Uuid::new_v4(); - let other_user_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-dm-3", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(1)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri(format!("/dms/{other_user_id}/attachments")) - .header( - header::COOKIE, - format!("{}=session-dm-3", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .header("x-file-name", "photo.png") - .header(header::CONTENT_TYPE, "image/png") - .body(Body::from(Vec::from(&b"\x89PNG\r\n\x1a\nrest"[..]))) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); - } - - #[tokio::test] - async fn join_invite_route_returns_guild_for_valid_invite() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let created_by_user_id = Uuid::new_v4(); - let created_at = Utc::now().fixed_offset(); - let invite = crate::entity::invites::Model { - code: "invite-route".to_string(), - guild_id, - created_by_user_id, - created_at, - expires_at: Some((Utc::now() + chrono::Duration::minutes(5)).fixed_offset()), - max_uses: Some(5), - use_count: 0, - }; - let updated_invite = crate::entity::invites::Model { - use_count: 1, - ..invite.clone() - }; - let guild = guilds::Model { - id: guild_id, - name: "Guild".to_string(), - owner_user_id: created_by_user_id, - created_at, - }; - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-invite-1", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![invite]]) - .append_query_results([Vec::::new()]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![updated_invite]]) - .append_query_results([vec![guild]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri("/invites/invite-route/join") - .header( - header::COOKIE, - format!("{}=session-invite-1", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - } - - #[tokio::test] - async fn list_guild_channels_requires_membership() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-guild-channels", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(0)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .uri(format!("/guilds/{guild_id}/channels")) - .header( - header::COOKIE, - format!("{}=session-guild-channels", auth::SESSION_COOKIE), - ) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn list_guild_members_requires_membership() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-guild-members", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(0)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .uri(format!("/guilds/{guild_id}/members")) - .header( - header::COOKIE, - format!("{}=session-guild-members", auth::SESSION_COOKIE), - ) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn channel_message_requires_membership() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let channel_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-channel-message-1", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![guild_id_row(guild_id)]]) - .append_query_results([vec![count_row(0)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri(format!("/channels/{channel_id}/messages")) - .header( - header::COOKIE, - format!("{}=session-channel-message-1", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from(r#"{"body":"hello"}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn channel_message_rejects_empty_body_for_members() { - let user_id = Uuid::new_v4(); - let guild_id = Uuid::new_v4(); - let channel_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-channel-message-2", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![guild_id_row(guild_id)]]) - .append_query_results([vec![count_row(1)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri(format!("/channels/{channel_id}/messages")) - .header( - header::COOKIE, - format!("{}=session-channel-message-2", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from(r#"{"body":" "}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - } - - #[tokio::test] - async fn dm_messages_reject_self_conversation() { - let user_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-dm-messages-1", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .uri(format!("/dms/{user_id}/messages")) - .header( - header::COOKIE, - format!("{}=session-dm-messages-1", auth::SESSION_COOKIE), - ) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - } - - #[tokio::test] - async fn dm_messages_return_not_found_for_missing_user() { - let user_id = Uuid::new_v4(); - let other_user_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-dm-messages-2", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(0)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .uri(format!("/dms/{other_user_id}/messages")) - .header( - header::COOKIE, - format!("{}=session-dm-messages-2", auth::SESSION_COOKIE), - ) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn send_dm_message_rejects_empty_body() { - let user_id = Uuid::new_v4(); - let other_user_id = Uuid::new_v4(); - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![mock_session("session-dm-send-1", user_id)]]) - .append_exec_results([MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }]) - .append_query_results([vec![count_row(1)]]) - .into_connection(); - - let app = build_app(build_state(db)); - let response = app - .oneshot( - Request::builder() - .method(Method::POST) - .uri(format!("/dms/{other_user_id}/messages")) - .header( - header::COOKIE, - format!("{}=session-dm-send-1", auth::SESSION_COOKIE), - ) - .header(header::ORIGIN, "http://localhost:3000") - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from(r#"{"body":" "}"#)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - } - - #[test] - fn csp_includes_media_origin_when_configured() { - let settings = Arc::new(Settings { - media: Some(MediaSettings { - account_id: "acct".to_string(), - access_key_id: "key".to_string(), - secret_access_key: "secret".to_string(), - bucket: "bucket".to_string(), - public_base_url: "https://media.example.com".to_string(), - }), - ..(*test_settings()).clone() - }); - let csp = build_csp(&settings); - - assert!(csp.contains("https://media.example.com")); - assert!(csp.contains("object-src 'none'")); - } - - #[tokio::test] - async fn health_route_uses_media_origin_in_csp_when_configured() { - let settings = Arc::new(Settings { - media: Some(MediaSettings { - account_id: "acct".to_string(), - access_key_id: "key".to_string(), - secret_access_key: "secret".to_string(), - bucket: "bucket".to_string(), - public_base_url: "https://media.example.com".to_string(), - }), - ..(*test_settings()).clone() - }); - let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); - let app = build_app(build_state_with_settings(db, settings)); - let response = app - .oneshot( - Request::builder() - .uri("/health") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - let csp = response - .headers() - .get("content-security-policy") - .unwrap() - .to_str() - .unwrap(); - - assert!(csp.contains("https://media.example.com")); - } -} diff --git a/src/media.rs b/src/media.rs deleted file mode 100644 index d3f3255..0000000 --- a/src/media.rs +++ /dev/null @@ -1,87 +0,0 @@ -use anyhow::{Result, anyhow}; -use aws_config::BehaviorVersion; -use aws_sdk_s3::{ - Client, - config::{Credentials, Region}, - primitives::ByteStream, -}; - -use crate::config::MediaSettings; - -#[derive(Clone)] -pub struct MediaStorage { - client: Client, - bucket: String, - public_base_url: String, -} - -impl MediaStorage { - pub async fn new(settings: &MediaSettings) -> Result { - let shared_config = aws_config::defaults(BehaviorVersion::latest()) - .region(Region::new("auto")) - .credentials_provider(Credentials::new( - settings.access_key_id.clone(), - settings.secret_access_key.clone(), - None, - None, - "chattz-r2", - )) - .load() - .await; - - let config = aws_sdk_s3::config::Builder::from(&shared_config) - .endpoint_url(settings.endpoint_url()) - .force_path_style(true) - .build(); - - Ok(Self { - client: Client::from_conf(config), - bucket: settings.bucket.clone(), - public_base_url: settings.public_base_url.trim_end_matches('/').to_string(), - }) - } - - pub async fn upload_object( - &self, - object_key: &str, - bytes: Vec, - content_type: &str, - original_filename: &str, - inline: bool, - ) -> Result { - self.client - .put_object() - .bucket(&self.bucket) - .key(object_key) - .body(ByteStream::from(bytes)) - .content_type(content_type) - .content_disposition(format!( - "{}; filename=\"{}\"", - if inline { "inline" } else { "attachment" }, - sanitize_header_value(original_filename) - )) - .send() - .await - .map_err(|e| anyhow!("failed to upload object to R2: {e}"))?; - - Ok(format!("{}/{}", self.public_base_url, object_key)) - } - - pub async fn delete_object(&self, object_key: &str) -> Result<()> { - self.client - .delete_object() - .bucket(&self.bucket) - .key(object_key) - .send() - .await - .map_err(|e| anyhow!("failed to delete object from R2: {e}"))?; - Ok(()) - } -} - -fn sanitize_header_value(value: &str) -> String { - value - .chars() - .filter(|c| *c != '\\' && *c != '"' && !c.is_control()) - .collect() -} diff --git a/src/migration/m20260213_000001_init.rs b/src/migration/m20260213_000001_init.rs index 3c1d1ff..9c671d4 100644 --- a/src/migration/m20260213_000001_init.rs +++ b/src/migration/m20260213_000001_init.rs @@ -11,13 +11,13 @@ impl MigrationTrait for Migration { Table::create() .table(Users::Table) .if_not_exists() - .col(ColumnDef::new(Users::Id).uuid().not_null().primary_key()) .col( - ColumnDef::new(Users::OidcSub) - .string() + ColumnDef::new(Users::Id) + .uuid() .not_null() - .unique_key(), + .primary_key(), ) + .col(ColumnDef::new(Users::OidcSub).string().not_null().unique_key()) .col(ColumnDef::new(Users::Email).string()) .col(ColumnDef::new(Users::DisplayName).string().not_null()) .col(ColumnDef::new(Users::AvatarUrl).string()) @@ -42,7 +42,12 @@ impl MigrationTrait for Migration { Table::create() .table(Guilds::Table) .if_not_exists() - .col(ColumnDef::new(Guilds::Id).uuid().not_null().primary_key()) + .col( + ColumnDef::new(Guilds::Id) + .uuid() + .not_null() + .primary_key(), + ) .col(ColumnDef::new(Guilds::Name).string().not_null()) .col(ColumnDef::new(Guilds::OwnerUserId).uuid().not_null()) .col( @@ -103,7 +108,12 @@ impl MigrationTrait for Migration { Table::create() .table(Channels::Table) .if_not_exists() - .col(ColumnDef::new(Channels::Id).uuid().not_null().primary_key()) + .col( + ColumnDef::new(Channels::Id) + .uuid() + .not_null() + .primary_key(), + ) .col(ColumnDef::new(Channels::GuildId).uuid().not_null()) .col(ColumnDef::new(Channels::Name).string().not_null()) .col( @@ -128,7 +138,12 @@ impl MigrationTrait for Migration { Table::create() .table(Messages::Table) .if_not_exists() - .col(ColumnDef::new(Messages::Id).uuid().not_null().primary_key()) + .col( + ColumnDef::new(Messages::Id) + .uuid() + .not_null() + .primary_key(), + ) .col(ColumnDef::new(Messages::ChannelId).uuid().not_null()) .col(ColumnDef::new(Messages::AuthorUserId).uuid().not_null()) .col(ColumnDef::new(Messages::Body).text().not_null()) diff --git a/src/migration/m20260213_000002_invites.rs b/src/migration/m20260213_000002_invites.rs index 3dbcd5d..e99c28d 100644 --- a/src/migration/m20260213_000002_invites.rs +++ b/src/migration/m20260213_000002_invites.rs @@ -11,12 +11,7 @@ impl MigrationTrait for Migration { Table::create() .table(Invites::Table) .if_not_exists() - .col( - ColumnDef::new(Invites::Code) - .string() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Invites::Code).string().not_null().primary_key()) .col(ColumnDef::new(Invites::GuildId).uuid().not_null()) .col(ColumnDef::new(Invites::CreatedByUserId).uuid().not_null()) .col( diff --git a/src/migration/m20260213_000004_direct_messages.rs b/src/migration/m20260213_000004_direct_messages.rs index 63ba7e2..4b723ab 100644 --- a/src/migration/m20260213_000004_direct_messages.rs +++ b/src/migration/m20260213_000004_direct_messages.rs @@ -17,16 +17,8 @@ impl MigrationTrait for Migration { .not_null() .primary_key(), ) - .col( - ColumnDef::new(DirectMessages::SenderUserId) - .uuid() - .not_null(), - ) - .col( - ColumnDef::new(DirectMessages::RecipientUserId) - .uuid() - .not_null(), - ) + .col(ColumnDef::new(DirectMessages::SenderUserId).uuid().not_null()) + .col(ColumnDef::new(DirectMessages::RecipientUserId).uuid().not_null()) .col(ColumnDef::new(DirectMessages::Body).text().not_null()) .col( ColumnDef::new(DirectMessages::CreatedAt) diff --git a/src/migration/m20260227_000006_attachments.rs b/src/migration/m20260227_000006_attachments.rs deleted file mode 100644 index 96f6da6..0000000 --- a/src/migration/m20260227_000006_attachments.rs +++ /dev/null @@ -1,122 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Attachments::Table) - .if_not_exists() - .col( - ColumnDef::new(Attachments::Id) - .uuid() - .not_null() - .primary_key(), - ) - .col(ColumnDef::new(Attachments::ChannelMessageId).uuid().null()) - .col(ColumnDef::new(Attachments::DirectMessageId).uuid().null()) - .col(ColumnDef::new(Attachments::UploaderUserId).uuid().not_null()) - .col(ColumnDef::new(Attachments::ObjectKey).string().not_null()) - .col(ColumnDef::new(Attachments::MediaUrl).string().not_null()) - .col(ColumnDef::new(Attachments::MimeType).string().not_null()) - .col(ColumnDef::new(Attachments::SizeBytes).big_integer().not_null()) - .col(ColumnDef::new(Attachments::OriginalFilename).string().not_null()) - .col( - ColumnDef::new(Attachments::CreatedAt) - .timestamp_with_time_zone() - .not_null() - .default(Expr::current_timestamp()), - ) - .foreign_key( - ForeignKey::create() - .name("fk_attachments_channel_message") - .from(Attachments::Table, Attachments::ChannelMessageId) - .to(Messages::Table, Messages::Id) - .on_delete(ForeignKeyAction::Cascade), - ) - .foreign_key( - ForeignKey::create() - .name("fk_attachments_direct_message") - .from(Attachments::Table, Attachments::DirectMessageId) - .to(DirectMessages::Table, DirectMessages::Id) - .on_delete(ForeignKeyAction::Cascade), - ) - .foreign_key( - ForeignKey::create() - .name("fk_attachments_uploader") - .from(Attachments::Table, Attachments::UploaderUserId) - .to(Users::Table, Users::Id), - ) - .check( - Expr::cust( - "(channel_message_id IS NOT NULL AND direct_message_id IS NULL) OR (channel_message_id IS NULL AND direct_message_id IS NOT NULL)", - ), - ) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx_attachments_channel_message") - .table(Attachments::Table) - .col(Attachments::ChannelMessageId) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx_attachments_direct_message") - .table(Attachments::Table) - .col(Attachments::DirectMessageId) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Attachments::Table).to_owned()) - .await - } -} - -#[derive(DeriveIden)] -enum Attachments { - Table, - Id, - ChannelMessageId, - DirectMessageId, - UploaderUserId, - ObjectKey, - MediaUrl, - MimeType, - SizeBytes, - OriginalFilename, - CreatedAt, -} - -#[derive(DeriveIden)] -enum Messages { - Table, - Id, -} - -#[derive(DeriveIden)] -enum DirectMessages { - Table, - Id, -} - -#[derive(DeriveIden)] -enum Users { - Table, - Id, -} diff --git a/src/migration/m20260227_000007_sessions.rs b/src/migration/m20260227_000007_sessions.rs deleted file mode 100644 index 739f690..0000000 --- a/src/migration/m20260227_000007_sessions.rs +++ /dev/null @@ -1,107 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Sessions::Table) - .if_not_exists() - .col( - ColumnDef::new(Sessions::Id) - .string() - .not_null() - .primary_key(), - ) - .col(ColumnDef::new(Sessions::UserId).uuid().not_null()) - .col( - ColumnDef::new(Sessions::ExpiresAt) - .timestamp_with_time_zone() - .not_null(), - ) - .col( - ColumnDef::new(Sessions::CreatedAt) - .timestamp_with_time_zone() - .not_null() - .default(Expr::current_timestamp()), - ) - .col( - ColumnDef::new(Sessions::LastSeenAt) - .timestamp_with_time_zone() - .not_null() - .default(Expr::current_timestamp()), - ) - .col(ColumnDef::new(Sessions::RevokedAt).timestamp_with_time_zone()) - .col(ColumnDef::new(Sessions::UserAgentHash).string()) - .col(ColumnDef::new(Sessions::IpHash).string()) - .foreign_key( - ForeignKey::create() - .name("fk_sessions_user") - .from(Sessions::Table, Sessions::UserId) - .to(Users::Table, Users::Id) - .on_delete(ForeignKeyAction::Cascade), - ) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx_sessions_user_id") - .table(Sessions::Table) - .col(Sessions::UserId) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx_sessions_expires_at") - .table(Sessions::Table) - .col(Sessions::ExpiresAt) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx_sessions_revoked_at") - .table(Sessions::Table) - .col(Sessions::RevokedAt) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Sessions::Table).to_owned()) - .await - } -} - -#[derive(DeriveIden)] -enum Sessions { - Table, - Id, - UserId, - ExpiresAt, - CreatedAt, - LastSeenAt, - RevokedAt, - UserAgentHash, - IpHash, -} - -#[derive(DeriveIden)] -enum Users { - Table, - Id, -} diff --git a/src/migration/m20260227_000008_soundboard_media.rs b/src/migration/m20260227_000008_soundboard_media.rs deleted file mode 100644 index 01942fe..0000000 --- a/src/migration/m20260227_000008_soundboard_media.rs +++ /dev/null @@ -1,72 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(SoundboardSounds::Table) - .add_column(ColumnDef::new(SoundboardSounds::ObjectKey).string().null()) - .add_column(ColumnDef::new(SoundboardSounds::MediaUrl).string().null()) - .add_column(ColumnDef::new(SoundboardSounds::MimeType).string().null()) - .add_column( - ColumnDef::new(SoundboardSounds::SizeBytes) - .big_integer() - .null(), - ) - .to_owned(), - ) - .await?; - - manager - .get_connection() - .execute_unprepared( - r#" - UPDATE soundboard_sounds - SET media_url = file_path - WHERE media_url IS NULL - "#, - ) - .await?; - - manager - .alter_table( - Table::alter() - .table(SoundboardSounds::Table) - .modify_column( - ColumnDef::new(SoundboardSounds::MediaUrl) - .string() - .not_null(), - ) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(SoundboardSounds::Table) - .drop_column(SoundboardSounds::ObjectKey) - .drop_column(SoundboardSounds::MediaUrl) - .drop_column(SoundboardSounds::MimeType) - .drop_column(SoundboardSounds::SizeBytes) - .to_owned(), - ) - .await - } -} - -#[derive(DeriveIden)] -enum SoundboardSounds { - Table, - ObjectKey, - MediaUrl, - MimeType, - SizeBytes, -} diff --git a/src/migration/m20260227_000009_soundboard_file_path_nullable.rs b/src/migration/m20260227_000009_soundboard_file_path_nullable.rs deleted file mode 100644 index 116710c..0000000 --- a/src/migration/m20260227_000009_soundboard_file_path_nullable.rs +++ /dev/null @@ -1,50 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(SoundboardSounds::Table) - .modify_column(ColumnDef::new(SoundboardSounds::FilePath).string().null()) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .get_connection() - .execute_unprepared( - r#" - UPDATE soundboard_sounds - SET file_path = media_url - WHERE file_path IS NULL - "#, - ) - .await?; - - manager - .alter_table( - Table::alter() - .table(SoundboardSounds::Table) - .modify_column( - ColumnDef::new(SoundboardSounds::FilePath) - .string() - .not_null(), - ) - .to_owned(), - ) - .await - } -} - -#[derive(DeriveIden)] -enum SoundboardSounds { - Table, - FilePath, -} diff --git a/src/migration/mod.rs b/src/migration/mod.rs index ab46deb..4db4f51 100644 --- a/src/migration/mod.rs +++ b/src/migration/mod.rs @@ -5,10 +5,6 @@ mod m20260213_000002_invites; mod m20260213_000003_channel_kind; mod m20260213_000004_direct_messages; mod m20260224_000005_soundboard; -mod m20260227_000006_attachments; -mod m20260227_000007_sessions; -mod m20260227_000008_soundboard_media; -mod m20260227_000009_soundboard_file_path_nullable; pub struct Migrator; @@ -21,10 +17,6 @@ impl MigratorTrait for Migrator { Box::new(m20260213_000003_channel_kind::Migration), Box::new(m20260213_000004_direct_messages::Migration), Box::new(m20260224_000005_soundboard::Migration), - Box::new(m20260227_000006_attachments::Migration), - Box::new(m20260227_000007_sessions::Migration), - Box::new(m20260227_000008_soundboard_media::Migration), - Box::new(m20260227_000009_soundboard_file_path_nullable::Migration), ] } } diff --git a/src/models.rs b/src/models.rs index b54e303..74ac74a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -31,12 +31,12 @@ pub struct Channel { } #[derive(Debug, Clone, Serialize)] -pub struct Attachment { +pub struct Message { pub id: Uuid, - pub media_url: String, - pub mime_type: String, - pub size_bytes: i64, - pub original_filename: String, + pub channel_id: Uuid, + pub author_user_id: Uuid, + pub body: String, + pub created_at: DateTimeWithTimeZone, } #[derive(Debug, Clone, Serialize)] @@ -46,14 +46,13 @@ pub struct BasicUser { pub avatar_url: Option, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, sea_orm::FromQueryResult)] pub struct MessageWithAuthor { pub id: Uuid, pub channel_id: Uuid, pub author_user_id: Uuid, pub author_display_name: String, pub body: String, - pub attachments: Vec, pub created_at: DateTimeWithTimeZone, } @@ -72,7 +71,6 @@ pub struct DmMessageWithAuthor { pub recipient_user_id: Uuid, pub author_display_name: String, pub body: String, - pub attachments: Vec, pub created_at: DateTimeWithTimeZone, } @@ -93,9 +91,7 @@ pub struct SoundboardSound { pub guild_id: Uuid, pub name: String, pub icon: String, - pub media_url: String, - pub mime_type: Option, - pub size_bytes: Option, + pub file_path: String, pub created_by_user_id: Uuid, pub created_at: DateTimeWithTimeZone, } diff --git a/src/voice.rs b/src/voice.rs index 5dc3ec8..b15b81c 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -10,7 +10,7 @@ use crate::{AppState, db}; #[derive(Default)] pub struct VoiceHub { - rooms: RwLock>>>, + rooms: RwLock>>, } #[derive(Clone)] @@ -19,7 +19,6 @@ struct ClientHandle { is_sharing_video: bool, is_sharing_screen: bool, is_speaking: bool, - is_muted: bool, tx: mpsc::UnboundedSender, } @@ -30,7 +29,6 @@ pub struct VoiceParticipant { pub is_sharing_video: bool, pub is_sharing_screen: bool, pub is_speaking: bool, - pub is_muted: bool, } #[derive(Serialize, Clone)] @@ -58,10 +56,6 @@ enum ServerEvent { user_id: Uuid, is_speaking: bool, }, - MuteStatusChanged { - user_id: Uuid, - is_muted: bool, - }, Signal { from_user_id: Uuid, kind: String, @@ -72,7 +66,7 @@ enum ServerEvent { }, PlaySound { user_id: Uuid, - media_url: String, + sound_url: String, }, } @@ -93,24 +87,11 @@ enum ClientEvent { SetSpeakingStatus { is_speaking: bool, }, - SetMuteStatus { - is_muted: bool, - }, PlaySound { - sound_id: Uuid, + sound_url: String, }, } -#[derive(Default)] -struct VoiceStateChange { - joined: Option, - left_user_id: Option, - video_changed: Option<(Uuid, bool)>, - screen_changed: Option<(Uuid, bool)>, - speaking_changed: Option<(Uuid, bool)>, - mute_changed: Option<(Uuid, bool)>, -} - impl VoiceHub { pub async fn participants(&self, room_id: Uuid) -> Vec { let rooms = self.rooms.read().await; @@ -119,7 +100,13 @@ impl VoiceHub { }; room.iter() - .filter_map(|(user_id, connections)| aggregate_participant(Some(connections), *user_id)) + .map(|(user_id, handle)| VoiceParticipant { + user_id: *user_id, + display_name: handle.display_name.clone(), + is_sharing_video: handle.is_sharing_video, + is_sharing_screen: handle.is_sharing_screen, + is_speaking: handle.is_speaking, + }) .collect() } @@ -127,55 +114,61 @@ impl VoiceHub { &self, room_id: Uuid, user_id: Uuid, - connection_id: Uuid, display_name: String, tx: mpsc::UnboundedSender, - ) -> (Vec, VoiceStateChange) { + ) -> Vec { let mut rooms = self.rooms.write().await; let room = rooms.entry(room_id).or_default(); let peers = room .iter() - .filter_map(|(peer_id, connections)| aggregate_participant(Some(connections), *peer_id)) + .map(|(peer_id, peer)| VoiceParticipant { + user_id: *peer_id, + display_name: peer.display_name.clone(), + is_sharing_video: peer.is_sharing_video, + is_sharing_screen: peer.is_sharing_screen, + is_speaking: peer.is_speaking, + }) .collect::>(); - let previous = aggregate_participant(room.get(&user_id), user_id); - room.entry(user_id).or_default().insert( - connection_id, + room.insert( + user_id, ClientHandle { - display_name, + display_name: display_name.clone(), is_sharing_video: false, is_sharing_screen: false, is_speaking: false, - is_muted: false, tx, }, ); - let current = aggregate_participant(room.get(&user_id), user_id); - (peers, diff_voice_state(previous, current)) - } - - async fn leave(&self, room_id: Uuid, user_id: Uuid, connection_id: Uuid) -> VoiceStateChange { - let mut rooms = self.rooms.write().await; - let Some(room) = rooms.get_mut(&room_id) else { - return VoiceStateChange::default(); - }; - - let previous = aggregate_participant(room.get(&user_id), user_id); - if let Some(connections) = room.get_mut(&user_id) { - connections.remove(&connection_id); - if connections.is_empty() { - room.remove(&user_id); + for (peer_id, peer) in room.iter() { + if *peer_id != user_id { + let _ = peer.tx.send(ServerEvent::PeerJoined { + user_id, + display_name: display_name.clone(), + }); } } - let current = aggregate_participant(room.get(&user_id), user_id); + + peers + } + + async fn leave(&self, room_id: Uuid, user_id: Uuid) { + let mut rooms = self.rooms.write().await; + let Some(room) = rooms.get_mut(&room_id) else { + return; + }; + + room.remove(&user_id); + + for peer in room.values() { + let _ = peer.tx.send(ServerEvent::PeerLeft { user_id }); + } if room.is_empty() { rooms.remove(&room_id); } - - diff_voice_state(previous, current) } pub async fn relay_signal( @@ -191,10 +184,7 @@ impl VoiceHub { return; }; - if let Some(target) = room - .get(&to_user_id) - .and_then(|connections| connections.values().next()) - { + if let Some(target) = room.get(&to_user_id) { let _ = target.tx.send(ServerEvent::Signal { from_user_id, kind, @@ -203,185 +193,77 @@ impl VoiceHub { } } - async fn set_video_status( - &self, - room_id: Uuid, - user_id: Uuid, - connection_id: Uuid, - is_sharing_video: bool, - ) -> VoiceStateChange { + pub async fn set_video_status(&self, room_id: Uuid, user_id: Uuid, is_sharing_video: bool) { let mut rooms = self.rooms.write().await; let Some(room) = rooms.get_mut(&room_id) else { - return VoiceStateChange::default(); + return; }; - let previous = aggregate_participant(room.get(&user_id), user_id); - if let Some(handle) = room - .get_mut(&user_id) - .and_then(|connections| connections.get_mut(&connection_id)) - { + if let Some(handle) = room.get_mut(&user_id) { handle.is_sharing_video = is_sharing_video; + + for (peer_id, peer) in room.iter() { + if *peer_id != user_id { + let _ = peer.tx.send(ServerEvent::VideoStatusChanged { + user_id, + is_sharing_video, + }); + } + } } - let current = aggregate_participant(room.get(&user_id), user_id); - diff_voice_state(previous, current) } - async fn set_screen_status( - &self, - room_id: Uuid, - user_id: Uuid, - connection_id: Uuid, - is_sharing_screen: bool, - ) -> VoiceStateChange { + pub async fn set_screen_status(&self, room_id: Uuid, user_id: Uuid, is_sharing_screen: bool) { let mut rooms = self.rooms.write().await; let Some(room) = rooms.get_mut(&room_id) else { - return VoiceStateChange::default(); + return; }; - let previous = aggregate_participant(room.get(&user_id), user_id); - if let Some(handle) = room - .get_mut(&user_id) - .and_then(|connections| connections.get_mut(&connection_id)) - { + if let Some(handle) = room.get_mut(&user_id) { handle.is_sharing_screen = is_sharing_screen; + + for (peer_id, peer) in room.iter() { + if *peer_id != user_id { + let _ = peer.tx.send(ServerEvent::ScreenStatusChanged { + user_id, + is_sharing_screen, + }); + } + } } - let current = aggregate_participant(room.get(&user_id), user_id); - diff_voice_state(previous, current) } - async fn set_speaking_status( - &self, - room_id: Uuid, - user_id: Uuid, - connection_id: Uuid, - is_speaking: bool, - ) -> VoiceStateChange { + pub async fn set_speaking_status(&self, room_id: Uuid, user_id: Uuid, is_speaking: bool) { let mut rooms = self.rooms.write().await; let Some(room) = rooms.get_mut(&room_id) else { - return VoiceStateChange::default(); + return; }; - let previous = aggregate_participant(room.get(&user_id), user_id); - if let Some(handle) = room - .get_mut(&user_id) - .and_then(|connections| connections.get_mut(&connection_id)) - { + if let Some(handle) = room.get_mut(&user_id) { handle.is_speaking = is_speaking; + + for (peer_id, peer) in room.iter() { + if *peer_id != user_id { + let _ = peer.tx.send(ServerEvent::SpeakingStatusChanged { + user_id, + is_speaking, + }); + } + } } - let current = aggregate_participant(room.get(&user_id), user_id); - diff_voice_state(previous, current) } - async fn set_mute_status( - &self, - room_id: Uuid, - user_id: Uuid, - connection_id: Uuid, - is_muted: bool, - ) -> VoiceStateChange { - let mut rooms = self.rooms.write().await; - let Some(room) = rooms.get_mut(&room_id) else { - return VoiceStateChange::default(); - }; - - let previous = aggregate_participant(room.get(&user_id), user_id); - if let Some(handle) = room - .get_mut(&user_id) - .and_then(|connections| connections.get_mut(&connection_id)) - { - handle.is_muted = is_muted; - } - let current = aggregate_participant(room.get(&user_id), user_id); - diff_voice_state(previous, current) - } - - pub async fn play_sound(&self, room_id: Uuid, user_id: Uuid, media_url: String) { + pub async fn play_sound(&self, room_id: Uuid, user_id: Uuid, sound_url: String) { let rooms = self.rooms.read().await; let Some(room) = rooms.get(&room_id) else { return; }; - for (peer_id, connections) in room.iter() { - if *peer_id == user_id { - continue; - } - for peer in connections.values() { - let _ = peer.tx.send(ServerEvent::PlaySound { - user_id, - media_url: media_url.clone(), - }); - } - } - } - - async fn emit_change(&self, room_id: Uuid, user_id: Uuid, change: VoiceStateChange) { - if is_voice_state_change_empty(&change) { - return; - } - - let rooms = self.rooms.read().await; - let Some(room) = rooms.get(&room_id) else { - return; - }; - - if let Some(participant) = change.joined { - broadcast_voice_event( - room, + for peer in room.values() { + let _ = peer.tx.send(ServerEvent::PlaySound { user_id, - ServerEvent::PeerJoined { - user_id: participant.user_id, - display_name: participant.display_name, - }, - ); - } - if let Some(left_user_id) = change.left_user_id { - broadcast_voice_event( - room, - user_id, - ServerEvent::PeerLeft { - user_id: left_user_id, - }, - ); - } - if let Some((changed_user_id, is_sharing_video)) = change.video_changed { - broadcast_voice_event( - room, - user_id, - ServerEvent::VideoStatusChanged { - user_id: changed_user_id, - is_sharing_video, - }, - ); - } - if let Some((changed_user_id, is_sharing_screen)) = change.screen_changed { - broadcast_voice_event( - room, - user_id, - ServerEvent::ScreenStatusChanged { - user_id: changed_user_id, - is_sharing_screen, - }, - ); - } - if let Some((changed_user_id, is_speaking)) = change.speaking_changed { - broadcast_voice_event( - room, - user_id, - ServerEvent::SpeakingStatusChanged { - user_id: changed_user_id, - is_speaking, - }, - ); - } - if let Some((changed_user_id, is_muted)) = change.mute_changed { - broadcast_voice_event( - room, - user_id, - ServerEvent::MuteStatusChanged { - user_id: changed_user_id, - is_muted, - }, - ); + sound_url: sound_url.clone(), + }); } } } @@ -390,26 +272,15 @@ pub async fn handle_socket(state: AppState, socket: WebSocket, room_id: Uuid, us let Some(user) = db::get_user_by_id(&state.db, user_id).await.ok().flatten() else { return; }; - let Ok(Some(guild_id)) = db::guild_id_for_channel(&state.db, room_id).await else { - return; - }; let (mut ws_sender, mut ws_receiver) = socket.split(); let (tx, mut rx) = mpsc::unbounded_channel::(); - let connection_id = Uuid::new_v4(); - let (peers, change) = state + let peers = state .voice - .join( - room_id, - user_id, - connection_id, - user.display_name.clone(), - tx.clone(), - ) + .join(room_id, user_id, user.display_name.clone(), tx.clone()) .await; let _ = tx.send(ServerEvent::Peers { peers }); - state.voice.emit_change(room_id, user_id, change).await; let send_task = tokio::spawn(async move { while let Some(event) = rx.recv().await { @@ -438,42 +309,25 @@ pub async fn handle_socket(state: AppState, socket: WebSocket, room_id: Uuid, us .await; } Ok(ClientEvent::SetVideoStatus { is_sharing_video }) => { - let change = state + state .voice - .set_video_status(room_id, user_id, connection_id, is_sharing_video) + .set_video_status(room_id, user_id, is_sharing_video) .await; - state.voice.emit_change(room_id, user_id, change).await; } Ok(ClientEvent::SetScreenStatus { is_sharing_screen }) => { - let change = state + state .voice - .set_screen_status(room_id, user_id, connection_id, is_sharing_screen) + .set_screen_status(room_id, user_id, is_sharing_screen) .await; - state.voice.emit_change(room_id, user_id, change).await; } Ok(ClientEvent::SetSpeakingStatus { is_speaking }) => { - let change = state + state .voice - .set_speaking_status(room_id, user_id, connection_id, is_speaking) + .set_speaking_status(room_id, user_id, is_speaking) .await; - state.voice.emit_change(room_id, user_id, change).await; } - Ok(ClientEvent::SetMuteStatus { is_muted }) => { - let change = state - .voice - .set_mute_status(room_id, user_id, connection_id, is_muted) - .await; - state.voice.emit_change(room_id, user_id, change).await; - } - Ok(ClientEvent::PlaySound { sound_id }) => { - if let Ok(Some(sound)) = db::get_sound_by_id(&state.db, sound_id).await - && sound.guild_id == guild_id - { - state - .voice - .play_sound(room_id, user_id, sound.media_url) - .await; - } + Ok(ClientEvent::PlaySound { sound_url }) => { + state.voice.play_sound(room_id, user_id, sound_url).await; } Err(err) => { let _ = tx.send(ServerEvent::Error { @@ -488,183 +342,5 @@ pub async fn handle_socket(state: AppState, socket: WebSocket, room_id: Uuid, us } send_task.abort(); - let change = state.voice.leave(room_id, user_id, connection_id).await; - state.voice.emit_change(room_id, user_id, change).await; -} - -fn aggregate_participant( - connections: Option<&HashMap>, - user_id: Uuid, -) -> Option { - let connections = connections?; - if connections.is_empty() { - return None; - } - - let mut handles = connections.values(); - let first = handles.next()?; - Some(VoiceParticipant { - user_id, - display_name: first.display_name.clone(), - is_sharing_video: connections.values().any(|handle| handle.is_sharing_video), - is_sharing_screen: connections.values().any(|handle| handle.is_sharing_screen), - is_speaking: connections.values().any(|handle| handle.is_speaking), - is_muted: connections.values().all(|handle| handle.is_muted), - }) -} - -fn diff_voice_state( - previous: Option, - current: Option, -) -> VoiceStateChange { - match (previous, current) { - (None, None) => VoiceStateChange::default(), - (None, Some(current)) => VoiceStateChange { - joined: Some(current), - ..Default::default() - }, - (Some(previous), None) => VoiceStateChange { - left_user_id: Some(previous.user_id), - ..Default::default() - }, - (Some(previous), Some(current)) => VoiceStateChange { - video_changed: (previous.is_sharing_video != current.is_sharing_video) - .then_some((current.user_id, current.is_sharing_video)), - screen_changed: (previous.is_sharing_screen != current.is_sharing_screen) - .then_some((current.user_id, current.is_sharing_screen)), - speaking_changed: (previous.is_speaking != current.is_speaking) - .then_some((current.user_id, current.is_speaking)), - mute_changed: (previous.is_muted != current.is_muted) - .then_some((current.user_id, current.is_muted)), - ..Default::default() - }, - } -} - -fn broadcast_voice_event( - room: &HashMap>, - source_user_id: Uuid, - event: ServerEvent, -) { - for (peer_id, connections) in room { - if *peer_id == source_user_id { - continue; - } - for peer in connections.values() { - let _ = peer.tx.send(event.clone()); - } - } -} - -fn is_voice_state_change_empty(change: &VoiceStateChange) -> bool { - change.joined.is_none() - && change.left_user_id.is_none() - && change.video_changed.is_none() - && change.screen_changed.is_none() - && change.speaking_changed.is_none() - && change.mute_changed.is_none() -} - -#[cfg(test)] -mod tests { - use super::{VoiceHub, aggregate_participant}; - use std::collections::HashMap; - use tokio::sync::mpsc; - use uuid::Uuid; - - #[tokio::test] - async fn multiple_connections_keep_voice_participant_until_last_leave() { - let hub = VoiceHub::default(); - let room_id = Uuid::new_v4(); - let user_id = Uuid::new_v4(); - let first_connection = Uuid::new_v4(); - let second_connection = Uuid::new_v4(); - let (tx1, _rx1) = mpsc::unbounded_channel(); - let (tx2, _rx2) = mpsc::unbounded_channel(); - - let (_peers, first_join) = hub - .join(room_id, user_id, first_connection, "User".to_string(), tx1) - .await; - let (_peers, second_join) = hub - .join(room_id, user_id, second_connection, "User".to_string(), tx2) - .await; - let first_leave = hub.leave(room_id, user_id, first_connection).await; - let second_leave = hub.leave(room_id, user_id, second_connection).await; - - assert!(first_join.joined.is_some()); - assert!(second_join.joined.is_none()); - assert!(first_leave.left_user_id.is_none()); - assert_eq!(second_leave.left_user_id, Some(user_id)); - } - - #[tokio::test] - async fn voice_mute_state_only_flips_when_all_connections_are_muted() { - let hub = VoiceHub::default(); - let room_id = Uuid::new_v4(); - let user_id = Uuid::new_v4(); - let first_connection = Uuid::new_v4(); - let second_connection = Uuid::new_v4(); - let (tx1, _rx1) = mpsc::unbounded_channel(); - let (tx2, _rx2) = mpsc::unbounded_channel(); - - let _ = hub - .join(room_id, user_id, first_connection, "User".to_string(), tx1) - .await; - let _ = hub - .join(room_id, user_id, second_connection, "User".to_string(), tx2) - .await; - - let first_mute = hub - .set_mute_status(room_id, user_id, first_connection, true) - .await; - let second_mute = hub - .set_mute_status(room_id, user_id, second_connection, true) - .await; - let unmute = hub - .set_mute_status(room_id, user_id, first_connection, false) - .await; - - assert!(first_mute.mute_changed.is_none()); - assert_eq!(second_mute.mute_changed, Some((user_id, true))); - assert_eq!(unmute.mute_changed, Some((user_id, false))); - } - - #[test] - fn aggregate_participant_combines_connection_state() { - let user_id = Uuid::new_v4(); - let first_connection = Uuid::new_v4(); - let second_connection = Uuid::new_v4(); - let mut connections = HashMap::new(); - let (tx1, _rx1) = mpsc::unbounded_channel(); - let (tx2, _rx2) = mpsc::unbounded_channel(); - - connections.insert( - first_connection, - super::ClientHandle { - display_name: "User".to_string(), - is_sharing_video: true, - is_sharing_screen: false, - is_speaking: false, - is_muted: true, - tx: tx1, - }, - ); - connections.insert( - second_connection, - super::ClientHandle { - display_name: "User".to_string(), - is_sharing_video: false, - is_sharing_screen: true, - is_speaking: true, - is_muted: false, - tx: tx2, - }, - ); - - let participant = aggregate_participant(Some(&connections), user_id).unwrap(); - assert!(participant.is_sharing_video); - assert!(participant.is_sharing_screen); - assert!(participant.is_speaking); - assert!(!participant.is_muted); - } + state.voice.leave(room_id, user_id).await; } diff --git a/static/app.js b/static/app.js index 2794dc8..f9aad1a 100644 --- a/static/app.js +++ b/static/app.js @@ -1 +1,1349 @@ -import "./shared/app-core.js"; +const state = { + me: null, + guilds: [], + channels: [], + dmConversations: [], + members: [], + voicePresence: new Map(), + selectedGuildId: null, + selectedTextChannelId: null, + selectedDmUserId: null, + selectedDmDisplayName: null, + selectedVoiceChannelId: null, + voice: { + ws: null, + joinedChannelId: null, + localStream: null, + rawStream: null, + videoStream: null, + screenStream: null, + audioContext: null, + peerConnections: new Map(), + muted: false, + sharingVideo: false, + sharingScreen: false, + viewMode: 'chat', // 'chat' or 'video' + iceServers: [{ urls: "stun:stun.l.google.com:19302" }], + }, + voicePresencePollId: null, + chatWs: null, + lastMessageId: null, + onlineUsers: new Set(), +}; + +const el = { + authScreen: document.getElementById("auth-screen"), + loginBtn: document.getElementById("login-btn"), + main: document.getElementById("main"), + status: document.getElementById("status"), + + // Guilds + guildList: document.getElementById("guild-list"), + addGuildBtn: document.getElementById("add-guild-btn"), + guildTitle: document.getElementById("guild-title"), + createInviteBtn: document.getElementById("create-invite-btn"), + + // Channels + channelList: document.getElementById("channel-list"), + voiceChannelList: document.getElementById("voice-channel-list"), + addTextBtn: document.getElementById("add-text-btn"), + addVoiceBtn: document.getElementById("add-voice-btn"), + dmList: document.getElementById("dm-list"), + channelTitle: document.getElementById("channel-title"), + + // Messages + messageList: document.getElementById("message-list"), + messageForm: document.getElementById("message-form"), + messageBody: document.getElementById("message-body"), + + // User Panel + userName: document.getElementById("user-name"), + userAvatar: document.getElementById("user-avatar"), + logoutBtn: document.getElementById("logout-btn"), + + // Voice Connection + voiceConnection: document.getElementById("voice-connection"), + vcChannelName: document.getElementById("vc-channel-name"), + voiceVideoBtn: document.getElementById("voice-video-btn"), + voiceScreenBtn: document.getElementById("voice-screen-btn"), + voiceMuteBtn: document.getElementById("voice-mute-btn"), + voiceLeaveBtn: document.getElementById("voice-leave-btn"), + videoGrid: document.getElementById("video-grid"), + messageForm: document.getElementById("message-form"), + messageInputWrapper: document.querySelector(".chat-input-wrapper"), + + // Members + memberList: document.getElementById("member-list"), + + // Modals + modalContainer: document.getElementById("modal-container"), + guildForm: document.getElementById("guild-form"), + guildName: document.getElementById("guild-name"), + modalCancel: document.getElementById("modal-cancel"), + + channelModal: document.getElementById("channel-modal"), + channelForm: document.getElementById("channel-form"), + channelName: document.getElementById("channel-name"), + channelModalCancel: document.getElementById("channel-modal-cancel"), + + // Sound Board + soundboard: document.getElementById('soundboard'), + soundboardGrid: document.getElementById('soundboard-grid'), + addSoundBtn: document.getElementById('add-sound-btn'), + soundModal: document.getElementById('sound-modal'), + soundForm: document.getElementById('sound-form'), + soundName: document.getElementById('sound-name'), + soundIcon: document.getElementById('sound-icon'), + soundFile: document.getElementById('sound-file'), + soundModalCancel: document.getElementById('sound-modal-cancel'), + soundSubmitBtn: document.getElementById('sound-submit-btn'), + + // Mobile + mobileMenuBtn: document.getElementById("mobile-menu-btn"), + mobileMembersBtn: document.getElementById("mobile-members-btn"), + overlay: document.getElementById("overlay"), + utilitySidebar: document.getElementById("utility-sidebar"), +}; + +// --- API Helpers --- + +async function api(path, options = {}) { + const res = await fetch(path, { + ...options, + headers: { + "content-type": "application/json", + ...(options.headers || {}), + }, + credentials: "same-origin", + }); + + if (!res.ok) { + let detail = "request failed"; + try { + const body = await res.json(); + detail = body.error || detail; + } catch { } + throw new Error(`${res.status}: ${detail}`); + } + + if (res.status === 204) return null; + return res.json(); +} + +// --- Utils --- + +function shortName(name) { + if (!name || typeof name !== 'string') return "?"; + const trimmed = name.trim(); + if (!trimmed) return "?"; + const words = trimmed.split(/\s+/).filter(w => w.length > 0).slice(0, 2); + if (words.length === 0) return "?"; + if (words.length === 1) return words[0].substring(0, 2).toUpperCase(); + return words.map((w) => w[0]?.toUpperCase() || "").join(""); +} + +function escapeHtml(s) { + if (s === null || s === undefined) return ""; + return String(s) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function formatDate(isoString) { + const d = new Date(isoString); + const now = new Date(); + const isToday = d.toDateString() === now.toDateString(); + + if (isToday) { + return `Today at ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; + } + return d.toLocaleDateString(); +} + +let audioCtx = null; + +// Global interaction listener to unlock AudioContext (autoplay policy) +window.addEventListener('click', () => { + if (audioCtx && audioCtx.state === 'suspended') { + audioCtx.resume(); + } +}, { once: true }); + +function playSound(type) { + try { + if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + if (audioCtx.state === 'suspended') audioCtx.resume(); + + const sounds = { + message: { freq: 880, type: 'sine', duration: 0.1, volume: 0.1 }, + dm: { freq: [660, 880], type: 'sine', duration: 0.15, volume: 0.1 }, + join: { freq: [440, 880], type: 'sine', duration: 0.2, volume: 0.1 }, + leave: { freq: [880, 440], type: 'sine', duration: 0.2, volume: 0.1 }, + 'peer-join': { freq: [660, 990], type: 'sine', duration: 0.15, volume: 0.05 }, + 'peer-leave': { freq: [990, 660], type: 'sine', duration: 0.15, volume: 0.05 }, + }; + + const s = sounds[type]; + if (!s) return; + + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + + osc.type = s.type; + if (Array.isArray(s.freq)) { + osc.frequency.setValueAtTime(s.freq[0], audioCtx.currentTime); + osc.frequency.exponentialRampToValueAtTime(s.freq[1], audioCtx.currentTime + s.duration); + } else { + osc.frequency.setValueAtTime(s.freq, audioCtx.currentTime); + } + + gain.gain.setValueAtTime(s.volume, audioCtx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + s.duration); + + osc.connect(gain); + gain.connect(audioCtx.destination); + + osc.start(); + osc.stop(audioCtx.currentTime + s.duration); + } catch (err) { + console.warn("playSound failed", err); + } +} +const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId"; + +// --- Renderers --- + +function renderGuilds() { + el.guildList.innerHTML = ""; + + for (const guild of state.guilds) { + const btn = document.createElement("button"); + btn.className = `guild-pill ${state.selectedGuildId === guild.id ? "active" : ""}`; + btn.title = guild.name; + btn.textContent = shortName(guild.name); + btn.onclick = async () => { + state.selectedGuildId = guild.id; + state.selectedTextChannelId = null; + state.selectedVoiceChannelId = null; + state.selectedDmUserId = null; + state.selectedDmDisplayName = null; + localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id); + renderGuilds(); + renderDMs(); + await loadChannels(); + await loadGuildMembers(); + await refreshVoicePresence(); + startVoicePresencePolling(); + renderMessages([]); + updateHeaderLabels(); + }; + el.guildList.appendChild(btn); + } +} + +function renderChannels() { + el.channelList.innerHTML = ""; + el.voiceChannelList.innerHTML = ""; + + for (const channel of state.channels) { + const row = document.createElement("button"); + row.className = `channel-row ${(channel.kind === 'text' && state.selectedTextChannelId === channel.id) || + (channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : "" + }`; + + const iconName = channel.kind === 'text' ? 'hash' : 'volume-2'; + row.innerHTML = ` ${escapeHtml(channel.name)}`; + + row.onclick = async () => { + if (channel.kind === 'text') { + state.selectedDmUserId = null; + state.selectedDmDisplayName = null; + state.selectedTextChannelId = channel.id; + state.voice.viewMode = 'chat'; + renderChannels(); + renderDMs(); + updateView(); + updateHeaderLabels(); + const messages = await api(`/channels/${channel.id}/messages?limit=100`); + renderMessages(messages); + } else { + state.selectedVoiceChannelId = channel.id; + state.voice.viewMode = 'video'; + renderChannels(); + renderDMs(); + updateView(); + updateHeaderLabels(); + joinVoice(); + } + if (window.innerWidth <= 768) closeMobileMenus(); + }; + + if (channel.kind === 'text') { + el.channelList.appendChild(row); + } else { + el.voiceChannelList.appendChild(row); + + const participants = state.voicePresence.get(channel.id) || []; + if (participants.length > 0) { + const pList = document.createElement("div"); + pList.className = "voice-row-members channel-list"; + pList.style.paddingLeft = "24px"; + for (const p of participants) { + const pRow = document.createElement("div"); + pRow.className = `channel-row ${p.is_speaking ? 'voice-speaking' : ''}`; + pRow.style.padding = "2px 8px"; + pRow.innerHTML = `
${shortName(p.display_name)}
${escapeHtml(p.display_name)}`; + pList.appendChild(pRow); + } + el.voiceChannelList.appendChild(pList); + } + } + } + lucide.createIcons(); +} + +function renderDMs() { + el.dmList.innerHTML = ""; + for (const dm of state.dmConversations) { + const isOnline = state.onlineUsers.has(dm.user_id); + const row = document.createElement("button"); + row.className = `channel-row ${state.selectedDmUserId === dm.user_id ? "active" : ""}`; + row.innerHTML = ` +
+ +
+
+ ${escapeHtml(dm.display_name)} + `; + row.onclick = async () => { + state.selectedDmUserId = dm.user_id; + state.selectedDmDisplayName = dm.display_name; + state.selectedTextChannelId = null; + state.selectedVoiceChannelId = null; + state.voice.viewMode = 'chat'; + renderChannels(); + renderDMs(); + updateView(); + updateHeaderLabels(); + const messages = await api(`/dms/${dm.user_id}/messages?limit=100`); + renderMessages(messages); + }; + el.dmList.appendChild(row); + } + + lucide.createIcons(); +} + +function renderMessages(messages) { + el.messageList.innerHTML = ""; + let lastAuthorId = null; + let lastTime = null; + + for (const m of messages.slice().reverse()) { + const row = document.createElement("div"); + const mDate = new Date(m.created_at); + const isGrouped = lastAuthorId === m.author_user_id && + lastTime && (mDate - lastTime < 300000); // 5 minutes + + row.className = `msg ${isGrouped ? "msg-grouped" : ""}`; + + const displayName = m.author_display_name || "Unknown User"; + + if (isGrouped) { + row.innerHTML = `
${escapeHtml(m.body)}
`; + } else { + row.innerHTML = ` +
${shortName(displayName)}
+
+
+ ${escapeHtml(displayName)} + ${formatDate(m.created_at)} +
+
${escapeHtml(m.body)}
+
+ `; + } + el.messageList.appendChild(row); + lastAuthorId = m.author_user_id; + lastTime = mDate; + } + + if (messages.length > 0) { + state.lastMessageId = messages[0].id; + } + + el.messageList.scrollTop = el.messageList.scrollHeight; +} + +function renderMembers() { + el.memberList.innerHTML = ""; + for (const m of state.members) { + const isOnline = state.onlineUsers.has(m.id); + const row = document.createElement("div"); + row.className = "member-row"; + row.innerHTML = ` +
+ ${shortName(m.display_name)} +
+
+
${escapeHtml(m.display_name)}
+ `; + row.style.cursor = "pointer"; + row.onclick = async () => { + if (state.me && m.id === state.me.id) return; + state.selectedDmUserId = m.id; + state.selectedDmDisplayName = m.display_name; + state.selectedTextChannelId = null; + state.selectedVoiceChannelId = null; + state.voice.viewMode = 'chat'; + renderChannels(); + renderDMs(); + updateView(); + updateHeaderLabels(); + const messages = await api(`/dms/${m.id}/messages?limit=100`); + renderMessages(messages); + }; + el.memberList.appendChild(row); + } +} + +function updateHeaderLabels() { + const guild = state.guilds.find((g) => g.id === state.selectedGuildId); + el.guildTitle.textContent = guild ? guild.name : "No server selected"; + + if (state.selectedDmUserId) { + const dmFromConversations = state.dmConversations.find((u) => u.user_id === state.selectedDmUserId); + const dmFromMembers = state.members.find((m) => m.id === state.selectedDmUserId); + const displayName = dmFromConversations?.display_name || dmFromMembers?.display_name || state.selectedDmDisplayName; + const dmName = displayName ? `@${displayName}` : "Direct Message"; + el.channelTitle.textContent = dmName; + el.messageBody.placeholder = displayName + ? `Message @${displayName}` + : "Message"; + return; + } + + if (state.voice.viewMode === 'video' && state.selectedVoiceChannelId) { + const channel = state.channels.find((c) => c.id === state.selectedVoiceChannelId); + el.channelTitle.textContent = channel ? channel.name : "Voice Arena"; + return; + } + + const channel = state.channels.find((c) => c.id === state.selectedTextChannelId); + el.channelTitle.textContent = channel ? channel.name : "Select a channel"; + el.messageBody.placeholder = channel ? `Message #${channel.name}` : "Select a channel"; +} + +async function createInviteLink() { + if (!state.selectedGuildId) { + alert("Select a server first."); + return; + } + try { + const invite = await api(`/guilds/${state.selectedGuildId}/invites`, { + method: "POST", + body: JSON.stringify({ max_uses: 50, expires_in_hours: 24 }), + }); + const link = `${location.origin}/?invite=${encodeURIComponent(invite.code)}`; + try { + await navigator.clipboard.writeText(link); + alert(`Invite link copied:\n${link}`); + } catch { + prompt("Copy invite link:", link); + } + } catch (err) { + alert(err.message); + } +} + +// --- Logic --- + +async function loadGuilds() { + state.guilds = await api("/guilds"); + renderGuilds(); +} + +async function loadDMConversations() { + state.dmConversations = await api("/dms"); + renderDMs(); +} + +async function loadGuildMembers() { + if (!state.selectedGuildId) { + state.members = []; + renderMembers(); + return; + } + state.members = await api(`/guilds/${state.selectedGuildId}/members`); + renderMembers(); +} + +async function loadChannels() { + if (!state.selectedGuildId) { + state.channels = []; + renderChannels(); + return; + } + + state.channels = await api(`/guilds/${state.selectedGuildId}/channels`); + renderChannels(); +} + +async function refreshVoicePresence() { + if (!state.selectedGuildId) { + state.voicePresence.clear(); + renderChannels(); + return; + } + + try { + const res = await api(`/guilds/${state.selectedGuildId}/voice-presence`); + state.voicePresence.clear(); + for (const entry of res.channels || []) { + state.voicePresence.set(entry.channel_id, entry.participants || []); + } + renderChannels(); + } catch (err) { + console.warn("voice presence failed", err); + } +} + +function startVoicePresencePolling() { + if (state.voicePresencePollId) clearInterval(state.voicePresencePollId); + state.voicePresencePollId = setInterval(() => { + refreshVoicePresence().catch(() => { }); + }, 3000); +} + +function initChatWs() { + if (state.chatWs) state.chatWs.close(); + const protocol = location.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = `${protocol}//${location.host}/ws`; + const ws = new WebSocket(wsUrl); + state.chatWs = ws; + + ws.onmessage = (event) => { + const msg = JSON.parse(event.data); + const authorId = msg.message?.author_user_id; + const isFromMe = authorId && state.me && authorId === state.me.id; + + if (msg.type === "message_created") { + if (!isFromMe) playSound('message'); + if (state.selectedTextChannelId === msg.channel_id) { + api(`/channels/${state.selectedTextChannelId}/messages?limit=100`).then(renderMessages); + } + } else if (msg.type === "dm_created") { + if (!isFromMe) playSound('dm'); + if (state.selectedDmUserId === msg.other_user_id) { + api(`/dms/${state.selectedDmUserId}/messages?limit=100`).then(renderMessages); + playSound('dm'); + loadDMConversations().then(renderDMs); + } else { + loadDMConversations().then(renderDMs); + } + } else if (msg.type === "user_presence") { + if (msg.online) { + state.onlineUsers.add(msg.user_id); + } else { + state.onlineUsers.delete(msg.user_id); + } + renderMembers(); + renderDMs(); + } + }; + + ws.onclose = () => { + console.log("Chat WS closed, reconnecting..."); + setTimeout(initChatWs, 3000); + }; +} + +// --- Voice --- + +function getVoiceWsUrl(channelId) { + const proto = location.protocol === "https:" ? "wss" : "ws"; + return `${proto}://${location.host}/channels/${channelId}/voice/ws`; +} + +function shouldInitiateOffer(peerId) { + if (!state.me || !state.me.id) return false; + return state.me.id > peerId; +} + +function stopAndClearAudioPipeline() { + if (state.voice.localStream) { + for (const track of state.voice.localStream.getTracks()) track.stop(); + } + if (state.voice.rawStream && state.voice.rawStream !== state.voice.localStream) { + for (const track of state.voice.rawStream.getTracks()) track.stop(); + } + if (state.voice.audioContext) { + state.voice.audioContext.close().catch(() => { }); + } + state.voice.localStream = null; + state.voice.rawStream = null; + state.voice.audioContext = null; +} + +function stopAndClearVideoPipeline() { + if (state.voice.videoStream) { + for (const track of state.voice.videoStream.getTracks()) track.stop(); + } + state.voice.videoStream = null; + state.voice.sharingVideo = false; + document.getElementById(`video-${state.me.id}-camera`)?.parentElement?.remove(); +} + +function stopAndClearScreenPipeline() { + if (state.voice.screenStream) { + for (const track of state.voice.screenStream.getTracks()) track.stop(); + } + state.voice.screenStream = null; + state.voice.sharingScreen = false; + document.getElementById(`video-${state.me.id}-screen`)?.parentElement?.remove(); +} + +function updateVideoGridVisibility() { + // Persistence logic: we always update the grid visibility based on participants + // however the actual container visibility in the UI is managed by updateView() +} + +function updateView() { + if (state.voice.viewMode === 'video') { + el.videoGrid.classList.remove("hidden"); + el.messageList.classList.add("hidden"); + el.messageInputWrapper.classList.add("hidden"); + } else { + el.videoGrid.classList.add("hidden"); + el.messageList.classList.remove("hidden"); + el.messageInputWrapper.classList.remove("hidden"); + } +} + +function renderVideo(peerId, displayName, stream, source) { + const videoId = `video-${peerId}-${source}`; + let videoEl = document.getElementById(videoId); + if (!videoEl) { + const container = document.createElement("div"); + container.className = "video-item"; + const label = source === 'screen' ? `${escapeHtml(displayName)}'s Screen` : escapeHtml(displayName); + container.innerHTML = ` + +
${label}
+ `; + el.videoGrid.appendChild(container); + videoEl = container.querySelector("video"); + } + videoEl.srcObject = stream; +} + +async function buildAudioPipeline(rawStream) { + const ctx = new AudioContext(); + state.voice.audioContext = ctx; + const source = ctx.createMediaStreamSource(rawStream); + + // Metering/Speaking detection + const analyser = ctx.createAnalyser(); + analyser.fftSize = 512; + source.connect(analyser); + + const dataArray = new Uint8Array(analyser.frequencyBinCount); + let localIsSpeaking = false; + let speakCounter = 0; + + const checkVolume = () => { + if (!state.voice.audioContext || state.voice.audioContext.state === 'closed') return; + analyser.getByteFrequencyData(dataArray); + let sum = 0; + for (let i = 0; i < dataArray.length; i++) sum += dataArray[i]; + const avg = sum / dataArray.length; + + const isCurrentlySpeaking = avg > 30; // Calibrated for normal speech + + if (isCurrentlySpeaking) { + speakCounter = Math.min(speakCounter + 1, 5); + } else { + speakCounter = Math.max(speakCounter - 1, 0); + } + + const newSpeakingState = speakCounter >= 2; + + if (newSpeakingState !== localIsSpeaking) { + localIsSpeaking = newSpeakingState; + if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { + state.voice.ws.send(JSON.stringify({ type: 'set_speaking_status', is_speaking: localIsSpeaking })); + } + // Local UI update + state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { + if (p.user_id === state.me.id) p.is_speaking = localIsSpeaking; + }); + renderChannels(); + const myVideo = document.getElementById(`video-${state.me.id}-camera`) || document.getElementById(`video-${state.me.id}-screen`); + if (myVideo) { + myVideo.parentElement.classList.toggle('speaking', localIsSpeaking); + } + } + setTimeout(checkVolume, 100); + }; + checkVolume(); + + const destination = ctx.createMediaStreamDestination(); + source.connect(destination); + return destination.stream; +} + +async function createLocalVoiceStream() { + const constraints = { + audio: { + channelCount: 1, + sampleRate: 48000, + echoCancellation: true, + noiseSuppression: true, + autoGainControl: false, + }, + video: false, + }; + const rawStream = await navigator.mediaDevices.getUserMedia(constraints); + const localStream = await buildAudioPipeline(rawStream); + state.voice.rawStream = rawStream; + state.voice.localStream = localStream; + if (state.voice.muted) { + state.voice.localStream.getAudioTracks().forEach((t) => { + t.enabled = false; + }); + } +} + +function ensurePeerConnection(peerId) { + if (state.voice.peerConnections.has(peerId)) { + return state.voice.peerConnections.get(peerId); + } + + const pc = new RTCPeerConnection({ iceServers: state.voice.iceServers }); + + if (state.voice.localStream) { + for (const track of state.voice.localStream.getTracks()) { + pc.addTrack(track, state.voice.localStream); + } + } + + if (state.voice.videoStream) { + for (const track of state.voice.videoStream.getTracks()) { + pc.addTrack(track, state.voice.videoStream); + } + } + + if (state.voice.screenStream) { + for (const track of state.voice.screenStream.getTracks()) { + pc.addTrack(track, state.voice.screenStream); + } + } + + pc.onicecandidate = (event) => { + if (!event.candidate || !state.voice.ws) return; + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: peerId, + kind: "ice", + data: event.candidate, + })); + }; + + pc.ontrack = (event) => { + if (event.track.kind === "audio") { + let audio = document.getElementById(`audio-${peerId}`); + if (!audio) { + audio = document.createElement("audio"); + audio.id = `audio-${peerId}`; + audio.autoplay = true; + audio.playsInline = true; + document.body.appendChild(audio); + } + audio.srcObject = event.streams[0]; + } else if (event.track.kind === "video") { + const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" }; + // Distinguish screen share from camera by checking track labels or signaling + // For simplicity, we can use the stream index or a dedicated track check. + // A more robust way is to have separate transceivers/signaling. + // Here we check if the label suggests a screen. + const isScreen = event.track.label.toLowerCase().includes('screen') || + event.track.label.toLowerCase().includes('monitor'); + renderVideo(peerId, peer.display_name, event.streams[0], isScreen ? 'screen' : 'camera'); + } + }; + + pc.onnegotiationneeded = async () => { + try { + await sendOffer(peerId); + } catch (err) { + console.error("negotiation failed", err); + } + }; + + state.voice.peerConnections.set(peerId, pc); + return pc; +} + +async function sendOffer(peerId) { + const pc = ensurePeerConnection(peerId); + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: peerId, + kind: "offer", + data: offer, + })); +} + +async function handleSignal(fromPeerId, kind, data) { + const pc = ensurePeerConnection(fromPeerId); + const polite = !shouldInitiateOffer(fromPeerId); + + try { + if (kind === "offer") { + const offerCollision = (pc.signalingState !== "stable"); + if (offerCollision) { + if (!polite) return; + await Promise.all([ + pc.setLocalDescription({ type: "rollback" }), + pc.setRemoteDescription(new RTCSessionDescription(data)) + ]); + } else { + await pc.setRemoteDescription(new RTCSessionDescription(data)); + } + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: fromPeerId, + kind: "answer", + data: answer, + })); + } else if (kind === "answer") { + await pc.setRemoteDescription(new RTCSessionDescription(data)); + } else if (kind === "ice") { + try { + await pc.addIceCandidate(data ? new RTCIceCandidate(data) : null); + } catch (err) { + if (pc.signalingState !== "stable") { + // Ignore ICE candidates during negotiation + } else { + console.warn("failed to add ice candidate", err); + } + } + } + } catch (err) { + console.error("handleSignal failed", err); + } +} + +async function joinVoice() { + if (!state.selectedVoiceChannelId) return; + if (state.voice.joinedChannelId === state.selectedVoiceChannelId) return; + + await leaveVoice(); + + try { + await createLocalVoiceStream(); + } catch (err) { + console.error("microphone denied", err); + return; + } + + const ws = new WebSocket(getVoiceWsUrl(state.selectedVoiceChannelId)); + state.voice.ws = ws; + state.voice.joinedChannelId = state.selectedVoiceChannelId; + + ws.onopen = () => { + const channel = state.channels.find(c => c.id === state.selectedVoiceChannelId); + el.vcChannelName.textContent = channel ? channel.name : "Voice"; + el.voiceConnection.classList.remove("hidden"); + el.soundboard.classList.remove("hidden"); + playSound('join'); + refreshVoicePresence().catch(() => { }); + loadSounds().catch(() => { }); + }; + + ws.onmessage = async (event) => { + const msg = JSON.parse(event.data); + if (msg.type === "peers") { + for (const peer of msg.peers) { + if (shouldInitiateOffer(peer.user_id)) await sendOffer(peer.user_id); + } + } else if (msg.type === "peer_joined") { + playSound('peer-join'); + if (shouldInitiateOffer(msg.user_id)) await sendOffer(msg.user_id); + } else if (msg.type === "peer_left") { + playSound('peer-leave'); + const pc = state.voice.peerConnections.get(msg.user_id); + if (pc) { + pc.close(); + state.voice.peerConnections.delete(msg.user_id); + } + document.getElementById(`audio-${msg.user_id}`)?.remove(); + } else if (msg.type === "signal") { + await handleSignal(msg.from_user_id, msg.kind, msg.data); + } else if (msg.type === "video_status_changed") { + if (!msg.is_sharing_video) { + document.getElementById(`video-${msg.user_id}-camera`)?.parentElement?.remove(); + } + } else if (msg.type === "screen_status_changed") { + if (!msg.is_sharing_screen) { + document.getElementById(`video-${msg.user_id}-screen`)?.parentElement?.remove(); + } + } else if (msg.type === "speaking_status_changed") { + state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { + if (p.user_id === msg.user_id) p.is_speaking = msg.is_speaking; + }); + renderChannels(); + const videoEl = document.getElementById(`video-${msg.user_id}-camera`) || document.getElementById(`video-${msg.user_id}-screen`); + if (videoEl) { + videoEl.parentElement.classList.toggle('speaking', msg.is_speaking); + } + } else if (msg.type === "play_sound") { + const audio = new Audio(msg.sound_url); + audio.play().catch(console.error); + } + refreshVoicePresence().catch(() => { }); + }; + + ws.onclose = () => { + el.voiceConnection.classList.add("hidden"); + el.soundboard.classList.add("hidden"); + for (const pc of state.voice.peerConnections.values()) pc.close(); + state.voice.peerConnections.clear(); + stopAndClearAudioPipeline(); + state.voice.joinedChannelId = null; + state.voice.ws = null; + playSound('leave'); + refreshVoicePresence().catch(() => { }); + }; +} + +async function leaveVoice() { + if (state.voice.ws) state.voice.ws.close(); +} + +function toggleMute() { + if (!state.voice.localStream) return; + state.voice.muted = !state.voice.muted; + state.voice.localStream.getAudioTracks().forEach((t) => { + t.enabled = !state.voice.muted; + }); + el.voiceMuteBtn.innerHTML = state.voice.muted ? '' : ''; + el.voiceMuteBtn.style.color = state.voice.muted ? 'var(--danger)' : 'var(--text-muted)'; + lucide.createIcons(); +} + +async function toggleVideo() { + if (state.voice.sharingVideo) { + stopAndClearVideoPipeline(); + if (state.voice.ws) { + state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: false })); + } + const trackId = `video-${state.me.id}-camera`; + for (const pc of state.voice.peerConnections.values()) { + const senders = pc.getSenders(); + const videoSender = senders.find(s => s.track && s.track.kind === "video" && !s.track.label.toLowerCase().includes('screen')); + if (videoSender) pc.removeTrack(videoSender); + } + } else { + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + state.voice.videoStream = stream; + state.voice.sharingVideo = true; + renderVideo(state.me.id, state.me.display_name, stream, 'camera'); + + if (state.voice.ws) { + state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: true })); + } + + for (const pc of state.voice.peerConnections.values()) { + for (const track of stream.getTracks()) { + pc.addTrack(track, stream); + } + } + } catch (err) { + console.error("camera denied", err); + alert("Could not access camera."); + return; + } + } + + el.voiceVideoBtn.innerHTML = state.voice.sharingVideo ? '' : ''; + el.voiceVideoBtn.style.color = state.voice.sharingVideo ? 'var(--green)' : 'var(--text-muted)'; + lucide.createIcons(); +} + +async function toggleScreenShare() { + if (state.voice.sharingScreen) { + stopAndClearScreenPipeline(); + if (state.voice.ws) { + state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: false })); + } + for (const pc of state.voice.peerConnections.values()) { + const senders = pc.getSenders(); + const screenSender = senders.find(s => s.track && s.track.kind === "video" && (s.track.label.toLowerCase().includes('screen') || s.track.label.toLowerCase().includes('monitor'))); + if (screenSender) pc.removeTrack(screenSender); + } + } else { + try { + const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); + state.voice.screenStream = stream; + state.voice.sharingScreen = true; + renderVideo(state.me.id, state.me.display_name, stream, 'screen'); + + // Stop sharing if user clicks "Stop sharing" in browser UI + stream.getVideoTracks()[0].onended = () => { + if (state.voice.sharingScreen) toggleScreenShare(); + }; + + if (state.voice.ws) { + state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: true })); + } + + for (const pc of state.voice.peerConnections.values()) { + for (const track of stream.getTracks()) { + pc.addTrack(track, stream); + } + } + } catch (err) { + console.error("screen share denied", err); + return; + } + } + + el.voiceScreenBtn.innerHTML = state.voice.sharingScreen ? '' : ''; + el.voiceScreenBtn.style.color = state.voice.sharingScreen ? 'var(--green)' : 'var(--text-muted)'; + lucide.createIcons(); +} + +function toggleWatchVideo() { + state.voice.watchingVideo = !state.voice.watchingVideo; + updateVideoGridVisibility(); + + el.voiceWatchBtn.innerHTML = state.voice.watchingVideo ? '' : ''; + el.voiceWatchBtn.style.color = state.voice.watchingVideo ? 'var(--green)' : 'var(--danger)'; + el.voiceWatchBtn.title = state.voice.watchingVideo ? 'Stop Watching' : 'Start Watching'; + lucide.createIcons(); +} + +// --- Sound Board --- + +async function loadSounds() { + if (!state.selectedGuildId) return; + try { + const sounds = await api(`/guilds/${state.selectedGuildId}/sounds`); + renderSounds(sounds); + } catch (err) { + console.error("failed to load sounds", err); + } +} + +function renderSounds(sounds) { + el.soundboardGrid.innerHTML = ''; + sounds.forEach(sound => { + const item = document.createElement('div'); + item.className = 'sound-item'; + item.innerHTML = ` +
${sound.icon}
+
${sound.name}
+ `; + item.onclick = () => playRemoteSound(sound.file_path); + el.soundboardGrid.appendChild(item); + }); +} + +function playRemoteSound(url) { + if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { + state.voice.ws.send(JSON.stringify({ type: 'play_sound', sound_url: url })); + } + // Also play locally immediately + const audio = new Audio(url); + audio.play().catch(console.error); +} + +// --- Mobile Logic --- + +function toggleMobileMenu() { + const isOpen = el.main.classList.contains("menu-open"); + if (isOpen) { + el.main.classList.remove("menu-open"); + el.overlay.classList.add("hidden"); + } else { + el.main.classList.add("menu-open"); + el.utilitySidebar.classList.remove("active"); // close members if open + el.overlay.classList.remove("hidden"); + } +} + +function toggleMobileMembers() { + const isOpen = el.utilitySidebar.classList.contains("active"); + if (isOpen) { + el.utilitySidebar.classList.remove("active"); + el.overlay.classList.add("hidden"); + } else { + el.utilitySidebar.classList.add("active"); + el.main.classList.remove("menu-open"); // close menu if open + el.overlay.classList.remove("hidden"); + } +} + +function closeMobileMenus() { + el.main.classList.remove("menu-open"); + el.utilitySidebar.classList.remove("active"); + el.overlay.classList.add("hidden"); +} + +// --- Initialization --- + +async function init() { + lucide.createIcons(); + + el.loginBtn.onclick = () => { location.href = "/auth/login"; }; + + el.logoutBtn.onclick = async () => { + await leaveVoice(); + await api("/auth/logout", { method: "POST" }); + location.reload(); + }; + + el.addGuildBtn.onclick = () => { el.modalContainer.classList.remove("hidden"); }; + el.createInviteBtn.onclick = createInviteLink; + el.modalCancel.onclick = () => { el.modalContainer.classList.add("hidden"); }; + + el.addTextBtn.onclick = () => { + if (!state.selectedGuildId) { + alert("Create or select a server first."); + return; + } + document.querySelector('input[name="channel-kind"][value="text"]').checked = true; + el.channelModal.classList.remove("hidden"); + }; + el.addVoiceBtn.onclick = () => { + if (!state.selectedGuildId) { + alert("Create or select a server first."); + return; + } + document.querySelector('input[name="channel-kind"][value="voice"]').checked = true; + el.channelModal.classList.remove("hidden"); + }; + el.channelModalCancel.onclick = () => { el.channelModal.classList.add("hidden"); }; + + // Mobile Listeners + if (el.mobileMenuBtn) el.mobileMenuBtn.onclick = toggleMobileMenu; + if (el.mobileMembersBtn) el.mobileMembersBtn.onclick = toggleMobileMembers; + if (el.overlay) el.overlay.onclick = closeMobileMenus; + + el.guildForm.onsubmit = async (e) => { + e.preventDefault(); + try { + const guild = await api("/guilds", { + method: "POST", + body: JSON.stringify({ name: el.guildName.value }), + }); + el.guildName.value = ""; + el.modalContainer.classList.add("hidden"); + state.guilds.push(guild); + state.selectedGuildId = guild.id; + state.selectedTextChannelId = null; + state.selectedVoiceChannelId = null; + state.selectedDmUserId = null; + state.selectedDmDisplayName = null; + localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id); + renderGuilds(); + await loadChannels(); + await loadGuildMembers(); + await refreshVoicePresence(); + renderMessages([]); + updateHeaderLabels(); + } catch (err) { alert(err.message); } + }; + + el.channelForm.onsubmit = async (e) => { + e.preventDefault(); + if (!state.selectedGuildId) { + alert("Select a server first."); + return; + } + const kindInput = document.querySelector('input[name="channel-kind"]:checked'); + const kind = kindInput ? kindInput.value : "text"; + const channelName = el.channelName.value.trim(); + if (!channelName) { + alert("Channel name is required."); + return; + } + try { + const created = await api("/channels", { + method: "POST", + body: JSON.stringify({ + guild_id: state.selectedGuildId, + name: channelName, + kind: kind, + }), + }); + el.channelName.value = ""; + el.channelModal.classList.add("hidden"); + await loadChannels(); + if (created.kind === "text") { + state.selectedTextChannelId = created.id; + state.selectedVoiceChannelId = null; + state.selectedDmUserId = null; + state.selectedDmDisplayName = null; + renderChannels(); + renderDMs(); + updateHeaderLabels(); + const messages = await api(`/channels/${created.id}/messages?limit=100`); + renderMessages(messages); + } else { + state.selectedVoiceChannelId = created.id; + state.selectedTextChannelId = null; + state.selectedDmUserId = null; + state.selectedDmDisplayName = null; + renderChannels(); + renderDMs(); + updateHeaderLabels(); + } + } catch (err) { alert(err.message); } + }; + + el.messageForm.onsubmit = async (e) => { + e.preventDefault(); + const body = el.messageBody.value.trim(); + if (!body) return; + + try { + if (state.selectedTextChannelId) { + await api(`/channels/${state.selectedTextChannelId}/messages`, { + method: "POST", + body: JSON.stringify({ body }), + }); + } else if (state.selectedDmUserId) { + await api(`/dms/${state.selectedDmUserId}/messages`, { + method: "POST", + body: JSON.stringify({ body }), + }); + } else { + return; + } + el.messageBody.value = ""; + const messages = state.selectedTextChannelId + ? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`) + : await api(`/dms/${state.selectedDmUserId}/messages?limit=100`); + renderMessages(messages); + await loadDMConversations(); + renderDMs(); + } catch (err) { alert(err.message); } + }; + + el.voiceVideoBtn.onclick = toggleVideo; + el.voiceScreenBtn.onclick = toggleScreenShare; + el.voiceMuteBtn.onclick = toggleMute; + el.voiceLeaveBtn.onclick = leaveVoice; + + el.addSoundBtn.onclick = () => { + el.soundModal.classList.remove("hidden"); + }; + el.soundModalCancel.onclick = () => { + el.soundModal.classList.add("hidden"); + }; + + el.soundForm.onsubmit = async (e) => { + e.preventDefault(); + if (!state.selectedGuildId) return; + + const formData = new FormData(); + formData.append('name', el.soundName.value); + formData.append('icon', el.soundIcon.value); + formData.append('file', el.soundFile.files[0]); + + el.soundSubmitBtn.disabled = true; + el.soundSubmitBtn.textContent = 'Uploading...'; + + try { + // Need to use fetch directly because api() might not handle FormData correctly depending on implementation + const response = await fetch(`/guilds/${state.selectedGuildId}/sounds`, { + method: 'POST', + body: formData + }); + if (!response.ok) { + const err = await response.json(); + throw new Error(err.error || 'Upload failed'); + } + el.soundModal.classList.add("hidden"); + el.soundForm.reset(); + await loadSounds(); + } catch (err) { + alert(err.message); + } finally { + el.soundSubmitBtn.disabled = false; + el.soundSubmitBtn.textContent = 'Add Sound'; + } + }; + + try { + state.me = await api("/me"); + if (state.me && state.me.display_name) { + el.userName.textContent = state.me.display_name; + el.userAvatar.textContent = shortName(state.me.display_name); + } + el.authScreen.classList.add("hidden"); + el.main.classList.remove("hidden"); + + const params = new URLSearchParams(location.search); + const inviteCode = params.get("invite"); + if (inviteCode) { + try { + const joinedGuild = await api(`/invites/${encodeURIComponent(inviteCode)}/join`, { + method: "POST", + }); + localStorage.setItem(LAST_GUILD_STORAGE_KEY, joinedGuild.id); + } catch (err) { + alert(`Failed to join invite: ${err.message}`); + } finally { + params.delete("invite"); + const nextQuery = params.toString(); + const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`; + history.replaceState(null, "", nextUrl); + } + } + + await loadGuilds(); + await loadDMConversations(); + + try { + const online = await api("/presence"); + state.onlineUsers = new Set(online); + } catch (err) { console.warn("presence sync failed", err); } + + if (state.guilds.length > 0) { + const lastGuildId = localStorage.getItem(LAST_GUILD_STORAGE_KEY); + const guild = state.guilds.find((g) => g.id === lastGuildId) || state.guilds[0]; + state.selectedGuildId = guild.id; + localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id); + renderGuilds(); + await loadChannels(); + await loadGuildMembers(); + await refreshVoicePresence(); + updateHeaderLabels(); + } else { + state.members = []; + renderMembers(); + updateHeaderLabels(); + } + + initChatWs(); + startVoicePresencePolling(); + lucide.createIcons(); + } catch (err) { + console.error("init failed", err); + el.authScreen.classList.remove("hidden"); + el.main.classList.add("hidden"); + } +} + +init(); diff --git a/static/index.html b/static/index.html index 550c656..e13e17c 100644 --- a/static/index.html +++ b/static/index.html @@ -1,4 +1,3 @@ - @@ -6,9 +5,12 @@ Chattz + + + - + @@ -18,11 +20,6 @@

Chattz

Sign in to open your servers.

-
- Desktop downloads: - Windows - Linux (RPM) -

@@ -43,7 +40,6 @@