diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 00000000..8af91ca6 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' + +const stagedPackageJson = spawnSync('git', ['diff', '--cached', '--name-only', '--', 'package.json'], { + encoding: 'utf8', +}) + +if (stagedPackageJson.status !== 0) { + process.stderr.write(stagedPackageJson.stderr || 'Failed to inspect staged package.json\n') + process.exit(stagedPackageJson.status ?? 1) +} + +const hasStagedPackageJson = stagedPackageJson.stdout + .split(/\r?\n/u) + .map(line => line.trim()) + .includes('package.json') + +if (!hasStagedPackageJson) { + process.exit(0) +} + +const packageJsonPath = 'package.json' +const packageJson = fs.readFileSync(packageJsonPath, 'utf8') +const strippedPackageJson = spawnSync(process.execPath, ['scripts/strip-package-buildinfo.cjs'], { + encoding: 'utf8', + input: packageJson, +}) + +if (strippedPackageJson.status !== 0) { + process.stderr.write(strippedPackageJson.stderr || 'Failed to strip package.json buildInfo\n') + process.exit(strippedPackageJson.status ?? 1) +} + +if (strippedPackageJson.stdout !== packageJson) { + fs.writeFileSync(packageJsonPath, strippedPackageJson.stdout, 'utf8') +} + +const addPackageJson = spawnSync('git', ['add', '--', packageJsonPath], { + stdio: 'inherit', +}) + +process.exit(addPackageJson.status ?? 1) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index face8cc2..c7cde25d 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -26,6 +26,7 @@ jobs: - uses: dorny/paths-filter@v4.0.1 id: filter with: + base: ${{ github.ref_name }} list-files: shell filters: | build: @@ -35,7 +36,6 @@ jobs: - 'nativeModules/**' - 'scripts/**' - 'scriptsInstaller/**' - - '!packaging/**' - 'package.json' - 'yarn.lock' - '.yarnrc.yml' @@ -43,12 +43,12 @@ jobs: - 'vite.main.config.ts' - 'vite.preload.config.ts' - 'vite.renderer.config.ts' + - 'vite.worker.config.ts' - 'electron-builder.yml' - 'tsconfig*.json' - 'Info.plist' - 'graphql.config.yml' - 'schema.graphql' - - '.github/workflows/build-dev-windows.yml' - name: Report changed build-related files shell: bash @@ -121,12 +121,21 @@ jobs: cache: yarn cache-dependency-path: '**/yarn.lock' + - name: Set up Rust + shell: bash + run: | + rustup toolchain install 1.88.0 --profile minimal --no-self-update + rustup default 1.88.0 + - uses: actions/cache@v5 with: path: | nativeModules/**/node_modules nativeModules/**/build - key: native-modules-${{ runner.os }}-${{ runner.arch }}-node22-${{ hashFiles('nativeModules/**/package.json', 'nativeModules/**/yarn.lock', 'nativeModules/**/binding.gyp', 'nativeModules/**/*.c', 'nativeModules/**/*.cc', 'nativeModules/**/*.cpp', 'nativeModules/**/*.h', 'nativeModules/**/*.hpp') }} + nativeModules/**/target + ~/.cargo/registry + ~/.cargo/git + key: native-modules-${{ runner.os }}-${{ runner.arch }}-node22-rust188-${{ hashFiles('nativeModules/**/package.json', 'nativeModules/**/Cargo.toml', 'nativeModules/**/Cargo.lock', 'nativeModules/**/build.rs', 'nativeModules/**/*.rs') }} - name: Create .env shell: bash @@ -140,11 +149,20 @@ jobs: - name: Install deps run: yarn install --frozen-lockfile + - name: Check Sentry CLI + run: yarn -s sentry-cli --version + - name: Build and publish dev run: yarn build:package --nativeModules --publish dev env: BUILD_VERSION: ${{ needs.version.outputs.build_version }} S3_KEEP_RECENT_VERSIONS: 3 + GLITCHTIP_SOURCEMAPS: "1" + GLITCHTIP_SOURCEMAPS_UPLOAD: "1" + SENTRY_URL: https://issues.pulsesync.dev + SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }} + SENTRY_ORG: ${{ vars.GLITCHTIP_ORG }} + SENTRY_PROJECT: ${{ vars.GLITCHTIP_PROJECT }} - name: Show release contents shell: bash diff --git a/.github/workflows/build-manual.yml b/.github/workflows/build-manual.yml index 98d874fe..f662c6f8 100644 --- a/.github/workflows/build-manual.yml +++ b/.github/workflows/build-manual.yml @@ -73,12 +73,21 @@ jobs: cache: yarn cache-dependency-path: '**/yarn.lock' + - name: Set up Rust + shell: bash + run: | + rustup toolchain install 1.88.0 --profile minimal --no-self-update + rustup default 1.88.0 + - uses: actions/cache@v5 with: path: | nativeModules/**/node_modules nativeModules/**/build - key: native-modules-${{ runner.os }}-${{ runner.arch }}-node22-${{ hashFiles('nativeModules/**/package.json', 'nativeModules/**/yarn.lock', 'nativeModules/**/binding.gyp', 'nativeModules/**/*.c', 'nativeModules/**/*.cc', 'nativeModules/**/*.cpp', 'nativeModules/**/*.h', 'nativeModules/**/*.hpp') }} + nativeModules/**/target + ~/.cargo/registry + ~/.cargo/git + key: native-modules-${{ runner.os }}-${{ runner.arch }}-node22-rust188-${{ hashFiles('nativeModules/**/package.json', 'nativeModules/**/Cargo.toml', 'nativeModules/**/Cargo.lock', 'nativeModules/**/build.rs', 'nativeModules/**/*.rs') }} - name: Create .env shell: bash diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 93653a09..a8e60877 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -22,12 +22,21 @@ jobs: cache: yarn cache-dependency-path: '**/yarn.lock' + - name: Set up Rust + shell: bash + run: | + rustup toolchain install 1.88.0 --profile minimal --no-self-update + rustup default 1.88.0 + - uses: actions/cache@v5 with: path: | nativeModules/**/node_modules nativeModules/**/build - key: native-modules-${{ runner.os }}-${{ runner.arch }}-node22-${{ hashFiles('nativeModules/**/package.json', 'nativeModules/**/yarn.lock', 'nativeModules/**/binding.gyp', 'nativeModules/**/*.c', 'nativeModules/**/*.cc', 'nativeModules/**/*.cpp', 'nativeModules/**/*.h', 'nativeModules/**/*.hpp') }} + nativeModules/**/target + ~/.cargo/registry + ~/.cargo/git + key: native-modules-${{ runner.os }}-${{ runner.arch }}-node22-rust188-${{ hashFiles('nativeModules/**/package.json', 'nativeModules/**/Cargo.toml', 'nativeModules/**/Cargo.lock', 'nativeModules/**/build.rs', 'nativeModules/**/*.rs') }} - name: Create .env if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 24b767b0..98397590 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -25,12 +25,21 @@ jobs: cache: yarn cache-dependency-path: '**/yarn.lock' + - name: Set up Rust + shell: bash + run: | + rustup toolchain install 1.88.0 --profile minimal --no-self-update + rustup default 1.88.0 + - uses: actions/cache@v5 with: path: | nativeModules/**/node_modules nativeModules/**/build - key: native-modules-${{ runner.os }}-${{ runner.arch }}-node22-${{ hashFiles('nativeModules/**/package.json', 'nativeModules/**/yarn.lock', 'nativeModules/**/binding.gyp', 'nativeModules/**/*.c', 'nativeModules/**/*.cc', 'nativeModules/**/*.cpp', 'nativeModules/**/*.h', 'nativeModules/**/*.hpp') }} + nativeModules/**/target + ~/.cargo/registry + ~/.cargo/git + key: native-modules-${{ runner.os }}-${{ runner.arch }}-node22-rust188-${{ hashFiles('nativeModules/**/package.json', 'nativeModules/**/Cargo.toml', 'nativeModules/**/Cargo.lock', 'nativeModules/**/build.rs', 'nativeModules/**/*.rs') }} - name: Create .env shell: bash @@ -44,11 +53,20 @@ jobs: - name: Install deps run: yarn install --frozen-lockfile + - name: Check Sentry CLI + run: yarn -s sentry-cli --version + - name: Build (package) run: yarn build:package --nativeModules ${{ startsWith(matrix.os, 'macos-') && runner.arch == 'X64' && '--mac-x64' || '' }} env: BUILD_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }} PUBLISH_BRANCH_FROM_TAG: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }} + GLITCHTIP_SOURCEMAPS: "1" + GLITCHTIP_SOURCEMAPS_UPLOAD: "1" + SENTRY_URL: https://issues.pulsesync.dev + SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }} + SENTRY_ORG: ${{ vars.GLITCHTIP_ORG }} + SENTRY_PROJECT: ${{ vars.GLITCHTIP_PROJECT }} - name: Show release contents (debug) shell: bash diff --git a/.gitignore b/.gitignore index def09a2f..3c8a8fc9 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,7 @@ typings/ # Vite .vite/ +.glitchtip-sourcemaps/ *.zip diff --git a/PATCHNOTES.md b/PATCHNOTES.md index 0beebf48..c7b22498 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,21 +1,27 @@ ### Новое -- Добавили поддержку системного прокси. PulseSync теперь корректнее работает в сетях, где интернет идёт через прокси. -- Добавили окно розыгрышей подписок и уведомления по ним. +- Добавили нативные кнопки управления окном на macOS. +- Добавили поддержку системного прокси при загрузке и установке мода. +- Добавили уведомления о покупке подписки, окончании подписки и активных розыгрышах. +- Обновили внешний вид профиля пользователя. ### Улучшено -- Улучшили установку Yandex Music и порядок проверки прав на macOS. -- Улучшили публикацию аддонов: окно публикации стало стабильнее. +- Установка и обновление мода стали стабильнее и лучше восстанавливаются после ошибок. +- Улучшили импорт `.pext` и `.zip` аддонов: PulseSync теперь проверяет архив перед установкой и сохраняет настройки существующего аддона. +- Улучшили работу аддонов после перезапуска PulseSync: активные темы, скрипты и настройки синхронизируются корректнее. +- Улучшили сообщения об ошибках при установке мода и обновлении приложения. - Обновили лицензионные файлы. ### Исправлено -- Настройки аддонов больше не сбрасываются при установке или обновлении аддона. -- Если у аддона нет README, но есть патчноуты, теперь по умолчанию открываются патчноуты, а не настройки. -- Toast-уведомления больше не оставляют невидимую область, которая мешает нажимать кнопки, и снова закрываются по клику. -- Исправили фокус окна после успешной авторизации. -- Исправили ошибку с форматом сообщения для `privacySettings`. -- Исправили отображение shimmer-блока новостей и таймаут проверки онлайна. -- Исправили несколько проблем в процессе установки мода и обновления приложения. +- Аддоны больше не пропадают и не сбрасываются после перезапуска PulseSync. +- OBS-виджет теперь может получать данные о треке без входа в аккаунт. +- В автономном режиме разделы, требующие авторизации, теперь показывают понятное окно входа вместо молчаливого отключения. +- Исправили определение версии и данных установленной Яндекс Музыки. +- Исправили верхнее действие контекстного меню. +- Исправили отступы новостей, стиль поиска пользователей и отображение подсказок на главной странице. +- В русской локализации заменили "Каталог расширений" на "Каталог аддонов". +- В нужных местах снова можно выделять текст. ### Техническое -- Убрали устаревший HTTP-эндпоинт `/get_handle`. -- Поправили CORS-обработку локального HTTP-сервера. +- Обновили внутренний модуль для работы с файлами, установкой мода и проверкой приложения. +- Улучшили автоматическую диагностику сбоев, чтобы ошибки было проще находить и исправлять. +- Обновили сборку и служебные инструменты релиза. diff --git a/forge.config.ts b/forge.config.ts index 74edf4e5..05be32c9 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -5,6 +5,7 @@ import { FuseV1Options, FuseVersion } from '@electron/fuses' import path from 'path' import fs from 'fs' import { fileURLToPath } from 'node:url' +import { prepareGlitchTipSourceMaps } from './scripts/glitchtip-sourcemaps.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -39,6 +40,7 @@ const copyNativeModules = (resourcesPath: string): void => { for (const addonName of fs.readdirSync(nativeModulesRoot)) { const addonPath = path.join(nativeModulesRoot, addonName) if (!fs.statSync(addonPath).isDirectory()) continue + if (!fs.existsSync(path.join(addonPath, 'package.json'))) continue const nodeFiles = collectNativeNodeFiles(addonPath) if (nodeFiles.length === 0) continue @@ -59,7 +61,7 @@ const forgeConfig: ForgeConfig = { executableName: process.platform === 'linux' ? 'pulsesync' : 'PulseSync', appCopyright: `Copyright (C) ${new Date().getFullYear()} Матвиенко Артём Евгеньевич`, asar: { - unpack: '**/.vite/renderer/**/static/assets/icon/**', + unpack: '{**/.vite/renderer/**/static/assets/icon/**,**/.vite/worker/**}', }, win32metadata: { CompanyName: 'Матвиенко Артём Евгеньевич', @@ -83,6 +85,10 @@ const forgeConfig: ForgeConfig = { config: 'vite.preload.config.ts', target: 'preload', }, + { + entry: 'src/main/modules/mod/network/artifactWorker.ts', + config: 'vite.worker.config.ts', + }, ], renderer: [ { @@ -125,6 +131,7 @@ const forgeConfig: ForgeConfig = { fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, '\t')) }, packageAfterCopy: async (_forgeConfig, buildPath, electronVersion, platform, arch) => { + prepareGlitchTipSourceMaps(buildPath, platform, arch) const resourcesPath = path.resolve(buildPath, '..') const iconSource = path.resolve(__dirname, 'static', 'assets', 'icon') const iconDestination = path.join(resourcesPath, 'assets', 'icon') diff --git a/nativeModules/fileOperations/binding.gyp b/nativeModules/fileOperations/binding.gyp deleted file mode 100644 index 8d6ac352..00000000 --- a/nativeModules/fileOperations/binding.gyp +++ /dev/null @@ -1,38 +0,0 @@ -{ - "targets": [ - { - "target_name": "fileOperations", - "sources": [ - "src/addon.cc", - "src/file_ops.cpp", - "src/file_watcher.cpp" - ], - "include_dirs": [ - " - -#include "file_ops.h" -#include "file_watcher.h" - -Napi::Object Init(Napi::Env env, Napi::Object exports) { - RegisterFileOperations(env, exports); - RegisterFileWatcher(env, exports); - return exports; -} - -NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/nativeModules/fileOperations/src/file_ops.cpp b/nativeModules/fileOperations/src/file_ops.cpp deleted file mode 100644 index 8075fe65..00000000 --- a/nativeModules/fileOperations/src/file_ops.cpp +++ /dev/null @@ -1,589 +0,0 @@ -#include "file_ops.h" - -#include -#include -#include -#include - -#ifdef _WIN32 -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#else -#include -#include -#include -#include -#include -#include -#endif - -namespace { - -std::string GetLastErrorMessage() { -#ifdef _WIN32 - DWORD err = GetLastError(); - if (err == 0) return std::string(); - - LPWSTR buf = nullptr; - DWORD len = FormatMessageW( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, - err, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPWSTR)&buf, - 0, - nullptr - ); - - if (len == 0 || buf == nullptr) { - return std::string("WinAPI error code: ") + std::to_string(err); - } - - int size = WideCharToMultiByte( - CP_UTF8, - 0, - buf, - static_cast(len), - nullptr, - 0, - nullptr, - nullptr - ); - - if (size <= 0) { - LocalFree(buf); - return std::string("WinAPI error code: ") + std::to_string(err); - } - - std::string result(size, 0); - WideCharToMultiByte( - CP_UTF8, - 0, - buf, - static_cast(len), - &result[0], - size, - nullptr, - nullptr - ); - - LocalFree(buf); - - while (!result.empty() && (result.back() == '\r' || result.back() == '\n')) { - result.pop_back(); - } - - return result; -#else - int err = errno; - const char* msg = strerror(err); - if (!msg) return std::string("errno: ") + std::to_string(err); - return std::string(msg); -#endif -} - -void ThrowFsError(Napi::Env env, const std::string& prefix) { - std::string msg = prefix; - std::string osErr = GetLastErrorMessage(); - if (!osErr.empty()) { - msg += ": "; - msg += osErr; - } - Napi::Error::New(env, msg).ThrowAsJavaScriptException(); -} - -#ifdef _WIN32 -std::wstring Utf8ToWide(const std::string& s) { - if (s.empty()) return std::wstring(); - int size = MultiByteToWideChar( - CP_UTF8, - 0, - s.c_str(), - static_cast(s.size()), - nullptr, - 0 - ); - if (size <= 0) { - return std::wstring(); - } - std::wstring result(size, 0); - MultiByteToWideChar( - CP_UTF8, - 0, - s.c_str(), - static_cast(s.size()), - &result[0], - size - ); - return result; -} - -bool RemoveDirectoryRecursiveW(const std::wstring& path) { - WIN32_FIND_DATAW findData; - HANDLE findHandle = FindFirstFileW((path + L"\\*").c_str(), &findData); - - if (findHandle == INVALID_HANDLE_VALUE) { - return false; - } - - bool success = true; - do { - std::wstring fileName = findData.cFileName; - if (fileName == L"." || fileName == L"..") { - continue; - } - - std::wstring fullPath = path + L"\\" + fileName; - - if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { - if (!RemoveDirectoryRecursiveW(fullPath)) { - success = false; - break; - } - } else { - if (!DeleteFileW(fullPath.c_str())) { - success = false; - break; - } - } - } while (FindNextFileW(findHandle, &findData)); - - FindClose(findHandle); - - if (success && !RemoveDirectoryW(path.c_str())) { - return false; - } - - return success; -} -#else -bool RemoveDirectoryRecursive(const std::string& path) { - DIR* dir = opendir(path.c_str()); - if (!dir) { - return false; - } - - bool success = true; - struct dirent* entry; - while ((entry = readdir(dir)) != nullptr) { - std::string fileName = entry->d_name; - if (fileName == "." || fileName == "..") { - continue; - } - - std::string fullPath = path + "/" + fileName; - struct stat st; - - if (stat(fullPath.c_str(), &st) != 0) { - success = false; - break; - } - - if (S_ISDIR(st.st_mode)) { - if (!RemoveDirectoryRecursive(fullPath)) { - success = false; - break; - } - } else { - if (unlink(fullPath.c_str()) != 0) { - success = false; - break; - } - } - } - - closedir(dir); - - if (success && rmdir(path.c_str()) != 0) { - return false; - } - - return success; -} -#endif - -Napi::Value FileExistsWrapped(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - if (info.Length() < 1 || !info[0].IsString()) { - Napi::TypeError::New(env, "Path must be a string").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::string path = info[0].As().Utf8Value(); - -#ifdef _WIN32 - std::wstring wpath = Utf8ToWide(path); - if (wpath.empty()) { - return Napi::Boolean::New(env, false); - } - DWORD attrs = GetFileAttributesW(wpath.c_str()); - if (attrs == INVALID_FILE_ATTRIBUTES) { - return Napi::Boolean::New(env, false); - } - return Napi::Boolean::New(env, true); -#else - struct stat st; - if (stat(path.c_str(), &st) == 0) { - return Napi::Boolean::New(env, true); - } - return Napi::Boolean::New(env, false); -#endif -} - -Napi::Value ReadFileWrapped(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - if (info.Length() < 1 || !info[0].IsString()) { - Napi::TypeError::New(env, "Path must be a string").ThrowAsJavaScriptException(); - return env.Null(); - } - std::string path = info[0].As().Utf8Value(); - -#ifdef _WIN32 - std::wstring wpath = Utf8ToWide(path); - if (wpath.empty()) { - ThrowFsError(env, "Failed to convert path to wide string"); - return env.Null(); - } - - HANDLE h = CreateFileW( - wpath.c_str(), - GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - nullptr - ); - if (h == INVALID_HANDLE_VALUE) { - ThrowFsError(env, "Failed to open file for read"); - return env.Null(); - } - - LARGE_INTEGER size; - if (!GetFileSizeEx(h, &size)) { - CloseHandle(h); - ThrowFsError(env, "Failed to get file size"); - return env.Null(); - } - - if (size.QuadPart < 0) { - CloseHandle(h); - Napi::Error::New(env, "Negative file size").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (size.QuadPart > static_cast(std::numeric_limits::max())) { - CloseHandle(h); - Napi::Error::New(env, "File too large").ThrowAsJavaScriptException(); - return env.Null(); - } - - size_t sz = static_cast(size.QuadPart); - std::vector buf; - buf.resize(sz); - - DWORD totalRead = 0; - while (totalRead < sz) { - DWORD toRead = static_cast(std::min(sz - totalRead, 64 * 1024 * 1024)); - DWORD readNow = 0; - BOOL ok = ReadFile(h, buf.data() + totalRead, toRead, &readNow, nullptr); - if (!ok) { - CloseHandle(h); - ThrowFsError(env, "Failed to read file"); - return env.Null(); - } - if (readNow == 0) break; - totalRead += readNow; - } - - CloseHandle(h); - return Napi::Buffer::Copy(env, buf.data(), totalRead); -#else - int fd = open(path.c_str(), O_RDONLY); - if (fd < 0) { - ThrowFsError(env, "Failed to open file for read"); - return env.Null(); - } - - struct stat st; - if (fstat(fd, &st) != 0) { - int savedErr = errno; - close(fd); - errno = savedErr; - ThrowFsError(env, "Failed to stat file"); - return env.Null(); - } - - if (!S_ISREG(st.st_mode)) { - close(fd); - Napi::Error::New(env, "Not a regular file").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (st.st_size < 0) { - close(fd); - Napi::Error::New(env, "Negative file size").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (static_cast(st.st_size) > - static_cast(std::numeric_limits::max())) { - close(fd); - Napi::Error::New(env, "File too large").ThrowAsJavaScriptException(); - return env.Null(); - } - - size_t sz = static_cast(st.st_size); - std::vector buf; - buf.resize(sz); - - size_t totalRead = 0; - while (totalRead < sz) { - ssize_t r = read(fd, buf.data() + totalRead, sz - totalRead); - if (r < 0) { - int savedErr = errno; - close(fd); - errno = savedErr; - ThrowFsError(env, "Failed to read file"); - return env.Null(); - } - if (r == 0) break; - totalRead += static_cast(r); - } - - close(fd); - return Napi::Buffer::Copy(env, buf.data(), totalRead); -#endif -} - -Napi::Value DeleteFileWrapped(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - if (info.Length() < 1 || !info[0].IsString()) { - Napi::TypeError::New(env, "Path must be a string").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::string path = info[0].As().Utf8Value(); - -#ifdef _WIN32 - std::wstring wpath = Utf8ToWide(path); - if (wpath.empty()) { - ThrowFsError(env, "Failed to convert path to wide string"); - return env.Null(); - } - - DWORD attrs = GetFileAttributesW(wpath.c_str()); - if (attrs == INVALID_FILE_ATTRIBUTES) { - ThrowFsError(env, "Path does not exist"); - return env.Null(); - } - - if (attrs & FILE_ATTRIBUTE_DIRECTORY) { - if (!RemoveDirectoryRecursiveW(wpath)) { - ThrowFsError(env, "Failed to delete directory"); - return env.Null(); - } - } else { - if (!DeleteFileW(wpath.c_str())) { - ThrowFsError(env, "Failed to delete file"); - return env.Null(); - } - } -#else - struct stat st; - if (stat(path.c_str(), &st) != 0) { - ThrowFsError(env, "Path does not exist"); - return env.Null(); - } - - if (S_ISDIR(st.st_mode)) { - if (!RemoveDirectoryRecursive(path)) { - ThrowFsError(env, "Failed to delete directory"); - return env.Null(); - } - } else { - if (unlink(path.c_str()) != 0) { - ThrowFsError(env, "Failed to delete file"); - return env.Null(); - } - } -#endif - - return env.Undefined(); -} - -Napi::Value RenameFileWrapped(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) { - Napi::TypeError::New(env, "Old and new path must be strings").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::string oldPath = info[0].As().Utf8Value(); - std::string newPath = info[1].As().Utf8Value(); - -#ifdef _WIN32 - std::wstring wold = Utf8ToWide(oldPath); - std::wstring wnew = Utf8ToWide(newPath); - if (wold.empty() || wnew.empty()) { - ThrowFsError(env, "Failed to convert path to wide string"); - return env.Null(); - } - - BOOL ok = MoveFileExW( - wold.c_str(), - wnew.c_str(), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED - ); - if (!ok) { - ThrowFsError(env, "Failed to rename file"); - return env.Null(); - } -#else - if (rename(oldPath.c_str(), newPath.c_str()) != 0) { - ThrowFsError(env, "Failed to rename file"); - return env.Null(); - } -#endif - - return env.Undefined(); -} - -#ifndef _WIN32 -bool CopyFilePosix(const std::string& src, const std::string& dst, std::string& errMsg) { - int inFd = open(src.c_str(), O_RDONLY); - if (inFd < 0) { - errMsg = GetLastErrorMessage(); - return false; - } - - struct stat st; - if (fstat(inFd, &st) != 0) { - int savedErr = errno; - close(inFd); - errno = savedErr; - errMsg = GetLastErrorMessage(); - return false; - } - - mode_t mode = st.st_mode & 0777; - int outFd = open(dst.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode); - if (outFd < 0) { - int savedErr = errno; - close(inFd); - errno = savedErr; - errMsg = GetLastErrorMessage(); - return false; - } - - const size_t bufSize = 65536; - std::vector buf(bufSize); - - while (true) { - ssize_t r = read(inFd, buf.data(), bufSize); - if (r < 0) { - int savedErr = errno; - close(inFd); - close(outFd); - errno = savedErr; - errMsg = GetLastErrorMessage(); - return false; - } - if (r == 0) break; - - ssize_t off = 0; - while (off < r) { - ssize_t w = write(outFd, buf.data() + off, r - off); - if (w < 0) { - int savedErr = errno; - close(inFd); - close(outFd); - errno = savedErr; - errMsg = GetLastErrorMessage(); - return false; - } - off += w; - } - } - - if (close(inFd) != 0) { - int savedErr = errno; - close(outFd); - errno = savedErr; - errMsg = GetLastErrorMessage(); - return false; - } - - if (close(outFd) != 0) { - errMsg = GetLastErrorMessage(); - return false; - } - - return true; -} -#endif - -Napi::Value MoveFileWrapped(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) { - Napi::TypeError::New(env, "Source and destination path must be strings").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::string src = info[0].As().Utf8Value(); - std::string dst = info[1].As().Utf8Value(); - -#ifdef _WIN32 - std::wstring wsrc = Utf8ToWide(src); - std::wstring wdst = Utf8ToWide(dst); - if (wsrc.empty() || wdst.empty()) { - ThrowFsError(env, "Failed to convert path to wide string"); - return env.Null(); - } - - BOOL ok = MoveFileExW( - wsrc.c_str(), - wdst.c_str(), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED - ); - if (!ok) { - ThrowFsError(env, "Failed to move file"); - return env.Null(); - } -#else - if (rename(src.c_str(), dst.c_str()) == 0) { - return env.Undefined(); - } - - if (errno != EXDEV) { - ThrowFsError(env, "Failed to move file"); - return env.Null(); - } - - std::string msg; - if (!CopyFilePosix(src, dst, msg)) { - Napi::Error::New(env, std::string("Failed to move file (copy phase): ") + msg).ThrowAsJavaScriptException(); - return env.Null(); - } - - if (unlink(src.c_str()) != 0) { - ThrowFsError(env, "Failed to remove source after move"); - return env.Null(); - } -#endif - - return env.Undefined(); -} - -} // namespace - -void RegisterFileOperations(Napi::Env env, Napi::Object exports) { - exports.Set("fileExists", Napi::Function::New(env, FileExistsWrapped)); - exports.Set("readFile", Napi::Function::New(env, ReadFileWrapped)); - exports.Set("deleteFile", Napi::Function::New(env, DeleteFileWrapped)); - exports.Set("renameFile", Napi::Function::New(env, RenameFileWrapped)); - exports.Set("moveFile", Napi::Function::New(env, MoveFileWrapped)); -} - diff --git a/nativeModules/fileOperations/src/file_ops.h b/nativeModules/fileOperations/src/file_ops.h deleted file mode 100644 index 5b5ee2d1..00000000 --- a/nativeModules/fileOperations/src/file_ops.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef FILE_OPS_H -#define FILE_OPS_H - -#include - -void RegisterFileOperations(Napi::Env env, Napi::Object exports); - -#endif diff --git a/nativeModules/fileOperations/src/file_watcher.cpp b/nativeModules/fileOperations/src/file_watcher.cpp deleted file mode 100644 index d11ac97c..00000000 --- a/nativeModules/fileOperations/src/file_watcher.cpp +++ /dev/null @@ -1,146 +0,0 @@ -#include "file_watcher.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace { - -inline std::string ToLower(std::string s) { - for (auto& ch : s) ch = static_cast(::tolower(static_cast(ch))); - return s; -} - -std::unordered_map SnapshotDir(const std::filesystem::path& root) { - namespace fs = std::filesystem; - std::unordered_map out; - std::error_code ec; - fs::directory_options opts = fs::directory_options::skip_permission_denied; - for (fs::recursive_directory_iterator it(root, opts, ec), end; it != end; it.increment(ec)) { - if (ec) { - ec.clear(); - continue; - } - const fs::directory_entry& entry = *it; - if (!entry.is_regular_file(ec)) { - if (ec) ec.clear(); - continue; - } -#ifdef _WIN32 - auto u8ext = entry.path().extension().u8string(); - std::string ext; - ext.reserve(u8ext.size()); - for (char8_t c : u8ext) ext.push_back(static_cast(c)); - ext = ToLower(std::move(ext)); -#else - std::string ext = ToLower(entry.path().extension().string()); -#endif - if (ext == ".js" || ext == ".css") { - auto ft = entry.last_write_time(ec); - if (ec) { ec.clear(); continue; } -#ifdef _WIN32 - auto u8key = entry.path().generic_u8string(); - std::string key; - key.reserve(u8key.size()); - for (char8_t c : u8key) key.push_back(static_cast(c)); -#else - std::string key = entry.path().string(); -#endif - out.emplace(std::move(key), ft); - } - } - return out; -} - -void Watcher(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - if (info.Length() < 3 || - !info[0].IsString() || - !info[1].IsNumber() || - !info[2].IsFunction()) { - Napi::TypeError::New(env, "Expected (path: string, intervalMs: number, callback: function)") - .ThrowAsJavaScriptException(); - return; - } - std::string pathUtf8 = info[0].As().Utf8Value(); - int interval = info[1].As().Int32Value(); - Napi::Function jsCallback = info[2].As(); - auto tsfn = Napi::ThreadSafeFunction::New( - env, - jsCallback, - "FileWatcher", - 0, - 1 - ); -#ifdef _WIN32 - std::u8string u8path; - u8path.reserve(pathUtf8.size()); - for (unsigned char c : pathUtf8) u8path.push_back(static_cast(c)); - std::filesystem::path fsPath(u8path); -#else - std::filesystem::path fsPath(pathUtf8); -#endif - std::thread([fsPath, interval, tsfn]() mutable { - namespace fs = std::filesystem; - auto prev = SnapshotDir(fsPath); - for (;;) { - std::this_thread::sleep_for(std::chrono::milliseconds(interval)); - auto cur = SnapshotDir(fsPath); - for (const auto& kv : cur) { - const auto& file = kv.first; - const auto& mtime = kv.second; - auto it = prev.find(file); - if (it == prev.end()) { - std::string event = "add"; - napi_status status = tsfn.BlockingCall( - [file, event](Napi::Env env, Napi::Function callback) { - callback.Call({ - Napi::String::New(env, event), - Napi::String::New(env, file) - }); - } - ); - (void)status; - } else if (mtime != it->second) { - std::string event = "change"; - napi_status status = tsfn.BlockingCall( - [file, event](Napi::Env env, Napi::Function callback) { - callback.Call({ - Napi::String::New(env, event), - Napi::String::New(env, file) - }); - } - ); - (void)status; - } - } - for (const auto& kv : prev) { - const auto& file = kv.first; - if (cur.find(file) == cur.end()) { - std::string event = "unlink"; - napi_status status = tsfn.BlockingCall( - [file, event](Napi::Env env, Napi::Function callback) { - callback.Call({ - Napi::String::New(env, event), - Napi::String::New(env, file) - }); - } - ); - (void)status; - } - } - prev.swap(cur); - } - }).detach(); -} - -} // namespace - -void RegisterFileWatcher(Napi::Env env, Napi::Object exports) { - exports.Set("watch", Napi::Function::New(env, Watcher)); -} - diff --git a/nativeModules/fileOperations/src/file_watcher.h b/nativeModules/fileOperations/src/file_watcher.h deleted file mode 100644 index 691f0937..00000000 --- a/nativeModules/fileOperations/src/file_watcher.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef FILE_WATCHER_H -#define FILE_WATCHER_H - -#include - -void RegisterFileWatcher(Napi::Env env, Napi::Object exports); - -#endif diff --git a/nativeModules/fileOperations/yarn.lock b/nativeModules/fileOperations/yarn.lock deleted file mode 100644 index 7e8184d1..00000000 --- a/nativeModules/fileOperations/yarn.lock +++ /dev/null @@ -1,432 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@isaacs/balanced-match@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" - integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== - -"@isaacs/brace-expansion@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" - integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== - dependencies: - "@isaacs/balanced-match" "^4.0.1" - -"@isaacs/fs-minipass@^4.0.0": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" - integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== - dependencies: - minipass "^7.0.4" - -"@npmcli/agent@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@npmcli/agent/-/agent-4.0.0.tgz#2bb2b1c0a170940511554a7986ae2a8be9fedcce" - integrity sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA== - dependencies: - agent-base "^7.1.0" - http-proxy-agent "^7.0.0" - https-proxy-agent "^7.0.1" - lru-cache "^11.2.1" - socks-proxy-agent "^8.0.3" - -"@npmcli/fs@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-5.0.0.tgz#674619771907342b3d1ac197aaf1deeb657e3539" - integrity sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og== - dependencies: - semver "^7.3.5" - -abbrev@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-4.0.0.tgz#ec933f0e27b6cd60e89b5c6b2a304af42209bb05" - integrity sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA== - -agent-base@^7.1.0, agent-base@^7.1.2: - version "7.1.4" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" - integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -cacache@^20.0.1: - version "20.0.3" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-20.0.3.tgz#bd65205d5e6d86e02bbfaf8e4ce6008f1b81d119" - integrity sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw== - dependencies: - "@npmcli/fs" "^5.0.0" - fs-minipass "^3.0.0" - glob "^13.0.0" - lru-cache "^11.1.0" - minipass "^7.0.3" - minipass-collect "^2.0.1" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - p-map "^7.0.2" - ssri "^13.0.0" - unique-filename "^5.0.0" - -chownr@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" - integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== - -debug@4, debug@^4.3.4: - version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - -encoding@^0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -err-code@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" - integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== - -exponential-backoff@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz#51cf92c1c0493c766053f9d3abee4434c244d2f6" - integrity sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA== - -fdir@^6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" - integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fs-minipass@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54" - integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== - dependencies: - minipass "^7.0.3" - -glob@^13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.0.tgz#9d9233a4a274fc28ef7adce5508b7ef6237a1be3" - integrity sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA== - dependencies: - minimatch "^10.1.1" - minipass "^7.1.2" - path-scurry "^2.0.0" - -graceful-fs@^4.2.6: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -http-cache-semantics@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" - integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== - -http-proxy-agent@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" - integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== - dependencies: - agent-base "^7.1.0" - debug "^4.3.4" - -https-proxy-agent@^7.0.1: - version "7.0.6" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" - integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== - dependencies: - agent-base "^7.1.2" - debug "4" - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -ip-address@^10.0.1: - version "10.1.0" - resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4" - integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q== - -isexe@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" - integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== - -lru-cache@^11.0.0, lru-cache@^11.1.0, lru-cache@^11.2.1: - version "11.2.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.4.tgz#ecb523ebb0e6f4d837c807ad1abaea8e0619770d" - integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== - -make-fetch-happen@^15.0.0: - version "15.0.3" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz#1578d72885f2b3f9e5daa120b36a14fc31a84610" - integrity sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw== - dependencies: - "@npmcli/agent" "^4.0.0" - cacache "^20.0.1" - http-cache-semantics "^4.1.1" - minipass "^7.0.2" - minipass-fetch "^5.0.0" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^1.0.0" - proc-log "^6.0.0" - promise-retry "^2.0.1" - ssri "^13.0.0" - -minimatch@^10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" - integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== - dependencies: - "@isaacs/brace-expansion" "^5.0.0" - -minipass-collect@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-2.0.1.tgz#1621bc77e12258a12c60d34e2276ec5c20680863" - integrity sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw== - dependencies: - minipass "^7.0.3" - -minipass-fetch@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-5.0.0.tgz#644ed3fa172d43b3163bb32f736540fc138c4afb" - integrity sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A== - dependencies: - minipass "^7.0.3" - minipass-sized "^1.0.3" - minizlib "^3.0.1" - optionalDependencies: - encoding "^0.1.13" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-pipeline@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" - integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== - dependencies: - minipass "^3.0.0" - -minipass-sized@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" - integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^7.0.2, minipass@^7.0.3, minipass@^7.0.4, minipass@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - -minizlib@^3.0.1, minizlib@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.1.0.tgz#6ad76c3a8f10227c9b51d1c9ac8e30b27f5a251c" - integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw== - dependencies: - minipass "^7.1.2" - -ms@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -negotiator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" - integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== - -node-addon-api@^8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.6.0.tgz#b22497201b465cd0a92ef2c01074ee5068c79a6d" - integrity sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q== - -node-gyp@^12.2.0: - version "12.2.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-12.2.0.tgz#ff73f6f509e33d8b7e768f889ffc9738ad117b07" - integrity sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ== - dependencies: - env-paths "^2.2.0" - exponential-backoff "^3.1.1" - graceful-fs "^4.2.6" - make-fetch-happen "^15.0.0" - nopt "^9.0.0" - proc-log "^6.0.0" - semver "^7.3.5" - tar "^7.5.4" - tinyglobby "^0.2.12" - which "^6.0.0" - -nopt@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-9.0.0.tgz#6bff0836b2964d24508b6b41b5a9a49c4f4a1f96" - integrity sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw== - dependencies: - abbrev "^4.0.0" - -p-map@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-7.0.4.tgz#b81814255f542e252d5729dca4d66e5ec14935b8" - integrity sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ== - -path-scurry@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.1.tgz#4b6572376cfd8b811fca9cd1f5c24b3cbac0fe10" - integrity sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA== - dependencies: - lru-cache "^11.0.0" - minipass "^7.1.2" - -picomatch@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" - integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== - -proc-log@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-6.1.0.tgz#18519482a37d5198e231133a70144a50f21f0215" - integrity sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ== - -promise-retry@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" - integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== - -"safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -semver@^7.3.5: - version "7.7.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" - integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== - -smart-buffer@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - -socks-proxy-agent@^8.0.3: - version "8.0.5" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee" - integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw== - dependencies: - agent-base "^7.1.2" - debug "^4.3.4" - socks "^2.8.3" - -socks@^2.8.3: - version "2.8.7" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea" - integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A== - dependencies: - ip-address "^10.0.1" - smart-buffer "^4.2.0" - -ssri@^13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-13.0.0.tgz#4226b303dc474003d88905f9098cb03361106c74" - integrity sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng== - dependencies: - minipass "^7.0.3" - -tar@^7.5.4: - version "7.5.11" - resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.11.tgz#1250fae45d98806b36d703b30973fa8e0a6d8868" - integrity sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ== - dependencies: - "@isaacs/fs-minipass" "^4.0.0" - chownr "^3.0.0" - minipass "^7.1.2" - minizlib "^3.1.0" - yallist "^5.0.0" - -tinyglobby@^0.2.12: - version "0.2.15" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" - integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== - dependencies: - fdir "^6.5.0" - picomatch "^4.0.3" - -unique-filename@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-5.0.0.tgz#8b17bbde1a7ca322dd1a1d23fe17c2b798c43f8f" - integrity sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg== - dependencies: - unique-slug "^6.0.0" - -unique-slug@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-6.0.0.tgz#f46fd688a9bd972fd356c23d95812a3a4862ed88" - integrity sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw== - dependencies: - imurmurhash "^0.1.4" - -which@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/which/-/which-6.0.0.tgz#a3a721a14cdd9b991a722e493c177eeff82ff32a" - integrity sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg== - dependencies: - isexe "^3.1.1" - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yallist@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" - integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== diff --git a/nativeModules/pulsesyncNative/.gitignore b/nativeModules/pulsesyncNative/.gitignore new file mode 100644 index 00000000..76daa494 --- /dev/null +++ b/nativeModules/pulsesyncNative/.gitignore @@ -0,0 +1,2 @@ +/build/ +/target/ diff --git a/nativeModules/pulsesyncNative/Cargo.lock b/nativeModules/pulsesyncNative/Cargo.lock new file mode 100644 index 00000000..27d38712 --- /dev/null +++ b/nativeModules/pulsesyncNative/Cargo.lock @@ -0,0 +1,718 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctor" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "napi" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad513ff22558f1830b595ea6eb4091da48145d09a222ce157e781896f78be0b9" +dependencies = [ + "bitflags", + "ctor", + "futures", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", +] + +[[package]] +name = "napi-build" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" + +[[package]] +name = "napi-derive" +version = "3.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b3f766e04667e6da0e181e2da4f85475d5a6513b7cf6a80bea184e224a5b42" +dependencies = [ + "convert_case", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "5.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f5bcdf71abd3a50d00b49c1c2c75251cb3c913777d6139cd37dabc093a5e400" +dependencies = [ + "libloading", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulsesync-native" +version = "1.0.0" +dependencies = [ + "flate2", + "memmap2", + "napi", + "napi-build", + "napi-derive", + "plist", + "serde_json", + "sha2", + "zip", + "zstd", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/nativeModules/pulsesyncNative/Cargo.toml b/nativeModules/pulsesyncNative/Cargo.toml new file mode 100644 index 00000000..57330f8d --- /dev/null +++ b/nativeModules/pulsesyncNative/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "pulsesync-native" +version = "1.0.0" +edition = "2024" +rust-version = "1.88" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +flate2 = "1.1.9" +memmap2 = "0.9.10" +napi = { version = "3.9.1", default-features = false, features = ["napi10"] } +napi-derive = "3.5.6" +plist = "1.9.0" +serde_json = "1.0.150" +sha2 = "0.11.0" +zstd = "0.13.3" +zip = { version = "8.6.0", default-features = false, features = ["deflate"] } + +[build-dependencies] +napi-build = "2.3.2" + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = "symbols" diff --git a/nativeModules/pulsesyncNative/build.rs b/nativeModules/pulsesyncNative/build.rs new file mode 100644 index 00000000..0f1b0100 --- /dev/null +++ b/nativeModules/pulsesyncNative/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/nativeModules/pulsesyncNative/package.json b/nativeModules/pulsesyncNative/package.json new file mode 100644 index 00000000..5561986a --- /dev/null +++ b/nativeModules/pulsesyncNative/package.json @@ -0,0 +1,9 @@ +{ + "name": "pulsesync-native", + "version": "1.0.0", + "private": true, + "scripts": { + "clean": "node scripts/build.mjs --clean", + "build": "node scripts/build.mjs" + } +} diff --git a/nativeModules/pulsesyncNative/scripts/build.mjs b/nativeModules/pulsesyncNative/scripts/build.mjs new file mode 100644 index 00000000..a7b3c9da --- /dev/null +++ b/nativeModules/pulsesyncNative/scripts/build.mjs @@ -0,0 +1,31 @@ +import { spawnSync } from 'node:child_process' +import { copyFileSync, mkdirSync, rmSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const moduleRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const releaseDir = resolve(moduleRoot, 'build', 'Release') + +if (process.argv.includes('--clean')) { + rmSync(resolve(moduleRoot, 'build'), { recursive: true, force: true }) + rmSync(resolve(moduleRoot, 'target'), { recursive: true, force: true }) + process.exit(0) +} + +const cargo = spawnSync('cargo', ['build', '--release', '--locked'], { + cwd: moduleRoot, + stdio: 'inherit', +}) + +if (cargo.status !== 0) { + process.exit(cargo.status ?? 1) +} + +const libraryName = + process.platform === 'win32' ? 'pulsesync_native.dll' : process.platform === 'darwin' ? 'libpulsesync_native.dylib' : 'libpulsesync_native.so' +const source = resolve(moduleRoot, 'target', 'release', libraryName) +const destination = resolve(releaseDir, 'pulsesyncNative.node') + +mkdirSync(releaseDir, { recursive: true }) +copyFileSync(source, destination) +console.log(`Copied ${source} -> ${destination}`) diff --git a/nativeModules/pulsesyncNative/src/artifact.rs b/nativeModules/pulsesyncNative/src/artifact.rs new file mode 100644 index 00000000..293c5494 --- /dev/null +++ b/nativeModules/pulsesyncNative/src/artifact.rs @@ -0,0 +1,539 @@ +use crate::file_ops::{copy_path, delete_path}; +use crate::fs_transaction::{DirectoryTransaction, unique_sibling}; +use flate2::read::GzDecoder; +use napi_derive::napi; +use sha2::{Digest, Sha256}; +use std::fs::{self, File}; +use std::io::{self, BufReader, BufWriter, Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::Instant; +use zip::ZipArchive; + +#[napi(object)] +pub struct NativeArtifactDurations { + pub read_ms: u32, + pub checksum_ms: u32, + pub decompress_ms: u32, + pub write_ms: u32, + pub clone_ms: u32, + pub extract_ms: u32, + pub cache_write_ms: u32, + pub backup_ms: u32, + pub install_ms: u32, + pub cleanup_ms: u32, +} + +impl Default for NativeArtifactDurations { + fn default() -> Self { + Self { + read_ms: 0, + checksum_ms: 0, + decompress_ms: 0, + write_ms: 0, + clone_ms: 0, + extract_ms: 0, + cache_write_ms: 0, + backup_ms: 0, + install_ms: 0, + cleanup_ms: 0, + } + } +} + +#[napi(object)] +pub struct NativeArtifactWarning { + pub stage: String, + pub code: Option, + pub message: String, +} + +#[napi(object)] +pub struct NativeArtifactResult { + pub ok: bool, + pub stage: Option, + pub code: Option, + pub message: Option, + pub prepared_path: Option, + pub durations: NativeArtifactDurations, + pub warnings: Vec, +} + +#[napi(object)] +pub struct PrepareAsarArtifactRequest { + pub archive_path: String, + pub archive_extension: String, + pub expected_checksum: Option, + pub output_path: String, +} + +#[napi(object)] +pub struct PreparedDirectoryMarker { + pub file_name: String, + pub value: String, +} + +#[napi(object)] +pub struct InstallUnpackedArtifactRequest { + pub source_kind: String, + pub archive_path: String, + pub archive_extension: String, + pub expected_checksum: Option, + pub prepared_directory_path: Option, + pub prepared_directory_marker: Option, + pub staging_path: String, + pub target_path: String, +} + +struct ArtifactError { + stage: &'static str, + code: Option, + message: String, +} + +impl ArtifactError { + fn new(stage: &'static str, message: impl Into) -> Self { + Self { + stage, + code: None, + message: message.into(), + } + } + + fn checksum_mismatch() -> Self { + Self { + stage: "checksum", + code: Some("CHECKSUM_MISMATCH".to_owned()), + message: "Archive checksum mismatch".to_owned(), + } + } + + fn from_io(stage: &'static str, error: io::Error) -> Self { + Self { + stage, + code: Some(io_error_code(&error)), + message: error.to_string(), + } + } +} + +fn io_error_code(error: &io::Error) -> String { + match error.kind() { + io::ErrorKind::NotFound => "ENOENT".to_owned(), + io::ErrorKind::PermissionDenied => "EACCES".to_owned(), + io::ErrorKind::AlreadyExists => "EEXIST".to_owned(), + _ => platform_io_error_code(error.raw_os_error()), + } +} + +#[cfg(windows)] +fn platform_io_error_code(raw_code: Option) -> String { + match raw_code { + Some(2 | 3) => "ENOENT".to_owned(), + Some(5) => "EACCES".to_owned(), + Some(17) => "EXDEV".to_owned(), + Some(32 | 33) => "EBUSY".to_owned(), + Some(80 | 183) => "EEXIST".to_owned(), + Some(code) => format!("WIN32_{code}"), + None => "EIO".to_owned(), + } +} + +#[cfg(not(windows))] +fn platform_io_error_code(raw_code: Option) -> String { + match raw_code { + Some(1) => "EPERM".to_owned(), + Some(2) => "ENOENT".to_owned(), + Some(13) => "EACCES".to_owned(), + Some(16) => "EBUSY".to_owned(), + Some(17) => "EEXIST".to_owned(), + Some(18) => "EXDEV".to_owned(), + Some(39 | 66) => "ENOTEMPTY".to_owned(), + Some(code) => format!("ERRNO_{code}"), + None => "EIO".to_owned(), + } +} + +fn elapsed_ms(started_at: Instant) -> u32 { + started_at.elapsed().as_millis().min(u128::from(u32::MAX)) as u32 +} + +fn success( + prepared_path: Option, + durations: NativeArtifactDurations, + warnings: Vec, +) -> NativeArtifactResult { + NativeArtifactResult { + ok: true, + stage: None, + code: None, + message: None, + prepared_path, + durations, + warnings, + } +} + +fn failure( + error: ArtifactError, + durations: NativeArtifactDurations, + warnings: Vec, +) -> NativeArtifactResult { + NativeArtifactResult { + ok: false, + stage: Some(error.stage.to_owned()), + code: error.code, + message: Some(error.message), + prepared_path: None, + durations, + warnings, + } +} + +struct HashingReader { + inner: R, + hasher: Sha256, +} + +impl HashingReader { + fn new(inner: R) -> Self { + Self { + inner, + hasher: Sha256::new(), + } + } + + fn finish(self) -> String { + digest_hex(self.hasher.finalize().as_slice()) + } +} + +fn digest_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut output = String::with_capacity(bytes.len() * 2); + for byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} + +impl Read for HashingReader { + fn read(&mut self, buffer: &mut [u8]) -> io::Result { + let read = self.inner.read(buffer)?; + if read > 0 { + self.hasher.update(&buffer[..read]); + } + Ok(read) + } +} + +fn verify_checksum(actual: String, expected: Option<&str>) -> Result<(), ArtifactError> { + if expected.is_some_and(|expected| !actual.eq_ignore_ascii_case(expected)) { + return Err(ArtifactError::checksum_mismatch()); + } + Ok(()) +} + +fn hash_file_impl(path: &Path) -> io::Result { + let file = File::open(path)?; + let mut reader = BufReader::with_capacity(1024 * 1024, file); + let mut hasher = Sha256::new(); + let mut buffer = vec![0_u8; 1024 * 1024]; + + loop { + let read = reader.read(&mut buffer)?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + + Ok(digest_hex(hasher.finalize().as_slice())) +} + +#[napi] +pub fn hash_file(path: String) -> napi::Result { + hash_file_impl(Path::new(&path)) + .map_err(|error| napi::Error::from_reason(format!("Failed to hash '{path}': {error}"))) +} + +fn stream_archive_to_file( + archive_path: &Path, + archive_extension: &str, + output_path: &Path, + expected_checksum: Option<&str>, +) -> Result<(), ArtifactError> { + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent).map_err(|error| ArtifactError::from_io("write", error))?; + } + + let input = File::open(archive_path).map_err(|error| ArtifactError::from_io("read", error))?; + let hashing_reader = HashingReader::new(BufReader::with_capacity(1024 * 1024, input)); + let output = + File::create(output_path).map_err(|error| ArtifactError::from_io("write", error))?; + let mut output = BufWriter::with_capacity(1024 * 1024, output); + let extension = archive_extension.to_ascii_lowercase(); + + let actual_checksum = if extension == ".gz" { + let mut decoder = GzDecoder::new(hashing_reader); + io::copy(&mut decoder, &mut output) + .map_err(|error| ArtifactError::from_io("decompress", error))?; + let mut reader = decoder.into_inner(); + io::copy(&mut reader, &mut io::sink()) + .map_err(|error| ArtifactError::from_io("read", error))?; + reader.finish() + } else if extension == ".zst" || extension == ".zstd" { + let mut decoder = zstd::stream::read::Decoder::new(hashing_reader) + .map_err(|error| ArtifactError::from_io("decompress", error))?; + io::copy(&mut decoder, &mut output) + .map_err(|error| ArtifactError::from_io("decompress", error))?; + let mut reader = decoder.finish().into_inner(); + io::copy(&mut reader, &mut io::sink()) + .map_err(|error| ArtifactError::from_io("read", error))?; + reader.finish() + } else { + let mut reader = hashing_reader; + io::copy(&mut reader, &mut output) + .map_err(|error| ArtifactError::from_io("write", error))?; + reader.finish() + }; + + output + .flush() + .map_err(|error| ArtifactError::from_io("write", error))?; + verify_checksum(actual_checksum, expected_checksum) +} + +#[napi] +pub fn prepare_asar_artifact(request: PrepareAsarArtifactRequest) -> NativeArtifactResult { + let mut durations = NativeArtifactDurations::default(); + let warnings = Vec::new(); + let output_path = PathBuf::from(&request.output_path); + let started_at = Instant::now(); + + let result = stream_archive_to_file( + Path::new(&request.archive_path), + &request.archive_extension, + &output_path, + request.expected_checksum.as_deref(), + ); + durations.decompress_ms = elapsed_ms(started_at); + + match result { + Ok(()) => success(Some(request.output_path), durations, warnings), + Err(error) => { + let _ = fs::remove_file(output_path); + failure(error, durations, warnings) + } + } +} + +fn extract_zip(zip_path: &Path, destination: &Path) -> Result<(), ArtifactError> { + let file = File::open(zip_path).map_err(|error| ArtifactError::from_io("extract", error))?; + let mut archive = + ZipArchive::new(file).map_err(|error| ArtifactError::new("extract", error.to_string()))?; + + for index in 0..archive.len() { + let mut entry = archive + .by_index(index) + .map_err(|error| ArtifactError::new("extract", error.to_string()))?; + let enclosed = entry.enclosed_name().ok_or_else(|| { + ArtifactError::new("extract", format!("Unsafe ZIP entry: {}", entry.name())) + })?; + let output_path = destination.join(enclosed); + + if entry.is_dir() { + fs::create_dir_all(&output_path) + .map_err(|error| ArtifactError::from_io("extract", error))?; + continue; + } + + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent).map_err(|error| ArtifactError::from_io("extract", error))?; + } + let output = + File::create(&output_path).map_err(|error| ArtifactError::from_io("extract", error))?; + let mut output = BufWriter::new(output); + io::copy(&mut entry, &mut output) + .map_err(|error| ArtifactError::from_io("extract", error))?; + output + .flush() + .map_err(|error| ArtifactError::from_io("extract", error))?; + + #[cfg(unix)] + if let Some(mode) = entry.unix_mode() { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&output_path, fs::Permissions::from_mode(mode)) + .map_err(|error| ArtifactError::from_io("extract", error))?; + } + } + + Ok(()) +} + +fn resolve_extracted_root(staging_path: &Path, target_path: &Path) -> PathBuf { + let Ok(entries) = fs::read_dir(staging_path) else { + return staging_path.to_path_buf(); + }; + let entries: Vec<_> = entries + .flatten() + .filter(|entry| entry.file_name() != "__MACOSX" && entry.file_name() != ".DS_Store") + .collect(); + if entries.len() == 1 + && entries[0] + .file_type() + .is_ok_and(|file_type| file_type.is_dir()) + && target_path + .file_name() + .is_some_and(|name| entries[0].file_name() == name) + { + return entries[0].path(); + } + staging_path.to_path_buf() +} + +fn write_prepared_cache( + source: &Path, + prepared_path: &Path, + marker: &PreparedDirectoryMarker, +) -> Result<(), ArtifactError> { + let temporary_path = unique_sibling(prepared_path, "tmp"); + let _ = delete_path(&temporary_path); + if let Some(parent) = prepared_path.parent() { + fs::create_dir_all(parent).map_err(|error| ArtifactError::from_io("cache", error))?; + } + copy_path(source, &temporary_path).map_err(|error| ArtifactError::from_io("cache", error))?; + fs::write( + temporary_path.join(&marker.file_name), + format!("{}\n", marker.value), + ) + .map_err(|error| ArtifactError::from_io("cache", error))?; + let _ = delete_path(prepared_path); + fs::rename(&temporary_path, prepared_path) + .map_err(|error| ArtifactError::from_io("cache", error)) +} + +fn run_unpacked_install( + request: &InstallUnpackedArtifactRequest, + durations: &mut NativeArtifactDurations, + warnings: &mut Vec, +) -> Result<(), ArtifactError> { + let archive_path = PathBuf::from(&request.archive_path); + let staging_path = PathBuf::from(&request.staging_path); + let target_path = PathBuf::from(&request.target_path); + let _ = delete_path(&staging_path); + + let extracted_root = if request.source_kind == "directory" { + let started_at = Instant::now(); + copy_path(&archive_path, &staging_path) + .map_err(|error| ArtifactError::from_io("extract", error))?; + durations.clone_ms = elapsed_ms(started_at); + staging_path.clone() + } else { + let extension = request.archive_extension.to_ascii_lowercase(); + let temporary_zip = unique_sibling(&staging_path, "archive.zip"); + let zip_path = if extension == ".gz" || extension == ".zst" || extension == ".zstd" { + let started_at = Instant::now(); + let prepare_result = stream_archive_to_file( + &archive_path, + &extension, + &temporary_zip, + request.expected_checksum.as_deref(), + ); + durations.decompress_ms = elapsed_ms(started_at); + if let Err(error) = prepare_result { + let _ = fs::remove_file(&temporary_zip); + return Err(error); + } + temporary_zip.clone() + } else { + if let Some(expected) = request.expected_checksum.as_deref() { + let started_at = Instant::now(); + let actual = hash_file_impl(&archive_path) + .map_err(|error| ArtifactError::from_io("checksum", error))?; + durations.checksum_ms = elapsed_ms(started_at); + verify_checksum(actual, Some(expected))?; + } + archive_path.clone() + }; + + fs::create_dir_all(&staging_path) + .map_err(|error| ArtifactError::from_io("extract", error))?; + let started_at = Instant::now(); + let extract_result = extract_zip(&zip_path, &staging_path); + durations.extract_ms = elapsed_ms(started_at); + if zip_path == temporary_zip { + let _ = fs::remove_file(&temporary_zip); + } + extract_result?; + resolve_extracted_root(&staging_path, &target_path) + }; + + if let (Some(prepared_path), Some(marker)) = ( + request.prepared_directory_path.as_deref(), + request.prepared_directory_marker.as_ref(), + ) { + let started_at = Instant::now(); + if let Err(error) = write_prepared_cache(&extracted_root, Path::new(prepared_path), marker) + { + warnings.push(NativeArtifactWarning { + stage: "cache".to_owned(), + code: error.code, + message: error.message, + }); + } + durations.cache_write_ms = elapsed_ms(started_at); + } + + if let Some(parent) = target_path.parent() { + fs::create_dir_all(parent).map_err(|error| ArtifactError::from_io("install", error))?; + } + + let started_at = Instant::now(); + let mut transaction = DirectoryTransaction::begin(&target_path) + .map_err(|error| ArtifactError::from_io("backup", error))?; + durations.backup_ms = elapsed_ms(started_at); + + let started_at = Instant::now(); + if let Err(error) = transaction.install(&extracted_root) { + transaction + .restore() + .map_err(|restore_error| ArtifactError::from_io("restore", restore_error))?; + return Err(ArtifactError::from_io("install", error)); + } + durations.install_ms = elapsed_ms(started_at); + + let started_at = Instant::now(); + if let Err(error) = transaction.commit() { + warnings.push(NativeArtifactWarning { + stage: "cleanup".to_owned(), + code: Some(io_error_code(&error)), + message: error.to_string(), + }); + } + if staging_path.exists() { + if let Err(error) = delete_path(&staging_path) { + warnings.push(NativeArtifactWarning { + stage: "cleanup".to_owned(), + code: Some(io_error_code(&error)), + message: error.to_string(), + }); + } + } + durations.cleanup_ms = elapsed_ms(started_at); + + Ok(()) +} + +#[napi] +pub fn install_unpacked_artifact(request: InstallUnpackedArtifactRequest) -> NativeArtifactResult { + let mut durations = NativeArtifactDurations::default(); + let mut warnings = Vec::new(); + match run_unpacked_install(&request, &mut durations, &mut warnings) { + Ok(()) => success(None, durations, warnings), + Err(error) => { + let _ = delete_path(Path::new(&request.staging_path)); + failure(error, durations, warnings) + } + } +} diff --git a/nativeModules/pulsesyncNative/src/file_ops.rs b/nativeModules/pulsesyncNative/src/file_ops.rs new file mode 100644 index 00000000..58a86486 --- /dev/null +++ b/nativeModules/pulsesyncNative/src/file_ops.rs @@ -0,0 +1,120 @@ +use napi::bindgen_prelude::Buffer; +use napi::{Error, Result}; +use napi_derive::napi; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +fn error_with_path(action: &str, path: &Path, error: io::Error) -> Error { + Error::from_reason(format!("{action} '{}': {error}", path.to_string_lossy())) +} + +fn remove_existing(path: &Path) -> io::Result<()> { + match fs::symlink_metadata(path) { + Ok(metadata) if metadata.is_dir() => fs::remove_dir_all(path), + Ok(_) => fs::remove_file(path), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error), + } +} + +pub(crate) fn copy_path(source: &Path, destination: &Path) -> io::Result<()> { + let metadata = fs::symlink_metadata(source)?; + if metadata.is_dir() { + fs::create_dir_all(destination)?; + for entry in fs::read_dir(source)? { + let entry = entry?; + copy_path(&entry.path(), &destination.join(entry.file_name()))?; + } + return Ok(()); + } + + if let Some(parent) = destination.parent() { + fs::create_dir_all(parent)?; + } + fs::copy(source, destination)?; + fs::set_permissions(destination, metadata.permissions())?; + Ok(()) +} + +pub(crate) fn delete_path(path: &Path) -> io::Result<()> { + let metadata = fs::symlink_metadata(path)?; + if metadata.is_dir() { + fs::remove_dir_all(path) + } else { + fs::remove_file(path) + } +} + +pub(crate) fn replace_rename(source: &Path, destination: &Path) -> io::Result<()> { + if let Some(parent) = destination.parent() { + fs::create_dir_all(parent)?; + } + remove_existing(destination)?; + fs::rename(source, destination) +} + +#[napi] +pub fn file_exists(path: String) -> bool { + Path::new(&path).exists() +} + +#[napi] +pub fn read_file(path: String) -> Result { + let path = PathBuf::from(path); + fs::read(&path) + .map(Buffer::from) + .map_err(|error| error_with_path("Failed to read", &path, error)) +} + +#[napi] +pub fn delete_file(path: String) -> Result<()> { + let path = PathBuf::from(path); + delete_path(&path).map_err(|error| error_with_path("Failed to delete", &path, error)) +} + +#[napi] +pub fn rename_file(source: String, destination: String) -> Result<()> { + let source = PathBuf::from(source); + let destination = PathBuf::from(destination); + replace_rename(&source, &destination).map_err(|error| { + Error::from_reason(format!( + "Failed to rename '{}' to '{}': {error}", + source.to_string_lossy(), + destination.to_string_lossy() + )) + }) +} + +#[napi] +pub fn move_file(source: String, destination: String) -> Result<()> { + let source = PathBuf::from(source); + let destination = PathBuf::from(destination); + + if replace_rename(&source, &destination).is_ok() { + return Ok(()); + } + + copy_path(&source, &destination).map_err(|error| { + Error::from_reason(format!( + "Failed to move '{}' to '{}' during copy: {error}", + source.to_string_lossy(), + destination.to_string_lossy() + )) + })?; + delete_path(&source) + .map_err(|error| error_with_path("Failed to remove source after move", &source, error)) +} + +#[napi] +pub fn copy_file(source: String, destination: String) -> Result<()> { + let source = PathBuf::from(source); + let destination = PathBuf::from(destination); + copy_path(&source, &destination).map_err(|error| { + Error::from_reason(format!( + "Failed to copy '{}' to '{}': {error}", + source.to_string_lossy(), + destination.to_string_lossy() + )) + }) +} diff --git a/nativeModules/pulsesyncNative/src/fs_transaction.rs b/nativeModules/pulsesyncNative/src/fs_transaction.rs new file mode 100644 index 00000000..fc8ef164 --- /dev/null +++ b/nativeModules/pulsesyncNative/src/fs_transaction.rs @@ -0,0 +1,116 @@ +use crate::file_ops::{copy_path, delete_path}; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::thread; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub(crate) fn unique_sibling(path: &Path, label: &str) -> PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let name = path + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| "artifact".to_owned()); + path.with_file_name(format!("{name}.{label}-{}-{suffix}", std::process::id())) +} + +pub(crate) fn retry_io(mut operation: F) -> io::Result<()> +where + F: FnMut() -> io::Result<()>, +{ + let attempts = if cfg!(windows) { 6 } else { 2 }; + let mut last_error = None; + + for attempt in 1..=attempts { + match operation() { + Ok(()) => return Ok(()), + Err(error) => { + last_error = Some(error); + if attempt < attempts { + thread::sleep(Duration::from_millis(150 * attempt as u64)); + } + } + } + } + + Err(last_error.unwrap_or_else(|| io::Error::other("Filesystem operation failed"))) +} + +pub(crate) fn move_with_copy_fallback(source: &Path, destination: &Path) -> io::Result<()> { + if let Some(parent) = destination.parent() { + fs::create_dir_all(parent)?; + } + + if retry_io(|| fs::rename(source, destination)).is_ok() { + return Ok(()); + } + + if destination.exists() { + delete_path(destination)?; + } + if let Err(error) = copy_path(source, destination) { + let _ = delete_path(destination); + return Err(error); + } + delete_path(source) +} + +pub(crate) struct DirectoryTransaction { + target: PathBuf, + backup: Option, + committed: bool, +} + +impl DirectoryTransaction { + pub(crate) fn begin(target: &Path) -> io::Result { + let backup = if target.exists() { + let backup = unique_sibling(target, "pulsesync-backup"); + retry_io(|| fs::rename(target, &backup))?; + Some(backup) + } else { + None + }; + + Ok(Self { + target: target.to_path_buf(), + backup, + committed: false, + }) + } + + pub(crate) fn install(&self, source: &Path) -> io::Result<()> { + move_with_copy_fallback(source, &self.target) + } + + pub(crate) fn commit(mut self) -> io::Result> { + self.committed = true; + if let Some(backup) = self.backup.take() { + delete_path(&backup)?; + return Ok(Some(backup)); + } + Ok(None) + } + + pub(crate) fn restore(&mut self) -> io::Result<()> { + if self.target.exists() { + delete_path(&self.target)?; + } + if let Some(backup) = &self.backup { + retry_io(|| fs::rename(backup, &self.target))?; + } + self.committed = true; + Ok(()) + } +} + +impl Drop for DirectoryTransaction { + fn drop(&mut self) { + if self.committed { + return; + } + let _ = self.restore(); + } +} diff --git a/nativeModules/pulsesyncNative/src/hardware_identity.rs b/nativeModules/pulsesyncNative/src/hardware_identity.rs new file mode 100644 index 00000000..5eab8a47 --- /dev/null +++ b/nativeModules/pulsesyncNative/src/hardware_identity.rs @@ -0,0 +1,101 @@ +use napi_derive::napi; +use sha2::{Digest, Sha256}; +#[cfg(target_os = "linux")] +use std::fs; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use std::process::Command; + +const HWID_NAMESPACE: &str = "pulsesync-client-hwid-v1"; + +#[napi(object)] +pub struct HardwareIdentity { + pub hash: String, + pub source: String, + pub algorithm: String, +} + +fn sha256_hex(value: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(HWID_NAMESPACE.as_bytes()); + hasher.update(b"\n"); + hasher.update(value.trim().as_bytes()); + hasher + .finalize() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +#[cfg(target_os = "windows")] +fn read_raw_hardware_id() -> Option<(String, String)> { + let output = Command::new("reg") + .args(["query", r"HKLM\SOFTWARE\Microsoft\Cryptography", "/v", "MachineGuid"]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + if !line.contains("MachineGuid") { + continue; + } + let value = line.split_whitespace().last()?.trim().to_string(); + if !value.is_empty() { + return Some((value, "windows_machine_guid".to_string())); + } + } + None +} + +#[cfg(target_os = "macos")] +fn read_raw_hardware_id() -> Option<(String, String)> { + let output = Command::new("ioreg") + .args(["-rd1", "-c", "IOPlatformExpertDevice"]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + if !line.contains("IOPlatformUUID") { + continue; + } + let value = line.split('=').nth(1)?.trim().trim_matches('"').to_string(); + if !value.is_empty() { + return Some((value, "macos_platform_uuid".to_string())); + } + } + None +} + +#[cfg(target_os = "linux")] +fn read_raw_hardware_id() -> Option<(String, String)> { + for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"] { + let value = match fs::read_to_string(path) { + Ok(value) => value.trim().to_string(), + Err(_) => continue, + }; + if !value.is_empty() { + return Some((value, "linux_machine_id".to_string())); + } + } + None +} + +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +fn read_raw_hardware_id() -> Option<(String, String)> { + None +} + +#[napi] +pub fn get_hardware_identity() -> napi::Result> { + Ok(read_raw_hardware_id().map(|(raw_id, source)| HardwareIdentity { + hash: sha256_hex(&raw_id), + source, + algorithm: "sha256".to_string(), + })) +} diff --git a/nativeModules/pulsesyncNative/src/integrity.rs b/nativeModules/pulsesyncNative/src/integrity.rs new file mode 100644 index 00000000..5f7730a3 --- /dev/null +++ b/nativeModules/pulsesyncNative/src/integrity.rs @@ -0,0 +1,543 @@ +use memmap2::MmapOptions; +use napi::{Error, Result}; +use napi_derive::napi; +#[cfg(target_os = "macos")] +use plist::Value as PlistValue; +use serde_json::Value; +use sha2::{Digest, Sha256}; +#[cfg(target_os = "macos")] +use std::fs; +use std::fs::{File, OpenOptions}; +#[cfg(target_os = "macos")] +use std::io::Write; +use std::io::{Read, Seek, SeekFrom}; +use std::path::Path; +#[cfg(target_os = "macos")] +use std::process::Command; + +const MAX_ASAR_HEADER_SIZE: usize = 128 * 1024 * 1024; +const INTEGRITY_MARKER: &[u8] = br#""file":"resources\\app.asar""#; +const VALUE_MARKER: &[u8] = br#""value":""#; +const SHA256_HEX_LENGTH: usize = 64; +const MAX_ASAR_PACKAGE_JSON_SIZE: usize = 1024 * 1024; + +struct AsarHeader { + json: Vec, + pickle_size: usize, +} + +fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() || needle.len() > haystack.len() { + return None; + } + haystack + .windows(needle.len()) + .position(|window| window == needle) +} + +fn digest_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let digest = Sha256::digest(bytes); + let mut output = String::with_capacity(SHA256_HEX_LENGTH); + for byte in digest { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} + +fn read_asar_header(file: &mut File) -> std::result::Result { + let mut size_pickle = [0_u8; 8]; + file.read_exact(&mut size_pickle) + .map_err(|error| format!("Unable to read ASAR header size: {error}"))?; + + let header_size = + u32::from_le_bytes(size_pickle[4..8].try_into().expect("fixed slice")) as usize; + if !(8..=MAX_ASAR_HEADER_SIZE).contains(&header_size) { + return Err(format!("Invalid ASAR header size: {header_size}")); + } + + file.seek(SeekFrom::Start(8)) + .map_err(|error| format!("Unable to seek to ASAR header: {error}"))?; + let mut header_pickle = vec![0_u8; header_size]; + file.read_exact(&mut header_pickle) + .map_err(|error| format!("Unable to read ASAR header: {error}"))?; + + if header_pickle.len() < 8 { + return Err("ASAR header pickle is truncated".to_owned()); + } + let payload_size = + u32::from_le_bytes(header_pickle[0..4].try_into().expect("fixed slice")) as usize; + let string_size = + u32::from_le_bytes(header_pickle[4..8].try_into().expect("fixed slice")) as usize; + let string_end = 8_usize + .checked_add(string_size) + .ok_or_else(|| "ASAR header string size overflow".to_owned())?; + + if payload_size > header_pickle.len().saturating_sub(4) || string_end > header_pickle.len() { + return Err("ASAR header pickle contains invalid lengths".to_owned()); + } + + let header_string = header_pickle[8..string_end].to_vec(); + std::str::from_utf8(&header_string) + .map_err(|error| format!("ASAR header is not valid UTF-8: {error}"))?; + Ok(AsarHeader { + json: header_string, + pickle_size: header_size, + }) +} + +fn read_asar_header_string(path: &Path) -> std::result::Result, String> { + let mut file = File::open(path) + .map_err(|error| format!("Failed to open ASAR '{}': {error}", path.display()))?; + read_asar_header(&mut file).map(|header| header.json) +} + +fn calculate_header_hash(path: &Path) -> std::result::Result { + read_asar_header_string(path).map(|header| digest_hex(&header)) +} + +#[napi] +pub fn calculate_asar_header_hash(path: String) -> Result { + calculate_header_hash(Path::new(&path)).map_err(Error::from_reason) +} + +fn read_asar_version_impl(path: &Path) -> std::result::Result { + let package_json = { + let mut file = File::open(path) + .map_err(|error| format!("Failed to open ASAR '{}': {error}", path.display()))?; + let header = read_asar_header(&mut file)?; + let header_json: Value = serde_json::from_slice(&header.json) + .map_err(|error| format!("Failed to parse ASAR header JSON: {error}"))?; + let package_entry = header_json + .get("files") + .and_then(Value::as_object) + .and_then(|files| files.get("package.json")) + .ok_or_else(|| "package.json was not found in ASAR header".to_owned())?; + if package_entry + .get("unpacked") + .and_then(Value::as_bool) + .unwrap_or(false) + { + return Err("Unpacked package.json is not supported".to_owned()); + } + + let package_size = package_entry + .get("size") + .and_then(Value::as_u64) + .ok_or_else(|| "ASAR package.json size is missing".to_owned())?; + let package_size = usize::try_from(package_size) + .map_err(|_| "ASAR package.json size is too large".to_owned())?; + if package_size == 0 || package_size > MAX_ASAR_PACKAGE_JSON_SIZE { + return Err(format!("Invalid ASAR package.json size: {package_size}")); + } + let package_offset = package_entry + .get("offset") + .and_then(Value::as_str) + .ok_or_else(|| "ASAR package.json offset is missing".to_owned())? + .parse::() + .map_err(|error| format!("Invalid ASAR package.json offset: {error}"))?; + let data_offset = 8_u64 + .checked_add(header.pickle_size as u64) + .and_then(|offset| offset.checked_add(package_offset)) + .ok_or_else(|| "ASAR package.json offset overflow".to_owned())?; + + file.seek(SeekFrom::Start(data_offset)) + .map_err(|error| format!("Unable to seek to ASAR package.json: {error}"))?; + let mut package_json = vec![0_u8; package_size]; + file.read_exact(&mut package_json) + .map_err(|error| format!("Unable to read ASAR package.json: {error}"))?; + package_json + }; + + let package: Value = serde_json::from_slice(&package_json) + .map_err(|error| format!("Failed to parse ASAR package.json: {error}"))?; + let version = package + .get("modification") + .and_then(|modification| modification.get("realYMVersion")) + .and_then(Value::as_str) + .or_else(|| package.get("version").and_then(Value::as_str)) + .map(str::trim) + .filter(|version| !version.is_empty()) + .ok_or_else(|| "ASAR package.json does not contain a version".to_owned())?; + Ok(version.to_owned()) +} + +#[napi] +pub fn read_asar_version(path: String) -> Result { + read_asar_version_impl(Path::new(&path)).map_err(Error::from_reason) +} + +#[napi] +pub fn patch_windows_integrity(exe_path: String, asar_path: String) -> Result { + let hash = calculate_header_hash(Path::new(&asar_path)).map_err(Error::from_reason)?; + let file = OpenOptions::new() + .read(true) + .write(true) + .open(&exe_path) + .map_err(|error| { + Error::from_reason(format!("Failed to open executable '{exe_path}': {error}")) + })?; + let mut map = unsafe { MmapOptions::new().map_mut(&file) }.map_err(|error| { + Error::from_reason(format!("Failed to map executable '{exe_path}': {error}")) + })?; + + let marker_offset = find_bytes(&map, INTEGRITY_MARKER) + .ok_or_else(|| Error::from_reason("resources\\app.asar integrity record not found"))?; + let object_start = map[..marker_offset] + .iter() + .rposition(|byte| *byte == b'{') + .ok_or_else(|| Error::from_reason("Integrity JSON object start not found"))?; + let object_end = map[marker_offset..] + .iter() + .position(|byte| *byte == b'}') + .map(|offset| marker_offset + offset) + .ok_or_else(|| Error::from_reason("Integrity JSON object end not found"))?; + let object = &map[object_start..=object_end]; + let value_marker_offset = find_bytes(object, VALUE_MARKER) + .map(|offset| object_start + offset) + .ok_or_else(|| Error::from_reason("Integrity value field not found"))?; + let value_offset = value_marker_offset + VALUE_MARKER.len(); + let value_end = value_offset + SHA256_HEX_LENGTH; + + if value_end >= map.len() || map[value_end] != b'"' { + return Err(Error::from_reason( + "Integrity SHA-256 value has unexpected length", + )); + } + if !map[value_offset..value_end] + .iter() + .all(u8::is_ascii_hexdigit) + { + return Err(Error::from_reason( + "Integrity value is not a SHA-256 hex string", + )); + } + + map[value_offset..value_end].copy_from_slice(hash.as_bytes()); + map.flush_range(value_offset, SHA256_HEX_LENGTH) + .map_err(|error| { + Error::from_reason(format!( + "Failed to flush executable integrity patch: {error}" + )) + })?; + Ok(hash) +} + +#[cfg(target_os = "macos")] +fn update_mac_info_plist( + info_plist_path: &Path, + asar_path: &Path, +) -> std::result::Result<(String, Vec), String> { + let hash = calculate_header_hash(asar_path)?; + let original = fs::read(info_plist_path).map_err(|error| { + format!( + "Failed to read Info.plist '{}': {error}", + info_plist_path.display() + ) + })?; + let is_binary = original.starts_with(b"bplist"); + let mut plist = PlistValue::from_reader(std::io::Cursor::new(&original)).map_err(|error| { + format!( + "Failed to read Info.plist '{}': {error}", + info_plist_path.display() + ) + })?; + let root = plist + .as_dictionary_mut() + .ok_or_else(|| "Info.plist root is not a dictionary".to_owned())?; + let integrity = root + .get_mut("ElectronAsarIntegrity") + .and_then(PlistValue::as_dictionary_mut) + .ok_or_else(|| "ElectronAsarIntegrity is missing from Info.plist".to_owned())?; + let app_asar = integrity + .get_mut("Resources/app.asar") + .and_then(PlistValue::as_dictionary_mut) + .ok_or_else(|| { + "Resources/app.asar integrity entry is missing from Info.plist".to_owned() + })?; + + app_asar.insert("hash".to_owned(), PlistValue::String(hash.clone())); + let mut output = Vec::new(); + if is_binary { + plist.to_writer_binary(&mut output) + } else { + plist.to_writer_xml(&mut output) + } + .map_err(|error| format!("Failed to serialize Info.plist: {error}"))?; + + write_file_atomically(info_plist_path, &output)?; + Ok((hash, original)) +} + +#[cfg(target_os = "macos")] +fn write_file_atomically(path: &Path, contents: &[u8]) -> std::result::Result<(), String> { + let parent = path + .parent() + .ok_or_else(|| format!("File '{}' has no parent directory", path.display()))?; + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| format!("File '{}' has an invalid name", path.display()))?; + let permissions = fs::metadata(path) + .map(|metadata| metadata.permissions()) + .map_err(|error| { + format!( + "Failed to read permissions for '{}': {error}", + path.display() + ) + })?; + + for attempt in 0..10 { + let temporary_path = parent.join(format!( + ".{file_name}.pulsesync-{}-{attempt}.tmp", + std::process::id() + )); + let mut temporary = match fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&temporary_path) + { + Ok(file) => file, + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => { + return Err(format!( + "Failed to create temporary file '{}': {error}", + temporary_path.display() + )); + } + }; + + let result = temporary + .write_all(contents) + .and_then(|_| temporary.sync_all()) + .and_then(|_| fs::set_permissions(&temporary_path, permissions.clone())) + .and_then(|_| fs::rename(&temporary_path, path)); + if let Err(error) = result { + let _ = fs::remove_file(&temporary_path); + return Err(format!( + "Failed to replace '{}' atomically: {error}", + path.display() + )); + } + return Ok(()); + } + + Err(format!( + "Failed to create a unique temporary file for '{}'", + path.display() + )) +} + +#[cfg(target_os = "macos")] +fn command_output(command: &mut Command, action: &str) -> std::result::Result, String> { + let output = command + .output() + .map_err(|error| format!("Failed to run {action}: {error}"))?; + if output.status.success() { + return Ok(output.stdout); + } + + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned(); + let detail = if !stderr.is_empty() { + stderr + } else if !stdout.is_empty() { + stdout + } else { + format!("exit status {}", output.status) + }; + Err(format!("Failed to {action}: {detail}")) +} + +#[cfg(target_os = "macos")] +fn dump_mac_entitlements( + app_bundle_path: &Path, + entitlements_path: &Path, +) -> std::result::Result<(), String> { + let mut command = Command::new("codesign"); + command + .arg("-d") + .arg("--entitlements") + .arg(":-") + .arg(app_bundle_path); + let entitlements = command_output(&mut command, "dump macOS code signing entitlements")?; + if entitlements.is_empty() { + return Err("codesign returned empty entitlements".to_owned()); + } + if let Some(parent) = entitlements_path.parent() { + fs::create_dir_all(parent).map_err(|error| { + format!( + "Failed to create entitlements directory '{}': {error}", + parent.display() + ) + })?; + } + fs::write(entitlements_path, entitlements).map_err(|error| { + format!( + "Failed to write entitlements '{}': {error}", + entitlements_path.display() + ) + }) +} + +#[cfg(target_os = "macos")] +fn sign_mac_app( + app_bundle_path: &Path, + entitlements_path: &Path, +) -> std::result::Result<(), String> { + let mut command = Command::new("codesign"); + command + .arg("--force") + .arg("--entitlements") + .arg(entitlements_path) + .arg("--sign") + .arg("-") + .arg(app_bundle_path); + command_output(&mut command, "re-sign macOS application").map(|_| ()) +} + +#[napi] +#[cfg(target_os = "macos")] +pub fn patch_mac_integrity( + app_bundle_path: String, + asar_path: String, + entitlements_path: String, +) -> Result { + let app_bundle_path = Path::new(&app_bundle_path); + let asar_path = Path::new(&asar_path); + let entitlements_path = Path::new(&entitlements_path); + let info_plist_path = app_bundle_path.join("Contents").join("Info.plist"); + + dump_mac_entitlements(app_bundle_path, entitlements_path).map_err(Error::from_reason)?; + let (hash, original_info_plist) = match update_mac_info_plist(&info_plist_path, asar_path) { + Ok(result) => result, + Err(error) => { + let _ = fs::remove_file(entitlements_path); + return Err(Error::from_reason(error)); + } + }; + let result = sign_mac_app(app_bundle_path, entitlements_path); + let _ = fs::remove_file(entitlements_path); + if let Err(error) = result { + return match write_file_atomically(&info_plist_path, &original_info_plist) { + Ok(()) => Err(Error::from_reason(error)), + Err(rollback_error) => Err(Error::from_reason(format!( + "{error}; failed to restore Info.plist: {rollback_error}" + ))), + }; + } + Ok(hash) +} + +#[napi] +#[cfg(not(target_os = "macos"))] +pub fn patch_mac_integrity( + _app_bundle_path: String, + _asar_path: String, + _entitlements_path: String, +) -> Result { + Err(Error::from_reason( + "macOS integrity patching is only available on macOS", + )) +} + +#[cfg(all(test, target_os = "macos"))] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + const INFO_PLIST_XML: &[u8] = br#" + + + + ElectronAsarIntegrity + + Resources/app.asar + + algorithm + SHA256 + hash + old + + + + +"#; + + fn test_directory(name: &str) -> std::path::PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock should be after UNIX epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "pulsesync-integrity-{name}-{}-{nonce}", + std::process::id() + )) + } + + fn write_test_asar(path: &Path) { + let header = br#"{"files":{}}"#; + let header_size = 8 + header.len(); + let mut archive = Vec::new(); + archive.extend_from_slice(&0_u32.to_le_bytes()); + archive.extend_from_slice(&(header_size as u32).to_le_bytes()); + archive.extend_from_slice(&((header_size - 4) as u32).to_le_bytes()); + archive.extend_from_slice(&(header.len() as u32).to_le_bytes()); + archive.extend_from_slice(header); + fs::write(path, archive).expect("test ASAR should be written"); + } + + fn installed_hash(path: &Path) -> String { + let plist = PlistValue::from_file(path).expect("updated plist should parse"); + plist + .as_dictionary() + .and_then(|root| root.get("ElectronAsarIntegrity")) + .and_then(PlistValue::as_dictionary) + .and_then(|integrity| integrity.get("Resources/app.asar")) + .and_then(PlistValue::as_dictionary) + .and_then(|entry| entry.get("hash")) + .and_then(PlistValue::as_string) + .expect("updated plist should contain integrity hash") + .to_owned() + } + + fn assert_plist_update(name: &str, original: Vec, expected_prefix: &[u8]) { + let directory = test_directory(name); + fs::create_dir_all(&directory).expect("test directory should be created"); + let plist_path = directory.join("Info.plist"); + let asar_path = directory.join("app.asar"); + fs::write(&plist_path, &original).expect("test plist should be written"); + write_test_asar(&asar_path); + + let (hash, preserved_original) = + update_mac_info_plist(&plist_path, &asar_path).expect("plist update should succeed"); + + assert_eq!(preserved_original, original); + assert!( + fs::read(&plist_path) + .expect("updated plist should be readable") + .starts_with(expected_prefix) + ); + assert_eq!(installed_hash(&plist_path), hash); + assert_eq!(hash.len(), SHA256_HEX_LENGTH); + fs::remove_dir_all(directory).expect("test directory should be removed"); + } + + #[test] + fn updates_xml_plist_atomically() { + assert_plist_update("xml", INFO_PLIST_XML.to_vec(), b" &'static str { + env!("CARGO_PKG_VERSION") +} diff --git a/nativeModules/pulsesyncNative/src/watcher.rs b/nativeModules/pulsesyncNative/src/watcher.rs new file mode 100644 index 00000000..ae819560 --- /dev/null +++ b/nativeModules/pulsesyncNative/src/watcher.rs @@ -0,0 +1,100 @@ +use napi::bindgen_prelude::{FnArgs, Function}; +use napi::threadsafe_function::ThreadsafeFunctionCallMode; +use napi::{Result, Status}; +use napi_derive::napi; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::thread; +use std::time::{Duration, SystemTime}; + +type Snapshot = HashMap; + +fn is_watched_file(path: &Path) -> bool { + path.extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| { + extension.eq_ignore_ascii_case("js") || extension.eq_ignore_ascii_case("css") + }) +} + +fn visit_directory(path: &Path, snapshot: &mut Snapshot) { + let Ok(entries) = fs::read_dir(path) else { + return; + }; + + for entry in entries.flatten() { + let entry_path = entry.path(); + let Ok(metadata) = entry.metadata() else { + continue; + }; + if metadata.is_dir() { + visit_directory(&entry_path, snapshot); + } else if metadata.is_file() && is_watched_file(&entry_path) { + if let Ok(modified) = metadata.modified() { + snapshot.insert(entry_path, modified); + } + } + } +} + +fn snapshot_directory(root: &Path) -> Snapshot { + let mut snapshot = Snapshot::new(); + visit_directory(root, &mut snapshot); + snapshot +} + +#[napi] +pub fn watch( + path: String, + interval_ms: u32, + callback: Function<'_, FnArgs<(String, String)>, ()>, +) -> Result<()> { + let callback = callback + .build_threadsafe_function() + .weak::() + .build()?; + let root = PathBuf::from(path); + let interval = Duration::from_millis(u64::from(interval_ms.max(50))); + + thread::spawn(move || { + let mut previous = snapshot_directory(&root); + loop { + thread::sleep(interval); + let current = snapshot_directory(&root); + + for (path, modified) in ¤t { + let event = match previous.get(path) { + None => Some("add"), + Some(previous_modified) if previous_modified != modified => Some("change"), + _ => None, + }; + if let Some(event) = event { + let status = callback.call( + FnArgs::from((event.to_owned(), path.to_string_lossy().into_owned())), + ThreadsafeFunctionCallMode::NonBlocking, + ); + if status == Status::Closing { + return; + } + } + } + + for path in previous.keys() { + if !current.contains_key(path) { + let status = callback.call( + FnArgs::from(("unlink".to_owned(), path.to_string_lossy().into_owned())), + ThreadsafeFunctionCallMode::NonBlocking, + ); + if status == Status::Closing { + return; + } + } + } + + previous = current; + } + }); + + Ok(()) +} diff --git a/package.json b/package.json index 4aa07c55..62264aa4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "author": "PulseSync ", "description": "PulseSync app for desktops", "homepage": "https://pulsesync.dev", - "version": "2.16.0-beta", + "version": "2.17.0-beta", "main": ".vite/main/index.cjs", "scripts": { "start": "electron-forge start", @@ -23,13 +23,13 @@ }, "type": "module", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" }, "keywords": [], "license": "SEE LICENSE IN LICENSE", "devDependencies": { - "@aws-sdk/client-s3": "^3.1053.0", - "@babel/core": "7.29.0", + "@aws-sdk/client-s3": "^3.1075.0", + "@babel/core": "8.0.1", "@electron-forge/cli": "^7.11.2", "@electron-forge/maker-deb": "^7.11.2", "@electron-forge/maker-dmg": "^7.11.2", @@ -38,14 +38,15 @@ "@electron-forge/maker-zip": "^7.11.2", "@electron-forge/plugin-vite": "^7.11.2", "@rolldown/plugin-babel": "^0.2.3", + "@sentry/cli": "3.6.0", "@types/adm-zip": "^0.5.8", "@types/electron": "^1.6.12", "@types/fs-extra": "^11.0.4", "@types/glob": "^9.0.0", "@types/lodash.debounce": "^4.0.9", - "@types/node": "^25.9.1", + "@types/node": "^26.0.1", "@types/plist": "^3.0.5", - "@types/react": "^19.2.15", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/react-modal": "^3.16.3", "@types/react-transition-group": "^4.4.12", @@ -59,81 +60,81 @@ "@types/unzipper": "^0.10.11", "@types/uuid": "^11.0.0", "@types/yazl": "^3.3.1", - "@typescript-eslint/eslint-plugin": "^8.59.4", - "@typescript-eslint/parser": "^8.59.4", - "@vitejs/plugin-react": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^8.62.0", + "@typescript-eslint/parser": "^8.62.0", + "@vitejs/plugin-react": "^6.0.3", "babel-plugin-react-compiler": "^1.0.0", - "electron": "42.2.0", - "electron-builder": "^26.8.1", - "eslint": "^10.4.0", + "electron": "42.5.1", + "electron-builder": "^26.15.3", + "eslint": "^10.6.0", "eslint-plugin-import": "^2.32.0", "iconv-lite": "^0.7.2", - "prettier": "^3.8.3", - "sass": "^1.100.0", + "prettier": "^3.9.3", + "sass": "^1.101.0", "ts-node": "^10.9.2", - "tsx": "4.22.3", + "tsx": "4.22.4", "typescript": "^6.0.3", - "vite": "^8.0.14", + "vite": "^8.1.0", "vite-plugin-svgr": "^5.2.0" }, "dependencies": { - "@apollo/client": "^4.2.0", + "@apollo/client": "^4.2.3", "@dr.pogodin/react-helmet": "^3.2.2", "@electron-forge/plugin-fuses": "^7.11.2", - "@electron/fuses": "2.1.1", + "@electron/fuses": "2.1.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/joy": "^5.0.0-beta.52", - "@radix-ui/react-tooltip": "^1.2.8", - "acorn": "^8.16.0", + "@radix-ui/react-tooltip": "^1.2.10", + "@sentry/electron": "7.14.0", + "acorn": "^8.17.0", "acorn-walk": "^8.3.5", - "adm-zip": "^0.5.17", - "axios": "^1.16.1", + "adm-zip": "^0.5.18", + "axios": "^1.18.1", "browserify-fs": "^1.0.0", - "browserify-sign": "^4.2.5", + "browserify-sign": "^4.2.6", "chart.js": "^4.5.1", "crypto-browserify": "^3.12.1", "electron-chrome-web-store": "^0.13.0", "electron-store": "11.0.2", - "electron-updater": "^6.8.3", + "electron-updater": "^6.8.9", "formik": "^2.4.9", - "framer-motion": "^12.40.0", + "framer-motion": "^12.42.0", "fs-extra": "^11.3.5", "glob": "^13.0.6", - "graphql": "^16.14.0", + "graphql": "^17.0.1", "graphql-ws": "^6.0.8", - "i18next": "^26.2.0", + "i18next": "^26.3.3", "lodash.debounce": "^4.0.8", "log4js": "^6.9.1", "net": "^1.0.2", "path-browserify": "^1.0.1", - "plist": "^5.0.0", - "react": "^19.2.6", + "react": "^19.2.7", "react-chartjs-2": "^5.3.1", - "react-dom": "^19.2.6", + "react-dom": "^19.2.7", "react-hot-toast": "^2.6.0", "react-i18next": "^17.0.8", "react-icons": "^5.6.0", "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", "react-modal": "^3.16.3", - "react-router-dom": "^7.15.1", + "react-router-dom": "^7.18.0", "react-transition-group": "^4.4.5", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", "resedit": "^3.0.2", "rxjs": "^7.8.2", - "semver": "^7.8.1", + "semver": "^7.8.5", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "source-map-support": "^0.5.21", "stream-browserify": "^3.0.0", "string-similarity": "^4.0.4", - "systeminformation": "^5.31.6", - "tar": "^7.5.15", + "systeminformation": "^5.31.11", + "tar": "^7.5.19", "url": "^0.11.4", - "uuid": "^14.0.0", + "uuid": "^14.0.1", "yaml": "^2.9.0", "yup": "^1.7.1", "zstd-codec": "^0.1.5" @@ -141,18 +142,13 @@ "resolutions": { "@electron/rebuild": "4.0.4", "@parcel/watcher": "2.5.6", - "node-gyp": "12.3.0" + "node-gyp": "13.0.0" }, "config": { "forge": "./forge.config.ts" }, - "buildInfo": { - "VERSION": "2.16.0-beta", - "BRANCH": "4cc7ca30", - "BUILD_TIME": "17.05.2026, 23:38:32" - }, "optionalDependencies": { "bufferutil": "^4.1.0", "utf-8-validate": "^6.0.6" } -} \ No newline at end of file +} diff --git a/scripts/build.ts b/scripts/build.ts index 37120ba3..8f474088 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -13,6 +13,7 @@ import * as tar from 'tar' import { fileURLToPath } from 'node:url' import { generateAndPublishMacDownloadJson, publishToS3 } from './s3-upload.js' import { publishChangelogToApi, publishPatchNotesToDiscord } from './changelog-publish.js' +import { assertGlitchTipSourceMapConfig, uploadGlitchTipSourceMaps } from './glitchtip-sourcemaps.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -118,6 +119,24 @@ function resolvePublishedVersion(currentVersion: string, targetBranch: string): return `${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}-${targetBranch}` } +function normalizePemEnv(value: string): string { + return value.replace(/\\n/g, '\n').trim() +} + +function createBuildIdentityPayload(identity: { origin: string; version: string; commit: string; builtAt: string }): Buffer { + return Buffer.from(`${identity.origin}\n${identity.version}\n${identity.commit}\n${identity.builtAt}`, 'utf8') +} + +function signBuildIdentity(identity: { origin: string; version: string; commit: string; builtAt: string }): string { + const privateKeyRaw = process.env.CLIENT_BUILD_IDENTITY_PRIVATE_KEY?.trim() + if (!privateKeyRaw) { + return '' + } + + const privateKey = crypto.createPrivateKey(normalizePemEnv(privateKeyRaw)) + return crypto.sign(null, createBuildIdentityPayload(identity), privateKey).toString('base64') +} + function generateBuildInfo(): { version: string } { const pkgPath = path.resolve(__dirname, '../package.json') log(LogLevel.INFO, `Reading package.json from ${pkgPath}`) @@ -149,14 +168,28 @@ function generateBuildInfo(): { version: string } { pkg.version = newVersion } + const builtAt = new Date().toISOString() + const buildIdentity = { + origin: 'PulseSync-LLC/PulseSync-client', + version: pkg.version, + commit: branchHash, + builtAt, + } + const signature = signBuildIdentity(buildIdentity) + pkg.buildInfo = { - VERSION: pkg.version, - BRANCH: branchHash, - BUILD_TIME: new Date().toLocaleString(), + VERSION: buildIdentity.version, + BRANCH: buildIdentity.commit, + BUILD_TIME: buildIdentity.builtAt, + SIGNATURE_ALGORITHM: 'ed25519', + SIGNATURE: signature, } fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4), 'utf-8') - log(LogLevel.SUCCESS, `Updated package.json → version=${newVersion}, buildInfo.BRANCH=${branchHash}`) + log( + LogLevel.SUCCESS, + `Updated package.json → version=${newVersion}, buildInfo.BRANCH=${branchHash}, buildIdentity=${signature ? 'signed' : 'unsigned'}`, + ) return { version: newVersion } } @@ -193,6 +226,13 @@ async function runCommandStep(name: string, command: string): Promise { } } +function setBuildDist(platform: NodeJS.Platform, arch: string): string { + const dist = `${platform}-${arch}` + process.env.PULSESYNC_BUILD_DIST = dist + log(LogLevel.INFO, `GlitchTip dist: ${dist}`) + return dist +} + function ensureNodeHeapForMac(): void { if (os.platform() !== 'darwin') return const currentOptions = process.env.NODE_OPTIONS ?? '' @@ -283,6 +323,9 @@ async function main(): Promise { return } ensureNodeHeapForMac() + if (buildApplication) { + assertGlitchTipSourceMapConfig() + } log(LogLevel.INFO, `Platform: ${os.platform()}, Arch: ${os.arch()}`) log(LogLevel.INFO, `CWD: ${process.cwd()}`) @@ -373,8 +416,10 @@ async function main(): Promise { if (os.platform() === 'darwin') { const targetArch = macX64Build ? 'x64' : 'arm64' + setBuildDist('darwin', targetArch) await runCommandStep(`Package (electron-forge:${targetArch})`, `electron-forge package --arch ${targetArch}`) } else { + setBuildDist(os.platform(), os.arch()) await runCommandStep('Package (electron-forge)', 'electron-forge package') const nativeDir = path.resolve(__dirname, '../nativeModules') @@ -383,6 +428,9 @@ async function main(): Promise { if (!fs.existsSync(modulePath) || !fs.statSync(modulePath).isDirectory()) { continue } + if (!fs.existsSync(path.join(modulePath, 'package.json'))) { + continue + } const releasePath = path.join(modulePath, 'build', 'Release') if (!fs.existsSync(releasePath) || !fs.statSync(releasePath).isDirectory()) { @@ -474,6 +522,8 @@ async function main(): Promise { fs.unlinkSync(tmpPath) + await uploadGlitchTipSourceMaps(version) + if (publishBranch) { await publishToS3(publishBranch, releaseDir, version) if (os.platform() === 'darwin') { diff --git a/scripts/glitchtip-sourcemaps.ts b/scripts/glitchtip-sourcemaps.ts new file mode 100644 index 00000000..8a5186ca --- /dev/null +++ b/scripts/glitchtip-sourcemaps.ts @@ -0,0 +1,218 @@ +import fs from 'node:fs' +import path from 'node:path' +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const stagingRoot = path.resolve(__dirname, '..', '.glitchtip-sourcemaps') +const bundledSentryCliPath = path.resolve(__dirname, '..', 'node_modules', '@sentry', 'cli', 'bin', 'sentry-cli') +const sourceMapExtensions = new Set(['.js', '.cjs', '.mjs', '.map']) + +const sourceMapsEnabled = (): boolean => process.env.GLITCHTIP_SOURCEMAPS === '1' +const uploadEnabled = (): boolean => process.env.GLITCHTIP_SOURCEMAPS_UPLOAD === '1' + +function runSentryCli(args: string[]): void { + const override = process.env.SENTRY_CLI_PATH?.trim() + const command = override || process.execPath + const commandArgs = override ? args : [bundledSentryCliPath, ...args] + + const result = spawnSync(command, commandArgs, { + env: process.env, + stdio: 'inherit', + }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`Sentry CLI failed with exit code ${result.status ?? 'unknown'}`) + } +} + +function copySourceMapArtifacts(sourceDir: string, destinationDir: string): { files: number; maps: number } { + let files = 0 + let maps = 0 + const entries = fs.readdirSync(sourceDir, { withFileTypes: true }) + const mappedSourceFiles = new Set( + entries + .filter(entry => entry.isFile() && path.extname(entry.name).toLowerCase() === '.map') + .map(entry => entry.name.slice(0, -'.map'.length)), + ) + + for (const entry of entries) { + const sourcePath = path.join(sourceDir, entry.name) + const destinationPath = path.join(destinationDir, entry.name) + + if (entry.isDirectory()) { + const copied = copySourceMapArtifacts(sourcePath, destinationPath) + files += copied.files + maps += copied.maps + continue + } + + const extension = path.extname(entry.name).toLowerCase() + if (!entry.isFile() || !sourceMapExtensions.has(extension)) continue + if (extension !== '.map' && !mappedSourceFiles.has(entry.name)) continue + + fs.mkdirSync(destinationDir, { recursive: true }) + fs.copyFileSync(sourcePath, destinationPath) + files += 1 + if (extension === '.map') maps += 1 + } + + return { files, maps } +} + +function removeSourceMaps(directory: string): void { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name) + if (entry.isDirectory()) { + removeSourceMaps(entryPath) + } else if (entry.isFile() && path.extname(entry.name).toLowerCase() === '.map') { + fs.rmSync(entryPath, { force: true }) + } + } +} + +function findSourceMapDirectories(directory: string): string[] { + const directories = new Set() + + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name) + if (entry.isDirectory()) { + for (const child of findSourceMapDirectories(entryPath)) directories.add(child) + } else if (entry.isFile() && path.extname(entry.name).toLowerCase() === '.map') { + directories.add(directory) + } + } + + return [...directories].sort() +} + +function getViteUrlPrefix(stagedDirectory: string): string { + const relativeParts = path.relative(stagingRoot, stagedDirectory).split(path.sep).filter(Boolean) + if (relativeParts.length < 2) { + throw new Error(`Unexpected GlitchTip source-map staging directory: ${stagedDirectory}`) + } + + return `app:///.vite/${relativeParts.slice(1).join('/')}` +} + +function getViteDist(stagedDirectory: string): string { + const [dist] = path.relative(stagingRoot, stagedDirectory).split(path.sep).filter(Boolean) + if (!dist) { + throw new Error(`Unexpected GlitchTip source-map staging directory: ${stagedDirectory}`) + } + + return dist +} + +async function ensureGlitchTipRelease(release: string): Promise { + const baseUrl = process.env.SENTRY_URL!.trim().replace(/\/$/u, '') + const organization = process.env.SENTRY_ORG!.trim() + const project = process.env.SENTRY_PROJECT!.trim() + const releaseUrl = `${baseUrl}/api/0/organizations/${encodeURIComponent(organization)}/releases/${encodeURIComponent(release)}/` + const headers = { + Authorization: `Bearer ${process.env.SENTRY_AUTH_TOKEN!.trim()}`, + } + + const existingRelease = await fetch(releaseUrl, { headers }) + if (existingRelease.ok) return + if (existingRelease.status !== 404) { + throw new Error(`Failed to check GlitchTip release: HTTP ${existingRelease.status}`) + } + + const createRelease = await fetch(`${baseUrl}/api/0/organizations/${encodeURIComponent(organization)}/releases/`, { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + version: release, + projects: [project], + dateStarted: new Date().toISOString(), + }), + }) + if (createRelease.ok) { + console.log(`Created GlitchTip release ${release}`) + return + } + + // Matrix jobs may race while creating the same release. Accept the race only + // when the release is now visible; surface every other API failure. + const releaseAfterConflict = await fetch(releaseUrl, { headers }) + if (!releaseAfterConflict.ok) { + throw new Error(`Failed to create GlitchTip release: HTTP ${createRelease.status}`) + } +} + +export function assertGlitchTipSourceMapConfig(): void { + if (!uploadEnabled()) return + if (!sourceMapsEnabled()) { + throw new Error('GLITCHTIP_SOURCEMAPS_UPLOAD=1 requires GLITCHTIP_SOURCEMAPS=1') + } + + const requiredVariables = ['SENTRY_URL', 'SENTRY_AUTH_TOKEN', 'SENTRY_ORG', 'SENTRY_PROJECT'] as const + const missingVariables = requiredVariables.filter(name => !process.env[name]?.trim()) + if (missingVariables.length > 0) { + throw new Error(`Missing GlitchTip source-map configuration: ${missingVariables.join(', ')}`) + } +} + +export function prepareGlitchTipSourceMaps(buildPath: string, platform: string, arch: string): void { + if (!sourceMapsEnabled()) return + + const viteDirectory = path.join(buildPath, '.vite') + if (!fs.existsSync(viteDirectory)) { + throw new Error(`Vite output not found for GlitchTip source maps: ${viteDirectory}`) + } + + fs.rmSync(stagingRoot, { force: true, recursive: true }) + runSentryCli(['sourcemaps', 'inject', viteDirectory]) + + const destinationDirectory = path.join(stagingRoot, `${platform}-${arch}`) + const copied = copySourceMapArtifacts(viteDirectory, destinationDirectory) + if (copied.maps === 0) { + throw new Error(`No source maps found after GlitchTip injection in ${viteDirectory}`) + } + + removeSourceMaps(viteDirectory) + console.log(`Prepared ${copied.maps} GlitchTip source maps (${copied.files} files) for ${platform}-${arch}`) +} + +export async function uploadGlitchTipSourceMaps(version: string): Promise { + if (!uploadEnabled()) return + assertGlitchTipSourceMapConfig() + + if (!fs.existsSync(stagingRoot)) { + throw new Error(`GlitchTip source-map staging directory not found: ${stagingRoot}`) + } + + const release = `pulsesync-client@${version}` + await ensureGlitchTipRelease(release) + + const sourceMapDirectories = findSourceMapDirectories(stagingRoot) + if (sourceMapDirectories.length === 0) { + throw new Error(`No GlitchTip source maps found in ${stagingRoot}`) + } + + for (const sourceMapDirectory of sourceMapDirectories) { + runSentryCli([ + 'sourcemaps', + 'upload', + sourceMapDirectory, + '--release', + release, + '--org', + process.env.SENTRY_ORG!.trim(), + '--project', + process.env.SENTRY_PROJECT!.trim(), + '--dist', + getViteDist(sourceMapDirectory), + '--url-prefix', + getViteUrlPrefix(sourceMapDirectory), + '--validate', + ]) + } + + fs.rmSync(stagingRoot, { force: true, recursive: true }) + console.log(`Uploaded GlitchTip source maps for ${release}`) +} diff --git a/scripts/strip-package-buildinfo.cjs b/scripts/strip-package-buildinfo.cjs new file mode 100644 index 00000000..1ca7eb74 --- /dev/null +++ b/scripts/strip-package-buildinfo.cjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +const chunks = [] + +process.stdin.on('data', chunk => chunks.push(chunk)) +process.stdin.on('end', () => { + const input = Buffer.concat(chunks).toString('utf8') + + try { + const packageJson = JSON.parse(input) + delete packageJson.buildInfo + process.stdout.write(`${JSON.stringify(packageJson, null, 4)}\n`) + } catch { + process.stdout.write(input) + } +}) diff --git a/src/common/addons/handleEvents.ts b/src/common/addons/handleEvents.ts index a1508bd2..e18acfe4 100644 --- a/src/common/addons/handleEvents.ts +++ b/src/common/addons/handleEvents.ts @@ -105,16 +105,13 @@ export const normalizeAddonSettingsValues = (input: unknown): AddonSettingsValue return {} } - return Object.entries(input as Record).reduce( - (acc, [key, value]) => { - const normalizedKey = String(key || '').trim() - if (normalizedKey) { - acc[normalizedKey] = value - } - return acc - }, - {} as AddonSettingsValues, - ) + return Object.entries(input as Record).reduce((acc, [key, value]) => { + const normalizedKey = String(key || '').trim() + if (normalizedKey) { + acc[normalizedKey] = value + } + return acc + }, {} as AddonSettingsValues) } export const extractHandleConfigItemValue = (item: HandleConfigItem, storedValues?: AddonSettingsValues): unknown => { diff --git a/src/common/appConfig.ts b/src/common/appConfig.ts index 4675ce24..9e25cc21 100644 --- a/src/common/appConfig.ts +++ b/src/common/appConfig.ts @@ -1,10 +1,10 @@ export const isDev = false export const isDevmark = true -export const branch = "beta" +export const branch = 'beta' const PORT = '2007' const MAIN_PORT = 2007 -const AUTONOMOUS_MUSIC_VERSION = '5.90.0' +const AUTONOMOUS_MUSIC_VERSION = '5.105.3' const config = { PORT, diff --git a/src/common/errorTracking.ts b/src/common/errorTracking.ts new file mode 100644 index 00000000..2967c44a --- /dev/null +++ b/src/common/errorTracking.ts @@ -0,0 +1,189 @@ +export const ERROR_TRACKING_DSN = 'https://f8abbc9ce46c42989b72758349a3a245@ru-node-1.pulsesync.dev/events/1' +export const ERROR_TRACKING_ENABLED = import.meta.env.PROD +export const ERROR_TRACKING_ENVIRONMENT = import.meta.env.PROD ? 'production' : 'development' +export const ERROR_TRACKING_RELEASE = `pulsesync-client@${PULSESYNC_VERSION}` +export const ERROR_TRACKING_DIST = PULSESYNC_DIST + +export const ERROR_TRACKING_BUILD_TAGS = { + branch: PULSESYNC_BRANCH || 'unknown', + dist: ERROR_TRACKING_DIST || 'unknown', +} + +type ErrorTrackingEvent = { + message?: string + tags?: Record + user?: unknown + request?: unknown + breadcrumbs?: unknown + extra?: unknown + contexts?: Record + transaction?: string + debug_meta?: { + images?: Array<{ + type?: string + code_file?: string + debug_id?: string + }> + } + logentry?: { + message?: string + formatted?: string + } + exception?: { + values?: Array<{ + value?: string + stacktrace?: { + frames?: Array<{ + filename?: string + abs_path?: string + }> + } + }> + } +} + +const getContextString = (context: unknown, key: string): string | undefined => { + if (!context || typeof context !== 'object') return undefined + const value = (context as Record)[key] + return typeof value === 'string' && value ? value : undefined +} + +export const addErrorTrackingRuntimeTags = (event: T): T => { + const contexts = event.contexts + if (!contexts) return event + + const runtimeName = getContextString(contexts.runtime, 'name') + const runtimeVersion = getContextString(contexts.runtime, 'version') + const chromeVersion = getContextString(contexts.chrome, 'version') + const nodeVersion = getContextString(contexts.node, 'version') + const appVersion = getContextString(contexts.app, 'app_version') + const runtimeTags: Record = {} + + if (chromeVersion && contexts.browser && typeof contexts.browser === 'object') { + contexts.browser = { ...(contexts.browser as Record), version: chromeVersion } + } + + if (runtimeName?.toLowerCase() === 'electron' && runtimeVersion) runtimeTags['electron.version'] = runtimeVersion + if (chromeVersion) runtimeTags['chrome.version'] = chromeVersion + if (nodeVersion) runtimeTags['node.version'] = nodeVersion + if (appVersion) runtimeTags['app.version'] = appVersion + + if (Object.keys(runtimeTags).length > 0) event.tags = { ...event.tags, ...runtimeTags } + return event +} + +const normalizeBundlePath = (value: string): string => + value + .replace(/\\/g, '/') + .replace(/^file:\/\//u, '') + .toLowerCase() + +const comparableBundlePath = (value: string): string => { + const normalized = normalizeBundlePath(value) + const viteIndex = normalized.indexOf('/.vite/') + return viteIndex >= 0 ? normalized.slice(viteIndex) : normalized +} + +const canonicalBundlePath = (value: string): string => { + const comparablePath = comparableBundlePath(value) + return comparablePath.startsWith('/.vite/') ? `app://${comparablePath}` : value +} + +const extractBundlePathFromStackLine = (line: string): string | undefined => { + const location = line.match(/\((.+):\d+:\d+\)$/u)?.[1] ?? line.match(/\bat\s+(.+):\d+:\d+$/u)?.[1] + return location && /\.(?:cjs|mjs|js)$/u.test(location) ? location : undefined +} + +export const addErrorTrackingDebugIds = (event: T): T => { + const debugIds = (globalThis as typeof globalThis & { _sentryDebugIds?: Record })._sentryDebugIds + if (!debugIds) { + if (event.debug_meta?.images?.some(image => image.type === 'sourcemap')) { + event.debug_meta.images = event.debug_meta.images.map(image => + image.type === 'sourcemap' && image.code_file ? { ...image, code_file: canonicalBundlePath(image.code_file) } : image, + ) + } + return event + } + const debugIdValues = [...new Set(Object.values(debugIds).filter(Boolean))] + const singleDebugId = debugIdValues.length === 1 ? debugIdValues[0] : undefined + + const mappings = Object.entries(debugIds).flatMap(([stack, debugId]) => { + const codeFile = stack + .split('\n') + .slice(1) + .map(extractBundlePathFromStackLine) + .find((value): value is string => Boolean(value)) + return codeFile ? [{ codeFile: canonicalBundlePath(codeFile), comparablePath: comparableBundlePath(codeFile), debugId }] : [] + }) + if (event.debug_meta?.images?.some(image => image.type === 'sourcemap')) { + event.debug_meta.images = event.debug_meta.images.map(image => + image.type === 'sourcemap' && image.code_file ? { ...image, code_file: canonicalBundlePath(image.code_file) } : image, + ) + } + + const images = event.debug_meta?.images ?? [] + for (const exception of event.exception?.values ?? []) { + for (const frame of exception.stacktrace?.frames ?? []) { + const codeFile = frame.abs_path ?? frame.filename + if (!codeFile) continue + + const comparablePath = comparableBundlePath(codeFile) + const mapping = mappings.find(candidate => candidate.comparablePath === comparablePath) + const image = + mapping ?? + (singleDebugId && comparablePath.startsWith('/.vite/') + ? { codeFile: canonicalBundlePath(codeFile), debugId: singleDebugId } + : undefined) + if (!image || images.some(existingImage => existingImage.code_file === image.codeFile && existingImage.debug_id === image.debugId)) + continue + + images.push({ + type: 'sourcemap', + code_file: image.codeFile, + debug_id: image.debugId, + }) + } + } + + if (images.length > 0) { + event.debug_meta = { ...event.debug_meta, images } + } + return event +} + +const stripUrlDetails = (value: string): string => + value.replace(/https?:\/\/[^\s)\]}]+/g, rawUrl => { + try { + const url = new URL(rawUrl) + url.search = '' + url.hash = '' + return url.toString() + } catch { + return rawUrl + } + }) + +const redactSensitiveText = (value: string): string => + stripUrlDetails(value) + .replace(/\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi, 'Bearer [Filtered]') + .replace(/\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, '[Filtered JWT]') + .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[Filtered Email]') + .replace(/\b(authorization|password|secret|token|api[_-]?key)\s*[:=]\s*[^\s,;]+/gi, '$1=[Filtered]') + +const redactUserPath = (value: string): string => + value.replace(/([A-Z]:\\Users\\)[^\\]+/gi, '$1[Filtered]').replace(/(\/(?:home|Users)\/)[^/]+/g, '$1[Filtered]') + +export const sanitizeErrorTrackingEvent = (event: T): T => { + if (event.message) event.message = redactSensitiveText(event.message) + if (event.logentry?.message) event.logentry.message = redactSensitiveText(event.logentry.message) + if (event.logentry?.formatted) event.logentry.formatted = redactSensitiveText(event.logentry.formatted) + for (const exception of event.exception?.values ?? []) { + if (exception.value) exception.value = redactSensitiveText(exception.value) + for (const frame of exception.stacktrace?.frames ?? []) { + if (frame.filename) frame.filename = redactUserPath(frame.filename) + if (frame.abs_path) frame.abs_path = redactUserPath(frame.abs_path) + } + } + + return event +} diff --git a/src/common/http/createHttpClient.ts b/src/common/http/createHttpClient.ts index e99f72ea..a6e5a923 100644 --- a/src/common/http/createHttpClient.ts +++ b/src/common/http/createHttpClient.ts @@ -90,7 +90,13 @@ export function createHttpClient({ baseUrl, defaultHeaders, getAuthToken, transp let body: BodyInit | string | undefined - if (typeof options.body === 'string' || isFormData(options.body) || isBlob(options.body) || isArrayBuffer(options.body) || isUrlSearchParams(options.body)) { + if ( + typeof options.body === 'string' || + isFormData(options.body) || + isBlob(options.body) || + isArrayBuffer(options.body) || + isUrlSearchParams(options.body) + ) { body = options.body } else if (isJsonBody(options.body)) { body = JSON.stringify(options.body) diff --git a/src/common/types/clientBuildIdentity.ts b/src/common/types/clientBuildIdentity.ts new file mode 100644 index 00000000..83c53db6 --- /dev/null +++ b/src/common/types/clientBuildIdentity.ts @@ -0,0 +1,10 @@ +export type ClientBuildSignatureAlgorithm = 'ed25519' + +export interface ClientBuildIdentity { + origin: string + version: string + commit: string + builtAt: string + signatureAlgorithm: ClientBuildSignatureAlgorithm + signature: string +} diff --git a/src/common/types/clientHardwareIdentity.ts b/src/common/types/clientHardwareIdentity.ts new file mode 100644 index 00000000..cdb5b27f --- /dev/null +++ b/src/common/types/clientHardwareIdentity.ts @@ -0,0 +1,7 @@ +export type ClientHardwareIdentityAlgorithm = 'sha256' + +export interface ClientHardwareIdentity { + hash: string + source: string + algorithm: ClientHardwareIdentityAlgorithm +} diff --git a/src/common/types/mainEvents.ts b/src/common/types/mainEvents.ts index 36709b21..1116c4bc 100644 --- a/src/common/types/mainEvents.ts +++ b/src/common/types/mainEvents.ts @@ -96,6 +96,7 @@ const Events = { CHECK_SLEEP_MODE: 'CHECK_SLEEP_MODE', GET_ADDONS: 'GET_ADDONS', + SET_ADDON_ENABLED: 'SET_ADDON_ENABLED', CREATE_NEW_EXTENSION: 'CREATE_NEW_EXTENSION', EXPORT_ADDON: 'EXPORT_ADDON', PACKAGE_ADDON_ARCHIVE: 'PACKAGE_ADDON_ARCHIVE', @@ -110,6 +111,7 @@ const Events = { GET_VERSION: 'GET_VERSION', GET_LAST_BRANCH: 'GET_LAST_BRANCH', GET_BUILD_CHANNEL: 'GET_BUILD_CHANNEL', + GET_CLIENT_HARDWARE_IDENTITY: 'GET_CLIENT_HARDWARE_IDENTITY', GET_EFFECTIVE_UPDATE_CHANNEL: 'GET_EFFECTIVE_UPDATE_CHANNEL', GET_UPDATE_CHANNEL_OVERRIDE: 'GET_UPDATE_CHANNEL_OVERRIDE', GET_UPDATE_SOURCE: 'GET_UPDATE_SOURCE', diff --git a/src/index.ts b/src/index.ts index 617f3751..d2fa24e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import { consumePendingInstallModUpdateFromPath, isFirstInstance, } from './main/modules/singleInstance' -import { sendAddonSettings, sendAllAddonSettings, setAddon } from './main/modules/httpServer' +import { sendAddonSettings, sendAllAddonSettings, sendExtensions, setAddon } from './main/modules/httpServer' import { checkAsar, findAppByName, getPathToYandexMusic, isLinux, isMac, isWindows } from './main/utils/appUtils' import logger from './main/modules/logger' import isAppDev from './main/utils/isAppDev' @@ -18,7 +18,7 @@ import { HandleErrorsElectron } from './main/modules/handlers/handleErrorsElectr import { checkCLIArguments } from './main/utils/processUtils' import { registerSchemes } from './main/utils/serverUtils' -import { createDefaultAddonIfNotExists } from './main/utils/addonUtils' +import { createDefaultAddonIfNotExists, loadAddons } from './main/utils/addonUtils' import { migrateLegacyAddonSettings } from './main/utils/addonSettingsMigration' import { createWindow, mainWindow } from './main/modules/createWindow' import { handleEvents } from './main/events' @@ -35,6 +35,9 @@ import { processBrowserAuth } from './main/modules/auth/browserAuth' import { runWhenUiReady } from './main/modules/uiReady' import { sendAppStartupTelemetry } from './main/modules/telemetry/appTelemetry' import { enableSystemProxySupport } from './main/modules/network/systemProxy' +import { initMainErrorTracking } from './main/modules/errorTracking' +import { handleUncaughtException } from './main/modules/handlers/handleError' +import { getAddonsRoot, resolveExistingDirectoryInsideBase } from './main/utils/addonPaths' export let updated = false export let musicPath: string @@ -42,6 +45,8 @@ export let asarFilename = 'app.backup.asar' export let asarBackup: string export let selectedAddon: string +initMainErrorTracking() +handleUncaughtException() registerSchemes() const State = getState() initMainI18n(State.get('settings.language')) @@ -259,6 +264,26 @@ const mimeFromExt = (p: string) => { const handleSettingsFilenames = new Set([HANDLE_EVENTS_FILENAME.toLowerCase(), HANDLE_EVENTS_SETTINGS_FILENAME.toLowerCase()]) +const readStoredAddonScripts = (): string[] => { + const scripts = State.get('addons.scripts') + + if (typeof scripts === 'string') { + return scripts + .split(',') + .map(script => script.trim()) + .filter(Boolean) + } + + return Array.isArray(scripts) ? scripts.map(script => String(script || '').trim()).filter(Boolean) : [] +} + +const syncAddonClients = async (): Promise => { + selectedAddon = State.get('addons.theme') || 'Default' + setAddon(selectedAddon) + await sendExtensions() + sendAllAddonSettings({ force: true }) +} + const emitAddonSettingsWriteIfNeeded = (writtenPath: string): void => { if (!writtenPath) return @@ -267,7 +292,7 @@ const emitAddonSettingsWriteIfNeeded = (writtenPath: string): void => { return } - const addonsRoot = path.join(app.getPath('appData'), 'PulseSync', 'addons') + const addonsRoot = getAddonsRoot() const relativePath = path.relative(addonsRoot, normalizedPath) const isOutsideAddonsRoot = relativePath.startsWith('..') || path.isAbsolute(relativePath) if (isOutsideAddonsRoot) { @@ -433,19 +458,84 @@ ipcMain.handle(MainEvents.FILE_EVENT, async (_event, eventType, filePath, data) } }) -ipcMain.handle(MainEvents.DELETE_ADDON_DIRECTORY, async (_event, themeDirectoryPath) => { +ipcMain.handle(MainEvents.DELETE_ADDON_DIRECTORY, async (_event, themeDirectoryPath: string) => { try { - if (fs.existsSync(themeDirectoryPath)) { - await fsp.rm(themeDirectoryPath, { - recursive: true, - force: true, - }) - return { success: true } - } else { - logger.main.error('Директория темы не найдена.') + const addonsRoot = getAddonsRoot() + const addonDirectoryPath = resolveExistingDirectoryInsideBase(addonsRoot, String(themeDirectoryPath || '')) + if (!addonDirectoryPath) { + return { success: false, reason: 'INVALID_ADDON_PATH' } + } + + const addonDirectoryName = path.basename(addonDirectoryPath) + if (addonDirectoryName === 'Default') { + return { success: false, reason: 'DEFAULT_ADDON_DELETE_BLOCKED' } + } + + if (State.get('addons.theme') === addonDirectoryName) { + State.set('addons.theme', 'Default') + } + + const nextScripts = readStoredAddonScripts().filter(script => script !== addonDirectoryName) + State.set('addons.scripts', nextScripts) + + await fsp.rm(addonDirectoryPath, { + recursive: true, + force: true, + }) + + const addons = await loadAddons() + await syncAddonClients() + + return { + success: true, + addons, + scripts: State.get('addons.scripts') || [], + theme: State.get('addons.theme') || 'Default', } } catch (error) { logger.main.error('Ошибка при удалении директории темы:', error) + return { success: false, reason: error instanceof Error ? error.message : 'DELETE_FAILED' } + } +}) + +ipcMain.handle(MainEvents.SET_ADDON_ENABLED, async (_event, payload: { directoryName?: string; enabled?: boolean }) => { + try { + const directoryName = String(payload?.directoryName || '').trim() + if (!directoryName) { + return { success: false, reason: 'ADDON_DIRECTORY_REQUIRED' } + } + + const addons = await loadAddons() + const addon = addons.find(item => item.directoryName === directoryName) + if (!addon) { + return { success: false, reason: 'ADDON_NOT_FOUND' } + } + + const enabled = Boolean(payload?.enabled) + if (addon.type === 'theme') { + State.set('addons.theme', enabled ? addon.directoryName : 'Default') + } else { + const scripts = new Set(readStoredAddonScripts()) + if (enabled) { + scripts.add(addon.directoryName) + } else { + scripts.delete(addon.directoryName) + } + State.set('addons.scripts', Array.from(scripts)) + } + + const nextAddons = await loadAddons() + await syncAddonClients() + + return { + success: true, + addons: nextAddons, + scripts: State.get('addons.scripts') || [], + theme: State.get('addons.theme') || 'Default', + } + } catch (error) { + logger.main.error('Ошибка при изменении состояния аддона:', error) + return { success: false, reason: error instanceof Error ? error.message : 'SET_ADDON_ENABLED_FAILED' } } }) @@ -455,7 +545,7 @@ ipcMain.on(MainEvents.THEME_CHANGED, async (_event, addon: Addon) => { logger.main.error('Addons: No addon data received') return } - const addonsFolder = path.join(app.getPath('appData'), 'PulseSync', 'addons') + const addonsFolder = getAddonsRoot() const addonFolder = path.join(addonsFolder, addon.directoryName) const metadataPath = path.join(addonFolder, 'metadata.json') @@ -511,7 +601,7 @@ export async function prestartCheck() { checkAsar() initializeAddon() - const themesPath = path.join(app.getPath('appData'), 'PulseSync', 'addons') + const themesPath = getAddonsRoot() createDefaultAddonIfNotExists(themesPath) await migrateLegacyAddonSettings(themesPath) try { diff --git a/src/locales/en/renderer.json b/src/locales/en/renderer.json index 31e58656..11bc4600 100644 --- a/src/locales/en/renderer.json +++ b/src/locales/en/renderer.json @@ -74,6 +74,7 @@ "deprecatedRequiresUpdate": "The app is outdated and requires an update 😡😡😡", "deprecatedRequiresUpdateShort": "The app is outdated and requires an update", "deprecatedSoon": "Your app version is outdated 🤠 and will stop working soon. Please update the app.", + "hardwareIdentityWarning": "Could not verify this client's HWID. Some features may be limited.", "discordAuth": "Authorize", "failedToFetchUser": "Failed to fetch user data. Please log in again.", "images": { @@ -561,15 +562,26 @@ "localizationApprovedBody": "Your changes passed review and are already published.", "localizationRejectedTitle": "Localization suggestion rejected", "localizationRejectedBody": "Open the notification to review the feedback.", + "giveawayStartedTitle": "A new giveaway has started", + "giveawayStartedBody": "The \"{{giveaway}}\" giveaway is live. You can enter until {{date}}.", "giveawayWonTitle": "You won a giveaway", "giveawayWonBody": "You won \"{{prize}}\" in \"{{giveaway}}\".", "giveawayWonPrize": "{{name}} for {{months}} mo", "giveawayFallbackTitle": "Subscription giveaway", "giveawayFallbackPrize": "PulseSync subscription", + "subscriptionPurchaseSucceededTitle": "Subscription purchased", + "subscriptionPurchaseSucceededBody": "{{plan}} is active until {{date}}. Thanks for your support.", + "subscriptionExpiringSoonTitle": "Subscription ends soon", + "subscriptionExpiringSoonBody": "{{plan}} ends on {{date}}. You can extend it early.", + "subscriptionFallbackPlan": "PulseSync subscription", + "subscriptionFallbackDate": "soon", "genericTitle": "New notification", "genericBody": "Open the notification to view details." } }, + "subscription": { + "open": "Buy subscription" + }, "giveaways": { "open": "Open subscription giveaways", "eyebrow": "Subscriptions", @@ -781,6 +793,10 @@ "installAction": "Install", "installErrorTitle": "Install error", "installUpdateTooltip": "Install update", + "authRequired": { + "description": "This section is available only after you sign in. Sign in first, then open it again.", + "title": "Sign in required" + }, "installedVersionOutdated": "Installed version ({{version}}) is outdated", "modInstallAlreadyRunning": "Installation already started", "modInstallErrorTitle": "Mod install error", diff --git a/src/locales/ru/renderer.json b/src/locales/ru/renderer.json index c4d5d0d3..d5c902ab 100644 --- a/src/locales/ru/renderer.json +++ b/src/locales/ru/renderer.json @@ -74,6 +74,7 @@ "deprecatedRequiresUpdate": "Приложение устарело и требует обновления 😡😡😡", "deprecatedRequiresUpdateShort": "Приложение устарело и требует обновления", "deprecatedSoon": "Ваша версия приложения устарела 🤠 и скоро прекратит работу. Пожалуйста, обновите приложение.", + "hardwareIdentityWarning": "Не удалось подтвердить HWID клиента. Некоторые функции могут быть ограничены.", "discordAuth": "Авторизация", "failedToFetchUser": "Не удалось получить данные пользователя. Пожалуйста, войдите снова.", "images": { @@ -561,15 +562,26 @@ "localizationApprovedBody": "Изменения прошли проверку и уже опубликованы.", "localizationRejectedTitle": "Предложение по локализации отклонено", "localizationRejectedBody": "Открой уведомление, чтобы посмотреть детали ревью.", + "giveawayStartedTitle": "Начался новый розыгрыш", + "giveawayStartedBody": "Розыгрыш «{{giveaway}}» уже идёт. Участвовать можно до {{date}}.", "giveawayWonTitle": "Вы выиграли розыгрыш", "giveawayWonBody": "Вы выиграли «{{prize}}» в розыгрыше «{{giveaway}}».", "giveawayWonPrize": "{{name}} на {{months}} мес.", "giveawayFallbackTitle": "Розыгрыш подписки", "giveawayFallbackPrize": "подписку PulseSync", + "subscriptionPurchaseSucceededTitle": "Подписка оформлена", + "subscriptionPurchaseSucceededBody": "{{plan}} активна до {{date}}. Спасибо за поддержку.", + "subscriptionExpiringSoonTitle": "Подписка скоро закончится", + "subscriptionExpiringSoonBody": "{{plan}} заканчивается {{date}}. Можно продлить её заранее.", + "subscriptionFallbackPlan": "Подписка PulseSync", + "subscriptionFallbackDate": "скоро", "genericTitle": "Новое уведомление", "genericBody": "Открой уведомление, чтобы посмотреть подробности." } }, + "subscription": { + "open": "Купить подписку" + }, "giveaways": { "open": "Открыть розыгрыши подписок", "eyebrow": "Подписки", @@ -781,6 +793,10 @@ "installAction": "Установить", "installErrorTitle": "Ошибка установки", "installUpdateTooltip": "Установить обновление", + "authRequired": { + "description": "Этот раздел доступен только после входа в учётную запись. Сначала войди, потом открой его снова.", + "title": "Нужен вход в аккаунт" + }, "installedVersionOutdated": "Установленная версия ({{version}}) устарела", "modInstallAlreadyRunning": "Установка уже запущена", "modInstallErrorTitle": "Ошибка установки мода", @@ -810,7 +826,7 @@ "nav": { "addonsBeta": "Аддоны", "development": "Development", - "extensionsStore": "Каталог расширений", + "extensionsStore": "Каталог аддонов", "home": "Главная", "trackInfo": "Информация о треке", "users": "Пользователи", @@ -987,7 +1003,7 @@ "volumeControl": "Контроль громкости" }, "headerSubtitle": "Обновляйте интерфейс, открывайте новые возможности", - "headerTitle": "Каталог расширений", + "headerTitle": "Каталог аддонов", "title": "Store" } }, diff --git a/src/main/events/index.ts b/src/main/events/index.ts index be82ddbf..89decea4 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -38,6 +38,7 @@ import { isUiReady, markUiReady } from '../modules/uiReady' import MainEvents from '../../common/types/mainEvents' import RendererEvents from '../../common/types/rendererEvents' import type { SubcomponentsMeta } from '../../common/types/subcomponentsMeta' +import { nativeGetHardwareIdentity } from '../modules/nativeModules' import { obsWidgetManager } from '../modules/obsWidget/obsWidgetManager' import { YM_SETUP_DOWNLOAD_URLS } from '../constants/urls' import { t } from '../i18n' @@ -52,7 +53,13 @@ import { } from '../modules/updater/updateChannel' import { getUpdateSource, setUpdateSource } from '../modules/updater/updateSource' import { getModReleasesForSource } from '../modules/mod/network/releaseCatalog' -import { CLIENT_REPO, listStableGitHubReleases, normalizeGitHubTagVersion, resolveClientGitHubMacManifest } from '../modules/updater/githubReleaseResolver' +import { setMainErrorTrackingUser } from '../modules/errorTracking' +import { + CLIENT_REPO, + listStableGitHubReleases, + normalizeGitHubTagVersion, + resolveClientGitHubMacManifest, +} from '../modules/updater/githubReleaseResolver' import { getFfmpegMeta, getYtDlpMeta } from '../modules/submodulesChecker' import { beginBrowserAuthFlow, cancelBrowserAuthFlow } from '../modules/auth/browserAuth' @@ -108,7 +115,7 @@ const syncMacUpdaterFeed = () => { macUpdater.setAllowDowngrade(shouldAllowDowngradeForCurrentChannel()) } -const getCurrentUpdateStatus = () => (isMac() ? macUpdater?.getStatus() ?? UpdateStatus.IDLE : updater.getStatus()) +const getCurrentUpdateStatus = () => (isMac() ? (macUpdater?.getStatus() ?? UpdateStatus.IDLE) : updater.getStatus()) const ensureUpdateSourceSwitchAllowed = () => { const status = getCurrentUpdateStatus() @@ -367,6 +374,9 @@ const registerSystemEvents = (window: BrowserWindow): void => { ipcMain.on(MainEvents.GET_LAST_BRANCH, event => { event.returnValue = process.env.BRANCH }) + ipcMain.on(MainEvents.GET_CLIENT_HARDWARE_IDENTITY, event => { + event.returnValue = nativeGetHardwareIdentity() + }) ipcMain.handle(MainEvents.GET_BUILD_CHANNEL, async () => getBuildUpdateChannel()) ipcMain.handle(MainEvents.GET_EFFECTIVE_UPDATE_CHANNEL, async () => getEffectiveUpdateChannel()) ipcMain.handle(MainEvents.GET_UPDATE_CHANNEL_OVERRIDE, async () => getUpdateChannelOverride()) @@ -808,6 +818,7 @@ const registerLoggingEvents = (window: BrowserWindow): void => { ipcMain.on(MainEvents.AUTH_STATUS, (_event, data: any) => { authorized = data.status + setMainErrorTrackingUser(data.status ? data.user : null) tryOpenPendingAddon() }) ipcMain.handle(MainEvents.START_BROWSER_AUTH, async () => { diff --git a/src/main/http/client.ts b/src/main/http/client.ts index 1886b524..9b2c6ddf 100644 --- a/src/main/http/client.ts +++ b/src/main/http/client.ts @@ -29,8 +29,7 @@ async function parseResponseBody(response: Response, responseType: Ht async function fetchTransport(request: PreparedHttpRequest): Promise> { const controller = new AbortController() - const timeoutId = - typeof request.timeoutMs === 'number' && request.timeoutMs > 0 ? setTimeout(() => controller.abort(), request.timeoutMs) : null + const timeoutId = typeof request.timeoutMs === 'number' && request.timeoutMs > 0 ? setTimeout(() => controller.abort(), request.timeoutMs) : null try { const response = await fetch(request.url, { diff --git a/src/main/mainWindowPreload.ts b/src/main/mainWindowPreload.ts index 80caa5b5..6dad4f9d 100644 --- a/src/main/mainWindowPreload.ts +++ b/src/main/mainWindowPreload.ts @@ -1,6 +1,19 @@ +import '@sentry/electron/preload' + import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron' import packageJson from '../../package.json' import MainEvents from '../common/types/mainEvents' +import type { ClientBuildIdentity } from '@common/types/clientBuildIdentity' +import type { ClientHardwareIdentity } from '@common/types/clientHardwareIdentity' + +const buildPackageJson = packageJson as typeof packageJson & { + buildInfo?: { + VERSION?: string + BRANCH?: string + BUILD_TIME?: string + SIGNATURE?: string + } +} export interface DesktopEvents { emit(channel: string, ...args: any[]): void @@ -39,7 +52,7 @@ contextBridge.exposeInMainWorld('electron', { }, isMaximized() { return ipcRenderer.invoke(MainEvents.ELECTRON_WINDOW_IS_MAXIMIZED) - } + }, }, isAppDev() { return ipcRenderer.sendSync(MainEvents.ELECTRON_ISDEV) @@ -53,7 +66,22 @@ contextBridge.exposeInMainWorld('electron', { }) contextBridge.exposeInMainWorld('appInfo', { getBranch: () => ipcRenderer.sendSync(MainEvents.GET_LAST_BRANCH), - getVersion: () => packageJson.version, + getVersion: () => buildPackageJson.version, + getHardwareIdentity: (): ClientHardwareIdentity | null => { + try { + return ipcRenderer.sendSync(MainEvents.GET_CLIENT_HARDWARE_IDENTITY) ?? null + } catch { + return null + } + }, + getBuildIdentity: (): ClientBuildIdentity => ({ + origin: 'PulseSync-LLC/PulseSync-client', + version: buildPackageJson.buildInfo?.VERSION || buildPackageJson.version, + commit: buildPackageJson.buildInfo?.BRANCH || 'unknown', + builtAt: buildPackageJson.buildInfo?.BUILD_TIME || '', + signatureAlgorithm: 'ed25519', + signature: buildPackageJson.buildInfo?.SIGNATURE || '', + }), }) const desktopEvents: DesktopEvents = { emit: (channel, ...args) => { diff --git a/src/main/modules/createWindow.ts b/src/main/modules/createWindow.ts index 1609d3cd..2aa62deb 100644 --- a/src/main/modules/createWindow.ts +++ b/src/main/modules/createWindow.ts @@ -153,7 +153,8 @@ export async function createWindow(): Promise { ...(position ? { x: position.x, y: position.y } : { center: true }), minWidth: minMain.width, minHeight: minMain.height, - trafficLightPosition: { x: 16, y: 10 }, + titleBarStyle: 'hidden', + trafficLightPosition: { x: 15, y: 20 }, icon, webPreferences: { preload: path.join(__dirname, 'mainWindowPreload.cjs'), @@ -255,10 +256,6 @@ export async function createWindow(): Promise { State.set('settings.lastDisplayId', disp.id) }) - if (isAppDev) { - Object.defineProperty(app, 'isPackaged', { get: () => true }) - } - powerMonitor.on('suspend', () => { inSleepMode = true }) diff --git a/src/main/modules/errorTracking.ts b/src/main/modules/errorTracking.ts new file mode 100644 index 00000000..8a511cc3 --- /dev/null +++ b/src/main/modules/errorTracking.ts @@ -0,0 +1,106 @@ +import * as Sentry from '@sentry/electron/main' + +import { + addErrorTrackingDebugIds, + addErrorTrackingRuntimeTags, + ERROR_TRACKING_BUILD_TAGS, + ERROR_TRACKING_DIST, + ERROR_TRACKING_DSN, + ERROR_TRACKING_ENABLED, + ERROR_TRACKING_ENVIRONMENT, + ERROR_TRACKING_RELEASE, + sanitizeErrorTrackingEvent, +} from '@common/errorTracking' +import logger from './logger' + +let initialized = false + +export const initMainErrorTracking = (): void => { + if (!ERROR_TRACKING_ENABLED || initialized) return + + try { + Sentry.init({ + dsn: ERROR_TRACKING_DSN, + release: ERROR_TRACKING_RELEASE, + dist: ERROR_TRACKING_DIST, + environment: ERROR_TRACKING_ENVIRONMENT, + dataCollection: { + userInfo: false, + }, + maxBreadcrumbs: 0, + tracesSampleRate: 0, + attachScreenshot: false, + includeLocalVariables: false, + integrations: defaults => + defaults.filter( + integration => + !['OnUncaughtException', 'OnUnhandledRejection', 'PreloadInjection', 'MainProcessSession'].includes(integration.name), + ), + beforeSend: event => { + event.platform = 'javascript' + return addErrorTrackingDebugIds(addErrorTrackingRuntimeTags(sanitizeErrorTrackingEvent(event))) + }, + }) + Sentry.setTags({ + ...ERROR_TRACKING_BUILD_TAGS, + process: 'main', + platform: process.platform, + architecture: process.arch, + }) + initialized = true + } catch (error) { + logger.main.warn('Failed to initialize error tracking:', error) + } +} + +export const setMainErrorTrackingUser = (user?: { id?: string | null; email?: string | null } | null): void => { + if (!initialized) return + const id = user?.id?.trim() + if (!id || id === '-1') { + Sentry.setUser(null) + return + } + + const email = user?.email?.trim() + Sentry.setUser({ + id, + ...(email ? { email } : {}), + }) +} + +export const captureMainException = (error: unknown, source: string): void => { + if (!initialized) return + try { + Sentry.withScope(scope => { + scope.setTag('source', source) + Sentry.captureException(error instanceof Error ? error : new Error(String(error))) + }) + } catch (captureError) { + logger.main.warn('Failed to capture error:', captureError) + } +} + +export const captureRendererTermination = (details: Electron.RenderProcessGoneDetails): void => { + if (!initialized) return + try { + Sentry.withScope(scope => { + scope.setTags({ + source: 'render_process_gone', + reason: details.reason, + exitCode: String(details.exitCode), + }) + Sentry.captureMessage('Electron renderer process terminated', 'error') + }) + } catch (captureError) { + logger.main.warn('Failed to capture renderer termination:', captureError) + } +} + +export const flushErrorTracking = async (timeout = 1500): Promise => { + if (!initialized) return + try { + await Sentry.flush(timeout) + } catch (error) { + logger.main.warn('Failed to flush error tracking:', error) + } +} diff --git a/src/main/modules/handlers/handleError.ts b/src/main/modules/handlers/handleError.ts index 7b791445..49b72296 100644 --- a/src/main/modules/handlers/handleError.ts +++ b/src/main/modules/handlers/handleError.ts @@ -1,6 +1,7 @@ import logger from '../logger' import { app } from 'electron' import { HandleErrorsElectron } from './handleErrorsElectron' +import { captureRendererTermination, flushErrorTracking } from '../errorTracking' const firstLine = (message: string | Error) => { if (typeof message === 'string') { @@ -18,15 +19,24 @@ export const toPlainError = (error: Error | any) => { } export const handleUncaughtException = () => { - process.on('uncaughtException', (error: Error) => { + process.on('uncaughtException', async (error: Error) => { logger.main.error('Uncaught Exception:', toPlainError(error)) - HandleErrorsElectron.handleError('error_handler', error?.name, firstLine(error?.message), error) + HandleErrorsElectron.handleError('error_handler', 'uncaught_exception', error.name, error) + await flushErrorTracking() process.exit(1) }) + + process.on('unhandledRejection', reason => { + const error = reason instanceof Error ? reason : new Error(String(reason)) + logger.main.error('Unhandled Rejection:', toPlainError(error)) + HandleErrorsElectron.handleError('error_handler', 'unhandled_rejection', error.name, error) + }) + app.on('render-process-gone', (event, webContents, detailed) => { const REASON_CRASHED = 'crashed' const REASON_OOM = 'oom' - HandleErrorsElectron.handleError('error_handler', 'render_process_gone', 'render_process_gone', detailed) + captureRendererTermination(detailed) + HandleErrorsElectron.handleError('error_handler', 'render_process_gone', 'render_process_gone', detailed, { capture: false }) logger.renderer.error('Error in renderer: ' + detailed) if ([REASON_CRASHED, REASON_OOM].includes(detailed?.reason)) { if (detailed.reason === REASON_CRASHED) { diff --git a/src/main/modules/handlers/handleErrorsElectron.ts b/src/main/modules/handlers/handleErrorsElectron.ts index 605aec10..0ef9a27f 100644 --- a/src/main/modules/handlers/handleErrorsElectron.ts +++ b/src/main/modules/handlers/handleErrorsElectron.ts @@ -3,17 +3,27 @@ import path from 'path' import logger from '../logger' import { app } from 'electron' import { t } from '../../i18n' +import { captureMainException } from '../errorTracking' const CRASH_FILE = path.join(app.getPath('appData'), 'PulseSync', 'logs', 'crash_app.log') export class HandleErrorsElectron { - public static handleError(className: string, method: string, block: string, error: unknown): void { + public static handleError( + className: string, + method: string, + block: string, + error: unknown, + options: { capture: boolean } = { capture: true }, + ): void { try { const errorObj = error instanceof Error ? error : new Error(String(error)) const errorContext = `${className}/${method}/${block}:${errorObj.message}` const errorMessage = HandleErrorsElectron.formatLogMessage('ERROR', errorContext, errorObj.stack || errorObj.message) HandleErrorsElectron.storeCrash(errorMessage) + if (options.capture) { + captureMainException(errorObj, `${className}/${method}/${block}`) + } } catch (internalError) { logger.main.error(t('main.handleErrors.internalError'), internalError) } diff --git a/src/main/modules/httpServer/addonService.ts b/src/main/modules/httpServer/addonService.ts index 969bb9be..235174e3 100644 --- a/src/main/modules/httpServer/addonService.ts +++ b/src/main/modules/httpServer/addonService.ts @@ -1,13 +1,13 @@ import * as fs from 'original-fs' import * as path from 'path' -import { app } from 'electron' +import { createHash } from 'node:crypto' import MainEvents from '../../../common/types/mainEvents' import RendererEvents from '../../../common/types/rendererEvents' import { sanitizeScript } from '../../utils/addonUtils' import { Server as IOServer, Socket } from 'socket.io' -import { mainWindow } from '../createWindow' import { readAddonSettings } from './addonSettings' import { resolveAddonDirectory, resolveAddonDisplayName } from '../../utils/addonRegistry' +import { getAddonsRoot } from '../../utils/addonPaths' interface StateLike { get: (key: string) => any @@ -29,6 +29,14 @@ interface CreateAddonServiceOptions { interface DataToMusicOptions { targetSocket?: Socket + currentAddonStateHashVersion?: number + currentAddonStateHash?: string +} + +type ThemePayload = { + name: string + css: string + script: string } type RefreshedAddonPayload = { @@ -40,10 +48,78 @@ type RefreshedAddonPayload = { script: string | null } +type AddonStateSnapshot = { + theme: ThemePayload | null + extensions: RefreshedAddonPayload[] +} + +const canonicalizeAddonState = ({ theme, extensions }: AddonStateSnapshot) => ({ + theme: + !theme || theme.name.toLowerCase() === 'default' + ? { name: 'default', css: '', script: '' } + : { + name: theme.name, + css: theme.css, + script: theme.script, + }, + extensions: extensions + .map(extension => ({ + addon: String(extension.addon || ''), + name: String(extension.name || ''), + directoryName: String(extension.directoryName || ''), + id: String(extension.id || ''), + css: typeof extension.css === 'string' ? extension.css : '', + script: typeof extension.script === 'string' ? extension.script : '', + })) + .sort((left, right) => { + const leftKey = JSON.stringify(left) + const rightKey = JSON.stringify(right) + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0 + }), +}) + +const hashAddonState = (snapshot: AddonStateSnapshot): string => + createHash('sha256') + .update(JSON.stringify(canonicalizeAddonState(snapshot))) + .digest('hex') + export const createAddonService = ({ state, logger, getIo, getAuthorized, getSelectedAddon }: CreateAddonServiceOptions) => { const lastAddonSettings = new Map() const pendingDataSyncTimers = new Map>() + const readStoredAddonScripts = (): string[] => { + const scripts = state.get('addons.scripts') + if (typeof scripts === 'string') { + return scripts + .split(',') + .map((script: string) => script.trim()) + .filter(Boolean) + } + + return Array.isArray(scripts) ? scripts.map(script => String(script || '').trim()).filter(Boolean) : [] + } + + const getSelectedThemeDirectory = (): string => { + const stateTheme = state.get('addons.theme') + const selectedTheme = + typeof stateTheme === 'string' && stateTheme.trim() + ? stateTheme.trim() + : typeof getSelectedAddon() === 'string' && getSelectedAddon().trim() + ? getSelectedAddon().trim() + : 'Default' + + return resolveAddonDirectory(selectedTheme) || 'Default' + } + + const getSelectedScriptDirectories = (): string[] => + Array.from( + new Set( + readStoredAddonScripts() + .map(script => resolveAddonDirectory(script)) + .filter(Boolean), + ), + ) + const getMusicRecipients = (targetSocket?: Socket): Socket[] => { const io = getIo() if (!io) return [] @@ -56,7 +132,7 @@ export const createAddonService = ({ state, logger, getIo, getAuthorized, getSel } const getAllAllowedUrls = (): string[] => { - const addonsFolder = path.join(app.getPath('appData'), 'PulseSync', 'addons') + const addonsFolder = getAddonsRoot() const urls = new Set() let folders: string[] = [] @@ -66,14 +142,7 @@ export const createAddonService = ({ state, logger, getIo, getAuthorized, getSel return [] } - const stateTheme = state.get('addons.theme') - const selected = getSelectedAddon() - const themeFolder = - typeof stateTheme === 'string' && stateTheme.trim() - ? stateTheme.trim() - : typeof selected === 'string' && selected.trim() - ? selected.trim() - : 'Default' + const themeFolder = getSelectedThemeDirectory() const themeMetaPath = path.join(addonsFolder, themeFolder, 'metadata.json') if (fs.existsSync(themeMetaPath)) { @@ -89,14 +158,7 @@ export const createAddonService = ({ state, logger, getIo, getAuthorized, getSel } catch {} } - let scripts = state.get('addons.scripts') - if (typeof scripts === 'string') { - scripts = scripts - .split(',') - .map((s: string) => s.trim()) - .filter(Boolean) - } - if (!Array.isArray(scripts)) scripts = [] + const scripts = getSelectedScriptDirectories() for (const folder of folders) { if (!scripts.includes(folder)) continue @@ -120,41 +182,108 @@ export const createAddonService = ({ state, logger, getIo, getAuthorized, getSel const getEnabledAddonNames = (): string[] => { const enabled = new Set() - const stateTheme = state.get('addons.theme') - const selectedTheme = - typeof stateTheme === 'string' && stateTheme.trim() - ? stateTheme.trim() - : typeof getSelectedAddon() === 'string' && getSelectedAddon().trim() - ? getSelectedAddon().trim() - : 'Default' + enabled.add(getSelectedThemeDirectory()) + getSelectedScriptDirectories().forEach(scriptName => enabled.add(scriptName)) - enabled.add(selectedTheme) + return Array.from(enabled) + } - let scripts = state.get('addons.scripts') - if (typeof scripts === 'string') { - scripts = scripts - .split(',') - .map((s: string) => s.trim()) - .filter(Boolean) + const readThemePayload = (useDefault = false): ThemePayload | null => { + const themesPath = getAddonsRoot() + const themeFolder = useDefault ? 'Default' : getSelectedThemeDirectory() + const themePath = path.join(themesPath, themeFolder) + const metadataPath = path.join(themePath, 'metadata.json') + if (!fs.existsSync(metadataPath)) return null + + try { + const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')) + if ((!metadata.type || (metadata.type !== 'theme' && metadata.type !== 'script')) && metadata.name !== 'Default') { + return null + } + + const cssPath = path.join(themePath, metadata.css || '') + const scriptPath = metadata.script ? path.join(themePath, metadata.script) : null + const css = metadata.css && fs.existsSync(cssPath) && fs.statSync(cssPath).isFile() ? fs.readFileSync(cssPath, 'utf8') : '' + const script = + scriptPath && fs.existsSync(scriptPath) && fs.statSync(scriptPath).isFile() ? sanitizeScript(fs.readFileSync(scriptPath, 'utf8')) : '' + + return { + name: useDefault ? 'Default' : metadata.name || themeFolder, + css: css || '{}', + script, + } + } catch { + return null + } + } + + const readExtensionPayloads = (): RefreshedAddonPayload[] => { + const scripts = getSelectedScriptDirectories() + + const addonsFolder = getAddonsRoot() + let dirs: string[] = [] + try { + dirs = fs.readdirSync(addonsFolder) + } catch { + return [] } - if (Array.isArray(scripts)) { - scripts.forEach(scriptName => { - if (typeof scriptName === 'string' && scriptName.trim()) { - enabled.add(scriptName.trim()) + return dirs + .map(folderName => { + const metadataPath = path.join(addonsFolder, folderName, 'metadata.json') + if (!fs.existsSync(metadataPath)) return null + + try { + const meta = JSON.parse(fs.readFileSync(metadataPath, 'utf8')) + const metaName = typeof meta.name === 'string' ? meta.name.trim() : '' + const addonName = metaName || folderName + if (!scripts.includes(folderName) && !(metaName.length > 0 && scripts.includes(metaName))) return null + if ((!meta.type || (meta.type !== 'theme' && meta.type !== 'script')) && folderName !== 'Default') return null + + let css: string | null = null + if (meta.css) { + const cssFile = path.join(addonsFolder, folderName, meta.css) + if (fs.existsSync(cssFile)) css = fs.readFileSync(cssFile, 'utf8') + } + + let script: string | null = null + if (meta.script) { + const scriptFile = path.join(addonsFolder, folderName, meta.script) + if (fs.existsSync(scriptFile)) script = sanitizeScript(fs.readFileSync(scriptFile, 'utf8')) + } + + return { + addon: folderName, + name: addonName, + directoryName: folderName, + id: typeof meta.id === 'string' ? meta.id : undefined, + css, + script, + } + } catch { + return null } }) - } + .filter((addon): addon is RefreshedAddonPayload => addon !== null) + } - return Array.from(enabled) + const readAddonStateSnapshot = (): AddonStateSnapshot => ({ + theme: readThemePayload() || readThemePayload(true), + extensions: readExtensionPayloads(), + }) + + const emitAddonStateSnapshot = (socket: Socket, snapshot: AddonStateSnapshot): void => { + if (snapshot.theme) socket.emit('THEME', { theme: snapshot.theme }) + socket.emit(MainEvents.REFRESH_EXTENSIONS, { addons: snapshot.extensions }) + socket.emit('ALLOWED_URLS', { allowedUrls: getAllAllowedUrls() }) } const setAddon = (_theme: string) => { const io = getIo() if (!getAuthorized() || !io) return - const themesPath = path.join(app.getPath('appData'), 'PulseSync', 'addons') - const selected = getSelectedAddon() + const themesPath = getAddonsRoot() + const selected = getSelectedThemeDirectory() const themePath = path.join(themesPath, selected) const metadataPath = path.join(themePath, 'metadata.json') if (!fs.existsSync(metadataPath)) return @@ -197,32 +326,8 @@ export const createAddonService = ({ state, logger, getIo, getAuthorized, getSel const sendAddon = (withJs: boolean, themeDef?: boolean) => { const io = getIo() if (!io) return - - const themesPath = path.join(app.getPath('appData'), 'PulseSync', 'addons') - const themeFolder = themeDef ? 'Default' : state.get('addons.theme') || 'Default' - const themePath = path.join(themesPath, themeFolder) - const metadataPath = path.join(themePath, 'metadata.json') - if (!fs.existsSync(metadataPath)) return - - const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')) - let css = '' - let js = '' - - const cssPath = path.join(themePath, metadata.css || '') - if (metadata.css && fs.existsSync(cssPath) && fs.statSync(cssPath).isFile()) { - css = fs.readFileSync(cssPath, 'utf8') - } - const jsPath = metadata.script ? path.join(themePath, metadata.script) : null - if (jsPath && fs.existsSync(jsPath) && fs.statSync(jsPath).isFile()) { - js = fs.readFileSync(jsPath, 'utf8') - js = sanitizeScript(js) - } - - const themeData = { - name: themeDef ? 'Default' : metadata.name || state.get('addons.theme') || 'Default', - css: css || '{}', - script: js || '', - } + const themeData = readThemePayload(Boolean(themeDef)) + if (!themeData) return io.sockets.sockets.forEach(sock => { const s = sock as any @@ -242,74 +347,7 @@ export const createAddonService = ({ state, logger, getIo, getAuthorized, getSel const sendExtensions = async (): Promise => { const io = getIo() if (!io) return - - let scripts = state.get('addons.scripts') - if (!scripts) return - - if (typeof scripts === 'string') { - scripts = scripts - .split(',') - .map((s: string) => s.trim()) - .filter(Boolean) - } else if (!Array.isArray(scripts)) { - scripts = [] - } - - const addonsFolder = path.join(app.getPath('appData'), 'PulseSync', 'addons') - let dirs: string[] = [] - try { - dirs = fs.readdirSync(addonsFolder) - } catch { - return - } - - const found = dirs - .map(folderName => { - const metadataPath = path.join(addonsFolder, folderName, 'metadata.json') - if (!fs.existsSync(metadataPath)) { - return null - } - try { - const meta = JSON.parse(fs.readFileSync(metadataPath, 'utf8')) - const metaName = typeof meta.name === 'string' ? meta.name.trim() : '' - const addonName = metaName || folderName - - const folderMatches = scripts.includes(folderName) - const nameMatches = metaName.length > 0 && scripts.includes(metaName) - if (!folderMatches && !nameMatches) return null - if ((!meta.type || (meta.type !== 'theme' && meta.type !== 'script')) && folderName !== 'Default') { - return null - } - - let css: string | null = null - if (meta.css) { - const cssFile = path.join(addonsFolder, folderName, meta.css) - if (fs.existsSync(cssFile)) css = fs.readFileSync(cssFile, 'utf8') - } - - let script: string | null = null - if (meta.script) { - const jsFile = path.join(addonsFolder, folderName, meta.script) - if (fs.existsSync(jsFile)) { - let content = fs.readFileSync(jsFile, 'utf8') - content = sanitizeScript(content) - script = content - } - } - - return { - addon: folderName, - name: addonName, - directoryName: folderName, - id: typeof meta.id === 'string' ? meta.id : undefined, - css, - script, - } - } catch { - return null - } - }) - .filter((x): x is RefreshedAddonPayload => x !== null) + const found = readExtensionPayloads() io.sockets.sockets.forEach(sock => { const s = sock as any @@ -376,22 +414,23 @@ export const createAddonService = ({ state, logger, getIo, getAuthorized, getSel } } - const sendDataToMusic = ({ targetSocket }: DataToMusicOptions = {}) => { + const sendDataToMusic = ({ targetSocket, currentAddonStateHashVersion, currentAddonStateHash }: DataToMusicOptions = {}) => { const io = getIo() if (!io) return const syncKey = targetSocket?.id || '__all__' - - const sendOnce = (sock: Socket) => { - const s = sock as any - if (s.clientType === 'yaMusic' && getAuthorized() && s.hasPong) { - sendAddon(true, true) - sock.emit(MainEvents.REFRESH_EXTENSIONS, { addons: [] }) - logger.http.log('Data sent after READY') - } + const snapshot = readAddonStateSnapshot() + const desiredAddonStateHash = hashAddonState(snapshot) + const stateMatches = + currentAddonStateHashVersion === 1 && + typeof currentAddonStateHash === 'string' && + currentAddonStateHash.length > 0 && + currentAddonStateHash === desiredAddonStateHash + + for (const socket of getMusicRecipients(targetSocket)) { + if (!stateMatches) emitAddonStateSnapshot(socket, snapshot) + else socket.emit('ALLOWED_URLS', { allowedUrls: getAllAllowedUrls() }) } - - if (targetSocket) sendOnce(targetSocket) - else io.sockets.sockets.forEach(sendOnce) + logger.http.log(stateMatches ? 'Addon state unchanged after READY' : 'Current addon state sent after READY') const existingTimer = pendingDataSyncTimers.get(syncKey) if (existingTimer) { @@ -400,13 +439,11 @@ export const createAddonService = ({ state, logger, getIo, getAuthorized, getSel const timer = setTimeout(async () => { pendingDataSyncTimers.delete(syncKey) - io.sockets.sockets.forEach(sock => { - const s = sock as any - if (s.clientType === 'yaMusic' && getAuthorized() && s.hasPong) { - sendAddon(true) + if (!stateMatches) { + for (const socket of getMusicRecipients(targetSocket)) { + emitAddonStateSnapshot(socket, snapshot) } - }) - await sendExtensions() + } sendAllAddonSettings({ targetSocket, force: true }) }, 1000) pendingDataSyncTimers.set(syncKey, timer) diff --git a/src/main/modules/httpServer/addonSettings.ts b/src/main/modules/httpServer/addonSettings.ts index a9479d01..0dd201cc 100644 --- a/src/main/modules/httpServer/addonSettings.ts +++ b/src/main/modules/httpServer/addonSettings.ts @@ -1,6 +1,5 @@ import * as fs from 'original-fs' import * as path from 'path' -import { app } from 'electron' import { type AddonSettingsValues, type HandleConfig, @@ -11,10 +10,10 @@ import { normalizeAddonSettingsValues, } from '@common/addons/handleEvents' import { resolveAddonDirectory, resolveAddonDisplayName } from '../../utils/addonRegistry' +import { getAddonsRoot } from '../../utils/addonPaths' export type AddonSettingsPayload = Record -const getAddonRoot = () => path.join(app.getPath('appData'), 'PulseSync', 'addons') const readStoredValue = (storedValues: AddonSettingsValues | undefined, keys: string[]): unknown => { if (!storedValues) { return undefined @@ -30,7 +29,7 @@ const readStoredValue = (storedValues: AddonSettingsValues | undefined, keys: st } const readAddonSettingsValuesFile = (directory: string): AddonSettingsValues => { - const valuesPath = path.join(getAddonRoot(), directory, HANDLE_EVENTS_SETTINGS_FILENAME) + const valuesPath = path.join(getAddonsRoot(), directory, HANDLE_EVENTS_SETTINGS_FILENAME) if (!fs.existsSync(valuesPath)) { return {} } @@ -98,7 +97,7 @@ export const readAddonSettings = (addonName: string): AddonSettingsPayload => { const directory = resolveAddonDirectory(addonName) if (!directory) return {} - const handlePath = path.join(getAddonRoot(), directory, HANDLE_EVENTS_FILENAME) + const handlePath = path.join(getAddonsRoot(), directory, HANDLE_EVENTS_FILENAME) if (!fs.existsSync(handlePath)) return {} try { @@ -110,7 +109,7 @@ export const readAddonSettings = (addonName: string): AddonSettingsPayload => { } export const readAllAddonSettings = (): Record => { - const addonsRoot = getAddonRoot() + const addonsRoot = getAddonsRoot() const result: Record = {} let folders: string[] = [] diff --git a/src/main/modules/httpServer/events/registerSocketClientEvents.ts b/src/main/modules/httpServer/events/registerSocketClientEvents.ts index aec0d641..4aecfa1f 100644 --- a/src/main/modules/httpServer/events/registerSocketClientEvents.ts +++ b/src/main/modules/httpServer/events/registerSocketClientEvents.ts @@ -24,7 +24,7 @@ interface RegisterSocketClientEventsOptions { mainWindow: BrowserWindow getAuthorized: () => boolean getTrackData: () => Track - sendDataToMusic: (options?: { targetSocket?: Socket }) => void + sendDataToMusic: (options?: { targetSocket?: Socket; currentAddonStateHashVersion?: number; currentAddonStateHash?: string }) => void updateData: (newData: any) => void handleBrowserAuth: (payload: any, client: Socket) => void } @@ -40,6 +40,11 @@ export const registerSocketClientEvents = ({ updateData, handleBrowserAuth, }: RegisterSocketClientEventsOptions) => { + const sendToRenderer = (channel: string, ...args: any[]) => { + if (mainWindow.isDestroyed() || mainWindow.webContents.isDestroyed()) return + mainWindow.webContents.send(channel, ...args) + } + const version = (socket.handshake.query.v as string) || state.get('mod.version') const clientType = (socket.handshake.query.type as string) || 'yaMusic' ;(socket as any).clientType = clientType @@ -48,14 +53,18 @@ export const registerSocketClientEvents = ({ logger.http.log(`New client connected: version=${version}, type=${clientType}`) socket.emit('PING', { message: 'Connected to server' }) - socket.on('READY', async () => { + socket.on('READY', async (payload?: { addonStateHashVersion?: number; addonStateHash?: string }) => { logger.http.log('READY received from client') if ((socket as any).clientType !== 'yaMusic') return - mainWindow.webContents.send(RendererEvents.CLIENT_READY) + sendToRenderer(RendererEvents.CLIENT_READY) ;(socket as any).hasPong = true if (getAuthorized()) { - sendDataToMusic({ targetSocket: socket }) + sendDataToMusic({ + targetSocket: socket, + currentAddonStateHashVersion: payload?.addonStateHashVersion, + currentAddonStateHash: typeof payload?.addonStateHash === 'string' ? payload.addonStateHash : undefined, + }) } }) @@ -64,7 +73,7 @@ export const registerSocketClientEvents = ({ if (!getAuthorized()) { logger.http.warn('Unauthorized IS_PREMIUM_USER request, ignoring.') } else { - mainWindow.webContents.send(RendererEvents.IS_PREMIUM_USER) + sendToRenderer(RendererEvents.IS_PREMIUM_USER) } }) @@ -75,11 +84,11 @@ export const registerSocketClientEvents = ({ socket.on('BROWSER_BAN', (args: any) => { logger.http.log('BROWSER_BAN received:', args) - mainWindow.webContents.send(RendererEvents.AUTH_BANNED, { reason: args.reason }) + sendToRenderer(RendererEvents.AUTH_BANNED, { reason: args.reason }) }) socket.on('UPDATE_DATA', (payload: any) => { - if (!getAuthorized()) return + if ((socket as any).clientType !== 'yaMusic') return logger.http.log('UPDATE_DATA received:', payload) updateData(payload) }) @@ -87,7 +96,7 @@ export const registerSocketClientEvents = ({ socket.on('UPDATE_DOWNLOAD_INFO', (payload: any) => { if (!getAuthorized()) return logger.http.log('UPDATE_DOWNLOAD_INFO received:', payload) - mainWindow.webContents.send(RendererEvents.TRACK_INFO, getTrackData()) + sendToRenderer(RendererEvents.TRACK_INFO, getTrackData()) }) socket.on('INSTALL_MOD_UPDATE_FROM', async (payload: any) => { @@ -108,12 +117,12 @@ export const registerSocketClientEvents = ({ socket.on(RendererEvents.SEND_TRACK, (payload: any) => { if (!getAuthorized()) return logger.http.log('SEND_TRACK received:', payload) - mainWindow.webContents.send(RendererEvents.SEND_TRACK, payload.data) + sendToRenderer(RendererEvents.SEND_TRACK, payload.data) }) socket.on('disconnect', () => { logger.http.log('Client disconnected') - mainWindow.webContents.send(RendererEvents.TRACK_INFO, { + sendToRenderer(RendererEvents.TRACK_INFO, { type: 'refresh', }) }) diff --git a/src/main/modules/httpServer/httpRequestHandler.ts b/src/main/modules/httpServer/httpRequestHandler.ts index e0cd5cb3..1793a558 100644 --- a/src/main/modules/httpServer/httpRequestHandler.ts +++ b/src/main/modules/httpServer/httpRequestHandler.ts @@ -1,11 +1,11 @@ import * as http from 'http' import * as fs from 'original-fs' import * as path from 'path' -import { app } from 'electron' import { parse } from 'url' import type { Track } from '@entities/track/model/track.interface' import { resolveAddonDirectory } from '../../utils/addonRegistry' import { buildCorsHeaders } from './cors' +import { getAddonsRoot, resolveExistingFileInsideBase, resolveExistingPathInsideBase, resolvePathInsideBase } from '../../utils/addonPaths' interface LoggerLike { http: { @@ -20,7 +20,6 @@ interface LoggerLike { interface CreateHttpRequestHandlerOptions { logger: LoggerLike allowedOrigins: string[] - getAuthorized: () => boolean getTrackData: () => Track } @@ -83,8 +82,6 @@ const findAssetsDirectory = (basePath: string): string | null => { return null } -const getAddonRoot = () => path.join(app.getPath('appData'), 'PulseSync', 'addons') - const imageMimes: Record = { jpg: 'image/jpeg', png: 'image/png', @@ -106,7 +103,7 @@ const resolveAddonDirectoryRef = (query: Record): string => { return '' } -export const createHttpRequestHandler = ({ logger, allowedOrigins, getAuthorized, getTrackData }: CreateHttpRequestHandlerOptions) => { +export const createHttpRequestHandler = ({ logger, allowedOrigins, getTrackData }: CreateHttpRequestHandlerOptions) => { const handleGetAssetsRequest = (req: http.IncomingMessage, res: http.ServerResponse) => { try { const { query } = parse(req.url || '', true) @@ -114,7 +111,8 @@ export const createHttpRequestHandler = ({ logger, allowedOrigins, getAuthorized if (!directory) return sendJson(res, 400, { error: 'Missing query parameter: directory, id or name' }) - const addonPath = path.join(getAddonRoot(), directory) + const addonPath = resolvePathInsideBase(getAddonsRoot(), path.join(getAddonsRoot(), directory)) + if (!addonPath) return sendJson(res, 400, { error: 'Invalid addon directory' }) const assetsDir = findAssetsDirectory(addonPath) if (!assetsDir) return sendJson(res, 404, { error: 'Assets folder not found' }) @@ -136,14 +134,17 @@ export const createHttpRequestHandler = ({ logger, allowedOrigins, getAuthorized const directory = resolveAddonDirectoryRef(query as Record) if (!directory) return sendJson(res, 400, { error: 'Missing query parameter: directory, id or name' }) - const assetsDir = findAssetsDirectory(path.join(getAddonRoot(), directory)) + const addonPath = resolvePathInsideBase(getAddonsRoot(), path.join(getAddonsRoot(), directory)) + if (!addonPath) return sendJson(res, 400, { error: 'Invalid addon directory' }) + + const assetsDir = findAssetsDirectory(addonPath) if (!assetsDir) return sendJson(res, 404, { error: 'Assets folder not found' }) const fileName = pathname!.substring(ASSET_PREFIX.length) const filePath = findFileInDirectory(fileName, assetsDir) logger.http.log('File Path:', filePath) - if (!filePath) return sendJson(res, 404, { error: 'File not found' }) + if (!filePath || !resolveExistingPathInsideBase(assetsDir, filePath)) return sendJson(res, 404, { error: 'File not found' }) const ext = path.extname(filePath).slice(1) res.writeHead(200, { 'Content-Type': imageMimes[ext] || 'application/octet-stream' }) @@ -166,8 +167,11 @@ export const createHttpRequestHandler = ({ logger, allowedOrigins, getAuthorized return sendJson(res, 400, { ok: false, error: 'Remote URLs are not served by this endpoint.' }) } - const targetPath = path.join(getAddonRoot(), directory, fileName) - if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isFile()) { + const addonPath = resolvePathInsideBase(getAddonsRoot(), path.join(getAddonsRoot(), directory)) + if (!addonPath) return sendJson(res, 400, { error: 'Invalid addon directory' }) + + const targetPath = resolveExistingFileInsideBase(addonPath, fileName) + if (!targetPath) { return sendJson(res, 404, { error: 'File not found in addon root' }) } @@ -192,7 +196,6 @@ export const createHttpRequestHandler = ({ logger, allowedOrigins, getAuthorized const handleGetTrack = (_req: http.IncomingMessage, res: http.ServerResponse) => { try { - if (!getAuthorized()) return sendJson(res, 403, { error: 'Unauthorized' }) sendJson(res, 200, getTrackData()) } catch (err) { logger.http.error('Error processing get_track:', err) diff --git a/src/main/modules/httpServer/index.ts b/src/main/modules/httpServer/index.ts index 15140266..1d51a592 100644 --- a/src/main/modules/httpServer/index.ts +++ b/src/main/modules/httpServer/index.ts @@ -62,7 +62,6 @@ const initializeServer = () => { const handleHttpRequest = createHttpRequestHandler({ logger, allowedOrigins, - getAuthorized: () => authorized, getTrackData: () => data, }) diff --git a/src/main/modules/mod/download.helpers.ts b/src/main/modules/mod/download.helpers.ts index c30ffde3..9593e758 100644 --- a/src/main/modules/mod/download.helpers.ts +++ b/src/main/modules/mod/download.helpers.ts @@ -3,12 +3,16 @@ import * as fs from 'original-fs' import axios from 'axios' import * as https from 'https' import crypto from 'crypto' -import { Transform, pipeline as nodePipeline } from 'stream' +import { Readable, Transform, pipeline as nodePipeline } from 'stream' import { promisify } from 'util' import RendererEvents, { RendererEvent } from '../../../common/types/rendererEvents' import { t } from '../../i18n' +import logger from '../logger' +import { HandleErrorsElectron } from '../handlers/handleErrorsElectron' const pipeline = promisify(nodePipeline) +const DOWNLOAD_REQUEST_TIMEOUT_MS = 30_000 +const DOWNLOAD_INACTIVITY_TIMEOUT_MS = 30_000 export function sendToRenderer(window: BrowserWindow | null | undefined, channel: RendererEvent, payload: any) { window?.webContents.send(channel, payload) @@ -42,8 +46,10 @@ export function unlinkIfExists(p: string) { export function restoreBackupIfExists(savePath: string, backupPath: string) { try { - if (fs.existsSync(backupPath)) fs.renameSync(backupPath, savePath) - } catch {} + if (fs.existsSync(backupPath)) fs.copyFileSync(backupPath, savePath) + } catch (error) { + HandleErrorsElectron.handleError('download.helpers', 'restoreBackupIfExists', 'copy', error) + } } export class DownloadError extends Error { @@ -67,44 +73,90 @@ export async function downloadToTempWithProgress(args: { name: string }): Promise<{ totalBytes: number; computedHash?: string }> { const { window, url, tempFilePath, expectedChecksum, progressScale = 0.6, progressBase = 0, userAgent, rejectUnauthorized = true, name } = args + const startedAt = Date.now() + const downloadHost = (() => { + try { + return new URL(url).host + } catch { + return 'unknown-host' + } + })() const headers: Record = { 'User-Agent': userAgent, Accept: 'application/octet-stream', } const httpsAgent = new https.Agent({ rejectUnauthorized }) - - const response = await axios.get(url, { httpsAgent, responseType: 'stream', headers }) - - const total = Number(response.headers['content-length'] || 0) let downloaded = 0 - const hasher = expectedChecksum ? crypto.createHash('sha256') : null + let inactivityTimer: NodeJS.Timeout | null = null + let responseStream: Readable | null = null - const progressTap = new Transform({ - transform(chunk, _enc, cb) { - downloaded += chunk.length - if (total > 0) { - const frac = downloaded / total - const scaled = Math.min(frac * progressScale, progressScale) - const combined = Math.min(progressBase + scaled, progressBase + progressScale) - setProgress(window, combined) - sendProgress(window, Math.round(Math.min(progressBase + Math.min(frac, 1) * progressScale, 1) * 100), name) - } - if (hasher) hasher.update(chunk) - this.push(chunk) - cb() - }, - }) + const clearInactivityTimer = () => { + if (!inactivityTimer) return + clearTimeout(inactivityTimer) + inactivityTimer = null + } + + const resetInactivityTimer = () => { + clearInactivityTimer() + inactivityTimer = setTimeout(() => { + const error = new DownloadError(t('main.modDownload.networkError'), 'network') + responseStream?.destroy(error) + }, DOWNLOAD_INACTIVITY_TIMEOUT_MS) + } - const writer = fs.createWriteStream(tempFilePath) + logger.modManager.info(`Starting ${name} download from ${downloadHost}`) try { + const response = await axios.get(url, { + httpsAgent, + responseType: 'stream', + headers, + timeout: DOWNLOAD_REQUEST_TIMEOUT_MS, + }) + + const total = Number(response.headers['content-length'] || 0) + responseStream = response.data as Readable + + const progressTap = new Transform({ + transform(chunk, _enc, cb) { + resetInactivityTimer() + downloaded += chunk.length + if (total > 0) { + const frac = downloaded / total + const scaled = Math.min(frac * progressScale, progressScale) + const combined = Math.min(progressBase + scaled, progressBase + progressScale) + setProgress(window, combined) + sendProgress(window, Math.round(Math.min(progressBase + Math.min(frac, 1) * progressScale, 1) * 100), name) + } + if (hasher) hasher.update(chunk) + this.push(chunk) + cb() + }, + }) + + const writer = fs.createWriteStream(tempFilePath) + resetInactivityTimer() await pipeline(response.data, progressTap, writer) - } catch (e: any) { - throw new DownloadError(e?.message || t('main.modDownload.networkError'), 'network') + } catch (error: any) { + logger.modManager.warn(`${name} download failed from ${downloadHost}`, { + bytes: downloaded, + durationMs: Date.now() - startedAt, + code: error?.code, + message: error?.message, + }) + if (error instanceof DownloadError) throw error + throw new DownloadError(error?.message || t('main.modDownload.networkError'), 'network') + } finally { + clearInactivityTimer() } + logger.modManager.info(`Completed ${name} download from ${downloadHost}`, { + bytes: downloaded, + durationMs: Date.now() - startedAt, + }) + let digest: string | undefined if (expectedChecksum && hasher) { digest = hasher.digest('hex') diff --git a/src/main/modules/mod/installModUpdateFrom.ts b/src/main/modules/mod/installModUpdateFrom.ts index c4e5d8b0..75ae9659 100644 --- a/src/main/modules/mod/installModUpdateFrom.ts +++ b/src/main/modules/mod/installModUpdateFrom.ts @@ -1,16 +1,19 @@ import { BrowserWindow } from 'electron' import * as fs from 'original-fs' +import os from 'node:os' import * as path from 'path' -import { fileURLToPath, pathToFileURL } from 'node:url' +import { fileURLToPath } from 'node:url' import RendererEvents from '../../../common/types/rendererEvents' import { t } from '../../i18n' import { isLinuxAccessError } from '../../utils/appUtils/elevation' import logger from '../logger' import { sendToRenderer } from './download.helpers' import { closeMusicIfRunning, persistInstalledModState, readChecksum, sendSuccessAfterLaunch } from './mod-manager.helpers' -import { ensureBackup, ensureLinuxModPath, resolveBasePaths, writePatchedAsarAndPatchBundle } from './mod-files' +import { ensureBackup, ensureLinuxModPath, installPreparedAsarAndPatchBundle, resolveBasePaths } from './mod-files' import { resolveInstallModMatch } from './network/modCatalog' import { getState } from '../state' +import { prepareAsarArtifactInWorker } from './network/artifactWorkerClient' +import { HandleErrorsElectron } from '../handlers/handleErrorsElectron' const ACTION_PATCH = 'PATCH' const PATCH_TYPE_FROM_MOD = 'FROM_MOD' @@ -177,7 +180,7 @@ export const installModUpdateFromAsar = async ( let wasClosed = false try { - const { incomingAsar, incomingChecksum, matchedMod } = await resolveInstallModMatch(asarPath) + const { incomingChecksum, matchedMod } = await resolveInstallModMatch(asarPath) if (!matchedMod) { const error = t('main.modManager.modVersionNotFoundByChecksum') @@ -189,8 +192,19 @@ export const installModUpdateFromAsar = async ( wasClosed = await closeMusicIfRunning(window) await ensureBackup(paths) - const sourceUrl = pathToFileURL(asarPath).toString() - const patched = await writePatchedAsarAndPatchBundle(paths.modAsar, incomingAsar, sourceUrl, paths.backupAsar) + const preparedAsarPath = path.join(os.tmpdir(), `pulsesync-local-${process.pid}-${Date.now()}.asar`) + let patched = false + try { + const prepared = await prepareAsarArtifactInWorker({ + archivePath: asarPath, + archiveExtension: '.asar', + expectedChecksum: incomingChecksum, + outputPath: preparedAsarPath, + }) + patched = await installPreparedAsarAndPatchBundle(paths.modAsar, prepared.preparedPath, paths.backupAsar) + } finally { + await fs.promises.rm(preparedAsarPath, { force: true }).catch(() => {}) + } if (!patched) { const error = t('main.modNetwork.patchError') @@ -198,7 +212,7 @@ export const installModUpdateFromAsar = async ( return { success: false, type: 'patch_error', error } } - const checksum = readChecksum(paths.modAsar) + const checksum = await readChecksum(paths.modAsar) const resolvedChecksum = checksum ?? incomingChecksum await persistInstalledModState(paths, matchedMod, resolvedChecksum) @@ -220,6 +234,7 @@ export const installModUpdateFromAsar = async ( sendInstallFailure(window, { error: message, type: 'linux_permissions_required' }) return { success: false, type: 'linux_permissions_required', error: message } } + HandleErrorsElectron.handleError('installModUpdateFrom', source, 'pipeline', error) const message = error?.message || String(error) sendInstallFailure(window, { error: message, type: 'install_mod_update_from_error' }) return { success: false, type: 'install_mod_update_from_error', error: message } diff --git a/src/main/modules/mod/mod-files.ts b/src/main/modules/mod/mod-files.ts index ad80e42e..7f91e06e 100644 --- a/src/main/modules/mod/mod-files.ts +++ b/src/main/modules/mod/mod-files.ts @@ -1,18 +1,11 @@ import * as path from 'path' import * as fs from 'original-fs' -import * as zlib from 'node:zlib' -import { promisify } from 'util' -import crypto from 'crypto' -import os from 'os' -import asar from '@electron/asar' import logger from '../logger' import { getState } from '../state' import { AsarPatcher, copyFile, getPathToYandexMusic, isLinux, resolveModAsarPath, updateIntegrityHashInExe } from '../../utils/appUtils' -import { DownloadError } from './download.helpers' import { t } from '../../i18n' +import { HandleErrorsElectron } from '../handlers/handleErrorsElectron' -export const gunzipAsync = promisify(zlib.gunzip) -export const zstdDecompressAsync = promisify((zlib as any).zstdDecompress || ((b: Buffer, cb: any) => cb(new Error('zstd not available')))) const State = getState() export type Paths = { @@ -61,47 +54,19 @@ export async function ensureBackup(paths: Paths): Promise { logger.modManager.info(`Backup created ${path.basename(source)} -> ${path.basename(paths.backupAsar)}`) } -export async function writePatchedAsarAndPatchBundle( - savePath: string, - rawDownloaded: Buffer, - link: string, - backupPath: string, - expectedChecksum?: string, -): Promise { - let asarBuf: Buffer = rawDownloaded - const ext = path.extname(new URL(link).pathname).toLowerCase() - if (ext === '.gz') asarBuf = await gunzipAsync(rawDownloaded) - else if (ext === '.zst' || ext === '.zstd') asarBuf = (await zstdDecompressAsync(rawDownloaded as any)) as any - if (expectedChecksum) { - const checksumTarget = ext === '.gz' || ext === '.zst' || ext === '.zstd' ? rawDownloaded : asarBuf - const actualHash = crypto.createHash('sha256').update(checksumTarget).digest('hex') - if (actualHash !== expectedChecksum) { - console.error(`[CHECKSUM ERROR] Expected: ${expectedChecksum}, Got: ${actualHash}, Size: ${checksumTarget.length} bytes, URL: ${link}`) - throw new DownloadError( - `checksum mismatch (expected: ${expectedChecksum.substring(0, 8)}..., got: ${actualHash.substring(0, 8)}...)`, - 'checksum_mismatch', - ) - } - } - const tempAsarPath = path.join(os.tmpdir(), `pulsesync-${Date.now()}-${process.pid}.asar`) - await fs.promises.writeFile(tempAsarPath, asarBuf) - try { - await copyFile(tempAsarPath, savePath) - } finally { - try { - await fs.promises.unlink(tempAsarPath) - } catch {} - } +export async function installPreparedAsarAndPatchBundle(savePath: string, preparedAsarPath: string, backupPath: string): Promise { + await copyFile(preparedAsarPath, savePath) const patcher = new AsarPatcher(path.resolve(path.dirname(savePath), '..', '..')) let ok: boolean try { ok = await patcher.patch(() => {}) - } catch { + } catch (error) { + HandleErrorsElectron.handleError('mod-files', 'installPreparedAsarAndPatchBundle', 'patch', error) ok = false } if (!ok) { - if (fs.existsSync(backupPath)) fs.renameSync(backupPath, savePath) + if (fs.existsSync(backupPath)) await copyFile(backupPath, savePath) return false } return true @@ -110,12 +75,11 @@ export async function writePatchedAsarAndPatchBundle( export async function restoreWindowsIntegrity(paths: Paths): Promise { try { const exePath = path.join(process.env.LOCALAPPDATA || '', 'Programs', 'YandexMusic', 'Яндекс Музыка.exe') - const header = asar.getRawHeader(paths.modAsar) - const newHash = crypto.createHash('sha256').update(header.headerString).digest('hex') - await updateIntegrityHashInExe(exePath, newHash) + await updateIntegrityHashInExe(exePath, paths.modAsar) logger.modManager.info('Windows Integrity hash restored.') } catch (err) { logger.modManager.error('Error restoring Integrity hash in exe:', err) + HandleErrorsElectron.handleError('mod-files', 'restoreWindowsIntegrity', 'catch', err) } } @@ -127,5 +91,6 @@ export async function restoreMacIntegrity(paths: Paths): Promise { logger.modManager.info('macOS Integrity hash restored.') } catch (err) { logger.modManager.error('Error restoring Integrity hash in Info.plist:', err) + HandleErrorsElectron.handleError('mod-files', 'restoreMacIntegrity', 'catch', err) } } diff --git a/src/main/modules/mod/mod-manager.helpers.ts b/src/main/modules/mod/mod-manager.helpers.ts index 5e7ac9ea..dd9cac18 100644 --- a/src/main/modules/mod/mod-manager.helpers.ts +++ b/src/main/modules/mod/mod-manager.helpers.ts @@ -2,25 +2,19 @@ import { app, BrowserWindow } from 'electron' import * as path from 'path' import * as fs from 'original-fs' import os from 'os' -import crypto from 'crypto' import RendererEvents, { RendererEvent } from '../../../common/types/rendererEvents' import { getState } from '../state' import logger from '../logger' -import { - closeYandexMusic, - copyFile, - getInstalledYmMetadata, - getYandexMusicProcesses, - isYandexMusicRunning, - launchYandexMusic, -} from '../../utils/appUtils' -import { Paths, writePatchedAsarAndPatchBundle } from './mod-files' -import { downloadAndUpdateFile } from './network' +import { closeYandexMusic, getInstalledYmMetadata, getYandexMusicProcesses, isYandexMusicRunning, launchYandexMusic } from '../../utils/appUtils' +import { Paths } from './mod-files' +import { downloadAndUpdateFile, prepareAndInstallAsarArtifact } from './network' import { nativeDeleteFile, nativeFileExists } from '../nativeModules' import { resetProgress, sendProgress, sendToRenderer, setProgress } from './download.helpers' import { CACHE_DIR } from '../../constants/paths' import { t } from '../../i18n' import type { RemoteModInfo } from './network/modCatalog' +import { hashArtifactInWorker } from './network/artifactWorkerClient' +import { HandleErrorsElectron } from '../handlers/handleErrorsElectron' const State = getState() @@ -71,10 +65,17 @@ export async function tryUseCacheOrDownload( sendToRenderer(window, RendererEvents.UPDATE_MESSAGE, { message: t('main.modManager.usingCache') }) try { logger.modManager.info(`Using cached app.asar from ${cacheFile}`) - await copyFile(cacheFile, tempFilePath) - const fileBuffer = fs.readFileSync(tempFilePath) - const ok = await writePatchedAsarAndPatchBundle(paths.modAsar, fileBuffer, link, paths.backupAsar, checksum) + const progressBase = progress?.base ?? 0 + const progressScale = progress?.scale ?? 1 + const processingProgress = progressBase + progressScale * 0.85 + setProgress(window, processingProgress) + sendProgress(window, Math.round(processingProgress * 100), 'app.asar') + const preparedFilePath = `${tempFilePath}.prepared.${process.pid}.${Date.now()}.asar` + const ok = await prepareAndInstallAsarArtifact(cacheFile, preparedFilePath, link, paths.modAsar, paths.backupAsar, checksum) if (ok) { + const completedProgress = progressBase + progressScale + setProgress(window, completedProgress) + sendProgress(window, Math.round(completedProgress * 100), 'app.asar') logger.modManager.info('Successfully restored app.asar from cache') return true } @@ -84,13 +85,30 @@ export async function tryUseCacheOrDownload( resetProgress(window) } } - return await downloadAndUpdateFile(window, link, tempFilePath, paths.modAsar, paths.backupAsar, checksum, cacheDir, progress, 'app.asar', onFailure) + return await downloadAndUpdateFile( + window, + link, + tempFilePath, + paths.modAsar, + paths.backupAsar, + checksum, + cacheDir, + progress, + 'app.asar', + onFailure, + ) } -export function readChecksum(filePath: string): string | null { +export async function readChecksum(filePath: string): Promise { try { - const buf = fs.readFileSync(filePath) - return crypto.createHash('sha256').update(buf).digest('hex') + const startedAt = Date.now() + const result = await hashArtifactInWorker({ filePath }) + logger.modManager.info('Hashed artifact in worker', { + totalMs: Date.now() - startedAt, + workerThreadId: result.workerThreadId, + checksum: result.durationMs, + }) + return result.checksum } catch (err: any) { logger.modManager.warn('Failed to verify existing file:', err) return null @@ -165,11 +183,20 @@ export async function sendSuccessAfterLaunch( channel: RendererEvent, payload: { success: true }, ): Promise { - if (!(await isYandexMusicRunning()) && wasClosed) { - await launchYandexMusic() - setTimeout(() => sendToRenderer(window, channel, payload), 1500) - return true - } sendToRenderer(window, channel, payload) - return false + resetProgress(window) + + if (!wasClosed) return false + + void (async () => { + try { + if (!(await isYandexMusicRunning())) { + await launchYandexMusic() + } + } catch (error) { + logger.modManager.warn('Failed to relaunch Yandex Music after mod operation:', error) + HandleErrorsElectron.handleError('mod-manager.helpers', 'sendSuccessAfterLaunch', 'relaunch', error) + } + })() + return true } diff --git a/src/main/modules/mod/modManager.ts b/src/main/modules/mod/modManager.ts index 9c0d6748..9efaff7d 100644 --- a/src/main/modules/mod/modManager.ts +++ b/src/main/modules/mod/modManager.ts @@ -9,7 +9,7 @@ import { copyFile, downloadYandexMusic, getInstalledYmMetadata, isLinux, isMac, import { ensureBackup, ensureLinuxModPath, resolveBasePaths, restoreMacIntegrity, restoreWindowsIntegrity } from './mod-files' import { downloadAndExtractUnpacked, downloadAndUpdateFile } from './network' import { nativeRenameFile } from '../nativeModules' -import { resetProgress, sendFailure, sendToRenderer } from './download.helpers' +import { sendFailure, sendToRenderer } from './download.helpers' import { CACHE_DIR, TEMP_DIR } from '../../constants/paths' import { t } from '../../i18n' import { formatPkexecError, grantLinuxOwnershipWithPkexec, isLinuxAccessError } from '../../utils/appUtils/elevation' @@ -26,11 +26,12 @@ import { } from './mod-manager.helpers' import { getGithubModRelease } from './network/releaseCatalog' import type { ModDownloadFailure } from './network/types' +import { HandleErrorsElectron } from '../handlers/handleErrorsElectron' const State = getState() -const PROGRESS_ASAR_ONLY = { base: 0, scale: 1, resetOnComplete: true } -const PROGRESS_ASAR_WITH_UNPACKED = { base: 0, scale: 0.6, resetOnComplete: false } -const PROGRESS_UNPACKED = { base: 0.6, scale: 0.4, resetOnComplete: true } +const PROGRESS_ASAR_ONLY = { base: 0, scale: 0.95, resetOnComplete: false } +const PROGRESS_ASAR_WITH_UNPACKED = { base: 0, scale: 0.58, resetOnComplete: false } +const PROGRESS_UNPACKED = { base: 0.6, scale: 0.39, resetOnComplete: false } const MOD_DOWNLOAD_FALLBACK_TYPES = new Set(['download_error', 'download_unpacked_error', 'checksum_mismatch']) const isFallbackEligibleDownloadFailure = (failure: ModDownloadFailure | null): failure is ModDownloadFailure => @@ -81,6 +82,7 @@ export const modManager = (window: BrowserWindow): void => { const ymMetadata = await getInstalledYmMetadata() const resolvedMusicVersion = ymMetadata?.version ?? musicVersion + let finalProgressName = 'app.asar' if (isMac()) { try { @@ -107,6 +109,7 @@ export const modManager = (window: BrowserWindow): void => { sendFailure(window, { error: t('main.modManager.linuxPermissionsRequired'), type: 'linux_permissions_required' }) return } + HandleErrorsElectron.handleError('modManager', 'install', 'backup', e) sendFailure(window, { error: e?.message || String(e), type: 'backup_error' }) return } @@ -124,6 +127,7 @@ export const modManager = (window: BrowserWindow): void => { ): Promise => { const tempFilePath = path.join(TEMP_DIR, 'app.asar.download') const hasUnpacked = Boolean(releaseData.unpackLink) + finalProgressName = hasUnpacked ? 'app.asar.unpacked' : 'app.asar' const asarProgress = hasUnpacked ? PROGRESS_ASAR_WITH_UNPACKED : PROGRESS_ASAR_ONLY const unpackedProgress = hasUnpacked ? PROGRESS_UNPACKED : undefined @@ -133,14 +137,14 @@ export const modManager = (window: BrowserWindow): void => { logger.modManager.warn('Failed to create cache dir:', err) }) - const currentHash = fileExists(paths.modAsar) ? readChecksum(paths.modAsar) : null + const currentHash = fileExists(paths.modAsar) ? await readChecksum(paths.modAsar) : null if (currentHash === releaseData.checksum) { logger.modManager.info('app.asar hash matches, skipping download') sendToRenderer(window, RendererEvents.UPDATE_MESSAGE, { message: t('main.modManager.modAlreadyInstalled') }) if (hasUnpacked) { setProgressPercent(window, PROGRESS_UNPACKED.base, 'app.asar.unpacked') } else { - resetProgress(window) + setProgressPercent(window, PROGRESS_ASAR_ONLY.scale, 'app.asar') } } else if ( !(await tryUseCacheOrDownload( @@ -177,11 +181,15 @@ export const modManager = (window: BrowserWindow): void => { } if (releaseData.unpackLink) { + const unpackedBoundaryStartedAt = Date.now() setProgressPercent(window, PROGRESS_UNPACKED.base, 'app.asar.unpacked') + logger.modManager.info('Starting app.asar.unpacked stage', { + progressUpdateMs: Date.now() - unpackedBoundaryStartedAt, + }) const unpackName = path.basename(new URL(releaseData.unpackLink).pathname) const tempUnpackedArchive = path.join(TEMP_DIR, unpackName || 'app.asar.unpacked') - const tempUnpackedDir = path.join(TEMP_DIR, 'app.asar.unpacked') + const tempUnpackedDir = path.join(TEMP_DIR, `pulsesync-unpacked-${process.pid}-${Date.now()}`) const targetUnpackedDir = path.join(path.dirname(paths.modAsar), 'app.asar.unpacked') const unpackedOk = await downloadAndExtractUnpacked( @@ -198,7 +206,7 @@ export const modManager = (window: BrowserWindow): void => { if (!unpackedOk) return false } - const actualAsarChecksum = readChecksum(paths.modAsar) ?? releaseData.checksum + const actualAsarChecksum = (await readChecksum(paths.modAsar)) ?? releaseData.checksum if (actualAsarChecksum) { logger.modManager.info('Calculated actual asar checksum:', actualAsarChecksum) } @@ -266,6 +274,7 @@ export const modManager = (window: BrowserWindow): void => { } } catch (fallbackError) { logger.modManager.error('GitHub fallback for mod update failed', fallbackError) + HandleErrorsElectron.handleError('modManager', 'install', 'github_fallback', fallbackError) sendFailure(window, backendFailure) return } @@ -292,6 +301,7 @@ export const modManager = (window: BrowserWindow): void => { logger.modManager.warn('Skipping version.bin update because no Yandex Music version was resolved') } + setProgressPercent(window, 1, finalProgressName) if (await sendSuccessAfterLaunch(window, wasClosed, RendererEvents.DOWNLOAD_SUCCESS, { success: true })) return } catch (error: any) { logger.modManager.error('Unexpected error:', error) @@ -299,6 +309,7 @@ export const modManager = (window: BrowserWindow): void => { sendFailure(window, { error: t('main.modManager.linuxPermissionsRequired'), type: 'linux_permissions_required' }) return } + HandleErrorsElectron.handleError('modManager', 'install', 'unexpected', error) sendFailure(window, { error: error.message, type: 'unexpected_error' }) } }, @@ -339,6 +350,7 @@ export const modManager = (window: BrowserWindow): void => { }) return } + HandleErrorsElectron.handleError('modManager', 'remove', 'unexpected', error) sendToRenderer(window, RendererEvents.REMOVE_MOD_FAILURE, { success: false, error: error.message, type: 'remove_mod_error' }) } }) @@ -348,6 +360,7 @@ export const modManager = (window: BrowserWindow): void => { sendToRenderer(window, RendererEvents.CLEAR_MOD_CACHE_SUCCESS, { success: true }) } catch (error: any) { logger.modManager.error('Failed to clear mod cache:', error) + HandleErrorsElectron.handleError('modManager', 'clear_cache', 'unexpected', error) sendToRenderer(window, RendererEvents.CLEAR_MOD_CACHE_FAILURE, { success: false, error: error?.message || 'Failed to clear mod cache', diff --git a/src/main/modules/mod/network/artifactWorker.ts b/src/main/modules/mod/network/artifactWorker.ts new file mode 100644 index 00000000..a27b263b --- /dev/null +++ b/src/main/modules/mod/network/artifactWorker.ts @@ -0,0 +1,146 @@ +import { parentPort, threadId, workerData } from 'node:worker_threads' +import { requirePulseSyncNative, type NativeArtifactDurations, type NativeArtifactResult } from '../../nativeModules/pulsesyncNative' +import type { + ArtifactWorkerFailure, + ArtifactWorkerRequest, + ArtifactWorkerRequestMessage, + ArtifactWorkerResponse, + ArtifactWorkerResponseMessage, + ArtifactWorkerStage, + ArtifactWorkerWarning, + HashArtifactRequest, + InstallUnpackedArtifactRequest, + PrepareAsarArtifactRequest, +} from './artifactWorker.types' + +const durationFields: Array<[keyof NativeArtifactDurations, string]> = [ + ['readMs', 'read'], + ['checksumMs', 'checksum'], + ['decompressMs', 'decompress'], + ['writeMs', 'write'], + ['cloneMs', 'clone'], + ['extractMs', 'extract'], + ['cacheWriteMs', 'cacheWrite'], + ['backupMs', 'backup'], + ['installMs', 'install'], + ['cleanupMs', 'cleanup'], +] + +function mapDurations(durations: NativeArtifactDurations): Record { + const result: Record = {} + for (const [nativeName, workerName] of durationFields) { + const value = durations[nativeName] + if (value > 0) result[workerName] = value + } + return result +} + +function mapWarnings(result: NativeArtifactResult): ArtifactWorkerWarning[] { + return result.warnings.map(warning => ({ + stage: warning.stage === 'cache' ? 'cache' : 'cleanup', + code: warning.code, + message: warning.message, + })) +} + +function mapFailure(result: NativeArtifactResult, fallbackStage: ArtifactWorkerStage): ArtifactWorkerFailure { + return { + ok: false, + stage: (result.stage as ArtifactWorkerStage | undefined) ?? fallbackStage, + code: result.code, + message: result.message ?? 'Native artifact operation failed', + } +} + +async function prepareAsarArtifact(request: PrepareAsarArtifactRequest): Promise { + const result = requirePulseSyncNative().prepareAsarArtifact({ + archivePath: request.archivePath, + archiveExtension: request.archiveExtension, + expectedChecksum: request.expectedChecksum, + outputPath: request.outputPath, + }) + if (!result.ok) return mapFailure(result, 'write') + return { + ok: true, + mode: 'prepareAsar', + durations: mapDurations(result.durations), + preparedPath: result.preparedPath ?? request.outputPath, + warnings: mapWarnings(result), + } +} + +async function installUnpackedArtifact(request: InstallUnpackedArtifactRequest): Promise { + const result = requirePulseSyncNative().installUnpackedArtifact({ + sourceKind: request.sourceKind ?? 'archive', + archivePath: request.archivePath, + archiveExtension: request.archiveExtension, + expectedChecksum: request.expectedChecksum, + preparedDirectoryPath: request.preparedDirectoryPath, + preparedDirectoryMarker: request.preparedDirectoryMarker, + stagingPath: request.stagingPath, + targetPath: request.targetPath, + }) + if (!result.ok) return mapFailure(result, 'install') + return { + ok: true, + mode: 'installUnpacked', + durations: mapDurations(result.durations), + warnings: mapWarnings(result), + } +} + +async function hashArtifact(request: HashArtifactRequest): Promise { + const startedAt = Date.now() + try { + return { + ok: true, + mode: 'hashFile', + checksum: requirePulseSyncNative().hashFile(request.filePath), + durations: { checksum: Date.now() - startedAt }, + warnings: [], + } + } catch (error: any) { + return { + ok: false, + stage: 'checksum', + code: typeof error?.code === 'string' ? error.code : undefined, + message: error instanceof Error ? error.message : String(error), + } + } +} + +async function processArtifact(request: ArtifactWorkerRequest): Promise { + if (request.mode === 'prepareAsar') return await prepareAsarArtifact(request) + if (request.mode === 'hashFile') return await hashArtifact(request) + return await installUnpackedArtifact(request) +} + +async function processArtifactSafely(request: ArtifactWorkerRequest): Promise { + try { + return await processArtifact(request) + } catch (error: any) { + return { + ok: false, + stage: request.mode === 'hashFile' ? 'checksum' : 'install', + code: typeof error?.code === 'string' ? error.code : undefined, + message: error instanceof Error ? error.message : String(error), + } + } +} + +if (workerData !== undefined) { + void processArtifactSafely(workerData as ArtifactWorkerRequest).then(response => parentPort?.postMessage(response)) +} else { + let requestQueue = Promise.resolve() + parentPort?.on('message', (message: ArtifactWorkerRequestMessage) => { + requestQueue = requestQueue.then(async () => { + const response = await processArtifactSafely(message.request) + const responseMessage: ArtifactWorkerResponseMessage = { + id: message.id, + response, + workerThreadId: threadId, + } + parentPort?.postMessage(responseMessage) + }) + }) +} diff --git a/src/main/modules/mod/network/artifactWorker.types.ts b/src/main/modules/mod/network/artifactWorker.types.ts new file mode 100644 index 00000000..0bc4cb26 --- /dev/null +++ b/src/main/modules/mod/network/artifactWorker.types.ts @@ -0,0 +1,80 @@ +export type ArtifactWorkerStage = 'read' | 'checksum' | 'decompress' | 'write' | 'extract' | 'backup' | 'install' | 'cleanup' | 'restore' + +export type InstallUnpackedArtifactRequest = { + mode: 'installUnpacked' + sourceKind?: 'archive' | 'directory' + archivePath: string + archiveExtension: string + expectedChecksum?: string + preparedDirectoryPath?: string + preparedDirectoryMarker?: { + fileName: string + value: string + } + stagingPath: string + targetPath: string +} + +export type PrepareAsarArtifactRequest = { + mode: 'prepareAsar' + archivePath: string + archiveExtension: string + expectedChecksum?: string + outputPath: string +} + +export type HashArtifactRequest = { + mode: 'hashFile' + filePath: string +} + +export type ArtifactWorkerRequest = InstallUnpackedArtifactRequest | PrepareAsarArtifactRequest | HashArtifactRequest + +export type ArtifactWorkerRequestMessage = { + id: number + request: ArtifactWorkerRequest +} + +export type InstallUnpackedArtifactSuccess = { + ok: true + mode: 'installUnpacked' + durations: Record + warnings: ArtifactWorkerWarning[] +} + +export type PrepareAsarArtifactSuccess = { + ok: true + mode: 'prepareAsar' + durations: Record + preparedPath: string + warnings: ArtifactWorkerWarning[] +} + +export type HashArtifactSuccess = { + ok: true + mode: 'hashFile' + checksum: string + durations: Record + warnings: ArtifactWorkerWarning[] +} + +export type ArtifactWorkerWarning = { + stage: 'cache' | 'cleanup' + code?: string + message: string +} + +export type ArtifactWorkerFailure = { + ok: false + stage: ArtifactWorkerStage + code?: string + message: string +} + +export type ArtifactWorkerResponse = InstallUnpackedArtifactSuccess | PrepareAsarArtifactSuccess | HashArtifactSuccess | ArtifactWorkerFailure + +export type ArtifactWorkerResponseMessage = { + id: number + response: ArtifactWorkerResponse + workerThreadId: number +} diff --git a/src/main/modules/mod/network/artifactWorkerClient.ts b/src/main/modules/mod/network/artifactWorkerClient.ts new file mode 100644 index 00000000..9ab3f891 --- /dev/null +++ b/src/main/modules/mod/network/artifactWorkerClient.ts @@ -0,0 +1,163 @@ +import * as path from 'node:path' +import { Worker } from 'node:worker_threads' +import isAppDev from '../../../utils/isAppDev' +import type { + ArtifactWorkerRequest, + ArtifactWorkerRequestMessage, + ArtifactWorkerResponse, + ArtifactWorkerResponseMessage, + ArtifactWorkerStage, + HashArtifactRequest, + InstallUnpackedArtifactRequest, + PrepareAsarArtifactRequest, +} from './artifactWorker.types' + +const ARTIFACT_WORKER_TIMEOUT_MS = 5 * 60 * 1000 +const ARTIFACT_WORKER_IDLE_TIMEOUT_MS = 15 * 1000 + +type PendingRequest = { + resolve: (result: ArtifactWorkerResult) => void + reject: (error: Error) => void + timeout: NodeJS.Timeout +} + +type ArtifactWorkerResult = { + response: ArtifactWorkerResponse + workerThreadId: number +} + +let activeSession: ArtifactWorkerSession | null = null +let nextRequestId = 1 + +export class ArtifactWorkerError extends Error { + constructor( + message: string, + public readonly stage: ArtifactWorkerStage, + public readonly code?: string, + ) { + super(message) + } +} + +function resolveArtifactWorkerPath(): string { + return isAppDev + ? path.resolve(__dirname, '..', 'worker', 'artifactWorker.cjs') + : path.join(process.resourcesPath, 'app.asar.unpacked', '.vite', 'worker', 'artifactWorker.cjs') +} + +class ArtifactWorkerSession { + private readonly worker = new Worker(resolveArtifactWorkerPath()) + private readonly pending = new Map() + private idleTimer: NodeJS.Timeout | null = null + private closed = false + + constructor() { + this.worker.on('message', (message: ArtifactWorkerResponseMessage) => this.handleMessage(message)) + this.worker.once('error', error => this.close(error instanceof Error ? error : new Error(String(error)))) + this.worker.once('exit', code => { + if (!this.closed) this.close(new Error(`Artifact worker exited before returning all results (code ${code})`), false) + }) + this.worker.unref() + } + + request(request: ArtifactWorkerRequest): Promise { + if (this.closed) return Promise.reject(new Error('Artifact worker session is closed')) + + this.clearIdleTimer() + this.worker.ref() + const id = nextRequestId++ + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.close(new Error(`Artifact worker timed out after ${ARTIFACT_WORKER_TIMEOUT_MS / 1000} seconds`)) + }, ARTIFACT_WORKER_TIMEOUT_MS) + const pendingRequest: PendingRequest = { resolve, reject, timeout } + this.pending.set(id, pendingRequest) + + const message: ArtifactWorkerRequestMessage = { id, request } + try { + this.worker.postMessage(message) + } catch (error) { + this.close(error instanceof Error ? error : new Error(String(error))) + } + }) + } + + private handleMessage(message: ArtifactWorkerResponseMessage): void { + const pendingRequest = this.pending.get(message.id) + if (!pendingRequest) return + + this.pending.delete(message.id) + clearTimeout(pendingRequest.timeout) + if (message.response.ok) { + pendingRequest.resolve({ response: message.response, workerThreadId: message.workerThreadId }) + } else { + pendingRequest.reject(new ArtifactWorkerError(message.response.message, message.response.stage, message.response.code)) + } + this.scheduleIdleTermination() + } + + private scheduleIdleTermination(): void { + if (this.closed || this.pending.size > 0) return + this.worker.unref() + this.idleTimer = setTimeout(() => this.close(undefined), ARTIFACT_WORKER_IDLE_TIMEOUT_MS) + this.idleTimer.unref() + } + + private clearIdleTimer(): void { + if (!this.idleTimer) return + clearTimeout(this.idleTimer) + this.idleTimer = null + } + + private close(error?: Error, terminate = true): void { + if (this.closed) return + this.closed = true + this.clearIdleTimer() + if (activeSession === this) activeSession = null + + const closeError = error ?? new Error('Artifact worker session closed') + for (const pendingRequest of this.pending.values()) { + clearTimeout(pendingRequest.timeout) + pendingRequest.reject(closeError) + } + this.pending.clear() + if (terminate) void this.worker.terminate() + } +} + +function getArtifactWorkerSession(): ArtifactWorkerSession { + if (!activeSession) activeSession = new ArtifactWorkerSession() + return activeSession +} + +async function runArtifactWorker(request: ArtifactWorkerRequest): Promise { + return await getArtifactWorkerSession().request(request) +} + +const formatWarnings = (response: Extract): string[] => + response.warnings.map(warning => `${warning.code ? `${warning.code}: ` : ''}${warning.message}`) + +export async function installUnpackedArtifactInWorker( + request: Omit, +): Promise<{ durations: Record; warnings: string[]; workerThreadId: number }> { + const { response, workerThreadId } = await runArtifactWorker({ ...request, mode: 'installUnpacked' }) + if (!response.ok || response.mode !== 'installUnpacked') throw new Error('Artifact worker returned an unexpected response') + return { durations: response.durations, warnings: formatWarnings(response), workerThreadId } +} + +export async function prepareAsarArtifactInWorker( + request: Omit, +): Promise<{ durations: Record; preparedPath: string; warnings: string[]; workerThreadId: number }> { + const { response, workerThreadId } = await runArtifactWorker({ ...request, mode: 'prepareAsar' }) + if (!response.ok || response.mode !== 'prepareAsar') throw new Error('Artifact worker returned an unexpected response') + return { durations: response.durations, preparedPath: response.preparedPath, warnings: formatWarnings(response), workerThreadId } +} + +export async function hashArtifactInWorker( + request: Omit, +): Promise<{ checksum: string; durationMs: number; workerThreadId: number }> { + const { response, workerThreadId } = await runArtifactWorker({ ...request, mode: 'hashFile' }) + if (!response.ok || response.mode !== 'hashFile') throw new Error('Artifact worker returned an unexpected response') + return { checksum: response.checksum, durationMs: response.durations.checksum ?? 0, workerThreadId } +} diff --git a/src/main/modules/mod/network/helpers.ts b/src/main/modules/mod/network/helpers.ts index e167578c..c611255e 100644 --- a/src/main/modules/mod/network/helpers.ts +++ b/src/main/modules/mod/network/helpers.ts @@ -1,19 +1,12 @@ import * as fs from 'original-fs' import * as path from 'path' -import crypto from 'crypto' -import AdmZip from 'adm-zip' import logger from '../../logger' -import { gunzipAsync, zstdDecompressAsync } from '../mod-files' -import type { ReplaceDirFailure, ReplaceDirResult, RetryStageFailure, RetryStageResult } from './types' +import { hashArtifactInWorker } from './artifactWorkerClient' export const UNPACKED_MARKER_FILE = '.pulsesync_unpacked_checksum' -const REPLACE_RECOVERABLE_CODES = new Set(['EXDEV', 'EPERM', 'EACCES', 'EBUSY', 'ENOTEMPTY', 'EEXIST']) - -const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) - -export function sha256Hex(buf: Buffer): string { - return crypto.createHash('sha256').update(buf).digest('hex') +export async function sha256File(filePath: string): Promise { + return (await hashArtifactInWorker({ filePath })).checksum } export async function ensureDir(dir: string): Promise { @@ -42,91 +35,43 @@ export async function pruneCacheFiles(cacheDir: string, keepFile: string, matche } } -export function readCachedArchive(cacheFile: string, checksum?: string): Buffer | null { - if (!fs.existsSync(cacheFile)) return null +export async function pruneCacheDirectories(cacheDir: string, keepDirectory: string, matcher: (directory: string) => boolean, warnLabel: string) { + try { + const entries = await fs.promises.readdir(cacheDir, { withFileTypes: true }) + const keepName = path.basename(keepDirectory) + for (const entry of entries) { + if (!entry.isDirectory() || entry.name === keepName || !matcher(entry.name)) continue + try { + await fs.promises.rm(path.join(cacheDir, entry.name), { recursive: true, force: true }) + } catch (e) { + logger.modManager.warn(warnLabel, entry.name, e) + } + } + } catch (e) { + logger.modManager.warn('Failed to cleanup cache:', e) + } +} + +export async function isCachedArchiveValid(cacheFile: string, checksum?: string): Promise { try { - const cached = fs.readFileSync(cacheFile) + await fs.promises.access(cacheFile, fs.constants.R_OK) if (checksum) { - const cachedHash = sha256Hex(cached) + const cachedHash = await sha256File(cacheFile) if (cachedHash !== checksum) { logger.modManager.warn('Cached archive hash mismatch, redownloading') try { - fs.rmSync(cacheFile, { force: true }) + await fs.promises.rm(cacheFile, { force: true }) } catch {} - return null + return false } } - return cached + return true } catch (e) { - logger.modManager.warn('Failed to read cached archive, redownloading:', e) - return null - } -} - -export async function decompressArchive(rawArchive: Buffer, extLower: string): Promise { - if (extLower === '.zst' || extLower === '.zstd') { - return (await zstdDecompressAsync(rawArchive as any)) as Buffer - } - if (extLower === '.gz') { - return await gunzipAsync(rawArchive) - } - return rawArchive -} - -function isZipBuffer(buf: Buffer): boolean { - return ( - !!buf && - buf.length >= 4 && - buf[0] === 0x50 && - buf[1] === 0x4b && - ((buf[2] === 0x03 && buf[3] === 0x04) || (buf[2] === 0x05 && buf[3] === 0x06) || (buf[2] === 0x07 && buf[3] === 0x08)) - ) -} - -export function extractZipBuffer(zipBuffer: Buffer, destination: string): void { - fs.rmSync(destination, { recursive: true, force: true }) - fs.mkdirSync(destination, { recursive: true }) - - if (!zipBuffer || zipBuffer.length < 4) { - throw new Error('Invalid ZIP buffer') - } - - if (!isZipBuffer(zipBuffer)) { - throw new Error('Expected ZIP archive') - } - - try { - const zip = new AdmZip(zipBuffer) - zip.extractAllTo(destination, true) - } catch { - throw new Error('Failed to extract ZIP archive') - } -} - -export function resolveExtractedRoot(extractDir: string, targetPath: string): string { - const expectedRootName = path.basename(targetPath) - - let entries: fs.Dirent[] - try { - entries = fs.readdirSync(extractDir, { withFileTypes: true }) - } catch { - return extractDir + if ((e as NodeJS.ErrnoException)?.code !== 'ENOENT') { + logger.modManager.warn('Failed to validate cached archive, redownloading:', e) + } + return false } - - const meaningful = entries.filter(e => { - const n = e.name - if (!n) return false - if (n === '__MACOSX') return false - return n !== '.DS_Store' - }) - - if (meaningful.length !== 1) return extractDir - - const only = meaningful[0] - if (!only.isDirectory()) return extractDir - if (only.name !== expectedRootName) return extractDir - - return path.join(extractDir, only.name) } export function readUnpackedMarker(targetPath: string): string | null { @@ -148,73 +93,3 @@ export function writeUnpackedMarker(targetPath: string, checksum: string): void logger.modManager.warn('Failed to write unpacked marker:', e) } } - -function cleanupTempExtractPath(sourceDir: string, tempExtractPath: string): void { - if (sourceDir === tempExtractPath) return - fs.rmSync(tempExtractPath, { recursive: true, force: true }) -} - -function isRetryStageFailure(result: RetryStageResult): result is RetryStageFailure { - return result.success === false -} - -export function isReplaceDirFailure(result: ReplaceDirResult): result is ReplaceDirFailure { - return result.ok === false -} - -async function runReplaceStageWithRetries(runStage: () => void, maxAttempts: number, retryDelayStepMs: number): Promise { - let lastErr: any - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - runStage() - return { success: true } - } catch (err: any) { - lastErr = err - const recoverable = REPLACE_RECOVERABLE_CODES.has(err?.code) - if (!recoverable) { - return { success: false, error: err, recoverable: false } - } - if (attempt < maxAttempts) { - await sleep(retryDelayStepMs * attempt) - } - } - } - return { success: false, error: lastErr, recoverable: true } -} - -export async function tryReplaceDir(sourceDir: string, targetDir: string, tempExtractPath: string): Promise { - const maxAttempts = process.platform === 'win32' ? 5 : 2 - - const moveResult = await runReplaceStageWithRetries( - () => { - fs.rmSync(targetDir, { recursive: true, force: true }) - fs.renameSync(sourceDir, targetDir) - }, - maxAttempts, - 120, - ) - - if (!isRetryStageFailure(moveResult)) { - cleanupTempExtractPath(sourceDir, tempExtractPath) - return { ok: true } - } - if (!moveResult.recoverable) { - return { ok: false, error: moveResult.error, stage: 'move' } - } - - const copyResult = await runReplaceStageWithRetries( - () => { - fs.rmSync(targetDir, { recursive: true, force: true }) - fs.cpSync(sourceDir, targetDir, { recursive: true, force: true }) - }, - maxAttempts, - 150, - ) - - if (!isRetryStageFailure(copyResult)) { - fs.rmSync(tempExtractPath, { recursive: true, force: true }) - return { ok: true } - } - - return { ok: false, error: copyResult.error, stage: 'copy' } -} diff --git a/src/main/modules/mod/network/index.ts b/src/main/modules/mod/network/index.ts index 90a21173..c757720a 100644 --- a/src/main/modules/mod/network/index.ts +++ b/src/main/modules/mod/network/index.ts @@ -3,9 +3,8 @@ import axios from 'axios' import * as fs from 'original-fs' import * as path from 'path' import logger from '../../logger' -import RendererEvents from '../../../../common/types/rendererEvents' import { HandleErrorsElectron } from '../../handlers/handleErrorsElectron' -import { isCompressedArchiveLink, writePatchedAsarAndPatchBundle } from '../mod-files' +import { installPreparedAsarAndPatchBundle, isCompressedArchiveLink } from '../mod-files' import { t } from '../../../i18n' import { copyFile } from '../../../utils/appUtils' import { @@ -16,24 +15,43 @@ import { restoreBackupIfExists, downloadToTempWithProgress, DownloadError, + sendProgress, + setProgress, } from '../download.helpers' import { isLinuxAccessError } from '../../../utils/appUtils/elevation' import type { DownloadProgress, ModDownloadFailure } from './types' import { - decompressArchive, ensureDir, - extractZipBuffer, - isReplaceDirFailure, + isCachedArchiveValid, + pruneCacheDirectories, pruneCacheFiles, - readCachedArchive, readUnpackedMarker, - resolveExtractedRoot, - sha256Hex, - tryReplaceDir, + sha256File, + UNPACKED_MARKER_FILE, writeUnpackedMarker, } from './helpers' +import { ArtifactWorkerError, hashArtifactInWorker, installUnpackedArtifactInWorker, prepareAsarArtifactInWorker } from './artifactWorkerClient' const USER_AGENT = () => `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) PulseSync/${app.getVersion()} Chrome/142.0.7444.59 Electron/39.1.1 Safari/537.36` +const NETWORK_PROGRESS_RATIO = 0.85 +const DERIVED_UNPACKED_DIRECTORY_SUFFIX = '.unpacked-dir' +const LEGACY_PREPARED_UNPACKED_SUFFIX = '.unpacked.zip' +const DERIVED_CACHE_RECOVERY_STAGES = new Set(['read', 'decompress', 'extract']) + +function reportArtifactProgress(window: BrowserWindow, fraction: number, name: string): void { + const boundedFraction = Math.min(Math.max(fraction, 0), 1) + setProgress(window, boundedFraction) + sendProgress(window, Math.round(boundedFraction * 100), name) +} + +async function isCachedUnpackedDirectoryValid(directoryPath: string, checksum: string): Promise { + try { + const stats = await fs.promises.stat(directoryPath) + return stats.isDirectory() && readUnpackedMarker(directoryPath) === checksum + } catch { + return false + } +} function reportFailure(window: BrowserWindow, failure: ModDownloadFailure, onFailure?: (failure: ModDownloadFailure) => void) { if (onFailure) { @@ -57,17 +75,18 @@ export async function downloadAndUpdateFile( name?: string, onFailure?: (failure: ModDownloadFailure) => void, ): Promise { + const preparedFilePath = `${tempFilePath}.prepared.${process.pid}.${Date.now()}.asar` + const progressBase = progress?.base ?? 0 + const progressScale = progress?.scale ?? 1 + const networkProgressScale = progressScale * NETWORK_PROGRESS_RATIO + const completedProgress = progressBase + progressScale + const artifactName = name ?? 'app.asar' try { if (checksum && fs.existsSync(savePath) && !isCompressedArchiveLink(link)) { - const buf = fs.readFileSync(savePath) - const currentHash = sha256Hex(buf) + const currentHash = (await hashArtifactInWorker({ filePath: savePath })).checksum if (currentHash === checksum) { logger.modManager.info('app.asar hash matches, skipping download') - sendToRenderer(window, RendererEvents.DOWNLOAD_SUCCESS, { - success: true, - message: t('main.modManager.modAlreadyInstalled'), - }) - resetProgress(window) + reportArtifactProgress(window, completedProgress, artifactName) return true } } @@ -78,14 +97,14 @@ export async function downloadAndUpdateFile( tempFilePath, expectedChecksum: checksum, userAgent: USER_AGENT(), - progressScale: progress?.scale ?? 1, - progressBase: progress?.base ?? 0, + progressScale: networkProgressScale, + progressBase, rejectUnauthorized: false, - name: name ?? 'app.asar', + name: artifactName, }) - const fileBuffer = fs.readFileSync(tempFilePath) - const ok = await writePatchedAsarAndPatchBundle(savePath, fileBuffer, link, backupPath, checksum) + reportArtifactProgress(window, progressBase + networkProgressScale, artifactName) + const ok = await prepareAndInstallAsarArtifact(tempFilePath, preparedFilePath, link, savePath, backupPath, checksum) if (checksum && cacheDir) { try { const cacheFile = path.join(cacheDir, `${checksum}.asar`) @@ -104,12 +123,14 @@ export async function downloadAndUpdateFile( return false } + reportArtifactProgress(window, completedProgress, artifactName) if (progress?.resetOnComplete ?? true) { resetProgress(window) } return true } catch (err: any) { unlinkIfExists(tempFilePath) + unlinkIfExists(preparedFilePath) restoreBackupIfExists(savePath, backupPath) logger.modManager.error('File download/install error:', err) logger.modManager.error('Error details:', { @@ -124,7 +145,10 @@ export async function downloadAndUpdateFile( return false } - if (err instanceof DownloadError && err.code === 'checksum_mismatch') { + if ( + (err instanceof DownloadError && err.code === 'checksum_mismatch') || + (err instanceof ArtifactWorkerError && err.code === 'CHECKSUM_MISMATCH') + ) { reportFailure(window, { error: t('main.modNetwork.integrityError'), type: 'checksum_mismatch' }, onFailure) } else { reportFailure(window, { error: err?.message || t('main.modDownload.networkError'), type: 'download_error' }, onFailure) @@ -133,6 +157,38 @@ export async function downloadAndUpdateFile( } } +export async function prepareAndInstallAsarArtifact( + archivePath: string, + preparedFilePath: string, + link: string, + savePath: string, + backupPath: string, + checksum?: string, +): Promise { + const extension = path.extname(new URL(link).pathname).toLowerCase() + const startedAt = Date.now() + + try { + const workerResult = await prepareAsarArtifactInWorker({ + archivePath, + archiveExtension: extension, + expectedChecksum: checksum, + outputPath: preparedFilePath, + }) + logger.modManager.info('Prepared app.asar in worker', { + totalMs: Date.now() - startedAt, + workerThreadId: workerResult.workerThreadId, + ...workerResult.durations, + }) + for (const warning of workerResult.warnings) { + logger.modManager.warn('ASAR worker warning:', warning) + } + return await installPreparedAsarAndPatchBundle(savePath, workerResult.preparedPath, backupPath) + } finally { + await fs.promises.rm(preparedFilePath, { force: true }).catch(() => {}) + } +} + export async function downloadAndExtractUnpacked( window: BrowserWindow, link: string, @@ -144,11 +200,19 @@ export async function downloadAndExtractUnpacked( progress?: DownloadProgress, onFailure?: (failure: ModDownloadFailure) => void, ): Promise { + const progressBase = progress?.base ?? 0 + const progressScale = progress?.scale ?? 1 + const networkProgressScale = progressScale * NETWORK_PROGRESS_RATIO + const completedProgress = progressBase + progressScale + const artifactName = 'app.asar.unpacked' + const preflightStartedAt = Date.now() try { + const markerStartedAt = Date.now() if (checksum && fs.existsSync(targetPath)) { const installed = readUnpackedMarker(targetPath) if (installed && installed === checksum) { logger.modManager.info('app.asar.unpacked hash matches, skipping') + reportArtifactProgress(window, completedProgress, artifactName) if (progress?.resetOnComplete ?? true) { resetProgress(window) } @@ -156,105 +220,250 @@ export async function downloadAndExtractUnpacked( } if (installed && installed !== checksum) { logger.modManager.info(`app.asar.unpacked hash mismatch, reinstalling`) - try { - fs.rmSync(targetPath, { recursive: true, force: true }) - } catch (e) { - logger.modManager.warn('Failed to remove old unpacked dir:', e) - } } } + const markerMs = Date.now() - markerStartedAt + const tempCleanupStartedAt = Date.now() unlinkIfExists(tempArchivePath) - fs.rmSync(tempExtractPath, { recursive: true, force: true }) + const tempCleanupMs = Date.now() - tempCleanupStartedAt const pathname = new URL(link).pathname const ext = path.extname(pathname) || '.zip' const extLower = ext.toLowerCase() - let rawArchive: Buffer | null = null - let cacheFile: string | null = null + let preparedCacheDirectory: string | null = null + let legacyPreparedCacheFile: string | null = null + let sourceKind: 'archive' | 'directory' = 'archive' + let archivePath = tempArchivePath + let archiveExtension = extLower + let archiveChecksum = checksum + let downloaded = false if (cacheDir) { + const cacheDirStartedAt = Date.now() await ensureDir(cacheDir) + const cacheDirMs = Date.now() - cacheDirStartedAt if (checksum) { cacheFile = path.join(cacheDir, `${checksum}${ext}`) - const cached = readCachedArchive(cacheFile, checksum) - if (cached) { - logger.modManager.info('Using cached unpacked archive') - rawArchive = cached + if (extLower !== '.zip') { + preparedCacheDirectory = path.join(cacheDir, `${checksum}${DERIVED_UNPACKED_DIRECTORY_SUFFIX}`) + legacyPreparedCacheFile = path.join(cacheDir, `${checksum}${LEGACY_PREPARED_UNPACKED_SUFFIX}`) + } + const cacheValidationStartedAt = Date.now() + const validPreparedCacheDirectory = + preparedCacheDirectory && (await isCachedUnpackedDirectoryValid(preparedCacheDirectory, checksum)) ? preparedCacheDirectory : null + if (validPreparedCacheDirectory) { + logger.modManager.info('Using cached extracted app.asar.unpacked', { + preflightMs: Date.now() - preflightStartedAt, + markerMs, + tempCleanupMs, + cacheDirMs, + cacheValidationMs: Date.now() - cacheValidationStartedAt, + }) + sourceKind = 'directory' + archivePath = validPreparedCacheDirectory + archiveExtension = '' + archiveChecksum = undefined + } else { + if (preparedCacheDirectory) { + await fs.promises.rm(preparedCacheDirectory, { recursive: true, force: true }).catch(() => {}) + } + + if (legacyPreparedCacheFile && (await isCachedArchiveValid(legacyPreparedCacheFile))) { + logger.modManager.info('Using legacy prepared unpacked ZIP', { + preflightMs: Date.now() - preflightStartedAt, + markerMs, + tempCleanupMs, + cacheDirMs, + cacheValidationMs: Date.now() - cacheValidationStartedAt, + }) + archivePath = legacyPreparedCacheFile + archiveExtension = '.zip' + archiveChecksum = undefined + } else if (await isCachedArchiveValid(cacheFile)) { + logger.modManager.info('Using cached unpacked archive', { + preflightMs: Date.now() - preflightStartedAt, + markerMs, + tempCleanupMs, + cacheDirMs, + cacheValidationMs: Date.now() - cacheValidationStartedAt, + }) + archivePath = cacheFile + } } } } - if (!rawArchive) { + const downloadArchive = async (): Promise => { await downloadToTempWithProgress({ window, url: link, tempFilePath: tempArchivePath, userAgent: USER_AGENT(), - progressScale: progress?.scale ?? 1, - progressBase: progress?.base ?? 0, + progressScale: networkProgressScale, + progressBase, rejectUnauthorized: false, expectedChecksum: checksum, - name: 'app.asar.unpacked', + name: artifactName, }) + downloaded = true + sourceKind = 'archive' + archivePath = tempArchivePath + archiveExtension = extLower + archiveChecksum = checksum + } - rawArchive = fs.readFileSync(tempArchivePath) - - if (cacheDir) { - try { - if (!cacheFile) { - const fileHash = sha256Hex(rawArchive) - cacheFile = path.join(cacheDir, `${fileHash}${ext}`) - } - - await copyFile(tempArchivePath, cacheFile) - await pruneCacheFiles(cacheDir, cacheFile, file => file.toLowerCase().endsWith(extLower), 'Failed to remove old unpacked cache:') - } catch (e: any) { - logger.modManager.warn('Failed to cache unpacked archive:', e) - } - } + if (archivePath === tempArchivePath) { + await downloadArchive() } - const zipBuffer = await decompressArchive(rawArchive as Buffer, extLower) + reportArtifactProgress(window, progressBase + networkProgressScale, artifactName) + const processArchive = () => + installUnpackedArtifactInWorker({ + sourceKind, + archivePath, + archiveExtension, + expectedChecksum: archiveChecksum, + preparedDirectoryPath: preparedCacheDirectory && sourceKind === 'archive' ? preparedCacheDirectory : undefined, + preparedDirectoryMarker: + preparedCacheDirectory && sourceKind === 'archive' && checksum ? { fileName: UNPACKED_MARKER_FILE, value: checksum } : undefined, + stagingPath: tempExtractPath, + targetPath, + }) - extractZipBuffer(zipBuffer, tempExtractPath) + let workerStartedAt = Date.now() + let workerResult + for (let attempt = 0; ; attempt++) { + try { + workerResult = await processArchive() + break + } catch (error) { + const canRecoverDirectoryCache = + attempt === 0 && + preparedCacheDirectory !== null && + sourceKind === 'directory' && + archivePath === preparedCacheDirectory && + error instanceof ArtifactWorkerError && + error.stage === 'extract' + if (canRecoverDirectoryCache) { + const invalidPreparedCacheDirectory = preparedCacheDirectory as string + logger.modManager.warn('Extracted unpacked cache is invalid, rebuilding:', error) + await fs.promises.rm(invalidPreparedCacheDirectory, { recursive: true, force: true }).catch(() => {}) + sourceKind = 'archive' + if (legacyPreparedCacheFile && (await isCachedArchiveValid(legacyPreparedCacheFile))) { + archivePath = legacyPreparedCacheFile + archiveExtension = '.zip' + archiveChecksum = undefined + } else if (cacheFile && (await isCachedArchiveValid(cacheFile))) { + archivePath = cacheFile + archiveExtension = extLower + archiveChecksum = checksum + } else { + await downloadArchive() + } + workerStartedAt = Date.now() + continue + } - const extractedRoot = resolveExtractedRoot(tempExtractPath, targetPath) + const canRecoverLegacyPreparedCache = + attempt <= 1 && + legacyPreparedCacheFile !== null && + archivePath === legacyPreparedCacheFile && + error instanceof ArtifactWorkerError && + DERIVED_CACHE_RECOVERY_STAGES.has(error.stage) + if (canRecoverLegacyPreparedCache) { + const invalidLegacyPreparedCacheFile = legacyPreparedCacheFile as string + logger.modManager.warn('Legacy prepared unpacked ZIP is invalid, rebuilding:', error) + await fs.promises.rm(invalidLegacyPreparedCacheFile, { force: true }).catch(() => {}) + if (cacheFile && (await isCachedArchiveValid(cacheFile))) { + sourceKind = 'archive' + archivePath = cacheFile + archiveExtension = extLower + archiveChecksum = checksum + } else { + await downloadArchive() + } + workerStartedAt = Date.now() + continue + } - fs.mkdirSync(path.dirname(targetPath), { recursive: true }) + const canRecoverSourceCache = + attempt <= 1 && + cacheFile !== null && + archivePath === cacheFile && + error instanceof ArtifactWorkerError && + error.code === 'CHECKSUM_MISMATCH' + if (!canRecoverSourceCache) throw error - const moved = await tryReplaceDir(extractedRoot, targetPath, tempExtractPath) - if (isReplaceDirFailure(moved)) { - if (isLinuxAccessError(moved.error)) { - reportFailure(window, { error: t('main.modManager.linuxPermissionsRequired'), type: 'linux_permissions_required' }, onFailure) - return false + const invalidSourceCacheFile = cacheFile as string + logger.modManager.warn('Cached unpacked archive hash mismatch, redownloading') + await fs.promises.rm(invalidSourceCacheFile, { force: true }).catch(() => {}) + await downloadArchive() + workerStartedAt = Date.now() } - const messageKey = moved.stage === 'copy' ? 'main.modNetwork.unpackedCopyError' : 'main.modNetwork.unpackedMoveError' - logger.modManager.error('Failed to replace unpacked dir:', moved.error) - reportFailure(window, { error: moved.error?.message || t(messageKey), type: 'download_unpacked_error' }, onFailure) - return false + } + logger.modManager.info('Processed app.asar.unpacked in worker', { + totalMs: Date.now() - workerStartedAt, + workerThreadId: workerResult.workerThreadId, + ...workerResult.durations, + }) + for (const warning of workerResult.warnings) { + logger.modManager.warn('Artifact worker warning:', warning) } if (checksum) { writeUnpackedMarker(targetPath, checksum) } + if (downloaded && cacheDir) { + try { + if (!cacheFile) { + cacheFile = path.join(cacheDir, `${await sha256File(tempArchivePath)}${ext}`) + } + await copyFile(tempArchivePath, cacheFile) + await pruneCacheFiles(cacheDir, cacheFile, file => file.toLowerCase().endsWith(extLower), 'Failed to remove old unpacked cache:') + } catch (e: any) { + logger.modManager.warn('Failed to cache unpacked archive:', e) + } + } + + if (preparedCacheDirectory && cacheDir && checksum && (await isCachedUnpackedDirectoryValid(preparedCacheDirectory, checksum))) { + await pruneCacheDirectories( + cacheDir, + preparedCacheDirectory, + directory => directory.toLowerCase().endsWith(DERIVED_UNPACKED_DIRECTORY_SUFFIX), + 'Failed to remove old extracted unpacked cache:', + ) + if (legacyPreparedCacheFile) { + await fs.promises.rm(legacyPreparedCacheFile, { force: true }).catch(() => {}) + } + } + + reportArtifactProgress(window, completedProgress, artifactName) if (progress?.resetOnComplete ?? true) { resetProgress(window) } return true } catch (err: any) { logger.modManager.error('Failed to download/extract unpacked:', err) + HandleErrorsElectron.handleError('downloadAndExtractUnpacked', 'pipeline', 'catch', err) if (isLinuxAccessError(err)) { reportFailure(window, { error: t('main.modManager.linuxPermissionsRequired'), type: 'linux_permissions_required' }, onFailure) return false } + if (err instanceof ArtifactWorkerError) { + logger.modManager.error('Artifact worker failed:', { + stage: err.stage, + code: err.code, + message: err.message, + }) + } reportFailure(window, { error: err?.message || t('main.modNetwork.unpackedDownloadError'), type: 'download_unpacked_error' }, onFailure) return false } finally { unlinkIfExists(tempArchivePath) - fs.rmSync(tempExtractPath, { recursive: true, force: true }) + await fs.promises.rm(tempExtractPath, { recursive: true, force: true }).catch(() => {}) } } diff --git a/src/main/modules/mod/network/modCatalog.ts b/src/main/modules/mod/network/modCatalog.ts index fd0430cf..2b99e511 100644 --- a/src/main/modules/mod/network/modCatalog.ts +++ b/src/main/modules/mod/network/modCatalog.ts @@ -1,11 +1,9 @@ -import * as fs from 'original-fs' -import crypto from 'crypto' import { fetchBackendModReleases, type ModReleaseEntry } from './releaseCatalog' +import { hashArtifactInWorker } from './artifactWorkerClient' export type RemoteModInfo = Pick export type ResolvedInstallModMatch = { - incomingAsar: Buffer incomingChecksum: string matchedMod: RemoteModInfo | null } @@ -34,8 +32,7 @@ const findRemoteModByChecksum = async (checksum: string): Promise => { - const incomingAsar = await fs.promises.readFile(asarPath) - const incomingChecksum = crypto.createHash('sha256').update(incomingAsar).digest('hex') + const { checksum: incomingChecksum } = await hashArtifactInWorker({ filePath: asarPath }) const matchedMod = await findRemoteModByChecksum(incomingChecksum) - return { incomingAsar, incomingChecksum, matchedMod } + return { incomingChecksum, matchedMod } } diff --git a/src/main/modules/nativeModules/index.ts b/src/main/modules/nativeModules/index.ts index d47f7cac..e3130f3c 100644 --- a/src/main/modules/nativeModules/index.ts +++ b/src/main/modules/nativeModules/index.ts @@ -1,92 +1,36 @@ -import isAppDev from '../../utils/isAppDev' import path from 'path' -import fs from 'fs' -import logger from '../logger' import { HANDLE_EVENTS_FILENAME, HANDLE_EVENTS_SETTINGS_FILENAME } from '@common/addons/handleEvents' import { sendAddon, sendAddonSettings, sendAllAddonSettings, sendExtensions } from '../httpServer' +import logger from '../logger' +import { loadPulseSyncNative } from './pulsesyncNative' -declare const __non_vite_require__: (moduleId: string) => any - -interface FileOperationsAddon { - watch(target: string, intervalMs: number, callback: (eventType: string, filename: string) => void): void - readFile(target: string): Buffer - deleteFile(target: string): void - renameFile(oldPath: string, newPath: string): void - moveFile(src: string, dest: string): void - fileExists(target: string): boolean -} - -interface NativeModules { - fileOperations?: FileOperationsAddon - [addonName: string]: any -} - -const loadNativeModules = (): NativeModules => { - const baseDir = isAppDev ? path.resolve(process.cwd(), 'nativeModules') : path.join(process.resourcesPath, 'modules') - - const modules: NativeModules = {} - - if (!fs.existsSync(baseDir)) { - return modules - } - - const scanDir = (dir: string) => { - fs.readdirSync(dir, { withFileTypes: true }).forEach(entry => { - const fullPath = path.join(dir, entry.name) - - if (entry.isDirectory()) { - scanDir(fullPath) - } else if (entry.isFile() && path.extname(entry.name).toLowerCase() === '.node') { - const relative = path.relative(baseDir, fullPath) - const parts = relative.split(path.sep) - const addonName = parts[0] - - logger.nativeModuleManager.info(`Native module found: ${relative}`) - try { - modules[addonName] = __non_vite_require__(fullPath) - logger.nativeModuleManager.info(`Loaded native module '${addonName}' from ${fullPath}`) - } catch (err) { - logger.nativeModuleManager.error(`Failed to load native module '${addonName}': ${err}`) - } - } - }) - } - - try { - scanDir(baseDir) - } catch (err) { - logger.nativeModuleManager.error(`Error scanning native modules directory: ${err}`) - } - - return modules +const nativeModule = loadPulseSyncNative() +if (nativeModule) { + logger.nativeModuleManager.info(`Loaded pulsesyncNative v${nativeModule.nativeVersion()}`) +} else { + logger.nativeModuleManager.error('pulsesyncNative addon was not found') } -const nativeModules = loadNativeModules() - const handleSettingsFilenames = new Set([HANDLE_EVENTS_FILENAME.toLowerCase(), HANDLE_EVENTS_SETTINGS_FILENAME.toLowerCase()]) const tryExtractAddonNameFromWatchPath = (filename: string): string | null => { if (!filename) return null const normalized = path.normalize(filename) - if (!handleSettingsFilenames.has(path.basename(normalized).toLowerCase())) { - return null - } + if (!handleSettingsFilenames.has(path.basename(normalized).toLowerCase())) return null const parts = normalized.split(/[\\/]+/).filter(Boolean) if (parts.length < 2) return null - return parts[parts.length - 2] || null } export function startThemeWatcher(themesPath: string, intervalMs: number = 1000): void { - const addon = nativeModules['fileOperations'] as FileOperationsAddon | undefined - if (!addon) { - logger.main.warn('fileOperations addon not loaded. startThemeWatcher will not watch files.') + if (!nativeModule) { + logger.main.warn('pulsesyncNative addon not loaded. startThemeWatcher will not watch files.') return } logger.main.info(`Starting native watcher on ${themesPath} with interval ${intervalMs}ms`) - addon.watch(themesPath, intervalMs, (eventType, filename) => { + nativeModule.watch(themesPath, intervalMs, (eventType, filename) => { const watchedAddonName = tryExtractAddonNameFromWatchPath(filename) if (watchedAddonName) { sendAddonSettings({ addonName: watchedAddonName, force: true }) @@ -119,14 +63,20 @@ export function startThemeWatcher(themesPath: string, intervalMs: number = 1000) }) } -export const nativeReadFile = (filePath: string): Buffer | null => { - const addon = nativeModules['fileOperations'] as FileOperationsAddon | undefined - if (!addon) { - logger.nativeModuleManager.warn('fileOperations addon not loaded. nativeReadFile will return null.') +export const nativeGetHardwareIdentity = (): { hash: string; source: string; algorithm: 'sha256' } | null => { + if (!nativeModule) return null + try { + return nativeModule.getHardwareIdentity() + } catch (err) { + logger.nativeModuleManager.error(`Error in nativeGetHardwareIdentity: ${err}`) return null } +} + +export const nativeReadFile = (filePath: string): Buffer | null => { + if (!nativeModule) return null try { - return addon.readFile(filePath) + return nativeModule.readFile(filePath) } catch (err) { logger.nativeModuleManager.error(`Error in nativeReadFile for '${filePath}': ${err}`) return null @@ -134,13 +84,9 @@ export const nativeReadFile = (filePath: string): Buffer | null => { } export const nativeDeleteFile = (filePath: string): boolean => { - const addon = nativeModules['fileOperations'] as FileOperationsAddon | undefined - if (!addon) { - logger.nativeModuleManager.warn('fileOperations addon not loaded. nativeDeleteFile will be a no-op.') - return false - } + if (!nativeModule) return false try { - addon.deleteFile(filePath) + nativeModule.deleteFile(filePath) return true } catch (err) { logger.nativeModuleManager.error(`Error in nativeDeleteFile for '${filePath}': ${err}`) @@ -149,13 +95,9 @@ export const nativeDeleteFile = (filePath: string): boolean => { } export const nativeRenameFile = (oldPath: string, newPath: string): boolean => { - const addon = nativeModules['fileOperations'] as FileOperationsAddon | undefined - if (!addon) { - logger.nativeModuleManager.warn('fileOperations addon not loaded. nativeRenameFile will be a no-op.') - return false - } + if (!nativeModule) return false try { - addon.renameFile(oldPath, newPath) + nativeModule.renameFile(oldPath, newPath) return true } catch (err) { logger.nativeModuleManager.error(`Error in nativeRenameFile from '${oldPath}' to '${newPath}': ${err}`) @@ -163,33 +105,56 @@ export const nativeRenameFile = (oldPath: string, newPath: string): boolean => { } } -export const nativeMoveFile = (src: string, dest: string): boolean => { - const addon = nativeModules['fileOperations'] as FileOperationsAddon | undefined - if (!addon) { - logger.nativeModuleManager.warn('fileOperations addon not loaded. nativeMoveFile will be a no-op.') +export const nativeMoveFile = (source: string, destination: string): boolean => { + if (!nativeModule) return false + try { + nativeModule.moveFile(source, destination) + return true + } catch (err) { + logger.nativeModuleManager.error(`Error in nativeMoveFile from '${source}' to '${destination}': ${err}`) return false } +} + +export const nativeCopyFile = (source: string, destination: string): boolean => { + if (!nativeModule) return false try { - addon.moveFile(src, dest) + nativeModule.copyFile(source, destination) return true } catch (err) { - logger.nativeModuleManager.error(`Error in nativeMoveFile from '${src}' to '${dest}': ${err}`) + logger.nativeModuleManager.error(`Error in nativeCopyFile from '${source}' to '${destination}': ${err}`) return false } } export const nativeFileExists = (filePath: string): boolean => { - const addon = nativeModules['fileOperations'] as FileOperationsAddon | undefined - if (!addon) { - logger.nativeModuleManager.warn('fileOperations addon not loaded. nativeFileExists will return false.') - return false - } + if (!nativeModule) return false try { - return addon.fileExists(filePath) + return nativeModule.fileExists(filePath) } catch (err) { logger.nativeModuleManager.error(`Error in nativeFileExists for '${filePath}': ${err}`) return false } } -export default nativeModules as NativeModules +export const nativeCalculateAsarHeaderHash = (filePath: string): string => { + if (!nativeModule) throw new Error('pulsesyncNative addon is not available') + return nativeModule.calculateAsarHeaderHash(filePath) +} + +export const nativePatchWindowsIntegrity = (exePath: string, asarPath: string): string => { + if (!nativeModule) throw new Error('pulsesyncNative addon is not available') + return nativeModule.patchWindowsIntegrity(exePath, asarPath) +} + +export const nativeReadAsarVersion = (filePath: string): string => { + if (!nativeModule) throw new Error('pulsesyncNative addon is not available') + return nativeModule.readAsarVersion(filePath) +} + +export const nativePatchMacIntegrity = (appBundlePath: string, asarPath: string, entitlementsPath: string): string => { + if (!nativeModule) throw new Error('pulsesyncNative addon is not available') + return nativeModule.patchMacIntegrity(appBundlePath, asarPath, entitlementsPath) +} + +export default { pulsesyncNative: nativeModule } diff --git a/src/main/modules/nativeModules/pulsesyncNative.ts b/src/main/modules/nativeModules/pulsesyncNative.ts new file mode 100644 index 00000000..11b719f2 --- /dev/null +++ b/src/main/modules/nativeModules/pulsesyncNative.ts @@ -0,0 +1,100 @@ +import fs from 'node:fs' +import path from 'node:path' + +declare const __non_vite_require__: (moduleId: string) => unknown + +export type NativeArtifactDurations = { + readMs: number + checksumMs: number + decompressMs: number + writeMs: number + cloneMs: number + extractMs: number + cacheWriteMs: number + backupMs: number + installMs: number + cleanupMs: number +} + +export type NativeArtifactWarning = { + stage: string + code?: string + message: string +} + +export type NativeArtifactResult = { + ok: boolean + stage?: string + code?: string + message?: string + preparedPath?: string + durations: NativeArtifactDurations + warnings: NativeArtifactWarning[] +} + +export type NativePrepareAsarArtifactRequest = { + archivePath: string + archiveExtension: string + expectedChecksum?: string + outputPath: string +} + +export type NativeInstallUnpackedArtifactRequest = { + sourceKind: 'archive' | 'directory' + archivePath: string + archiveExtension: string + expectedChecksum?: string + preparedDirectoryPath?: string + preparedDirectoryMarker?: { + fileName: string + value: string + } + stagingPath: string + targetPath: string +} + +export interface PulseSyncNativeAddon { + nativeVersion(): string + getHardwareIdentity(): { hash: string; source: string; algorithm: 'sha256' } | null + watch(target: string, intervalMs: number, callback: (eventType: string, filename: string) => void): void + readFile(target: string): Buffer + deleteFile(target: string): void + renameFile(oldPath: string, newPath: string): void + moveFile(source: string, destination: string): void + copyFile(source: string, destination: string): void + fileExists(target: string): boolean + hashFile(target: string): string + prepareAsarArtifact(request: NativePrepareAsarArtifactRequest): NativeArtifactResult + installUnpackedArtifact(request: NativeInstallUnpackedArtifactRequest): NativeArtifactResult + calculateAsarHeaderHash(target: string): string + readAsarVersion(target: string): string + patchWindowsIntegrity(exePath: string, asarPath: string): string + patchMacIntegrity(appBundlePath: string, asarPath: string, entitlementsPath: string): string +} + +let cachedAddon: PulseSyncNativeAddon | null | undefined + +function resolveNativeModulePath(): string | null { + const candidates = [path.resolve(process.cwd(), 'nativeModules', 'pulsesyncNative', 'build', 'Release', 'pulsesyncNative.node')] + if (typeof process.resourcesPath === 'string') { + candidates.push(path.join(process.resourcesPath, 'modules', 'pulsesyncNative', 'pulsesyncNative.node')) + } + return candidates.find(candidate => fs.existsSync(candidate)) ?? null +} + +export function loadPulseSyncNative(): PulseSyncNativeAddon | null { + if (cachedAddon !== undefined) return cachedAddon + const modulePath = resolveNativeModulePath() + if (!modulePath) { + cachedAddon = null + return null + } + cachedAddon = __non_vite_require__(modulePath) as PulseSyncNativeAddon + return cachedAddon +} + +export function requirePulseSyncNative(): PulseSyncNativeAddon { + const addon = loadPulseSyncNative() + if (!addon) throw new Error('pulsesyncNative addon is not available') + return addon +} diff --git a/src/main/modules/network/systemProxy.ts b/src/main/modules/network/systemProxy.ts index 2a8951fd..8dfcf69c 100644 --- a/src/main/modules/network/systemProxy.ts +++ b/src/main/modules/network/systemProxy.ts @@ -118,7 +118,13 @@ function rejectForStatus(response: TResponse, status: number, config: return response } - throw new AxiosError(`Request failed with status code ${status}`, status >= 500 ? AxiosError.ERR_BAD_RESPONSE : AxiosError.ERR_BAD_REQUEST, config, null, response as any) + throw new AxiosError( + `Request failed with status code ${status}`, + status >= 500 ? AxiosError.ERR_BAD_RESPONSE : AxiosError.ERR_BAD_REQUEST, + config, + null, + response as any, + ) } async function requestBuffer(options: ElectronRequestOptions): Promise { @@ -195,10 +201,21 @@ async function requestStream(options: ElectronRequestOptions): Promise stream.write(chunk)) - response.on('end', () => stream.end()) + response.on('end', () => { + responseEnded = true + stream.end() + }) response.on('aborted', () => stream.destroy(new Error('Response aborted'))) response.on('error', error => stream.destroy(error)) + stream.on('close', () => { + if (!responseEnded) { + try { + request.abort() + } catch {} + } + }) resolve({ data: stream, diff --git a/src/main/modules/pextImporter.ts b/src/main/modules/pextImporter.ts index 3d9702ad..c016109b 100644 --- a/src/main/modules/pextImporter.ts +++ b/src/main/modules/pextImporter.ts @@ -5,15 +5,16 @@ import fs from 'original-fs' import * as fsp from 'fs/promises' import { fileURLToPath } from 'node:url' import logger from './logger' -import { clearDirectory } from '../utils/appUtils' import { getState } from './state' import { HandleErrorsElectron } from './handlers/handleErrorsElectron' import { computeAddonPackageHash, resolveAddonDirectoryKey, resolveAddonPublicationFingerprint, resolveAddonStableId } from '../utils/addonIdentity' import { findAddonByPublicationFingerprint } from '../utils/addonRegistry' import { readPreservedAddonSettings, restorePreservedAddonSettings } from './addonSettingsPreservation' +import { getAddonsRoot } from '../utils/addonPaths' const State = getState() const SUPPORTED_ADDON_ARCHIVE_EXTENSIONS = new Set(['.pext', '.zip']) +const MAX_ADDON_ARCHIVE_BYTES = 100 * 1024 * 1024 type ImportAddonArchiveOptions = { installSource?: 'store' | 'local' storeAddonId?: string | null @@ -52,6 +53,79 @@ const removeSourcePextIfNeeded = async (filePath: string): Promise => { } } +const normalizeArchiveEntryName = (entryName: string): string | null => { + const rawName = String(entryName || '').replace(/\\/g, '/') + if (!rawName || rawName.includes('\0') || rawName.startsWith('/') || rawName.startsWith('//') || /^[a-zA-Z]:/.test(rawName)) { + return null + } + + const normalizedName = path.posix.normalize(rawName) + if ( + !normalizedName || + normalizedName === '.' || + normalizedName === '..' || + normalizedName.startsWith('../') || + path.posix.isAbsolute(normalizedName) + ) { + return null + } + + return normalizedName +} + +const validateAddonArchive = (zip: AdmZip): boolean => { + let hasRootMetadata = false + let totalUncompressedSize = 0 + + for (const entry of zip.getEntries()) { + const normalizedName = normalizeArchiveEntryName(entry.entryName) + if (!normalizedName) { + logger.main.warn(`Rejected addon archive with unsafe entry: ${entry.entryName}`) + return false + } + + if (!entry.isDirectory && normalizedName === 'metadata.json') { + hasRootMetadata = true + } + + totalUncompressedSize += Number((entry.header as { size?: number }).size) || 0 + if (totalUncompressedSize > MAX_ADDON_ARCHIVE_BYTES) { + logger.main.warn('Rejected addon archive because its unpacked size is too large') + return false + } + } + + if (!hasRootMetadata) { + logger.main.error('Missing metadata.json in addon archive') + return false + } + + return true +} + +const replaceAddonDirectory = async (stagingDir: string, outputDir: string): Promise => { + await fsp.mkdir(path.dirname(outputDir), { recursive: true }) + + if (!fs.existsSync(outputDir)) { + await fsp.rename(stagingDir, outputDir) + return + } + + const backupDir = path.join(path.dirname(outputDir), `.${path.basename(outputDir)}.backup-${Date.now()}-${process.pid}`) + await fsp.rename(outputDir, backupDir) + + try { + await fsp.rename(stagingDir, outputDir) + await fsp.rm(backupDir, { recursive: true, force: true }) + } catch (error) { + if (fs.existsSync(outputDir)) { + await fsp.rm(outputDir, { recursive: true, force: true }) + } + await fsp.rename(backupDir, outputDir) + throw error + } +} + export const importAddonArchive = async (rawPath: string, options: ImportAddonArchiveOptions = {}): Promise => { const filePath = normalizePextPath(rawPath) if (!isAddonArchivePath(filePath)) return null @@ -65,11 +139,21 @@ export const importAddonArchive = async (rawPath: string, options: ImportAddonAr try { const archiveBuffer = await fsp.readFile(filePath) + if (archiveBuffer.byteLength > MAX_ADDON_ARCHIVE_BYTES) { + logger.main.warn(`Rejected addon archive because it is too large: ${filePath}`) + return null + } + const zip = new AdmZip(filePath) + if (!validateAddonArchive(zip)) { + return null + } + tempDir = await fsp.mkdtemp(path.join(app.getPath('temp'), 'pext-import-')) - zip.extractAllTo(tempDir, true) + const stagingDir = path.join(tempDir, 'staging') + zip.extractAllTo(stagingDir, true) - const metadataPath = path.join(tempDir, 'metadata.json') + const metadataPath = path.join(stagingDir, 'metadata.json') if (!fs.existsSync(metadataPath)) { logger.main.error('Missing metadata.json in .pext archive') return null @@ -110,18 +194,12 @@ export const importAddonArchive = async (rawPath: string, options: ImportAddonAr resolveAddonDirectoryKey(metadata, metadata.id, { preferStoreId: metadata.installSource === 'store', }) - const outputDir = path.join(app.getPath('userData'), 'addons', addonDirectory) + const outputDir = path.join(getAddonsRoot(), addonDirectory) const preservedSettings = await readPreservedAddonSettings(outputDir) - if (fs.existsSync(outputDir)) { - await clearDirectory(outputDir) - } else { - await fsp.mkdir(outputDir, { recursive: true }) - } - - zip.extractAllTo(outputDir, true) - await restorePreservedAddonSettings(outputDir, preservedSettings) - fs.writeFileSync(path.join(outputDir, 'metadata.json'), JSON.stringify(metadata, null, 4)) + await restorePreservedAddonSettings(stagingDir, preservedSettings) + await fsp.writeFile(path.join(stagingDir, 'metadata.json'), JSON.stringify(metadata, null, 4), 'utf8') + await replaceAddonDirectory(stagingDir, outputDir) logger.main.info(`Extension imported successfully from ${ext} archive to ${outputDir}`) if (ext === '.pext') { diff --git a/src/main/modules/singleInstance.ts b/src/main/modules/singleInstance.ts index 8b58f829..aa52fcb1 100644 --- a/src/main/modules/singleInstance.ts +++ b/src/main/modules/singleInstance.ts @@ -1,7 +1,6 @@ import { app, BrowserWindow } from 'electron' import logger from './logger' import { prestartCheck } from '../../index' -import { handleUncaughtException } from './handlers/handleError' import { queueAddonOpen } from '../events' import { importPextFile, isPextFilePath, normalizePextPath } from './pextImporter' import { createDeeplinkCommandsHandler, findDeepLinkArg, navigateToDeeplink } from './handleDeeplinks' @@ -79,8 +78,6 @@ export const checkForSingleInstance = async (): Promise => { await handlePextFile(pextPath) } } - - handleUncaughtException() } else { logger.main.info('Another instance is already running, quitting this instance.') app.quit() diff --git a/src/main/modules/submodulesChecker.ts b/src/main/modules/submodulesChecker.ts index 833f73fb..4b9b8543 100644 --- a/src/main/modules/submodulesChecker.ts +++ b/src/main/modules/submodulesChecker.ts @@ -66,11 +66,7 @@ const buildExecutablePathMap = (entries: Array<[ExecutableTargetKey, string | nu return new Map(entries.filter((entry): entry is [ExecutableTargetKey, string] => typeof entry[1] === 'string')) } -const getInstallerBaseDir = ( - toolName: 'ffmpeg' | 'yt-dlp', - platform: SupportedPlatform, - context: Required, -): string | null => { +const getInstallerBaseDir = (toolName: 'ffmpeg' | 'yt-dlp', platform: SupportedPlatform, context: Required): string | null => { if (platform === 'linux') { return context.userDataPath ? path.join(context.userDataPath, toolName) : null } diff --git a/src/main/modules/telemetry/appTelemetry.ts b/src/main/modules/telemetry/appTelemetry.ts index dc48df09..b9d201b3 100644 --- a/src/main/modules/telemetry/appTelemetry.ts +++ b/src/main/modules/telemetry/appTelemetry.ts @@ -244,12 +244,7 @@ async function sendEvent(installId: string, event: string, apiKey: string): Prom await postTelemetry('/metrics/app', { ...buildBasePayload(installId), event }, apiKey) } -async function sendFeatureSnapshotIfChanged( - statePath: string, - state: TelemetryState, - installId: string, - apiKey: string, -): Promise { +async function sendFeatureSnapshotIfChanged(statePath: string, state: TelemetryState, installId: string, apiKey: string): Promise { const normalizedFeatures = normalizeBooleanFeatureTree(buildFeatureSnapshot()) if (!normalizedFeatures) { return state diff --git a/src/main/modules/updater/githubReleaseResolver.ts b/src/main/modules/updater/githubReleaseResolver.ts index 346fc936..1f892243 100644 --- a/src/main/modules/updater/githubReleaseResolver.ts +++ b/src/main/modules/updater/githubReleaseResolver.ts @@ -43,7 +43,9 @@ function getGithubRequestHeaders(): Record { } export function normalizeGitHubTagVersion(tagName: string): string { - return String(tagName || '').trim().replace(/^v(?=\d)/u, '') + return String(tagName || '') + .trim() + .replace(/^v(?=\d)/u, '') } export async function listGitHubReleases(repo: GitHubRepo, perPage = 20): Promise { diff --git a/src/main/utils/addonIdentity.ts b/src/main/utils/addonIdentity.ts index d300b700..f940fe3a 100644 --- a/src/main/utils/addonIdentity.ts +++ b/src/main/utils/addonIdentity.ts @@ -36,10 +36,7 @@ const normalizeAuthors = (value: unknown): string[] => { return author ? [author.toLowerCase()] : [] } -const normalizeAddonName = (value: unknown): string => - readText(value) - .toLowerCase() - .replace(/\s+/g, ' ') +const normalizeAddonName = (value: unknown): string => readText(value).toLowerCase().replace(/\s+/g, ' ') const normalizeAddonType = (value: unknown): string => readText(value).toLowerCase() @@ -108,11 +105,7 @@ export const resolveAddonCanonicalId = (source: AddonIdentitySource, fallbackId? return resolveAddonStableId(source, fallbackId) } -export const resolveAddonDirectoryKey = ( - source: AddonIdentitySource, - fallbackId?: string, - options?: { preferStoreId?: boolean }, -): string => { +export const resolveAddonDirectoryKey = (source: AddonIdentitySource, fallbackId?: string, options?: { preferStoreId?: boolean }): string => { if (isDefaultAddon(source)) { return DEFAULT_ADDON_NAME } diff --git a/src/main/utils/addonPaths.ts b/src/main/utils/addonPaths.ts new file mode 100644 index 00000000..98f8f6bb --- /dev/null +++ b/src/main/utils/addonPaths.ts @@ -0,0 +1,86 @@ +import { app } from 'electron' +import * as fs from 'original-fs' +import path from 'path' + +export const getAddonsRoot = (): string => path.join(app.getPath('appData'), 'PulseSync', 'addons') + +export const isPathInsideBase = (baseDir: string, targetPath: string): boolean => { + const resolvedBase = path.resolve(baseDir) + const resolvedTarget = path.resolve(targetPath) + const relativePath = path.relative(resolvedBase, resolvedTarget) + + return relativePath === '' || (!!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)) +} + +export const resolvePathInsideBase = (baseDir: string, targetPath: string): string | null => { + const resolvedTarget = path.resolve(targetPath) + return isPathInsideBase(baseDir, resolvedTarget) ? resolvedTarget : null +} + +export const resolveRelativePathInsideBase = (baseDir: string, relativePath: string): string | null => { + const normalizedRelativePath = String(relativePath || '') + .trim() + .replace(/^["']|["']$/g, '') + if (!normalizedRelativePath || path.isAbsolute(normalizedRelativePath)) { + return null + } + + return resolvePathInsideBase(baseDir, path.join(baseDir, normalizedRelativePath)) +} + +export const resolveExistingPathInsideBase = (baseDir: string, targetPath: string): string | null => { + const resolvedTarget = resolvePathInsideBase(baseDir, targetPath) + if (!resolvedTarget || !fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isFile()) { + return null + } + + try { + const realBase = fs.realpathSync(baseDir) + const realTarget = fs.realpathSync(resolvedTarget) + if (!isPathInsideBase(realBase, realTarget)) { + return null + } + } catch { + return null + } + + return resolvedTarget +} + +export const resolveExistingFileInsideBase = (baseDir: string, relativePath: string): string | null => { + const targetPath = resolveRelativePathInsideBase(baseDir, relativePath) + if (!targetPath || !fs.existsSync(targetPath) || !fs.statSync(targetPath).isFile()) { + return null + } + + try { + const realBase = fs.realpathSync(baseDir) + const realTarget = fs.realpathSync(targetPath) + if (!isPathInsideBase(realBase, realTarget)) { + return null + } + } catch { + return null + } + + return targetPath +} + +export const resolveExistingDirectoryInsideBase = (baseDir: string, targetPath: string): string | null => { + const resolvedTarget = resolvePathInsideBase(baseDir, targetPath) + if (!resolvedTarget || !fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isDirectory()) { + return null + } + + try { + const realBase = fs.realpathSync(baseDir) + const realTarget = fs.realpathSync(resolvedTarget) + if (!isPathInsideBase(realBase, realTarget)) { + return null + } + } catch { + return null + } + + return resolvedTarget +} diff --git a/src/main/utils/addonRegistry.ts b/src/main/utils/addonRegistry.ts index e9cde538..a8d653d1 100644 --- a/src/main/utils/addonRegistry.ts +++ b/src/main/utils/addonRegistry.ts @@ -1,7 +1,7 @@ -import { app } from 'electron' import * as fs from 'original-fs' import * as path from 'path' import { resolveAddonPublicationFingerprint } from './addonIdentity' +import { getAddonsRoot } from './addonPaths' type AddonMetadataRecord = { author?: string | string[] @@ -14,12 +14,10 @@ type AddonMetadataRecord = { type?: string } -const getAddonRoot = () => path.join(app.getPath('appData'), 'PulseSync', 'addons') - const readText = (value: unknown): string => (typeof value === 'string' ? value.trim() : '') export const listAddonMetadata = (): AddonMetadataRecord[] => { - const addonsRoot = getAddonRoot() + const addonsRoot = getAddonsRoot() let folders: string[] = [] try { @@ -56,7 +54,7 @@ export const resolveAddonDirectory = (ref: unknown): string => { const raw = readText(ref) if (!raw) return '' - const directPath = path.join(getAddonRoot(), raw) + const directPath = path.join(getAddonsRoot(), raw) if (fs.existsSync(directPath) && fs.statSync(directPath).isDirectory()) { return raw } diff --git a/src/main/utils/addonSettingsMigration.ts b/src/main/utils/addonSettingsMigration.ts index 46e7b825..bcbbbc8e 100644 --- a/src/main/utils/addonSettingsMigration.ts +++ b/src/main/utils/addonSettingsMigration.ts @@ -26,7 +26,9 @@ export const migrateLegacyAddonSettings = async (addonsRoot: string): Promise> }> } + const parsed = JSON.parse(await fs.promises.readFile(schemaPath, 'utf8')) as { + sections?: Array<{ items?: Array> }> + } const values = collectAddonSettingsValuesFromConfig(parsed) if (!isNonEmptyObject(values)) { diff --git a/src/main/utils/addonUtils.ts b/src/main/utils/addonUtils.ts index 92a6372b..af903fa6 100644 --- a/src/main/utils/addonUtils.ts +++ b/src/main/utils/addonUtils.ts @@ -1,4 +1,3 @@ -import { app } from 'electron' import path from 'path' import * as fs from 'original-fs' import { getFolderSize, formatSizeUnits } from './appUtils' @@ -9,6 +8,7 @@ import { getState } from '../modules/state' import * as acorn from 'acorn' import { simple as walkSimple } from 'acorn-walk' import { resolveAddonCanonicalId, resolveAddonDirectoryKey, resolveAddonPublicationFingerprint, resolveAddonStableId } from './addonIdentity' +import { getAddonsRoot } from './addonPaths' const State = getState() const defaultAddon: Partial = { @@ -35,9 +35,7 @@ let loadAddonsInFlight: Promise | null = null const normalizeRelationValues = (value: unknown): string[] => { if (!Array.isArray(value)) return [] - return value - .map(entry => String(entry || '').trim()) - .filter(Boolean) + return value.map(entry => String(entry || '').trim()).filter(Boolean) } export function createDefaultAddonIfNotExists(themesFolderPath: string) { @@ -84,7 +82,7 @@ export function createDefaultAddonIfNotExists(themesFolderPath: string) { } async function loadAddonsInternal(): Promise { - const addonsFolderPath = path.join(app.getPath('appData'), 'PulseSync', 'addons') + const addonsFolderPath = getAddonsRoot() createDefaultAddonIfNotExists(addonsFolderPath) @@ -161,11 +159,9 @@ async function loadAddonsInternal(): Promise { metadata.installSource === 'store' || metadata.installSource === 'local' ? metadata.installSource : null const inferredLegacyStoreInstall = !!metadata.storeAddonId && - (currentFolder === String(metadata.storeAddonId).trim() || String(metadata.id || '').trim() === String(metadata.storeAddonId).trim()) - const resolvedInstallSource = - normalizedInstallSource || inferredLegacyStoreInstall - ? normalizedInstallSource || 'store' - : 'local' + (currentFolder === String(metadata.storeAddonId).trim() || + String(metadata.id || '').trim() === String(metadata.storeAddonId).trim()) + const resolvedInstallSource = normalizedInstallSource || inferredLegacyStoreInstall ? normalizedInstallSource || 'store' : 'local' if (metadata.installSource !== resolvedInstallSource) { metadata.installSource = resolvedInstallSource metadataChanged = true @@ -231,6 +227,7 @@ async function loadAddonsInternal(): Promise { } metadata.lastModified = diffString + metadata.lastModifiedAt = modificationDate.getTime() metadata.path = addonFolderPath metadata.size = formatSizeUnits(folderSize) metadata.directoryName = currentFolder @@ -290,13 +287,9 @@ async function loadAddonsInternal(): Promise { try { await fs.promises.rm(shadowedPath, { recursive: true, force: true }) - logger.main.info( - `Addons: removed duplicate ${reason} folder ${shadowedAddon.directoryName}. Keeping ${preferredAddon.directoryName}.`, - ) + logger.main.info(`Addons: removed duplicate ${reason} folder ${shadowedAddon.directoryName}. Keeping ${preferredAddon.directoryName}.`) } catch (error) { - logger.main.warn( - `Addons: failed to remove duplicate ${reason} folder ${shadowedAddon.directoryName}: ${String(error)}`, - ) + logger.main.warn(`Addons: failed to remove duplicate ${reason} folder ${shadowedAddon.directoryName}: ${String(error)}`) } } @@ -487,7 +480,7 @@ async function loadAddonsInternal(): Promise { return true } - const requestedTheme = selectedTheme !== 'Default' ? addonByDirectory.get(selectedTheme) ?? null : null + const requestedTheme = selectedTheme !== 'Default' ? (addonByDirectory.get(selectedTheme) ?? null) : null if (requestedTheme && !activateAddon(requestedTheme)) { selectedTheme = 'Default' } diff --git a/src/main/utils/appUtils/index.ts b/src/main/utils/appUtils/index.ts index 3d0079b9..e49a1bf7 100644 --- a/src/main/utils/appUtils/index.ts +++ b/src/main/utils/appUtils/index.ts @@ -2,25 +2,29 @@ import { exec, execFile, spawn, execSync } from 'child_process' import { promisify } from 'util' import os from 'os' import path from 'path' -import crypto from 'crypto' import fso, { promises as fsp } from 'original-fs' import { asarBackup, musicPath } from '../../../index' import { app, dialog, shell } from 'electron' import RendererEvents from '../../../common/types/rendererEvents' import axios from 'axios' -import * as plist from 'plist' import { mainWindow } from '../../modules/createWindow' import logger from '../../modules/logger' import { getState } from '../../modules/state' import { t } from '../../i18n' import * as yaml from 'yaml' import { YM_RELEASE_METADATA_URL } from '../../constants/urls' -import asar from '@electron/asar' -import { nativeFileExists } from '../../modules/nativeModules' +import { + nativeCopyFile, + nativeFileExists, + nativePatchMacIntegrity, + nativePatchWindowsIntegrity, + nativeReadAsarVersion, +} from '../../modules/nativeModules' import type { AppxPackage, PatchCallback, ProcessInfo } from './types' import { parseLinuxPgrep, parseMacPgrep, parseWindowsTasklist } from './process' import { isLinuxAccessError } from './elevation' import { runPowerShell } from './powershell' +import { HandleErrorsElectron } from '../../modules/handlers/handleErrorsElectron' const execAsync = promisify(exec) const execFileAsync = promisify(execFile) @@ -167,6 +171,7 @@ export function getYandexMusicLogsPath(): string { } export async function copyFile(target: string, dest: string): Promise { try { + if (nativeCopyFile(target, dest)) return await fsp.copyFile(target, dest) } catch (error: any) { if (isLinuxAccessError(error)) { @@ -434,28 +439,9 @@ export const downloadYandexMusic = async (type?: string) => { } } -export async function updateIntegrityHashInExe(exePath: string, newHash: string): Promise { +export async function updateIntegrityHashInExe(exePath: string, asarPath: string): Promise { try { - const rawBuf = await fsp.readFile(exePath) - const buf = rawBuf as Buffer - const marker = Buffer.from('"file":"resources\\\\app.asar"', 'utf8') - const markerIdx = buf.indexOf(marker) - if (markerIdx < 0) throw new Error(t('main.appUtils.rcdataJsonNotFound')) - const startIdx = buf.lastIndexOf(Buffer.from('[', 'utf8'), markerIdx) - if (startIdx < 0) throw new Error(t('main.appUtils.jsonArrayStartNotFound')) - const endIdx = buf.indexOf(Buffer.from(']', 'utf8'), markerIdx + marker.length) - if (endIdx < 0) throw new Error(t('main.appUtils.jsonArrayEndNotFound')) - const jsonBuf = buf.subarray(startIdx, endIdx + 1) - const arr = JSON.parse(jsonBuf.toString('utf8')) as Array<{ file: string; alg: string; value: string }> - const entry = arr.find(e => e.file.replace(/\\\\/g, '\\').toLowerCase() === 'resources\\app.asar') - if (!entry) throw new Error(t('main.appUtils.resourcesAsarNotFound')) - entry.value = newHash - const newJson = JSON.stringify(arr) - if (Buffer.byteLength(newJson, 'utf8') !== jsonBuf.length) { - throw new Error(t('main.appUtils.jsonLengthMismatch')) - } - Buffer.from(newJson, 'utf8').copy(buf, startIdx) - await fsp.writeFile(exePath, buf) + nativePatchWindowsIntegrity(exePath, asarPath) } catch (err) { logger.main.error(t('main.appUtils.updateIntegrityError'), err) await downloadYandexMusic('reinstall') @@ -466,7 +452,6 @@ export async function updateIntegrityHashInExe(exePath: string, newHash: string) export class AsarPatcher { private readonly appBundlePath: string private readonly resourcesDir: string - private readonly infoPlistPath: string private readonly asarRelPath = 'app.asar' private readonly asarPath: string private readonly tmpEntitlements: string @@ -474,7 +459,6 @@ export class AsarPatcher { constructor(appBundlePath: string) { this.appBundlePath = appBundlePath this.resourcesDir = path.join(appBundlePath, 'Contents', 'Resources') - this.infoPlistPath = path.join(appBundlePath, 'Contents', 'Info.plist') this.asarPath = path.join(this.resourcesDir, this.asarRelPath) this.tmpEntitlements = path.join(os.tmpdir(), 'extracted_entitlements.xml') } @@ -483,11 +467,6 @@ export class AsarPatcher { return os.platform() === 'darwin' } - private calcAsarHeaderHash(archivePath: string): string { - const headerString = asar.getRawHeader(archivePath).headerString - return crypto.createHash('sha256').update(headerString).digest('hex') - } - public async patch(callback?: PatchCallback): Promise { if (isLinux()) return true if (isWindows()) { @@ -500,11 +479,11 @@ export class AsarPatcher { try { callback?.(0, t('main.appUtils.readingExe')) const asarPathFull = path.join(localAppData, 'Programs', 'YandexMusic', 'resources', 'app.asar') - const newHash = this.calcAsarHeaderHash(asarPathFull) - await updateIntegrityHashInExe(exePath, newHash) + await updateIntegrityHashInExe(exePath, asarPathFull) callback?.(1, t('main.appUtils.windowsPatchSuccess')) return true } catch (err) { + HandleErrorsElectron.handleError('AsarPatcher', 'patch', 'windows_integrity', err) callback?.(0, t('main.appUtils.windowsPatchError', { message: (err as Error).message })) return false } @@ -545,29 +524,16 @@ export class AsarPatcher { } try { - const raw = await fsp.readFile(this.infoPlistPath, 'utf8') - const data = plist.parse(raw) as any - - if (data.ElectronAsarIntegrity && data.ElectronAsarIntegrity['Resources/app.asar']) { - callback?.(0.2, t('main.appUtils.updatingInfoPlistHash')) - data.ElectronAsarIntegrity['Resources/app.asar'].hash = this.calcAsarHeaderHash(this.asarPath) - await fsp.writeFile(this.infoPlistPath, plist.build(data), 'utf8') - callback?.(0.5, t('main.appUtils.hashUpdated')) - } - - callback?.(0.6, t('main.appUtils.dumpingEntitlements')) - execSync(`codesign -d --entitlements :- '${this.appBundlePath}' > '${this.tmpEntitlements}'`, { stdio: 'ignore' }) - - callback?.(0.7, t('main.appUtils.reSigningApp')) - execSync(`codesign --force --entitlements '${this.tmpEntitlements}' --sign - '${this.appBundlePath}'`, { stdio: 'ignore' }) - await fsp.unlink(this.tmpEntitlements) - + callback?.(0.2, t('main.appUtils.updatingInfoPlistHash')) + nativePatchMacIntegrity(this.appBundlePath, this.asarPath, this.tmpEntitlements) callback?.(1, t('main.appUtils.macPatchSuccess')) return true } catch (err) { try { await fsp.unlink(this.tmpEntitlements) } catch {} + logger.main.error(t('main.appUtils.macPatchError', { message: (err as Error).message }), err) + HandleErrorsElectron.handleError('AsarPatcher', 'patch', 'mac_integrity', err) callback?.(0, t('main.appUtils.macPatchError', { message: (err as Error).message })) return false } @@ -634,15 +600,15 @@ export async function getInstalledYmMetadata() { logger.modManager.warn('getPathToYandexMusic returned empty path') return null } - const versionFilePath = path.join(ymDir, 'version.bin') - if (!nativeFileExists(versionFilePath)) { - logger.modManager.warn('version file not found in Yandex Music directory') + const asarPath = path.join(ymDir, 'app.asar') + if (!nativeFileExists(asarPath)) { + logger.modManager.warn('app.asar not found in Yandex Music directory') return null } - const version = (await fso.promises.readFile(versionFilePath, 'utf8')).trim() + const version = nativeReadAsarVersion(asarPath) return { version } } catch (error) { - logger.modManager.error('Error reading version file:', error) + logger.modManager.warn('Error reading Yandex Music version from app.asar:', error) return null } } diff --git a/src/renderer/app/AppShell.tsx b/src/renderer/app/AppShell.tsx index d6688d34..17b588f1 100644 --- a/src/renderer/app/AppShell.tsx +++ b/src/renderer/app/AppShell.tsx @@ -200,107 +200,108 @@ function App() { } }, []) - const syncStoreAddonUpdates = useCallback(async (installedAddons: Addon[]) => { - if (isAutonomousMode) { - return - } - - const storeInstalledAddons = installedAddons.filter(addon => addon.installSource === 'store' && addon.storeAddonId) - if (!storeInstalledAddons.length || !window.desktopEvents || storeAddonUpdateCheckInFlightRef.current) { - return - } - - storeAddonUpdateCheckInFlightRef.current = true - - try { - const updates = await fetchStoreAddonUpdates(storeInstalledAddons.map(addon => addon.storeAddonId || '')) - const installedByStoreId = new Map(storeInstalledAddons.map(addon => [addon.storeAddonId!, addon])) - const outdatedAddons = updates.filter(publishedAddon => { - const installedAddon = installedByStoreId.get(publishedAddon.id) - return ( - !!installedAddon && - !!publishedAddon.currentRelease?.downloadUrl && - compareVersions(publishedAddon.currentRelease.version, installedAddon.version) > 0 - ) - }) - - if (!outdatedAddons.length) { + const syncStoreAddonUpdates = useCallback( + async (installedAddons: Addon[]) => { + if (isAutonomousMode) { return } - const canAutoUpdate = appRef.current.settings.autoUpdateStoreAddons !== false - const musicRunning = canAutoUpdate - ? Boolean(await window.desktopEvents.invoke(MainEvents.GET_MUSIC_RUNNING_STATUS)) - : true + const storeInstalledAddons = installedAddons.filter(addon => addon.installSource === 'store' && addon.storeAddonId) + if (!storeInstalledAddons.length || !window.desktopEvents || storeAddonUpdateCheckInFlightRef.current) { + return + } - let hasInstalledUpdates = false + storeAddonUpdateCheckInFlightRef.current = true + + try { + const updates = await fetchStoreAddonUpdates(storeInstalledAddons.map(addon => addon.storeAddonId || '')) + const installedByStoreId = new Map(storeInstalledAddons.map(addon => [addon.storeAddonId!, addon])) + const outdatedAddons = updates.filter(publishedAddon => { + const installedAddon = installedByStoreId.get(publishedAddon.id) + return ( + !!installedAddon && + !!publishedAddon.currentRelease?.downloadUrl && + compareVersions(publishedAddon.currentRelease.version, installedAddon.version) > 0 + ) + }) - for (const publishedAddon of outdatedAddons) { - const release = publishedAddon.currentRelease - const installedAddon = installedByStoreId.get(publishedAddon.id) - if (!release?.downloadUrl || !installedAddon) { - continue + if (!outdatedAddons.length) { + return } - const notificationKey = `lastNotifiedStoreAddonVersion:${publishedAddon.id}` + const canAutoUpdate = appRef.current.settings.autoUpdateStoreAddons !== false + const musicRunning = canAutoUpdate ? Boolean(await window.desktopEvents.invoke(MainEvents.GET_MUSIC_RUNNING_STATUS)) : true + + let hasInstalledUpdates = false - if (!musicRunning && canAutoUpdate) { - if (autoUpdatingStoreAddonIdsRef.current.has(publishedAddon.id)) { + for (const publishedAddon of outdatedAddons) { + const release = publishedAddon.currentRelease + const installedAddon = installedByStoreId.get(publishedAddon.id) + if (!release?.downloadUrl || !installedAddon) { continue } - autoUpdatingStoreAddonIdsRef.current.add(publishedAddon.id) - try { - const result = await window.desktopEvents.invoke(MainEvents.INSTALL_STORE_ADDON, { - id: publishedAddon.id, - downloadUrl: release.downloadUrl, - title: publishedAddon.name, - }) + const notificationKey = `lastNotifiedStoreAddonVersion:${publishedAddon.id}` - if (!result?.success) { - throw new Error(result?.reason || 'STORE_ADDON_AUTO_UPDATE_FAILED') + if (!musicRunning && canAutoUpdate) { + if (autoUpdatingStoreAddonIdsRef.current.has(publishedAddon.id)) { + continue } - const title = tRef.current('common.doneTitle') - const body = tRef.current('extensions.storeUpdateComplete', { name: publishedAddon.name }) - window.desktopEvents.send(MainEvents.SHOW_NOTIFICATION, { title, body }) - toast.custom('success', title, body) - localStorage.setItem(notificationKey, release.version) - hasInstalledUpdates = true - } catch (error) { - console.error(`Failed to auto-update store addon "${publishedAddon.name}":`, error) - } finally { - autoUpdatingStoreAddonIdsRef.current.delete(publishedAddon.id) - } + autoUpdatingStoreAddonIdsRef.current.add(publishedAddon.id) + try { + const result = await window.desktopEvents.invoke(MainEvents.INSTALL_STORE_ADDON, { + id: publishedAddon.id, + downloadUrl: release.downloadUrl, + title: publishedAddon.name, + }) + + if (!result?.success) { + throw new Error(result?.reason || 'STORE_ADDON_AUTO_UPDATE_FAILED') + } + + const title = tRef.current('common.doneTitle') + const body = tRef.current('extensions.storeUpdateComplete', { name: publishedAddon.name }) + window.desktopEvents.send(MainEvents.SHOW_NOTIFICATION, { title, body }) + toast.custom('success', title, body) + localStorage.setItem(notificationKey, release.version) + hasInstalledUpdates = true + } catch (error) { + console.error(`Failed to auto-update store addon "${publishedAddon.name}":`, error) + } finally { + autoUpdatingStoreAddonIdsRef.current.delete(publishedAddon.id) + } - continue - } + continue + } - if (localStorage.getItem(notificationKey) === release.version) { - continue - } + if (localStorage.getItem(notificationKey) === release.version) { + continue + } - const title = tRef.current('extensions.storeUpdateAvailableTitle') - const body = tRef.current('extensions.storeUpdateAvailableMessage', { - name: publishedAddon.name, - version: release.version, - }) + const title = tRef.current('extensions.storeUpdateAvailableTitle') + const body = tRef.current('extensions.storeUpdateAvailableMessage', { + name: publishedAddon.name, + version: release.version, + }) - window.desktopEvents.send(MainEvents.SHOW_NOTIFICATION, { title, body }) - toast.custom('info', title, body) - localStorage.setItem(notificationKey, release.version) - } + window.desktopEvents.send(MainEvents.SHOW_NOTIFICATION, { title, body }) + toast.custom('info', title, body) + localStorage.setItem(notificationKey, release.version) + } - if (hasInstalledUpdates) { - const nextInstalledAddons = await window.desktopEvents.invoke(MainEvents.GET_ADDONS) - setAddons(Array.isArray(nextInstalledAddons) ? nextInstalledAddons : []) + if (hasInstalledUpdates) { + const nextInstalledAddons = await window.desktopEvents.invoke(MainEvents.GET_ADDONS) + setAddons(Array.isArray(nextInstalledAddons) ? nextInstalledAddons : []) + } + } catch (error) { + console.error('Failed to check store addon updates:', error) + } finally { + storeAddonUpdateCheckInFlightRef.current = false } - } catch (error) { - console.error('Failed to check store addon updates:', error) - } finally { - storeAddonUpdateCheckInFlightRef.current = false - } - }, [isAutonomousMode, setAddons]) + }, + [isAutonomousMode, setAddons], + ) const handleSocketAchievementsUpdate = useCallback( async (payload: unknown) => { diff --git a/src/renderer/app/errorTracking.ts b/src/renderer/app/errorTracking.ts new file mode 100644 index 00000000..72041fa9 --- /dev/null +++ b/src/renderer/app/errorTracking.ts @@ -0,0 +1,67 @@ +import * as Sentry from '@sentry/electron/renderer' + +import { + addErrorTrackingDebugIds, + addErrorTrackingRuntimeTags, + ERROR_TRACKING_BUILD_TAGS, + ERROR_TRACKING_DIST, + ERROR_TRACKING_ENABLED, + ERROR_TRACKING_ENVIRONMENT, + ERROR_TRACKING_RELEASE, + sanitizeErrorTrackingEvent, +} from '@common/errorTracking' + +let initialized = false + +export const initRendererErrorTracking = (): void => { + if (!ERROR_TRACKING_ENABLED || initialized) return + + try { + Sentry.init({ + release: ERROR_TRACKING_RELEASE, + dist: ERROR_TRACKING_DIST, + environment: ERROR_TRACKING_ENVIRONMENT, + dataCollection: { + userInfo: false, + }, + maxBreadcrumbs: 0, + tracesSampleRate: 0, + beforeSend: event => addErrorTrackingDebugIds(addErrorTrackingRuntimeTags(sanitizeErrorTrackingEvent(event))), + }) + Sentry.setTags({ + ...ERROR_TRACKING_BUILD_TAGS, + process: 'renderer', + platform: navigator.platform || 'unknown', + }) + initialized = true + } catch (error) { + console.warn('Failed to initialize error tracking:', error) + } +} + +export const setRendererErrorTrackingUser = (user?: { id?: string | null; email?: string | null } | null): void => { + if (!initialized) return + const id = user?.id?.trim() + if (!id || id === '-1') { + Sentry.setUser(null) + return + } + + const email = user?.email?.trim() + Sentry.setUser({ + id, + ...(email ? { email } : {}), + }) +} + +export const captureRendererException = (error: unknown, source: string): void => { + if (!initialized) return + try { + Sentry.withScope(scope => { + scope.setTag('source', source) + Sentry.captureException(error instanceof Error ? error : new Error(String(error))) + }) + } catch (captureError) { + console.warn('Failed to capture renderer error:', captureError) + } +} diff --git a/src/renderer/app/index.tsx b/src/renderer/app/index.tsx index 14aca74f..477d5bf4 100644 --- a/src/renderer/app/index.tsx +++ b/src/renderer/app/index.tsx @@ -2,14 +2,19 @@ import React from 'react' import ReactDOM from 'react-dom/client' import Modal from 'react-modal' import App from '@app/App' +import { captureRendererException, initRendererErrorTracking } from '@app/errorTracking' const rootElement = document.getElementById('root') if (!rootElement) { throw new Error('Root element not found') } +initRendererErrorTracking() + Modal.setAppElement('#root') -const root = ReactDOM.createRoot(rootElement) +const root = ReactDOM.createRoot(rootElement, { + onUncaughtError: error => captureRendererException(error, 'react_root'), +}) root.render( diff --git a/src/renderer/app/model/useAppAuthorization.ts b/src/renderer/app/model/useAppAuthorization.ts index 78bd583d..0a9ca5f2 100644 --- a/src/renderer/app/model/useAppAuthorization.ts +++ b/src/renderer/app/model/useAppAuthorization.ts @@ -12,6 +12,7 @@ import getUserToken from '@shared/lib/auth/getUserToken' import config from '@common/appConfig' import { checkInternetAccess, notifyUserRetries } from '@shared/lib/utils' import type { GetMeData, GetMeVars } from '@app/AppShell.types' +import { setRendererErrorTrackingUser } from '@app/errorTracking' type Params = { router: { @@ -72,6 +73,7 @@ export function useAppAuthorization({ router, setIsAppDeprecated, setLoading, se const sendAuthStatus = useCallback((user?: Partial | null) => { if (user?.id) { + setRendererErrorTrackingUser({ id: user.id, email: user.email }) window.desktopEvents?.send(MainEvents.AUTH_STATUS, { status: true, user: { @@ -83,6 +85,7 @@ export function useAppAuthorization({ router, setIsAppDeprecated, setLoading, se return } + setRendererErrorTrackingUser(null) window.desktopEvents?.send(MainEvents.AUTH_STATUS, { status: false }) }, []) diff --git a/src/renderer/app/model/useAppInitialization.ts b/src/renderer/app/model/useAppInitialization.ts index 90641eb6..281ccd3e 100644 --- a/src/renderer/app/model/useAppInitialization.ts +++ b/src/renderer/app/model/useAppInitialization.ts @@ -55,7 +55,7 @@ export function useAppInitialization({ window.desktopEvents?.invoke(MainEvents.GET_MUSIC_VERSION), window.desktopEvents?.invoke(MainEvents.GET_ADDONS), ]) - const resolvedMusicVersion = userId === '-1' ? config.AUTONOMOUS_MUSIC_VERSION : ((musicVersion as string | null | undefined) || null) + const resolvedMusicVersion = userId === '-1' ? config.AUTONOMOUS_MUSIC_VERSION : (musicVersion as string | null | undefined) || null setMusicInstalled(!!musicStatus) setMusicVersion(resolvedMusicVersion) @@ -85,9 +85,12 @@ export function useAppInitialization({ void initializeApp() - const modCheckId = setInterval(() => { - void fetchModInfo(appRef.current) - }, 10 * 60 * 1000) + const modCheckId = setInterval( + () => { + void fetchModInfo(appRef.current) + }, + 10 * 60 * 1000, + ) return () => { clearInterval(modCheckId) diff --git a/src/renderer/app/providers/experiments/constants.ts b/src/renderer/app/providers/experiments/constants.ts index c293f7dc..86a333ce 100644 --- a/src/renderer/app/providers/experiments/constants.ts +++ b/src/renderer/app/providers/experiments/constants.ts @@ -6,8 +6,7 @@ export const CLIENT_EXPERIMENTS = { ClientUsersPageAccess: 'ClientUsersPageAccess', ClientTrackSending: 'ClientTrackSending', ClientMetricsSending: 'ClientMetricsSending', - WebLocalizationContribution: 'WebLocalizationContribution', - WebHomeSections: 'WebHomeSections', + WebSubscriptionsPage: 'WebSubscriptionsPage', } as const export const KNOWN_CLIENT_EXPERIMENT_KEYS = Object.values(CLIENT_EXPERIMENTS) diff --git a/src/renderer/app/providers/experiments/index.tsx b/src/renderer/app/providers/experiments/index.tsx index b33785d5..a52a2a93 100644 --- a/src/renderer/app/providers/experiments/index.tsx +++ b/src/renderer/app/providers/experiments/index.tsx @@ -1,12 +1,7 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { fetchExperiments } from '@entities/experiment/api/experiments' import type { ClientExperimentKey } from '@app/providers/experiments/constants' -import type { - DesktopExperiment, - ExperimentOverrideMap, - ExperimentsContextValue, - ExperimentsProviderProps, -} from '@app/providers/experiments/types' +import type { DesktopExperiment, ExperimentOverrideMap, ExperimentsContextValue, ExperimentsProviderProps } from '@app/providers/experiments/types' const STORAGE_KEY = 'pulsesync.desktop.experimentOverrides.v2' diff --git a/src/renderer/app/providers/notifications/presentation.ts b/src/renderer/app/providers/notifications/presentation.ts index feec6928..50fafce1 100644 --- a/src/renderer/app/providers/notifications/presentation.ts +++ b/src/renderer/app/providers/notifications/presentation.ts @@ -9,6 +9,20 @@ export type NotificationPresentation = { tone: NotificationTone } +function formatPayloadDate(value: unknown): string { + const rawValue = typeof value === 'string' ? value : '' + const date = new Date(rawValue) + if (Number.isNaN(date.getTime())) { + return t('header.notifications.items.subscriptionFallbackDate') + } + + return new Intl.DateTimeFormat(undefined, { + day: '2-digit', + month: 'short', + year: 'numeric', + }).format(date) +} + export function getNotificationPresentation(notification: NotificationItem): NotificationPresentation { switch (notification.type) { case 'addon.review.pending': @@ -88,6 +102,50 @@ export function getNotificationPresentation(notification: NotificationItem): Not } } + case 'subscription.giveaway.started': { + const giveawayTitle = String(notification.payload?.['giveawayTitle'] || t('header.notifications.items.giveawayFallbackTitle')) + return { + tone: 'success', + title: t('header.notifications.items.giveawayStartedTitle'), + body: t('header.notifications.items.giveawayStartedBody', { + giveaway: giveawayTitle, + date: formatPayloadDate(notification.payload?.['endsAt']), + }), + } + } + + case 'subscription.purchase.succeeded': { + const planName = String( + notification.payload?.['planName'] || + notification.payload?.['subscriptionName'] || + t('header.notifications.items.subscriptionFallbackPlan'), + ) + return { + tone: 'success', + title: t('header.notifications.items.subscriptionPurchaseSucceededTitle'), + body: t('header.notifications.items.subscriptionPurchaseSucceededBody', { + plan: planName, + date: formatPayloadDate(notification.payload?.['expireAt']), + }), + } + } + + case 'subscription.expiring.soon': { + const planName = String( + notification.payload?.['subscriptionName'] || + notification.payload?.['planName'] || + t('header.notifications.items.subscriptionFallbackPlan'), + ) + return { + tone: 'warning', + title: t('header.notifications.items.subscriptionExpiringSoonTitle'), + body: t('header.notifications.items.subscriptionExpiringSoonBody', { + plan: planName, + date: formatPayloadDate(notification.payload?.['expireAt']), + }), + } + } + default: return { tone: 'warning', diff --git a/src/renderer/app/providers/notifications/useNotificationsController.ts b/src/renderer/app/providers/notifications/useNotificationsController.ts index 9a0fc155..8594b0e2 100644 --- a/src/renderer/app/providers/notifications/useNotificationsController.ts +++ b/src/renderer/app/providers/notifications/useNotificationsController.ts @@ -43,6 +43,13 @@ type NotificationsControllerResult = { } const MAX_NOTIFICATIONS = 20 +const REALTIME_TOAST_NOTIFICATION_TYPES = new Set([ + 'achievement.completed', + 'subscription.giveaway.started', + 'subscription.giveaway.won', + 'subscription.purchase.succeeded', + 'subscription.expiring.soon', +]) function dedupeNotifications(items: NotificationItem[]): NotificationItem[] { const seen = new Set() @@ -143,7 +150,7 @@ export function useNotificationsController(userId: string): NotificationsControl setNotificationsUnreadCount(data.unreadCount) } - if ((data.notification.type === 'achievement.completed' || data.notification.type === 'subscription.giveaway.won') && !data.notification.read) { + if (REALTIME_TOAST_NOTIFICATION_TYPES.has(data.notification.type) && !data.notification.read) { const presentation = getNotificationPresentation(data.notification) toast.custom(presentation.tone, presentation.title, presentation.body) window.desktopEvents?.send(MainEvents.SHOW_NOTIFICATION, { diff --git a/src/renderer/app/providers/socket/gateway.ts b/src/renderer/app/providers/socket/gateway.ts index 1ad1a031..577c936b 100644 --- a/src/renderer/app/providers/socket/gateway.ts +++ b/src/renderer/app/providers/socket/gateway.ts @@ -59,6 +59,14 @@ export function createGatewayHandler({ body: t('auth.deprecatedSoon'), }) break + case IncomingGatewayEvents.HARDWARE_IDENTITY_WARNING: + console.debug('Gateway hardware identity warning', gatewayPayload) + toast.custom('error', t('common.attentionTitle'), t('auth.hardwareIdentityWarning'), undefined, undefined, 15000) + window.desktopEvents?.send(MainEvents.SHOW_NOTIFICATION, { + title: t('common.attentionTitle'), + body: t('auth.hardwareIdentityWarning'), + }) + break case IncomingGatewayEvents.ERROR_MESSAGE: { console.debug('Gateway error message', gatewayPayload) const message = getGatewayErrorMessage(gatewayPayload) diff --git a/src/renderer/app/providers/socket/index.tsx b/src/renderer/app/providers/socket/index.tsx index 77a6d98a..02e594bd 100644 --- a/src/renderer/app/providers/socket/index.tsx +++ b/src/renderer/app/providers/socket/index.tsx @@ -165,7 +165,18 @@ export function SocketProvider({ currentSocket.off(IncomingSocketEvents.GATEWAY, onGatewayMessage) currentSocket.io.off(IncomingSocketEvents.RECONNECT, resetSocketFailures) } - }, [onAchievementsUpdate, onAddonStoreUpdated, onLogout, onNotificationCreated, onNotificationRead, onNotificationsReadAll, setLoading, setUser, t, zstdReady]) + }, [ + onAchievementsUpdate, + onAddonStoreUpdated, + onLogout, + onNotificationCreated, + onNotificationRead, + onNotificationsReadAll, + setLoading, + setUser, + t, + zstdReady, + ]) useEffect(() => { if (userId === '-1' || !zstdReady) return diff --git a/src/renderer/app/providers/socket/utils.ts b/src/renderer/app/providers/socket/utils.ts index bd8f311d..8cdd3432 100644 --- a/src/renderer/app/providers/socket/utils.ts +++ b/src/renderer/app/providers/socket/utils.ts @@ -19,6 +19,8 @@ export function buildRealtimeSocketAuth(appVersion: string): RealtimeSocketAuth page, token: getUserToken(), version, + buildIdentity: window.appInfo?.getBuildIdentity?.(), + hardwareIdentity: window.appInfo?.getHardwareIdentity?.() ?? null, compression: 'zstd-stream', inboundCompression: 'zstd-stream', } diff --git a/src/renderer/app/router.tsx b/src/renderer/app/router.tsx index 6538555f..3dc20a69 100644 --- a/src/renderer/app/router.tsx +++ b/src/renderer/app/router.tsx @@ -64,14 +64,35 @@ export function createAppRouter() { return createHashRouter([ { path: '/', element: }, { path: '/home', element: withErrorBoundary() }, - { path: '/extensions', element: withErrorBoundary() }, + { + path: '/extensions', + element: withErrorBoundary( + + + , + ), + }, { path: '/auth', element: withErrorBoundary() }, { path: '/dev', element: withErrorBoundary() }, { path: '/auth/callback', element: withErrorBoundary() }, { path: '/users', element: withErrorBoundary() }, - { path: '/:contactId', element: withErrorBoundary() }, + { + path: '/:contactId', + element: withErrorBoundary( + + + , + ), + }, { path: '/store', element: withErrorBoundary() }, { path: '/joint', element: withErrorBoundary() }, - { path: '/profile/:profileName', element: withErrorBoundary() }, + { + path: '/profile/:profileName', + element: withErrorBoundary( + + + , + ), + }, ]) } diff --git a/src/renderer/entities/addon/api/getStoreAddons.query.ts b/src/renderer/entities/addon/api/getStoreAddons.query.ts index 7b55db4e..f1dccdf0 100644 --- a/src/renderer/entities/addon/api/getStoreAddons.query.ts +++ b/src/renderer/entities/addon/api/getStoreAddons.query.ts @@ -1,14 +1,7 @@ import gql from 'graphql-tag' export default gql` - query GetStoreAddons( - $page: Int = 1 - $pageSize: Int = 30 - $search: String - $type: String - $sortBy: String - $sortOrder: String - ) { + query GetStoreAddons($page: Int = 1, $pageSize: Int = 30, $search: String, $type: String, $sortBy: String, $sortOrder: String) { getStoreAddons(page: $page, pageSize: $pageSize, search: $search, type: $type, sortBy: $sortBy, sortOrder: $sortOrder) { totalCount totalPages diff --git a/src/renderer/entities/addon/lib/storeAddonMetrics.ts b/src/renderer/entities/addon/lib/storeAddonMetrics.ts index 32790716..e62a4be7 100644 --- a/src/renderer/entities/addon/lib/storeAddonMetrics.ts +++ b/src/renderer/entities/addon/lib/storeAddonMetrics.ts @@ -6,11 +6,7 @@ export interface StoreAddonMetric { } export function buildStoreAddonMetrics(addons: Addon[], currentTheme: string, enabledScripts: string[]): StoreAddonMetric[] { - const enabledScriptsSet = new Set( - enabledScripts - .map(script => String(script || '').trim()) - .filter(Boolean), - ) + const enabledScriptsSet = new Set(enabledScripts.map(script => String(script || '').trim()).filter(Boolean)) const metricsMap = new Map() for (const addon of addons) { diff --git a/src/renderer/entities/addon/model/addon.initials.ts b/src/renderer/entities/addon/model/addon.initials.ts index 651b83e6..f4a5354f 100644 --- a/src/renderer/entities/addon/model/addon.initials.ts +++ b/src/renderer/entities/addon/model/addon.initials.ts @@ -15,6 +15,7 @@ const AddonInitials: Addon[] = [ version: '1.0.0', path: 'local', lastModified: '0', + lastModifiedAt: 0, size: '0', tags: [], type: 'theme', diff --git a/src/renderer/entities/addon/model/addon.interface.ts b/src/renderer/entities/addon/model/addon.interface.ts index 6bfc9bfb..328a248c 100644 --- a/src/renderer/entities/addon/model/addon.interface.ts +++ b/src/renderer/entities/addon/model/addon.interface.ts @@ -15,6 +15,7 @@ export default interface Addon { path: string lastModified: string + lastModifiedAt?: number size: string type: 'theme' | 'script' diff --git a/src/renderer/entities/user/ui/userCardV2/index.tsx b/src/renderer/entities/user/ui/userCardV2/index.tsx index b0c7e575..79f39a5d 100644 --- a/src/renderer/entities/user/ui/userCardV2/index.tsx +++ b/src/renderer/entities/user/ui/userCardV2/index.tsx @@ -123,9 +123,7 @@ const UserCardV2: React.FC = ({ user, onClick, animationsEnabledR className={cn( styles.container, !visibilityState.shouldAnimate && styles.softFadeIn, - visibilityState.shouldAnimate && - visibilityState.entryDirection === 'up' && - styles.enterFromTop, + visibilityState.shouldAnimate && visibilityState.entryDirection === 'up' && styles.enterFromTop, )} onClick={() => onClick(profileSlug)} onMouseEnter={() => setIsHovered(true)} diff --git a/src/renderer/features/configurationSettings/model/useConfigurationEditor.ts b/src/renderer/features/configurationSettings/model/useConfigurationEditor.ts index 11e09532..1b2276d3 100644 --- a/src/renderer/features/configurationSettings/model/useConfigurationEditor.ts +++ b/src/renderer/features/configurationSettings/model/useConfigurationEditor.ts @@ -306,7 +306,12 @@ export function useConfigurationEditor({ addMenuClassName, configData, onChange, return item.defaultValue !== (base as ButtonItem).defaultValue case 'slider': { const baseSlider = base as SliderItem - return item.min !== baseSlider.min || item.max !== baseSlider.max || item.step !== baseSlider.step || item.defaultValue !== baseSlider.defaultValue + return ( + item.min !== baseSlider.min || + item.max !== baseSlider.max || + item.step !== baseSlider.step || + item.defaultValue !== baseSlider.defaultValue + ) } case 'color': return item.defaultValue !== (base as ColorItem).defaultValue diff --git a/src/renderer/features/context_menu/index.tsx b/src/renderer/features/context_menu/index.tsx index c7772c17..03d91652 100644 --- a/src/renderer/features/context_menu/index.tsx +++ b/src/renderer/features/context_menu/index.tsx @@ -8,6 +8,7 @@ import RendererEvents from '@common/types/rendererEvents' import toast from '@shared/ui/toast' import SettingsInterface from '@entities/settings/model/settings.interface' import { useModalContext } from '@app/providers/modal' +import { CLIENT_EXPERIMENTS, useExperiments } from '@app/providers/experiments' import { useTranslation } from 'react-i18next' import { buildContextMenuSections, renderContextMenuSections } from '@features/context_menu/model/contextMenuSections' import config from '@common/appConfig' @@ -26,9 +27,11 @@ const ContextMenu: React.FC = ({ modalRef }) => { const { t, i18n } = useTranslation() const { app, setApp, widgetInstalled, setWidgetInstalled, isAutonomousMode } = useContext(userContext) const { Modals, openModal } = useModalContext() + const { isExperimentEnabled } = useExperiments() const widgetDownloadToastIdRef = useRef(null) const [updateSource, setUpdateSourceState] = React.useState('backend') const [updateStatus, setUpdateStatus] = React.useState('IDLE') + const subscriptionPageEnabled = isExperimentEnabled(CLIENT_EXPERIMENTS.WebSubscriptionsPage, false) const openUpdateModal = () => { modalRef.current?.openUpdateModal() @@ -42,8 +45,12 @@ const ContextMenu: React.FC = ({ modalRef }) => { window.desktopEvents?.send(MainEvents.OPEN_PATH, { action: 'appPath' }) } + const openSubscriptionPage = () => { + window.desktopEvents?.send(MainEvents.OPEN_EXTERNAL, `${config.WEBSITE_URL}/subscription`) + } + const openBoostyUrl = () => { - window.open(config.BOOSTY_URL) + window.desktopEvents?.send(MainEvents.OPEN_EXTERNAL, config.BOOSTY_URL) } const canResetAsarPath = window.electron.isLinux() && Boolean(window.electron.store.get('settings.modSavePath')) @@ -369,16 +376,16 @@ const ContextMenu: React.FC = ({ modalRef }) => { const response = (await window.desktopEvents?.invoke(MainEvents.SET_UPDATE_SOURCE, nextSource)) as { source?: UpdateSource } | undefined const appliedSource = response?.source || nextSource setUpdateSourceState(appliedSource) - toast.custom('success', t('common.doneTitle'), t('contextMenu.updates.sourceChanged', { source: t(`contextMenu.updates.${appliedSource}`) })) + toast.custom( + 'success', + t('common.doneTitle'), + t('contextMenu.updates.sourceChanged', { source: t(`contextMenu.updates.${appliedSource}`) }), + ) window.desktopEvents?.send(MainEvents.CHECK_UPDATE, { manual: true }) void window.getModInfo(app, { silentNotInstalled: true }) } catch (error: any) { const isBusy = error instanceof Error && error.message === 'UPDATE_SOURCE_BUSY' - toast.custom( - 'error', - t('common.errorTitle'), - isBusy ? t('contextMenu.updates.busy') : t('contextMenu.updates.sourceChangeError'), - ) + toast.custom('error', t('common.errorTitle'), isBusy ? t('contextMenu.updates.busy') : t('contextMenu.updates.sourceChangeError')) } } @@ -395,6 +402,8 @@ const ContextMenu: React.FC = ({ modalRef }) => { isAutonomousMode, openAppDirectory, openBoostyUrl, + openSubscriptionPage, + subscriptionPageEnabled, openUpdateChannelModal, openModal, openUpdateModal, diff --git a/src/renderer/features/context_menu/model/contextMenuSections.tsx b/src/renderer/features/context_menu/model/contextMenuSections.tsx index 4dc74123..77957167 100644 --- a/src/renderer/features/context_menu/model/contextMenuSections.tsx +++ b/src/renderer/features/context_menu/model/contextMenuSections.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { MdWorkspacePremium } from 'react-icons/md' import { SiBoosty } from 'react-icons/si' import MainEvents from '@common/types/mainEvents' @@ -67,6 +68,8 @@ type Params = { isAutonomousMode: boolean openAppDirectory: () => void openBoostyUrl: () => void + openSubscriptionPage: () => void + subscriptionPageEnabled: boolean openUpdateChannelModal: () => void openModal: (modal: ModalName) => void openUpdateModal: () => void @@ -97,6 +100,8 @@ export function buildContextMenuSections({ isAutonomousMode, openAppDirectory, openBoostyUrl, + openSubscriptionPage, + subscriptionPageEnabled, openUpdateChannelModal, openModal, openUpdateModal, @@ -135,10 +140,17 @@ export function buildContextMenuSections({ return [ createContentSection( - , + subscriptionPageEnabled ? ( + + ) : ( + + ), ), createButtonSection(t('contextMenu.obsWidget.title'), [ { diff --git a/src/renderer/features/context_menu_themes/sectionConfig.tsx b/src/renderer/features/context_menu_themes/sectionConfig.tsx index 5e573527..07a41ebb 100644 --- a/src/renderer/features/context_menu_themes/sectionConfig.tsx +++ b/src/renderer/features/context_menu_themes/sectionConfig.tsx @@ -101,7 +101,10 @@ export const createContextMenuActions = ( const themeDirPath = currentAddon.path window.desktopEvents .invoke(MainEvents.DELETE_ADDON_DIRECTORY, themeDirPath) - .then(() => { + .then(result => { + if (!result?.success) { + throw new Error(result?.reason || 'DELETE_FAILED') + } window.refreshAddons() console.log(t('contextMenuThemes.deleteSuccess', { name: currentAddon.name })) }) @@ -109,7 +112,7 @@ export const createContextMenuActions = ( console.error(t('contextMenuThemes.deleteError', { name: currentAddon.name }), error) }) }, - isOpen: true + isOpen: true, }) }, show: actionVisibility.showDelete ?? false, diff --git a/src/renderer/pages/auth/index.tsx b/src/renderer/pages/auth/index.tsx index 8e8876ef..2543610c 100644 --- a/src/renderer/pages/auth/index.tsx +++ b/src/renderer/pages/auth/index.tsx @@ -5,12 +5,13 @@ import AuthDefault from '@pages/auth/default/Auth' import { getSeasonByMSK } from '@shared/lib/seasonDetector' export default function AuthPage() { - const AUTH_THEME = useMemo(() => getSeasonByMSK(), []) - if (AUTH_THEME === 'summer') { - return - } else if (AUTH_THEME === 'winter') { - return - } else { - return - } + return + // const AUTH_THEME = useMemo(() => getSeasonByMSK(), []) + // if (AUTH_THEME === 'summer') { + // return + // } else if (AUTH_THEME === 'winter') { + // return + // } else { + // return + // } } diff --git a/src/renderer/pages/extension/index.tsx b/src/renderer/pages/extension/index.tsx index 869ab577..14cfddaf 100644 --- a/src/renderer/pages/extension/index.tsx +++ b/src/renderer/pages/extension/index.tsx @@ -33,8 +33,6 @@ import EnableAddonModal from '@pages/extension/ui/EnableAddonModal' import ThemeNotFound from '@pages/extension/ui/ThemeNotFound' import * as extensionStylesV2 from '@pages/extension/extension.module.scss' -import addonInitials from '@entities/addon/model/addon.initials' - import MainEvents from '@common/types/mainEvents' import { staticAsset } from '@shared/lib/staticAssets' import apolloClient from '@shared/api/apolloClient' @@ -378,52 +376,58 @@ export default function ExtensionPage() { [installedRelationAddons, relationLabels], ) - const getActiveConflictLabels = useCallback((addon: Addon, availableAddons: Addon[] = addons): string[] => { - const addonConflictIds = new Set( - Array.isArray(addon.conflictsWith) ? addon.conflictsWith.map(value => String(value || '').trim()).filter(Boolean) : [], - ) - const addonIdentifiers = new Set([addon.id, addon.storeAddonId].map(value => String(value || '').trim()).filter(Boolean)) - - return availableAddons - .filter(candidate => { - if (!candidate.enabled || candidate.directoryName === addon.directoryName) { - return false - } - - const candidateIdentifiers = [candidate.id, candidate.storeAddonId].map(value => String(value || '').trim()).filter(Boolean) - const candidateConflictIds = Array.isArray(candidate.conflictsWith) ? - candidate.conflictsWith.map(value => String(value || '').trim()).filter(Boolean) - : [] + const getActiveConflictLabels = useCallback( + (addon: Addon, availableAddons: Addon[] = addons): string[] => { + const addonConflictIds = new Set( + Array.isArray(addon.conflictsWith) ? addon.conflictsWith.map(value => String(value || '').trim()).filter(Boolean) : [], + ) + const addonIdentifiers = new Set([addon.id, addon.storeAddonId].map(value => String(value || '').trim()).filter(Boolean)) - return ( - candidateIdentifiers.some(identifier => addonConflictIds.has(identifier)) || - candidateConflictIds.some(conflictId => addonIdentifiers.has(conflictId)) - ) - }) - .map(candidate => candidate.name) - }, [addons]) + return availableAddons + .filter(candidate => { + if (!candidate.enabled || candidate.directoryName === addon.directoryName) { + return false + } - const getDependentAddonLabels = useCallback((addon: Addon, availableAddons: Addon[] = addons): string[] => { - const addonIdentifiers = new Set( - [addon.directoryName, addon.name, addon.id, addon.storeAddonId] - .map(value => String(value || '').trim()) - .filter(Boolean) - .flatMap(value => [value, value.toLowerCase()]), - ) + const candidateIdentifiers = [candidate.id, candidate.storeAddonId].map(value => String(value || '').trim()).filter(Boolean) + const candidateConflictIds = Array.isArray(candidate.conflictsWith) + ? candidate.conflictsWith.map(value => String(value || '').trim()).filter(Boolean) + : [] - return availableAddons - .filter(candidate => { - if (!candidate.enabled || candidate.directoryName === addon.directoryName) { - return false - } + return ( + candidateIdentifiers.some(identifier => addonConflictIds.has(identifier)) || + candidateConflictIds.some(conflictId => addonIdentifiers.has(conflictId)) + ) + }) + .map(candidate => candidate.name) + }, + [addons], + ) - return (candidate.dependencies || []) + const getDependentAddonLabels = useCallback( + (addon: Addon, availableAddons: Addon[] = addons): string[] => { + const addonIdentifiers = new Set( + [addon.directoryName, addon.name, addon.id, addon.storeAddonId] .map(value => String(value || '').trim()) .filter(Boolean) - .some(dependencyId => addonIdentifiers.has(dependencyId) || addonIdentifiers.has(dependencyId.toLowerCase())) - }) - .map(candidate => candidate.name) - }, [addons]) + .flatMap(value => [value, value.toLowerCase()]), + ) + + return availableAddons + .filter(candidate => { + if (!candidate.enabled || candidate.directoryName === addon.directoryName) { + return false + } + + return (candidate.dependencies || []) + .map(value => String(value || '').trim()) + .filter(Boolean) + .some(dependencyId => addonIdentifiers.has(dependencyId) || addonIdentifiers.has(dependencyId.toLowerCase())) + }) + .map(candidate => candidate.name) + }, + [addons], + ) const handleCheckboxChange = useCallback( async (addon: Addon, newChecked: boolean, showToast: boolean = true) => { @@ -431,30 +435,28 @@ export default function ExtensionPage() { const previousScripts = [...enabledScripts] const previousEnabledKeys = buildEnabledAddonKeys(previousTheme, previousScripts) - if (addon.type === 'theme') { - if (newChecked) { - window.electron.store.set('addons.theme', addon.directoryName) - } else { - window.electron.store.set('addons.theme', 'Default') - } - } else { - const updated = newChecked ? [...enabledScripts, addon.directoryName] : enabledScripts.filter(name => name !== addon.directoryName) - window.electron.store.set('addons.scripts', updated) + const result = await window.desktopEvents?.invoke(MainEvents.SET_ADDON_ENABLED, { + directoryName: addon.directoryName, + enabled: newChecked, + }) + if (!result?.success) { + throw new Error(result?.reason || 'SET_ADDON_ENABLED_FAILED') } - window.desktopEvents?.send(MainEvents.REFRESH_EXTENSIONS) - const refreshedAddons = await loadAddons(true) - const nextTheme = safeStoreGet('addons.theme', 'Default') || 'Default' - const nextEnabledScripts = readEnabledScriptsState() + const refreshedAddons = Array.isArray(result.addons) + ? (result.addons as Addon[]).filter(a => a.name !== 'Default') + : await loadAddons(true) + setAddons(refreshedAddons) + + const nextTheme = String(result.theme || 'Default') + const nextEnabledScripts = Array.isArray(result.scripts) + ? result.scripts.map((script: unknown) => String(script || '').trim()).filter(Boolean) + : readEnabledScriptsState() + setCurrentTheme(nextTheme) + setEnabledScripts(nextEnabledScripts) const nextEnabledKeys = buildEnabledAddonKeys(nextTheme, nextEnabledScripts) - const resolvedEnabled = - addon.type === 'theme' ? nextTheme === addon.directoryName : nextEnabledScripts.includes(addon.directoryName) - const nextThemeAddon = - nextTheme === 'Default' ? addonInitials[0] : refreshedAddons.find(item => item.type === 'theme' && item.directoryName === nextTheme) || addonInitials[0] + const resolvedEnabled = addon.type === 'theme' ? nextTheme === addon.directoryName : nextEnabledScripts.includes(addon.directoryName) - if (previousTheme !== nextTheme) { - window.desktopEvents?.send(MainEvents.THEME_CHANGED, nextThemeAddon) - } sendStoreAddonMetrics(nextTheme, nextEnabledScripts) if (showToast) { @@ -467,75 +469,73 @@ export default function ExtensionPage() { .flatMap(value => [value, value.toLowerCase()]), ) const autoDisabledAddons = addons.filter(candidate => autoDisabled.includes(candidate.directoryName)) - const dependentAutoDisabledAddons = !newChecked ? - autoDisabledAddons.filter(candidate => - (candidate.dependencies || []) - .map(value => String(value || '').trim()) - .filter(Boolean) - .some(dependencyId => disabledAddonIdentifiers.has(dependencyId) || disabledAddonIdentifiers.has(dependencyId.toLowerCase())), - ) - : [] + const dependentAutoDisabledAddons = !newChecked + ? autoDisabledAddons.filter(candidate => + (candidate.dependencies || []) + .map(value => String(value || '').trim()) + .filter(Boolean) + .some( + dependencyId => + disabledAddonIdentifiers.has(dependencyId) || disabledAddonIdentifiers.has(dependencyId.toLowerCase()), + ), + ) + : [] const dependentAutoDisabledKeys = new Set(dependentAutoDisabledAddons.map(candidate => candidate.directoryName)) const remainingAutoDisabledLabels = autoDisabled .filter(key => !dependentAutoDisabledKeys.has(key)) .map(key => relationLabels[key] || key) - const relationMessages = - addonRelationsEnabled ? - [ - autoEnabled.length ? - t('extensions.relations.autoEnabled', { + const relationMessages = addonRelationsEnabled + ? [ + autoEnabled.length + ? t('extensions.relations.autoEnabled', { value: autoEnabled.map(key => relationLabels[key] || key).join(', '), }) - : '', - dependentAutoDisabledAddons.length ? - t('extensions.relations.autoDisabledDependents', { + : '', + dependentAutoDisabledAddons.length + ? t('extensions.relations.autoDisabledDependents', { dependency: addon.name, value: dependentAutoDisabledAddons.map(item => item.name).join(', '), }) - : '', - remainingAutoDisabledLabels.length ? - t('extensions.relations.autoDisabled', { + : '', + remainingAutoDisabledLabels.length + ? t('extensions.relations.autoDisabled', { value: remainingAutoDisabledLabels.join(', '), }) - : '', - ].filter(Boolean) - : [] + : '', + ].filter(Boolean) + : [] const toastId = `addon-toggle:${addon.directoryName}:${newChecked ? 'enable' : 'disable'}` if (newChecked && !resolvedEnabled) { const missingDependencyLabels = addonRelationsEnabled ? getMissingDependencyLabels(addon) : [] const activeConflictLabels = addonRelationsEnabled ? getActiveConflictLabels(addon, refreshedAddons) : [] const blockingMessages = [ - missingDependencyLabels.length ? - t('extensions.relations.blockedByDependencies', { - value: missingDependencyLabels.join(', '), - }) - : '', - activeConflictLabels.length ? - t('extensions.relations.blockedByConflicts', { - value: activeConflictLabels.join(', '), - }) - : '', + missingDependencyLabels.length + ? t('extensions.relations.blockedByDependencies', { + value: missingDependencyLabels.join(', '), + }) + : '', + activeConflictLabels.length + ? t('extensions.relations.blockedByConflicts', { + value: activeConflictLabels.join(', '), + }) + : '', ].filter(Boolean) toast.custom( 'error', t('common.errorTitle'), - [ - t('extensions.relations.enableBlockedResolved', { name: addon.name }), - ...blockingMessages, - ...relationMessages, - ].join('\n'), + [t('extensions.relations.enableBlockedResolved', { name: addon.name }), ...blockingMessages, ...relationMessages].join('\n'), { id: toastId }, ) } else if (!newChecked && resolvedEnabled) { const dependentAddonLabels = addonRelationsEnabled ? getDependentAddonLabels(addon, refreshedAddons) : [] const blockingMessages = [ - dependentAddonLabels.length ? - t('extensions.relations.disableBlockedByDependents', { - value: dependentAddonLabels.join(', '), - }) - : t('extensions.relations.disableBlockedResolved', { name: addon.name }), + dependentAddonLabels.length + ? t('extensions.relations.disableBlockedByDependents', { + value: dependentAddonLabels.join(', '), + }) + : t('extensions.relations.disableBlockedResolved', { name: addon.name }), ...relationMessages, ].filter(Boolean) @@ -555,9 +555,9 @@ export default function ExtensionPage() { newChecked ? 'success' : 'info', newChecked ? t('extensions.scriptEnabled') : t('extensions.scriptDisabled'), [ - newChecked ? - t('extensions.scriptEnabledMessage', { name: addon.name }) - : t('extensions.scriptDisabledMessage', { name: addon.name }), + newChecked + ? t('extensions.scriptEnabledMessage', { name: addon.name }) + : t('extensions.scriptDisabledMessage', { name: addon.name }), ...relationMessages, ].join('\n'), { id: toastId }, @@ -565,7 +565,19 @@ export default function ExtensionPage() { } } }, - [addonRelationsEnabled, currentTheme, enabledScripts, getActiveConflictLabels, getDependentAddonLabels, getMissingDependencyLabels, loadAddons, relationLabels, sendStoreAddonMetrics, t], + [ + addonRelationsEnabled, + currentTheme, + enabledScripts, + getActiveConflictLabels, + getDependentAddonLabels, + getMissingDependencyLabels, + loadAddons, + relationLabels, + sendStoreAddonMetrics, + setAddons, + t, + ], ) const handleSearchChange = useCallback((e: React.ChangeEvent) => { @@ -724,11 +736,11 @@ export default function ExtensionPage() { const selectedAddonEnableBlockedReason = useMemo( () => - selectedAddonMissingDependencies.length ? - t('extensions.relations.enableBlockedHintMissing', { - value: selectedAddonMissingDependencies.join(', '), - }) - : null, + selectedAddonMissingDependencies.length + ? t('extensions.relations.enableBlockedHintMissing', { + value: selectedAddonMissingDependencies.join(', '), + }) + : null, [selectedAddonMissingDependencies, t], ) @@ -1109,7 +1121,16 @@ export default function ExtensionPage() { true, ) }, - [addonRelationsEnabled, currentTheme, enabledScripts, getMissingDependencyLabels, handleCheckboxChange, isAddonVersionSupported, musicVersion, t], + [ + addonRelationsEnabled, + currentTheme, + enabledScripts, + getMissingDependencyLabels, + handleCheckboxChange, + isAddonVersionSupported, + musicVersion, + t, + ], ) const shouldShowUntrustedAddonWarning = useCallback( @@ -1143,7 +1164,14 @@ export default function ExtensionPage() { continueEnableAddon(addon) }, - [Modals.UNTRUSTED_LOCAL_ADDON_MODAL, addonRelationsEnabled, continueEnableAddon, getMissingDependencyLabels, openModal, shouldShowUntrustedAddonWarning], + [ + Modals.UNTRUSTED_LOCAL_ADDON_MODAL, + addonRelationsEnabled, + continueEnableAddon, + getMissingDependencyLabels, + openModal, + shouldShowUntrustedAddonWarning, + ], ) return ( @@ -1216,37 +1244,37 @@ export default function ExtensionPage() { : enabledScripts.includes(selectedAddon.directoryName) return ( - { - void handleStoreAddonUpdate() - }} - publication={selectedPublication} - publicationReleases={visiblePublicationReleases} - publicationChangelogText={publicationChangelogText} - publicationGithubUrlText={publicationGithubUrlText} - canManagePublication={canManagePublication} - publicationBusy={publicationBusy} - onPublicationChangelogChange={setPublicationChangelogText} - onPublicationGithubUrlChange={setPublicationGithubUrlText} - onPublishAddon={handlePublishAddon} - onUpdateAddon={handleUpdateAddon} - onToggleEnabled={enabled => { - if (enabled) { - handleEnableAddon(selectedAddon) - } else { - handleCheckboxChange(selectedAddon, false, true) - } - }} - setSelectedTags={setSelectedTags} - setShowFilters={setShowFilters} - /> + { + void handleStoreAddonUpdate() + }} + publication={selectedPublication} + publicationReleases={visiblePublicationReleases} + publicationChangelogText={publicationChangelogText} + publicationGithubUrlText={publicationGithubUrlText} + canManagePublication={canManagePublication} + publicationBusy={publicationBusy} + onPublicationChangelogChange={setPublicationChangelogText} + onPublicationGithubUrlChange={setPublicationGithubUrlText} + onPublishAddon={handlePublishAddon} + onUpdateAddon={handleUpdateAddon} + onToggleEnabled={enabled => { + if (enabled) { + handleEnableAddon(selectedAddon) + } else { + handleCheckboxChange(selectedAddon, false, true) + } + }} + setSelectedTags={setSelectedTags} + setShowFilters={setShowFilters} + /> ) })() ) : ( diff --git a/src/renderer/pages/extension/model/addonCatalog.ts b/src/renderer/pages/extension/model/addonCatalog.ts index 9fec288b..40e218a4 100644 --- a/src/renderer/pages/extension/model/addonCatalog.ts +++ b/src/renderer/pages/extension/model/addonCatalog.ts @@ -166,8 +166,8 @@ export function filterAndSortAddons({ }) case 'date': return result.slice().sort((a, b) => { - const dateA = parseFloat(a.lastModified || '0') || 0 - const dateB = parseFloat(b.lastModified || '0') || 0 + const dateA = Number(a.lastModifiedAt) || 0 + const dateB = Number(b.lastModifiedAt) || 0 return sortOrder === 'asc' ? dateA - dateB : dateB - dateA }) case 'size': diff --git a/src/renderer/pages/extension/route/extBox/AddonRelationsPanel.tsx b/src/renderer/pages/extension/route/extBox/AddonRelationsPanel.tsx index 89b483a3..bfb6c28d 100644 --- a/src/renderer/pages/extension/route/extBox/AddonRelationsPanel.tsx +++ b/src/renderer/pages/extension/route/extBox/AddonRelationsPanel.tsx @@ -59,7 +59,10 @@ const AddonRelationsPanel: React.FC = ({ addon, relationLabels = {} }) => } }) - const dependencyItems = useMemo(() => buildRelationItems(addon.dependencies, 'dependency'), [addon.dependencies, installedAddonMap, relationLabels]) + const dependencyItems = useMemo( + () => buildRelationItems(addon.dependencies, 'dependency'), + [addon.dependencies, installedAddonMap, relationLabels], + ) const conflictItems = useMemo(() => buildRelationItems(addon.conflictsWith, 'conflict'), [addon.conflictsWith, installedAddonMap, relationLabels]) if (!dependencyItems.length && !conflictItems.length) { @@ -69,40 +72,41 @@ const AddonRelationsPanel: React.FC = ({ addon, relationLabels = {} }) => const renderCard = (item: RelationItem) => { const isConflictActive = item.kind === 'conflict' && item.isEnabled const statusText = - item.kind === 'dependency' ? - item.isEnabled ? - t('extensions.relations.statusEnabled') - : item.isInstalled ? - t('extensions.relations.statusInstalled') - : t('extensions.relations.statusMissing') - : item.isEnabled ? - t('extensions.relations.statusConflictActive') - : item.isInstalled ? - t('extensions.relations.statusInstalled') - : t('extensions.relations.statusNotInstalled') + item.kind === 'dependency' + ? item.isEnabled + ? t('extensions.relations.statusEnabled') + : item.isInstalled + ? t('extensions.relations.statusInstalled') + : t('extensions.relations.statusMissing') + : item.isEnabled + ? t('extensions.relations.statusConflictActive') + : item.isInstalled + ? t('extensions.relations.statusInstalled') + : t('extensions.relations.statusNotInstalled') const statusToneClass = - item.kind === 'dependency' ? - item.isEnabled ? - s.relationCardStatusSuccess - : item.isInstalled ? - s.relationCardStatusNeutral - : s.relationCardStatusMuted - : isConflictActive ? s.relationCardStatusDanger - : item.isInstalled ? s.relationCardStatusNeutral - : s.relationCardStatusMuted - - const sourceText = - item.isInstalled ? - item.installSource === 'store' ? - t('extensions.source.store') - : t('extensions.source.local') - : item.id + item.kind === 'dependency' + ? item.isEnabled + ? s.relationCardStatusSuccess + : item.isInstalled + ? s.relationCardStatusNeutral + : s.relationCardStatusMuted + : isConflictActive + ? s.relationCardStatusDanger + : item.isInstalled + ? s.relationCardStatusNeutral + : s.relationCardStatusMuted + + const sourceText = item.isInstalled ? (item.installSource === 'store' ? t('extensions.source.store') : t('extensions.source.local')) : item.id const icon = - item.addonType === 'theme' ? - : item.addonType === 'script' ? - : + item.addonType === 'theme' ? ( + + ) : item.addonType === 'script' ? ( + + ) : ( + + ) const isInteractive = !!item.directoryName diff --git a/src/renderer/pages/extension/route/extBox/MetadataEditor.tsx b/src/renderer/pages/extension/route/extBox/MetadataEditor.tsx index bd81ca77..172f3b4d 100644 --- a/src/renderer/pages/extension/route/extBox/MetadataEditor.tsx +++ b/src/renderer/pages/extension/route/extBox/MetadataEditor.tsx @@ -451,7 +451,9 @@ const MetadataEditor: React.FC = ({ addonPath, addonRelationsEnabled }) = const foundUser = response.data?.findUserByName const normalizedInput = normalizeComparableText(trimmedValue) - const exactMatchExists = [foundUser?.username, foundUser?.nickname].some(candidate => normalizeComparableText(candidate) === normalizedInput) + const exactMatchExists = [foundUser?.username, foundUser?.nickname].some( + candidate => normalizeComparableText(candidate) === normalizedInput, + ) const canonicalAuthor = getProfileSlug(foundUser) if (!exactMatchExists || !canonicalAuthor) { @@ -513,23 +515,17 @@ const MetadataEditor: React.FC = ({ addonPath, addonRelationsEnabled }) = .filter(addon => !(draft.storeAddonId && addon.id === draft.storeAddonId)) .filter(addon => !(draft.id && addon.id === draft.id)) - const normalizedNameCounts = relationCandidates.reduce( - (acc, addon) => { - const key = addon.name.trim().toLowerCase() - acc.set(key, (acc.get(key) || 0) + 1) - return acc - }, - new Map(), - ) + const normalizedNameCounts = relationCandidates.reduce((acc, addon) => { + const key = addon.name.trim().toLowerCase() + acc.set(key, (acc.get(key) || 0) + 1) + return acc + }, new Map()) return relationCandidates .map(addon => { const normalizedName = addon.name.trim().toLowerCase() const hasDuplicateName = (normalizedNameCounts.get(normalizedName) || 0) > 1 - const suffixParts = [ - hasDuplicateName ? addon.type : '', - addon.currentRelease?.version?.trim() || '', - ].filter(Boolean) + const suffixParts = [hasDuplicateName ? addon.type : '', addon.currentRelease?.version?.trim() || ''].filter(Boolean) const label = suffixParts.length ? `${addon.name} (${suffixParts.join(' • ')})` : addon.name return { @@ -541,7 +537,10 @@ const MetadataEditor: React.FC = ({ addonPath, addonRelationsEnabled }) = .sort((left, right) => left.label.localeCompare(right.label)) }, [availableAddons, draft.id, draft.storeAddonId]) - const relationOptions = useMemo(() => relationOptionRecords.map(({ value, label, searchText }) => ({ value, label, searchText })), [relationOptionRecords]) + const relationOptions = useMemo( + () => relationOptionRecords.map(({ value, label, searchText }) => ({ value, label, searchText })), + [relationOptionRecords], + ) const relationLabelMap = useMemo(() => { const entries = new Map() @@ -648,13 +647,19 @@ const MetadataEditor: React.FC = ({ addonPath, addonRelationsEnabled }) = setter(prev => prev.filter(entry => entry !== value)) }, []) - const removeDependency = useCallback((value: string) => { - removeRelation(value, setModalDependenciesDraft) - }, [removeRelation]) + const removeDependency = useCallback( + (value: string) => { + removeRelation(value, setModalDependenciesDraft) + }, + [removeRelation], + ) - const removeConflict = useCallback((value: string) => { - removeRelation(value, setModalConflictsDraft) - }, [removeRelation]) + const removeConflict = useCallback( + (value: string) => { + removeRelation(value, setModalConflictsDraft) + }, + [removeRelation], + ) const removeAllowedUrl = useCallback((value: string) => { setModalAllowedUrlsDraft(prev => prev.filter(entry => entry !== value)) @@ -782,24 +787,27 @@ const MetadataEditor: React.FC = ({ addonPath, addonRelationsEnabled }) = [resolvePulseAuthor], ) - const removeAuthor = useCallback((value: string) => { - if (normalizeComparableText(value) === normalizeComparableText(selfAuthorSlug)) { - toast.custom('info', t('common.attentionTitle'), t('metadata.authorsEditor.selfRemovalBlocked')) - return - } + const removeAuthor = useCallback( + (value: string) => { + if (normalizeComparableText(value) === normalizeComparableText(selfAuthorSlug)) { + toast.custom('info', t('common.attentionTitle'), t('metadata.authorsEditor.selfRemovalBlocked')) + return + } - if (selectedAuthors.length <= 1) { - toast.custom('info', t('common.attentionTitle'), t('metadata.authorsEditor.minimumOneAuthor')) - return - } + if (selectedAuthors.length <= 1) { + toast.custom('info', t('common.attentionTitle'), t('metadata.authorsEditor.minimumOneAuthor')) + return + } - setDraft(prev => ({ - ...prev, - author: splitAuthorEntries(prev.author) - .filter(entry => normalizeComparableText(entry) !== normalizeComparableText(value)) - .join(', '), - })) - }, [selectedAuthors.length, selfAuthorSlug, t]) + setDraft(prev => ({ + ...prev, + author: splitAuthorEntries(prev.author) + .filter(entry => normalizeComparableText(entry) !== normalizeComparableText(value)) + .join(', '), + })) + }, + [selectedAuthors.length, selfAuthorSlug, t], + ) if (loading) { return @@ -812,7 +820,12 @@ const MetadataEditor: React.FC = ({ addonPath, addonRelationsEnabled }) =
- setField('name', value)} /> + setField('name', value)} + /> = ({ addonPath, addonRelationsEnabled }) = setAuthorSearchOpen(true) }} onFocus={() => setAuthorSearchOpen(true)} - placeholder={selectedAuthors.length ? t('metadata.authorsEditor.searchPlaceholder') : t('metadata.authorsEditor.requiredPlaceholder')} + placeholder={ + selectedAuthors.length + ? t('metadata.authorsEditor.searchPlaceholder') + : t('metadata.authorsEditor.requiredPlaceholder') + } />
@@ -1120,7 +1137,11 @@ const MetadataEditor: React.FC = ({ addonPath, addonRelationsEnabled }) = {modalDependenciesDraft.map(dependencyId => (
{relationLabelMap.get(dependencyId) || dependencyId}
-
diff --git a/src/renderer/pages/extension/route/extBox/TabContent.tsx b/src/renderer/pages/extension/route/extBox/TabContent.tsx index 802f1298..db0a492f 100644 --- a/src/renderer/pages/extension/route/extBox/TabContent.tsx +++ b/src/renderer/pages/extension/route/extBox/TabContent.tsx @@ -278,7 +278,7 @@ const TabContent: React.FC = ({ return {alt} } - const activeConfig = editMode ? editConfig ?? config : config + const activeConfig = editMode ? (editConfig ?? config) : config const isConfigEmpty = !activeConfig || !Array.isArray(activeConfig.sections) || activeConfig.sections.length === 0 if (active === 'Settings') { diff --git a/src/renderer/pages/extension/route/extBox/TabNavigation.tsx b/src/renderer/pages/extension/route/extBox/TabNavigation.tsx index e25a26d6..6d58b425 100644 --- a/src/renderer/pages/extension/route/extBox/TabNavigation.tsx +++ b/src/renderer/pages/extension/route/extBox/TabNavigation.tsx @@ -14,7 +14,15 @@ interface Props { stickyTop?: number } -const TabNavigation: React.FC = ({ active, onChange, docs, hasPublicationChangelog = false, hasRelations = false, showMetadataTab = true, stickyTop }) => { +const TabNavigation: React.FC = ({ + active, + onChange, + docs, + hasPublicationChangelog = false, + hasRelations = false, + showMetadataTab = true, + stickyTop, +}) => { const { t } = useTranslation() const docTabs: TabItem[] = docs.map(d => ({ @@ -25,7 +33,9 @@ const TabNavigation: React.FC = ({ active, onChange, docs, hasPublication const tabs: TabItem[] = [ ...docTabs, - ...(hasPublicationChangelog ? [{ title: t('extensions.tabs.changelog'), value: PUBLICATION_CHANGELOG_TAB, icon: }] : []), + ...(hasPublicationChangelog + ? [{ title: t('extensions.tabs.changelog'), value: PUBLICATION_CHANGELOG_TAB, icon: }] + : []), ...(hasRelations ? [{ title: t('extensions.tabs.relations'), value: RELATIONS_TAB, icon: }] : []), { title: t('extensions.tabs.settings'), value: 'Settings', icon: }, ...(showMetadataTab ? [{ title: t('extensions.tabs.metadata'), value: 'Metadata', icon: }] : []), diff --git a/src/renderer/pages/extension/route/extBox/ThemeInfo/ThemeInfo.module.scss b/src/renderer/pages/extension/route/extBox/ThemeInfo/ThemeInfo.module.scss index fdd6a54b..78dc1132 100644 --- a/src/renderer/pages/extension/route/extBox/ThemeInfo/ThemeInfo.module.scss +++ b/src/renderer/pages/extension/route/extBox/ThemeInfo/ThemeInfo.module.scss @@ -101,13 +101,14 @@ text-shadow: 0 2px 8px rgba(8, 10, 18, 0.5), 0 12px 32px rgba(8, 10, 18, 0.38); + user-select: text; } .invisible { background: transparent; height: 266px; width: -webkit-fill-available; - z-index: 1; + z-index: 0; position: relative; } diff --git a/src/renderer/pages/extension/route/extBox/ThemeInfo/index.tsx b/src/renderer/pages/extension/route/extBox/ThemeInfo/index.tsx index fb03e86b..e5d566a5 100644 --- a/src/renderer/pages/extension/route/extBox/ThemeInfo/index.tsx +++ b/src/renderer/pages/extension/route/extBox/ThemeInfo/index.tsx @@ -300,7 +300,6 @@ const ThemeInfo: React.FC = ({ {addon.installSource === 'store' ? t('extensions.source.store') : t('extensions.source.local')}
-
@@ -340,7 +339,11 @@ const ThemeInfo: React.FC = ({ title={!isEnabled && enableBlockedReason ? enableBlockedReason : undefined} onClick={() => onToggleEnabled(!isEnabled)} > - {isEnabled ? t('common.disable') : enableBlockedReason ? t('extensions.relations.enableBlockedButtonLabel') : t('common.enable')} + {isEnabled + ? t('common.disable') + : enableBlockedReason + ? t('extensions.relations.enableBlockedButtonLabel') + : t('common.enable')} )} diff --git a/src/renderer/pages/extension/route/extensionview.module.scss b/src/renderer/pages/extension/route/extensionview.module.scss index 3d909061..44530b75 100644 --- a/src/renderer/pages/extension/route/extensionview.module.scss +++ b/src/renderer/pages/extension/route/extensionview.module.scss @@ -301,9 +301,7 @@ padding: 14px 16px; border-radius: 16px; border: 1px solid rgba(129, 141, 179, 0.14); - background: - linear-gradient(180deg, rgba(73, 81, 107, 0.24) 0%, rgba(53, 59, 79, 0.34) 100%), - rgba(129, 141, 179, 0.08); + background: linear-gradient(180deg, rgba(73, 81, 107, 0.24) 0%, rgba(53, 59, 79, 0.34) 100%), rgba(129, 141, 179, 0.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02); } @@ -317,9 +315,7 @@ &:hover { border-color: rgba(129, 141, 179, 0.28); - background: - linear-gradient(180deg, rgba(87, 96, 126, 0.28) 0%, rgba(60, 67, 90, 0.38) 100%), - rgba(129, 141, 179, 0.1); + background: linear-gradient(180deg, rgba(87, 96, 126, 0.28) 0%, rgba(60, 67, 90, 0.38) 100%), rgba(129, 141, 179, 0.1); transform: translateY(-1px); } } diff --git a/src/renderer/pages/extension/route/extensionview.tsx b/src/renderer/pages/extension/route/extensionview.tsx index aedc9f3d..344946c4 100644 --- a/src/renderer/pages/extension/route/extensionview.tsx +++ b/src/renderer/pages/extension/route/extensionview.tsx @@ -60,21 +60,26 @@ const ExtensionView: React.FC = ({ const canEditMetadata = useMemo(() => { const currentUserCandidates = [user.username, user.nickname, user.id] - .map(value => String(value || '').trim().toLowerCase()) + .map(value => + String(value || '') + .trim() + .toLowerCase(), + ) .filter(Boolean) if (!currentUserCandidates.length) { return false } - const addonAuthors = - Array.isArray(addon.author) ? - addon.author - : typeof addon.author === 'string' ? - addon.author.split(',') - : [] + const addonAuthors = Array.isArray(addon.author) ? addon.author : typeof addon.author === 'string' ? addon.author.split(',') : [] - const normalizedAuthors = addonAuthors.map(author => String(author || '').trim().toLowerCase()).filter(Boolean) + const normalizedAuthors = addonAuthors + .map(author => + String(author || '') + .trim() + .toLowerCase(), + ) + .filter(Boolean) if (!normalizedAuthors.length) { return false } diff --git a/src/renderer/pages/home/model/homeDashboard.ts b/src/renderer/pages/home/model/homeDashboard.ts index 9b1c10bd..6977760b 100644 --- a/src/renderer/pages/home/model/homeDashboard.ts +++ b/src/renderer/pages/home/model/homeDashboard.ts @@ -1,5 +1,5 @@ export type HomePrimaryComponent = { - id: 'mod' | 'client' | 'music', + id: 'mod' | 'client' | 'music' titleKey: string iconAsset: string } diff --git a/src/renderer/pages/home/ui/HomeNewsSection.tsx b/src/renderer/pages/home/ui/HomeNewsSection.tsx index ffce90b1..45f89680 100644 --- a/src/renderer/pages/home/ui/HomeNewsSection.tsx +++ b/src/renderer/pages/home/ui/HomeNewsSection.tsx @@ -115,10 +115,7 @@ export default function HomeNewsSection() { return `${day}.${month}.${year}` }, []) - const formatCompactReadTime = useCallback( - (value: number) => `${value} ${i18n.language === 'ru' ? 'мин' : 'min'}`, - [i18n.language], - ) + const formatCompactReadTime = useCallback((value: number) => `${value} ${i18n.language === 'ru' ? 'мин' : 'min'}`, [i18n.language]) const openArticle = useCallback((slug: string) => { if (!slug) { diff --git a/src/renderer/pages/home/ui/HomePrimaryComponentsSection.tsx b/src/renderer/pages/home/ui/HomePrimaryComponentsSection.tsx index e2895de4..0b576d28 100644 --- a/src/renderer/pages/home/ui/HomePrimaryComponentsSection.tsx +++ b/src/renderer/pages/home/ui/HomePrimaryComponentsSection.tsx @@ -31,7 +31,6 @@ export default function HomePrimaryComponentsSection({ items, versions, isModIns
{items.map(item => (
-
diff --git a/src/renderer/pages/home/ui/HomeSecondaryComponentsSection.tsx b/src/renderer/pages/home/ui/HomeSecondaryComponentsSection.tsx index 6a1286e6..9063a2e9 100644 --- a/src/renderer/pages/home/ui/HomeSecondaryComponentsSection.tsx +++ b/src/renderer/pages/home/ui/HomeSecondaryComponentsSection.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next' +import { HiQuestionMarkCircle } from 'react-icons/hi' import { staticAsset } from '@shared/lib/staticAssets' import ButtonV2 from '@shared/ui/buttonV2' import TooltipButton from '@shared/ui/tooltip_button' @@ -51,14 +52,14 @@ export default function HomeSecondaryComponentsSection({ items, isObsInstalled, ) : isMetadataBackedSubcomponent(item.id) ? (
{item.version ? t('pages.home.installed') : t('pages.home.notInstalled')} +
) : ( diff --git a/src/renderer/pages/home/ui/home.module.scss b/src/renderer/pages/home/ui/home.module.scss index 2e7ad327..b54304a1 100644 --- a/src/renderer/pages/home/ui/home.module.scss +++ b/src/renderer/pages/home/ui/home.module.scss @@ -12,9 +12,7 @@ min-height: 0; padding: 18px; border-radius: 14px; - background: - linear-gradient(180deg, rgba(79, 87, 116, 0.08) 0%, rgba(79, 87, 116, 0) 100%), - #2f3446; + background: linear-gradient(180deg, rgba(79, 87, 116, 0.08) 0%, rgba(79, 87, 116, 0) 100%), #2f3446; border: 1px solid rgba(91, 100, 128, 0.55); box-sizing: border-box; } @@ -36,7 +34,7 @@ .panelHollow { min-height: 0; - gap: 10px + gap: 10px; } .newsPanel { @@ -46,7 +44,8 @@ min-height: 0; width: 100%; background: #434962; - padding-inline: 25px; + padding-inline-start: 25px; + padding-inline-end: 10px; padding-block-start: 15px; position: relative; overflow: hidden; @@ -58,6 +57,7 @@ justify-content: space-between; gap: 16px; margin-bottom: 14px; + margin-right: 18px; } .panelTitle { @@ -71,7 +71,7 @@ font-weight: 900; font-size: 24px; line-height: 33px; - color: #F2F3F5; + color: #f2f3f5; } .newsSubtitle { @@ -142,25 +142,46 @@ } .ymItem { - background: conic-gradient(from 0deg at 100% 100%, #FED42B -1.26deg, rgba(0, 0, 0, 0.31) 8.08deg, #FED42B 358.74deg, rgba(0, 0, 0, 0.31) 368.08deg), #232632; + background: conic-gradient( + from 0deg at 100% 100%, + #fed42b -1.26deg, + rgba(0, 0, 0, 0.31) 8.08deg, + #fed42b 358.74deg, + rgba(0, 0, 0, 0.31) 368.08deg + ), + #232632; } .modItem { - background: conic-gradient(from 0deg at 100% 100%, #5865F2 -1.26deg, rgba(0, 0, 0, 0.31) 8.08deg, #5865F2 358.74deg, rgba(0, 0, 0, 0.31) 368.08deg), #232632; + background: conic-gradient( + from 0deg at 100% 100%, + #5865f2 -1.26deg, + rgba(0, 0, 0, 0.31) 8.08deg, + #5865f2 358.74deg, + rgba(0, 0, 0, 0.31) 368.08deg + ), + #232632; .actionButton { - color: #505BD8; + color: #505bd8; &:not(:disabled):active { - color: #505BD8; + color: #505bd8; } } } .clientItem { - background: conic-gradient(from 0deg at 100% 100%, #5865F2 -1.26deg, rgba(0, 0, 0, 0.31) 8.08deg, #5865F2 358.74deg, rgba(0, 0, 0, 0.31) 368.08deg), #232632; + background: conic-gradient( + from 0deg at 100% 100%, + #5865f2 -1.26deg, + rgba(0, 0, 0, 0.31) 8.08deg, + #5865f2 358.74deg, + rgba(0, 0, 0, 0.31) 368.08deg + ), + #232632; .actionButton { - color: #505BD8; + color: #505bd8; &:not(:disabled):active { - color: #505BD8; + color: #505bd8; } } } @@ -177,12 +198,11 @@ align-items: center; flex-grow: 0; - background: #3B4052; + background: #3b4052; .componentLogo { box-shadow: none; } - } .secondaryItemMain { @@ -223,7 +243,6 @@ flex: none; order: 0; flex-grow: 1; - } .componentTitle { @@ -233,16 +252,8 @@ font-weight: 700; font-size: 18px; line-height: 110%; - /* identical to box height, or 20px */ - - color: #FFFFFF; - - - /* Inside auto layout */ - flex: none; - order: 0; - flex-grow: 0; - + color: #ffffff; + user-select: text; } .componentVersion, @@ -251,6 +262,7 @@ font-weight: 600; line-height: 1.15; color: rgba(241, 244, 251, 0.92); + user-select: text; } .animatedComponentVersion { @@ -270,7 +282,7 @@ padding: 10px; gap: 4px; - background: #FFFFFF; + background: #ffffff; border-radius: 8px; flex: none; @@ -282,21 +294,21 @@ font-size: 14px; line-height: 110%; - color: #C8A826; + color: #c8a826; transition: transform 0.16s ease, background-color 0.16s ease; &:not(:disabled):hover { - background: #FFFFFF; + background: #ffffff; transform: translateY(-1px); } &:not(:disabled):active { background: #ececec; transform: translateY(0); - color: #C8A826; + color: #c8a826; } &:disabled { @@ -312,6 +324,10 @@ line-height: 110%; color: rgba(244, 246, 251, 0.72); + + display: flex; + align-items: center; + gap: 4px; } .secondaryActionButton { @@ -321,7 +337,7 @@ padding: 10px; gap: 4px; - background: #FFFFFF; + background: #ffffff; border-radius: 8px; flex: none; @@ -337,11 +353,11 @@ margin-left: auto; transition: - transform 0.16s ease, - background-color 0.16s ease; + transform 0.16s ease, + background-color 0.16s ease; &:not(:disabled):hover { - background: #FFFFFF; + background: #ffffff; transform: translateY(-1px); } @@ -420,8 +436,8 @@ .newsFeaturedCard { overflow: hidden; border-radius: 16px; - background-color: #4D536B; - border: 1px solid #2A2A2A; + background-color: #4d536b; + border: 1px solid #2a2a2a; transition: transform 0.22s ease, border-color 0.22s ease; @@ -448,8 +464,7 @@ .newsListItemMediaFallback { position: absolute; inset: 0; - background: - radial-gradient(circle at top right, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0) 30%), + background: radial-gradient(circle at top right, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0) 30%), linear-gradient(135deg, #6073a7 0%, #32394f 55%, #232836 100%); } @@ -558,10 +573,9 @@ .newsListItem { overflow: hidden; border-radius: 16px; - background: #4D536B; - border: 1px solid #2A2A2A; - transition: - transform 0.18s ease; + background: #4d536b; + border: 1px solid #2a2a2a; + transition: transform 0.18s ease; &:hover, &:focus-visible { @@ -587,9 +601,7 @@ align-items: center; justify-content: center; text-align: center; - background: - radial-gradient(circle at top, rgba(144, 161, 214, 0.2) 0%, rgba(144, 161, 214, 0) 45%), - rgba(34, 39, 55, 0.76); + background: radial-gradient(circle at top, rgba(144, 161, 214, 0.2) 0%, rgba(144, 161, 214, 0) 45%), rgba(34, 39, 55, 0.76); border: 1px solid rgba(118, 131, 172, 0.24); } @@ -645,7 +657,7 @@ .newsSkeletonItemTitle { background: linear-gradient(90deg, rgba(115, 127, 164, 0.18) 0%, rgba(159, 173, 214, 0.28) 50%, rgba(115, 127, 164, 0.18) 100%); background-size: 220% 100%; - animation: newsShimmer 1.35s ease-in-out .75s infinite; + animation: newsShimmer 1.35s ease-in-out 0.75s infinite; } .newsSkeletonImage { diff --git a/src/renderer/pages/profile/[username].tsx b/src/renderer/pages/profile/[username].tsx index 0d390c55..e54aed64 100644 --- a/src/renderer/pages/profile/[username].tsx +++ b/src/renderer/pages/profile/[username].tsx @@ -81,7 +81,8 @@ const ProfilePage: React.FC = () => { return payload } - const hasLiveAchievementData = (Array.isArray(user.userAchievements) && user.userAchievements.length > 0) || Number(user.levelInfoV2?.totalPoints || 0) > 0 + const hasLiveAchievementData = + (Array.isArray(user.userAchievements) && user.userAchievements.length > 0) || Number(user.levelInfoV2?.totalPoints || 0) > 0 const liveStatus = socketConnected ? 'online' : user.status || payload.status const liveLastOnline = user.lastOnline || payload.lastOnline diff --git a/src/renderer/pages/store/index.tsx b/src/renderer/pages/store/index.tsx index 7cfd3eff..8d034967 100644 --- a/src/renderer/pages/store/index.tsx +++ b/src/renderer/pages/store/index.tsx @@ -196,16 +196,19 @@ export default function StorePage() { const hasSearchOrFilter = Boolean(debouncedSearchQuery) || typeFilter !== 'all' const shouldShowPendingSection = isDeveloperUser && (pendingAddons.length > 0 || hasSearchOrFilter) - const handleSortOptionClick = useCallback((option: StoreSortKey) => { - setSortKey(option) - setSortOrder(currentOrder => { - if (sortKey === option) { - return currentOrder === 'asc' ? 'desc' : 'asc' - } + const handleSortOptionClick = useCallback( + (option: StoreSortKey) => { + setSortKey(option) + setSortOrder(currentOrder => { + if (sortKey === option) { + return currentOrder === 'asc' ? 'desc' : 'asc' + } - return getDefaultSortOrder(option) - }) - }, [sortKey]) + return getDefaultSortOrder(option) + }) + }, + [sortKey], + ) const handleStoreAddonAction = useCallback( async (addon: StoreAddon, release: StoreAddon['currentRelease'], installedStoreAddon?: Addon) => { @@ -422,7 +425,8 @@ export default function StorePage() { const shimmerCount = useMemo(() => { const columns = Math.max(1, gridColumns) const fallbackViewportHeight = - scrollViewportHeight || (typeof window === 'undefined' ? STORE_CARD_MIN_HEIGHT * 2 : Math.max(window.innerHeight - 220, STORE_CARD_MIN_HEIGHT * 2)) + scrollViewportHeight || + (typeof window === 'undefined' ? STORE_CARD_MIN_HEIGHT * 2 : Math.max(window.innerHeight - 220, STORE_CARD_MIN_HEIGHT * 2)) const rowHeight = STORE_CARD_MIN_HEIGHT + STORE_GRID_ROW_GAP const rows = Math.max(2, Math.ceil(fallbackViewportHeight / rowHeight) + 1) @@ -595,9 +599,11 @@ export default function StorePage() { : t('store.filters.downloads')} {sortKey === option && - (sortOrder === 'asc' ? + (sortOrder === 'asc' ? ( - : )} + ) : ( + + ))} ))} diff --git a/src/renderer/pages/store/store.module.scss b/src/renderer/pages/store/store.module.scss index d0074172..b65d6258 100644 --- a/src/renderer/pages/store/store.module.scss +++ b/src/renderer/pages/store/store.module.scss @@ -351,7 +351,6 @@ .store_sectionGrid { grid-template-columns: minmax(0, 1fr); } - } @media (max-width: 640px) { diff --git a/src/renderer/pages/users/users.module.scss b/src/renderer/pages/users/users.module.scss index 34de90d1..0c0e777c 100644 --- a/src/renderer/pages/users/users.module.scss +++ b/src/renderer/pages/users/users.module.scss @@ -124,8 +124,9 @@ height: 48px; max-width: 400px; width: 100%; - background-color: rgb(111 120 153 / 65%); - border-radius: 12px; + background-color: #12151fb8; + border: 1px solid #69739452; + border-radius: 10px; cursor: text; position: relative; transition: border-color 0.2s; diff --git a/src/renderer/shared/api/socket/enums/incomingGatewayEvents.ts b/src/renderer/shared/api/socket/enums/incomingGatewayEvents.ts index dc09ec0c..26a563b0 100644 --- a/src/renderer/shared/api/socket/enums/incomingGatewayEvents.ts +++ b/src/renderer/shared/api/socket/enums/incomingGatewayEvents.ts @@ -1,5 +1,6 @@ const IncomingGatewayEvents = { DEPRECATED_VERSION: 'deprecated_version', + HARDWARE_IDENTITY_WARNING: 'hardware_identity_warning', ERROR_MESSAGE: 'error_message', LOGOUT: 'logout', USER_UPDATE: 'user_update', diff --git a/src/renderer/shared/api/socket/realtimeSocket.ts b/src/renderer/shared/api/socket/realtimeSocket.ts index 3941ba4f..ddcacfa8 100644 --- a/src/renderer/shared/api/socket/realtimeSocket.ts +++ b/src/renderer/shared/api/socket/realtimeSocket.ts @@ -1,5 +1,7 @@ import { io, Socket } from 'socket.io-client' import config from '@common/appConfig' +import type { ClientBuildIdentity } from '@common/types/clientBuildIdentity' +import type { ClientHardwareIdentity } from '@common/types/clientHardwareIdentity' export type GatewayFrame = { e?: string @@ -10,6 +12,8 @@ export type RealtimeSocketAuth = { page: string token: string | null version: string + buildIdentity?: ClientBuildIdentity + hardwareIdentity?: ClientHardwareIdentity | null compression: 'zstd-stream' inboundCompression: 'zstd-stream' } diff --git a/src/renderer/shared/api/subscriptionGiveaways.ts b/src/renderer/shared/api/subscriptionGiveaways.ts new file mode 100644 index 00000000..b12901bf --- /dev/null +++ b/src/renderer/shared/api/subscriptionGiveaways.ts @@ -0,0 +1,83 @@ +import rendererHttpClient from '@shared/api/http/client' +import getUserToken from '@shared/lib/auth/getUserToken' + +export type SubscriptionGiveaway = { + uuid: string + title: string + description?: string | null + planCode: string + durationMonths?: number | null + winnersCount: number + status: string + startsAt: string + endsAt: string +} + +type SubscriptionGiveawaysResponse = { + giveaways?: SubscriptionGiveaway[] + ok?: boolean +} + +type EnteredSubscriptionGiveawaysResponse = { + giveawayIds?: string[] + ok?: boolean +} + +export type SubscriptionGiveawaysSnapshot = { + enteredIds: Set + giveaways: SubscriptionGiveaway[] +} + +const CACHE_TTL_MS = 5_000 + +let cachedSnapshot: { authKey: string; fetchedAt: number; value: SubscriptionGiveawaysSnapshot } | null = null +let snapshotRequest: { authKey: string; promise: Promise } | null = null + +export async function loadSubscriptionGiveawaysSnapshot(options?: { force?: boolean }): Promise { + const authKey = getUserToken() ?? '' + const currentRequest = snapshotRequest + if (currentRequest && currentRequest.authKey === authKey) { + return currentRequest.promise + } + + const currentSnapshot = cachedSnapshot + if (!options?.force && currentSnapshot && currentSnapshot.authKey === authKey && Date.now() - currentSnapshot.fetchedAt < CACHE_TTL_MS) { + return currentSnapshot.value + } + + const request = Promise.all([ + rendererHttpClient.get('/subscription/giveaways'), + rendererHttpClient.get('/subscription/giveaways/entered', { auth: true }), + ]).then(([giveawaysResponse, enteredResponse]) => { + if (!giveawaysResponse.ok || !giveawaysResponse.data?.ok || !Array.isArray(giveawaysResponse.data.giveaways)) { + throw new Error('Failed to load subscription giveaways') + } + if (!enteredResponse.ok || !enteredResponse.data?.ok || !Array.isArray(enteredResponse.data.giveawayIds)) { + throw new Error('Failed to load entered subscription giveaways') + } + + const value = { + giveaways: giveawaysResponse.data.giveaways, + enteredIds: new Set(enteredResponse.data.giveawayIds), + } + cachedSnapshot = { + authKey, + fetchedAt: Date.now(), + value, + } + return value + }) + snapshotRequest = { authKey, promise: request } + + try { + return await request + } finally { + if (snapshotRequest?.promise === request) { + snapshotRequest = null + } + } +} + +export function invalidateSubscriptionGiveawaysSnapshot(): void { + cachedSnapshot = null +} diff --git a/src/renderer/shared/lib/utils.ts b/src/renderer/shared/lib/utils.ts index 139de10a..bcff1831 100644 --- a/src/renderer/shared/lib/utils.ts +++ b/src/renderer/shared/lib/utils.ts @@ -9,7 +9,7 @@ export const checkInternetAccess = async (): Promise => { try { const response = await rendererHttpClient.get(`${config.SERVER_v2_URL}/api/v2/health`, { responseType: 'text', - timeoutMs: 10000 + timeoutMs: 10000, }) return response.status === 200 } catch (error) { diff --git a/src/renderer/shared/ui/PSUI/ExtensionCardStore/card.module.scss b/src/renderer/shared/ui/PSUI/ExtensionCardStore/card.module.scss index 2549c001..3fabc73d 100644 --- a/src/renderer/shared/ui/PSUI/ExtensionCardStore/card.module.scss +++ b/src/renderer/shared/ui/PSUI/ExtensionCardStore/card.module.scss @@ -182,6 +182,8 @@ overflow: hidden; text-overflow: ellipsis; text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); + + user-select: text; } .card_title_version { @@ -198,6 +200,8 @@ font-weight: 700; line-height: 1; white-space: nowrap; + + user-select: text; } .card_title_verified { @@ -222,6 +226,8 @@ -webkit-line-clamp: 2; -webkit-box-orient: vertical; text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); + + user-select: text; } .card_meta { diff --git a/src/renderer/shared/ui/PSUI/NavButton/index.tsx b/src/renderer/shared/ui/PSUI/NavButton/index.tsx index f29e5b96..927a976d 100644 --- a/src/renderer/shared/ui/PSUI/NavButton/index.tsx +++ b/src/renderer/shared/ui/PSUI/NavButton/index.tsx @@ -8,7 +8,7 @@ interface NavButtonPulseProps { text: string children: React.ReactNode disabled?: boolean - onClick?: () => void + onClick?: React.MouseEventHandler tipEnabled?: boolean end?: boolean } diff --git a/src/renderer/shared/ui/PSUI/SelectInput/index.tsx b/src/renderer/shared/ui/PSUI/SelectInput/index.tsx index b4181fb6..c5ad9715 100644 --- a/src/renderer/shared/ui/PSUI/SelectInput/index.tsx +++ b/src/renderer/shared/ui/PSUI/SelectInput/index.tsx @@ -56,7 +56,12 @@ const SelectInput: React.FC = ({ return options } - return options.filter(option => String(option.searchText ?? option.label).trim().toLowerCase().includes(normalizedQuery)) + return options.filter(option => + String(option.searchText ?? option.label) + .trim() + .toLowerCase() + .includes(normalizedQuery), + ) }, [options, searchable, searchQuery]) const idxByValue = useMemo(() => filteredOptions.findIndex(o => String(o.value) === String(value)), [filteredOptions, value]) @@ -127,8 +132,7 @@ const SelectInput: React.FC = ({ if (next) { updatePanelLayout() setHover(idxByValue >= 0 ? idxByValue : 0) - } - else setSearchQuery('') + } else setSearchQuery('') return next }) } diff --git a/src/renderer/shared/ui/PSUI/Shimmer/styles/_skeleton.scss b/src/renderer/shared/ui/PSUI/Shimmer/styles/_skeleton.scss index 74e26b2a..92f760fa 100644 --- a/src/renderer/shared/ui/PSUI/Shimmer/styles/_skeleton.scss +++ b/src/renderer/shared/ui/PSUI/Shimmer/styles/_skeleton.scss @@ -17,7 +17,7 @@ inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.12) 42%, rgba(255, 255, 255, 0.22) 50%, transparent 100%); - animation: shimmer-slide 1.35s ease-in-out .75s infinite; + animation: shimmer-slide 1.35s ease-in-out 0.75s infinite; } } diff --git a/src/renderer/shared/ui/PSUI/Shimmer/variants/ModChangelogShimmer.tsx b/src/renderer/shared/ui/PSUI/Shimmer/variants/ModChangelogShimmer.tsx index fc800fe8..a40af7da 100644 --- a/src/renderer/shared/ui/PSUI/Shimmer/variants/ModChangelogShimmer.tsx +++ b/src/renderer/shared/ui/PSUI/Shimmer/variants/ModChangelogShimmer.tsx @@ -18,11 +18,7 @@ export default function ModChangelogShimmer() { {Array.from({ length: bulletCount }, (_, bulletIndex) => (
-
+
))}
diff --git a/src/renderer/shared/ui/PSUI/Shimmer/variants/ProfileShimmer.module.scss b/src/renderer/shared/ui/PSUI/Shimmer/variants/ProfileShimmer.module.scss index 0eb034f0..55b4d60a 100644 --- a/src/renderer/shared/ui/PSUI/Shimmer/variants/ProfileShimmer.module.scss +++ b/src/renderer/shared/ui/PSUI/Shimmer/variants/ProfileShimmer.module.scss @@ -41,11 +41,20 @@ gap: 15px; } +.profileMetaContainer { + display: flex; + width: 100%; + flex-direction: row; + gap: 15px; + align-items: center; + justify-content: space-between; +} + .identity { display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; + flex-direction: row; + align-items: center; + justify-content: start; gap: 15px; position: relative; z-index: 1; @@ -120,12 +129,12 @@ position: relative; z-index: 1; width: 100%; - margin-top: 4px; + justify-content: end; } .buttonFull { @include shimmer.surface; - width: 100%; + width: 200px; height: 40px; border-radius: 8px; } @@ -133,7 +142,7 @@ .section { @include shimmer.surface(#292c36); min-height: 240px; - padding: 20px 40px; + padding: 10px 40px; display: flex; flex-direction: column; gap: 15px; diff --git a/src/renderer/shared/ui/PSUI/Shimmer/variants/ProfileShimmer.tsx b/src/renderer/shared/ui/PSUI/Shimmer/variants/ProfileShimmer.tsx index 0ea659ee..24a32f25 100644 --- a/src/renderer/shared/ui/PSUI/Shimmer/variants/ProfileShimmer.tsx +++ b/src/renderer/shared/ui/PSUI/Shimmer/variants/ProfileShimmer.tsx @@ -10,34 +10,30 @@ export default function ProfileShimmer() {
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- -
-
+
+
+
-
-
-
-
-
@@ -52,6 +48,11 @@ export default function ProfileShimmer() {
+
+
+
+
+
{Array.from({ length: 3 }, (_, index) => ( diff --git a/src/renderer/widgets/layout/ExperimentOverridesDevButton.tsx b/src/renderer/widgets/layout/ExperimentOverridesDevButton.tsx index a1e8484c..b6a8248f 100644 --- a/src/renderer/widgets/layout/ExperimentOverridesDevButton.tsx +++ b/src/renderer/widgets/layout/ExperimentOverridesDevButton.tsx @@ -14,7 +14,7 @@ const ExperimentOverridesDevButton: React.FC = () => { const label = t('header.devOverrides.open') return ( - + diff --git a/src/renderer/widgets/layout/NotificationsBell.tsx b/src/renderer/widgets/layout/NotificationsBell.tsx index 1b5f74fb..8f19e528 100644 --- a/src/renderer/widgets/layout/NotificationsBell.tsx +++ b/src/renderer/widgets/layout/NotificationsBell.tsx @@ -6,6 +6,7 @@ import config from '@common/appConfig' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { useNotifications } from '@app/providers/notifications' +import { useModalContext } from '@app/providers/modal' import { getNotificationPresentation, NotificationTone } from '@app/providers/notifications/presentation' import type { NotificationItem } from '@app/providers/notifications/types' import Loader from '@shared/ui/PSUI/Loader' @@ -56,6 +57,7 @@ const NotificationsBell: React.FC = () => { const { t } = useTranslation() const navigate = useNavigate() const notificationsContext = useNotifications() + const { Modals, openModal } = useModalContext() const [isOpen, setOpen] = useState(false) const rootRef = useRef(null) const notificationItems = notificationsContext.notifications @@ -128,22 +130,30 @@ const NotificationsBell: React.FC = () => { return read ? styles.notificationItemWarning : styles.notificationItemUnreadWarning }, []) - const openNotificationTarget = useCallback((notification: NotificationItem) => { - const internalPath = getInternalNotificationPath(notification.link) - if (internalPath) { - void navigate(internalPath) - return - } + const openNotificationTarget = useCallback( + (notification: NotificationItem) => { + if (notification.type === 'subscription.giveaway.started') { + openModal(Modals.SUBSCRIPTION_GIVEAWAYS) + return + } - const rawLink = notification.link?.trim() - const externalUrl = !rawLink - ? `${config.WEBSITE_URL}/contribute/localization` - : /^https?:\/\//i.test(rawLink) - ? rawLink - : `${config.WEBSITE_URL}${rawLink.startsWith('/') ? rawLink : `/${rawLink}`}` + const internalPath = getInternalNotificationPath(notification.link) + if (internalPath) { + void navigate(internalPath) + return + } - window.desktopEvents?.send(MainEvents.OPEN_EXTERNAL, externalUrl) - }, [navigate]) + const rawLink = notification.link?.trim() + const externalUrl = !rawLink + ? `${config.WEBSITE_URL}/contribute/localization` + : /^https?:\/\//i.test(rawLink) + ? rawLink + : `${config.WEBSITE_URL}${rawLink.startsWith('/') ? rawLink : `/${rawLink}`}` + + window.desktopEvents?.send(MainEvents.OPEN_EXTERNAL, externalUrl) + }, + [Modals.SUBSCRIPTION_GIVEAWAYS, navigate, openModal], + ) const handleNotificationClick = useCallback( async (notification: NotificationItem) => { @@ -195,7 +205,7 @@ const NotificationsBell: React.FC = () => { return (
- + + {activeCount > 0 && {activeCount > 99 ? '99+' : activeCount}} ) } diff --git a/src/renderer/widgets/layout/UpdateChannelOverrideButton.tsx b/src/renderer/widgets/layout/UpdateChannelOverrideButton.tsx index 86d84d3e..c047de1c 100644 --- a/src/renderer/widgets/layout/UpdateChannelOverrideButton.tsx +++ b/src/renderer/widgets/layout/UpdateChannelOverrideButton.tsx @@ -55,7 +55,7 @@ const UpdateChannelOverrideButton: React.FC = () => { }, [refreshStatus]) return ( - + - + {/**/} + {/* */} + {/**/}
-
- - - -
+ {!isMac && ( +
+ + + +
+ )}
diff --git a/src/renderer/widgets/layout/index.tsx b/src/renderer/widgets/layout/index.tsx index 36272b48..a88915d5 100644 --- a/src/renderer/widgets/layout/index.tsx +++ b/src/renderer/widgets/layout/index.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react' +import React, { useCallback, useContext } from 'react' import { Helmet, HelmetProvider } from '@dr.pogodin/react-helmet' import MainEvents from '@common/types/mainEvents' import { MdDownload, MdHandyman, MdHome, MdPeople, MdPower, MdStoreMallDirectory } from 'react-icons/md' @@ -17,6 +17,7 @@ import clsx from 'clsx' import { useTranslation } from 'react-i18next' import { useLayoutInstallers } from '@widgets/layout/model/useLayoutInstallers' import ModUpdateBanner from '@widgets/layout/ui/ModUpdateBanner' +import { useNavigate } from 'react-router-dom' interface LayoutProps { title: string @@ -25,10 +26,22 @@ interface LayoutProps { } const Layout: React.FC = ({ title, children, goBack }) => { - const { user, app, setApp, updateAvailable, setUpdate, modInfo, modInfoFetched, musicInstalled, setMusicInstalled, setMusicVersion, isAutonomousMode } = - useContext(userContext) + const { + user, + app, + setApp, + updateAvailable, + setUpdate, + modInfo, + modInfoFetched, + musicInstalled, + setMusicInstalled, + setMusicVersion, + isAutonomousMode, + } = useContext(userContext) const { t } = useTranslation() const { Modals, openModal } = useModalContext() + const navigate = useNavigate() const { isExperimentEnabled, loading: experimentsLoading } = useExperiments() const { isModUpdateAvailable, modInstallError, startUpdate, isUserDeveloper } = useLayoutInstallers({ app, @@ -49,6 +62,18 @@ const Layout: React.FC = ({ title, children, goBack }) => { }) const storePageEnabled = !experimentsLoading && isExperimentEnabled(CLIENT_EXPERIMENTS.ClientExtensionStoreAccess, false) const usersPageEnabled = !experimentsLoading && isExperimentEnabled(CLIENT_EXPERIMENTS.ClientUsersPageAccess, false) + const openAuthRequiredModal = useCallback( + (event: React.MouseEvent) => { + event.preventDefault() + openModal(Modals.BASIC_CONFIRMATION, { + title: t('layout.authRequired.title'), + description: t('layout.authRequired.description'), + confirmLabel: t('header.login'), + onConfirm: () => navigate('/auth'), + }) + }, + [Modals.BASIC_CONFIRMATION, navigate, openModal, t], + ) if (!modInfoFetched) { return @@ -70,21 +95,24 @@ const Layout: React.FC = ({ title, children, goBack }) => { diff --git a/src/renderer/widgets/layout/model/useLayoutInstallers.ts b/src/renderer/widgets/layout/model/useLayoutInstallers.ts index 980acff9..ec33376d 100644 --- a/src/renderer/widgets/layout/model/useLayoutInstallers.ts +++ b/src/renderer/widgets/layout/model/useLayoutInstallers.ts @@ -79,7 +79,9 @@ export function useLayoutInstallers({ (error: any) => { const rawMessage = typeof error?.error === 'string' ? error.error.trim() : '' const normalizedMessage = - rawMessage && rawMessage.toLowerCase().includes('aborted') ? t('layout.modInstallInterrupted') : rawMessage || t('layout.unknownError') + rawMessage && rawMessage.toLowerCase().includes('aborted') + ? t('layout.modInstallInterrupted') + : rawMessage || t('layout.unknownError') const isUpdate = currentModActionRef.current === 'update' const details = errorTypesToShow.has(error?.type) @@ -253,78 +255,57 @@ export function useLayoutInstallers({ window.desktopEvents?.removeAllListeners(RendererEvents.DOWNLOAD_FAILURE) ;(window as any).__listenersAdded = false } - }, [ - modals.LINUX_PERMISSIONS_MODAL, - modals.MOD_CHANGELOG, - openModal, - readInstalledModFromStore, - setApp, - setMusicInstalled, - setMusicVersion, - t, - ]) - - const startUpdate = useCallback( - () => { - if (window.electron.isLinux()) { - const savedPath = window.electron.store.get('settings.modSavePath') - if (!savedPath) { - openModal(modals.LINUX_ASAR_PATH) - return - } - } - if (isUpdating) { - toast.custom( - 'error', - t('common.errorTitle'), - app.mod.installed ? t('layout.modUpdateAlreadyRunning') : t('layout.modInstallAlreadyRunning'), - ) - return - } - if (modInfo.length === 0) { - toast.custom( - 'error', - app.mod.installed ? t('layout.noModUpdatesAvailable') : t('layout.noModInstallsAvailable'), - app.mod.installed ? t('layout.modUpdateLoadError') : t('layout.modInstallErrorTitle'), - ) + }, [modals.LINUX_PERMISSIONS_MODAL, modals.MOD_CHANGELOG, openModal, readInstalledModFromStore, setApp, setMusicInstalled, setMusicVersion, t]) + + const startUpdate = useCallback(() => { + if (window.electron.isLinux()) { + const savedPath = window.electron.store.get('settings.modSavePath') + if (!savedPath) { + openModal(modals.LINUX_ASAR_PATH) return } + } + if (isUpdating) { + toast.custom( + 'error', + t('common.errorTitle'), + app.mod.installed ? t('layout.modUpdateAlreadyRunning') : t('layout.modInstallAlreadyRunning'), + ) + return + } + if (modInfo.length === 0) { + toast.custom( + 'error', + app.mod.installed ? t('layout.noModUpdatesAvailable') : t('layout.noModInstallsAvailable'), + app.mod.installed ? t('layout.modUpdateLoadError') : t('layout.modInstallErrorTitle'), + ) + return + } - setIsUpdating(true) - setModInstallError(null) - currentModActionRef.current = app.mod.installed ? 'update' : 'install' - const id = toast.custom('loading', app.mod.installed ? t('layout.modUpdateStart') : t('layout.modInstallStart'), t('common.pleaseWait'), { - id: MOD_DOWNLOAD_TOAST_ID, - duration: Infinity, - }) - downloadToastIdRef.current = id - - const { - modVersion, - realMusicVersion, - downloadUrl, - checksum_v2, - name, - shouldReinstall, - downloadUnpackedUrl, - unpackedChecksum, - source, - } = modInfo[0] - - window.desktopEvents?.send(MainEvents.INSTALL_MOD, { - version: modVersion, - musicVersion: realMusicVersion, - name, - link: downloadUrl, - unpackLink: downloadUnpackedUrl, - unpackedChecksum, - checksum: checksum_v2, - shouldReinstall, - source: source || 'backend', - }) - }, - [app.mod.installed, isUpdating, modInfo, modals.LINUX_ASAR_PATH, openModal, t], - ) + setIsUpdating(true) + setModInstallError(null) + currentModActionRef.current = app.mod.installed ? 'update' : 'install' + const id = toast.custom('loading', app.mod.installed ? t('layout.modUpdateStart') : t('layout.modInstallStart'), t('common.pleaseWait'), { + id: MOD_DOWNLOAD_TOAST_ID, + duration: Infinity, + }) + downloadToastIdRef.current = id + + const { modVersion, realMusicVersion, downloadUrl, checksum_v2, name, shouldReinstall, downloadUnpackedUrl, unpackedChecksum, source } = + modInfo[0] + + window.desktopEvents?.send(MainEvents.INSTALL_MOD, { + version: modVersion, + musicVersion: realMusicVersion, + name, + link: downloadUrl, + unpackLink: downloadUnpackedUrl, + unpackedChecksum, + checksum: checksum_v2, + shouldReinstall, + source: source || 'backend', + }) + }, [app.mod.installed, isUpdating, modInfo, modals.LINUX_ASAR_PATH, openModal, t]) useEffect(() => { if (!modInfoFetched || modInfo.length === 0 || isUpdating || !app.mod.installed || !app.mod.version) return diff --git a/src/renderer/widgets/layout/ui/HeaderModals.tsx b/src/renderer/widgets/layout/ui/HeaderModals.tsx index b6ee2c70..901e48df 100644 --- a/src/renderer/widgets/layout/ui/HeaderModals.tsx +++ b/src/renderer/widgets/layout/ui/HeaderModals.tsx @@ -74,18 +74,18 @@ export default function HeaderModals({ {!loadingAppUpdates && !appError && visibleAppUpdates.map(info => ( -
-
-

{info.version}

- {formatDate(info.createdAt)} -
-
- - {info.changelog} - -
+
+
+

{info.version}

+ {formatDate(info.createdAt)}
- ))} +
+ + {info.changelog} + +
+
+ ))} {!loadingAppUpdates && !appError && visibleAppUpdates.length === 0 &&

{t('header.noChangelogFound')}

}
diff --git a/src/renderer/widgets/layout/ui/ModUpdateBanner.tsx b/src/renderer/widgets/layout/ui/ModUpdateBanner.tsx index 679a4d37..9cfde373 100644 --- a/src/renderer/widgets/layout/ui/ModUpdateBanner.tsx +++ b/src/renderer/widgets/layout/ui/ModUpdateBanner.tsx @@ -20,14 +20,7 @@ type Props = { t: (key: string, options?: Record) => string } -export default function ModUpdateBanner({ - app, - isModUpdateAvailable, - modInstallError, - modInfo, - onStartUpdate, - t, -}: Props) { +export default function ModUpdateBanner({ app, isModUpdateAvailable, modInstallError, modInfo, onStartUpdate, t }: Props) { if (!isModUpdateAvailable) return null return ( @@ -65,7 +58,7 @@ export default function ModUpdateBanner({ {modInstallError?.showProxyHint && (
{t('layout.modInstallErrorProxyHint')}
- {modInstallProxyDomains.map((domain) => ( + {modInstallProxyDomains.map(domain => (
{domain}
))}
diff --git a/src/renderer/widgets/modalContainer/modals/ExperimentOverridesDevModal.tsx b/src/renderer/widgets/modalContainer/modals/ExperimentOverridesDevModal.tsx index 6eb8393e..ea8531dc 100644 --- a/src/renderer/widgets/modalContainer/modals/ExperimentOverridesDevModal.tsx +++ b/src/renderer/widgets/modalContainer/modals/ExperimentOverridesDevModal.tsx @@ -248,7 +248,9 @@ const ExperimentOverridesDevModal: React.FC = () => { {sortedExperiments.map(experiment => { const isSelected = experiment.key === selectedKey const hasOverride = Boolean(localOverrides[experiment.key]) - const activeExperiment = hasOverride ? localOverrides[experiment.key] : experiments.find(active => active.key === experiment.key) + const activeExperiment = hasOverride + ? localOverrides[experiment.key] + : experiments.find(active => active.key === experiment.key) return (
{activeExperiment?.group || t('header.devOverrides.noGroup')} - {t('header.devOverrides.groupsCount', { count: experiment.groups.length })} + + {t('header.devOverrides.groupsCount', { count: experiment.groups.length })} +
) @@ -291,7 +295,9 @@ const ExperimentOverridesDevModal: React.FC = () => { {selectedExperiment.description &&

{selectedExperiment.description}

}
- {t('header.devOverrides.groupsCount', { count: selectedExperiment.groups.length })} + + {t('header.devOverrides.groupsCount', { count: selectedExperiment.groups.length })} + {selectedOverride && {t('header.devOverrides.overrideActive')}}
@@ -308,7 +314,9 @@ const ExperimentOverridesDevModal: React.FC = () => { >
{group.group} - {t('header.devOverrides.groupRollout', { percentage: group.rollout })} + + {t('header.devOverrides.groupRollout', { percentage: group.rollout })} +
{group.description || t('header.devOverrides.groupDescriptionEmpty')} diff --git a/src/renderer/widgets/modalContainer/modals/ExtensionPublicationModal.tsx b/src/renderer/widgets/modalContainer/modals/ExtensionPublicationModal.tsx index 271d6e8c..b4689ab7 100644 --- a/src/renderer/widgets/modalContainer/modals/ExtensionPublicationModal.tsx +++ b/src/renderer/widgets/modalContainer/modals/ExtensionPublicationModal.tsx @@ -39,9 +39,18 @@ function PublicationCheckbox({ checked, onChange, children }: PublicationCheckbo const ExtensionPublicationModal: React.FC = () => { const { t, i18n } = useTranslation() const { Modals, closeModal, isModalOpen, getModalState, setModalState } = useModalContext() - const { addon, authorsDisplay, publication, publicationBusy, changelogText, githubUrlText, onChangeChangelog, onChangeGithubUrl, onPublish, onUpdate } = getModalState( - Modals.EXTENSION_PUBLICATION_MODAL, - ) + const { + addon, + authorsDisplay, + publication, + publicationBusy, + changelogText, + githubUrlText, + onChangeChangelog, + onChangeGithubUrl, + onPublish, + onUpdate, + } = getModalState(Modals.EXTENSION_PUBLICATION_MODAL) const isPublicationModalOpen = isModalOpen(Modals.EXTENSION_PUBLICATION_MODAL) const publicationRelease = publication?.currentRelease const [rulesAccepted, setRulesAccepted] = useState(false) diff --git a/src/renderer/widgets/modalContainer/modals/SubscriptionGiveawaysModal.module.scss b/src/renderer/widgets/modalContainer/modals/SubscriptionGiveawaysModal.module.scss index fbe9df37..c73e55e3 100644 --- a/src/renderer/widgets/modalContainer/modals/SubscriptionGiveawaysModal.module.scss +++ b/src/renderer/widgets/modalContainer/modals/SubscriptionGiveawaysModal.module.scss @@ -95,7 +95,7 @@ } span:not(:last-child)::after { - content: ""; + content: ''; width: 4px; height: 4px; display: inline-flex; @@ -173,7 +173,7 @@ background: #2c313d; &::after { - content: ""; + content: ''; position: absolute; inset: 0; transform: translateX(-100%); @@ -340,7 +340,7 @@ } span:not(:last-child)::after { - content: ""; + content: ''; width: 3px; height: 3px; display: inline-flex; diff --git a/src/renderer/widgets/modalContainer/modals/SubscriptionGiveawaysModal.tsx b/src/renderer/widgets/modalContainer/modals/SubscriptionGiveawaysModal.tsx index dd312995..84bc17ef 100644 --- a/src/renderer/widgets/modalContainer/modals/SubscriptionGiveawaysModal.tsx +++ b/src/renderer/widgets/modalContainer/modals/SubscriptionGiveawaysModal.tsx @@ -8,27 +8,15 @@ import remarkGfm from 'remark-gfm' import { useTranslation } from 'react-i18next' import { useModalContext } from '@app/providers/modal' import rendererHttpClient from '@shared/api/http/client' +import { + invalidateSubscriptionGiveawaysSnapshot, + loadSubscriptionGiveawaysSnapshot, + type SubscriptionGiveaway, +} from '@shared/api/subscriptionGiveaways' import CustomModalPS from '@shared/ui/PSUI/CustomModalPS' import toast from '@shared/ui/toast' import * as styles from '@widgets/modalContainer/modals/SubscriptionGiveawaysModal.module.scss' -type SubscriptionGiveaway = { - uuid: string - title: string - description?: string | null - planCode: string - durationMonths?: number | null - winnersCount: number - status: string - startsAt: string - endsAt: string -} - -type SubscriptionGiveawaysResponse = { - ok?: boolean - giveaways?: SubscriptionGiveaway[] -} - type SubscriptionGiveawayEntryResponse = { ok?: boolean } @@ -137,12 +125,9 @@ const SubscriptionGiveawaysModal: React.FC = () => { setError(null) try { - const response = await rendererHttpClient.get('/subscription/giveaways') - if (!response.ok || !response.data?.ok || !Array.isArray(response.data.giveaways)) { - throw new Error('Failed to load subscription giveaways') - } - - setGiveaways(response.data.giveaways) + const snapshot = await loadSubscriptionGiveawaysSnapshot({ force: true }) + setGiveaways(snapshot.giveaways) + setEnteredIds(new Set(snapshot.enteredIds)) } catch (loadError) { console.error('Failed to load subscription giveaways:', loadError) setError(t('header.giveaways.loadError')) @@ -217,7 +202,13 @@ const SubscriptionGiveawaysModal: React.FC = () => { hasEntered, remainingLabel: formatRemaining(endsAtTime), endsAtLabel: formatDate(giveaway.endsAt), - actionLabel: hasEntered ? t('header.giveaways.entered') : hasEnded ? t('header.giveaways.ended') : !hasStarted ? t('header.giveaways.notStarted') : t('header.giveaways.join'), + actionLabel: hasEntered + ? t('header.giveaways.entered') + : hasEnded + ? t('header.giveaways.ended') + : !hasStarted + ? t('header.giveaways.notStarted') + : t('header.giveaways.join'), } }) .sort((a, b) => a.endsAtTime - b.endsAtTime) @@ -240,6 +231,7 @@ const SubscriptionGiveawaysModal: React.FC = () => { throw new Error('Failed to enter subscription giveaway') } + invalidateSubscriptionGiveawaysSnapshot() setEnteredIds(current => new Set(current).add(giveaway.uuid)) toast.custom('success', t('header.giveaways.enteredTitle'), t('header.giveaways.enteredText', { title: giveaway.title })) } catch (enterError) { @@ -306,13 +298,16 @@ const SubscriptionGiveawaysModal: React.FC = () => {
{item.isEnterable || isJoining ? ( - ) : ( - - {item.actionLabel} - + {item.actionLabel} )}
@@ -337,7 +332,13 @@ const SubscriptionGiveawaysModal: React.FC = () => {
-
)}
-
-
{t('profile.achievements.title')}
-
{t('profile.achievements.subtitle')}
-
+
+
{t('profile.achievements.title')}
+
{t('profile.achievements.subtitle')}
+
{userProfile.allAchievements && userProfile.allAchievements.length > 0 ? ( <>
diff --git a/src/renderer/widgets/userProfileModal/tabs/ProfileTab/FriendButton.tsx b/src/renderer/widgets/userProfileModal/tabs/ProfileTab/FriendButton.tsx index 56f23aee..981ffdb0 100644 --- a/src/renderer/widgets/userProfileModal/tabs/ProfileTab/FriendButton.tsx +++ b/src/renderer/widgets/userProfileModal/tabs/ProfileTab/FriendButton.tsx @@ -60,7 +60,7 @@ const FriendButton: React.FC = ({ userProfile, user, profileN } let buttonTextNormal = t('profile.friendButton.addFriend') - let buttonTextHover = t('profile.friendButton.follow') + let buttonTextHover = t('profile.friendButton.addFriend') let normalIcon = let hoverIcon = let buttonClass = styles.buttonAddFriendWhite diff --git a/src/renderer/widgets/userProfileModal/tabs/ProfileTab/ProfileHeader.tsx b/src/renderer/widgets/userProfileModal/tabs/ProfileTab/ProfileHeader.tsx index 327325e1..8ee84f50 100644 --- a/src/renderer/widgets/userProfileModal/tabs/ProfileTab/ProfileHeader.tsx +++ b/src/renderer/widgets/userProfileModal/tabs/ProfileTab/ProfileHeader.tsx @@ -45,7 +45,7 @@ const ProfileHeader: React.FC = ({ userProfile, user, childr const updateAllowAnimate = () => { const scrollTop = useWindowScroll ? window.scrollY || window.pageYOffset || document.documentElement.scrollTop || 0 - : scrollContainer?.scrollTop ?? 0 + : (scrollContainer?.scrollTop ?? 0) setAllowAnimate(scrollTop < threshold) } @@ -76,91 +76,98 @@ const ProfileHeader: React.FC = ({ userProfile, user, childr allowAnimate={allowAnimate} />
-
- -
-
+
+
+ +
- {new Date(userProfile.createdAt) <= new Date(2025, 0, 17) ? ( - - {new Date(userProfile.createdAt).toLocaleString(i18n.language, { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} -
- } - side="top" - > - {t('profile.sinceBeginning')} - - ) : ( - - {new Date(userProfile.createdAt).toLocaleString(i18n.language, { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} -
- } - side="top" - > - {t('profile.registrationDate')}{' '} - {new Date(userProfile.createdAt).toLocaleDateString(i18n.language, { - month: 'long', - year: 'numeric', - })} - - )} +
+ {new Date(userProfile.createdAt) <= new Date(2025, 0, 17) ? ( + + {new Date(userProfile.createdAt).toLocaleString(i18n.language, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} +
+ } + side="top" + > + {t('profile.sinceBeginning')} + + ) : ( + + {new Date(userProfile.createdAt).toLocaleString(i18n.language, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} +
+ } + side="top" + > + {t('profile.registrationDate')}{' '} + {new Date(userProfile.createdAt).toLocaleDateString(i18n.language, { + month: 'long', + year: 'numeric', + })} + + )} +
-
-
- {userProfile.nickname || t('profile.noNickname')} -
- - - - {visibleBadges.length > 0 && - visibleBadges - .sort((a: any, b: any) => (b.level ?? 0) - (a.level ?? 0)) - .map((badge: any) => ( - - {badge.type} - - ))} +
+ {userProfile.nickname || t('profile.noNickname')} +
+ + + + {visibleBadges.length > 0 && + visibleBadges + .sort((a: any, b: any) => (b.level ?? 0) - (a.level ?? 0)) + .map((badge: any) => ( + + {badge.type} + + ))} +
+
@{userProfile.username}
-
@{userProfile.username}
+
{children}
-
{children}
) } diff --git a/src/renderer/widgets/userProfileModal/userProfileModal.module.scss b/src/renderer/widgets/userProfileModal/userProfileModal.module.scss index ee73dab0..556db7a7 100644 --- a/src/renderer/widgets/userProfileModal/userProfileModal.module.scss +++ b/src/renderer/widgets/userProfileModal/userProfileModal.module.scss @@ -123,6 +123,15 @@ padding: 0; } +.profileMetaContainer { + width: 100%; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + .bannerBackground { width: 100%; height: 350px; @@ -203,9 +212,9 @@ .userImage { display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; + flex-direction: row; + justify-content: start; + align-items: center; gap: 15px; width: 100%; position: relative; @@ -250,6 +259,7 @@ display: flex; flex-direction: row; align-items: center; + justify-content: end; gap: 10px; width: 100%; position: relative; @@ -268,7 +278,7 @@ border-radius: 8px; padding: 10px 15px; gap: 8px; - width: -webkit-fill-available; + width: fit-content; svg { margin-right: 5px; @@ -324,7 +334,7 @@ align-items: flex-start; padding: 10px 15px; gap: 10px; - width: 100%; + width: fit-content; background: #ffffff; border-radius: 8px; font-weight: 700; @@ -404,7 +414,7 @@ display: flex; flex-direction: column; align-items: flex-start; - padding: 20px 40px; + padding: 10px 40px; gap: 15px; width: 100%; background: #292c36; diff --git a/static/assets/policy/terms.ru.md b/static/assets/policy/terms.ru.md index dbf1d16e..3fafe92b 100644 --- a/static/assets/policy/terms.ru.md +++ b/static/assets/policy/terms.ru.md @@ -42,7 +42,7 @@ 6.1. Лицензиар не несет ответственности за любые убытки, возникающие в результате использования или невозможности использования Программного обеспечения, даже если Лицензиар был уведомлен о возможности таких убытков. - 6.2. Пользователь настоящим уведомлен и соглашается, что при использовании Программного обеспечения Лицензиару в автоматическом режиме передаётся следующая информация: тип операционной системы Пользователя, версия и идентификатор Программного обеспечения, статистика использования функций Программного обеспечения, а также иная техническая информация. + 6.2. Пользователь настоящим уведомлен и соглашается, что при использовании Программного обеспечения Лицензиару в автоматическом режиме передаётся следующая информация: тип операционной системы Пользователя, версия и идентификатор Программного обеспечения, статистика использования функций Программного обеспечения, сведения о подлинности сборки, а также технический идентификатор устройства в виде SHA-256 хэша. Исходный аппаратный идентификатор устройства Лицензиару не передается и не хранится. Указанные сведения используются для защиты аккаунтов, предотвращения злоупотреблений, выявления неофициальных или модифицированных клиентов и проверки подлинности клиентского приложения. 6.3. Пользователь настоящим уведомлен и соглашается, что при использовании программного обеспечения он будет автоматически подключен к Discord-серверу Лицензиара. @@ -78,7 +78,7 @@ Используя Программное обеспечение, вы подтверждаете, что прочитали и поняли настоящее соглашение и соглашаетесь соблюдать его условия. -Матвиенко Артём Евгеньевич 18.05.2026 +Матвиенко Артём Евгеньевич 17.06.2026 ЛИЦЕНЗИОННОЕ СОГЛАШЕНИЕ С КОНЕЧНЫМ ПОЛЬЗОВАТЕЛЕМ (EULA) ДЛЯ «PulseSync-backend» @@ -122,7 +122,7 @@ 6.1. Лицензиар не несет ответственности за любые убытки, возникающие в результате использования или невозможности использования Программного обеспечения, даже если Лицензиар был уведомлен о возможности таких убытков. - 6.2. Пользователь настоящим уведомлен и соглашается, что при использовании Программного обеспечения Лицензиару в автоматическом режиме передаётся следующая информация: тип операционной системы Пользователя, версия и идентификатор Программного обеспечения, статистика использования функций Программного обеспечения, а также иная техническая информация. + 6.2. Пользователь настоящим уведомлен и соглашается, что при использовании Программного обеспечения Лицензиару в автоматическом режиме передаётся следующая информация: тип операционной системы Пользователя, версия и идентификатор Программного обеспечения, статистика использования функций Программного обеспечения, сведения о подлинности сборки, а также технический идентификатор устройства в виде SHA-256 хэша. Исходный аппаратный идентификатор устройства Лицензиару не передается и не хранится. Указанные сведения используются для защиты аккаунтов, предотвращения злоупотреблений, выявления неофициальных или модифицированных клиентов и проверки подлинности клиентского приложения. 6.3. Пользователь настоящим уведомлен и соглашается, что при использовании Программного обеспечения он может прямо или косвенно нарушить Лицензионное соглашение на использование программы «Яндекс Музыка». @@ -142,4 +142,4 @@ Используя Программное обеспечение, вы подтверждаете, что прочитали и поняли настоящее соглашение и соглашаетесь соблюдать его условия. -Матвиенко Артём Евгеньевич 18.05.2026 +Матвиенко Артём Евгеньевич 17.06.2026 diff --git a/tsconfig.node.json b/tsconfig.node.json index 7c9cdc4e..df863e8e 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -13,6 +13,7 @@ "declarations.d.ts", "vite.main.config.ts", "vite.preload.config.ts", + "vite.worker.config.ts", "vite.renderer.config.ts", "forge.config.ts", "scripts/**/*.ts" diff --git a/vite-env.d.ts b/vite-env.d.ts index f80506ed..81e6dc38 100644 --- a/vite-env.d.ts +++ b/vite-env.d.ts @@ -10,6 +10,10 @@ declare const SETTINGS_WINDOW_VITE_NAME: string declare const PRELOADER_VITE_DEV_SERVER_URL: string declare const PRELOADER_VITE_NAME: string +declare const PULSESYNC_VERSION: string +declare const PULSESYNC_BRANCH: string +declare const PULSESYNC_DIST: string + interface ImportMetaEnv { readonly VITE_APP_TITLE: string } diff --git a/vite.main.config.ts b/vite.main.config.ts index 3cb402fa..2933d75a 100644 --- a/vite.main.config.ts +++ b/vite.main.config.ts @@ -8,14 +8,16 @@ const packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'package. version: string buildInfo?: { BRANCH?: string } } +const buildDist = process.env.PULSESYNC_BUILD_DIST || `${process.platform}-${process.arch}` export default defineConfig(({ mode, forgeConfigSelf }: any): UserConfig => { const isDevMode = mode === 'development' + const sourceMapMode = isDevMode ? true : process.env.GLITCHTIP_SOURCEMAPS === '1' ? 'hidden' : false const entry = forgeConfigSelf?.entry ?? 'src/index.ts' return { build: { - sourcemap: isDevMode, + sourcemap: sourceMapMode, target: 'node24.14', outDir: path.resolve(__dirname, `.vite/main`), lib: { @@ -33,6 +35,9 @@ export default defineConfig(({ mode, forgeConfigSelf }: any): UserConfig => { }, define: { + PULSESYNC_VERSION: JSON.stringify(packageJson.version), + PULSESYNC_BRANCH: JSON.stringify(packageJson.buildInfo?.BRANCH ?? 'unknown'), + PULSESYNC_DIST: JSON.stringify(buildDist), 'process.env.BRANCH': JSON.stringify((packageJson as any).buildInfo?.BRANCH), 'process.env.VERSION': JSON.stringify(packageJson.version), 'import.meta.env.DEV': JSON.stringify(isDevMode), diff --git a/vite.preload.config.ts b/vite.preload.config.ts index 6ae23274..f46e8f1f 100644 --- a/vite.preload.config.ts +++ b/vite.preload.config.ts @@ -6,6 +6,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) export default defineConfig(({ mode, forgeConfigSelf }: any) => { const isDevMode = mode === 'development' + const sourceMapMode = isDevMode ? true : process.env.GLITCHTIP_SOURCEMAPS === '1' ? 'hidden' : false const entry = forgeConfigSelf?.entry ?? 'src/main/mainWindowPreload.ts' return { @@ -30,7 +31,7 @@ export default defineConfig(({ mode, forgeConfigSelf }: any) => { }, }, build: { - sourcemap: isDevMode, + sourcemap: sourceMapMode, target: 'node24.14', outDir: path.resolve(__dirname, `.vite/main`), rolldownOptions: { diff --git a/vite.renderer.config.ts b/vite.renderer.config.ts index 5bfcc7a5..4b7f4542 100644 --- a/vite.renderer.config.ts +++ b/vite.renderer.config.ts @@ -7,6 +7,11 @@ import fs from 'fs' import { fileURLToPath } from 'node:url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8')) as { + version: string + buildInfo?: { BRANCH?: string } +} +const buildDist = process.env.PULSESYNC_BUILD_DIST || `${process.platform}-${process.arch}` const rendererHtmlEntries: Record = { main_window: 'src/renderer/index.html', @@ -23,6 +28,7 @@ export default defineConfig(({ mode, forgeConfigSelf }: any) => { const isDevMode = mode === 'development' const isDevSourceMapMode = process.env.NODE_ENV === 'development' + const sourceMapMode = isDevSourceMapMode ? true : process.env.GLITCHTIP_SOURCEMAPS === '1' ? 'hidden' : false const rendererAssetsDir = path.resolve(__dirname, '.vite/renderer/assets') const staticAssetsDir = path.resolve(__dirname, 'static/assets') const publicDir: string | false = isDevMode ? path.resolve(__dirname, 'static') : false @@ -32,6 +38,9 @@ export default defineConfig(({ mode, forgeConfigSelf }: any) => { base: isDevMode ? '/' : './', publicDir, define: { + PULSESYNC_VERSION: JSON.stringify(packageJson.version), + PULSESYNC_BRANCH: JSON.stringify(packageJson.buildInfo?.BRANCH ?? 'unknown'), + PULSESYNC_DIST: JSON.stringify(buildDist), 'import.meta.env.DEV': JSON.stringify(isDevMode), 'import.meta.env.PROD': JSON.stringify(!isDevMode), }, @@ -44,7 +53,7 @@ export default defineConfig(({ mode, forgeConfigSelf }: any) => { cors: true, }, build: { - sourcemap: isDevSourceMapMode, + sourcemap: sourceMapMode, target: 'chrome146', outDir: path.resolve(__dirname, `.vite/renderer/${name}`), assetsDir: '../assets', diff --git a/vite.worker.config.ts b/vite.worker.config.ts new file mode 100644 index 00000000..5517e692 --- /dev/null +++ b/vite.worker.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, type UserConfig } from 'vite' +import path from 'path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig(({ mode, forgeConfigSelf }: any): UserConfig => { + const isDevMode = mode === 'development' + const sourceMapMode = isDevMode ? true : process.env.GLITCHTIP_SOURCEMAPS === '1' ? 'hidden' : false + const entry = forgeConfigSelf?.entry ?? 'src/main/modules/mod/network/artifactWorker.ts' + + return { + build: { + sourcemap: sourceMapMode, + target: 'node24.14', + outDir: path.resolve(__dirname, '.vite/worker'), + ssr: entry, + rolldownOptions: { + output: { + format: 'cjs' as const, + entryFileNames: 'artifactWorker.cjs', + preserveModules: false, + }, + }, + }, + define: { + __non_vite_require__: 'require', + }, + } +}) diff --git a/window.d.ts b/window.d.ts index 906f6a92..d21054d9 100644 --- a/window.d.ts +++ b/window.d.ts @@ -1,5 +1,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios' import { Track } from './src/renderer/api/interfaces/track.interface' +import type { ClientBuildIdentity } from './src/common/types/clientBuildIdentity' +import type { ClientHardwareIdentity } from './src/common/types/clientHardwareIdentity' interface DesktopEvents { emit(channel: string, ...args: any[]): void @@ -47,6 +49,8 @@ declare global { appInfo: { getBranch: () => string getVersion: () => string + getHardwareIdentity: () => ClientHardwareIdentity | null + getBuildIdentity: () => ClientBuildIdentity } } } diff --git a/yarn.lock b/yarn.lock index 3a8a969e..68a05627 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,15 +2,41 @@ # yarn lockfile v1 -"7zip-bin@~5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.2.0.tgz#7a03314684dd6572b7dfa89e68ce31d60286854d" - integrity sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A== +"@apm-js-collab/code-transformer-bundler-plugins@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.5.0.tgz#8a08136e8281f7e8e36ac6810d2b122c5917de93" + integrity sha512-YxLBY5nGlurL7QeJLq6e5g0ouBpAp0pwgyA/5rHXEXwhiPLn9ZHbT+Y2LlP90GT872cSocfjWRYu/fnpuBudNQ== + dependencies: + "@apm-js-collab/code-transformer" "^0.15.0" + es-module-lexer "^2.1.0" + magic-string "^0.30.21" + module-details-from-path "^1.0.4" -"@apollo/client@^4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@apollo/client/-/client-4.2.0.tgz#cd5d40f2fd04eabdda77c2e873a9e4c59ac0759f" - integrity sha512-uZAiXwIRidDqQKZRcL88O01IVZjY6IhLio6g+XzX4N4++Ue9pVK9WCoZvBm1dvx+x2mH0oGfVUZvTvacCW4WcQ== +"@apm-js-collab/code-transformer@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@apm-js-collab/code-transformer/-/code-transformer-0.15.0.tgz#a3a1b6c7b92db16f8277636b4a72a1626e2fa52a" + integrity sha512-XmXYVs8CzJ1Aj79noVbn2weUO/XWtRyURpGqx7aU7DOXlUQhR0WKOQNF0okh7PCeY37vxf7kU3v57OAkEPm3ww== + dependencies: + "@types/estree" "^1.0.8" + astring "^1.9.0" + esquery "^1.7.0" + meriyah "^6.1.4" + semifies "^1.0.0" + source-map "^0.6.0" + +"@apm-js-collab/tracing-hooks@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.10.0.tgz#b31f4bd474380475dc72f57f1b84dd71ba98edde" + integrity sha512-2/Z3NTewJTruUkmsSnBC5bJlLNUd9keuD1OLlTEpim4FyLhm6m2Rnfv+wrFdUvFfhmH8CRdiDZBqBrn+wyaGuA== + dependencies: + "@apm-js-collab/code-transformer" "^0.15.0" + debug "^4.4.1" + module-details-from-path "^1.0.4" + +"@apollo/client@^4.2.3": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-4.2.3.tgz#04177a6a170e3733346f9ae15103161abd50cdce" + integrity sha512-+auRYBXow2v7cT+wKzvjyMyyEojq+G7Sf80vIR57rtEPcxRFuMXuU9IKjwxZ3muclUgdGKwZXNeuki+g0GabgQ== dependencies: "@graphql-typed-document-node/core" "^3.1.1" "@wry/caches" "^1.0.0" @@ -88,264 +114,218 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-sdk/client-s3@^3.1053.0": - version "3.1053.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.1053.0.tgz#13568ec27b695d01c56c11c3c11afb6d3d96496a" - integrity sha512-/oGxoB6p1Nqs935Blt+v1o+anSCEf2n3RjIrcLz84i4cn2Gr+Z7JpDdUkG5+74r5ctqEPG7k/phTGbJ9fNKnHg== +"@aws-sdk/checksums@^3.1000.8": + version "3.1000.8" + resolved "https://registry.yarnpkg.com/@aws-sdk/checksums/-/checksums-3.1000.8.tgz#74733e693c3ca6e894a05f95728c1a8fd90c5527" + integrity sha512-v0U9S7gBIme3OTgt1LdbAF4RpvavCc+4GK1+1xqAcqtbrHsEhjQo6R45LKcjhs/+WrRJij1Y0Gztw7QPAIeUfA== + dependencies: + "@aws-crypto/crc32" "5.2.0" + "@aws-crypto/crc32c" "5.2.0" + "@aws-crypto/util" "5.2.0" + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" + tslib "^2.6.2" + +"@aws-sdk/client-s3@^3.1075.0": + version "3.1075.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.1075.0.tgz#1b5c49ed2f6230af974a437197092817016b35bc" + integrity sha512-h1A6nIl1YX6Y45enGsTK7ef3ZrOnBiQJ1qF5R2K/nMWfsu6A9mc2Y5T66nxerABzyjjyyvign3MrzafnFoQKmA== dependencies: "@aws-crypto/sha1-browser" "5.2.0" "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/credential-provider-node" "^3.972.44" - "@aws-sdk/middleware-bucket-endpoint" "^3.972.15" - "@aws-sdk/middleware-expect-continue" "^3.972.13" - "@aws-sdk/middleware-flexible-checksums" "^3.974.21" - "@aws-sdk/middleware-location-constraint" "^3.972.11" - "@aws-sdk/middleware-sdk-s3" "^3.972.42" - "@aws-sdk/middleware-ssec" "^3.972.11" - "@aws-sdk/signature-v4-multi-region" "^3.996.28" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/fetch-http-handler" "^5.4.3" - "@smithy/node-http-handler" "^4.7.3" - "@smithy/types" "^4.14.2" + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/credential-provider-node" "^3.972.58" + "@aws-sdk/middleware-flexible-checksums" "^3.974.33" + "@aws-sdk/middleware-sdk-s3" "^3.972.54" + "@aws-sdk/signature-v4-multi-region" "^3.996.35" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/fetch-http-handler" "^5.4.6" + "@smithy/node-http-handler" "^4.7.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/core@^3.974.13": - version "3.974.13" - resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.974.13.tgz#a785d4a726590f679671d18b36c69e3fc9b6cab5" - integrity sha512-+Y5/4tHki0uYgyx8eun146DegRVQBpdKGK5RbV0FTKJPpaKTchvqVxrrRFK6Wk0JksO4iAZKw3eqxGEIwtO98w== +"@aws-sdk/core@^3.974.23": + version "3.974.23" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.974.23.tgz#baf720519aaed49a8689e5e76f62d10c7a0d1da3" + integrity sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ== dependencies: - "@aws-sdk/types" "^3.973.9" - "@aws-sdk/xml-builder" "^3.972.25" + "@aws-sdk/types" "^3.973.13" + "@aws-sdk/xml-builder" "^3.972.31" "@aws/lambda-invoke-store" "^0.2.2" - "@smithy/core" "^3.24.3" - "@smithy/signature-v4" "^5.4.2" - "@smithy/types" "^4.14.2" + "@smithy/core" "^3.24.6" + "@smithy/signature-v4" "^5.4.6" + "@smithy/types" "^4.14.3" bowser "^2.11.0" tslib "^2.6.2" -"@aws-sdk/crc64-nvme@^3.972.9": - version "3.972.9" - resolved "https://registry.yarnpkg.com/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.9.tgz#4ea4d574d473e25e59973fcbab101ca1b64fab91" - integrity sha512-P+QGozmXn2mZZI7sDgk+aUm+RTI61MPSFB+Ir2vjEjEbEsE4e7hYtzrDvAUxZy9ko81h53e11+F/GYlvwDkaOQ== +"@aws-sdk/credential-provider-env@^3.972.49": + version "3.972.49" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.49.tgz#7927b7ec8f13bbda1f51242bf028f0510b8f83b5" + integrity sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ== dependencies: - "@smithy/types" "^4.14.2" + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/credential-provider-env@^3.972.39": - version "3.972.39" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.39.tgz#538cc859f2ac0e15b141b9e246613a752849ae8c" - integrity sha512-29wX9zpAvEt1vcj0psha+y6ygBHy2V/S72mp6e7q0KARLWXq+pwE/lR6qGkwknQvruh52lXvlqZIga8Hdxkucw== - dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" - tslib "^2.6.2" - -"@aws-sdk/credential-provider-http@^3.972.41": - version "3.972.41" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.41.tgz#07037e7346881cb8bb8ec1fe9f8ed0104072b63a" - integrity sha512-IA3CQTjtJkb6u1H4mE4936c8OPBMa9Jggtwe8U2Mqw/vvb/tZ5Ebd0mcZcX0uKWQhOyYo/+qNIwkV5Xh+FeJJA== - dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/fetch-http-handler" "^5.4.3" - "@smithy/node-http-handler" "^4.7.3" - "@smithy/types" "^4.14.2" +"@aws-sdk/credential-provider-http@^3.972.51": + version "3.972.51" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.51.tgz#7581b5a077b3a1f1278aa680e365162045b8662a" + integrity sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA== + dependencies: + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/fetch-http-handler" "^5.4.6" + "@smithy/node-http-handler" "^4.7.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/credential-provider-ini@^3.972.43": - version "3.972.43" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.43.tgz#cb9779beebd45bd242c12ea48a820047c77e1b05" - integrity sha512-4mzII+3mZEVXXE1xzrLQrCJL7/r62A63bA6SVzZoNL5rqCJghpf+xgGltVrIBBs0n+mOZBKrQl2tRREtvZ5l6A== - dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/credential-provider-env" "^3.972.39" - "@aws-sdk/credential-provider-http" "^3.972.41" - "@aws-sdk/credential-provider-login" "^3.972.43" - "@aws-sdk/credential-provider-process" "^3.972.39" - "@aws-sdk/credential-provider-sso" "^3.972.43" - "@aws-sdk/credential-provider-web-identity" "^3.972.43" - "@aws-sdk/nested-clients" "^3.997.11" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/credential-provider-imds" "^4.3.2" - "@smithy/types" "^4.14.2" +"@aws-sdk/credential-provider-ini@^3.972.56": + version "3.972.56" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.56.tgz#3832ab637b27b273443e3f54a7c554a98822847b" + integrity sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w== + dependencies: + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/credential-provider-env" "^3.972.49" + "@aws-sdk/credential-provider-http" "^3.972.51" + "@aws-sdk/credential-provider-login" "^3.972.55" + "@aws-sdk/credential-provider-process" "^3.972.49" + "@aws-sdk/credential-provider-sso" "^3.972.55" + "@aws-sdk/credential-provider-web-identity" "^3.972.55" + "@aws-sdk/nested-clients" "^3.997.23" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/credential-provider-imds" "^4.3.7" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/credential-provider-login@^3.972.43": - version "3.972.43" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.43.tgz#2d6dd4a7d082b0c54be9c5c5269161c14f7ae717" - integrity sha512-HG7kQCwXtbv3oBV61Ins0oNX8KKyvrMqqRkb6ZiAfQHbMuHaiNaEb2KnpKLPkNpqImSBK82UkVE/kaY6IfWikA== +"@aws-sdk/credential-provider-login@^3.972.55": + version "3.972.55" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.55.tgz#ccea9dfc063336a845ca01bc4370907044bfc09a" + integrity sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA== dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/nested-clients" "^3.997.11" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/nested-clients" "^3.997.23" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/credential-provider-node@^3.972.44": - version "3.972.44" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.44.tgz#af009a773d2e20214edfcc98894d3d0779fbc1c3" - integrity sha512-sDaBIT0yrNNIPfvlsiTCmANm07zKju+ipWODjEXgZlsjMeIJR3LVp7RDyAOzUoAsTbDfYKDWp+i5WrFiQP6rmQ== - dependencies: - "@aws-sdk/credential-provider-env" "^3.972.39" - "@aws-sdk/credential-provider-http" "^3.972.41" - "@aws-sdk/credential-provider-ini" "^3.972.43" - "@aws-sdk/credential-provider-process" "^3.972.39" - "@aws-sdk/credential-provider-sso" "^3.972.43" - "@aws-sdk/credential-provider-web-identity" "^3.972.43" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/credential-provider-imds" "^4.3.2" - "@smithy/types" "^4.14.2" +"@aws-sdk/credential-provider-node@^3.972.58": + version "3.972.58" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.58.tgz#a788d4821973add09648306f5ee7b25ea10d255f" + integrity sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.49" + "@aws-sdk/credential-provider-http" "^3.972.51" + "@aws-sdk/credential-provider-ini" "^3.972.56" + "@aws-sdk/credential-provider-process" "^3.972.49" + "@aws-sdk/credential-provider-sso" "^3.972.55" + "@aws-sdk/credential-provider-web-identity" "^3.972.55" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/credential-provider-imds" "^4.3.7" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/credential-provider-process@^3.972.39": - version "3.972.39" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.39.tgz#236f8822180b297e0e98771ee69aea428280a4a7" - integrity sha512-2k/amBifLd75eXNwgvPw/2lKYSQ3NhvHQgkVKVjfUq13/eJ3JRtHmznuFenn74OK3sSfp4SMy1YB2w+UVXoKqA== +"@aws-sdk/credential-provider-process@^3.972.49": + version "3.972.49" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.49.tgz#de3ba652d6c62ddf3f4bce2a5afb12717212fd6d" + integrity sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ== dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/credential-provider-sso@^3.972.43": - version "3.972.43" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.43.tgz#24546d197ce74a29c89c37b3860952ee28f90c9b" - integrity sha512-LPc3+Y4vhH1T4x6CMqwCM6hk5+SRf/Lwmgm8INm95wxTtIRHcMwQUVkDzWu4Iw/RSncxYM2BC01OrYbxOPZvyg== - dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/nested-clients" "^3.997.11" - "@aws-sdk/token-providers" "3.1052.0" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" +"@aws-sdk/credential-provider-sso@^3.972.55": + version "3.972.55" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.55.tgz#1210ca5a8e8d52b94f1ac7ecc88f157b0e065b8f" + integrity sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w== + dependencies: + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/nested-clients" "^3.997.23" + "@aws-sdk/token-providers" "3.1074.0" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/credential-provider-web-identity@^3.972.43": - version "3.972.43" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.43.tgz#1d4e99a4cba32c63bab21184f8d5d418c0744224" - integrity sha512-wQtL34lUD/09VXjwAUo2T+I3aEXRDxMB3DKmTJL/Zj0Gi6sLDTrVhae1XVt01yzkquOWajI/sZW72JGDZ1ciTw== +"@aws-sdk/credential-provider-web-identity@^3.972.55": + version "3.972.55" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.55.tgz#d275bedf248d7defb7e042c463186b8953033e03" + integrity sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg== dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/nested-clients" "^3.997.11" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/nested-clients" "^3.997.23" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/middleware-bucket-endpoint@^3.972.15": - version "3.972.15" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.15.tgz#f1752b8107289df1313b647bf42e8e5f78f44192" - integrity sha512-O2HDANa+MrvbxpaRVQDiH3T13uAa9AkMjKyZmDygwauAmmvqZ5B0iRmKW+fuVGW6NPXuyXurFgIx69lSvmAWGA== +"@aws-sdk/middleware-flexible-checksums@^3.974.33": + version "3.974.33" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.33.tgz#7245fe9627bc5e4a5730675109d002169c508ad3" + integrity sha512-qMgQSPemQq2/eW/e/0+SpY4kYR5L7dUgBiVdEc5bd+ztHNv07ZMYiI+sTiir3TgKndFfglSw/VFi7oZJ6bZ63g== dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" + "@aws-sdk/checksums" "^3.1000.8" tslib "^2.6.2" -"@aws-sdk/middleware-expect-continue@^3.972.13": - version "3.972.13" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.13.tgz#d6eac0372151e7aa978985ceb67311ab77b03939" - integrity sha512-sHiqIFg8o2ipT7t40B89Vj0ubSUtY6OSt/+Ee/OXhHch5K4+81zP2+QX8Lkc/nJ2QSmCySxOke7TEbmX69fe2g== +"@aws-sdk/middleware-sdk-s3@^3.972.54": + version "3.972.54" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.54.tgz#903eb3e47552c03a8c9e7d11faa76e427d44b448" + integrity sha512-GDfDQ0gwLFRKN9gWIKcmVrHJ3e7XagnY7N1LLzMVNgnOnuY7f/ALgmy3CuBjosWD95T/Z6e+gs1IeWmLPkyLKQ== dependencies: - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/signature-v4-multi-region" "^3.996.35" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/middleware-flexible-checksums@^3.974.21": - version "3.974.21" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.21.tgz#efa1acea9921691f8fe80160ebaa6514b2e0839c" - integrity sha512-alAu9heyiBK/OmRNXVxq8mmPTgeW2AQ6EYjRsI38kPZa1MZvt2Jh+BlGq7/GG9OVXOaEgD7DlGj/Lzfy5OmuEg== - dependencies: - "@aws-crypto/crc32" "5.2.0" - "@aws-crypto/crc32c" "5.2.0" - "@aws-crypto/util" "5.2.0" - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/crc64-nvme" "^3.972.9" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" - tslib "^2.6.2" - -"@aws-sdk/middleware-location-constraint@^3.972.11": - version "3.972.11" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.11.tgz#272507843738acd4a5644842911a2016f7dfb0e1" - integrity sha512-hkfspNUP4criAH6ton6BGKgnm5dZx+7bUOy1YqlTfejDeUPAM23D81q/IX+hdlS3KUsfwGz5ADTqZWKBEUpf4A== - dependencies: - "@aws-sdk/types" "^3.973.9" - "@smithy/types" "^4.14.2" - tslib "^2.6.2" - -"@aws-sdk/middleware-sdk-s3@^3.972.42": - version "3.972.42" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.42.tgz#f4217eea10d2de43b482f6f2d0d9895be061e571" - integrity sha512-/xNqNGXv9LaxZd25L9VV4pnSOw9OdDNO4rAHamM+h3KQBSITljIH9vk3dveGga1I2j36lQd0rdG3gjNEXvtNew== - dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/signature-v4-multi-region" "^3.996.28" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/signature-v4" "^5.4.2" - "@smithy/types" "^4.14.2" - tslib "^2.6.2" - -"@aws-sdk/middleware-ssec@^3.972.11": - version "3.972.11" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.11.tgz#b5d5ddde7d54239137949f63b3d5dee6331628ea" - integrity sha512-7PQvGNhtveKlvVqNahqWx5yrwxP7ecwAoB1dYBf8eKwfo2tzzCbNnW+q2nO3N066ktQaB4iBQbDRWtizm+amoQ== - dependencies: - "@aws-sdk/types" "^3.973.9" - "@smithy/types" "^4.14.2" - tslib "^2.6.2" - -"@aws-sdk/nested-clients@^3.997.11": - version "3.997.11" - resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.997.11.tgz#ed97d5dadc5ee15a31834e8af218e502d986d632" - integrity sha512-nWXXJ1r/r8N2Gw1pWolRgED38/A9A8DHR2ETWIv220zh4PZHcybbR4hUVWWktmNXTRHzDJwRluapHn0rZxuoqA== +"@aws-sdk/nested-clients@^3.997.23": + version "3.997.23" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.997.23.tgz#fc37380e064a3a31646170d7fd84b8dd7ef6fb81" + integrity sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/signature-v4-multi-region" "^3.996.28" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/fetch-http-handler" "^5.4.3" - "@smithy/node-http-handler" "^4.7.3" - "@smithy/types" "^4.14.2" + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/signature-v4-multi-region" "^3.996.35" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/fetch-http-handler" "^5.4.6" + "@smithy/node-http-handler" "^4.7.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/signature-v4-multi-region@^3.996.28": - version "3.996.28" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.28.tgz#79c12506d5545953c06fe75956b38050d57902f2" - integrity sha512-qs9z5LqXO/CZC2Lg9SGKpoLU8Rhi+m2pFKZqfO9pytX1clc0katqtsDNupJxFy0xT9wsZSPzM2v1y+/H/zfp5Q== +"@aws-sdk/signature-v4-multi-region@^3.996.35": + version "3.996.35" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.35.tgz#2994b9f33e84b9c74392b7495f89e5c958bda503" + integrity sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg== dependencies: - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/signature-v4" "^5.4.2" - "@smithy/types" "^4.14.2" + "@aws-sdk/types" "^3.973.13" + "@smithy/signature-v4" "^5.4.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@aws-sdk/token-providers@3.1052.0": - version "3.1052.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1052.0.tgz#0793c2f58351bf91937e8f83abf39d11937ec8f2" - integrity sha512-QqZNB3so7UIDxZtroc85TQaLVxdZRFm0eWM1CSR2N+b06as9TOrilvrlTZuj3guYlxMs6yLOgGxnklJ5qMYtTw== +"@aws-sdk/token-providers@3.1074.0": + version "3.1074.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1074.0.tgz#f0ea81a54fde54765378b9afbbbcdc96ef9e5e71" + integrity sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg== dependencies: - "@aws-sdk/core" "^3.974.13" - "@aws-sdk/nested-clients" "^3.997.11" - "@aws-sdk/types" "^3.973.9" - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" + "@aws-sdk/core" "^3.974.23" + "@aws-sdk/nested-clients" "^3.997.23" + "@aws-sdk/types" "^3.973.13" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" "@aws-sdk/types@^3.222.0": @@ -356,12 +336,12 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/types@^3.973.9": - version "3.973.9" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.973.9.tgz#7d1c08cc6e82ec2ac2f2da102a7dd55806592f7f" - integrity sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg== +"@aws-sdk/types@^3.973.13": + version "3.973.13" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.973.13.tgz#c076f611e94394a49c1bc1aeb64371ef6db4b3da" + integrity sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg== dependencies: - "@smithy/types" "^4.14.2" + "@smithy/types" "^4.14.3" tslib "^2.6.2" "@aws-sdk/util-locate-window@^3.0.0": @@ -371,14 +351,12 @@ dependencies: tslib "^2.6.2" -"@aws-sdk/xml-builder@^3.972.25": - version "3.972.25" - resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.25.tgz#252ed0afef165a247c2dcc5d72e54b8f9e45f2e2" - integrity sha512-GH+Kjz4nPKWKHnsiQpnhP1MJdTGIcK4rAka6tzakgjjUkVgNsmPeEbbRAf09SzS1hjGu6duGHCBsxYke0BhHjQ== +"@aws-sdk/xml-builder@^3.972.31": + version "3.972.31" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.31.tgz#8cbfa41fbc6bab6ef279d2088b85e916956a3fb2" + integrity sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ== dependencies: - "@nodable/entities" "2.1.0" - "@smithy/types" "^4.14.2" - fast-xml-parser "5.7.3" + "@smithy/types" "^4.14.3" tslib "^2.6.2" "@aws/lambda-invoke-store@^0.2.2": @@ -395,40 +373,45 @@ js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/code-frame@^7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" - integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== +"@babel/code-frame@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-8.0.0.tgz#f374ea9392011ffecf223805dd0d9315606e5bbf" + integrity sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw== dependencies: - "@babel/helper-validator-identifier" "^7.28.5" - js-tokens "^4.0.0" - picocolors "^1.1.1" + "@babel/helper-validator-identifier" "^8.0.0" + js-tokens "^10.0.0" "@babel/compat-data@^7.28.6": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.6.tgz#103f466803fa0f059e82ccac271475470570d74c" integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg== -"@babel/core@7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" - integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== - dependencies: - "@babel/code-frame" "^7.29.0" - "@babel/generator" "^7.29.0" - "@babel/helper-compilation-targets" "^7.28.6" - "@babel/helper-module-transforms" "^7.28.6" - "@babel/helpers" "^7.28.6" - "@babel/parser" "^7.29.0" - "@babel/template" "^7.28.6" - "@babel/traverse" "^7.29.0" - "@babel/types" "^7.29.0" - "@jridgewell/remapping" "^2.3.5" +"@babel/compat-data@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-8.0.0.tgz#e780eb6052d8ceeaeadd3a6cd82dda470a945622" + integrity sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw== + +"@babel/core@8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-8.0.1.tgz#dcb133d3ddcfb2b5a6558da03bf240319da0b8c8" + integrity sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw== + dependencies: + "@babel/code-frame" "^8.0.0" + "@babel/generator" "^8.0.0" + "@babel/helper-compilation-targets" "^8.0.0" + "@babel/helpers" "^8.0.0" + "@babel/parser" "^8.0.0" + "@babel/template" "^8.0.0" + "@babel/traverse" "^8.0.0" + "@babel/types" "^8.0.0" + "@types/gensync" "^1.0.5" convert-source-map "^2.0.0" - debug "^4.1.0" + empathic "^2.0.1" gensync "^1.0.0-beta.2" + import-meta-resolve "^4.2.0" json5 "^2.2.3" - semver "^6.3.1" + obug "^2.1.1" + semver "^7.7.3" "@babel/core@^7.21.3": version "7.28.6" @@ -462,15 +445,16 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" -"@babel/generator@^7.29.0": - version "7.29.1" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" - integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== +"@babel/generator@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-8.0.0.tgz#24b7b53a74fa8e74cc2377e054fecef4dcdada96" + integrity sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g== dependencies: - "@babel/parser" "^7.29.0" - "@babel/types" "^7.29.0" + "@babel/parser" "^8.0.0" + "@babel/types" "^8.0.0" "@jridgewell/gen-mapping" "^0.3.12" "@jridgewell/trace-mapping" "^0.3.28" + "@types/jsesc" "^2.5.0" jsesc "^3.0.2" "@babel/helper-compilation-targets@^7.28.6": @@ -484,11 +468,27 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0.tgz#63f230f427c9ea82323d829c935f6190d93b3736" + integrity sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew== + dependencies: + "@babel/compat-data" "^8.0.0" + "@babel/helper-validator-option" "^8.0.0" + browserslist "^4.24.0" + lru-cache "^11.0.0" + semver "^7.7.3" + "@babel/helper-globals@^7.28.0": version "7.28.0" resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== +"@babel/helper-globals@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-8.0.0.tgz#c93789d6c1c2f1b65a97a07515c471a049559bf1" + integrity sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw== + "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.28.6": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" @@ -511,16 +511,31 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== +"@babel/helper-string-parser@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz#4d47d9ad35d0f5bd7d202b348bf558328a347977" + integrity sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg== + "@babel/helper-validator-identifier@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== +"@babel/helper-validator-identifier@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0.tgz#9b208cab253adfc9ecd40c95763f99ece690e85a" + integrity sha512-kXxQVZHNOctSJJsqzmcbPSCEkM6oHNnDIkua7g9RCO9xRHj2eCiKvRx2KPdfWR9QxcGWnK/oArrtunmie3rL9g== + "@babel/helper-validator-option@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== +"@babel/helper-validator-option@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz#53307a6882254fa8d940017a701a8f7ec603a1a9" + integrity sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q== + "@babel/helpers@^7.28.6": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" @@ -529,6 +544,14 @@ "@babel/template" "^7.28.6" "@babel/types" "^7.28.6" +"@babel/helpers@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-8.0.0.tgz#2c612d4b7fdbd5f1d2fa7321015e36b02d1a2d79" + integrity sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg== + dependencies: + "@babel/template" "^8.0.0" + "@babel/types" "^8.0.0" + "@babel/parser@^7.28.6": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" @@ -536,12 +559,12 @@ dependencies: "@babel/types" "^7.28.6" -"@babel/parser@^7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" - integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== +"@babel/parser@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-8.0.0.tgz#31f6860840277dc1c6d6f8b67bf74e0ccaa5df0a" + integrity sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ== dependencies: - "@babel/types" "^7.29.0" + "@babel/types" "^8.0.0" "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.28.6" @@ -562,6 +585,15 @@ "@babel/parser" "^7.28.6" "@babel/types" "^7.28.6" +"@babel/template@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-8.0.0.tgz#18c12626b0d6d23a0443771655864ed7566a3452" + integrity sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ== + dependencies: + "@babel/code-frame" "^8.0.0" + "@babel/parser" "^8.0.0" + "@babel/types" "^8.0.0" + "@babel/traverse@^7.28.6": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e" @@ -575,18 +607,18 @@ "@babel/types" "^7.28.6" debug "^4.3.1" -"@babel/traverse@^7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" - integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== +"@babel/traverse@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-8.0.0.tgz#0473589140c796d13e27641ab0783069f0fe7512" + integrity sha512-bxTj/W2VclGE6CctlfQOpxg8MPDzXArRqkOBePw8EHfebcjF7fETWSS3BriEECo+UiU/Yblq+xUtSImFu7cTbw== dependencies: - "@babel/code-frame" "^7.29.0" - "@babel/generator" "^7.29.0" - "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.29.0" - "@babel/template" "^7.28.6" - "@babel/types" "^7.29.0" - debug "^4.3.1" + "@babel/code-frame" "^8.0.0" + "@babel/generator" "^8.0.0" + "@babel/helper-globals" "^8.0.0" + "@babel/parser" "^8.0.0" + "@babel/template" "^8.0.0" + "@babel/types" "^8.0.0" + obug "^2.1.1" "@babel/types@^7.21.3", "@babel/types@^7.28.6": version "7.28.6" @@ -596,7 +628,7 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" -"@babel/types@^7.26.0", "@babel/types@^7.29.0": +"@babel/types@^7.26.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== @@ -604,6 +636,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" +"@babel/types@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-8.0.0.tgz#b518f1ef7f9838bffdca4e123b449d3f644f4494" + integrity sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw== + dependencies: + "@babel/helper-string-parser" "^8.0.0" + "@babel/helper-validator-identifier" "^8.0.0" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -611,14 +651,6 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@develar/schema-utils@~2.6.5": - version "2.6.5" - resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" - integrity sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig== - dependencies: - ajv "^6.12.0" - ajv-keywords "^3.4.1" - "@dr.pogodin/react-helmet@^3.2.2": version "3.2.2" resolved "https://registry.yarnpkg.com/@dr.pogodin/react-helmet/-/react-helmet-3.2.2.tgz#adb8c7f0862e88d2e0d3bdf5c00e11fa75b8d5f1" @@ -866,6 +898,11 @@ dependencies: chrome-trace-event "^1.0.3" +"@electron-internal/extract-zip@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@electron-internal/extract-zip/-/extract-zip-1.0.2.tgz#c612e3db45f78261a1e16d577cea9eac8663defc" + integrity sha512-VJuNETNPEhrmQEZezeTZO5TZMV+dobBRyJ7zHjGJWIhMS7m7W1UeClt69u4hkUxv9ZZVxuli/E9Yvc4gDNHGsg== + "@electron/asar@3.4.1", "@electron/asar@^3.2.1", "@electron/asar@^3.2.13", "@electron/asar@^3.2.5", "@electron/asar@^3.3.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.4.1.tgz#4e9196a4b54fba18c56cd8d5cac67c5bdc588065" @@ -875,10 +912,10 @@ glob "^7.1.6" minimatch "^3.0.4" -"@electron/fuses@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@electron/fuses/-/fuses-2.1.1.tgz#8b15eb91dcde51e20a2b3cefb5fb11adb43d237a" - integrity sha512-38ho27/mtUV/LpsZ1LCDJUomKBBSUZDk/qBH4FNNtoN5fmnkmWDcIp5pm1Kv3InqhRjKZKs7Jzx+wWZNMArHrA== +"@electron/fuses@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@electron/fuses/-/fuses-2.1.2.tgz#258e34b51f86dd94005d57afd7c29813aef2b68d" + integrity sha512-x083OSf4xO9CKUKmJFYW+Egua9y/v2XOCcCIuWnM4RlpMj8xHwKICezlVN6vZyI2cZTr/ysZXRU5AKtYPJivBQ== "@electron/fuses@^1.8.0": version "1.8.0" @@ -981,7 +1018,7 @@ semver "^7.1.3" yargs-parser "^21.1.1" -"@electron/rebuild@4.0.4", "@electron/rebuild@^3.7.0", "@electron/rebuild@^4.0.3": +"@electron/rebuild@4.0.4", "@electron/rebuild@^3.7.0", "@electron/rebuild@^4.0.4": version "4.0.4" resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-4.0.4.tgz#a61331d9ae3b8e2c7eddca8e446fcd7fcd60e4ce" integrity sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg== @@ -1017,25 +1054,25 @@ minimist "^1.2.8" postject "^1.0.0-alpha.6" -"@emnapi/core@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467" - integrity sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== +"@emnapi/core@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6" + integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ== dependencies: - "@emnapi/wasi-threads" "1.2.1" + "@emnapi/wasi-threads" "1.2.2" tslib "^2.4.0" -"@emnapi/runtime@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" - integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== +"@emnapi/runtime@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24" + integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw== dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" - integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== +"@emnapi/wasi-threads@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz#4c93becf5bfa3b13d1bbdcc06aee38321ad8139a" + integrity sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA== dependencies: tslib "^2.4.0" @@ -1316,10 +1353,10 @@ resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.5.tgz#88e9bf4d11d2b19c082e78ebe7ce88724a5eb091" integrity sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw== -"@eslint/plugin-kit@^0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz#c4125fd015eceeb09b793109fdbcd4dd0a02d346" - integrity sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ== +"@eslint/plugin-kit@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz#4b0962f3f2c7ce8bc98b3ecfe34525c09d2cb729" + integrity sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A== dependencies: "@eslint/core" "^1.2.1" levn "^0.4.1" @@ -1577,7 +1614,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== @@ -1717,17 +1754,22 @@ prop-types "^15.8.1" react-is "^19.0.0" -"@napi-rs/wasm-runtime@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz#a46bbfedc29751b7170c5d23bc1d8ee8c7e3c1e1" - integrity sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow== +"@napi-rs/wasm-runtime@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz#ed33806d0f9be98dc76d0c3d4fd872fda701b5d5" + integrity sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg== dependencies: - "@tybys/wasm-util" "^0.10.1" + "@tybys/wasm-util" "^0.10.3" -"@nodable/entities@2.1.0", "@nodable/entities@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@nodable/entities/-/entities-2.1.0.tgz#f543e5c6446720d4cf9e498a83019dd159973bc2" - integrity sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA== +"@noble/hashes@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@noble/hashes@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.2.0.tgz#22da1d16a469954fce877055d559900a6c73b63b" + integrity sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1750,10 +1792,60 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oxc-project/types@=0.132.0": - version "0.132.0" - resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.132.0.tgz#d77243df4fe1a0a1e60e12ac6240fa898d2363ff" - integrity sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ== +"@opentelemetry/api-logs@0.214.0": + version "0.214.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz#74a54ad7b166c6fa30a0df811954c0f5a435deee" + integrity sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA== + dependencies: + "@opentelemetry/api" "^1.3.0" + +"@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.1.tgz#c1b0346de336ba55af2d5a7970882037baedec05" + integrity sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q== + +"@opentelemetry/core@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.8.0.tgz#f6e86de3688bdb54a6ca8f4935363a5b588ae91c" + integrity sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww== + dependencies: + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/instrumentation@^0.214.0": + version "0.214.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz#2649e8a29a8c4748bc583d35281c80632f046e25" + integrity sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w== + dependencies: + "@opentelemetry/api-logs" "0.214.0" + import-in-the-middle "^3.0.0" + require-in-the-middle "^8.0.0" + +"@opentelemetry/resources@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.8.0.tgz#9bcb658ab6254f33099f4a95544b40d6f53cc946" + integrity sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg== + dependencies: + "@opentelemetry/core" "2.8.0" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/sdk-trace-base@^2.6.1": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz#ec9c1d69e2e6fba256c9df0c8e8d67d42386d52b" + integrity sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ== + dependencies: + "@opentelemetry/core" "2.8.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.40.0": + version "1.41.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz#b04e7151c5913a7a006d4f465479da75efb98a7a" + integrity sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA== + +"@oxc-project/types@=0.137.0": + version "0.137.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.137.0.tgz#56e77f8bb221fa05f18b1cd34d73f94f0954a773" + integrity sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA== "@parcel/watcher-android-arm64@2.5.6": version "2.5.6" @@ -1844,172 +1936,205 @@ "@parcel/watcher-win32-ia32" "2.5.6" "@parcel/watcher-win32-x64" "2.5.6" +"@peculiar/asn1-schema@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz#f2dcb25995ce7cac8687ba1039f043e5eff43820" + integrity sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg== + dependencies: + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/json-schema@^1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" + integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== + dependencies: + tslib "^2.0.0" + +"@peculiar/utils@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@peculiar/utils/-/utils-2.0.3.tgz#a27ca4c4b73652e110f19a7d16d664f458a5528e" + integrity sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ== + dependencies: + tslib "^2.8.1" + +"@peculiar/webcrypto@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz#ef8cfae878ca1f1a44ef0c82cf3fa6f3cbe9c26a" + integrity sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/json-schema" "^1.1.12" + "@peculiar/utils" "^2.0.2" + tslib "^2.8.1" + webcrypto-core "^1.9.2" + "@popperjs/core@^2.11.8": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@radix-ui/primitive@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.3.tgz#e2dbc13bdc5e4168f4334f75832d7bdd3e2de5ba" - integrity sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg== +"@radix-ui/primitive@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.4.tgz#47ef0f6cff4a1a1c09ebbf6d79159b7f01b967cf" + integrity sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ== -"@radix-ui/react-arrow@1.1.7": - version "1.1.7" - resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz#e14a2657c81d961598c5e72b73dd6098acc04f09" - integrity sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w== +"@radix-ui/react-arrow@1.1.10": + version "1.1.10" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz#67b778e5811d21d2ed6d8e6cafdbe5e6280abeb0" + integrity sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ== dependencies: - "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-primitive" "2.1.6" -"@radix-ui/react-compose-refs@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" - integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== +"@radix-ui/react-compose-refs@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz#5f1e61e1a5f52800d31e7f8affa6d046e38f50d1" + integrity sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA== -"@radix-ui/react-context@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" - integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== +"@radix-ui/react-context@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.4.tgz#5e39f26ebbefed27836e46e763e8f71e09999ccd" + integrity sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg== -"@radix-ui/react-dismissable-layer@1.1.11": - version "1.1.11" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz#e33ab6f6bdaa00f8f7327c408d9f631376b88b37" - integrity sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg== +"@radix-ui/react-dismissable-layer@1.1.13": + version "1.1.13" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz#2296b5589584f2eea9f276d76beddea0a5b4a0f4" + integrity sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg== dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - "@radix-ui/react-use-escape-keydown" "1.1.1" + "@radix-ui/primitive" "1.1.4" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-primitive" "2.1.6" + "@radix-ui/react-use-callback-ref" "1.1.2" + "@radix-ui/react-use-escape-keydown" "1.1.2" -"@radix-ui/react-id@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" - integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== +"@radix-ui/react-id@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.2.tgz#6fe97e7289c7133b44f8c9c61fdddf2a6be1421d" + integrity sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA== dependencies: - "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.2" -"@radix-ui/react-popper@1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz#a79f39cdd2b09ab9fb50bf95250918422c4d9602" - integrity sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw== +"@radix-ui/react-popper@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.3.1.tgz#4f7d36a7ec49959e1f48d2629ca98323eaf95cb1" + integrity sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw== dependencies: "@floating-ui/react-dom" "^2.0.0" - "@radix-ui/react-arrow" "1.1.7" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - "@radix-ui/react-use-layout-effect" "1.1.1" - "@radix-ui/react-use-rect" "1.1.1" - "@radix-ui/react-use-size" "1.1.1" - "@radix-ui/rect" "1.1.1" - -"@radix-ui/react-portal@1.1.9": - version "1.1.9" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472" - integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ== - dependencies: - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-presence@1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz#5d8f28ac316c32f078afce2996839250c10693db" - integrity sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ== + "@radix-ui/react-arrow" "1.1.10" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-context" "1.1.4" + "@radix-ui/react-primitive" "2.1.6" + "@radix-ui/react-use-callback-ref" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.2" + "@radix-ui/react-use-rect" "1.1.2" + "@radix-ui/react-use-size" "1.1.2" + "@radix-ui/rect" "1.1.2" + +"@radix-ui/react-portal@1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.12.tgz#e62f8b5882c71bdda87cf74465ebb6a219a09971" + integrity sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw== dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-primitive" "2.1.6" + "@radix-ui/react-use-layout-effect" "1.1.2" -"@radix-ui/react-primitive@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc" - integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ== +"@radix-ui/react-presence@1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.6.tgz#f0edff4f119dbc8ef81611e539a8f58d9afb33c3" + integrity sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ== dependencies: - "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-layout-effect" "1.1.2" -"@radix-ui/react-slot@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" - integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== +"@radix-ui/react-primitive@2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz#8e7c170a35d538325a5ff9194f7a842562a1d57d" + integrity sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g== dependencies: - "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-slot" "1.3.0" -"@radix-ui/react-tooltip@^1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz#3f50267e25bccfc9e20bb3036bfd9ab4c2c30c2c" - integrity sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-dismissable-layer" "1.1.11" - "@radix-ui/react-id" "1.1.1" - "@radix-ui/react-popper" "1.2.8" - "@radix-ui/react-portal" "1.1.9" - "@radix-ui/react-presence" "1.1.5" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-slot" "1.2.3" - "@radix-ui/react-use-controllable-state" "1.2.2" - "@radix-ui/react-visually-hidden" "1.2.3" - -"@radix-ui/react-use-callback-ref@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40" - integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg== +"@radix-ui/react-slot@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.3.0.tgz#e311c7a6c8d65b1af9e69af8e3318c6c7105a212" + integrity sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.3" -"@radix-ui/react-use-controllable-state@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190" - integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg== +"@radix-ui/react-tooltip@^1.2.10": + version "1.2.10" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz#e20b87d625f78961b4997987190a744266b242b6" + integrity sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw== + dependencies: + "@radix-ui/primitive" "1.1.4" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-context" "1.1.4" + "@radix-ui/react-dismissable-layer" "1.1.13" + "@radix-ui/react-id" "1.1.2" + "@radix-ui/react-popper" "1.3.1" + "@radix-ui/react-portal" "1.1.12" + "@radix-ui/react-presence" "1.1.6" + "@radix-ui/react-primitive" "2.1.6" + "@radix-ui/react-slot" "1.3.0" + "@radix-ui/react-use-controllable-state" "1.2.3" + "@radix-ui/react-visually-hidden" "1.2.6" + +"@radix-ui/react-use-callback-ref@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz#ddc0bc1381ff3b62368c248808efc45a098bafde" + integrity sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw== + +"@radix-ui/react-use-controllable-state@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz#516996f6443207546aa15a59bc71cdf5b54e01d1" + integrity sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA== dependencies: - "@radix-ui/react-use-effect-event" "0.0.2" - "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-effect-event" "0.0.3" + "@radix-ui/react-use-layout-effect" "1.1.2" -"@radix-ui/react-use-effect-event@0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907" - integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA== +"@radix-ui/react-use-effect-event@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz#e8f45e8e6ef64ce5bea7b5a9effc373f067e3530" + integrity sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA== dependencies: - "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.2" -"@radix-ui/react-use-escape-keydown@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" - integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== +"@radix-ui/react-use-escape-keydown@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz#d63d64956da411192ef15d8c274b94d7d0adb038" + integrity sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw== dependencies: - "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-callback-ref" "1.1.2" -"@radix-ui/react-use-layout-effect@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" - integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== +"@radix-ui/react-use-layout-effect@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz#c882e66497174d061f250e65251974b699c65b65" + integrity sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA== -"@radix-ui/react-use-rect@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz#01443ca8ed071d33023c1113e5173b5ed8769152" - integrity sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w== +"@radix-ui/react-use-rect@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz#83b9de1ea8f6abd1425eb79f2930e00047cb8d19" + integrity sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw== dependencies: - "@radix-ui/rect" "1.1.1" + "@radix-ui/rect" "1.1.2" -"@radix-ui/react-use-size@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz#6de276ffbc389a537ffe4316f5b0f24129405b37" - integrity sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ== +"@radix-ui/react-use-size@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz#33eb275755424d7dda33ffa32c23ea85ca23be40" + integrity sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w== dependencies: - "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.2" -"@radix-ui/react-visually-hidden@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz#a8c38c8607735dc9f05c32f87ab0f9c2b109efbf" - integrity sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug== +"@radix-ui/react-visually-hidden@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz#da362f6fef6b6107e059db6030b9a0da86599fa8" + integrity sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ== dependencies: - "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-primitive" "2.1.6" -"@radix-ui/rect@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" - integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== +"@radix-ui/rect@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.2.tgz#0761a82af55c7e302d5b509eaf1c97ea1fc5feea" + integrity sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA== "@reduxjs/toolkit@1.x.x || 2.x.x": version "2.11.2" @@ -2023,84 +2148,84 @@ redux-thunk "^3.1.0" reselect "^5.1.0" -"@rolldown/binding-android-arm64@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz#ebe1264e43ba5bb224c58c85e0ac238f87e5ad14" - integrity sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ== +"@rolldown/binding-android-arm64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz#cc6153029c3d9afc9caaae2dc362d899ae94ac4f" + integrity sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g== -"@rolldown/binding-darwin-arm64@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz#f972fb71bc03840629bee923babbb7048d14a5d0" - integrity sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w== +"@rolldown/binding-darwin-arm64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz#3af681a5d7610340257b3ac7753353b23e884765" + integrity sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw== -"@rolldown/binding-darwin-x64@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz#da1c062c135e1e50067084be5ab8055cc1e05b29" - integrity sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA== +"@rolldown/binding-darwin-x64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz#80ada35e9f35efb7e48a887444ce2052f615d645" + integrity sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw== -"@rolldown/binding-freebsd-x64@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz#a4c8532072705ce597c0d75418513709b75b3f1d" - integrity sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA== +"@rolldown/binding-freebsd-x64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz#65b2bbb82f005f08aeeff0b6d81e19be68360201" + integrity sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw== -"@rolldown/binding-linux-arm-gnueabihf@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz#78f3bd274a1bab07356017d728b70289f04011c1" - integrity sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w== +"@rolldown/binding-linux-arm-gnueabihf@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz#7e8d34ad0c7bcfd3baed268e9798571e3888ca71" + integrity sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg== -"@rolldown/binding-linux-arm64-gnu@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz#4dc530aed0b554762ffc1a5e4f016b8be495eea0" - integrity sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig== +"@rolldown/binding-linux-arm64-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz#52bbf400ff219bda1e56c042160d96deb08bfecc" + integrity sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA== -"@rolldown/binding-linux-arm64-musl@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz#56043cdf4768ea3184bb08a3f45b24f183003877" - integrity sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw== +"@rolldown/binding-linux-arm64-musl@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz#9e82899186f73329f3d8155fa1618ae2e86ffa2a" + integrity sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w== -"@rolldown/binding-linux-ppc64-gnu@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz#92eba998d5cd9e4cfc8b95407b4b80f46dacadf4" - integrity sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA== +"@rolldown/binding-linux-ppc64-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz#050520177316586ccad816eb466ea11015e17ba7" + integrity sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw== -"@rolldown/binding-linux-s390x-gnu@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz#50b0e95dc3c22af1ecd1669ad7e6030e6860819e" - integrity sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ== +"@rolldown/binding-linux-s390x-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz#50efa7b20219c6e31235fded0fd3427f36123e5a" + integrity sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA== -"@rolldown/binding-linux-x64-gnu@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz#d9fd5af004a373a2d26a09ce8fb66bd0927cbc0b" - integrity sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ== +"@rolldown/binding-linux-x64-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz#c5973445113dff50d4077d0edaa4b8a69533dc6f" + integrity sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg== -"@rolldown/binding-linux-x64-musl@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz#c0ad80adf4d4df430d0ec309e97924db85065c3c" - integrity sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw== +"@rolldown/binding-linux-x64-musl@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz#ddb023f7fc98ccbb8c1b683545216ca7b4e7ebdd" + integrity sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g== -"@rolldown/binding-openharmony-arm64@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz#341fc254ef7759f865bb219beb5cea6f5c9a44f6" - integrity sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w== +"@rolldown/binding-openharmony-arm64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz#832a6da3472722427c73d178c75681858b76aeed" + integrity sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ== -"@rolldown/binding-wasm32-wasi@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz#65f9389f74168fd54e67b12d0630fb90348051f0" - integrity sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ== +"@rolldown/binding-wasm32-wasi@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz#256cdcc06ad9ada611606526f319642fc0830b0f" + integrity sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg== dependencies: - "@emnapi/core" "1.10.0" - "@emnapi/runtime" "1.10.0" - "@napi-rs/wasm-runtime" "^1.1.4" + "@emnapi/core" "1.11.1" + "@emnapi/runtime" "1.11.1" + "@napi-rs/wasm-runtime" "^1.1.6" -"@rolldown/binding-win32-arm64-msvc@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz#17e80f3402308f12c76ff4172768d51715b522ac" - integrity sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A== +"@rolldown/binding-win32-arm64-msvc@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz#e735c7024a5e17ebaf13689112fa0bdfc6886c38" + integrity sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g== -"@rolldown/binding-win32-x64-msvc@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz#875cfa37f26d11692dbe28b8331499c97aa99d1f" - integrity sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ== +"@rolldown/binding-win32-x64-msvc@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz#f06c09db5c8ad4b6904b4d406c9b6f17f392b5c6" + integrity sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA== "@rolldown/plugin-babel@^0.2.3": version "0.2.3" @@ -2109,7 +2234,7 @@ dependencies: picomatch "^4.0.4" -"@rolldown/pluginutils@^1.0.0": +"@rolldown/pluginutils@^1.0.0", "@rolldown/pluginutils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== @@ -2128,45 +2253,200 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== +"@sentry/browser-utils@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/browser-utils/-/browser-utils-10.60.0.tgz#c71c80bb2103026c3f9938da690c9335fbd7356a" + integrity sha512-YhdPeMJnMaKVi5NQ2tD9RJ2AxU7cav5khmiMHFppmbP3I3Os2EHVaQjAVb6/ePAINr/d5mj5SxpnWmFX17iXsg== + dependencies: + "@sentry/core" "10.60.0" + +"@sentry/browser@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.60.0.tgz#1470c206dd8177abbd02c5a9d11edc8742f2ab63" + integrity sha512-20vzPKGrmupJPCaWd+soCOLkZRwKZxt0AVF4XGPNoGp7D7wVPRlHf+FWwVMx+rRRkahKupFefM2D9YoiUiBeag== + dependencies: + "@sentry/browser-utils" "10.60.0" + "@sentry/core" "10.60.0" + "@sentry/feedback" "10.60.0" + "@sentry/replay" "10.60.0" + "@sentry/replay-canvas" "10.60.0" + +"@sentry/cli-darwin@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-3.6.0.tgz#8186789f4cd8c14251f290a0ad27221dcccfa4c7" + integrity sha512-C2SWHKaEP8NoYkLDr5jwrzklTwlJkzPIx7lu2LZrwBuHG/sNhWdili0ED2mD86b6Q90hlcMtuBm5IHUwcZ6FTQ== + +"@sentry/cli-linux-arm64@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.6.0.tgz#c1b4433b7b9d8e88f59375038ae410d4a7b6db0b" + integrity sha512-Rc+DB8vuTDpwqY2BxIPnooYk2ZDYQytF+n4nfi6pZZsJtr3SFFe+3wIWVmCVqxiHkaISb33+iJIDxOOqhkSNbQ== + +"@sentry/cli-linux-arm@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-3.6.0.tgz#09826f9ca6beeb7cf7cd7fe9010bb23bb0c71f3d" + integrity sha512-S9xsDZTvybOGbrqqZ7DvF7JCNKp4cakDWJ4LdvQX+z82cHQSoLkYOXkA3EafDfWV9BGIRMIXitMMiSsV2PMU4g== + +"@sentry/cli-linux-i686@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-3.6.0.tgz#3271a02b552b0a3cec66fb0ee7516659f9774b3e" + integrity sha512-PQ7+ctNmWtHgmbKa+rJheHU7D9GHJXafgWYfVW6gt7R0Ag9LxiDVYLGjrL4G/i7AGFNgudXFaLkGKNX7HUjc+g== + +"@sentry/cli-linux-x64@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-3.6.0.tgz#b976325e3efb7a640461e2701c24513bbd215a1a" + integrity sha512-c+7xNg5BAaPE8N2Q6pg3Q/kt97JSaskuQIjRxHaFuDbCkJvww4VozY6mW5NUMJaW48rEs3mahWKamTLqJsO3wQ== + +"@sentry/cli-win32-arm64@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.6.0.tgz#33e863b1067d0cf29b022ae7c308ea8b4e8f6413" + integrity sha512-zhZ7YyGreHSKZ92Mwb9h4cEyL0I/eND7W6XIUXUW0BCCmxFOMc71vlQpUw8gijHIsFDbv8c8a6VOSkeRuwbSwQ== + +"@sentry/cli-win32-i686@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-3.6.0.tgz#b2a2b302cb892801a1b385def2f8c78a0e388363" + integrity sha512-JaxzVDdyetrPBp8NHh2yNAYdDk79ROXqfAfjQwG5z6V764MMMrf2WrhQ7EwoKXOPtBLm/drbOcYgaxHuDZKGRw== + +"@sentry/cli-win32-x64@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-3.6.0.tgz#4437101b8a07228bcdf0c30a6e5d3f9b27e79522" + integrity sha512-LW0078VlxaUeVMMLA15A1zhkvZ5vby/lwthtBXPoBSDdTcgbTE4D4gQXM9vEapV28SvCO3fVIek3pbtrkaeDMw== + +"@sentry/cli@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-3.6.0.tgz#75609e353e5a4f6baaa13b25ac1b2dba21c1ac7f" + integrity sha512-79XC8o59G/i3VqmnoQD74a/QD/422eQQlUqiPd3sEvcYCxnaZialRVAsxuNEFd6sx4aVGpqt775MMDT9cV/lMg== + dependencies: + progress "^2.0.3" + proxy-from-env "^1.1.0" + undici "^6.22.0" + which "^2.0.2" + optionalDependencies: + "@sentry/cli-darwin" "3.6.0" + "@sentry/cli-linux-arm" "3.6.0" + "@sentry/cli-linux-arm64" "3.6.0" + "@sentry/cli-linux-i686" "3.6.0" + "@sentry/cli-linux-x64" "3.6.0" + "@sentry/cli-win32-arm64" "3.6.0" + "@sentry/cli-win32-i686" "3.6.0" + "@sentry/cli-win32-x64" "3.6.0" + +"@sentry/conventions@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@sentry/conventions/-/conventions-0.12.0.tgz#e963cf928ac4d1585b1dcc196e767df3ac45ca5c" + integrity sha512-z1JQrl/1SLY+8wpzvork6vl+fpsg/oCCxM7HWWhUnI/R+OGNyoIzieQuggX3uUMY7NBtp8UWCQx6FeFazzOF9g== + +"@sentry/core@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.60.0.tgz#12548d27b7e4958a24cf7bbc19956b27fa2a2ce7" + integrity sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw== + +"@sentry/electron@7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@sentry/electron/-/electron-7.14.0.tgz#6b74518281a8a30232f99c1d30622bb438c37e68" + integrity sha512-H2Ommi2B/I2QwUT2WJW1uqBiuzDXhuv4pw8hkVm63qBnwGZkSH54YdDOAzkXF9bmw3SHaIAjZBFzsUrvFAzgyw== + dependencies: + "@sentry/browser" "10.60.0" + "@sentry/core" "10.60.0" + "@sentry/node" "10.60.0" + +"@sentry/feedback@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/feedback/-/feedback-10.60.0.tgz#c8090a2aa9de549f4894548ec08b549b8281706f" + integrity sha512-RcGUgaI8yrIXunhNLpdNLsBUJIDvnEGDRmFhC5v0oMltoGtovrIqrEhPXEiSWQvNB0x4q33ejkLeJRJoSDOp2w== + dependencies: + "@sentry/core" "10.60.0" + +"@sentry/node-core@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/node-core/-/node-core-10.60.0.tgz#d964354b09cec7e37f4f642ca096300e125e6deb" + integrity sha512-aXi9ixvP+hgUZPPZCRwMNHgY2I0gkSeoAKAUuysDJhWDmrygwfGdlkbGmmtW6PQjtMYFx69Igt5btvhjEBoJTw== + dependencies: + "@sentry/conventions" "^0.12.0" + "@sentry/core" "10.60.0" + "@sentry/opentelemetry" "10.60.0" + import-in-the-middle "^3.0.0" + +"@sentry/node@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-10.60.0.tgz#ac315d4049a65cc3540a095ef096c20f47146bca" + integrity sha512-u//paUrkKaCr0oNn7r7UulGydkYMSkU1wQOIpG/P/jf7psZWnyXhgeszHzUfZXo6pCdxXG9z9viPvzGjqPQN7A== + dependencies: + "@opentelemetry/api" "^1.9.1" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/sdk-trace-base" "^2.6.1" + "@opentelemetry/semantic-conventions" "^1.40.0" + "@sentry/core" "10.60.0" + "@sentry/node-core" "10.60.0" + "@sentry/opentelemetry" "10.60.0" + "@sentry/server-utils" "10.60.0" + import-in-the-middle "^3.0.0" + +"@sentry/opentelemetry@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-10.60.0.tgz#9707214b9374b4cb56bba48c2244e52f6cfbf8c4" + integrity sha512-gl+2NVH+9RmTu7pd9kV1tKif+Th+p9tmnXR1l3Sb3Wqo1ir5FaNMKrloWEKMXjnepii9EJUrEHdSC+i8NoexxQ== + dependencies: + "@sentry/conventions" "^0.12.0" + "@sentry/core" "10.60.0" + +"@sentry/replay-canvas@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/replay-canvas/-/replay-canvas-10.60.0.tgz#f06a40dc7b98eecf41054dcb4c252f9d4216549a" + integrity sha512-mYyQRJbhRRaqUkRvkJZyqt9AWdomh4108LOAph1YJvV1jW7tKYPPFNAUveJd7YkYCyhUOtekN0DKNJveK802qA== + dependencies: + "@sentry/core" "10.60.0" + "@sentry/replay" "10.60.0" + +"@sentry/replay@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-10.60.0.tgz#1367fb7c5993c4e20ebbfdbaf9dd72603fc9c577" + integrity sha512-j+w774BP1p+v/ga1hJAJSn0cXgTiB2VWwQCAxukF7AEXscny/OCXzTTsaPxdovw9kDHI641vKV+/yixLV2nKIQ== + dependencies: + "@sentry/browser-utils" "10.60.0" + "@sentry/core" "10.60.0" + +"@sentry/server-utils@10.60.0": + version "10.60.0" + resolved "https://registry.yarnpkg.com/@sentry/server-utils/-/server-utils-10.60.0.tgz#ec92038561bc45ff2492259bdb6a183775475096" + integrity sha512-SX+MzWM3nz5ttKT48rlfktm0ERyIpDLma+b6pYeWgW2oFHKcpIu0g0qMGJrZs4lKM3MlgV7IqLa4texMqTp9kQ== + dependencies: + "@apm-js-collab/code-transformer" "^0.15.0" + "@apm-js-collab/code-transformer-bundler-plugins" "^0.5.0" + "@apm-js-collab/tracing-hooks" "^0.10.0" + "@sentry/conventions" "^0.12.0" + "@sentry/core" "10.60.0" + magic-string "~0.30.0" + "@sindresorhus/is@^4.0.0": version "4.6.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== -"@smithy/core@^3.24.3": - version "3.24.3" - resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.24.3.tgz#c9689ce6d64b40eee594a259b4504f1a357f6a54" - integrity sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg== +"@smithy/core@^3.24.6": + version "3.24.6" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.24.6.tgz#72891bad85d577b2e43f30a8fc67adf36d577798" + integrity sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug== dependencies: "@aws-crypto/crc32" "5.2.0" - "@smithy/types" "^4.14.2" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@smithy/core@^3.24.4": - version "3.24.4" - resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.24.4.tgz#aded2ba46962b5cceaaa75f646433ac4813c2e17" - integrity sha512-3UNRKEyQyAgVgM0LGlerCLm+ChZWZ1GPfde+jBEW6bm6bSBGU1p0EbblaUV3unbhwvidjLA5Zs3sOs7mnZwvAw== +"@smithy/credential-provider-imds@^4.3.7": + version "4.3.7" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.7.tgz#f81a2208fc42e134ead1fed4a6c676cc8b89689b" + integrity sha512-xj8gq/bjFABAh6qWPSDCYcY3kzQIm4b561C+YnHH4zGq8rOgzQ3Shk+JGlpUxSd41UGiO6FkLdUCtNX1FAeHgg== dependencies: - "@aws-crypto/crc32" "5.2.0" - "@smithy/types" "^4.14.2" - tslib "^2.6.2" - -"@smithy/credential-provider-imds@^4.3.2": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz#bead31aad6bebac48f034016bce77f68f8b2e4ab" - integrity sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w== - dependencies: - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@smithy/fetch-http-handler@^5.4.3": - version "5.4.4" - resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.4.tgz#df28cfdbdbd192cef9508347b488d8874d0166dd" - integrity sha512-qM7AUKI4G6d7lNgaZD3lA1tWSolh5r6gcixfTZAPstVURfjIbvreVTPz+994M0yC3HbX4YYhDRgr31Xy3XwWOQ== +"@smithy/fetch-http-handler@^5.4.6": + version "5.4.6" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.6.tgz#745cdf8b6c333632672f8f48360bde04b8955b47" + integrity sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g== dependencies: - "@smithy/core" "^3.24.4" - "@smithy/types" "^4.14.2" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" "@smithy/is-array-buffer@^2.2.0": @@ -2176,22 +2456,22 @@ dependencies: tslib "^2.6.2" -"@smithy/node-http-handler@^4.7.3": - version "4.7.4" - resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.7.4.tgz#dfa9634130841cbb0a780c8b4a3ea7ec1c904f0c" - integrity sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ== +"@smithy/node-http-handler@^4.7.6": + version "4.7.6" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.7.6.tgz#bc53f75d34106a0cdd867f4e652999ba8a4926fd" + integrity sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ== dependencies: - "@smithy/core" "^3.24.4" - "@smithy/types" "^4.14.2" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" -"@smithy/signature-v4@^5.4.2": - version "5.4.3" - resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.4.3.tgz#d5bea6e6c32fef6bee0afe6819b9c9551b905103" - integrity sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g== +"@smithy/signature-v4@^5.4.6": + version "5.4.6" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.4.6.tgz#5d2b98aa10e629b6aef36f2289226df81ba4c98e" + integrity sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ== dependencies: - "@smithy/core" "^3.24.3" - "@smithy/types" "^4.14.2" + "@smithy/core" "^3.24.6" + "@smithy/types" "^4.14.3" tslib "^2.6.2" "@smithy/types@^4.12.0": @@ -2201,10 +2481,10 @@ dependencies: tslib "^2.6.2" -"@smithy/types@^4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.14.2.tgz#6034ff1e0e52bfb7d744ac371b651a8bf21f30f1" - integrity sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw== +"@smithy/types@^4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.14.3.tgz#784e6d556231645744edf3fea85daaac9054eb40" + integrity sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ== dependencies: tslib "^2.6.2" @@ -2349,10 +2629,10 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@tybys/wasm-util@^0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" - integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== +"@tybys/wasm-util@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d" + integrity sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg== dependencies: tslib "^2.4.0" @@ -2520,6 +2800,11 @@ "@types/jsonfile" "*" "@types/node" "*" +"@types/gensync@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/gensync/-/gensync-1.0.5.tgz#1819eba537d036dcf4ec7e8c8f15fbfe938e99a0" + integrity sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg== + "@types/glob@^9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-9.0.0.tgz#7b942fafe09c55671912b34f04e8e4676faf32b1" @@ -2551,6 +2836,11 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== +"@types/jsesc@^2.5.0": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@types/jsesc/-/jsesc-2.5.1.tgz#c34defc608ec94b68dc6a12a581b440942c6d503" + integrity sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw== + "@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -2627,19 +2917,19 @@ dependencies: undici-types "~7.16.0" -"@types/node@^25.9.1": - version "25.9.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.1.tgz#3bda556db500ae4319c08e7fc9ab94f19013ba0b" - integrity sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg== +"@types/node@^26.0.1": + version "26.0.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.0.1.tgz#4a60e2c7a6d68bd261e265f8983bfe1601263ce3" + integrity sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw== dependencies: - undici-types ">=7.24.0 <7.24.7" + undici-types "~8.3.0" "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== -"@types/plist@^3.0.1", "@types/plist@^3.0.5": +"@types/plist@^3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.5.tgz#9a0c49c0f9886c8c8696a7904dd703f6284036e0" integrity sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA== @@ -2676,10 +2966,10 @@ dependencies: csstype "^3.2.2" -"@types/react@^19.2.15": - version "19.2.15" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.15.tgz#9e2c6a4251a290f5c525c3143caa872fa6e01e0d" - integrity sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q== +"@types/react@^19.2.17": + version "19.2.17" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.17.tgz#dccac365baa0f1734ec270ff4b51c89465e8dc7f" + integrity sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw== dependencies: csstype "^3.2.2" @@ -2764,11 +3054,6 @@ dependencies: uuid "*" -"@types/verror@^1.10.3": - version "1.10.11" - resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.11.tgz#d3d6b418978c8aa202d41e5bb3483227b6ecc1bb" - integrity sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg== - "@types/wrap-ansi@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" @@ -2788,100 +3073,100 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz#c67bfee32caae9cb587dce1ac59c3bf43b659707" - integrity sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A== +"@typescript-eslint/eslint-plugin@^8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz#ef482aab65b9b2c0abf92d36d670a0d270bcef4c" + integrity sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.59.4" - "@typescript-eslint/type-utils" "8.59.4" - "@typescript-eslint/utils" "8.59.4" - "@typescript-eslint/visitor-keys" "8.59.4" + "@typescript-eslint/scope-manager" "8.62.0" + "@typescript-eslint/type-utils" "8.62.0" + "@typescript-eslint/utils" "8.62.0" + "@typescript-eslint/visitor-keys" "8.62.0" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.5.0" -"@typescript-eslint/parser@^8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.4.tgz#77d99e3b27663e7a22cf12c3fb769db509e5e93c" - integrity sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ== +"@typescript-eslint/parser@^8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.62.0.tgz#8533094fb44427f50b82813c6d3876782f20dc3e" + integrity sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA== dependencies: - "@typescript-eslint/scope-manager" "8.59.4" - "@typescript-eslint/types" "8.59.4" - "@typescript-eslint/typescript-estree" "8.59.4" - "@typescript-eslint/visitor-keys" "8.59.4" + "@typescript-eslint/scope-manager" "8.62.0" + "@typescript-eslint/types" "8.62.0" + "@typescript-eslint/typescript-estree" "8.62.0" + "@typescript-eslint/visitor-keys" "8.62.0" debug "^4.4.3" -"@typescript-eslint/project-service@8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.4.tgz#5830535a0e7a3ae806e2669964f47a74c4bc6b0e" - integrity sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg== +"@typescript-eslint/project-service@8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.62.0.tgz#ab74c1abb4959fb4c3ba7d7edc6554ee245db990" + integrity sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.59.4" - "@typescript-eslint/types" "^8.59.4" + "@typescript-eslint/tsconfig-utils" "^8.62.0" + "@typescript-eslint/types" "^8.62.0" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz#507d1258c758147dac1adee9517a205a8ac1e046" - integrity sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q== +"@typescript-eslint/scope-manager@8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz#a7a7b428d32444bc9a4fe16f24a78fc124283fd4" + integrity sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA== dependencies: - "@typescript-eslint/types" "8.59.4" - "@typescript-eslint/visitor-keys" "8.59.4" + "@typescript-eslint/types" "8.62.0" + "@typescript-eslint/visitor-keys" "8.62.0" -"@typescript-eslint/tsconfig-utils@8.59.4", "@typescript-eslint/tsconfig-utils@^8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz#218ba229d96dde35212e3a76a7d0a6bc831398be" - integrity sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA== +"@typescript-eslint/tsconfig-utils@8.62.0", "@typescript-eslint/tsconfig-utils@^8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz#9440a673581c6d9de308c4d5803dd52ed5d71729" + integrity sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g== -"@typescript-eslint/type-utils@8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz#359fc53ba39a1f1860fddda40ebe5bfe0d87faed" - integrity sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA== +"@typescript-eslint/type-utils@8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz#6f64d813ed9f340d796baed40cdab86b8e9a491a" + integrity sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w== dependencies: - "@typescript-eslint/types" "8.59.4" - "@typescript-eslint/typescript-estree" "8.59.4" - "@typescript-eslint/utils" "8.59.4" + "@typescript-eslint/types" "8.62.0" + "@typescript-eslint/typescript-estree" "8.62.0" + "@typescript-eslint/utils" "8.62.0" debug "^4.4.3" ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.59.4", "@typescript-eslint/types@^8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.4.tgz#c29d5c21bfbaa8347ddc677d3ac1fcd2db0f848e" - integrity sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q== +"@typescript-eslint/types@8.62.0", "@typescript-eslint/types@^8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.0.tgz#601427c10203d9f0f34f0b3e474df735eb12b593" + integrity sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg== -"@typescript-eslint/typescript-estree@8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz#d005e5e1fb425526f39685594bed34a04ad755ea" - integrity sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag== +"@typescript-eslint/typescript-estree@8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz#b96b55d02e26aa09434421c3fa678e525ca09a4c" + integrity sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A== dependencies: - "@typescript-eslint/project-service" "8.59.4" - "@typescript-eslint/tsconfig-utils" "8.59.4" - "@typescript-eslint/types" "8.59.4" - "@typescript-eslint/visitor-keys" "8.59.4" + "@typescript-eslint/project-service" "8.62.0" + "@typescript-eslint/tsconfig-utils" "8.62.0" + "@typescript-eslint/types" "8.62.0" + "@typescript-eslint/visitor-keys" "8.62.0" debug "^4.4.3" minimatch "^10.2.2" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.4.tgz#8ccd2b08aecc72c7efc0d7ac6695631d199d256e" - integrity sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw== +"@typescript-eslint/utils@8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.62.0.tgz#b5228524ca1ee51af40e156c82d425dec3e01cfe" + integrity sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.59.4" - "@typescript-eslint/types" "8.59.4" - "@typescript-eslint/typescript-estree" "8.59.4" + "@typescript-eslint/scope-manager" "8.62.0" + "@typescript-eslint/types" "8.62.0" + "@typescript-eslint/typescript-estree" "8.62.0" -"@typescript-eslint/visitor-keys@8.59.4": - version "8.59.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz#1ac23b747b011f5cbdb449da97769f6c5f3a9355" - integrity sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ== +"@typescript-eslint/visitor-keys@8.62.0": + version "8.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz#b6daab190bf8f18612f5b86323469a12288c6b31" + integrity sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ== dependencies: - "@typescript-eslint/types" "8.59.4" + "@typescript-eslint/types" "8.62.0" eslint-visitor-keys "^5.0.0" "@ungap/structured-clone@^1.0.0": @@ -2889,12 +3174,12 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== -"@vitejs/plugin-react@^6.0.2": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz#f70cb8ed0ce225dbc3055d78070f820d8aa35eda" - integrity sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg== +"@vitejs/plugin-react@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz#55f1d7f558534d10aef03c007dc208b7c3771ce4" + integrity sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg== dependencies: - "@rolldown/pluginutils" "^1.0.0" + "@rolldown/pluginutils" "^1.0.1" "@vscode/sudo-prompt@^9.3.1": version "9.3.2" @@ -3055,11 +3340,6 @@ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz#b79de2d67389734c57c52595f7a7305e30c2d608" integrity sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw== -"@xmldom/xmldom@^0.9.10": - version "0.9.10" - resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz#a0ad5a26fe8aa996310870726e1704977f769dee" - integrity sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw== - "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -3070,10 +3350,10 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -abbrev@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-4.0.0.tgz#ec933f0e27b6cd60e89b5c6b2a304af42209bb05" - integrity sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA== +abbrev@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-5.0.0.tgz#bab70805adca6637a33eeecdb8b7b5474d6d6425" + integrity sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w== abstract-leveldown@~0.12.0, abstract-leveldown@~0.12.1: version "0.12.4" @@ -3090,6 +3370,11 @@ accepts@~1.3.4: mime-types "~2.1.34" negotiator "0.6.3" +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + acorn-import-phases@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" @@ -3124,15 +3409,20 @@ acorn@^8.16.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== +acorn@^8.17.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe" + integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg== + adm-zip@^0.5.16: version "0.5.16" resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== -adm-zip@^0.5.17: - version "0.5.17" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.17.tgz#5c0b65f37aeec5c2a94995c024f931f62e4bbc5a" - integrity sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ== +adm-zip@^0.5.18: + version "0.5.18" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.18.tgz#283a05f2bf1e3fd315f0f31cde29b7a6e3c1619d" + integrity sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng== agent-base@6: version "6.0.2" @@ -3160,11 +3450,6 @@ ajv-formats@^3.0.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.4.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" @@ -3172,16 +3457,6 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.0: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - 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" - ajv@^6.14.0: version "6.14.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" @@ -3202,6 +3477,16 @@ ajv@^8.0.0, ajv@^8.17.1, ajv@^8.9.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" +ajv@^8.18.0: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9" + integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ansi-escapes@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -3238,36 +3523,34 @@ ansi-styles@^6.0.0, ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== -app-builder-bin@5.0.0-alpha.12: - version "5.0.0-alpha.12" - resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz#2daf82f8badc698e0adcc95ba36af4ff0650dc80" - integrity sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w== - -app-builder-lib@26.8.1: - version "26.8.1" - resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.8.1.tgz#315c893bf1f5882cc6cd174cfcd00535dbb76786" - integrity sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw== +app-builder-lib@26.15.3: + version "26.15.3" + resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.15.3.tgz#0fa6c965e307bb6d86226407c750a45b3fd6b1bf" + integrity sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow== dependencies: - "@develar/schema-utils" "~2.6.5" "@electron/asar" "3.4.1" "@electron/fuses" "^1.8.0" "@electron/get" "^3.0.0" "@electron/notarize" "2.5.0" "@electron/osx-sign" "1.3.3" - "@electron/rebuild" "^4.0.3" + "@electron/rebuild" "^4.0.4" "@electron/universal" "2.0.3" "@malept/flatpak-bundler" "^0.4.0" + "@noble/hashes" "^2.2.0" + "@peculiar/webcrypto" "^1.7.1" "@types/fs-extra" "9.0.13" + ajv "^8.18.0" + asn1js "^3.0.10" async-exit-hook "^2.0.1" - builder-util "26.8.1" - builder-util-runtime "9.5.1" + builder-util "26.15.3" + builder-util-runtime "9.7.0" chromium-pickle-js "^0.2.0" ci-info "4.3.1" debug "^4.3.4" dotenv "^16.4.5" dotenv-expand "^11.0.6" ejs "^3.1.8" - electron-publish "26.8.1" + electron-publish "26.15.3" fs-extra "^10.1.0" hosted-git-info "^4.1.0" isbinaryfile "^5.0.0" @@ -3275,7 +3558,8 @@ app-builder-lib@26.8.1: js-yaml "^4.1.0" json5 "^2.2.3" lazy-val "^1.0.5" - minimatch "^10.0.3" + minimatch "^10.2.5" + pkijs "^3.4.0" plist "3.1.0" proper-lockfile "^4.1.2" resedit "^1.7.0" @@ -3283,6 +3567,7 @@ app-builder-lib@26.8.1: tar "^7.5.7" temp-file "^3.4.0" tiny-async-pool "1.3.0" + unzipper "^0.12.3" which "^5.0.0" appdmg@^0.6.4: @@ -3389,15 +3674,19 @@ asn1.js@^4.10.1: inherits "^2.0.1" minimalistic-assert "^1.0.0" -assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== +asn1js@^3.0.10, asn1js@^3.0.6: + version "3.0.10" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.10.tgz#df26c874c8a8b41ca605efea47b2ad07551013dd" + integrity sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg== + dependencies: + pvtsutils "^1.3.6" + pvutils "^1.1.5" + tslib "^2.8.1" -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +astring@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/astring/-/astring-1.9.0.tgz#cc73e6062a7eb03e7d19c22d8b0b3451fd9bfeef" + integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg== async-exit-hook@^2.0.1: version "2.0.1" @@ -3449,10 +3738,15 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@^1.16.1: - version "1.16.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.1.tgz#517e29291d19d6e8cf919ff264f4fe157261ba12" - integrity sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A== +aws4@^1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef" + integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== + +axios@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.18.1.tgz#d63f9863bcd8938815c86f9e2abd380189d96dfe" + integrity sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g== dependencies: follow-redirects "^1.16.0" form-data "^4.0.5" @@ -3499,7 +3793,7 @@ balanced-match@^4.0.2: dependencies: to-data-view "^1.1.0" -base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -3521,7 +3815,7 @@ bl@~0.8.1: dependencies: readable-stream "~1.0.26" -bluebird@^3.1.1: +bluebird@^3.1.1, bluebird@~3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -3536,6 +3830,11 @@ bn.js@^5.2.1, bn.js@^5.2.2: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.2.tgz#82c09f9ebbb17107cd72cb7fd39bd1f9d0aaa566" integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== +bn.js@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.3.tgz#16a9e409616b23fef3ccbedb8d42f13bff80295e" + integrity sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w== + boolean@^3.0.1: version "3.2.0" resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" @@ -3575,6 +3874,13 @@ brace-expansion@^5.0.2: dependencies: balanced-match "^4.0.2" +brace-expansion@^5.0.5: + version "5.0.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.6.tgz#ec68fe0a641a29d8711579caf641d05bae1f2285" + integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g== + dependencies: + balanced-match "^4.0.2" + braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -3636,7 +3942,7 @@ browserify-rsa@^4.0.0, browserify-rsa@^4.1.1: randombytes "^2.1.0" safe-buffer "^5.2.1" -browserify-sign@^4.2.3, browserify-sign@^4.2.5: +browserify-sign@^4.2.3: version "4.2.5" resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.5.tgz#3979269fa8af55ba18aac35deef11b45515cd27d" integrity sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw== @@ -3651,6 +3957,21 @@ browserify-sign@^4.2.3, browserify-sign@^4.2.5: readable-stream "^2.3.8" safe-buffer "^5.2.1" +browserify-sign@^4.2.6: + version "4.2.6" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.6.tgz#a8c9fd9a701a3600c7fea3a82c14ab82cad6f451" + integrity sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg== + dependencies: + bn.js "^5.2.3" + browserify-rsa "^4.1.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.6.1" + inherits "^2.0.4" + parse-asn1 "^5.1.9" + readable-stream "^2.3.8" + safe-buffer "^5.2.1" + browserslist@^4.24.0, browserslist@^4.28.1: version "4.28.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" @@ -3677,14 +3998,6 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== -buffer@^5.1.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - bufferutil@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.1.0.tgz#a4623541dd23867626bb08a051ec0d2ec0b70294" @@ -3692,23 +4005,21 @@ bufferutil@^4.1.0: dependencies: node-gyp-build "^4.3.0" -builder-util-runtime@9.5.1: - version "9.5.1" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz#74125fb374d1ecbf472ae1787485485ff7619702" - integrity sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ== +builder-util-runtime@9.7.0: + version "9.7.0" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz#c86fba303684e877daee15c29eede81987166fef" + integrity sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw== dependencies: debug "^4.3.4" sax "^1.2.4" -builder-util@26.8.1: - version "26.8.1" - resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.8.1.tgz#50fdfc2d4ffeb6f739af363b5bd60c49c95d4170" - integrity sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw== +builder-util@26.15.3: + version "26.15.3" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.15.3.tgz#1eeab37f0d4a6421fe18ce9db38ced58881d9519" + integrity sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw== dependencies: - "7zip-bin" "~5.2.0" "@types/debug" "^4.1.6" - app-builder-bin "5.0.0-alpha.12" - builder-util-runtime "9.5.1" + builder-util-runtime "9.7.0" chalk "^4.1.2" cross-spawn "^7.0.6" debug "^4.3.4" @@ -3722,6 +4033,11 @@ builder-util@26.8.1: temp-file "^3.4.0" tiny-async-pool "1.3.0" +bytestreamjs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bytestreamjs/-/bytestreamjs-2.0.1.tgz#a32947c7ce389a6fa11a09a9a563d0a45889535e" + integrity sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ== + cacheable-lookup@^5.0.3: version "5.0.4" resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" @@ -3862,6 +4178,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: safe-buffer "^5.2.1" to-buffer "^1.2.2" +cjs-module-lexer@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz#b3ca5101843389259ade7d88c77bd06ce55849ca" + integrity sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ== + cli-cursor@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" @@ -3869,14 +4190,6 @@ cli-cursor@^4.0.0: dependencies: restore-cursor "^4.0.0" -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - cli-truncate@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389" @@ -4034,11 +4347,6 @@ cookie@~0.7.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -4073,13 +4381,6 @@ cosmiconfig@^8.1.3: parse-json "^5.2.0" path-type "^4.0.0" -crc@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" - integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== - dependencies: - buffer "^5.1.0" - create-ecdh@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -4279,7 +4580,7 @@ debounce-fn@^6.0.0: dependencies: mimic-function "^5.0.0" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@^4.4.3, debug@~4.4.1: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7, debug@^4.4.1, debug@^4.4.3, debug@~4.4.1: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -4416,32 +4717,15 @@ dir-compare@^4.2.0: minimatch "^3.0.5" p-limit "^3.1.0 " -dmg-builder@26.8.1: - version "26.8.1" - resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.8.1.tgz#df99aa790676ac2a2ac0333bbadbef3b6076cb03" - integrity sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg== +dmg-builder@26.15.3: + version "26.15.3" + resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.15.3.tgz#d884362e223551ee0dbc96bbb7187b42905378f8" + integrity sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ== dependencies: - app-builder-lib "26.8.1" - builder-util "26.8.1" + app-builder-lib "26.15.3" + builder-util "26.15.3" fs-extra "^10.1.0" - iconv-lite "^0.6.2" js-yaml "^4.1.0" - optionalDependencies: - dmg-license "^1.0.11" - -dmg-license@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/dmg-license/-/dmg-license-1.0.11.tgz#7b3bc3745d1b52be7506b4ee80cb61df6e4cd79a" - integrity sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q== - 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" doctrine@^2.1.0: version "2.1.0" @@ -4503,6 +4787,13 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1: es-errors "^1.3.0" gopd "^1.2.0" +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -4515,17 +4806,17 @@ ejs@^3.1.8: dependencies: jake "^10.8.5" -electron-builder@^26.8.1: - version "26.8.1" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.8.1.tgz#d49056b2fe5d37f0f94aa2eb0e1db38f261fc8c0" - integrity sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw== +electron-builder@^26.15.3: + version "26.15.3" + resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.15.3.tgz#0e82828f6829c0b2596338365209170ac0acbcd5" + integrity sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA== dependencies: - app-builder-lib "26.8.1" - builder-util "26.8.1" - builder-util-runtime "9.5.1" + app-builder-lib "26.15.3" + builder-util "26.15.3" + builder-util-runtime "9.7.0" chalk "^4.1.2" ci-info "^4.2.0" - dmg-builder "26.8.1" + dmg-builder "26.15.3" fs-extra "^10.1.0" lazy-val "^1.0.5" simple-update-notifier "2.0.0" @@ -4596,14 +4887,15 @@ electron-installer-redhat@^3.2.0: word-wrap "^1.2.3" yargs "^16.0.2" -electron-publish@26.8.1: - version "26.8.1" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.8.1.tgz#6a32fa8eed0d41971dda53072bea06b9932be583" - integrity sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w== +electron-publish@26.15.3: + version "26.15.3" + resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.15.3.tgz#db65642070bf60c7bd26d607441506891d3ad054" + integrity sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q== dependencies: "@types/fs-extra" "^9.0.11" - builder-util "26.8.1" - builder-util-runtime "9.5.1" + aws4 "^1.13.2" + builder-util "26.15.3" + builder-util-runtime "9.7.0" chalk "^4.1.2" form-data "^4.0.5" fs-extra "^10.1.0" @@ -4623,12 +4915,12 @@ electron-to-chromium@^1.5.263: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7" integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw== -electron-updater@^6.8.3: - version "6.8.3" - resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.8.3.tgz#bb0c8ef6509e5c67663f6481a729244d1bce21fb" - integrity sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ== +electron-updater@^6.8.9: + version "6.8.9" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.8.9.tgz#21e3e2400ee3a58b7496f0a023d95d7ab1dae019" + integrity sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig== dependencies: - builder-util-runtime "9.5.1" + builder-util-runtime "9.7.0" fs-extra "^10.1.0" js-yaml "^4.1.0" lazy-val "^1.0.5" @@ -4659,14 +4951,14 @@ electron@*: "@types/node" "^24.9.0" extract-zip "^2.0.1" -electron@42.2.0: - version "42.2.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-42.2.0.tgz#513ac2d86c2a3217c17d25f24809bc46d470e7c2" - integrity sha512-b2Tc7sIKiZEl0tBVwFM5GJ+FT5KYhmy9QJHjx8BGVZPVW2SctXWEvrE959ElB56qw7H05dBkhlikDA1DmpaAMw== +electron@42.5.1: + version "42.5.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-42.5.1.tgz#ca6c6119c119b818c145c7e838c3484e36c35b05" + integrity sha512-2VFNJcHHbrhIpGsJHdkLoi/nWPZPxN3GHVPe+9At3Oz3/TJRwpr+7JL97ddBDbKyLmHGx3GfI2jvzcEQL28uFw== dependencies: + "@electron-internal/extract-zip" "^1.0.1" "@electron/get" "^5.0.0" "@types/node" "^24.9.0" - extract-zip "^2.0.1" elliptic@^6.5.3, elliptic@^6.6.1: version "6.6.1" @@ -4691,6 +4983,11 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +empathic@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/empathic/-/empathic-2.0.1.tgz#37b1bede31093e04a03d2abce95ea323fee8ef49" + integrity sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q== + encode-utf8@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" @@ -4856,6 +5153,11 @@ es-module-lexer@^2.0.0: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.0.0.tgz#f657cd7a9448dcdda9c070a3cb75e5dc1e85f5b1" integrity sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw== +es-module-lexer@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz#1dfcbb5ea3bbfb63f28e1fc3676c3676d1c9624c" + integrity sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ== + es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" @@ -5025,17 +5327,17 @@ eslint-visitor-keys@^5.0.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be" integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== -eslint@^10.4.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.4.0.tgz#d86b6c405de0f19f3318c47139b8cb6771b3f592" - integrity sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ== +eslint@^10.6.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.6.0.tgz#e1b4059c582be950c7088c9b55f984738b243c27" + integrity sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg== dependencies: "@eslint-community/eslint-utils" "^4.8.0" "@eslint-community/regexpp" "^4.12.2" "@eslint/config-array" "^0.23.5" "@eslint/config-helpers" "^0.6.0" "@eslint/core" "^1.2.1" - "@eslint/plugin-kit" "^0.7.1" + "@eslint/plugin-kit" "^0.7.2" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" @@ -5180,11 +5482,6 @@ extract-zip@^2.0.0, extract-zip@^2.0.1: optionalDependencies: "@types/yauzl" "^2.9.1" -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -5216,24 +5513,6 @@ fast-uri@^3.0.1: resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== -fast-xml-builder@^1.1.7: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz#abd2363145a7625d9789ad96da375fabe3cff28c" - integrity sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q== - dependencies: - path-expression-matcher "^1.5.0" - xml-naming "^0.1.0" - -fast-xml-parser@5.7.3: - version "5.7.3" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz#309b04b08d835defc62ab657a0bb340c0e0fbe6a" - integrity sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg== - dependencies: - "@nodable/entities" "^2.1.0" - fast-xml-builder "^1.1.7" - path-expression-matcher "^1.5.0" - strnum "^2.2.3" - fastq@^1.6.0: version "1.20.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" @@ -5378,12 +5657,12 @@ formik@^2.4.9: tiny-warning "^1.0.2" tslib "^2.0.0" -framer-motion@^12.40.0: - version "12.40.0" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.40.0.tgz#68e53aecd51c8a8a62b565f059b418bef2add0e2" - integrity sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg== +framer-motion@^12.42.0: + version "12.42.0" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.42.0.tgz#6068fc03c19df071e1df16419edd8717f0ce8d8b" + integrity sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA== dependencies: - motion-dom "^12.40.0" + motion-dom "^12.42.0" motion-utils "^12.39.0" tslib "^2.4.0" @@ -5405,7 +5684,7 @@ fs-extra@^11.1.0, fs-extra@^11.1.1: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^11.3.5: +fs-extra@^11.2.0, fs-extra@^11.3.5: version "11.3.5" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.5.tgz#07a44eff40bea53e719909a532f91a23bf0769ff" integrity sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg== @@ -5704,7 +5983,7 @@ got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -5721,10 +6000,10 @@ graphql-ws@^6.0.8: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-6.0.8.tgz#6712bb9da8462937e92343e3146726f89424df68" integrity sha512-m3EOaNsUBXwAnkBWbzPfe0Nq8pXUfxsWnolC54sru3FzHvhTZL0Ouf/BoQsaGAXqM+YPerXOJ47BUnmgmoupCw== -graphql@^16.14.0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.14.0.tgz#f1128a74b16a34d1245c8436bb07b488d87b83e1" - integrity sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q== +graphql@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-17.0.1.tgz#43dbd7b152944c31dab264212f102894886e5f9f" + integrity sha512-8eWbg5Zcv/8o20nzEjHUGPTj20MLFJjc5kagbIPxbaeGxvFwpitJhemEC/k17n5+UD4M/9ea5rTuce78mELujQ== has-bigints@^1.0.2: version "1.1.0" @@ -5969,18 +6248,10 @@ https-proxy-agent@^7.0.0: agent-base "^7.1.2" debug "4" -i18next@^26.2.0: - version "26.2.0" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-26.2.0.tgz#4dc31d5ada1495f6884851f7c7a3fb6a36f23794" - integrity sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA== - -iconv-corefoundation@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz#31065e6ab2c9272154c8b0821151e2c88f1b002a" - integrity sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ== - dependencies: - cli-truncate "^2.1.0" - node-addon-api "^1.6.3" +i18next@^26.3.3: + version "26.3.3" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-26.3.3.tgz#acab67bb8608deaf01886a1d573dbd6f2c201b9b" + integrity sha512-aYVegyBdXSO93CMMihvr47jI7GHSOcIahMpJX+qzUXDzW4xDJf2uenIA+45vDU+YhiVdcfsql70AC9RVdMNrHg== iconv-lite@^0.4.24: version "0.4.24" @@ -5989,13 +6260,6 @@ iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - iconv-lite@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e" @@ -6008,11 +6272,6 @@ idb-wrapper@^1.5.0: resolved "https://registry.yarnpkg.com/idb-wrapper/-/idb-wrapper-1.7.2.tgz#8251afd5e77fe95568b1c16152eb44b396767ea2" integrity sha512-zfNREywMuf0NzDo9mVsL0yegjsirJxHpKHvWcyRozIqQy89g0a3U+oBPOCN4cc0oCiOuYgZHimzaW/R46G1Mpg== -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -6051,6 +6310,21 @@ import-fresh@^3.2.1, import-fresh@^3.3.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-in-the-middle@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-3.1.0.tgz#0d0e88c93c276599bd4bf81835946d723656efab" + integrity sha512-c0AeAV8VcwZzfYE7euTZY3H+VXUPMVugiovdosq80lqEXJmOekg3zGUAYg6KImHMaMuBoTUfTv7xNpUFdy0hJA== + dependencies: + acorn "^8.15.0" + acorn-import-attributes "^1.9.5" + cjs-module-lexer "^2.2.0" + module-details-from-path "^1.0.4" + +import-meta-resolve@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz#08cb85b5bd37ecc8eb1e0f670dc2767002d43734" + integrity sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg== + imul@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/imul/-/imul-1.0.1.tgz#9d5867161e8b3de96c2c38d5dc7cb102f35e2ac9" @@ -6415,6 +6689,11 @@ isexe@^3.1.1: resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== +isexe@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-4.0.0.tgz#48f6576af8e87a18feb796b7ed5e2e5903b43dca" + integrity sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw== + jackspeak@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.2.3.tgz#27ef80f33b93412037c3bea4f8eddf80e1931483" @@ -6445,6 +6724,11 @@ jiti@^2.4.2: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92" integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ== +js-tokens@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-10.0.0.tgz#dffe7599b4a8bb7fe30aff8d0235234dffb79831" + integrity sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6876,6 +7160,13 @@ macos-alias@~0.2.5: dependencies: nan "^2.4.0" +magic-string@^0.30.21, magic-string@~0.30.0: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" @@ -7121,6 +7412,11 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +meriyah@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/meriyah/-/meriyah-6.1.4.tgz#2d49a8934fbcd9205c20564579c3560d9b1e077b" + integrity sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ== + micromark-core-commonmark@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" @@ -7457,7 +7753,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^10.0.3, minimatch@^10.1.1: +minimatch@^10.1.1: version "10.1.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== @@ -7478,6 +7774,13 @@ minimatch@^10.2.4: dependencies: brace-expansion "^5.0.2" +minimatch@^10.2.5: + version "10.2.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" + integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== + dependencies: + brace-expansion "^5.0.5" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -7528,10 +7831,15 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" -motion-dom@^12.40.0: - version "12.40.0" - resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.40.0.tgz#95fb411ac72e8adbaf5f1b17b8f07783da223bee" - integrity sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg== +module-details-from-path@^1.0.3, module-details-from-path@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.4.tgz#b662fdcd93f6c83d3f25289da0ce81c8d9685b94" + integrity sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w== + +motion-dom@^12.42.0: + version "12.42.0" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.42.0.tgz#89897281333cf6c23b1bbcd220822514cccb0435" + integrity sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w== dependencies: motion-utils "^12.39.0" @@ -7614,11 +7922,6 @@ node-abi@^4.2.0: dependencies: semver "^7.6.3" -node-addon-api@^1.6.3: - version "1.7.2" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" - integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== - node-addon-api@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" @@ -7643,33 +7946,38 @@ node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== -node-gyp@12.3.0, node-gyp@^12.2.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-12.3.0.tgz#a0e0d9364779451eaf4148b6f9a7366f98000b3f" - integrity sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg== +node-gyp@13.0.0, node-gyp@^12.2.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-13.0.0.tgz#c4265537249cd749cc57cb1d1215c12f4d10b40a" + integrity sha512-FYYyBDWdc+kzoyPd5PqHUgM9DGs1C/Z4jxBZAOnA2GRUVXPivKRREq5q+VVPXVr9aGVqGMaMqyFHbviy/yb7Hg== dependencies: env-paths "^2.2.0" exponential-backoff "^3.1.1" graceful-fs "^4.2.6" - nopt "^9.0.0" - proc-log "^6.0.0" + nopt "^10.0.0" + proc-log "^7.0.0" semver "^7.3.5" tar "^7.5.4" tinyglobby "^0.2.12" undici "^6.25.0" - which "^6.0.0" + which "^7.0.0" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.27: version "2.0.27" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== -nopt@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-9.0.0.tgz#6bff0836b2964d24508b6b41b5a9a49c4f4a1f96" - integrity sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw== +nopt@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-10.0.1.tgz#2d53bfda504b31c447967648ba5ea1a44e594d05" + integrity sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g== dependencies: - abbrev "^4.0.0" + abbrev "^5.0.0" normalize-package-data@^2.3.2: version "2.5.0" @@ -7763,6 +8071,11 @@ object.values@^1.2.1: define-properties "^1.2.1" es-object-atoms "^1.0.0" +obug@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/obug/-/obug-2.1.3.tgz#c02c60f95abd603409330e767db7f2823193331e" + integrity sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg== + octal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/octal/-/octal-1.0.0.tgz#63e7162a68efbeb9e213588d58e989d1e5c4530b" @@ -7955,11 +8268,6 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-expression-matcher@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz#3b98545dc88ffebb593e2d8458d0929da9275f4a" - integrity sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ== - path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -8072,7 +8380,19 @@ pify@^2.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== -plist@3.1.0, plist@^3.0.0, plist@^3.0.4, plist@^3.0.5, plist@^3.1.0: +pkijs@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pkijs/-/pkijs-3.4.0.tgz#d9164def30ff6d97be2d88966d5e36192499ca9c" + integrity sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw== + dependencies: + "@noble/hashes" "1.4.0" + asn1js "^3.0.6" + bytestreamjs "^2.0.1" + pvtsutils "^1.3.6" + pvutils "^1.1.3" + tslib "^2.8.1" + +plist@3.1.0, plist@^3.0.0, plist@^3.0.5, plist@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== @@ -8081,14 +8401,6 @@ plist@3.1.0, plist@^3.0.0, plist@^3.0.4, plist@^3.0.5, plist@^3.1.0: base64-js "^1.5.1" xmlbuilder "^15.1.1" -plist@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/plist/-/plist-5.0.0.tgz#a0a0db9d31667d43e11a4b8e3795af4d76bc0804" - integrity sha512-20N+g1DvMm/DFRbsvER7tT4wDryq0WunK7VMkDaiJcKNapAnUMkTsAnacFYf8n420F4Hf6/hefgmJRkMb1M0fg== - dependencies: - "@xmldom/xmldom" "^0.9.10" - xmlbuilder "^15.1.1" - possible-typed-array-names@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" @@ -8120,15 +8432,15 @@ prettier@^3.4.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== -prettier@^3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0" - integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw== +prettier@^3.9.3: + version "3.9.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.3.tgz#4caabe6fb955e8b8f13319f25872522ece519534" + integrity sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ== -proc-log@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-6.1.0.tgz#18519482a37d5198e231133a70144a50f21f0215" - integrity sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ== +proc-log@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-7.0.0.tgz#46c9cc943ed4127272b49b34faa8855b44e3a976" + integrity sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg== process-nextick-args@~2.0.0: version "2.0.1" @@ -8181,6 +8493,11 @@ protocol-buffers-schema@^3.3.1: resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + proxy-from-env@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" @@ -8226,6 +8543,18 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +pvtsutils@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.6.tgz#ec46e34db7422b9e4fdc5490578c1883657d6001" + integrity sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg== + dependencies: + tslib "^2.8.1" + +pvutils@^1.1.3, pvutils@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.5.tgz#84b0dea4a5d670249aa9800511804ee0b7c2809c" + integrity sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA== + qs@^6.12.3: version "6.14.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159" @@ -8276,10 +8605,10 @@ react-compiler-runtime@^1.0.0: resolved "https://registry.yarnpkg.com/react-compiler-runtime/-/react-compiler-runtime-1.0.0.tgz#ee565c2cd3437a41e7fc31d8ab6c662f6b568fc0" integrity sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w== -react-dom@^19.2.6: - version "19.2.6" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.6.tgz#44a81b0bcca22da814c00847d09d01c8615529b7" - integrity sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g== +react-dom@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.7.tgz#0450dc9ae9ddbff76ef196401cd8b8c7fb466ccc" + integrity sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ== dependencies: scheduler "^0.27.0" @@ -8365,17 +8694,17 @@ react-modal@^3.16.3: "@types/use-sync-external-store" "^0.0.6" use-sync-external-store "^1.4.0" -react-router-dom@^7.15.1: - version "7.15.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.15.1.tgz#cf5aee65a44e407a17a2e9718d5350482b1e281f" - integrity sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg== +react-router-dom@^7.18.0: + version "7.18.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.18.0.tgz#71a657374610e00cebf2783804d7812facfdb1b5" + integrity sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA== dependencies: - react-router "7.15.1" + react-router "7.18.0" -react-router@7.15.1: - version "7.15.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.15.1.tgz#0a12fece05887a47c54970480745385c793bcaac" - integrity sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A== +react-router@7.18.0: + version "7.18.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.18.0.tgz#e7d94b54745277aabe3cf93fac938cbebc9c1c5e" + integrity sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ== dependencies: cookie "^1.0.1" set-cookie-parser "^2.6.0" @@ -8390,10 +8719,10 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^19.2.6: - version "19.2.6" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.6.tgz#3dadb8e12b2a7934c1d5317973e5dce1301f9a4d" - integrity sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q== +react@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.7.tgz#1f47a1bfc06f8ec885752c6f4af14369a9f8260b" + integrity sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ== read-binary-file-arch@^1.0.6: version "1.0.6" @@ -8429,7 +8758,7 @@ readable-stream@^1.0.26-4: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.2.2, readable-stream@^2.3.8: +readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -8601,6 +8930,14 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-in-the-middle@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz#dbde2587f669398626d56b20c868ab87bf01cce4" + integrity sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ== + dependencies: + debug "^4.3.5" + module-details-from-path "^1.0.3" + resedit@^1.7.0: version "1.7.2" resolved "https://registry.yarnpkg.com/resedit/-/resedit-1.7.2.tgz#b1041170b99811710c13f949c7d225871de4cc78" @@ -8710,29 +9047,29 @@ roarr@^2.15.3: semver-compare "^1.0.0" sprintf-js "^1.1.2" -rolldown@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.2.tgz#ea2c786c5f063d08fd22b49e51997f15ec532bbd" - integrity sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g== +rolldown@~1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.1.3.tgz#87072bfd0d1bdd02a66076a261a62e8e49b3f0e2" + integrity sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g== dependencies: - "@oxc-project/types" "=0.132.0" + "@oxc-project/types" "=0.137.0" "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rolldown/binding-android-arm64" "1.0.2" - "@rolldown/binding-darwin-arm64" "1.0.2" - "@rolldown/binding-darwin-x64" "1.0.2" - "@rolldown/binding-freebsd-x64" "1.0.2" - "@rolldown/binding-linux-arm-gnueabihf" "1.0.2" - "@rolldown/binding-linux-arm64-gnu" "1.0.2" - "@rolldown/binding-linux-arm64-musl" "1.0.2" - "@rolldown/binding-linux-ppc64-gnu" "1.0.2" - "@rolldown/binding-linux-s390x-gnu" "1.0.2" - "@rolldown/binding-linux-x64-gnu" "1.0.2" - "@rolldown/binding-linux-x64-musl" "1.0.2" - "@rolldown/binding-openharmony-arm64" "1.0.2" - "@rolldown/binding-wasm32-wasi" "1.0.2" - "@rolldown/binding-win32-arm64-msvc" "1.0.2" - "@rolldown/binding-win32-x64-msvc" "1.0.2" + "@rolldown/binding-android-arm64" "1.1.3" + "@rolldown/binding-darwin-arm64" "1.1.3" + "@rolldown/binding-darwin-x64" "1.1.3" + "@rolldown/binding-freebsd-x64" "1.1.3" + "@rolldown/binding-linux-arm-gnueabihf" "1.1.3" + "@rolldown/binding-linux-arm64-gnu" "1.1.3" + "@rolldown/binding-linux-arm64-musl" "1.1.3" + "@rolldown/binding-linux-ppc64-gnu" "1.1.3" + "@rolldown/binding-linux-s390x-gnu" "1.1.3" + "@rolldown/binding-linux-x64-gnu" "1.1.3" + "@rolldown/binding-linux-x64-musl" "1.1.3" + "@rolldown/binding-openharmony-arm64" "1.1.3" + "@rolldown/binding-wasm32-wasi" "1.1.3" + "@rolldown/binding-win32-arm64-msvc" "1.1.3" + "@rolldown/binding-win32-x64-msvc" "1.1.3" run-parallel@^1.1.9: version "1.2.0" @@ -8798,10 +9135,10 @@ sanitize-filename@^1.6.3: dependencies: truncate-utf8-bytes "^1.0.0" -sass@^1.100.0: - version "1.100.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.100.0.tgz#b4cab1bed286fe22ac6c879c514f71cd36aa06c8" - integrity sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ== +sass@^1.101.0: + version "1.101.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.101.0.tgz#c2db5bbf2f956be7277f6223b899d0d4be3c899b" + integrity sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw== dependencies: chokidar "^5.0.0" immutable "^5.1.5" @@ -8829,6 +9166,11 @@ schema-utils@^4.3.0, schema-utils@^4.3.3: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +semifies@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semifies/-/semifies-1.0.0.tgz#b69569f32c2ba2ac04f705ea82831364289b2ae2" + integrity sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw== + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" @@ -8849,10 +9191,10 @@ semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semve resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== -semver@^7.8.1: - version "7.8.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.1.tgz#bf4970b5e70fda0686363cc18bfe8805d5ed957e" - integrity sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg== +semver@^7.8.5: + version "7.8.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== semver@~2.3.1: version "2.3.2" @@ -8999,15 +9341,6 @@ simple-update-notifier@2.0.0: dependencies: semver "^7.5.3" -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - slice-ansi@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" @@ -9016,11 +9349,6 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" -smart-buffer@^4.0.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -9280,11 +9608,6 @@ strip-outer@^1.0.1: dependencies: escape-string-regexp "^1.0.2" -strnum@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.2.3.tgz#0119fce02749a11bb126a4d686ac5dbdf6e57586" - integrity sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg== - stubborn-fs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/stubborn-fs/-/stubborn-fs-2.0.0.tgz#628750f81c51c44c04ef50fc70ed4d1caea4f1e9" @@ -9352,10 +9675,10 @@ systeminformation@*: resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.30.5.tgz#0b8840ff697b8f036901bf4f8586c9278c7c9e88" integrity sha512-DpWmpCckhwR3hG+6udb6/aQB7PpiqVnvSljrjbKxNSvTRsGsg7NVE3/vouoYf96xgwMxXFKcS4Ux+cnkFwYM7A== -systeminformation@^5.31.6: - version "5.31.6" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.31.6.tgz#2da4979a7262974fd068a3a306ded30aed6127c0" - integrity sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA== +systeminformation@^5.31.11: + version "5.31.11" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.31.11.tgz#9c637ff3e8d19a482deff70a136330e1b2399f00" + integrity sha512-I6O7iaUj23AXRgCPDDnvi3xHvdOLp4+1YMbF+X194lJwY1NeWojgHJPhslVKcmTtrLTguRk3QJK+xEdTiI3P0w== tagged-tag@^1.0.0: version "1.0.0" @@ -9378,10 +9701,10 @@ tar@*: minizlib "^3.1.0" yallist "^5.0.0" -tar@^7.5.15: - version "7.5.15" - resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.15.tgz#afe6d1316cddf614a566e3813e42fe01aed46fee" - integrity sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ== +tar@^7.5.19: + version "7.5.19" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.19.tgz#d8915e6b717f8036a79d839ca9448198b002b0a7" + integrity sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw== dependencies: "@isaacs/fs-minipass" "^4.0.0" chownr "^3.0.0" @@ -9488,10 +9811,10 @@ tinyglobby@^0.2.12, tinyglobby@^0.2.15: fdir "^6.5.0" picomatch "^4.0.3" -tinyglobby@^0.2.16: - version "0.2.16" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" - integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== +tinyglobby@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== dependencies: fdir "^6.5.0" picomatch "^4.0.4" @@ -9611,15 +9934,15 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.2: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.2, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -tsx@4.22.3: - version "4.22.3" - resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.22.3.tgz#7ca7cb34028e3e247f1fad300c157e42a90a1f50" - integrity sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg== +tsx@4.22.4: + version "4.22.4" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.22.4.tgz#0ab3b7fb4ec7feeee74e5b1f26337caa71e44700" + integrity sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg== dependencies: esbuild "~0.28.0" optionalDependencies: @@ -9739,11 +10062,6 @@ unbox-primitive@^1.1.0: has-symbols "^1.1.0" which-boxed-primitive "^1.1.1" -"undici-types@>=7.24.0 <7.24.7": - version "7.24.6" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91" - integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg== - undici-types@~6.21.0: version "6.21.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" @@ -9754,6 +10072,16 @@ undici-types@~7.16.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== +undici-types@~8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809" + integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ== + +undici@^6.22.0: + version "6.27.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.27.0.tgz#41f9e48f7c5a40d27376caaead8c9a9fc7bca9c4" + integrity sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg== + undici@^6.25.0: version "6.25.0" resolved "https://registry.yarnpkg.com/undici/-/undici-6.25.0.tgz#8c4efb8c998dc187fc1cfb5dde1ef19a211849fb" @@ -9830,6 +10158,17 @@ unorm@^1.4.1: resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.6.0.tgz#029b289661fba714f1a9af439eb51d9b16c205af" integrity sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA== +unzipper@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.12.3.tgz#31958f5eed7368ed8f57deae547e5a673e984f87" + integrity sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA== + dependencies: + bluebird "~3.7.2" + duplexer2 "~0.1.4" + fs-extra "^11.2.0" + graceful-fs "^4.2.2" + node-int64 "^0.4.0" + update-browserslist-db@^1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" @@ -9888,10 +10227,10 @@ uuid@*: resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8" integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w== -uuid@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.0.tgz#0af883220163d264ffe0c084f6b8a89b9666966d" - integrity sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg== +uuid@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.1.tgz#8a5975b3e038902bfd169a10b5202f5ec0cf3faf" + integrity sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew== v8-compile-cache-lib@^3.0.1: version "3.0.1" @@ -9911,15 +10250,6 @@ vary@^1: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -verror@^1.10.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb" - integrity sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - vfile-location@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3" @@ -9973,16 +10303,16 @@ vite-plugin-svgr@^5.2.0: "@svgr/core" "^8.1.0" "@svgr/plugin-jsx" "^8.1.0" -vite@^8.0.14: - version "8.0.14" - resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.14.tgz#da5d8d1f63dbd106385cbe9c211acbc7a7a5b192" - integrity sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw== +vite@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.1.0.tgz#0734dc1a48faeb2bd5f5b16b66dcbfae484fec55" + integrity sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q== dependencies: lightningcss "^1.32.0" picomatch "^4.0.4" postcss "^8.5.15" - rolldown "1.0.2" - tinyglobby "^0.2.16" + rolldown "~1.1.2" + tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" @@ -10011,6 +10341,17 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== +webcrypto-core@^1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.9.2.tgz#22dd6a0fb55217b1301eab7151d01d5c68e52ccb" + integrity sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/json-schema" "^1.1.12" + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.10" + tslib "^2.8.1" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -10139,12 +10480,12 @@ which@^5.0.0: dependencies: isexe "^3.1.1" -which@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/which/-/which-6.0.0.tgz#a3a721a14cdd9b991a722e493c177eeff82ff32a" - integrity sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg== +which@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-7.0.0.tgz#cdfdd7bfc31c5af050b97bc7c02b1844731fb8b2" + integrity sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA== dependencies: - isexe "^3.1.1" + isexe "^4.0.0" word-wrap@^1.2.3, word-wrap@^1.2.5: version "1.2.5" @@ -10188,11 +10529,6 @@ ws@~8.18.3: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== -xml-naming@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/xml-naming/-/xml-naming-0.1.0.tgz#8ab7106c5b8d23caa2fabac1cadf17136379fbd8" - integrity sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw== - xmlbuilder@>=11.0.1, xmlbuilder@^15.1.1: version "15.1.1" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"