diff --git a/.github/workflows/publish-apps.yml b/.github/workflows/publish-apps.yml new file mode 100644 index 00000000..2ce2f66f --- /dev/null +++ b/.github/workflows/publish-apps.yml @@ -0,0 +1,194 @@ +name: Publish Apps + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + desktop: + name: Electron (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: + - windows-latest + - macos-latest + - ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Resolve app version + id: app-version + shell: bash + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + DISPATCH_VERSION: ${{ github.event.inputs.version }} + REF_NAME: ${{ github.ref_name }} + run: | + VERSION="${RELEASE_TAG:-${DISPATCH_VERSION:-$REF_NAME}}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Set app version + working-directory: frontend + run: npm run app:version -- "${{ steps.app-version.outputs.version }}" "${{ github.run_number }}" + + - name: Build Electron package + if: matrix.os != 'ubuntu-latest' + working-directory: frontend + run: npm run electron:dist + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + + - name: Build Linux x64 Electron packages + if: matrix.os == 'ubuntu-latest' + working-directory: frontend + run: npm run electron:dist -- --linux deb rpm --x64 + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + + - name: Build Linux arm64 Electron packages + if: matrix.os == 'ubuntu-latest' + working-directory: frontend + run: npm run electron:dist -- --linux deb rpm --arm64 + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + + - name: Rename Linux artifacts + if: matrix.os == 'ubuntu-latest' + working-directory: frontend + run: node packages/scripts/rename-linux-artifacts.cjs + + - name: Upload Electron artifacts + uses: actions/upload-artifact@v4 + with: + name: mailflow-electron-${{ matrix.os }} + if-no-files-found: error + path: | + frontend/packages/release/*.deb + frontend/packages/release/*.dmg + frontend/packages/release/*.exe + frontend/packages/release/*.rpm + + android: + name: Android + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + + - uses: android-actions/setup-android@v3 + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Resolve app version + id: app-version + shell: bash + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + DISPATCH_VERSION: ${{ github.event.inputs.version }} + REF_NAME: ${{ github.ref_name }} + run: | + VERSION="${RELEASE_TAG:-${DISPATCH_VERSION:-$REF_NAME}}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Set app version + working-directory: frontend + run: npm run app:version -- "${{ steps.app-version.outputs.version }}" "${{ github.run_number }}" + + - name: Prepare Android release signing + shell: bash + env: + MAILFLOW_ANDROID_KEYSTORE_BASE64: ${{ secrets.MAILFLOW_ANDROID_KEYSTORE_BASE64 }} + run: | + if [ -n "$MAILFLOW_ANDROID_KEYSTORE_BASE64" ]; then + mkdir -p "$RUNNER_TEMP/mailflow-signing" + echo "$MAILFLOW_ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/mailflow-signing/release.keystore" + echo "MAILFLOW_ANDROID_STORE_FILE=$RUNNER_TEMP/mailflow-signing/release.keystore" >> "$GITHUB_ENV" + fi + + - name: Build Android package + working-directory: frontend + run: npm run android:dist + env: + MAILFLOW_ANDROID_STORE_PASSWORD: ${{ secrets.MAILFLOW_ANDROID_STORE_PASSWORD }} + MAILFLOW_ANDROID_KEY_ALIAS: ${{ secrets.MAILFLOW_ANDROID_KEY_ALIAS }} + MAILFLOW_ANDROID_KEY_PASSWORD: ${{ secrets.MAILFLOW_ANDROID_KEY_PASSWORD }} + + - name: List Android release artifacts + if: always() + working-directory: frontend + run: find packages/android/app/build -maxdepth 6 -type f | sort || true + + - name: Upload Android artifact + uses: actions/upload-artifact@v4 + with: + name: mailflow-android + if-no-files-found: error + path: frontend/packages/release/*.apk + + release: + name: Attach to GitHub Release + needs: + - desktop + - android + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'release' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + + steps: + - name: Resolve app version + id: app-version + shell: bash + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + DISPATCH_VERSION: ${{ github.event.inputs.version }} + REF_NAME: ${{ github.ref_name }} + run: | + VERSION="${RELEASE_TAG:-${DISPATCH_VERSION:-$REF_NAME}}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Publish release files + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.app-version.outputs.version }} + files: | + artifacts/**/*.apk + artifacts/**/*.deb + artifacts/**/*.dmg + artifacts/**/*.exe + artifacts/**/*.rpm diff --git a/.gitignore b/.gitignore index eebffc65..4a933a1f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules/ # Build output frontend/dist/ frontend/.vite/ +frontend/packages/release/ # TLS certs (generated locally or via Let's Encrypt) certs/ diff --git a/README.md b/README.md index c057cbad..d3aca435 100644 --- a/README.md +++ b/README.md @@ -499,6 +499,67 @@ nginx (frontend container — internal only) └── backend, PostgreSQL, Redis (internal network, unchanged) ``` +## Desktop and Android apps + +MailFlow remains a self-hosted web app, but release builds also publish native wrappers for users who prefer an installed desktop or mobile application. + +- Windows, macOS, and Linux use Electron-based packages. +- Android uses a Capacitor WebView wrapper. +- On first launch, the native wrapper prompts for the MailFlow server URL, such as `https://mail.your-domain.com`, stores it locally, and connects to that server. +- Native package sources live under `frontend/packages`. + +Install release builds from the latest MailFlow release: + +- **Windows**: + - Download the latest `.exe` installer and run it. +- **macOS**: + - Download the latest `Universal.dmg` release and install the app. + - On first launch, macOS may display: + - `"MailFlow" can't be opened because Apple cannot check it for malicious software`. + - Click OK, then open: + - Settings -> Privacy & Security. + - Click Open Anyway beside: + - `"MailFlow" was blocked from use because it is not from an identified developer`. + - Click Open on the second confirmation dialog. + - MailFlow will run normally afterward, including future updates. +- **Ubuntu / Debian** + - Download the latest `.deb` release and install it: + + ```bash + sudo dpkg -i MailFlow--amd64.deb + ``` + or + ```bash + sudo dpkg -i MailFlow--arm64.deb + ``` + - If dependencies are missing: + ```bash + sudo apt-get install -f + ``` +- **Fedora / Red Hat** + - Download the latest `.rpm` release and install it: + + ```bash + sudo dnf install MailFlow--x86_64.rpm + ``` + or + ```bash + sudo dnf install MailFlow--aarch64.rpm + ``` + +- **Android** + - Download the latest Android `.apk` release and install it manually. + +Local Development Builds: + +```bash +cd frontend +npm ci +npm run electron:dist +npm run android:dist +``` +--- + ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=maathimself/mailflow&type=Date)](https://star-history.com/#maathimself/mailflow&Date) diff --git a/frontend/.dockerignore b/frontend/.dockerignore index 19b37fe6..faae08bb 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -2,3 +2,6 @@ node_modules dist .git *.local +packages/android +packages/release +packages/electron diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3546eec6..50c3f70f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "mailflow-frontend", "version": "1.0.7", "dependencies": { + "@capacitor/android": "^8.3.4", + "@capacitor/core": "^8.3.4", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@tiptap/extension-image": "^3.23.1", @@ -30,8 +32,11 @@ "zustand": "^4.5.0" }, "devDependencies": { + "@capacitor/cli": "^8.3.4", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.18", + "electron": "^42.1.0", + "electron-builder": "^26.8.1", "postcss": "^8.4.35", "tailwindcss": "^3.4.1", "vite": "^5.1.4" @@ -341,6 +346,411 @@ "node": ">=6.9.0" } }, + "node_modules/@capacitor/android": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.3.4.tgz", + "integrity": "sha512-7gJjrG3X32Am1QMLqgMztWTYMLMVNE+VZwhekNxhvYizH4mOV05vH+rC9B+f17bCkYZfyu/qXQX6hoY7kLeVZw==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.3.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.4.tgz", + "integrity": "sha512-QEmyNdiDDVNYl0Mahm7YTVA/3t2tKcy7FWYDapeKGavS6HDNHZSjyTVwQpUXQbDZrrs/PS2Wau3Aii+LIFwm/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@capacitor/cli/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@capacitor/core": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.4.tgz", + "integrity": "sha512-CqRQCkb6HXxcx/N7s+hHTN6ef2CmamFiRMITwm4qB840ph56mS42bzUgn6tKCP+RZjdDweiRHj9ytDDeN6jFag==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/get/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/@emoji-mart/data": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", @@ -776,74 +1186,290 @@ "license": "MIT", "optional": true }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", "dev": true, "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, "engines": { - "node": ">=6.0.0" + "node": ">=16.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "debug": "^4.0.0", + "tslib": "^2.0.1" }, "engines": { - "node": ">= 8" + "node": ">=16.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -1230,6 +1856,32 @@ "win32" ] }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tiptap/core": { "version": "3.23.1", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.23.1.tgz", @@ -1808,6 +2460,29 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1815,6 +2490,62 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -1835,6 +2566,23 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -1848,6 +2596,25 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1869,326 +2636,1699 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true, "license": "MIT" }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, "engines": { - "node": ">= 8" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 14" + } }, - "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "peerDependencies": { - "postcss": "^8.1.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", - "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "color-convert": "^2.0.1" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">= 8" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", "dev": true, "license": "MIT", + "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/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "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", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, "engines": { - "node": ">= 6" + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001790", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", - "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=14" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "global-agent": "^3.0.0" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">= 6" + "node": ">=6 <7 || >=8" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 6" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, "engines": { - "node": ">=4" + "node": ">= 4.0.0" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/app-builder-lib/node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "@types/node": "*" + } }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "node_modules/app-builder-lib/node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" + "engines": { + "node": ">=10.0.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=8" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "Apache-2.0" + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, - "license": "MIT" - }, - "node_modules/dompurify": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", - "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.344", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", - "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "node_modules/app-builder-lib/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, - "license": "ISC" - }, - "node_modules/emoji-mart": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", - "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", "license": "MIT", - "peer": true + "bin": { + "jiti": "lib/jiti-cli.mjs" + } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/app-builder-lib/node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "dev": true, "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, "engines": { - "node": ">= 0.4" + "node": ">=10.4.0" } }, - "node_modules/esbuild": { + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", + "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", + "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "42.1.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-42.1.0.tgz", + "integrity": "sha512-0szNwC/0dWtkvNce5j3ThiuL0TxBNrZN/BZhdOiGwbLreiD/+u3MGpkct4hA5Ycagb8MXjpEr5/oosi+FwuKRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/elementtree/node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-mart": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", + "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", + "license": "MIT", + "peer": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", @@ -2237,6 +4377,66 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-equals": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", @@ -2263,27 +4463,84 @@ "node": ">=8.6.0" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "balanced-match": "^1.0.0" } }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { - "reusify": "^1.0.4" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, "node_modules/fill-range": { @@ -2299,6 +4556,23 @@ "node": ">=8" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -2313,6 +4587,28 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2348,39 +4644,389 @@ "node": ">=6.9.0" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, - "license": "ISC", + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=10.13.0" + "node": ">= 14" } }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10.19.0" } }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, "license": "MIT", "dependencies": { - "void-elements": "3.1.0" + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, "node_modules/i18next": { @@ -2411,6 +5057,88 @@ } } }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2440,6 +5168,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2450,6 +5194,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2473,6 +5227,57 @@ "node": ">=0.12.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -2489,6 +5294,19 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2502,6 +5320,28 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2515,6 +5355,46 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2541,6 +5421,13 @@ "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", "license": "MIT" }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2553,6 +5440,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2563,6 +5460,30 @@ "yallist": "^3.0.2" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2580,11 +5501,120 @@ "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" }, - "engines": { - "node": ">=8.6" + "bin": { + "mkdirp": "bin/cmd.js" } }, "node_modules/ms": { @@ -2625,6 +5655,163 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/node-abi": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", + "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", + "dev": true, + "license": "MIT", + "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", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", @@ -2632,6 +5819,22 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2642,6 +5845,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2662,12 +5878,104 @@ "node": ">= 6" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/orderedmap": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", "license": "MIT" }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2675,6 +5983,55 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2715,6 +6072,21 @@ "node": ">= 6" } }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, "node_modules/postcss": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", @@ -2878,6 +6250,106 @@ "dev": true, "license": "MIT" }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/prosemirror-changeset": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", @@ -3007,6 +6479,27 @@ "prosemirror-transform": "^1.1.0" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3028,6 +6521,19 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -3122,6 +6628,19 @@ "react-dom": ">=16.8" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -3132,6 +6651,21 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3145,6 +6679,34 @@ "node": ">=8.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -3167,6 +6729,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -3178,6 +6770,63 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", @@ -3249,37 +6898,290 @@ } ], "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" } }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0" + "safe-buffer": "~5.2.0" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/sucrase": { @@ -3305,6 +7207,32 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -3356,6 +7284,89 @@ "node": ">=14.0.0" } }, + "node_modules/tar": { + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -3379,6 +7390,36 @@ "node": ">=0.8" } }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -3427,6 +7468,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3440,6 +7501,26 @@ "node": ">=8.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -3447,6 +7528,64 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -3478,6 +7617,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -3487,6 +7636,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3494,6 +7650,22 @@ "dev": true, "license": "MIT" }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -3569,6 +7741,91 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -3576,6 +7833,59 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", diff --git a/frontend/package.json b/frontend/package.json index 73a65dca..5ed9d122 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,13 +1,140 @@ { "name": "mailflow-frontend", + "productName": "MailFlow", "version": "1.4.0", + "description": "A self-hosted, unified webmail client.", + "homepage": "https://mailflow.sh", + "author": { + "name": "maathimself", + "email": "matthias@mailflow.sh" + }, "type": "module", + "main": "packages/electron/main.cjs", + "repository": { + "type": "git", + "url": "git+https://github.com/maathimself/mailflow.git" + }, + "keywords": [ + "Email", + "Webmail", + "Email Client", + "Unified Inbox", + "GUI" + ], + "license": "GPL-3.0", + "bugs": { + "url": "https://github.com/maathimself/mailflow/issues" + }, "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "electron:dev": "electron .", + "preelectron:dist": "node packages/scripts/prepare-electron-icons.cjs && node packages/scripts/prepare-linux-metainfo.cjs", + "electron:dist": "electron-builder --publish never", + "app:version": "node packages/scripts/set-app-version.cjs", + "native:prepare": "npm run build && node packages/scripts/prepare-native-shell.cjs", + "android:sync": "npm run native:prepare && node packages/scripts/sync-android.cjs", + "android:dist": "npm run android:sync && node packages/scripts/build-android.cjs && node packages/scripts/collect-android-artifacts.cjs" + }, + "build": { + "appId": "sh.mailflow.app", + "productName": "MailFlow", + "executableName": "MailFlow", + "directories": { + "output": "packages/release", + "buildResources": "packages/electron/icons" + }, + "icon": "icon", + "protocols": [ + { + "name": "MailFlow email link", + "schemes": [ + "mailto" + ] + } + ], + "files": [ + "packages/electron/**/*", + "packages/native-shell/**/*", + "package.json" + ], + "win": { + "artifactName": "${productName}-${version}-Setup.${ext}", + "icon": "icon.ico", + "target": [ + "nsis" + ] + }, + "nsis": { + "include": "packages/electron/installer.nsh" + }, + "mac": { + "artifactName": "${productName}-${version}-Universal.${ext}", + "target": [ + { + "target": "dmg", + "arch": [ + "universal" + ] + } + ] + }, + "linux": { + "artifactName": "${productName}-${version}.${ext}", + "target": [ + { + "target": "deb", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "rpm", + "arch": [ + "x64", + "arm64" + ] + } + ], + "category": "Network", + "desktop": { + "entry": { + "Name": "MailFlow", + "StartupWMClass": "MailFlow" + } + } + }, + "deb": { + "packageName": "mailflow", + "artifactName": "${productName}-${version}-${arch}.${ext}", + "maintainer": "maathimself ", + "vendor": "maathimself", + "packageCategory": "net", + "synopsis": "A self-hosted, unified webmail client.", + "fpm": [ + "--replaces=mailflow-frontend", + "--conflicts=mailflow-frontend", + "packages/electron/package-type/deb=/opt/MailFlow/resources/package-type", + "packages/electron/metainfo/sh.mailflow.app.metainfo.xml=/usr/share/metainfo/sh.mailflow.app.metainfo.xml" + ] + }, + "rpm": { + "packageName": "mailflow", + "artifactName": "${productName}-${version}-${arch}.${ext}", + "depends": [ + "libunity" + ], + "fpm": [ + "--replaces=mailflow-frontend", + "packages/electron/package-type/rpm=/opt/MailFlow/resources/package-type" + ] + } }, "dependencies": { + "@capacitor/android": "^8.3.4", + "@capacitor/core": "^8.3.4", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@tiptap/extension-image": "^3.23.1", @@ -30,8 +157,11 @@ "zustand": "^4.5.0" }, "devDependencies": { + "@capacitor/cli": "^8.3.4", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.18", + "electron": "^42.1.0", + "electron-builder": "^26.8.1", "postcss": "^8.4.35", "tailwindcss": "^3.4.1", "vite": "^5.1.4" diff --git a/frontend/packages/android/.gitignore b/frontend/packages/android/.gitignore new file mode 100644 index 00000000..48354a3d --- /dev/null +++ b/frontend/packages/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/frontend/packages/android/app/.gitignore b/frontend/packages/android/app/.gitignore new file mode 100644 index 00000000..043df802 --- /dev/null +++ b/frontend/packages/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/frontend/packages/android/app/build.gradle b/frontend/packages/android/app/build.gradle new file mode 100644 index 00000000..3c28da23 --- /dev/null +++ b/frontend/packages/android/app/build.gradle @@ -0,0 +1,74 @@ +apply plugin: 'com.android.application' + +android { + namespace = "sh.mailflow.app" + compileSdk = rootProject.ext.compileSdkVersion + + def releaseStoreFile = System.getenv("MAILFLOW_ANDROID_STORE_FILE") + def releaseStorePassword = System.getenv("MAILFLOW_ANDROID_STORE_PASSWORD") + def releaseKeyAlias = System.getenv("MAILFLOW_ANDROID_KEY_ALIAS") + def releaseKeyPassword = System.getenv("MAILFLOW_ANDROID_KEY_PASSWORD") + def hasReleaseSigning = releaseStoreFile && releaseStorePassword && releaseKeyAlias && releaseKeyPassword + + defaultConfig { + applicationId "sh.mailflow.app" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + signingConfigs { + if (hasReleaseSigning) { + release { + storeFile file(releaseStoreFile) + storePassword releaseStorePassword + keyAlias releaseKeyAlias + keyPassword releaseKeyPassword + } + } + } + buildTypes { + release { + signingConfig hasReleaseSigning ? signingConfigs.release : signingConfigs.debug + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core:$androidxCoreVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation "androidx.work:work-runtime:$androidxWorkVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/frontend/packages/android/app/capacitor.build.gradle b/frontend/packages/android/app/capacitor.build.gradle new file mode 100644 index 00000000..bbfb44fa --- /dev/null +++ b/frontend/packages/android/app/capacitor.build.gradle @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/frontend/packages/android/app/proguard-rules.pro b/frontend/packages/android/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/frontend/packages/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/frontend/packages/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/frontend/packages/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 00000000..f2c2217e --- /dev/null +++ b/frontend/packages/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/frontend/packages/android/app/src/main/AndroidManifest.xml b/frontend/packages/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0a0b92c5 --- /dev/null +++ b/frontend/packages/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowBackgroundSync.java b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowBackgroundSync.java new file mode 100644 index 00000000..517c4a15 --- /dev/null +++ b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowBackgroundSync.java @@ -0,0 +1,43 @@ +package sh.mailflow.app; + +import android.content.Context; +import androidx.work.Constraints; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import java.util.concurrent.TimeUnit; + +public final class MailFlowBackgroundSync { + private static final String PERIODIC_WORK = "mailflow-background-mail-check"; + private static final String ONE_TIME_WORK = "mailflow-background-mail-check-once"; + + private MailFlowBackgroundSync() {} + + public static void schedule(Context context) { + if (context == null || MailFlowNativePlugin.getSavedHost(context) == null) return; + + Constraints constraints = new Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build(); + + PeriodicWorkRequest periodicRequest = new PeriodicWorkRequest.Builder( + MailFlowBackgroundWorker.class, + 15, + TimeUnit.MINUTES + ) + .setConstraints(constraints) + .build(); + + OneTimeWorkRequest oneTimeRequest = new OneTimeWorkRequest.Builder(MailFlowBackgroundWorker.class) + .setInitialDelay(45, TimeUnit.SECONDS) + .setConstraints(constraints) + .build(); + + WorkManager workManager = WorkManager.getInstance(context.getApplicationContext()); + workManager.enqueueUniquePeriodicWork(PERIODIC_WORK, ExistingPeriodicWorkPolicy.UPDATE, periodicRequest); + workManager.enqueueUniqueWork(ONE_TIME_WORK, ExistingWorkPolicy.REPLACE, oneTimeRequest); + } +} diff --git a/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowBackgroundWorker.java b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowBackgroundWorker.java new file mode 100644 index 00000000..d134a54b --- /dev/null +++ b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowBackgroundWorker.java @@ -0,0 +1,115 @@ +package sh.mailflow.app; + +import android.content.Context; +import android.content.SharedPreferences; +import android.webkit.CookieManager; +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import org.json.JSONArray; +import org.json.JSONObject; + +public class MailFlowBackgroundWorker extends Worker { + private static final String PREFS_NAME = "mailflow-background-sync"; + private static final String PREF_LAST_UNREAD_TOTAL = "lastUnreadTotal"; + + public MailFlowBackgroundWorker(@NonNull Context context, @NonNull WorkerParameters params) { + super(context, params); + } + + static void updateUnreadBaseline(Context context, int unreadTotal) { + if (context == null) return; + context.getApplicationContext() + .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putInt(PREF_LAST_UNREAD_TOTAL, Math.max(0, unreadTotal)) + .apply(); + } + + @NonNull + @Override + public Result doWork() { + Context context = getApplicationContext(); + String host = MailFlowNativePlugin.getSavedHost(context); + if (host == null || host.isEmpty()) return Result.success(); + + String cookie = CookieManager.getInstance().getCookie(host); + if (cookie == null || cookie.trim().isEmpty()) return Result.success(); + + try { + JSONObject counts = getJson(host + "/api/mail/unread-counts", cookie); + int unreadTotal = counts.optInt("total", 0); + + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + int lastUnreadTotal = prefs.getInt(PREF_LAST_UNREAD_TOTAL, -1); + + if (lastUnreadTotal >= 0 && unreadTotal > lastUnreadTotal) { + JSONObject latest = getLatestUnreadMessage(host, cookie); + int delta = unreadTotal - lastUnreadTotal; + String title = latest.optString("from_name", latest.optString("from_email", "New mail")); + String body = latest.optString("subject", delta == 1 ? "You have new mail." : delta + " new messages"); + String messageId = latest.optString("id", null); + String accountId = latest.optString("account_id", null); + String folder = latest.optString("folder", "INBOX"); + + MailFlowNativePlugin.postNewMailNotification( + context, + title == null || title.isEmpty() ? "New mail" : title, + body == null || body.isEmpty() ? "You have new mail." : body, + messageId, + accountId, + folder, + com.getcapacitor.JSObject.fromJSONObject(latest) + ); + } + + updateUnreadBaseline(context, unreadTotal); + return Result.success(); + } catch (Exception ignored) { + return Result.retry(); + } + } + + private static JSONObject getLatestUnreadMessage(String host, String cookie) throws Exception { + JSONObject result = getJson(host + "/api/mail/messages?folder=INBOX&limit=1&unreadOnly=true", cookie); + JSONArray messages = result.optJSONArray("messages"); + if (messages == null || messages.length() == 0) return new JSONObject(); + return messages.optJSONObject(0) == null ? new JSONObject() : messages.optJSONObject(0); + } + + private static JSONObject getJson(String url, String cookie) throws Exception { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(15000); + connection.setReadTimeout(15000); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Cookie", cookie); + + int status = connection.getResponseCode(); + InputStream stream = status >= 200 && status < 300 + ? connection.getInputStream() + : connection.getErrorStream(); + String body = readAll(stream); + connection.disconnect(); + + if (status < 200 || status >= 300) { + throw new IllegalStateException("MailFlow background check failed: HTTP " + status); + } + + return new JSONObject(body); + } + + private static String readAll(InputStream stream) throws Exception { + if (stream == null) return "{}"; + StringBuilder result = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String line; + while ((line = reader.readLine()) != null) result.append(line); + return result.toString(); + } +} diff --git a/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowNativePlugin.java b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowNativePlugin.java new file mode 100644 index 00000000..0cfa1031 --- /dev/null +++ b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowNativePlugin.java @@ -0,0 +1,1183 @@ +package sh.mailflow.app; + +import android.Manifest; +import android.app.AlertDialog; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.Settings; +import android.util.Log; +import android.webkit.JavascriptInterface; +import android.webkit.WebView; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import com.getcapacitor.JSArray; +import com.getcapacitor.JSObject; +import com.getcapacitor.PermissionState; +import com.getcapacitor.annotation.Permission; +import com.getcapacitor.annotation.PermissionCallback; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URL; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.json.JSONException; +import org.json.JSONObject; + +@CapacitorPlugin( + name = "MailFlowNative", + permissions = { + @Permission(alias = "notifications", strings = { Manifest.permission.POST_NOTIFICATIONS }) + } +) +public class MailFlowNativePlugin extends Plugin { + static final String ACTION_OPEN_MESSAGE = "sh.mailflow.app.OPEN_MESSAGE"; + static final String ACTION_REPLY_MESSAGE = "sh.mailflow.app.REPLY_MESSAGE"; + static final String ACTION_DELETE_MESSAGE = "sh.mailflow.app.DELETE_MESSAGE"; + static final String ACTION_STAR_MESSAGE = "sh.mailflow.app.STAR_MESSAGE"; + static final String ACTION_COMPOSE = "sh.mailflow.app.COMPOSE"; + static final String ACTION_SYNC = "sh.mailflow.app.SYNC"; + static final String ACTION_INSTALL_UPDATE = "sh.mailflow.app.INSTALL_UPDATE"; + private static final String TAG = "MailFlowUpdater"; + private static final String CHANNEL_NEW_MAIL = "mailflow_new_mail"; + private static final String CHANNEL_UPDATES = "mailflow_updates"; + private static final String PREFS_NAME = "mailflow-native"; + private static final String PREF_HOST = "host"; + private static final String PREF_UPDATE_APK_PATH = "update_apk_path"; + private static final String PREF_UPDATE_VERSION = "update_version"; + private static final String PREF_UPDATE_RELEASE_NAME = "update_release_name"; + private static final String SETUP_URL = "file:///android_asset/public/index.html"; + private static final String UPDATE_RELEASE_URL = "https://api.github.com/repos/maathimself/mailflow/releases/latest"; + private static final String UPDATE_ERROR_MESSAGE = "Could not check for MailFlow updates. Please visit the website instead."; + private static final Pattern VERSION_PATTERN = Pattern.compile("\\d+(?:\\.\\d+){0,2}"); + + private static final List pendingActions = new ArrayList<>(); + private static MailFlowNativePlugin instance; + private ReleaseInfo updateInfo = null; + private File downloadedUpdate = null; + private boolean updateCheckStarted = false; + private boolean installPendingPermission = false; + + @Override + public void load() { + instance = this; + createNotificationChannel(getContext()); + restoreDownloadedUpdateState(); + checkForUpdatesInBackground(false, null); + } + + @PluginMethod + public void getHost(PluginCall call) { + JSObject result = new JSObject(); + result.put("host", getSavedHost(getContext())); + call.resolve(result); + } + + @PluginMethod + public void saveHost(PluginCall call) { + String host = call.getString("host", ""); + String normalizedHost = normalizeHost(host); + + if (normalizedHost == null) { + call.reject("Host must start with https:// or http://"); + return; + } + + getPrefs(getContext()).edit().putString(PREF_HOST, normalizedHost).apply(); + MailFlowBackgroundSync.schedule(getContext()); + + JSObject result = new JSObject(); + result.put("host", normalizedHost); + call.resolve(result); + } + + @PluginMethod + public void resetHost(PluginCall call) { + getPrefs(getContext()).edit().remove(PREF_HOST).apply(); + getActivity().runOnUiThread(() -> getBridge().getWebView().loadUrl(SETUP_URL)); + call.resolve(); + } + + @PluginMethod + public void setUnreadCount(PluginCall call) { + Integer count = call.getInt("count"); + if (count != null) { + MailFlowBackgroundWorker.updateUnreadBaseline(getContext(), count); + MailFlowBackgroundSync.schedule(getContext()); + } + call.resolve(); + } + + @PluginMethod + public void checkForUpdates(PluginCall call) { + checkForUpdatesInBackground(Boolean.TRUE.equals(call.getBoolean("verbose")), call); + } + + private void checkForUpdatesInBackground(boolean verbose, PluginCall call) { + if (!verbose && updateCheckStarted) { + if (call != null) { + JSObject result = new JSObject(); + result.put("updateAvailable", false); + result.put("skipped", true); + call.resolve(result); + } + return; + } + + updateCheckStarted = true; + if (verbose) { + sendUpdateStatus(updateStatus("checking")); + } + + new Thread(() -> { + try { + Log.i(TAG, "Checking for updates from " + UPDATE_RELEASE_URL); + ReleaseInfo release = fetchLatestRelease(); + Log.i(TAG, "Latest release " + release.version + ", installed " + getInstalledVersion() + ", APK " + release.downloadUrl); + if (!isNewerVersion(release.version, getInstalledVersion())) { + clearDownloadedUpdateState(); + if (verbose) { + sendUpdateStatus(updateStatus("up-to-date")); + } + + if (call != null) { + JSObject result = new JSObject(); + result.put("updateAvailable", false); + call.resolve(result); + } + return; + } + + if (release.downloadUrl == null) { + sendUpdateError("A MailFlow update is available, but no Android APK was found."); + if (call != null) { + JSObject result = new JSObject(); + result.put("updateAvailable", true); + result.put("downloadAvailable", false); + call.resolve(result); + } + return; + } + + updateInfo = release; + downloadedUpdate = null; + sendUpdateStatus(updateStatus("available", release.toStatusData())); + + if (call != null) { + JSObject result = new JSObject(); + result.put("updateAvailable", true); + result.put("downloadAvailable", true); + call.resolve(result); + } + + downloadUpdate(release); + } catch (Exception error) { + Log.e(TAG, "Update check failed", error); + sendUpdateError(UPDATE_ERROR_MESSAGE); + if (call != null) { + JSObject result = new JSObject(); + result.put("updateAvailable", false); + result.put("error", error.getMessage()); + call.resolve(result); + } + } + }).start(); + } + + @PluginMethod + public void installDownloadedUpdate(PluginCall call) { + JSObject result = showUpdateReadyDialog(); + call.resolve(result); + } + + @PluginMethod + public void openDownloadedUpdate(PluginCall call) { + JSObject result = showUpdateReadyDialog(); + call.resolve(result); + } + + @PluginMethod + public void requestNotificationPermission(PluginCall call) { + if (hasNotificationPermission()) { + call.resolve(notificationPermissionResult("granted")); + return; + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + call.resolve(notificationPermissionResult("denied")); + return; + } + + requestPermissionForAlias("notifications", call, "notificationPermissionCallback"); + } + + @PluginMethod + public void checkNotificationPermission(PluginCall call) { + call.resolve(notificationPermissionResult(getNotificationPermissionState())); + } + + @PluginMethod + public void openNotificationSettings(PluginCall call) { + Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + try { + getContext().startActivity(intent); + } catch (ActivityNotFoundException err) { + Intent fallbackIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.parse("package:" + getContext().getPackageName())); + fallbackIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getContext().startActivity(fallbackIntent); + } + + call.resolve(); + } + + @PluginMethod + public void showNewMail(PluginCall call) { + String title = call.getString("title", "New mail"); + String body = call.getString("body", "You have new mail."); + String messageId = call.getString("messageId", null); + String accountId = call.getString("accountId", null); + String folder = call.getString("folder", "INBOX"); + JSObject message = call.getObject("message"); + + postNewMailNotification(getContext(), title, body, messageId, accountId, folder, message); + call.resolve(); + } + + static void postNewMailNotification(Context context, String title, String body, String messageId, String accountId, String folder, JSObject message) { + if (!hasNotificationPermission(context)) return; + + Intent intent = new Intent(context, MainActivity.class); + intent.setAction(ACTION_OPEN_MESSAGE); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + putExtra(intent, "messageId", messageId); + putExtra(intent, "accountId", accountId); + putExtra(intent, "folder", folder); + if (message != null) putExtra(intent, "message", message.toString()); + + int notificationId = Math.abs(UUID.randomUUID().hashCode()); + PendingIntent pendingIntent = PendingIntent.getActivity( + context, + notificationId, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + PendingIntent replyPendingIntent = messageActionPendingIntent( + context, + notificationId, + ACTION_REPLY_MESSAGE, + messageId, + accountId, + folder, + message + ); + PendingIntent deletePendingIntent = messageActionPendingIntent( + context, + notificationId, + ACTION_DELETE_MESSAGE, + messageId, + accountId, + folder, + message + ); + PendingIntent starPendingIntent = messageActionPendingIntent( + context, + notificationId, + ACTION_STAR_MESSAGE, + messageId, + accountId, + folder, + message + ); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_NEW_MAIL) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(title) + .setContentText(body) + .setStyle(new NotificationCompat.BigTextStyle().bigText(body)) + .setContentIntent(pendingIntent) + .addAction(R.mipmap.ic_launcher, "Reply", replyPendingIntent) + .addAction(R.mipmap.ic_launcher, "Delete", deletePendingIntent) + .addAction(R.mipmap.ic_launcher, "Star", starPendingIntent) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT); + + NotificationManagerCompat.from(context).notify(notificationId, builder.build()); + } + + private static PendingIntent messageActionPendingIntent(Context context, int notificationId, String action, String messageId, String accountId, String folder, JSObject message) { + boolean backgroundAction = ACTION_DELETE_MESSAGE.equals(action) || ACTION_STAR_MESSAGE.equals(action); + Intent intent = new Intent( + context, + backgroundAction ? MailFlowNotificationActionReceiver.class : MainActivity.class + ); + intent.setAction(action); + if (!backgroundAction) { + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + } + intent.putExtra("notificationId", notificationId); + putExtra(intent, "messageId", messageId); + putExtra(intent, "accountId", accountId); + putExtra(intent, "folder", folder); + if (message != null) putExtra(intent, "message", message.toString()); + + int requestCode = Math.abs((action + ":" + notificationId).hashCode()); + int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; + return backgroundAction + ? PendingIntent.getBroadcast(context, requestCode, intent, flags) + : PendingIntent.getActivity(context, requestCode, intent, flags); + } + + @PluginMethod + public void getPendingActions(PluginCall call) { + JSObject result = new JSObject(); + synchronized (pendingActions) { + result.put("actions", new JSArray(new ArrayList<>(pendingActions))); + } + call.resolve(result); + } + + @PluginMethod + public void ackAction(PluginCall call) { + String id = call.getString("id", null); + if (id != null) { + synchronized (pendingActions) { + pendingActions.removeIf((action) -> id.equals(action.getString("id"))); + } + } + call.resolve(); + } + + static String getSavedHost(Context context) { + return getPrefs(context).getString(PREF_HOST, null); + } + + static void injectPendingActions(WebView webView, Context context) { + if (webView == null || context == null || !isConfiguredHost(context, webView.getUrl())) return; + + injectCapacitorCompat(webView); + + List actions; + synchronized (pendingActions) { + if (pendingActions.isEmpty()) return; + actions = new ArrayList<>(pendingActions); + pendingActions.clear(); + } + + String actionJson = new JSArray(actions).toString(); + String script = "(function(actions){" + + "window.__mailflowPendingNativeActions=(window.__mailflowPendingNativeActions||[]).concat(actions);" + + "var delivered=false;" + + "var deliver=function(force){" + + "if(delivered)return true;" + + "if(!force&&window.__mailflowNativeBridgeReady!==true)return false;" + + "delivered=true;" + + "actions.forEach(function(payload){" + + "window.dispatchEvent(new CustomEvent('mailflow:native-action',{detail:payload}));" + + "window.postMessage({type:'mailflow:native-action',payload:payload},'*');" + + "});" + + "window.dispatchEvent(new CustomEvent('mailflow:native-actions-ready'));" + + "window.postMessage({type:'mailflow:native-actions-ready'},'*');" + + "return true;" + + "};" + + "if(!deliver(false)){" + + "var attempts=0;" + + "var timer=window.setInterval(function(){attempts+=1;if(deliver(false)||attempts>=100){if(!delivered)deliver(true);window.clearInterval(timer);}},100);" + + "}" + + "})( " + actionJson + " );"; + + webView.post(() -> webView.evaluateJavascript(script, null)); + } + + static void injectCapacitorCompat(WebView webView) { + if (webView == null) return; + + String script = "(function(){try{" + + "window.Capacitor=window.Capacitor||{};" + + "if(typeof window.Capacitor.triggerEvent!=='function'){" + + "window.Capacitor.triggerEvent=function(eventName,target,eventData){" + + "var receiver=target==='document'?document:window;" + + "var event;" + + "try{event=new CustomEvent(eventName,{detail:eventData});}" + + "catch(e){event=document.createEvent('CustomEvent');event.initCustomEvent(eventName,false,false,eventData);}" + + "receiver.dispatchEvent(event);" + + "return true;" + + "};" + + "}" + + "var androidNotifications=window.MailFlowAndroid;" + + "var plugin=function(){return window.Capacitor&&window.Capacitor.Plugins&&window.Capacitor.Plugins.MailFlowNative;};" + + "var call=function(method,args,fallback){var p=plugin();if(!p||typeof p[method]!=='function')return Promise.resolve(fallback||null);return p[method](args||{}).catch(function(){return fallback||null;});};" + + "window.mailflowNative=window.mailflowNative||{};" + + "window.mailflowNative.platform='android';" + + "window.mailflowNative.updates=window.mailflowNative.updates||{};" + + "window.mailflowNative.updates.check=function(verbose){return call('checkForUpdates',{verbose:!!verbose});};" + + "window.mailflowNative.updates.installDownloaded=function(){if(androidNotifications&&typeof androidNotifications.installDownloadedUpdate==='function'){try{return Promise.resolve(JSON.parse(androidNotifications.installDownloadedUpdate()||'{}'));}catch(e){return Promise.resolve({installed:false,reason:'unavailable'});}}return call('installDownloadedUpdate',{}, {installed:false,reason:'unavailable'});};" + + "window.mailflowNative.updates.installAuto=window.mailflowNative.updates.installDownloaded;" + + "window.mailflowNative.updates.openDownload=function(){return call('openDownloadedUpdate',{});};" + + "window.mailflowNative.updates.onStatus=function(callback){if(typeof callback!=='function')return function(){};var handler=function(event){callback(event.detail);};window.addEventListener('mailflow:update-status',handler);return function(){window.removeEventListener('mailflow:update-status',handler);};};" + + "window.mailflowNative.notifications=window.mailflowNative.notifications||{};" + + "window.mailflowNative.notifications.showNewMail=function(notification){if(androidNotifications&&typeof androidNotifications.showNewMail==='function'){androidNotifications.showNewMail(JSON.stringify(notification||{}));return Promise.resolve(null);}return call('showNewMail',notification||{});};" + + "window.mailflowNative.notifications.checkPermission=function(){return call('checkNotificationPermission',{},{}).then(function(result){return result&&result.permission||'default';});};" + + "window.mailflowNative.notifications.requestPermission=function(){return call('requestNotificationPermission',{},{}).then(function(result){return result&&result.permission||'default';});};" + + "window.mailflowNative.notifications.openSettings=function(){return call('openNotificationSettings',{});};" + + "}catch(e){}})();"; + + webView.post(() -> webView.evaluateJavascript(script, null)); + } + + static void sendOpenMessageAction(Intent intent) { + JSObject action = newAction("open-message"); + copyStringExtra(intent, action, "messageId"); + copyStringExtra(intent, action, "accountId"); + copyStringExtra(intent, action, "folder"); + + String messageJson = intent.getStringExtra("message"); + if (messageJson != null) { + try { + action.put("message", new JSObject(messageJson)); + } catch (JSONException ignored) {} + } + + dispatchAction(action); + } + + static void sendReplyMessageAction(Intent intent) { + sendMessageNotificationAction(intent, "reply-message"); + } + + static void sendDeleteMessageAction(Intent intent) { + sendMessageNotificationAction(intent, "delete-message"); + } + + static void sendStarMessageAction(Intent intent) { + sendMessageNotificationAction(intent, "star-message"); + } + + private static void sendMessageNotificationAction(Intent intent, String actionName) { + int notificationId = intent.getIntExtra("notificationId", -1); + if (notificationId != -1 && instance != null) { + NotificationManagerCompat.from(instance.getContext()).cancel(notificationId); + } + + JSObject action = newAction(actionName); + copyStringExtra(intent, action, "messageId"); + copyStringExtra(intent, action, "accountId"); + copyStringExtra(intent, action, "folder"); + + String messageJson = intent.getStringExtra("message"); + if (messageJson != null) { + try { + action.put("message", new JSObject(messageJson)); + } catch (JSONException ignored) {} + } + + dispatchAction(action); + } + + static void sendMailtoAction(Uri uri) { + JSObject composeData = parseMailto(uri); + if (composeData == null) return; + + JSObject action = newAction("new-mail"); + action.put("composeData", composeData); + action.put("source", "mailto"); + dispatchAction(action); + } + + static void sendComposeAction() { + JSObject action = newAction("new-mail"); + action.put("composeData", new JSObject()); + action.put("source", "shortcut"); + dispatchAction(action); + } + + static void sendSyncAction() { + JSObject action = newAction("sync"); + action.put("source", "shortcut"); + dispatchAction(action); + } + + static void installDownloadedUpdateFromIntent() { + if (instance != null) { + instance.showUpdateReadyDialog(); + } + } + + static void resumePendingUpdateInstall() { + if (instance != null) { + instance.continuePendingUpdateInstall(); + } + } + + private static void dispatchAction(JSObject action) { + synchronized (pendingActions) { + pendingActions.add(action); + } + + if (instance != null) { + instance.injectPendingActionsToWebView(); + } + } + + private void injectPendingActionsToWebView() { + if (getBridge() == null) return; + injectPendingActions(getBridge().getWebView(), getContext()); + } + + private static JSObject newAction(String actionName) { + JSObject action = new JSObject(); + action.put("id", UUID.randomUUID().toString()); + action.put("action", actionName); + return action; + } + + private static JSObject parseMailto(Uri uri) { + if (uri == null || !"mailto".equalsIgnoreCase(uri.getScheme())) return null; + + String schemeSpecificPart = uri.getEncodedSchemeSpecificPart(); + String[] parts = (schemeSpecificPart == null ? "" : schemeSpecificPart).split("\\?", 2); + String addressPart = parts.length > 0 ? parts[0] : ""; + String queryPart = parts.length > 1 ? parts[1] : ""; + + JSObject composeData = new JSObject(); + composeData.put("to", new JSArray(unique(splitAddresses(decodePath(addressPart))))); + composeData.put("cc", new JSArray()); + composeData.put("bcc", new JSArray()); + composeData.put("subject", ""); + composeData.put("body", ""); + + for (String pair : queryPart.split("&")) { + if (pair.isEmpty()) continue; + + String[] queryParts = pair.split("=", 2); + String normalizedName = decodeQuery(queryParts[0]).toLowerCase(); + String value = queryParts.length > 1 ? decodeQuery(queryParts[1]) : ""; + + if ("to".equals(normalizedName)) { + composeData.put("to", new JSArray(unique(merge(composeData.optJSONArray("to"), splitAddresses(value))))); + } else if ("cc".equals(normalizedName)) { + composeData.put("cc", new JSArray(unique(splitAddresses(value)))); + } else if ("bcc".equals(normalizedName)) { + composeData.put("bcc", new JSArray(unique(splitAddresses(value)))); + } else if ("subject".equals(normalizedName)) { + composeData.put("subject", value); + } else if ("body".equals(normalizedName)) { + composeData.put("body", value); + } + } + + return composeData; + } + + private static List splitAddresses(String value) { + List addresses = new ArrayList<>(); + if (value == null) return addresses; + for (String item : value.split(",")) { + String address = item.trim(); + if (!address.isEmpty()) addresses.add(address); + } + return addresses; + } + + private static List merge(org.json.JSONArray current, List next) { + List merged = new ArrayList<>(); + if (current != null) { + for (int i = 0; i < current.length(); i++) { + String value = current.optString(i, ""); + if (!value.isEmpty()) merged.add(value); + } + } + merged.addAll(next); + return merged; + } + + private static List unique(List values) { + Set set = new LinkedHashSet<>(values); + return new ArrayList<>(set); + } + + private static String decodePath(String value) { + return decodeQuery((value == null ? "" : value).replace("+", "%2B")); + } + + private static String decodeQuery(String value) { + try { + return URLDecoder.decode(value == null ? "" : value, "UTF-8"); + } catch (Exception ignored) { + return value == null ? "" : value; + } + } + + private static String normalizeHost(String host) { + try { + URI uri = new URI(host.trim()); + String scheme = uri.getScheme(); + if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) return null; + if (uri.getHost() == null) return null; + + return new URI(scheme.toLowerCase(), null, uri.getHost(), uri.getPort(), null, null, null).toString(); + } catch (Exception ignored) { + return null; + } + } + + private static SharedPreferences getPrefs(Context context) { + return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + private ReleaseInfo fetchLatestRelease() throws Exception { + JSONObject release = requestJson(UPDATE_RELEASE_URL); + org.json.JSONArray assets = release.optJSONArray("assets"); + JSONObject apkAsset = null; + + if (assets != null) { + for (int i = 0; i < assets.length(); i++) { + JSONObject asset = assets.optJSONObject(i); + if (asset == null) continue; + + String name = asset.optString("name", ""); + String downloadUrl = asset.optString("browser_download_url", ""); + if (name.toLowerCase().endsWith(".apk") && !downloadUrl.isEmpty()) { + apkAsset = asset; + break; + } + } + } + + ReleaseInfo info = new ReleaseInfo(); + info.version = release.optString("tag_name", release.optString("name", "")); + info.releaseName = release.optString("name", info.version); + info.releaseNotes = release.optString("body", ""); + info.releaseDate = release.optString("published_at", ""); + + if (apkAsset != null) { + info.assetName = apkAsset.optString("name", "MailFlow.apk"); + info.downloadUrl = apkAsset.optString("browser_download_url", null); + } + + return info; + } + + private JSONObject requestJson(String url) throws Exception { + HttpURLConnection connection = openConnection(url); + int status = connection.getResponseCode(); + if (status >= 300 && status < 400) { + String location = connection.getHeaderField("Location"); + connection.disconnect(); + if (location != null) return requestJson(location); + } + + if (status < 200 || status >= 300) { + connection.disconnect(); + throw new Exception("Update request failed with status " + status); + } + + try (InputStream stream = connection.getInputStream()) { + return new JSONObject(readStream(stream)); + } finally { + connection.disconnect(); + } + } + + private void downloadUpdate(ReleaseInfo release) { + sendUpdateStatus(updateStatus("downloading")); + + new Thread(() -> { + try { + Log.i(TAG, "Downloading update APK from " + release.downloadUrl); + File directory = getContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); + if (directory == null) directory = getContext().getCacheDir(); + if (!directory.exists()) directory.mkdirs(); + + File output = uniqueFile(directory, sanitizeApkName(release.assetName)); + HttpURLConnection connection = openConnection(release.downloadUrl); + int status = connection.getResponseCode(); + if (status >= 300 && status < 400 && connection.getHeaderField("Location") != null) { + release.downloadUrl = connection.getHeaderField("Location"); + connection.disconnect(); + downloadUpdate(release); + return; + } + if (status < 200 || status >= 300) { + connection.disconnect(); + throw new Exception("APK download failed with status " + status); + } + + try ( + InputStream input = new BufferedInputStream(connection.getInputStream()); + FileOutputStream outputStream = new FileOutputStream(output) + ) { + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) != -1) { + outputStream.write(buffer, 0, read); + } + } finally { + connection.disconnect(); + } + + downloadedUpdate = output; + persistDownloadedUpdateState(release, output); + Log.i(TAG, "Downloaded update APK to " + output.getAbsolutePath()); + sendUpdateStatus(updateStatus("downloaded", release.toStatusData(output.getAbsolutePath()))); + postUpdateReadyNotification(release); + } catch (Exception error) { + Log.e(TAG, "Update download failed", error); + sendUpdateError("The MailFlow update could not be downloaded."); + } + }).start(); + } + + private HttpURLConnection openConnection(String url) throws Exception { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setConnectTimeout(15000); + connection.setReadTimeout(30000); + connection.setRequestProperty("Accept", "application/vnd.github+json"); + connection.setRequestProperty("User-Agent", "MailFlow/" + getInstalledVersion()); + return connection; + } + + private String getInstalledVersion() { + try { + return getContext() + .getPackageManager() + .getPackageInfo(getContext().getPackageName(), 0) + .versionName; + } catch (Exception ignored) { + return "0.0.0"; + } + } + + private JSObject startDownloadedUpdateInstall() { + JSObject result = new JSObject(); + restoreDownloadedUpdateState(); + + if (downloadedUpdate == null || !downloadedUpdate.exists()) { + Log.w(TAG, "Install requested with no downloaded APK"); + result.put("installed", false); + result.put("reason", "missing-download"); + return result; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !getContext().getPackageManager().canRequestPackageInstalls()) { + Log.i(TAG, "Install requires unknown-apps permission"); + installPendingPermission = true; + openInstallPermissionSettings(); + result.put("installed", false); + result.put("reason", "permission-required"); + return result; + } + + try { + installPendingPermission = false; + Uri uri = FileProvider.getUriForFile( + getContext(), + getContext().getPackageName() + ".fileprovider", + downloadedUpdate + ); + Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + intent.setData(uri); + intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); + intent.putExtra(Intent.EXTRA_RETURN_RESULT, true); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + getActivity().startActivity(intent); + Log.i(TAG, "Started Android package installer for " + downloadedUpdate.getAbsolutePath()); + result.put("installed", true); + return result; + } catch (Exception error) { + Log.e(TAG, "Could not start package installer", error); + sendUpdateError("The update was downloaded, but MailFlow could not start the installer."); + result.put("installed", false); + result.put("reason", "launch-failed"); + result.put("error", error.getMessage()); + return result; + } + } + + private void continuePendingUpdateInstall() { + if (!installPendingPermission || downloadedUpdate == null || !downloadedUpdate.exists()) return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !getContext().getPackageManager().canRequestPackageInstalls()) return; + startDownloadedUpdateInstall(); + } + + private JSObject showUpdateReadyDialog() { + JSObject result = new JSObject(); + restoreDownloadedUpdateState(); + + if (downloadedUpdate == null || !downloadedUpdate.exists()) { + Log.w(TAG, "Install dialog requested with no downloaded APK"); + result.put("installed", false); + result.put("reason", "missing-download"); + return result; + } + + if (getActivity() == null || getActivity().isFinishing()) { + return startDownloadedUpdateInstall(); + } + + String version = updateInfo == null || updateInfo.version == null || updateInfo.version.isEmpty() + ? "update" + : updateInfo.version; + + getActivity().runOnUiThread(() -> { + if (getActivity() == null || getActivity().isFinishing()) { + startDownloadedUpdateInstall(); + return; + } + + new AlertDialog.Builder(getActivity()) + .setTitle("Update ready") + .setMessage("MailFlow " + version + " has been downloaded and is ready to install.") + .setPositiveButton("Install", (dialog, which) -> startDownloadedUpdateInstall()) + .setNegativeButton("Later", null) + .show(); + }); + + result.put("installed", true); + result.put("dialog", true); + return result; + } + + private void persistDownloadedUpdateState(ReleaseInfo release, File file) { + if (release == null || file == null) return; + + getPrefs(getContext()) + .edit() + .putString(PREF_UPDATE_APK_PATH, file.getAbsolutePath()) + .putString(PREF_UPDATE_VERSION, release.version == null ? "" : release.version) + .putString(PREF_UPDATE_RELEASE_NAME, release.releaseName == null ? "" : release.releaseName) + .apply(); + } + + private void restoreDownloadedUpdateState() { + if (downloadedUpdate != null && downloadedUpdate.exists()) return; + + SharedPreferences prefs = getPrefs(getContext()); + String path = prefs.getString(PREF_UPDATE_APK_PATH, null); + if (path == null || path.isEmpty()) return; + + File file = new File(path); + if (!file.exists()) { + clearDownloadedUpdateState(); + return; + } + + downloadedUpdate = file; + if (updateInfo == null) { + ReleaseInfo restored = new ReleaseInfo(); + restored.version = prefs.getString(PREF_UPDATE_VERSION, ""); + restored.releaseName = prefs.getString(PREF_UPDATE_RELEASE_NAME, restored.version); + restored.assetName = file.getName(); + updateInfo = restored; + } + } + + private void clearDownloadedUpdateState() { + downloadedUpdate = null; + installPendingPermission = false; + getPrefs(getContext()) + .edit() + .remove(PREF_UPDATE_APK_PATH) + .remove(PREF_UPDATE_VERSION) + .remove(PREF_UPDATE_RELEASE_NAME) + .apply(); + } + + private void openInstallPermissionSettings() { + Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES) + .setData(Uri.parse("package:" + getContext().getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getContext().startActivity(intent); + } + + private void sendUpdateError(String message) { + JSObject status = updateStatus("error"); + status.put("message", message); + sendUpdateStatus(status); + } + + private void postUpdateReadyNotification(ReleaseInfo release) { + Intent openIntent = new Intent(getContext(), MainActivity.class); + openIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + PendingIntent openPendingIntent = PendingIntent.getActivity( + getContext(), + 1002, + openIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + Intent installIntent = new Intent(getContext(), MainActivity.class); + installIntent.setAction(ACTION_INSTALL_UPDATE); + installIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + PendingIntent installPendingIntent = PendingIntent.getActivity( + getContext(), + 1003, + installIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext(), CHANNEL_UPDATES) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("MailFlow update ready") + .setContentText("MailFlow " + release.version + " has been downloaded.") + .setStyle(new NotificationCompat.BigTextStyle().bigText("MailFlow " + release.version + " has been downloaded and is ready to install.")) + .setContentIntent(openPendingIntent) + .addAction(R.mipmap.ic_launcher, "Install", installPendingIntent) + .setAutoCancel(false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_DEFAULT); + + if (hasNotificationPermission(getContext())) { + NotificationManagerCompat.from(getContext()).notify(1002, builder.build()); + } + } + + private void sendUpdateStatus(JSObject status) { + notifyListeners("updateStatus", status); + + if (getBridge() == null || getBridge().getWebView() == null) return; + String script = "(function(status){" + + "window.dispatchEvent(new CustomEvent('mailflow:update-status',{detail:status}));" + + "window.postMessage({type:'mailflow:update-status',payload:status},'*');" + + "})(" + status.toString() + ");"; + getBridge().getWebView().post(() -> getBridge().getWebView().evaluateJavascript(script, null)); + } + + private JSObject updateStatus(String type) { + JSObject status = new JSObject(); + status.put("type", type); + return status; + } + + private JSObject updateStatus(String type, JSObject data) { + JSObject status = updateStatus(type); + status.put("data", data); + return status; + } + + private static boolean isNewerVersion(String candidate, String current) { + int[] next = parseVersion(candidate); + int[] installed = parseVersion(current); + if (next == null || installed == null) return false; + + for (int i = 0; i < 3; i++) { + if (next[i] > installed[i]) return true; + if (next[i] < installed[i]) return false; + } + + return false; + } + + private static int[] parseVersion(String value) { + Matcher matcher = VERSION_PATTERN.matcher(value == null ? "" : value); + if (!matcher.find()) return null; + + String[] parts = matcher.group().split("\\."); + int[] version = new int[] { 0, 0, 0 }; + for (int i = 0; i < Math.min(parts.length, 3); i++) { + try { + version[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException ignored) { + return null; + } + } + return version; + } + + private static String readStream(InputStream stream) throws Exception { + StringBuilder builder = new StringBuilder(); + byte[] buffer = new byte[8192]; + int read; + while ((read = stream.read(buffer)) != -1) { + builder.append(new String(buffer, 0, read, "UTF-8")); + } + return builder.toString(); + } + + private static File uniqueFile(File directory, String filename) { + File file = new File(directory, filename); + if (!file.exists()) return file; + + String base = filename.replaceFirst("\\.apk$", ""); + for (int i = 1; i < 1000; i++) { + file = new File(directory, base + " (" + i + ").apk"); + if (!file.exists()) return file; + } + return new File(directory, base + "-" + UUID.randomUUID() + ".apk"); + } + + private static String sanitizeApkName(String value) { + String name = value == null ? "MailFlow.apk" : value.replaceAll("[^A-Za-z0-9._ -]", "_"); + if (!name.toLowerCase().endsWith(".apk")) name += ".apk"; + return name; + } + + private static boolean isConfiguredHost(Context context, String url) { + String host = getSavedHost(context); + return host != null && url != null && url.startsWith(host); + } + + private static class ReleaseInfo { + String version; + String releaseName; + String releaseNotes; + String releaseDate; + String assetName; + String downloadUrl; + + JSObject toStatusData() { + return toStatusData(null); + } + + JSObject toStatusData(String filePath) { + JSObject data = new JSObject(); + data.put("releaseNotes", releaseNotes); + data.put("releaseName", releaseName); + data.put("releaseDate", releaseDate); + data.put("updateUrl", downloadUrl); + data.put("manual", true); + if (filePath != null) data.put("filePath", filePath); + return data; + } + } + + private static void createNotificationChannel(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; + + NotificationChannel channel = new NotificationChannel( + CHANNEL_NEW_MAIL, + "New mail", + NotificationManager.IMPORTANCE_DEFAULT + ); + channel.setDescription("New mail notifications from MailFlow."); + NotificationManager manager = context.getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + + NotificationChannel updatesChannel = new NotificationChannel( + CHANNEL_UPDATES, + "Updates", + NotificationManager.IMPORTANCE_DEFAULT + ); + updatesChannel.setDescription("MailFlow app update notifications."); + manager.createNotificationChannel(updatesChannel); + } + } + + @PermissionCallback + private void notificationPermissionCallback(PluginCall call) { + call.resolve(notificationPermissionResult(getNotificationPermissionState())); + } + + private boolean hasNotificationPermission() { + return hasNotificationPermission(getContext()); + } + + private static boolean hasNotificationPermission(Context context) { + if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) { + return false; + } + + return Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED; + } + + private String getNotificationPermissionState() { + if (hasNotificationPermission()) { + return "granted"; + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return "denied"; + + PermissionState state = getPermissionState("notifications"); + if (state == PermissionState.DENIED) return "denied"; + return "default"; + } + + private JSObject notificationPermissionResult(String permission) { + JSObject result = new JSObject(); + result.put("permission", permission); + return result; + } + + public static class NotificationBridge { + private final Context context; + + NotificationBridge(Context context) { + this.context = context.getApplicationContext(); + createNotificationChannel(this.context); + } + + @JavascriptInterface + public void showNewMail(String notificationJson) { + try { + JSONObject notification = new JSONObject(notificationJson == null ? "{}" : notificationJson); + JSONObject messageObject = notification.optJSONObject("message"); + JSObject message = messageObject == null ? null : JSObject.fromJSONObject(messageObject); + + postNewMailNotification( + context, + notification.optString("title", "New mail"), + notification.optString("body", "You have new mail."), + notification.optString("messageId", null), + notification.optString("accountId", null), + notification.optString("folder", "INBOX"), + message + ); + } catch (JSONException ignored) {} + } + + @JavascriptInterface + public String installDownloadedUpdate() { + if (instance == null) { + JSObject result = new JSObject(); + result.put("installed", false); + result.put("reason", "unavailable"); + return result.toString(); + } + + return instance.showUpdateReadyDialog().toString(); + } + } + + private static void putExtra(Intent intent, String key, String value) { + if (value != null) intent.putExtra(key, value); + } + + private static void copyStringExtra(Intent intent, JSObject target, String key) { + String value = intent.getStringExtra(key); + if (value != null) target.put(key, value); + } +} diff --git a/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowNotificationActionReceiver.java b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowNotificationActionReceiver.java new file mode 100644 index 00000000..8db820e5 --- /dev/null +++ b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowNotificationActionReceiver.java @@ -0,0 +1,43 @@ +package sh.mailflow.app; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import androidx.core.app.NotificationManagerCompat; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; + +public class MailFlowNotificationActionReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (context == null || intent == null) return; + + String action = intent.getAction(); + String messageId = intent.getStringExtra("messageId"); + if (messageId == null || messageId.isEmpty()) return; + if (!MailFlowNativePlugin.ACTION_DELETE_MESSAGE.equals(action) + && !MailFlowNativePlugin.ACTION_STAR_MESSAGE.equals(action)) return; + + int notificationId = intent.getIntExtra("notificationId", -1); + if (notificationId != -1) { + NotificationManagerCompat.from(context).cancel(notificationId); + } + + Constraints constraints = new Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build(); + Data input = new Data.Builder() + .putString(MailFlowNotificationActionWorker.KEY_ACTION, action) + .putString(MailFlowNotificationActionWorker.KEY_MESSAGE_ID, messageId) + .build(); + OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(MailFlowNotificationActionWorker.class) + .setInputData(input) + .setConstraints(constraints) + .build(); + + WorkManager.getInstance(context.getApplicationContext()).enqueue(request); + } +} diff --git a/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowNotificationActionWorker.java b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowNotificationActionWorker.java new file mode 100644 index 00000000..2e0dd28e --- /dev/null +++ b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowNotificationActionWorker.java @@ -0,0 +1,73 @@ +package sh.mailflow.app; + +import android.content.Context; +import android.webkit.CookieManager; +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +public class MailFlowNotificationActionWorker extends Worker { + static final String KEY_ACTION = "action"; + static final String KEY_MESSAGE_ID = "messageId"; + + public MailFlowNotificationActionWorker(@NonNull Context context, @NonNull WorkerParameters params) { + super(context, params); + } + + @NonNull + @Override + public Result doWork() { + Context context = getApplicationContext(); + String host = MailFlowNativePlugin.getSavedHost(context); + String action = getInputData().getString(KEY_ACTION); + String messageId = getInputData().getString(KEY_MESSAGE_ID); + if (host == null || host.isEmpty() || action == null || messageId == null || messageId.isEmpty()) { + return Result.failure(); + } + + String cookie = CookieManager.getInstance().getCookie(host); + if (cookie == null || cookie.trim().isEmpty()) return Result.failure(); + + try { + if (MailFlowNativePlugin.ACTION_DELETE_MESSAGE.equals(action)) { + request(host + "/api/mail/messages/" + messageId, "DELETE", cookie, null); + } else if (MailFlowNativePlugin.ACTION_STAR_MESSAGE.equals(action)) { + request(host + "/api/mail/messages/" + messageId + "/star", "PATCH", cookie, "{\"starred\":true}"); + } else { + return Result.failure(); + } + + return Result.success(); + } catch (Exception ignored) { + return Result.retry(); + } + } + + private static void request(String url, String method, String cookie, String body) throws Exception { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setRequestMethod(method); + connection.setConnectTimeout(15000); + connection.setReadTimeout(15000); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Cookie", cookie); + + if (body != null) { + byte[] bytes = body.getBytes("UTF-8"); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setFixedLengthStreamingMode(bytes.length); + try (OutputStream output = connection.getOutputStream()) { + output.write(bytes); + } + } + + int status = connection.getResponseCode(); + connection.disconnect(); + if (status < 200 || status >= 300) { + throw new IllegalStateException("MailFlow notification action failed: HTTP " + status); + } + } +} diff --git a/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowWebViewClient.java b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowWebViewClient.java new file mode 100644 index 00000000..a3593240 --- /dev/null +++ b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MailFlowWebViewClient.java @@ -0,0 +1,71 @@ +package sh.mailflow.app; + +import android.content.Context; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import com.getcapacitor.Bridge; +import com.getcapacitor.BridgeWebViewClient; + +public class MailFlowWebViewClient extends BridgeWebViewClient { + private static final String FALLBACK_URL = "file:///android_asset/public/host-unavailable.html"; + private final Context context; + private boolean loadingFallback = false; + + public MailFlowWebViewClient(Bridge bridge, Context context) { + super(bridge); + this.context = context.getApplicationContext(); + } + + @Override + public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + + if (!request.isForMainFrame() || errorResponse == null) return; + int statusCode = errorResponse.getStatusCode(); + if ((statusCode == 404 || statusCode == 502 || statusCode == 503 || statusCode == 504) && isConfiguredHost(request.getUrl().toString())) { + loadFallback(view); + } + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + super.onReceivedError(view, request, error); + + if (request.isForMainFrame() && isConfiguredHost(request.getUrl().toString())) { + loadFallback(view); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + + if (FALLBACK_URL.equals(url)) return; + loadingFallback = false; + + if (!isConfiguredHost(url)) return; + + MailFlowNativePlugin.injectCapacitorCompat(view); + MailFlowNativePlugin.injectPendingActions(view, context); + + view.evaluateJavascript("(document.body ? document.body.innerText : '')", (text) -> { + String bodyText = text == null ? "" : text.toLowerCase(); + if (bodyText.contains("rewrite 502 bad gateway page") || bodyText.contains("rewrite 404 error page")) { + loadFallback(view); + } + }); + } + + private boolean isConfiguredHost(String url) { + String host = MailFlowNativePlugin.getSavedHost(context); + return host != null && url != null && url.startsWith(host); + } + + private void loadFallback(WebView view) { + if (loadingFallback) return; + loadingFallback = true; + view.post(() -> view.loadUrl(FALLBACK_URL)); + } +} diff --git a/frontend/packages/android/app/src/main/java/sh/mailflow/app/MainActivity.java b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MainActivity.java new file mode 100644 index 00000000..c49b1bc3 --- /dev/null +++ b/frontend/packages/android/app/src/main/java/sh/mailflow/app/MainActivity.java @@ -0,0 +1,181 @@ +package sh.mailflow.app; + +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.webkit.CookieManager; +import android.webkit.WebView; +import androidx.activity.OnBackPressedCallback; +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity { + private String lastHandledIntentKey = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + registerPlugin(MailFlowNativePlugin.class); + super.onCreate(savedInstanceState); + getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + handleAndroidBack(); + } + }); + + if (bridge != null) { + configureCookies(); + bridge.getWebView().addJavascriptInterface(new MailFlowNativePlugin.NotificationBridge(this), "MailFlowAndroid"); + bridge.setWebViewClient(new MailFlowWebViewClient(bridge, this)); + String savedHost = MailFlowNativePlugin.getSavedHost(this); + if (savedHost != null) { + MailFlowBackgroundSync.schedule(this); + bridge.getWebView().post(() -> bridge.getWebView().loadUrl(savedHost)); + } + } + + handleNativeIntent(getIntent()); + } + + @Override + public void onPause() { + flushCookies(); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + MailFlowNativePlugin.resumePendingUpdateInstall(); + } + + @Override + public void onStop() { + flushCookies(); + MailFlowBackgroundSync.schedule(this); + super.onStop(); + } + + @Override + public void onDestroy() { + flushCookies(); + super.onDestroy(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + handleNativeIntent(intent); + } + + private void handleAndroidBack() { + if (bridge == null || bridge.getWebView() == null) { + moveTaskToBack(true); + return; + } + + WebView webView = bridge.getWebView(); + webView.evaluateJavascript( + "(function(){try{" + + "if(typeof window.__mailflowHandleAndroidBack==='function'){return !!window.__mailflowHandleAndroidBack();}" + + "}catch(e){}" + + "return false;" + + "})()", + (handled) -> { + if ("true".equals(handled)) return; + + runOnUiThread(() -> moveTaskToBack(true)); + } + ); + } + + private void handleNativeIntent(Intent intent) { + if (intent == null) return; + if (!markIntentHandled(intent)) return; + + String action = intent.getAction(); + Uri data = intent.getData(); + + if (MailFlowNativePlugin.ACTION_OPEN_MESSAGE.equals(action)) { + MailFlowNativePlugin.sendOpenMessageAction(intent); + return; + } + + if (MailFlowNativePlugin.ACTION_REPLY_MESSAGE.equals(action)) { + MailFlowNativePlugin.sendReplyMessageAction(intent); + return; + } + + if (MailFlowNativePlugin.ACTION_DELETE_MESSAGE.equals(action)) { + MailFlowNativePlugin.sendDeleteMessageAction(intent); + return; + } + + if (MailFlowNativePlugin.ACTION_STAR_MESSAGE.equals(action)) { + MailFlowNativePlugin.sendStarMessageAction(intent); + return; + } + + if (MailFlowNativePlugin.ACTION_COMPOSE.equals(action)) { + MailFlowNativePlugin.sendComposeAction(); + return; + } + + if (MailFlowNativePlugin.ACTION_SYNC.equals(action)) { + MailFlowNativePlugin.sendSyncAction(); + return; + } + + if (MailFlowNativePlugin.ACTION_INSTALL_UPDATE.equals(action)) { + MailFlowNativePlugin.installDownloadedUpdateFromIntent(); + return; + } + + if (Intent.ACTION_VIEW.equals(action) && data != null && "mailflow".equalsIgnoreCase(data.getScheme())) { + String route = data.getHost(); + if (route == null || route.isEmpty()) { + route = data.getPath() == null ? "" : data.getPath().replaceFirst("^/", ""); + } + + if ("compose".equalsIgnoreCase(route)) { + MailFlowNativePlugin.sendComposeAction(); + return; + } + + if ("sync".equalsIgnoreCase(route)) { + MailFlowNativePlugin.sendSyncAction(); + return; + } + } + + if ((Intent.ACTION_SENDTO.equals(action) || Intent.ACTION_VIEW.equals(action)) && data != null && "mailto".equalsIgnoreCase(data.getScheme())) { + MailFlowNativePlugin.sendMailtoAction(data); + } + } + + private boolean markIntentHandled(Intent intent) { + String action = intent.getAction(); + Uri data = intent.getData(); + String messageId = intent.getStringExtra("messageId"); + String key = String.valueOf(action) + "|" + String.valueOf(data) + "|" + String.valueOf(messageId); + + if (key.equals(lastHandledIntentKey)) return false; + lastHandledIntentKey = key; + return true; + } + + private void configureCookies() { + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.setAcceptCookie(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && bridge != null && bridge.getWebView() != null) { + cookieManager.setAcceptThirdPartyCookies(bridge.getWebView(), true); + } + } + + private void flushCookies() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().flush(); + } + } +} diff --git a/frontend/packages/android/app/src/main/res/drawable-land-hdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 00000000..5b7b0d04 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-land-mdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 00000000..1e2132f5 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-land-xhdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 00000000..01595d2d Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 00000000..2dcdebd5 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 00000000..75037778 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-port-hdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 00000000..8258157d Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-port-mdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 00000000..5b5735c8 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-port-xhdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 00000000..20d64bcc Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 00000000..6a2ba60e Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/frontend/packages/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 00000000..7f5afca0 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/frontend/packages/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..c7bd21db --- /dev/null +++ b/frontend/packages/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/frontend/packages/android/app/src/main/res/drawable/ic_launcher_background.xml b/frontend/packages/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..d5fccc53 --- /dev/null +++ b/frontend/packages/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/packages/android/app/src/main/res/drawable/splash.png b/frontend/packages/android/app/src/main/res/drawable/splash.png new file mode 100644 index 00000000..1e2132f5 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/drawable/splash.png differ diff --git a/frontend/packages/android/app/src/main/res/layout/activity_main.xml b/frontend/packages/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..b5ad1387 --- /dev/null +++ b/frontend/packages/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/frontend/packages/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/frontend/packages/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/frontend/packages/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/packages/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/frontend/packages/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/frontend/packages/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..2bb26e1b Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/frontend/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..7f4c70eb Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/frontend/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..2bb26e1b Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..9e3b2525 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/frontend/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..11a493f4 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/frontend/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..9e3b2525 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..4c607d14 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/frontend/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..ba0a1a96 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/frontend/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..4c607d14 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..c8aed107 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/frontend/packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a5932011 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/frontend/packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..c8aed107 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..5707fc60 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/frontend/packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..7e17e00a Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/frontend/packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/frontend/packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..5707fc60 Binary files /dev/null and b/frontend/packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/frontend/packages/android/app/src/main/res/values/ic_launcher_background.xml b/frontend/packages/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..3bd67804 --- /dev/null +++ b/frontend/packages/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #101113 + diff --git a/frontend/packages/android/app/src/main/res/values/strings.xml b/frontend/packages/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..f1fd3a5d --- /dev/null +++ b/frontend/packages/android/app/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + + MailFlow + MailFlow + sh.mailflow.app + sh.mailflow.app + New email + Compose + Sync + Sync + diff --git a/frontend/packages/android/app/src/main/res/values/styles.xml b/frontend/packages/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..abf55cf2 --- /dev/null +++ b/frontend/packages/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/frontend/packages/android/app/src/main/res/xml/file_paths.xml b/frontend/packages/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..bd0c4d80 --- /dev/null +++ b/frontend/packages/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/packages/android/app/src/main/res/xml/shortcuts.xml b/frontend/packages/android/app/src/main/res/xml/shortcuts.xml new file mode 100644 index 00000000..530c7738 --- /dev/null +++ b/frontend/packages/android/app/src/main/res/xml/shortcuts.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/frontend/packages/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/frontend/packages/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 00000000..02973278 --- /dev/null +++ b/frontend/packages/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/frontend/packages/android/build.gradle b/frontend/packages/android/build.gradle new file mode 100644 index 00000000..f8f0e43b --- /dev/null +++ b/frontend/packages/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/frontend/packages/android/capacitor.settings.gradle b/frontend/packages/android/capacitor.settings.gradle new file mode 100644 index 00000000..874b109a --- /dev/null +++ b/frontend/packages/android/capacitor.settings.gradle @@ -0,0 +1,3 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor') diff --git a/frontend/packages/android/gradle.properties b/frontend/packages/android/gradle.properties new file mode 100644 index 00000000..2e87c52f --- /dev/null +++ b/frontend/packages/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/frontend/packages/android/gradle/wrapper/gradle-wrapper.jar b/frontend/packages/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..1b33c55b Binary files /dev/null and b/frontend/packages/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/frontend/packages/android/gradle/wrapper/gradle-wrapper.properties b/frontend/packages/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..7705927e --- /dev/null +++ b/frontend/packages/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/frontend/packages/android/gradlew b/frontend/packages/android/gradlew new file mode 100644 index 00000000..23d15a93 --- /dev/null +++ b/frontend/packages/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/frontend/packages/android/gradlew.bat b/frontend/packages/android/gradlew.bat new file mode 100644 index 00000000..db3a6ac2 --- /dev/null +++ b/frontend/packages/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/frontend/packages/android/settings.gradle b/frontend/packages/android/settings.gradle new file mode 100644 index 00000000..3b4431d7 --- /dev/null +++ b/frontend/packages/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/frontend/packages/android/variables.gradle b/frontend/packages/android/variables.gradle new file mode 100644 index 00000000..7d38ad3a --- /dev/null +++ b/frontend/packages/android/variables.gradle @@ -0,0 +1,17 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + androidxWorkVersion = '2.10.1' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} diff --git a/frontend/packages/capacitor.config.json b/frontend/packages/capacitor.config.json new file mode 100644 index 00000000..31b08ea8 --- /dev/null +++ b/frontend/packages/capacitor.config.json @@ -0,0 +1,13 @@ +{ + "appId": "sh.mailflow.app", + "appName": "MailFlow", + "webDir": "../dist", + "android": { + "path": "android" + }, + "server": { + "allowNavigation": [ + "*" + ] + } +} diff --git a/frontend/packages/electron/icons/128x128.png b/frontend/packages/electron/icons/128x128.png new file mode 100644 index 00000000..31c39a32 Binary files /dev/null and b/frontend/packages/electron/icons/128x128.png differ diff --git a/frontend/packages/electron/icons/144x144.png b/frontend/packages/electron/icons/144x144.png new file mode 100644 index 00000000..1c9553ce Binary files /dev/null and b/frontend/packages/electron/icons/144x144.png differ diff --git a/frontend/packages/electron/icons/152x152.png b/frontend/packages/electron/icons/152x152.png new file mode 100644 index 00000000..6bf68a11 Binary files /dev/null and b/frontend/packages/electron/icons/152x152.png differ diff --git a/frontend/packages/electron/icons/192x192.png b/frontend/packages/electron/icons/192x192.png new file mode 100644 index 00000000..677607b2 Binary files /dev/null and b/frontend/packages/electron/icons/192x192.png differ diff --git a/frontend/packages/electron/icons/32x32.png b/frontend/packages/electron/icons/32x32.png new file mode 100644 index 00000000..9675e735 Binary files /dev/null and b/frontend/packages/electron/icons/32x32.png differ diff --git a/frontend/packages/electron/icons/384x384.png b/frontend/packages/electron/icons/384x384.png new file mode 100644 index 00000000..e821b641 Binary files /dev/null and b/frontend/packages/electron/icons/384x384.png differ diff --git a/frontend/packages/electron/icons/512x512.png b/frontend/packages/electron/icons/512x512.png new file mode 100644 index 00000000..35138e6a Binary files /dev/null and b/frontend/packages/electron/icons/512x512.png differ diff --git a/frontend/packages/electron/icons/72x72.png b/frontend/packages/electron/icons/72x72.png new file mode 100644 index 00000000..8c94f231 Binary files /dev/null and b/frontend/packages/electron/icons/72x72.png differ diff --git a/frontend/packages/electron/icons/96x96.png b/frontend/packages/electron/icons/96x96.png new file mode 100644 index 00000000..524ca0da Binary files /dev/null and b/frontend/packages/electron/icons/96x96.png differ diff --git a/frontend/packages/electron/icons/icon.icns b/frontend/packages/electron/icons/icon.icns new file mode 100644 index 00000000..4f4704f7 Binary files /dev/null and b/frontend/packages/electron/icons/icon.icns differ diff --git a/frontend/packages/electron/icons/icon.ico b/frontend/packages/electron/icons/icon.ico new file mode 100644 index 00000000..965e5373 Binary files /dev/null and b/frontend/packages/electron/icons/icon.ico differ diff --git a/frontend/packages/electron/icons/icon.png b/frontend/packages/electron/icons/icon.png new file mode 100644 index 00000000..35138e6a Binary files /dev/null and b/frontend/packages/electron/icons/icon.png differ diff --git a/frontend/packages/electron/installer.nsh b/frontend/packages/electron/installer.nsh new file mode 100644 index 00000000..1413b636 --- /dev/null +++ b/frontend/packages/electron/installer.nsh @@ -0,0 +1,19 @@ +!macro customInstall + WriteRegStr SHCTX "Software\RegisteredApplications" "MailFlow" "Software\Clients\Mail\MailFlow\Capabilities" + + WriteRegStr SHCTX "Software\Clients\Mail\MailFlow" "" "MailFlow" + WriteRegStr SHCTX "Software\Clients\Mail\MailFlow\Capabilities" "ApplicationName" "MailFlow" + WriteRegStr SHCTX "Software\Clients\Mail\MailFlow\Capabilities" "ApplicationDescription" "A self-hosted, unified webmail client." + WriteRegStr SHCTX "Software\Clients\Mail\MailFlow\Capabilities\URLAssociations" "mailto" "MailFlow.mailto" + + WriteRegStr SHCTX "Software\Classes\MailFlow.mailto" "" "URL:MailFlow MailTo Protocol" + WriteRegStr SHCTX "Software\Classes\MailFlow.mailto" "URL Protocol" "" + WriteRegStr SHCTX "Software\Classes\MailFlow.mailto\DefaultIcon" "" "$INSTDIR\MailFlow.exe,0" + WriteRegStr SHCTX "Software\Classes\MailFlow.mailto\shell\open\command" "" '"$INSTDIR\MailFlow.exe" "%1"' +!macroend + +!macro customUnInstall + DeleteRegValue SHCTX "Software\RegisteredApplications" "MailFlow" + DeleteRegKey SHCTX "Software\Clients\Mail\MailFlow" + DeleteRegKey SHCTX "Software\Classes\MailFlow.mailto" +!macroend diff --git a/frontend/packages/electron/main.cjs b/frontend/packages/electron/main.cjs new file mode 100644 index 00000000..1def507a --- /dev/null +++ b/frontend/packages/electron/main.cjs @@ -0,0 +1,1593 @@ +const { app, BrowserWindow, Menu, Tray, nativeImage, ipcMain, shell, dialog, Notification, session } = require('electron'); +const { execFileSync, spawn } = require('child_process'); +const fs = require('fs'); +const http = require('http'); +const https = require('https'); +const path = require('path'); + +const CONFIG_FILE = 'mailflow-host.json'; +const UPDATE_STATUS_CHANNEL = 'mailflow:updates:status'; +const UPDATE_RELEASE_URL = 'https://api.github.com/repos/maathimself/mailflow/releases/latest'; +const UPDATE_ERROR_MESSAGE = 'Could not check for MailFlow updates. Please visit the website instead.'; +const NATIVE_ACTION_CHANNEL = 'mailflow:native-action'; +const NATIVE_ACTION_ARG = '--mailflow-action='; +const NEW_MAIL_NOTIFICATION_MAX_LENGTH = 240; +const MAILTO_PROTOCOL = 'mailto'; +const REWRITE_ERROR_PATTERNS = [ + /Rewrite\s+502\s+Bad\s+Gateway\s+Page/i, + /Rewrite\s+404\s+Error\s+Page/i, +]; +const HOST_UNAVAILABLE_STATUS_CODES = new Set([404, 502, 503, 504]); +const LINUX_BADGE_DESKTOP_IDS = [ + 'MailFlow.desktop', + 'mailflow.desktop', + 'sh.mailflow.app.desktop', + 'mailflow-frontend.desktop', +]; + +let mainWindow; +let tray = null; +let isQuitting = false; +let updateInfo = null; +let downloadedUpdate = null; +let updateDownloadsInitialized = false; +let nextNativeActionId = 1; +const pendingNativeActions = new Map(); +const pendingProtocolUrls = []; + +app.setName('MailFlow'); +if (process.platform === 'win32') { + app.setAppUserModelId('sh.mailflow.app'); +} +if (process.platform === 'linux' && typeof app.setDesktopName === 'function') { + app.setDesktopName('MailFlow.desktop'); +} + +if (process.platform === 'linux' && process.env.APPIMAGE) { + app.commandLine.appendSwitch('no-sandbox'); +} + +function registerMailtoProtocol() { + try { + if (process.defaultApp && process.argv.length >= 2) { + const registered = app.setAsDefaultProtocolClient(MAILTO_PROTOCOL, process.execPath, [path.resolve(process.argv[1])]); + registerWindowsMailtoCapabilities(); + return registered; + } + + const registered = app.setAsDefaultProtocolClient(MAILTO_PROTOCOL); + registerWindowsMailtoCapabilities(); + return registered; + } catch (error) { + console.error('Could not register mailto protocol handler:', error); + return false; + } +} + +function writeCurrentUserRegValue(key, name, value) { + const args = ['add', key, name ? '/v' : '/ve']; + if (name) args.push(name); + args.push('/t', 'REG_SZ', '/d', value, '/f'); + execFileSync('reg', args, { stdio: 'ignore', windowsHide: true }); +} + +function registerWindowsMailtoCapabilities() { + if (process.platform !== 'win32') return false; + + try { + const exePath = process.execPath; + const command = `"${exePath}" "%1"`; + + writeCurrentUserRegValue('HKCU\\Software\\RegisteredApplications', 'MailFlow', 'Software\\Clients\\Mail\\MailFlow\\Capabilities'); + writeCurrentUserRegValue('HKCU\\Software\\Clients\\Mail\\MailFlow', '', 'MailFlow'); + writeCurrentUserRegValue('HKCU\\Software\\Clients\\Mail\\MailFlow\\Capabilities', 'ApplicationName', 'MailFlow'); + writeCurrentUserRegValue('HKCU\\Software\\Clients\\Mail\\MailFlow\\Capabilities', 'ApplicationDescription', 'A self-hosted, unified webmail client.'); + writeCurrentUserRegValue('HKCU\\Software\\Clients\\Mail\\MailFlow\\Capabilities\\URLAssociations', 'mailto', 'MailFlow.mailto'); + writeCurrentUserRegValue('HKCU\\Software\\Classes\\MailFlow.mailto', '', 'URL:MailFlow MailTo Protocol'); + writeCurrentUserRegValue('HKCU\\Software\\Classes\\MailFlow.mailto', 'URL Protocol', ''); + writeCurrentUserRegValue('HKCU\\Software\\Classes\\MailFlow.mailto\\DefaultIcon', '', `${exePath},0`); + writeCurrentUserRegValue('HKCU\\Software\\Classes\\MailFlow.mailto\\shell\\open\\command', '', command); + + return true; + } catch (error) { + console.error('Could not register Windows mailto capabilities:', error); + return false; + } +} + +function getIconPath() { + return path.join(__dirname, 'icons', 'icon.png'); +} + +function getWindowIconPath() { + if (process.platform === 'win32') return path.join(__dirname, 'icons', 'icon.ico'); + if (process.platform === 'linux') return getIconPath(); + return undefined; +} + +function getConfigPath() { + return path.join(app.getPath('userData'), CONFIG_FILE); +} + +function readConfig() { + try { + return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8')); + } catch { + return {}; + } +} + +function writeConfig(config) { + fs.mkdirSync(app.getPath('userData'), { recursive: true }); + fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2)); +} + +function readHost() { + try { + const config = readConfig(); + return normalizeHost(config.host); + } catch { + return null; + } +} + +function writeHost(host) { + const normalized = normalizeHost(host); + writeConfig({ ...readConfig(), host: normalized }); + return normalized; +} + +function clearHost() { + const config = readConfig(); + delete config.host; + writeConfig(config); +} + +function normalizeHost(value) { + const input = String(value || '').trim(); + const url = new URL(input); + + if (!['https:', 'http:'].includes(url.protocol)) { + throw new Error('Host must start with https:// or http://'); + } + + url.username = ''; + url.password = ''; + url.hash = ''; + url.search = ''; + url.pathname = '/'; + + return url.toString().replace(/\/$/, ''); +} + +function requestJson(url) { + return new Promise((resolve, reject) => { + const request = https.get(url, { + headers: { + Accept: 'application/vnd.github+json', + 'User-Agent': `MailFlow/${app.getVersion()}`, + }, + }, (response) => { + if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { + response.resume(); + requestJson(response.headers.location).then(resolve, reject); + return; + } + + let body = ''; + response.setEncoding('utf8'); + response.on('data', (chunk) => { + body += chunk; + }); + response.on('end', () => { + if (response.statusCode < 200 || response.statusCode >= 300) { + reject(new Error(`Update request failed with status ${response.statusCode}`)); + return; + } + + try { + resolve(JSON.parse(body)); + } catch (error) { + reject(error); + } + }); + }); + + request.on('error', reject); + request.setTimeout(15000, () => { + request.destroy(new Error('Update request timed out')); + }); + }); +} + +function parseVersion(value) { + const match = String(value || '').match(/\d+(?:\.\d+){0,2}/); + if (!match) return null; + return match[0].split('.').map((part) => Number.parseInt(part, 10)); +} + +function isNewerVersion(candidate, current) { + const next = parseVersion(candidate); + const installed = parseVersion(current); + if (!next || !installed) return false; + + for (let index = 0; index < 3; index += 1) { + const nextPart = next[index] || 0; + const installedPart = installed[index] || 0; + if (nextPart > installedPart) return true; + if (nextPart < installedPart) return false; + } + + return false; +} + +function getLinuxDistributionIds() { + if (process.platform !== 'linux') return []; + + try { + const osRelease = fs.readFileSync('/etc/os-release', 'utf8'); + const ids = []; + + for (const key of ['ID', 'ID_LIKE']) { + const match = osRelease.match(new RegExp(`^${key}=(.+)$`, 'm')); + if (!match) continue; + + const values = match[1] + .replace(/^"|"$/g, '') + .toLowerCase() + .split(/\s+/) + .filter(Boolean); + ids.push(...values); + } + + return ids; + } catch { + return []; + } +} + +function isDebLikeLinuxDistribution(distroIds = getLinuxDistributionIds()) { + return distroIds.some((id) => ['debian', 'ubuntu', 'linuxmint', 'pop'].some((match) => id === match || id.includes(match))); +} + +function isRpmLikeLinuxDistribution(distroIds = getLinuxDistributionIds()) { + return distroIds.some((id) => ['fedora', 'rhel', 'centos', 'rocky', 'almalinux', 'suse', 'opensuse'].some((match) => id === match || id.includes(match))); +} + +function getInstalledLinuxPackageType() { + if (process.platform !== 'linux') return null; + if (process.env.APPIMAGE) return 'appimage'; + + try { + const packageType = fs.readFileSync(path.join(process.resourcesPath, 'package-type'), 'utf8').trim().toLowerCase(); + if (['deb', 'rpm', 'appimage'].includes(packageType)) return packageType; + } catch {} + + if (getLinuxPackageManagerVersion('rpm')) return 'rpm'; + if (getLinuxPackageManagerVersion('deb')) return 'deb'; + + const distroIds = getLinuxDistributionIds(); + if (isRpmLikeLinuxDistribution(distroIds) || getAvailableCommand(['rpm', 'dnf', 'dnf5', 'yum'])) return 'rpm'; + if (isDebLikeLinuxDistribution(distroIds) || getAvailableCommand(['dpkg', 'apt', 'apt-get'])) return 'deb'; + + return null; +} + +function getLinuxPackageManagerVersion(packageType) { + if (process.platform !== 'linux' || !['deb', 'rpm'].includes(packageType)) return null; + + const packageNames = ['mailflow', 'MailFlow', 'mailflow-frontend']; + for (const packageName of packageNames) { + try { + const args = packageType === 'rpm' + ? ['-q', '--qf', '%{VERSION}', packageName] + : ['-W', '-f=${Version}', packageName]; + const command = packageType === 'rpm' ? 'rpm' : 'dpkg-query'; + const output = execFileSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); + if (output) return output; + } catch {} + } + + return null; +} + +function getInstalledAppVersion(packageType = getInstalledLinuxPackageType()) { + return getLinuxPackageManagerVersion(packageType) || app.getVersion(); +} + +function getAvailableCommand(commands = []) { + for (const command of commands) { + try { + const output = execFileSync('which', [command], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); + if (output) return command; + } catch {} + } + + return null; +} + +function emitUnityLauncherBadgeCount(count) { + if (process.platform !== 'linux') return false; + + const gdbus = getAvailableCommand(['gdbus']); + if (!gdbus) return false; + + const visible = count > 0; + const properties = visible + ? `{'count': , 'count-visible': }` + : `{'count': , 'count-visible': }`; + + for (const desktopId of LINUX_BADGE_DESKTOP_IDS) { + const child = spawn(gdbus, [ + 'emit', + '--session', + '--object-path', + '/', + '--signal', + 'com.canonical.Unity.LauncherEntry.Update', + `application://${desktopId}`, + properties, + ], { + detached: true, + stdio: 'ignore', + }); + + child.unref(); + } + + return true; +} + +function setUnreadBadgeCount(count) { + let badgeSet = false; + + if (typeof app.setBadgeCount === 'function') { + badgeSet = app.setBadgeCount(count); + } + + return emitUnityLauncherBadgeCount(count) || badgeSet; +} + +function getLinuxPackagePatternGroups() { + const arch = process.arch === 'arm64' + ? '(?:arm64|aarch64)' + : '(?:amd64|x64|x86_64)'; + const deb = [new RegExp(`${arch}\\.deb$`, 'i'), /\.deb$/i]; + const rpm = [new RegExp(`${arch}\\.rpm$`, 'i'), /\.rpm$/i]; + + const installedPackageType = getInstalledLinuxPackageType(); + if (installedPackageType === 'appimage') return []; + if (installedPackageType === 'deb') return [deb]; + if (installedPackageType === 'rpm') return [rpm]; + + const distroIds = getLinuxDistributionIds(); + if (isDebLikeLinuxDistribution(distroIds)) { + return [deb]; + } + if (isRpmLikeLinuxDistribution(distroIds)) { + return [rpm]; + } + + if (getAvailableCommand(['rpm', 'dnf', 'dnf5', 'yum'])) return [rpm]; + if (getAvailableCommand(['dpkg', 'apt', 'apt-get'])) return [deb]; + + return []; +} + +function getUpdateAsset(release) { + const assets = Array.isArray(release.assets) ? release.assets : []; + const platformAssetPatternGroups = { + win32: [[/setup.*\.exe$/i], [/\.exe$/i]], + darwin: [[/\.dmg$/i]], + linux: getLinuxPackagePatternGroups(), + }; + const patternGroups = platformAssetPatternGroups[process.platform] || []; + + for (const patterns of patternGroups) { + for (const pattern of patterns) { + const asset = assets.find((item) => pattern.test(item.name || '') && item.browser_download_url); + if (asset) return asset; + } + } + + return null; +} + +function sendUpdateStatus(payload) { + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.webContents.send(UPDATE_STATUS_CHANNEL, payload); +} + +function showInAppNotification({ title = '', message = '', type = 'info', actionLabel = '', action = '', persistent = false }) { + if (!mainWindow || mainWindow.isDestroyed()) return; + + const payload = JSON.stringify({ title, message, type, actionLabel, action, persistent }); + mainWindow.webContents.executeJavaScript(` + (() => { + if (window.__mailflowNativeBridgeReady) return; + + const notification = ${payload}; + const id = 'mailflow-electron-toasts'; + let root = document.getElementById(id); + + if (!root) { + root = document.createElement('div'); + root.id = id; + root.style.position = 'fixed'; + root.style.right = '24px'; + root.style.bottom = '24px'; + root.style.zIndex = '2147483647'; + root.style.display = 'flex'; + root.style.flexDirection = 'column-reverse'; + root.style.gap = '8px'; + root.style.pointerEvents = 'none'; + document.documentElement.appendChild(root); + } + + const toast = document.createElement('div'); + toast.style.width = '340px'; + toast.style.maxWidth = 'calc(100vw - 48px)'; + toast.style.boxSizing = 'border-box'; + toast.style.display = 'flex'; + toast.style.alignItems = 'flex-start'; + toast.style.gap = '10px'; + toast.style.padding = '12px 14px'; + toast.style.borderRadius = '10px'; + toast.style.border = '1px solid rgba(255,255,255,0.10)'; + toast.style.background = 'rgba(36,36,41,0.98)'; + toast.style.boxShadow = '0 4px 20px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04)'; + toast.style.color = '#e8e8ed'; + toast.style.font = '13px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; + toast.style.pointerEvents = 'all'; + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + toast.style.transition = 'opacity 180ms ease, transform 180ms ease'; + + const icon = document.createElement('div'); + icon.style.width = '32px'; + icon.style.height = '32px'; + icon.style.borderRadius = '8px'; + icon.style.flex = '0 0 auto'; + icon.style.display = 'grid'; + icon.style.placeItems = 'center'; + icon.style.background = notification.type === 'negative' || notification.type === 'error' + ? 'rgba(248,113,113,0.15)' + : 'rgba(124,106,247,0.28)'; + icon.style.color = notification.type === 'negative' || notification.type === 'error' ? '#f87171' : '#a99cff'; + icon.textContent = notification.type === 'positive' ? '✓' : notification.type === 'negative' || notification.type === 'error' ? '!' : 'i'; + + const copy = document.createElement('div'); + copy.style.flex = '1'; + copy.style.minWidth = '0'; + + const heading = document.createElement('div'); + heading.textContent = notification.title; + heading.style.fontWeight = '650'; + heading.style.marginBottom = '2px'; + heading.style.whiteSpace = 'nowrap'; + heading.style.overflow = 'hidden'; + heading.style.textOverflow = 'ellipsis'; + + const body = document.createElement('div'); + body.textContent = notification.message; + body.style.fontSize = '12px'; + body.style.color = '#9898a8'; + body.style.whiteSpace = 'normal'; + body.style.overflow = 'visible'; + body.style.textOverflow = 'clip'; + body.style.lineHeight = '1.35'; + + const close = document.createElement('button'); + close.type = 'button'; + close.setAttribute('aria-label', 'Dismiss'); + close.textContent = '×'; + close.style.border = '0'; + close.style.background = 'transparent'; + close.style.color = '#9898a8'; + close.style.cursor = 'pointer'; + close.style.font = '20px/1 Inter, ui-sans-serif, system-ui'; + close.style.padding = '0'; + + let action = null; + if (notification.actionLabel && notification.action) { + action = document.createElement('button'); + action.type = 'button'; + action.textContent = notification.actionLabel; + action.style.border = '1px solid rgba(255,255,255,0.12)'; + action.style.borderRadius = '6px'; + action.style.background = 'rgba(255,255,255,0.08)'; + action.style.color = '#e8e8ed'; + action.style.cursor = 'pointer'; + action.style.font = '600 12px Inter, ui-sans-serif, system-ui'; + action.style.padding = '5px 10px'; + action.style.flex = '0 0 auto'; + action.addEventListener('click', () => { + if (notification.action === 'install-update') { + window.mailflowNative?.updates?.installDownloaded?.(); + } + dismiss(); + }); + } + + const dismiss = () => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + window.setTimeout(() => toast.remove(), 190); + }; + + close.addEventListener('click', dismiss); + copy.append(heading, body); + toast.append(icon, copy); + if (action) toast.appendChild(action); + toast.appendChild(close); + root.appendChild(toast); + + window.requestAnimationFrame(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateX(0)'; + }); + + if (!notification.persistent) { + window.setTimeout(dismiss, 5000); + } + })(); + `).catch(() => {}); +} + +function notifyUpdateStatus({ title, message, type = 'info' }) { + if (!mainWindow || mainWindow.isDestroyed()) return; + showInAppNotification({ title, message, type }); + mainWindow.webContents.send('mailflow:notifications:push', { title, message, type }); +} + +function cleanNotificationText(value, fallback = '') { + const text = String(value || fallback) + .replace(/\s+/g, ' ') + .trim(); + + if (text.length <= NEW_MAIL_NOTIFICATION_MAX_LENGTH) return text; + return `${text.slice(0, NEW_MAIL_NOTIFICATION_MAX_LENGTH - 1)}…`; +} + +function requestMailFlowApi(url, { method, body } = {}) { + return session.defaultSession.cookies.get({ url: readHost() }) + .then((cookies) => new Promise((resolve, reject) => { + const parsedUrl = new URL(url); + const request = (parsedUrl.protocol === 'http:' ? http : https).request(parsedUrl, { + method, + headers: { + Accept: 'application/json', + Cookie: cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; '), + ...(body ? { 'Content-Type': 'application/json' } : {}), + }, + }, (response) => { + response.resume(); + response.on('end', () => { + if (response.statusCode >= 200 && response.statusCode < 300) { + resolve(); + return; + } + + reject(new Error(`Mail action failed with status ${response.statusCode}`)); + }); + }); + + request.on('error', reject); + if (body) request.write(JSON.stringify(body)); + request.end(); + })); +} + +function runBackgroundMailAction(action, messageId) { + const host = readHost(); + if (!host || !messageId) return Promise.resolve(); + + const encodedMessageId = encodeURIComponent(messageId); + if (action === 'delete-message') { + return requestMailFlowApi(`${host}/api/mail/messages/${encodedMessageId}`, { + method: 'DELETE', + }); + } + + if (action === 'star-message') { + return requestMailFlowApi(`${host}/api/mail/messages/${encodedMessageId}/star`, { + method: 'PATCH', + body: { starred: true }, + }); + } + + return Promise.resolve(); +} + +function showNewMailNotification({ title, body, count, messageId, accountId, folder, message } = {}) { + if (!Notification.isSupported()) { + return { shown: false, reason: 'unsupported' }; + } + + const normalizedTitle = cleanNotificationText(title, 'New mail'); + const normalizedBody = cleanNotificationText(body, 'No subject'); + const notification = new Notification({ + title: normalizedTitle, + body: count > 1 ? `${normalizedBody}\n${count} new messages` : normalizedBody, + icon: getIconPath(), + silent: true, + ...(process.platform !== 'linux' ? { + actions: [ + { type: 'button', text: 'Reply' }, + { type: 'button', text: 'Delete' }, + { type: 'button', text: 'Star' }, + ], + } : {}), + }); + + notification.on('click', () => { + if (messageId) { + sendNativeAction('open-message', { + messageId, + accountId, + folder, + message, + }); + return; + } + + showMainWindow(); + }); + notification.on('action', (event, index) => { + const actionIndex = Number.isInteger(index) ? index : event.actionIndex; + if (!messageId) return; + + if (actionIndex === 0) { + sendNativeAction('reply-message', { + messageId, + accountId, + folder, + message, + }); + return; + } + + const action = actionIndex === 1 ? 'delete-message' : actionIndex === 2 ? 'star-message' : null; + if (!action) return; + + notification.close(); + runBackgroundMailAction(action, messageId) + .catch((error) => console.error(`Could not ${action} from desktop notification:`, error)); + }); + notification.show(); + + return { shown: true }; +} + +function notifyCheckingUpdate(verbose) { + if (!verbose) return; + + sendUpdateStatus({ type: 'checking' }); + notifyUpdateStatus({ + title: 'Checking for update', + message: 'Checking for new MailFlow updates.', + }); +} + +function notifyUpdateError(message = UPDATE_ERROR_MESSAGE) { + sendUpdateStatus({ type: 'error', message }); + notifyUpdateStatus({ + title: 'Update Error', + message, + type: 'negative', + }); +} + +function notifyUpToDate(verbose) { + if (!verbose) return; + + sendUpdateStatus({ type: 'up-to-date' }); + notifyUpdateStatus({ + title: 'Up to date', + message: 'Your version of MailFlow is up to date.', + type: 'positive', + }); +} + +function notifyUpdateAvailable(verbose = true) { + sendUpdateStatus({ + type: 'available', + data: { + releaseNotes: updateInfo.releaseNotes, + releaseName: updateInfo.releaseName, + releaseDate: updateInfo.releaseDate, + updateUrl: updateInfo.updateUrl, + manual: true, + }, + }); + + if (!verbose) return; + + notifyUpdateStatus({ + title: 'Update Available', + message: 'MailFlow is downloading the newest version for you.', + }); +} + +function notifyUpdateDownloaded() { + sendUpdateStatus({ + type: 'downloaded', + data: { + releaseNotes: updateInfo && updateInfo.releaseNotes, + releaseName: updateInfo && updateInfo.releaseName, + releaseDate: updateInfo && updateInfo.releaseDate, + updateUrl: updateInfo && updateInfo.updateUrl, + filePath: downloadedUpdate, + manual: true, + }, + }); + showInAppNotification({ + title: 'Update Ready', + message: 'MailFlow downloaded the update.', + type: 'positive', + actionLabel: 'Install', + action: 'install-update', + persistent: true, + }); +} + +function filePostfix() { + const date = new Date(); + return `${date.getMonth() + 1}.${date.getDate()}-${date.getHours()}.${date.getMinutes()}.${date.getSeconds()}`; +} + +function getUniqueFilename(filename) { + const extension = path.extname(filename); + const file = path.basename(filename, extension); + return `${file} (${filePostfix()})${extension}`; +} + +function setDownloadProgress(window, value) { + try { + if (!window || window.isDestroyed()) return; + window.setProgressBar(value); + } catch { + // Download events can outlive the BrowserWindow they started from. + } +} + +function getLinuxTerminalCommand() { + return getAvailableCommand([ + 'ptyxis', + 'kgx', + 'gnome-terminal', + 'konsole', + 'xterm', + 'x-terminal-emulator', + ]); +} + +function getTerminalArgs(terminal, command, args = []) { + const shellCommand = ['sh', '-lc', 'exec "$@"', 'mailflow-installer', command, ...args]; + if (['ptyxis', 'kgx', 'gnome-terminal'].includes(terminal)) return ['--', ...shellCommand]; + return ['-e', ...shellCommand]; +} + +function launchTerminalCommand(command, args = []) { + const terminal = getLinuxTerminalCommand(); + if (!terminal) { + throw new Error('No supported terminal was found.'); + } + + const child = spawn(terminal, getTerminalArgs(terminal, command, args), { + detached: true, + stdio: 'ignore', + }); + + child.unref(); +} + +function initializeUpdateDownloads(window) { + if (updateDownloadsInitialized) return; + updateDownloadsInitialized = true; + + window.webContents.session.on('will-download', (_event, item) => { + const totalBytes = item.getTotalBytes(); + const filePath = path.join(app.getPath('downloads'), getUniqueFilename(item.getFilename())); + + item.setSavePath(filePath); + + item.on('updated', () => { + if (totalBytes > 0) { + setDownloadProgress(window, item.getReceivedBytes() / totalBytes); + } + }); + + item.on('done', (_event, state) => { + setDownloadProgress(window, -1); + + if (state === 'interrupted') { + dialog.showErrorBox('Download error', `The download of ${item.getFilename()} was interrupted.`); + } + + if (state === 'completed') { + downloadedUpdate = item.getSavePath(); + notifyUpdateDownloaded(); + } + }); + }); +} + +function downloadUpdate(url) { + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.webContents.downloadURL(url); +} + +async function checkForUpdates(verbose = false) { + notifyCheckingUpdate(verbose); + + try { + const release = await requestJson(UPDATE_RELEASE_URL); + const releaseVersion = release.tag_name || release.name; + const installedPackageType = getInstalledLinuxPackageType(); + const installedVersion = getInstalledAppVersion(installedPackageType); + const asset = getUpdateAsset(release); + + if (!isNewerVersion(releaseVersion, installedVersion)) { + notifyUpToDate(verbose); + return { updateAvailable: false }; + } + + if (!asset) { + notifyUpdateError('A MailFlow update is available, but no installer was found for this platform.'); + return { updateAvailable: true, downloadAvailable: false }; + } + + updateInfo = { + releaseNotes: release.body || '', + releaseName: release.name || release.tag_name, + releaseDate: release.published_at, + updateUrl: asset.browser_download_url, + }; + + notifyUpdateAvailable(verbose); + downloadUpdate(asset.browser_download_url); + return { updateAvailable: true, downloadAvailable: true }; + } catch (error) { + console.error('Update check failed:', error); + notifyUpdateError(); + return { updateAvailable: false, error: error.message }; + } +} + +function launchDownloadedUpdate(updatePath) { + if (process.platform === 'win32' && /\.exe$/i.test(updatePath)) { + const child = spawn(updatePath, [], { + detached: true, + stdio: 'ignore', + windowsHide: false, + }); + + child.unref(); + return Promise.resolve(); + } + + if (process.platform === 'linux' && /\.deb$/i.test(updatePath)) { + launchTerminalCommand('sudo', ['dpkg', '--install', updatePath]); + return Promise.resolve(); + } + + if (process.platform === 'linux' && /\.rpm$/i.test(updatePath)) { + const packageInstaller = getAvailableCommand(['dnf', 'dnf5', 'yum']); + if (!packageInstaller) { + throw new Error('No RPM package installer was found.'); + } + + launchTerminalCommand('sudo', [packageInstaller, 'install', updatePath]); + return Promise.resolve(); + } + + return shell.openPath(updatePath).then((error) => { + if (error) throw new Error(error); + }); +} + +function installDownloadedUpdate() { + if (!downloadedUpdate) { + return Promise.resolve({ installed: false, reason: 'missing-download' }); + } + + return new Promise((resolve) => { + fs.access(downloadedUpdate, fs.constants.F_OK, async (error) => { + if (error) { + shell.showItemInFolder(downloadedUpdate); + resolve({ installed: false, reason: 'missing-file' }); + return; + } + + try { + await launchDownloadedUpdate(downloadedUpdate); + isQuitting = true; + setTimeout(() => app.quit(), 500); + resolve({ installed: true }); + } catch (launchError) { + console.error('Could not launch downloaded update:', launchError); + shell.showItemInFolder(downloadedUpdate); + notifyUpdateError('The update was downloaded, but MailFlow could not start the installer.'); + resolve({ installed: false, reason: 'launch-failed', error: launchError.message }); + } + }); + }); +} + +function openDownloadedUpdatePath() { + if (!downloadedUpdate) return; + shell.showItemInFolder(downloadedUpdate); +} + +function isMailtoUrl(value) { + return /^mailto:/i.test(String(value || '').trim()); +} + +function parseProtocolUrlArg(args = []) { + return args.find(isMailtoUrl) || null; +} + +function splitMailtoAddresses(value) { + return String(value || '') + .split(',') + .map((item) => item.trim()) + .filter(Boolean); +} + +function appendMailtoAddresses(target, value) { + target.push(...splitMailtoAddresses(value)); +} + +function parseMailtoUrl(url) { + const input = String(url || '').trim(); + if (!isMailtoUrl(input)) return null; + + try { + const parsed = new URL(input); + const composeData = { + to: [], + cc: [], + bcc: [], + subject: '', + body: '', + }; + + appendMailtoAddresses(composeData.to, decodeURIComponent(parsed.pathname || '')); + + for (const [key, value] of parsed.searchParams.entries()) { + const normalizedKey = key.toLowerCase(); + + if (normalizedKey === 'to') appendMailtoAddresses(composeData.to, value); + else if (normalizedKey === 'cc') appendMailtoAddresses(composeData.cc, value); + else if (normalizedKey === 'bcc') appendMailtoAddresses(composeData.bcc, value); + else if (normalizedKey === 'subject') composeData.subject = value; + else if (normalizedKey === 'body') composeData.body = value; + } + + composeData.to = [...new Set(composeData.to)]; + composeData.cc = [...new Set(composeData.cc)]; + composeData.bcc = [...new Set(composeData.bcc)]; + + return composeData; + } catch (error) { + console.error('Could not parse mailto URL:', error); + return null; + } +} + +function sendMailtoAction(url) { + const composeData = parseMailtoUrl(url); + if (!composeData) return false; + + sendNativeAction('new-mail', { composeData, source: 'mailto' }); + return true; +} + +function flushPendingProtocolUrls() { + while (pendingProtocolUrls.length > 0) { + sendMailtoAction(pendingProtocolUrls.shift()); + } +} + +function parseNativeActionArg(args = []) { + const actionArg = args.find((arg) => String(arg).startsWith(NATIVE_ACTION_ARG)); + if (!actionArg) return null; + + const action = actionArg.slice(NATIVE_ACTION_ARG.length); + if (['new-mail', 'sync'].includes(action)) return action; + return null; +} + +function createNativeActionPayload(action, data = {}) { + const payload = { + ...data, + id: nextNativeActionId, + action, + createdAt: Date.now(), + }; + nextNativeActionId += 1; + pendingNativeActions.set(payload.id, payload); + return payload; +} + +function sendNativeAction(action, data = {}) { + if (!action) return; + + const payload = createNativeActionPayload(action, data); + showMainWindow(); + + const dispatchScript = ` + window.dispatchEvent(new CustomEvent('mailflow:native-action', { + detail: ${JSON.stringify(payload)} + })); + window.postMessage({ + type: 'mailflow:native-action', + payload: ${JSON.stringify(payload)} + }, '*'); + `; + + const send = () => { + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.webContents.send(NATIVE_ACTION_CHANNEL, payload); + mainWindow.webContents.executeJavaScript(dispatchScript).catch(() => {}); + }; + + if (!mainWindow || mainWindow.isDestroyed()) return; + + if (mainWindow.webContents.isLoading()) { + mainWindow.webContents.once('did-finish-load', () => { + setTimeout(send, 100); + }); + return; + } + + setTimeout(send, 100); +} + +function nativeActionMenuItems() { + return [ + { + label: 'New Mail', + click: () => sendNativeAction('new-mail'), + }, + { + label: 'Sync', + click: () => sendNativeAction('sync'), + }, + ]; +} + +function changeMailFlowHost() { + clearHost(); + showMainWindow(); + loadSetup(); +} + +function fileMenuItems() { + return [ + { + label: 'Change MailFlow Host', + accelerator: 'CmdOrCtrl+,', + click: changeMailFlowHost, + }, + ]; +} + +function editMenuItems() { + return [ + { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, + { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, + { type: 'separator' }, + { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, + { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, + { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, + { label: 'Paste and Match Style', accelerator: 'Shift+CmdOrCtrl+V', role: 'pasteAndMatchStyle' }, + { label: 'Delete', role: 'delete' }, + { type: 'separator' }, + { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }, + ]; +} + +function viewMenuItems() { + return [ + { + label: 'Reload', + accelerator: 'CmdOrCtrl+R', + click(_item, focusedWindow) { + if (focusedWindow) focusedWindow.reload(); + }, + }, + { + label: 'Toggle Full Screen', + accelerator: process.platform === 'darwin' ? 'Ctrl+Command+F' : 'F11', + click(_item, focusedWindow) { + if (!focusedWindow) return; + focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); + }, + }, + ]; +} + +function windowMenuItems() { + if (process.platform === 'darwin') { + return [ + { label: 'Close', accelerator: 'CmdOrCtrl+W', role: 'close' }, + { label: 'Minimize', accelerator: 'CmdOrCtrl+M', role: 'minimize' }, + { label: 'Zoom', role: 'zoom' }, + { type: 'separator' }, + { label: 'Bring All to Front', role: 'front' }, + ]; + } + + return [ + { label: 'Minimize', accelerator: 'CmdOrCtrl+M', role: 'minimize' }, + { label: 'Close', accelerator: 'CmdOrCtrl+W', role: 'close' }, + ]; +} + +function helpMenuItems() { + return [ + { + label: 'Learn More', + click: () => shell.openExternal('https://mailflow.sh'), + }, + { type: 'separator' }, + { + label: 'Help', + click: () => shell.openExternal('https://mailflow.sh/docs'), + }, + { + label: 'Report Issue', + click: () => shell.openExternal('https://github.com/maathimself/mailflow/issues'), + }, + { type: 'separator' }, + { + label: 'Check For Updates', + click: () => checkForUpdates(true), + }, + ]; +} + +function buildDarwinMenuTemplate() { + const name = app.name; + + return [ + { + label: name, + submenu: [ + { label: `About ${name}`, role: 'about' }, + { type: 'separator' }, + { + label: 'Preferences', + accelerator: 'Command+,', + click: changeMailFlowHost, + }, + { label: 'Services', role: 'services', submenu: [] }, + { type: 'separator' }, + { label: `Hide ${name}`, accelerator: 'Command+H', role: 'hide' }, + { label: 'Hide Others', accelerator: 'Command+Alt+H', role: 'hideOthers' }, + { label: 'Show All', role: 'unhide' }, + { type: 'separator' }, + { label: `Quit ${name}`, accelerator: 'Command+Q', role: 'quit' }, + ], + }, + { + label: 'File', + id: 'file', + submenu: fileMenuItems(), + }, + { + label: 'Edit', + submenu: editMenuItems(), + }, + { + label: 'View', + submenu: viewMenuItems(), + }, + { + label: 'Window', + role: 'window', + submenu: windowMenuItems(), + }, + { + label: 'Help', + role: 'help', + submenu: helpMenuItems(), + }, + ]; +} + +function buildDefaultMenuTemplate() { + return [ + { + label: 'File', + id: 'file', + submenu: [ + ...fileMenuItems(), + { type: 'separator' }, + { label: 'Exit', role: 'quit' }, + ], + }, + { + label: 'Edit', + submenu: editMenuItems(), + }, + { + label: 'View', + submenu: viewMenuItems(), + }, + { + label: 'Window', + role: 'window', + submenu: windowMenuItems(), + }, + { + label: 'Help', + role: 'help', + submenu: helpMenuItems(), + }, + ]; +} + +function setupMenu() { + const template = process.platform === 'darwin' + ? buildDarwinMenuTemplate() + : buildDefaultMenuTemplate(); + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +function getDefaultWindowBounds() { + return { + width: 1280, + height: 860, + minWidth: 900, + minHeight: 620, + }; +} + +function getSavedWindowBounds() { + const bounds = readConfig().windowBounds; + if (!bounds || typeof bounds !== 'object') return {}; + + const numericBounds = {}; + for (const key of ['x', 'y', 'width', 'height']) { + if (Number.isFinite(bounds[key])) numericBounds[key] = bounds[key]; + } + + return numericBounds; +} + +function saveWindowBounds() { + if (!mainWindow || mainWindow.isDestroyed()) return; + + writeConfig({ + ...readConfig(), + windowBounds: mainWindow.getBounds(), + }); +} + +function showMainWindow({ reload = false } = {}) { + if (!mainWindow || mainWindow.isDestroyed()) { + createWindow(); + return; + } + + const wasHidden = !mainWindow.isVisible(); + + if (mainWindow.isMinimized()) mainWindow.restore(); + if (!mainWindow.isVisible()) mainWindow.show(); + mainWindow.focus(); + + if (reload && wasHidden) { + mainWindow.webContents.reload(); + } +} + +function getTrayIcon() { + const trayIconPath = process.platform === 'win32' + ? path.join(__dirname, 'icons', 'icon.ico') + : process.platform === 'darwin' + ? path.join(__dirname, 'icons', 'icon.icns') + : path.join(__dirname, 'icons', '96x96.png'); + + return nativeImage.createFromPath(trayIconPath); +} + +function refreshTrayMenu() { + if (!tray) return; + + const isWindowVisible = !!mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible(); + tray.setContextMenu(Menu.buildFromTemplate([ + ...nativeActionMenuItems(), + { type: 'separator' }, + { + label: isWindowVisible ? 'Hide MailFlow' : 'Show MailFlow', + click: () => { + if (isWindowVisible) { + saveWindowBounds(); + mainWindow.hide(); + } else { + showMainWindow({ reload: true }); + } + }, + }, + { type: 'separator' }, + { + label: 'Change MailFlow Host', + click: () => { + clearHost(); + showMainWindow(); + loadSetup(); + }, + }, + { type: 'separator' }, + { + label: 'Quit', + click: () => app.quit(), + }, + ])); +} + +function createTray() { + if (tray) return; + + const trayIcon = getTrayIcon(); + if (trayIcon.isEmpty()) return; + + tray = new Tray(trayIcon); + tray.setToolTip('MailFlow'); + tray.on('click', () => { + refreshTrayMenu(); + showMainWindow({ reload: true }); + }); + refreshTrayMenu(); +} + +function setupDockMenu() { + if (process.platform !== 'darwin' || !app.dock) return; + app.dock.setMenu(Menu.buildFromTemplate(nativeActionMenuItems())); +} + +function setupTaskbarTasks() { + if (process.platform !== 'win32') return; + + app.setUserTasks([ + { + program: process.execPath, + arguments: `${NATIVE_ACTION_ARG}new-mail`, + iconPath: getWindowIconPath(), + iconIndex: 0, + title: 'New Mail', + description: 'Compose a new MailFlow message', + }, + { + program: process.execPath, + arguments: `${NATIVE_ACTION_ARG}sync`, + iconPath: getWindowIconPath(), + iconIndex: 0, + title: 'Sync', + description: 'Sync MailFlow mail', + }, + ]); +} + +function createWindow() { + mainWindow = new BrowserWindow({ + ...getDefaultWindowBounds(), + ...getSavedWindowBounds(), + show: false, + title: 'MailFlow', + icon: getWindowIconPath(), + webPreferences: { + preload: path.join(__dirname, 'preload.cjs'), + backgroundThrottling: false, + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + }, + }); + + mainWindow.once('ready-to-show', () => { + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.show(); + }); + + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: 'deny' }; + }); + + mainWindow.webContents.on('did-fail-load', (_event, _errorCode, _errorDescription, validatedURL, isMainFrame) => { + if (!isMainFrame) return; + const host = readHost(); + if (!host || !String(validatedURL || '').startsWith(host)) return; + loadHostUnavailable(); + }); + + mainWindow.webContents.session.webRequest.onCompleted((details) => { + if (!mainWindow || mainWindow.isDestroyed()) return; + if (details.webContentsId !== mainWindow.webContents.id) return; + if (details.resourceType !== 'mainFrame') return; + if (!HOST_UNAVAILABLE_STATUS_CODES.has(details.statusCode)) return; + + const host = readHost(); + if (!host || !String(details.url || '').startsWith(host)) return; + + setTimeout(() => loadHostUnavailable(), 0); + }); + + mainWindow.webContents.on('did-finish-load', () => { + detectRewriteErrorPage(); + }); + + mainWindow.on('close', (event) => { + if (!isQuitting && tray) { + event.preventDefault(); + saveWindowBounds(); + mainWindow.hide(); + refreshTrayMenu(); + return; + } + + saveWindowBounds(); + }); + + mainWindow.on('show', refreshTrayMenu); + mainWindow.on('hide', refreshTrayMenu); + mainWindow.on('closed', () => { + mainWindow = null; + refreshTrayMenu(); + }); + + initializeUpdateDownloads(mainWindow); + + const host = readHost(); + if (host) { + mainWindow.loadURL(host); + } else { + loadSetup(); + } +} + +function loadSetup() { + if (!mainWindow) return; + mainWindow.loadFile(path.join(__dirname, '..', 'native-shell', 'index.html')); +} + +function loadHostUnavailable() { + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.loadFile(path.join(__dirname, '..', 'native-shell', 'host-unavailable.html')); +} + +function detectRewriteErrorPage() { + if (!mainWindow || mainWindow.isDestroyed()) return; + const currentUrl = mainWindow.webContents.getURL(); + const host = readHost(); + if (!host || !currentUrl.startsWith(host)) return; + + mainWindow.webContents.executeJavaScript('document.body ? document.body.innerText : ""', true) + .then((text) => { + if (!REWRITE_ERROR_PATTERNS.some((pattern) => pattern.test(String(text || '')))) return; + loadHostUnavailable(); + }) + .catch(() => {}); +} + +function scheduleStartupUpdateCheck() { + if (!mainWindow || mainWindow.isDestroyed()) return; + if (!readHost()) return; + + const check = () => { + setTimeout(() => { + if (!mainWindow || mainWindow.isDestroyed()) return; + checkForUpdates(false); + }, 5000); + }; + + if (mainWindow.webContents.isLoading()) { + mainWindow.webContents.once('did-finish-load', check); + return; + } + + check(); +} + +ipcMain.handle('mailflow:getHost', () => readHost()); + +ipcMain.handle('mailflow:saveHost', async (_event, host) => { + const normalized = writeHost(host); + return normalized; +}); + +ipcMain.handle('mailflow:resetHost', () => { + clearHost(); + loadSetup(); +}); + +ipcMain.handle('mailflow:badge:set-unread-count', (_event, count) => { + const unreadCount = Math.max(0, Number.parseInt(count, 10) || 0); + return setUnreadBadgeCount(unreadCount); +}); + +ipcMain.handle('mailflow:notification:new-mail', (_event, notification) => { + return showNewMailNotification(notification); +}); + +ipcMain.handle('mailflow:updates:check', async (_event, { verbose } = {}) => { + return checkForUpdates(verbose); +}); + +ipcMain.handle('mailflow:updates:install-downloaded', () => { + return installDownloadedUpdate(); +}); + +ipcMain.handle('mailflow:updates:install-auto', () => { + return installDownloadedUpdate(); +}); + +ipcMain.handle('mailflow:updates:open-download', () => { + openDownloadedUpdatePath(); +}); + +ipcMain.handle('mailflow:native-actions:pending', () => { + return Array.from(pendingNativeActions.values()); +}); + +ipcMain.handle('mailflow:native-actions:ack', (_event, id) => { + pendingNativeActions.delete(id); +}); + +const gotSingleInstanceLock = app.requestSingleInstanceLock(); + +if (!gotSingleInstanceLock) { + app.quit(); +} else { + app.whenReady().then(() => { + registerMailtoProtocol(); + setupMenu(); + setupDockMenu(); + setupTaskbarTasks(); + createTray(); + createWindow(); + scheduleStartupUpdateCheck(); + sendNativeAction(parseNativeActionArg(process.argv)); + sendMailtoAction(parseProtocolUrlArg(process.argv)); + flushPendingProtocolUrls(); + + app.on('activate', () => { + showMainWindow(); + }); + }); + + app.on('second-instance', (_event, args) => { + const mailtoUrl = parseProtocolUrlArg(args); + if (mailtoUrl) { + sendMailtoAction(mailtoUrl); + return; + } + + showMainWindow(); + sendNativeAction(parseNativeActionArg(args)); + }); +} + +app.on('before-quit', () => { + isQuitting = true; +}); + +app.on('open-url', (event, url) => { + event.preventDefault(); + + if (mainWindow) { + sendMailtoAction(url); + return; + } + + pendingProtocolUrls.push(url); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); +}); diff --git a/frontend/packages/electron/preload.cjs b/frontend/packages/electron/preload.cjs new file mode 100644 index 00000000..2228c93a --- /dev/null +++ b/frontend/packages/electron/preload.cjs @@ -0,0 +1,53 @@ +const { contextBridge, ipcRenderer } = require('electron'); +const pendingNativeActions = []; +const nativeActionSubscribers = new Set(); + +function subscribe(channel, callback) { + const listener = (_event, payload) => callback(payload); + ipcRenderer.on(channel, listener); + return () => ipcRenderer.removeListener(channel, listener); +} + +ipcRenderer.on('mailflow:native-action', (_event, payload) => { + if (nativeActionSubscribers.size === 0) { + pendingNativeActions.push(payload); + return; + } + + nativeActionSubscribers.forEach((callback) => callback(payload)); +}); + +function subscribeNativeAction(callback) { + nativeActionSubscribers.add(callback); + + while (pendingNativeActions.length > 0) { + callback(pendingNativeActions.shift()); + } + + return () => nativeActionSubscribers.delete(callback); +} + +contextBridge.exposeInMainWorld('mailflowNative', { + getHost: () => ipcRenderer.invoke('mailflow:getHost'), + saveHost: (host) => ipcRenderer.invoke('mailflow:saveHost', host), + resetHost: () => ipcRenderer.invoke('mailflow:resetHost'), + badges: { + setUnreadCount: (count) => ipcRenderer.invoke('mailflow:badge:set-unread-count', count), + }, + updates: { + check: (verbose) => ipcRenderer.invoke('mailflow:updates:check', { verbose }), + installDownloaded: () => ipcRenderer.invoke('mailflow:updates:install-downloaded'), + installAuto: () => ipcRenderer.invoke('mailflow:updates:install-auto'), + openDownload: () => ipcRenderer.invoke('mailflow:updates:open-download'), + onStatus: (callback) => subscribe('mailflow:updates:status', callback), + }, + notifications: { + onPush: (callback) => subscribe('mailflow:notifications:push', callback), + showNewMail: (notification) => ipcRenderer.invoke('mailflow:notification:new-mail', notification), + }, + actions: { + getPending: () => ipcRenderer.invoke('mailflow:native-actions:pending'), + ack: (id) => ipcRenderer.invoke('mailflow:native-actions:ack', id), + onAction: subscribeNativeAction, + }, +}); diff --git a/frontend/packages/native-shell/host-unavailable.html b/frontend/packages/native-shell/host-unavailable.html new file mode 100644 index 00000000..9eba9a46 --- /dev/null +++ b/frontend/packages/native-shell/host-unavailable.html @@ -0,0 +1,106 @@ + + + + + + MailFlow Host Unavailable + + + +
+ MailFlow +

Host is not available

+

MailFlow could not reach the configured server. Check the host address or choose a different MailFlow server.

+ +
+ + + + diff --git a/frontend/packages/native-shell/index.html b/frontend/packages/native-shell/index.html new file mode 100644 index 00000000..340befb2 --- /dev/null +++ b/frontend/packages/native-shell/index.html @@ -0,0 +1,232 @@ + + + + + + MailFlow Setup + + + +
+

Connect MailFlow

+

Enter the host for your MailFlow server.

+ +
+ + + +
+
+ + + + diff --git a/frontend/packages/package.json b/frontend/packages/package.json new file mode 100644 index 00000000..046ea70c --- /dev/null +++ b/frontend/packages/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "name": "mailflow-native-packages" +} diff --git a/frontend/packages/scripts/build-android.cjs b/frontend/packages/scripts/build-android.cjs new file mode 100644 index 00000000..c11f9da9 --- /dev/null +++ b/frontend/packages/scripts/build-android.cjs @@ -0,0 +1,26 @@ +const { spawnSync } = require('child_process'); +const path = require('path'); + +const root = path.join(__dirname, '..', '..'); +const androidDir = path.join(root, 'packages', 'android'); +const gradle = process.platform === 'win32' ? 'gradlew.bat' : 'sh'; +const args = process.platform === 'win32' + ? [':app:assembleRelease'] + : ['gradlew', ':app:assembleRelease']; + +const result = spawnSync(gradle, args, { + cwd: androidDir, + stdio: 'inherit', + shell: process.platform === 'win32', +}); + +if (result.error) { + console.error(result.error); + process.exit(1); +} + +if (typeof result.status === 'number') { + process.exit(result.status); +} + +process.exit(1); diff --git a/frontend/packages/scripts/collect-android-artifacts.cjs b/frontend/packages/scripts/collect-android-artifacts.cjs new file mode 100644 index 00000000..adce3d4a --- /dev/null +++ b/frontend/packages/scripts/collect-android-artifacts.cjs @@ -0,0 +1,55 @@ +const fs = require('fs'); +const path = require('path'); + +const root = path.join(__dirname, '..', '..'); +const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +const outputsDir = path.join(root, 'packages', 'android', 'app', 'build', 'outputs'); +const releaseDir = path.join(root, 'packages', 'release'); + +if (!fs.existsSync(outputsDir)) { + throw new Error(`Android output directory not found: ${outputsDir}`); +} + +const outputFiles = listFiles(outputsDir); +const apks = outputFiles.filter((file) => { + const normalized = file.replace(/\\/g, '/').toLowerCase(); + return normalized.endsWith('.apk') && normalized.includes('/release/') && !normalized.endsWith('-unsigned.apk'); +}); + +if (apks.length === 0) { + const unsignedApks = outputFiles.filter((file) => { + const normalized = file.replace(/\\/g, '/').toLowerCase(); + return normalized.endsWith('-unsigned.apk') && normalized.includes('/release/'); + }); + const found = outputFiles + .map((file) => path.relative(root, file)) + .join('\n '); + const unsignedHint = unsignedApks.length > 0 + ? '\nUnsigned release APKs were found, but Android cannot install them. Configure release signing or rebuild with the updated Gradle fallback.' + : ''; + throw new Error(`No signed Android release APK files found under ${outputsDir}.${unsignedHint}${found ? ` Found:\n ${found}` : ''}`); +} + +fs.mkdirSync(releaseDir, { recursive: true }); + +for (const apk of apks) { + const suffix = apks.length === 1 ? '' : `-${path.basename(apk, '.apk')}`; + const target = path.join(releaseDir, `MailFlow-${packageJson.version}${suffix}.apk`); + fs.copyFileSync(apk, target); + console.log(`Copied ${path.relative(root, apk)} -> ${path.relative(root, target)}`); +} + +function listFiles(dir) { + const files = []; + + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...listFiles(fullPath)); + } else if (entry.isFile()) { + files.push(fullPath); + } + } + + return files; +} diff --git a/frontend/packages/scripts/prepare-electron-icons.cjs b/frontend/packages/scripts/prepare-electron-icons.cjs new file mode 100644 index 00000000..d79be0ef --- /dev/null +++ b/frontend/packages/scripts/prepare-electron-icons.cjs @@ -0,0 +1,95 @@ +const fs = require('fs'); +const path = require('path'); + +const rootDir = path.join(__dirname, '..', '..'); +const publicDir = path.join(rootDir, 'public'); +const iconDir = path.join(rootDir, 'packages', 'electron', 'icons'); + +const pngBySize = new Map( + [32, 72, 96, 128, 144, 152, 192, 384, 512] + .map((size) => [size, path.join(publicDir, `icon-${size}.png`)]) + .filter(([, file]) => fs.existsSync(file)) +); + +if (!pngBySize.has(512)) { + throw new Error('Expected frontend/public/icon-512.png to exist.'); +} + +fs.mkdirSync(iconDir, { recursive: true }); + +fs.copyFileSync(pngBySize.get(512), path.join(iconDir, 'icon.png')); +for (const [size, file] of pngBySize) { + fs.copyFileSync(file, path.join(iconDir, `${size}x${size}.png`)); +} + +writeIco(path.join(iconDir, 'icon.ico'), [512, 128]); +writeIcns(path.join(iconDir, 'icon.icns'), [ + ['ic07', 128], + ['ic09', 512], +]); + +console.log(`Prepared Electron icons in ${path.relative(rootDir, iconDir)}`); + +function writeIco(outFile, sizes) { + const images = sizes + .filter((size) => pngBySize.has(size)) + .map((size) => ({ + size, + data: fs.readFileSync(pngBySize.get(size)), + })); + + if (images.length === 0) { + throw new Error('No suitable PNG files were found for icon.ico.'); + } + + const headerSize = 6; + const entrySize = 16; + let offset = headerSize + images.length * entrySize; + const header = Buffer.alloc(offset); + + header.writeUInt16LE(0, 0); + header.writeUInt16LE(1, 2); + header.writeUInt16LE(images.length, 4); + + images.forEach((image, index) => { + const entryOffset = headerSize + index * entrySize; + header.writeUInt8(image.size >= 256 ? 0 : image.size, entryOffset); + header.writeUInt8(image.size >= 256 ? 0 : image.size, entryOffset + 1); + header.writeUInt8(0, entryOffset + 2); + header.writeUInt8(0, entryOffset + 3); + header.writeUInt16LE(1, entryOffset + 4); + header.writeUInt16LE(32, entryOffset + 6); + header.writeUInt32LE(image.data.length, entryOffset + 8); + header.writeUInt32LE(offset, entryOffset + 12); + offset += image.data.length; + }); + + fs.writeFileSync(outFile, Buffer.concat([header, ...images.map((image) => image.data)])); +} + +function writeIcns(outFile, entries) { + const chunks = entries.map(([type, size]) => { + const data = readBestPng(size); + const header = Buffer.alloc(8); + header.write(type, 0, 4, 'ascii'); + header.writeUInt32BE(data.length + 8, 4); + return Buffer.concat([header, data]); + }); + + const header = Buffer.alloc(8); + header.write('icns', 0, 4, 'ascii'); + header.writeUInt32BE(chunks.reduce((total, chunk) => total + chunk.length, 8), 4); + + fs.writeFileSync(outFile, Buffer.concat([header, ...chunks])); +} + +function readBestPng(targetSize) { + const exact = pngBySize.get(targetSize); + if (exact) return fs.readFileSync(exact); + + const fallbackSize = [...pngBySize.keys()] + .filter((size) => size >= targetSize) + .sort((a, b) => a - b)[0] ?? 512; + + return fs.readFileSync(pngBySize.get(fallbackSize)); +} diff --git a/frontend/packages/scripts/prepare-linux-metainfo.cjs b/frontend/packages/scripts/prepare-linux-metainfo.cjs new file mode 100644 index 00000000..bb849e94 --- /dev/null +++ b/frontend/packages/scripts/prepare-linux-metainfo.cjs @@ -0,0 +1,55 @@ +const fs = require('fs'); +const path = require('path'); + +const root = path.join(__dirname, '..', '..'); +const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +const outputDir = path.join(root, 'packages', 'electron', 'metainfo'); +const outputPath = path.join(outputDir, 'sh.mailflow.app.metainfo.xml'); +const packageTypeDir = path.join(root, 'packages', 'electron', 'package-type'); + +function escapeXml(value) { + return String(value || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +const releaseDate = new Date().toISOString().slice(0, 10); +const productName = packageJson.productName || 'MailFlow'; +const description = packageJson.description || 'A self-hosted, unified webmail client.'; +const homepage = packageJson.homepage || 'https://mailflow.sh'; +const license = packageJson.license || 'GPL-3.0'; + +const metainfo = ` + + sh.mailflow.app + CC0-1.0 + ${escapeXml(license)} + ${escapeXml(productName)} + ${escapeXml(description)} + ${escapeXml(packageJson.author && packageJson.author.name ? packageJson.author.name : productName)} + ${escapeXml(homepage)} + ${escapeXml(packageJson.bugs && packageJson.bugs.url ? packageJson.bugs.url : `${homepage}/docs`)} + MailFlow.desktop + + Network + Email + + +

${escapeXml(description)}

+
+ + + + +
+`; + +fs.mkdirSync(outputDir, { recursive: true }); +fs.writeFileSync(outputPath, metainfo); +fs.mkdirSync(packageTypeDir, { recursive: true }); +fs.writeFileSync(path.join(packageTypeDir, 'deb'), 'deb\n'); +fs.writeFileSync(path.join(packageTypeDir, 'rpm'), 'rpm\n'); +console.log(`Prepared Linux AppStream metadata at ${path.relative(root, outputPath)}.`); diff --git a/frontend/packages/scripts/prepare-native-shell.cjs b/frontend/packages/scripts/prepare-native-shell.cjs new file mode 100644 index 00000000..53f5c2f2 --- /dev/null +++ b/frontend/packages/scripts/prepare-native-shell.cjs @@ -0,0 +1,17 @@ +const fs = require('fs'); +const path = require('path'); + +const root = path.join(__dirname, '..', '..'); +const shellIndex = path.join(root, 'packages', 'native-shell', 'index.html'); +const shellHostUnavailable = path.join(root, 'packages', 'native-shell', 'host-unavailable.html'); +const distIndex = path.join(root, 'dist', 'index.html'); +const distHostUnavailable = path.join(root, 'dist', 'host-unavailable.html'); + +fs.copyFileSync(shellIndex, distIndex); + +const hostUnavailableHtml = fs + .readFileSync(shellHostUnavailable, 'utf8') + .replace('../electron/icons/512x512.png', 'icon-512.png'); +fs.writeFileSync(distHostUnavailable, hostUnavailableHtml); + +console.log('Prepared native shell in dist/index.html and dist/host-unavailable.html'); diff --git a/frontend/packages/scripts/rename-linux-artifacts.cjs b/frontend/packages/scripts/rename-linux-artifacts.cjs new file mode 100644 index 00000000..a241f9e2 --- /dev/null +++ b/frontend/packages/scripts/rename-linux-artifacts.cjs @@ -0,0 +1,38 @@ +const fs = require('fs'); +const path = require('path'); + +const releaseDir = path.join(__dirname, '..', 'release'); + +if (!fs.existsSync(releaseDir)) { + console.log('No release directory found; skipping Linux artifact rename.'); + process.exit(0); +} + +const replacements = [ + [/^(.+)-x64\.AppImage$/, '$1.AppImage'], + [/^(.+)-x64\.deb$/, '$1-amd64.deb'], + [/^(.+)-arm64\.deb$/, '$1-arm64.deb'], + [/^(.+)-x64\.rpm$/, '$1-x86_64.rpm'], + [/^(.+)-arm64\.rpm$/, '$1-aarch64.rpm'], +]; + +for (const entry of fs.readdirSync(releaseDir)) { + if (/^(.+)-arm64\.AppImage$/.test(entry)) { + fs.rmSync(path.join(releaseDir, entry), { force: true }); + console.log(`Removed unrequested Linux artifact ${entry}`); + continue; + } + + for (const [pattern, replacement] of replacements) { + if (!pattern.test(entry)) continue; + + const renamed = entry.replace(pattern, replacement); + if (renamed === entry) break; + + const from = path.join(releaseDir, entry); + const to = path.join(releaseDir, renamed); + fs.renameSync(from, to); + console.log(`Renamed ${entry} -> ${renamed}`); + break; + } +} diff --git a/frontend/packages/scripts/set-app-version.cjs b/frontend/packages/scripts/set-app-version.cjs new file mode 100644 index 00000000..824884e8 --- /dev/null +++ b/frontend/packages/scripts/set-app-version.cjs @@ -0,0 +1,58 @@ +const fs = require('fs'); +const path = require('path'); + +const root = path.join(__dirname, '..', '..'); +const requestedVersion = process.argv[2] || process.env.APP_VERSION || process.env.GITHUB_REF_NAME; +const requestedCode = process.argv[3] || process.env.APP_VERSION_CODE || process.env.GITHUB_RUN_NUMBER; + +function normalizeVersion(value) { + if (!value) return null; + + const normalized = String(value).trim().replace(/^refs\/tags\//, '').replace(/^v[.]?/, ''); + if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(normalized)) { + return null; + } + + return normalized; +} + +function normalizeVersionCode(value) { + const parsed = Number.parseInt(value, 10); + return Number.isInteger(parsed) && parsed > 0 ? parsed : null; +} + +function updateJsonVersion(filePath, version) { + const json = JSON.parse(fs.readFileSync(filePath, 'utf8')); + json.version = version; + if (json.packages && json.packages['']) { + json.packages[''].version = version; + } + fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`); +} + +const version = normalizeVersion(requestedVersion); +if (!version) { + if (requestedVersion) { + console.error(`Invalid release tag version "${requestedVersion}". Expected a semver tag like v1.2.3.`); + process.exit(1); + } + + console.log('No release tag version detected; keeping package versions unchanged.'); + process.exit(0); +} + +updateJsonVersion(path.join(root, 'package.json'), version); +updateJsonVersion(path.join(root, 'package-lock.json'), version); + +const buildGradlePath = path.join(root, 'packages', 'android', 'app', 'build.gradle'); +let buildGradle = fs.readFileSync(buildGradlePath, 'utf8'); +buildGradle = buildGradle.replace(/versionName\s+"[^"]+"/, `versionName "${version}"`); + +const versionCode = normalizeVersionCode(requestedCode); +if (versionCode) { + buildGradle = buildGradle.replace(/versionCode\s+\d+/, `versionCode ${versionCode}`); +} + +fs.writeFileSync(buildGradlePath, buildGradle); + +console.log(`Prepared app package version ${version}${versionCode ? ` (${versionCode})` : ''}.`); diff --git a/frontend/packages/scripts/sync-android.cjs b/frontend/packages/scripts/sync-android.cjs new file mode 100644 index 00000000..1a4c9623 --- /dev/null +++ b/frontend/packages/scripts/sync-android.cjs @@ -0,0 +1,20 @@ +const { spawnSync } = require('child_process'); +const path = require('path'); + +const root = path.join(__dirname, '..', '..'); +const packagesDir = path.join(root, 'packages'); +const cap = path.join( + root, + 'node_modules', + '@capacitor', + 'cli', + 'bin', + 'capacitor', +); + +const result = spawnSync(process.execPath, [cap, 'sync', 'android'], { + cwd: packagesDir, + stdio: 'inherit', +}); + +process.exit(result.status || 0); diff --git a/frontend/src/components/ElectronNotificationBridge.jsx b/frontend/src/components/ElectronNotificationBridge.jsx new file mode 100644 index 00000000..bae683c5 --- /dev/null +++ b/frontend/src/components/ElectronNotificationBridge.jsx @@ -0,0 +1,264 @@ +import { useEffect, useRef } from 'react'; +import { useStore } from '../store/index.js'; +import { api } from '../utils/api.js'; +import { installCapacitorNativeBridge } from '../utils/capacitorNativeBridge.js'; + +export default function ElectronNotificationBridge() { + installCapacitorNativeBridge(); + + const addNotification = useStore(state => state.addNotification); + const openCompose = useStore(state => state.openCompose); + const setSelectedAccount = useStore(state => state.setSelectedAccount); + const setSelectedMessage = useStore(state => state.setSelectedMessage); + const setSearchQuery = useStore(state => state.setSearchQuery); + const totalUnread = useStore(state => state.unreadCounts.total); + const lastActionRef = useRef({ action: null, time: 0 }); + const processedActionIdsRef = useRef(new Set()); + + useEffect(() => { + window.__mailflowNativeBridgeReady = true; + + return () => { + window.__mailflowNativeBridgeReady = false; + }; + }, []); + + useEffect(() => { + window.mailflowNative?.badges?.setUnreadCount?.(totalUnread || 0); + }, [totalUnread]); + + useEffect(() => { + const unsubscribe = window.mailflowNative?.notifications?.onPush?.((notification) => { + addNotification({ + type: notification.type === 'negative' ? 'error' : notification.type, + title: notification.title, + body: notification.body || notification.message, + }); + }); + + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + }; + }, [addNotification]); + + useEffect(() => { + const unsubscribe = window.mailflowNative?.updates?.onStatus?.((status) => { + if (status?.type !== 'downloaded') return; + + addNotification({ + type: 'success', + title: 'Update ready', + body: 'MailFlow downloaded the update.', + allowWrap: true, + persistent: true, + actionLabel: 'Install', + onAction: async () => { + const result = await window.mailflowNative?.updates?.installDownloaded?.(); + if (result && result.installed === false) { + addNotification({ + type: 'error', + title: 'Install failed', + body: 'The update was downloaded, but the installer could not be started.', + }); + } + }, + }); + }); + + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + }; + }, [addNotification]); + + useEffect(() => { + if (window.mailflowNative?.platform !== 'android') return; + window.mailflowNative?.updates?.check?.(false)?.catch?.(() => {}); + }, []); + + useEffect(() => { + const getPayloadMessage = (payload) => { + const state = useStore.getState(); + return payload?.message || state.messages.find((item) => item.id === payload?.messageId) || null; + }; + + const openMessageFromPayload = (payload) => { + const messageId = payload?.messageId; + if (!messageId) return null; + + const folder = payload.folder || 'INBOX'; + const message = getPayloadMessage(payload); + const state = useStore.getState(); + + setSearchQuery(''); + if (payload.accountId) { + setSelectedAccount(payload.accountId, folder); + } + + if (message && !state.messages.some((item) => item.id === message.id)) { + useStore.setState((current) => ({ + messages: [{ ...message, account_id: message.account_id || payload.accountId }, ...current.messages], + })); + } + + window.dispatchEvent(new CustomEvent('mailflow:refresh')); + window.setTimeout(() => setSelectedMessage(messageId), 0); + return message; + }; + + const normalizeAddressList = (value) => { + if (Array.isArray(value)) return value; + try { + const parsed = JSON.parse(value || '[]'); + return Array.isArray(parsed) ? parsed : []; + } catch (_) { + return []; + } + }; + + const openReplyFromPayload = (payload) => { + const message = getPayloadMessage(payload); + if (!message) return; + + const replyTo = normalizeAddressList(message.reply_to ?? message.replyTo); + const replyTarget = replyTo[0]?.email + ? replyTo[0] + : { + name: message.from_name || message.fromName || '', + email: message.from_email || message.fromEmail || '', + }; + const sender = replyTarget.email ? [replyTarget] : []; + const rawSubject = (message.subject || '').trim(); + const subject = rawSubject.startsWith('Re:') ? rawSubject : rawSubject ? `Re: ${rawSubject}` : 'Re:'; + const originalMessageId = message.message_id || message.messageId; + const priorInReplyTo = message.in_reply_to || message.inReplyTo; + + openCompose({ + to: sender, + cc: [], + subject, + body: '', + inReplyTo: originalMessageId, + references: [priorInReplyTo, originalMessageId].filter(Boolean).join(' ').trim() || null, + accountId: message.account_id || message.accountId || payload.accountId, + isReply: true, + originalFrom: sender, + allRecipients: [], + }); + }; + + const runNativeAction = async (payload) => { + const action = typeof payload === 'string' ? payload : payload?.action; + const id = typeof payload === 'object' ? payload?.id : null; + + if (!action) return; + if (id && processedActionIdsRef.current.has(id)) return; + if (id) processedActionIdsRef.current.add(id); + + const now = Date.now(); + const last = lastActionRef.current; + + if (!id && last.action === action && now - last.time < 500) return; + lastActionRef.current = { action, time: now }; + + try { + if (action === 'new-mail') { + openCompose(payload?.composeData || {}); + return; + } + + if (action === 'open-message') { + openMessageFromPayload(payload); + return; + } + + if (action === 'reply-message') { + openReplyFromPayload(payload); + return; + } + + if (action === 'delete-message') { + const messageId = payload?.messageId; + if (!messageId) return; + + await api.deleteMessage(messageId); + useStore.getState().removeMessage(messageId); + window.dispatchEvent(new CustomEvent('mailflow:refresh')); + return; + } + + if (action === 'star-message') { + const messageId = payload?.messageId; + if (!messageId) return; + + await api.markStarred(messageId, true); + useStore.getState().updateMessage(messageId, { is_starred: true }); + return; + } + + if (action === 'sync') { + try { + addNotification({ + type: 'info', + title: 'Sync started', + body: 'MailFlow is checking for new mail.', + }); + await api.syncNow(); + } catch (error) { + addNotification({ + type: 'error', + title: 'Sync failed', + body: error.message || 'Could not sync mail.', + }); + } + } + } finally { + if (id) { + window.mailflowNative?.actions?.ack?.(id); + } + } + }; + + const handleNativeAction = (event) => { + runNativeAction(event.detail); + }; + + const handleNativeMessage = (event) => { + if (event.source !== window) return; + if (event.data?.type === 'mailflow:native-action') { + runNativeAction(event.data.payload); + } else if (event.data?.type === 'mailflow:native-actions-ready') { + drainInjectedActions(); + } + }; + + const drainInjectedActions = () => { + const actions = Array.isArray(window.__mailflowPendingNativeActions) + ? window.__mailflowPendingNativeActions.splice(0) + : []; + actions.forEach(runNativeAction); + }; + + const unsubscribe = window.mailflowNative?.actions?.onAction?.((payload) => { + runNativeAction(payload); + }); + + window.mailflowNative?.actions?.getPending?.() + .then((actions = []) => { + actions.forEach(runNativeAction); + }) + .catch(() => {}); + + drainInjectedActions(); + window.addEventListener('mailflow:native-action', handleNativeAction); + window.addEventListener('mailflow:native-actions-ready', drainInjectedActions); + window.addEventListener('message', handleNativeMessage); + return () => { + window.removeEventListener('mailflow:native-action', handleNativeAction); + window.removeEventListener('mailflow:native-actions-ready', drainInjectedActions); + window.removeEventListener('message', handleNativeMessage); + if (typeof unsubscribe === 'function') unsubscribe(); + }; + }, [addNotification, openCompose, setSearchQuery, setSelectedAccount, setSelectedMessage]); + + return null; +} diff --git a/frontend/src/components/MailApp.jsx b/frontend/src/components/MailApp.jsx index 87867990..0c6b99cc 100644 --- a/frontend/src/components/MailApp.jsx +++ b/frontend/src/components/MailApp.jsx @@ -12,6 +12,7 @@ import Sidebar from './Sidebar.jsx'; import MessageList from './MessageList.jsx'; import MessagePane from './MessagePane.jsx'; import NotificationToasts from './NotificationToasts.jsx'; +import ElectronNotificationBridge from './ElectronNotificationBridge.jsx'; import CommandPalette from './CommandPalette.jsx'; const ComposeModal = lazy(() => import('./ComposeModal.jsx')); @@ -156,6 +157,7 @@ export default function MailApp() { if (showAppBadge && total > 0) navigator.setAppBadge(total).catch(() => {}); else navigator.clearAppBadge().catch(() => {}); } + window.mailflowNative?.setUnreadCount?.(total).catch(() => {}); }, [unreadCounts, selectedAccountId, showAppBadge, showFaviconBadge]); // ── Global keyboard shortcut listener ────────────────────────────────────── @@ -167,6 +169,53 @@ export default function MailApp() { useEffect(() => { composingRef.current = composing; }, [composing]); useEffect(() => { showAdminRef.current = showAdmin; }, [showAdmin]); + const mobileSidebarOpenRef = useRef(mobileSidebarOpen); + const showShortcutHelpRef = useRef(showShortcutHelp); + const paletteOpenRef = useRef(paletteOpen); + useEffect(() => { mobileSidebarOpenRef.current = mobileSidebarOpen; }, [mobileSidebarOpen]); + useEffect(() => { showShortcutHelpRef.current = showShortcutHelp; }, [showShortcutHelp]); + useEffect(() => { paletteOpenRef.current = paletteOpen; }, [paletteOpen]); + + useEffect(() => { + window.__mailflowHandleAndroidBack = () => { + if (composingRef.current) { + useStore.getState().closeCompose(); + return true; + } + + if (showAdminRef.current) { + setShowAdmin(false); + return true; + } + + if (paletteOpenRef.current) { + setPaletteOpen(false); + return true; + } + + if (showShortcutHelpRef.current) { + setShowShortcutHelp(false); + return true; + } + + if (mobileSidebarOpenRef.current) { + setMobileSidebarOpen(false); + return true; + } + + if (selectedMessageIdRef.current) { + setSelectedMessage(null); + return true; + } + + return false; + }; + + return () => { + if (window.__mailflowHandleAndroidBack) delete window.__mailflowHandleAndroidBack; + }; + }, [setMobileSidebarOpen, setSelectedMessage, setShowAdmin]); + useEffect(() => { if (isMobile) return; const keyMap = buildKeyMap(shortcuts); @@ -402,6 +451,7 @@ export default function MailApp() { {composing && } {showAdmin && } + setPaletteOpen(false)} /> diff --git a/frontend/src/components/NotificationToasts.jsx b/frontend/src/components/NotificationToasts.jsx index 6815b027..9fc83d85 100644 --- a/frontend/src/components/NotificationToasts.jsx +++ b/frontend/src/components/NotificationToasts.jsx @@ -149,6 +149,8 @@ function Toast({ notification, onDismiss, isMobile }) { }; useEffect(() => { + if (notification.persistent) return undefined; + const duration = notification.onUndo ? 6000 : 5000; const timer = setTimeout(dismiss, duration); return () => clearTimeout(timer); @@ -169,7 +171,8 @@ function Toast({ notification, onDismiss, isMobile }) { background: 'var(--bg-elevated)', border: '1px solid var(--border)', borderRadius: 10, padding: '12px 14px', display: 'flex', alignItems: 'flex-start', gap: 10, - maxWidth: isMobile ? '100%' : 340, + width: notification.allowWrap && !isMobile ? 380 : undefined, + maxWidth: notification.allowWrap && !isMobile ? 'calc(100vw - 48px)' : isMobile ? '100%' : 340, boxShadow: 'var(--shadow-popover)', pointerEvents: 'all', }} @@ -198,7 +201,11 @@ function Toast({ notification, onDismiss, isMobile }) {
{notification.body}
diff --git a/frontend/src/hooks/usePushNotifications.js b/frontend/src/hooks/usePushNotifications.js index 7ea98f43..2f3f675a 100644 --- a/frontend/src/hooks/usePushNotifications.js +++ b/frontend/src/hooks/usePushNotifications.js @@ -42,6 +42,9 @@ export function usePushNotifications() { if (!ok) return; setPermission(Notification.permission); + window.mailflowNative?.notifications?.checkPermission?.() + .then((nativePermission) => setPermission(nativePermission || Notification.permission)) + .catch(() => {}); // Check whether the backend has VAPID keys configured before the user // tries to subscribe — lets us show a clear error instead of a cryptic 503. @@ -69,7 +72,9 @@ export function usePushNotifications() { const subscribe = async () => { setLoading(true); try { - const perm = await Notification.requestPermission(); + const perm = window.mailflowNative?.notifications?.requestPermission + ? await window.mailflowNative.notifications.requestPermission() + : await Notification.requestPermission(); setPermission(perm); if (perm !== 'granted') return false; diff --git a/frontend/src/hooks/useWebSocket.js b/frontend/src/hooks/useWebSocket.js index 12da4d1b..ee5a0ef3 100644 --- a/frontend/src/hooks/useWebSocket.js +++ b/frontend/src/hooks/useWebSocket.js @@ -2,6 +2,7 @@ import { useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useStore } from '../store/index.js'; import { api } from '../utils/api.js'; +import { installCapacitorNativeBridge } from '../utils/capacitorNativeBridge.js'; import { playNotificationSound } from '../utils/notificationSounds.js'; import { pendingMarkReadMap } from '../utils/pendingReads.js'; import { updateFaviconBadge } from '../themes.js'; @@ -46,6 +47,19 @@ function _applyServerCounts(counts) { } } +function _forwardNativeNewMailNotification(notification) { + installCapacitorNativeBridge(); + window.mailflowNative?.notifications?.showNewMail?.({ + title: notification.title, + body: notification.body, + count: notification.count, + accountId: notification.accountId, + folder: notification.folder, + messageId: notification.messageId, + message: notification.message, + }).catch(() => {}); +} + // Auth-related close codes that should not trigger reconnect const NO_RECONNECT_CODES = new Set([4001, 4003]); @@ -111,19 +125,28 @@ export function useWebSocket() { const isInbox = !folder || folder === 'INBOX'; if (messages && messages.length > 0) { - // In-app notifications and sounds are inbox-only — non-inbox folder syncs - // (Archive, Spam, on-demand syncs) should not trigger alerts for old mail. - if (isInbox && document.visibilityState === 'visible') { - const latest = messages[0]; - addNotification({ + // New-mail alerts are inbox-only — non-inbox folder syncs (Archive, + // Spam, on-demand syncs) should not trigger alerts for old mail. + if (isInbox) { + const latest = messages[messages.length - 1]; + const notification = { type: 'new_mail', accountId, + folder: folder || 'INBOX', + messageId: latest.id, + message: latest, title: latest.fromName || latest.fromEmail || t('notifications.newMessage'), body: latest.subject || t('common.noSubject'), count, - }); - const { notificationSound, customSoundDataUrl } = useStore.getState(); - playNotificationSound(notificationSound, customSoundDataUrl); + }; + + if (document.visibilityState === 'visible') { + addNotification(notification); + const { notificationSound, customSoundDataUrl } = useStore.getState(); + playNotificationSound(notificationSound, customSoundDataUrl); + } + + _forwardNativeNewMailNotification(notification); } // Refresh the message list when the affected folder is visible diff --git a/frontend/src/utils/capacitorNativeBridge.js b/frontend/src/utils/capacitorNativeBridge.js new file mode 100644 index 00000000..0bf40a98 --- /dev/null +++ b/frontend/src/utils/capacitorNativeBridge.js @@ -0,0 +1,99 @@ +import { Capacitor, registerPlugin } from '@capacitor/core'; + +let installed = false; +let plugin = null; +let pluginUnavailable = false; + +function getPlugin() { + if (plugin) return plugin; + plugin = registerPlugin('MailFlowNative'); + return plugin; +} + +async function callNative(method, args, fallback = null) { + if (pluginUnavailable) return fallback; + + try { + const MailFlowNative = getPlugin(); + return await MailFlowNative[method](args); + } catch (error) { + if (String(error?.message || error).includes('not implemented')) { + pluginUnavailable = true; + } + return fallback; + } +} + +export function installCapacitorNativeBridge() { + if (installed || !Capacitor.isNativePlatform()) return; + + const existingBridge = window.mailflowNative || {}; + + window.mailflowNative = { + ...existingBridge, + platform: 'android', + getHost: async () => { + const result = await callNative('getHost', undefined, {}); + return result?.host || null; + }, + saveHost: async (host) => { + const result = await callNative('saveHost', { host }, { host }); + return result?.host || host; + }, + resetHost: async () => callNative('resetHost'), + badges: { + ...existingBridge.badges, + setUnreadCount: async (count) => callNative('setUnreadCount', { count }), + }, + updates: { + ...existingBridge.updates, + check: async (verbose) => callNative('checkForUpdates', { verbose }), + installDownloaded: async () => callNative('installDownloadedUpdate', undefined, { installed: false, reason: 'unavailable' }), + installAuto: async () => callNative('installDownloadedUpdate', undefined, { installed: false, reason: 'unavailable' }), + openDownload: async () => callNative('openDownloadedUpdate'), + onStatus: (callback) => { + if (pluginUnavailable) return () => {}; + const MailFlowNative = getPlugin(); + const handlePromise = MailFlowNative.addListener('updateStatus', callback).catch(() => null); + return () => { + handlePromise.then((handle) => handle?.remove?.()).catch(() => {}); + }; + }, + }, + notifications: { + ...existingBridge.notifications, + checkPermission: async () => { + const result = await callNative('checkNotificationPermission', undefined, {}); + return result?.permission || 'default'; + }, + requestPermission: async () => { + const result = await callNative('requestNotificationPermission', undefined, {}); + return result?.permission || 'default'; + }, + openSettings: async () => callNative('openNotificationSettings'), + showNewMail: async (notification) => callNative('showNewMail', notification || {}), + }, + actions: { + ...existingBridge.actions, + getPending: async () => { + const result = await callNative('getPendingActions', undefined, {}); + return result?.actions || []; + }, + ack: async (id) => callNative('ackAction', { id }), + onAction: (callback) => { + if (pluginUnavailable) return () => {}; + const MailFlowNative = getPlugin(); + const handlePromise = MailFlowNative.addListener('nativeAction', callback).catch((error) => { + // Listener support can be unavailable on hosted/older native shells while + // direct plugin methods such as showNewMail still work. + return null; + }); + return () => { + handlePromise.then((handle) => handle?.remove?.()).catch(() => {}); + }; + }, + }, + }; + + installed = true; +}