diff --git a/.github/renovate.json5 b/.github/renovate.json5 index d2f0c75b9f5..6b34998b99b 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -231,6 +231,7 @@ "webpack-node-externals", "widestring", "windows", + "windows-core", "windows-future", "windows-registry", "zbus", @@ -255,6 +256,11 @@ groupName: "zbus", matchPackageNames: ["zbus", "zbus_polkit"], }, + { + // We need to group all windows-related packages together to avoid build errors caused by version incompatibilities. + groupName: "windows", + matchPackageNames: ["windows", "windows-core", "windows-future", "windows-registry"], + }, { // We group all webpack build-related minor and patch updates together to reduce PR noise. // We include patch updates here because we want PRs for webpack patch updates and it's in this group. diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 83e6c2d696e..772cb25d98d 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -193,7 +193,7 @@ jobs: zip -r browser-source.zip browser-source - name: Upload browser source - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: ${{matrix.license_type.archive_name_prefix}}browser-source-${{ env._BUILD_NUMBER }}.zip path: browser-source.zip @@ -218,6 +218,7 @@ jobs: source_archive_name_prefix: "" archive_name_prefix: "" npm_command_prefix: "dist:" + npm_package_dev_prefix: "package:dev:" readable: "open source license" type: "oss" - build_prefix: "bit-" @@ -225,6 +226,7 @@ jobs: source_archive_name_prefix: "bit-" archive_name_prefix: "bit-" npm_command_prefix: "dist:bit:" + npm_package_dev_prefix: "package:bit:dev:" readable: "commercial license" type: "commercial" browser: @@ -232,6 +234,8 @@ jobs: npm_command_suffix: "chrome" archive_name: "dist-chrome.zip" artifact_name: "dist-chrome-MV3" + artifact_name_dev: "dev-chrome-MV3" + archive_name_dev: "dev-chrome.zip" - name: "edge" npm_command_suffix: "edge" archive_name: "dist-edge.zip" @@ -268,7 +272,7 @@ jobs: npm --version - name: Download browser source - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: ${{matrix.license_type.source_archive_name_prefix}}browser-source-${{ env._BUILD_NUMBER }}.zip @@ -332,12 +336,25 @@ jobs: working-directory: browser-source/apps/browser - name: Upload extension artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: ${{ matrix.license_type.artifact_prefix }}${{ matrix.browser.artifact_name }}-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/${{matrix.license_type.archive_name_prefix}}${{ matrix.browser.archive_name }} if-no-files-found: error + - name: Package dev extension + if: ${{ matrix.browser.archive_name_dev != '' }} + run: npm run ${{ matrix.license_type.npm_package_dev_prefix }}${{ matrix.browser.npm_command_suffix }} + working-directory: browser-source/apps/browser + + - name: Upload dev extension artifact + if: ${{ matrix.browser.archive_name_dev != '' }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: ${{ matrix.license_type.artifact_prefix }}${{ matrix.browser.artifact_name_dev }}-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/${{matrix.license_type.archive_name_prefix}}${{ matrix.browser.archive_name_dev }} + if-no-files-found: error + build-safari: name: Build Safari - ${{ matrix.license_type.readable }} @@ -506,7 +523,7 @@ jobs: ls -la - name: Upload Safari artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: ${{matrix.license_type.archive_name_prefix}}dist-safari-${{ env._BUILD_NUMBER }}.zip path: apps/browser/dist/${{matrix.license_type.archive_name_prefix}}dist-safari.zip diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 414c043b89e..babd00a323f 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -268,7 +268,7 @@ jobs: fi - name: Upload unix zip asset - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }}-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }}-${{ env._PACKAGE_VERSION }}.zip @@ -482,7 +482,7 @@ jobs: } - name: Upload windows zip asset - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip @@ -490,7 +490,7 @@ jobs: - name: Upload Chocolatey asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg @@ -503,7 +503,7 @@ jobs: - name: Upload NPM Build Directory asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip @@ -535,7 +535,7 @@ jobs: echo "BW Package Version: $_PACKAGE_VERSION" - name: Get bw linux cli - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: bw-linux-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/snap @@ -572,7 +572,7 @@ jobs: run: sudo snap remove bw - name: Upload snap asset - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index f651af9dd7d..d3566535b65 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -251,48 +251,41 @@ jobs: TARGET: musl run: | rustup target add x86_64-unknown-linux-musl - node build.js --target=x86_64-unknown-linux-musl --release + node build.js --target=x86_64-unknown-linux-musl - name: Build application run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb if-no-files-found: error - name: Upload .rpm artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm if-no-files-found: error - - name: Upload .freebsd artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd - path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd - if-no-files-found: error - - name: Upload .snap artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload .AppImage artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml @@ -305,7 +298,7 @@ jobs: sudo npm run pack:lin:flatpak - name: Upload flatpak artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: com.bitwarden.desktop.flatpak path: apps/desktop/dist/com.bitwarden.desktop.flatpak @@ -414,7 +407,7 @@ jobs: TARGET: musl run: | rustup target add aarch64-unknown-linux-musl - node build.js --target=aarch64-unknown-linux-musl --release + node build.js --target=aarch64-unknown-linux-musl - name: Check index.d.ts generated if: github.event_name == 'pull_request' && steps.cache.outputs.cache-hit != 'true' @@ -433,14 +426,14 @@ jobs: run: npm run dist:lin:arm64 - name: Upload .snap artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_arm64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_arm64.snap if-no-files-found: error - name: Upload tar.gz artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_arm64.tar.gz path: apps/desktop/dist/bitwarden_desktop_arm64.tar.gz @@ -453,7 +446,7 @@ jobs: sudo npm run pack:lin:flatpak - name: Upload flatpak artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: com.bitwarden.desktop-arm64.flatpak path: apps/desktop/dist/com.bitwarden.desktop.flatpak @@ -624,7 +617,7 @@ jobs: -NewName bitwarden-$env:_PACKAGE_VERSION-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe @@ -632,15 +625,15 @@ jobs: - name: Upload installer exe artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: - name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}..exe + name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload appx ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx @@ -648,7 +641,7 @@ jobs: - name: Upload store appx ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx @@ -656,7 +649,7 @@ jobs: - name: Upload NSIS ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z @@ -664,7 +657,7 @@ jobs: - name: Upload appx x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx @@ -672,7 +665,7 @@ jobs: - name: Upload store appx x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx @@ -680,7 +673,7 @@ jobs: - name: Upload NSIS x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z @@ -688,7 +681,7 @@ jobs: - name: Upload appx ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx @@ -696,7 +689,7 @@ jobs: - name: Upload store appx ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx @@ -704,7 +697,7 @@ jobs: - name: Upload NSIS ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z @@ -712,7 +705,7 @@ jobs: - name: Upload nupkg artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg @@ -720,7 +713,7 @@ jobs: - name: Upload auto-update artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: ${{ needs.setup.outputs.release_channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml @@ -871,9 +864,11 @@ jobs: -NewName bitwarden-beta-$env:_PACKAGE_VERSION-x64.nsis.7z Rename-Item -Path .\dist\nsis-web\Bitwarden-Beta-$env:_PACKAGE_VERSION-arm64.nsis.7z ` -NewName bitwarden-beta-$env:_PACKAGE_VERSION-arm64.nsis.7z + Rename-Item -Path .\dist\nsis-web\latest.yml ` + -NewName latest-beta.yml - name: Upload portable exe artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-Beta-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Beta-Portable-${{ env._PACKAGE_VERSION }}.exe @@ -881,7 +876,7 @@ jobs: - name: Upload installer exe artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-Beta-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Beta-Installer-${{ env._PACKAGE_VERSION }}.exe @@ -889,7 +884,7 @@ jobs: - name: Upload appx ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32.appx @@ -897,7 +892,7 @@ jobs: - name: Upload store appx ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32-store.appx @@ -905,7 +900,7 @@ jobs: - name: Upload NSIS ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden-beta-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-beta-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z @@ -913,7 +908,7 @@ jobs: - name: Upload appx x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64.appx @@ -921,7 +916,7 @@ jobs: - name: Upload store appx x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64-store.appx @@ -929,7 +924,7 @@ jobs: - name: Upload NSIS x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden-beta-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-beta-${{ env._PACKAGE_VERSION }}-x64.nsis.7z @@ -937,7 +932,7 @@ jobs: - name: Upload appx ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64.appx @@ -945,7 +940,7 @@ jobs: - name: Upload store appx ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64-store.appx @@ -953,7 +948,7 @@ jobs: - name: Upload NSIS ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: bitwarden-beta-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-beta-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z @@ -961,10 +956,10 @@ jobs: - name: Upload auto-update artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: - name: ${{ needs.setup.outputs.release_channel }}-beta.yml - path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml + name: latest-beta.yml + path: apps/desktop/dist/nsis-web/latest-beta.yml if-no-files-found: error macos-build: @@ -995,12 +990,12 @@ jobs: cache: 'npm' cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} - + - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.14' - + - name: Set up Node-gyp run: python3 -m pip install setuptools @@ -1232,12 +1227,12 @@ jobs: cache: 'npm' cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} - + - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.14' - + - name: Set up Node-gyp run: python3 -m pip install setuptools @@ -1413,7 +1408,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -1446,28 +1441,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip if-no-files-found: error - name: Upload .dmg artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg if-no-files-found: error - name: Upload .dmg blockmap artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: ${{ needs.setup.outputs.release_channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml @@ -1504,12 +1499,12 @@ jobs: cache: 'npm' cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} - + - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.14' - + - name: Set up Node-gyp run: python3 -m pip install setuptools @@ -1693,7 +1688,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -1736,14 +1731,14 @@ jobs: $buildInfo | ConvertTo-Json | Set-Content -Path dist/macos-build-number.json - name: Upload MacOS App Store build number artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: macos-build-number.json path: apps/desktop/dist/macos-build-number.json if-no-files-found: error - name: Upload .pkg artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 719063958f7..caf806af9f0 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -307,7 +307,7 @@ jobs: zip -r web-$_VERSION-${{ matrix.artifact_name }}.zip build - name: Upload ${{ matrix.artifact_name }} artifact - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip path: apps/web/web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip @@ -334,7 +334,7 @@ jobs: - name: Scan Docker image if: ${{ needs.setup.outputs.has_secrets == 'true' }} id: container-scan - uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # v6.5.1 + uses: anchore/scan-action@568b89d27fc18c60e56937bff480c91c772cd993 # v7.1.0 with: image: ${{ steps.image-name.outputs.name }} fail-build: false diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 19532493071..311737a2c0e 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -54,6 +54,8 @@ jobs: with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} + permission-contents: write # for creating, committing to, and pushing new branches + permission-pull-requests: write # for generating pull requests - name: Checkout repo uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c14abd7cd86..67186905390 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -98,12 +98,27 @@ jobs: with: persist-credentials: false + - name: Install Rust + uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # stable + with: + toolchain: stable + components: rustfmt, clippy + + - name: Install Rust nightly + uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # stable + with: + toolchain: nightly + components: rustfmt + - name: Check Rust version run: rustup --version + - name: Cache cargo registry + uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7 + - name: Run cargo fmt working-directory: ./apps/desktop/desktop_native - run: cargo fmt --check + run: cargo +nightly fmt --check - name: Run Clippy working-directory: ./apps/desktop/desktop_native @@ -118,6 +133,13 @@ jobs: working-directory: ./apps/desktop/desktop_native run: cargo sort --workspace --check + - name: Install cargo-udeps + run: cargo install cargo-udeps --version 0.1.57 --locked + + - name: Cargo udeps + working-directory: ./apps/desktop/desktop_native + run: cargo +nightly udeps --workspace --all-features --all-targets + - name: Install cargo-deny uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index bcae79d077e..426947526a4 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -66,15 +66,17 @@ jobs: - name: Version output id: version-output env: - _INPUT_VERSION: ${{ inputs.version }} + INPUT_VERSION: ${{ inputs.version }} run: | - if [[ "$_INPUT_VERSION" == "latest" || "$_INPUT_VERSION" == "" ]]; then - VERSION=$(curl "https://api.github.com/repos/bitwarden/clients/releases" | jq -c '.[] | select(.tag_name | contains("cli")) | .tag_name' | head -1 | grep -ohE '20[0-9]{2}\.([1-9]|1[0-2])\.[0-9]+') + if [[ "$INPUT_VERSION" == "latest" || "$INPUT_VERSION" == "" ]]; then + TAG_NAME=$(curl -s "https://api.github.com/repos/bitwarden/clients/releases" \ + | jq -r '.[] | select(.tag_name | contains("cli")) | .tag_name' | head -1) + VERSION="${TAG_NAME#cli-v}" echo "Latest Released Version: $VERSION" echo "version=$VERSION" >> "$GITHUB_OUTPUT" else - echo "Release Version: $_INPUT_VERSION" - echo "version=$_INPUT_VERSION" >> "$GITHUB_OUTPUT" + echo "Release Version: $INPUT_VERSION" + echo "version=$INPUT_VERSION" >> "$GITHUB_OUTPUT" fi - name: Create GitHub deployment @@ -126,14 +128,14 @@ jobs: uses: samuelmeuli/action-snapcraft@fceeb3c308e76f3487e72ef608618de625fb7fe8 # v3.0.1 - name: Download artifacts - run: wget "https://github.com/bitwarden/clients/releases/download/cli-v$_PKG_VERSION/bw_$_PKG_VERSION_amd64.snap" + run: wget "https://github.com/bitwarden/clients/releases/download/cli-v${_PKG_VERSION}/bw_${_PKG_VERSION}_amd64.snap" - name: Publish Snap & logout if: ${{ inputs.publish_type != 'Dry Run' }} env: SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }} run: | - snapcraft upload "bw_$_PKG_VERSION_amd64.snap" --release stable + snapcraft upload "bw_${_PKG_VERSION}_amd64.snap" --release stable snapcraft logout choco: @@ -179,7 +181,7 @@ jobs: run: New-Item -ItemType directory -Path ./dist - name: Download artifacts - run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/cli-v$_PKG_VERSION/bitwarden-cli.$_PKG_VERSION.nupkg" -OutFile bitwarden-cli.$_PKG_VERSION.nupkg + run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/cli-v$($env:_PKG_VERSION)/bitwarden-cli.$($env:_PKG_VERSION).nupkg" -OutFile bitwarden-cli.$($env:_PKG_VERSION).nupkg working-directory: apps/cli/dist - name: Push to Chocolatey @@ -204,7 +206,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - + - name: Get Node version id: retrieve-node-version working-directory: ./ @@ -227,8 +229,8 @@ jobs: - name: Download and set up artifact run: | mkdir -p build - wget "https://github.com/bitwarden/clients/releases/download/cli-v$_PKG_VERSION/bitwarden-cli-$_PKG_VERSION-npm-build.zip" - unzip "bitwarden-cli-$_PKG_VERSION-npm-build.zip" -d build + wget "https://github.com/bitwarden/clients/releases/download/cli-v${_PKG_VERSION}/bitwarden-cli-${_PKG_VERSION}-npm-build.zip" + unzip "bitwarden-cli-${_PKG_VERSION}-npm-build.zip" -d build - name: Publish NPM if: ${{ inputs.publish_type != 'Dry Run' }} diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index 15a0ec77d5b..b17312950e9 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -73,12 +73,11 @@ jobs: - name: Check Publish Version id: version env: - _INPUT_VERSION: ${{ inputs.version }} + INPUT_VERSION: ${{ inputs.version }} run: | - if [[ "$_INPUT_VERSION" == "latest" || "$_INPUT_VERSION" == "" ]]; then - TAG_NAME=$(curl "https://api.github.com/repos/bitwarden/clients/releases" \ - | jq -c '.[] | select(.tag_name | contains("desktop")) | .tag_name' \ - | head -1 | cut -d '"' -f 2) + if [[ "$INPUT_VERSION" == "latest" || "$INPUT_VERSION" == "" ]]; then + TAG_NAME=$(curl -s "https://api.github.com/repos/bitwarden/clients/releases" \ + | jq -r '.[] | select(.tag_name | contains("desktop")) | .tag_name' | head -1) VERSION="${TAG_NAME#desktop-v}" echo "Latest Released Version: $VERSION" @@ -87,7 +86,7 @@ jobs: echo "Tag name: $TAG_NAME" echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" else - VERSION="$_INPUT_VERSION" + VERSION="$INPUT_VERSION" TAG_NAME="desktop-v$VERSION" echo "Release Version: $VERSION" @@ -100,9 +99,9 @@ jobs: - name: Get Version Channel id: release_channel env: - _VERSION: ${{ steps.version.outputs.version }} + VERSION: ${{ steps.version.outputs.version }} run: | - case "${_VERSION}" in + case "${VERSION}" in *"alpha"*) echo "channel=alpha" >> "$GITHUB_OUTPUT" echo "[!] We do not yet support 'alpha'" @@ -192,22 +191,6 @@ jobs: --recursive \ --quiet - - name: Update deployment status to Success - if: ${{ inputs.publish_type != 'Dry Run' && success() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment_id }} - - - name: Update deployment status to Failure - if: ${{ inputs.publish_type != 'Dry Run' && failure() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment_id }} - snap: name: Deploy Snap runs-on: ubuntu-22.04 @@ -251,14 +234,14 @@ jobs: - name: Download artifacts working-directory: apps/desktop/dist - run: wget "https://github.com/bitwarden/clients/releases/download/$_RELEASE_TAG/bitwarden_$_PKG_VERSION_amd64.snap" + run: wget "https://github.com/bitwarden/clients/releases/download/${_RELEASE_TAG}/bitwarden_${_PKG_VERSION}_amd64.snap" - name: Deploy to Snap Store if: ${{ inputs.publish_type != 'Dry Run' }} env: SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }} run: | - snapcraft upload "bitwarden_$_PKG_VERSION_amd64.snap" --release stable + snapcraft upload "bitwarden_${_PKG_VERSION}_amd64.snap" --release stable snapcraft logout working-directory: apps/desktop/dist @@ -312,7 +295,7 @@ jobs: - name: Download artifacts working-directory: apps/desktop/dist - run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/$_RELEASE_TAG/bitwarden.$_PKG_VERSION.nupkg" -OutFile "bitwarden.$_PKG_VERSION.nupkg" + run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/$($env:_RELEASE_TAG)/bitwarden.$($env:_PKG_VERSION).nupkg" -OutFile "bitwarden.$($env:_PKG_VERSION).nupkg" - name: Push to Chocolatey if: ${{ inputs.publish_type != 'Dry Run' }} @@ -337,7 +320,7 @@ jobs: persist-credentials: false - name: Validate release notes for MAS - if: inputs.mas_publish && (inputs.release_notes == '' || inputs.release_notes == null) + if: inputs.release_notes == '' || inputs.release_notes == null run: | echo "❌ Release notes are required when publishing to Mac App Store" echo "Please provide release notes using the 'Release Notes' input field" @@ -345,7 +328,7 @@ jobs: - name: Download MacOS App Store build number working-directory: apps/desktop - run: wget "https://github.com/bitwarden/clients/releases/download/$_RELEASE_TAG/macos-build-number.json" + run: wget "https://github.com/bitwarden/clients/releases/download/${_RELEASE_TAG}/macos-build-number.json" - name: Setup Ruby and Install Fastlane uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0 @@ -353,7 +336,7 @@ jobs: ruby-version: '3.4.7' bundler-cache: false working-directory: apps/desktop - + - name: Install Fastlane working-directory: apps/desktop run: gem install fastlane @@ -379,32 +362,32 @@ jobs: env: APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }} APP_STORE_CONNECT_AUTH_KEY: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} - _RELEASE_NOTES: ${{ inputs.release_notes }} - _PUBLISH_TYPE: ${{ inputs.publish_type }} + CHANGELOG: ${{ inputs.release_notes }} + PUBLISH_TYPE: ${{ inputs.publish_type }} working-directory: apps/desktop run: | BUILD_NUMBER=$(jq -r '.buildNumber' macos-build-number.json) - CHANGELOG="$_RELEASE_NOTES" - IS_DRY_RUN="$_PUBLISH_TYPE == 'Dry Run'" - if [ "$IS_DRY_RUN" = "true" ]; then + if [ "$PUBLISH_TYPE" = "Dry Run" ]; then echo "🧪 DRY RUN MODE - Testing without actual App Store submission" echo "📦 Would publish build $BUILD_NUMBER to Mac App Store" + IS_DRY_RUN="true" else echo "🚀 PRODUCTION MODE - Publishing to Mac App Store" echo "📦 Publishing build $BUILD_NUMBER to Mac App Store" + IS_DRY_RUN="false" fi - + echo "📝 Release notes (${#CHANGELOG} chars): ${CHANGELOG:0:100}..." - + # Validate changelog length (App Store limit is 4000 chars) if [ ${#CHANGELOG} -gt 4000 ]; then echo "❌ Release notes too long: ${#CHANGELOG} characters (max 4000)" exit 1 fi - + fastlane publish --verbose \ - app_version:"$PKG_VERSION" \ + app_version:"${_PKG_VERSION}" \ build_number:"$BUILD_NUMBER" \ changelog:"$CHANGELOG" \ dry_run:"$IS_DRY_RUN" diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index 6bf2b282b38..4f41898a9b2 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -157,11 +157,10 @@ jobs: - name: Log out of Docker run: docker logout - self-host-unified-build: - name: Trigger self-host unified build + bitwarden-lite-build: + name: Trigger Bitwarden Lite build runs-on: ubuntu-22.04 - needs: - - setup + needs: setup permissions: id-token: write steps: @@ -182,7 +181,7 @@ jobs: - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main - - name: Trigger self-host build + - name: Trigger Bitwarden Lite build uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} @@ -190,7 +189,7 @@ jobs: await github.rest.actions.createWorkflowDispatch({ owner: 'bitwarden', repo: 'self-host', - workflow_id: 'build-unified.yml', + workflow_id: 'build-bitwarden-lite.yml', ref: 'main', inputs: { use_latest_core_version: true diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index c7faefb2ce9..53382539b89 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -132,11 +132,11 @@ jobs: env: PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} run: | - mv browser-source.zip "browser-source-$PACKAGE_VERSION.zip" - mv dist-chrome.zip "dist-chrome-$PACKAGE_VERSION.zip" - mv dist-opera.zip "dist-opera-$PACKAGE_VERSION.zip" - mv dist-firefox.zip "dist-firefox-$PACKAGE_VERSION.zip" - mv dist-edge.zip "dist-edge-$PACKAGE_VERSION.zip" + mv browser-source.zip "browser-source-${PACKAGE_VERSION}.zip" + mv dist-chrome.zip "dist-chrome-${PACKAGE_VERSION}.zip" + mv dist-opera.zip "dist-opera-${PACKAGE_VERSION}.zip" + mv dist-firefox.zip "dist-firefox-${PACKAGE_VERSION}.zip" + mv dist-edge.zip "dist-edge-${PACKAGE_VERSION}.zip" - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 35fc8bed8a9..10a0f581faa 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -58,9 +58,9 @@ jobs: - name: Get Version Channel id: release_channel env: - _VERSION: ${{ steps.version.outputs.version }} + VERSION: ${{ steps.version.outputs.version }} run: | - case "$_VERSION" in + case "$VERSION" in *"alpha"*) echo "channel=alpha" >> "$GITHUB_OUTPUT" echo "[!] We do not yet support 'alpha'" @@ -96,7 +96,7 @@ jobs: env: PKG_VERSION: ${{ steps.version.outputs.version }} working-directory: apps/desktop/artifacts - run: mv "Bitwarden-$PKG_VERSION-universal.pkg" "Bitwarden-$PKG_VERSION-universal.pkg.archive" + run: mv "Bitwarden-${PKG_VERSION}-universal.pkg" "Bitwarden-${PKG_VERSION}-universal.pkg.archive" - name: Create Release uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 @@ -107,7 +107,6 @@ jobs: with: artifacts: "apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-amd64.deb, apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-x86_64.rpm, - apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-x64.freebsd, apps/desktop/artifacts/bitwarden_${{ env.PKG_VERSION }}_amd64.snap, apps/desktop/artifacts/bitwarden_${{ env.PKG_VERSION }}_arm64.snap, apps/desktop/artifacts/bitwarden_${{ env.PKG_VERSION }}_arm64.tar.gz, diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index 59022657398..9203769bc77 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -52,8 +52,7 @@ jobs: release: name: Create GitHub Release runs-on: ubuntu-22.04 - needs: - - setup + needs: setup permissions: contents: write steps: @@ -82,10 +81,10 @@ jobs: - name: Rename assets working-directory: apps/web/artifacts env: - _RELEASE_VERSION: ${{ needs.setup.outputs.release_version }} + RELEASE_VERSION: ${{ needs.setup.outputs.release_version }} run: | - mv web-*-selfhosted-COMMERCIAL.zip "web-$_RELEASE_VERSION-selfhosted-COMMERCIAL.zip" - mv web-*-selfhosted-open-source.zip "web-$_RELEASE_VERSION-selfhosted-open-source.zip" + mv web-*-selfhosted-COMMERCIAL.zip "web-${RELEASE_VERSION}-selfhosted-COMMERCIAL.zip" + mv web-*-selfhosted-open-source.zip "web-${RELEASE_VERSION}-selfhosted-open-source.zip" - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/sdk-breaking-change-check.yml b/.github/workflows/sdk-breaking-change-check.yml index 759f2292d2a..1b9653417f2 100644 --- a/.github/workflows/sdk-breaking-change-check.yml +++ b/.github/workflows/sdk-breaking-change-check.yml @@ -58,6 +58,8 @@ jobs: with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} + permission-actions: read # for reading and downloading the artifacts for a workflow run + - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71f8e7c9155..f471826355f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,10 +71,10 @@ jobs: fail-on-error: true - name: Upload results to codecov.io - uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0 + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 - name: Upload test coverage - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: jest-coverage path: ./coverage/lcov.info @@ -160,7 +160,7 @@ jobs: run: cargo llvm-cov --all-features --lcov --output-path lcov.info --workspace --no-cfg-coverage - name: Upload test coverage - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: rust-coverage path: ./apps/desktop/desktop_native/lcov.info @@ -178,13 +178,13 @@ jobs: persist-credentials: false - name: Download jest coverage - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: jest-coverage path: ./ - name: Download rust coverage - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: rust-coverage path: ./apps/desktop/desktop_native diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index 9ff252d2fe8..d807dd046d3 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -36,6 +36,7 @@ jobs: with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} + permission-contents: write # for committing and pushing to the current branch - name: Check out target ref uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/apps/browser/package.json b/apps/browser/package.json index 82d2ad7ab7a..a6a88b53db0 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,11 +1,13 @@ { "name": "@bitwarden/browser", - "version": "2025.11.0", + "version": "2025.11.1", "scripts": { "build": "npm run build:chrome", "build:bit": "npm run build:bit:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:bit:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js", + "build:dev:chrome": "npm run build:chrome && npm run update:dev:chrome", + "build:bit:dev:chrome": "npm run build:bit:chrome && npm run update:dev:chrome", "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:bit:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js", "build:firefox": "cross-env BROWSER=firefox NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", @@ -55,9 +57,12 @@ "dist:bit:opera:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:bit:opera", "dist:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:safari", "dist:bit:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:bit:safari", + "package:dev:chrome": "npm run update:dev:chrome && ./scripts/compress.sh dev-chrome.zip", + "package:bit:dev:chrome": "npm run update:dev:chrome && ./scripts/compress.sh bit-dev-chrome.zip", "test": "jest", "test:watch": "jest --watch", "test:watch:all": "jest --watchAll", - "test:clearCache": "jest --clear-cache" + "test:clearCache": "jest --clear-cache", + "update:dev:chrome": "./scripts/update-manifest-dev.sh" } } diff --git a/apps/browser/scripts/update-manifest-dev.sh b/apps/browser/scripts/update-manifest-dev.sh new file mode 100755 index 00000000000..2823d4cb510 --- /dev/null +++ b/apps/browser/scripts/update-manifest-dev.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +#### +# Update the manifest key in the build directory. +#### + +set -e +set -u +set -x +set -o pipefail + +SCRIPT_ROOT="$(dirname "$0")" +BUILD_DIR="$SCRIPT_ROOT/../build" + +# Check if build directory exists +if [ -d "$BUILD_DIR" ]; then + cd "$BUILD_DIR" + + # Update manifest with dev public key + MANIFEST_PATH="./manifest.json" + + # Generated arbitrary public key from Chrome Dev Console to pin side-loaded extension IDs during development + DEV_PUBLIC_KEY='MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuIvjtsAVWZM0i5jFhSZcrmwgaf3KWcxM5F16LNDNeivC1EqJ+H5xNZ5R9UN5ueHA2xyyYAOlxY07OcY6CKTGJRJyefbUhszb66sdx26SV5gVkCois99fKBlsbSbd6und/BJYmoFUWvFCNNVH+OxLMqMQWjMMhM2ItLqTYi7dxRE5qd+7LwQpnGG2vTkm/O7nu8U3CtkfcIAGLsiTd7/iuytcMDnC0qFM5tJyY/5I+9QOhpUJ7Ybj3C18BDWDORhqxutWv+MSw//SgUn2/lPQrnrKq7FIVQL7FxxEPqkv4QwFvaixps1cBbMdJ1Ygit1z5JldoSyNxzCa5vVcJLecMQIDAQAB' + + MANIFEST_PATH_TMP="${MANIFEST_PATH}.tmp" + if jq --arg key "$DEV_PUBLIC_KEY" '.key = $key' "$MANIFEST_PATH" > "$MANIFEST_PATH_TMP"; then + mv "$MANIFEST_PATH_TMP" "$MANIFEST_PATH" + echo "Updated manifest key in $MANIFEST_PATH" + else + echo "ERROR: Failed to update manifest with jq" + rm -f "$MANIFEST_PATH_TMP" + exit 1 + fi +fi diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 053fb3b101f..20eb31a5453 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "عند قفل النظام" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "عند إعادة تشغيل المتصفح" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 68ea40b6808..d8d8589f47e 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -592,7 +592,10 @@ "message": "Bax" }, "viewAll": { - "message": "View all" + "message": "Hamısına bax" + }, + "viewLess": { + "message": "Daha azına bax" }, "viewLogin": { "message": "Girişə bax" @@ -625,7 +628,7 @@ "message": "Element sevimlilərə əlavə edildi" }, "itemRemovedFromFavorites": { - "message": "Element sevimlilərdən çıxarıldı" + "message": "Element sevimlilərdən xaric edildi" }, "notes": { "message": "Notlar" @@ -685,7 +688,7 @@ "message": "Ayarlarda bir kilid açma üsulu qurun" }, "sessionTimeoutHeader": { - "message": "Seans vaxt bitməsi" + "message": "Sessiya vaxt bitməsi" }, "vaultTimeoutHeader": { "message": "Seyf vaxtının bitməsi" @@ -796,6 +799,12 @@ "onLocked": { "message": "Sistem kilidlənəndə" }, + "onIdle": { + "message": "Sistem boşda olduqda" + }, + "onSleep": { + "message": "Sistem yuxu rejimində olduqda" + }, "onRestart": { "message": "Brauzer yenidən başladılanda" }, @@ -916,7 +925,7 @@ "message": "Hesabınızdan çıxış etmisiniz." }, "loginExpired": { - "message": "Giriş seansınızın müddəti bitdi." + "message": "Giriş sessiyanızın müddəti bitdi." }, "logIn": { "message": "Giriş et" @@ -1035,10 +1044,10 @@ "message": "Element saxlanıldı" }, "savedWebsite": { - "message": "Saved website" + "message": "Saxlanılan veb sayt" }, "savedWebsites": { - "message": "Saved websites ( $COUNT$ )", + "message": "Saxlanılan veb sayt ( $COUNT$ )", "placeholders": { "count": { "content": "$1", @@ -1239,7 +1248,7 @@ "description": "Detailed error message shown when saving login details fails." }, "changePasswordWarning": { - "message": "Parolunuzu dəyişdirdikdən sonra yeni parolunuzla giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saat ərzində çıxış sonlandırılacaq." + "message": "Parolunuzu dəyişdirdikdən sonra yeni parolunuzla giriş etməli olacaqsınız. Digər cihazlardakı aktiv sessiyalar bir saat ərzində çıxış sonlandırılacaq." }, "accountRecoveryUpdateMasterPasswordSubtitle": { "message": "Hesabın geri qaytarılması prosesini tamamlamaq üçün ana parolunuzu dəyişdirin." @@ -1527,7 +1536,7 @@ "message": "Kimlik doğrulama vaxtı bitdi" }, "authenticationSessionTimedOut": { - "message": "Kimlik doğrulama seansının vaxtı bitdi. Lütfən giriş prosesini yenidən başladın." + "message": "Kimlik doğrulama sessiyasının vaxtı bitdi. Lütfən giriş prosesini yenidən başladın." }, "verificationCodeEmailSent": { "message": "Doğrulama poçtu $EMAIL$ ünvanına göndərildi.", @@ -1692,28 +1701,28 @@ "message": "Avto-doldurmanı söndür" }, "confirmAutofill": { - "message": "Confirm autofill" + "message": "Avto-doldurmanı təsdiqlə" }, "confirmAutofillDesc": { - "message": "This site doesn't match your saved login details. Before you fill in your login credentials, make sure it's a trusted site." + "message": "Bu sayt, saxlanılmış giriş məlumatlarınızla uyuşmur. Giriş məlumatlarınızı doldurmazdan əvvəl, güvənli sayt olduğuna əmin olun." }, "showInlineMenuLabel": { "message": "Avto-doldurma təkliflərini form xanalarında göstər" }, "howDoesBitwardenProtectFromPhishing": { - "message": "How does Bitwarden protect your data from phishing?" + "message": "Bitwarden verilərinizi fişinqdən necə qoruyur?" }, "currentWebsite": { - "message": "Current website" + "message": "Hazırkı veb sayt" }, "autofillAndAddWebsite": { - "message": "Autofill and add this website" + "message": "Avto-doldur və bu veb saytı əlavə et" }, "autofillWithoutAdding": { - "message": "Autofill without adding" + "message": "Əlavə etmədən avto-doldur" }, "doNotAutofill": { - "message": "Do not autofill" + "message": "Avto-doldurulmasın" }, "showInlineMenuIdentitiesLabel": { "message": "Kimlikləri təklif kimi göstər" @@ -2149,7 +2158,7 @@ } }, "passwordSafe": { - "message": "Bu parol, veri pozuntularında qeydə alınmayıb. Rahatlıqla istifadə edə bilərsiniz." + "message": "Bu parol, veri pozuntularında qeydə alınmayıb. Əmniyyətlə istifadə edə bilərsiniz." }, "baseDomain": { "message": "Baza domeni", @@ -2219,7 +2228,7 @@ "message": "Təzəlikcə heç nə yaratmamısınız" }, "remove": { - "message": "Çıxart" + "message": "Xaric et" }, "default": { "message": "İlkin" @@ -3058,10 +3067,10 @@ "message": "Ana parolu güncəllə" }, "updateMasterPasswordWarning": { - "message": "Ana parolunuz təzəlikcə təşkilatınızdakı bir inzibatçı tərəfindən dəyişdirildi. Seyfə erişmək üçün onu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış edəcəksiniz və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." + "message": "Ana parolunuz təzəlikcə təşkilatınızdakı bir inzibatçı tərəfindən dəyişdirildi. Seyfə erişmək üçün onu indi güncəlləməlisiniz. Davam etsəniz, hazırkı sessiyadan çıxış edəcəksiniz və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv sessiyalar bir saata qədər aktiv qalmağa davam edə bilər." }, "updateWeakMasterPasswordWarning": { - "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Seyfə erişmək üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış etmiş və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." + "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Seyfə erişmək üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı sessiyadan çıxış etmiş və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv sessiyalar bir saata qədər aktiv qalmağa davam edə bilər." }, "tdeDisabledMasterPasswordRequired": { "message": "Təşkilatınız, güvənli cihaz şifrələməsini sıradan çıxartdı. Seyfinizə erişmək üçün lütfən ana parol təyin edin." @@ -3217,7 +3226,7 @@ "message": "Simvol sayını dəyişdir" }, "sessionTimeout": { - "message": "Seansınızın vaxtı bitdi. Lütfən geri qayıdıb yenidən giriş etməyə cəhd edin." + "message": "Sessiyanızın vaxtı bitdi. Lütfən geri qayıdıb yenidən giriş etməyə cəhd edin." }, "exportingPersonalVaultTitle": { "message": "Fərdi seyfin xaricə köçürülməsi" @@ -3277,7 +3286,7 @@ "message": "Şifrə açma xətası" }, "errorGettingAutoFillData": { - "message": "Error getting autofill data" + "message": "Avto-doldurma verilərini alma xətası" }, "couldNotDecryptVaultItemsBelow": { "message": "Bitwarden, aşağıda sadalanan seyf element(lər)inin şifrəsini aça bilmədi." @@ -3723,7 +3732,7 @@ "message": "Cihazları idarə et" }, "currentSession": { - "message": "Hazırkı seans" + "message": "Hazırkı sessiya" }, "mobile": { "message": "Mobil", @@ -3920,7 +3929,7 @@ "message": "İstifadəçiyə güvən" }, "sendsTitleNoItems": { - "message": "Send, həssas məlumatlar təhlükəsizdir", + "message": "Send ilə həssas məlumatlar əmniyyətdədir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { @@ -4051,13 +4060,13 @@ "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "cannotAutofill": { - "message": "Cannot autofill" + "message": "Avto-doldurula bilmir" }, "cannotAutofillExactMatch": { "message": "Default matching is set to 'Exact Match'. The current website does not exactly match the saved login details for this item." }, "okay": { - "message": "Okay" + "message": "Oldu" }, "toggleSideNavigation": { "message": "Yan naviqasiyanı aç/bağla" @@ -5709,7 +5718,7 @@ "message": "Kimliklərinizlə, uzun qeydiyyat və ya əlaqə xanalarını daha tez avtomatik doldurun." }, "newNoteNudgeTitle": { - "message": "Həssas verilərinizi güvənli şəkildə saxlayın" + "message": "Həssas verilərinizi əmniyyətdə saxlayın" }, "newNoteNudgeBody": { "message": "Notlarla, bankçılıq və ya sığorta təfsilatları kimi həssas veriləri təhlükəsiz saxlayın." @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "\"Premium\"a yüksəlt" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Seyf yüklənir" + }, + "vaultLoaded": { + "message": "Seyf yükləndi" + }, "settingDisabledByPolicy": { "message": "Bu ayar, təşkilatınızın siyasəti tərəfindən sıradan çıxarılıb.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Kart nömrəsi" + }, + "sessionTimeoutSettingsAction": { + "message": "Vaxt bitmə əməliyyatı" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index b0735109b41..47bafb7efe3 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Пры блакіраванні сістэмы" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Пры перазапуску браўзера" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 30386fe625e..98da9bf033e 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Показване на всички" }, + "viewLess": { + "message": "Преглед на по-малко" + }, "viewLogin": { "message": "Преглед на елемента за вписване" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "При заключване на системата" }, + "onIdle": { + "message": "При бездействие на системата" + }, + "onSleep": { + "message": "При заспиване на системата" + }, "onRestart": { "message": "При повторно пускане на браузъра" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Надградете до Платения план" }, + "upgradeCompleteSecurity": { + "message": "Надградете, за да се възползвате от пълна защита" + }, + "premiumGivesMoreTools": { + "message": "Платеният план предоставя повече инструменти за защита, ефективна работа и контрол." + }, + "explorePremium": { + "message": "Разгледайте платения план" + }, + "loadingVault": { + "message": "Зареждане на трезора" + }, + "vaultLoaded": { + "message": "Трезорът е зареден" + }, "settingDisabledByPolicy": { "message": "Тази настройка е изключена съгласно политиката на организацията Ви.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Номер на картата" + }, + "sessionTimeoutSettingsAction": { + "message": "Действие при изтичането на времето за достъп" } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index d68d19b0a05..794e380d012 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "সিস্টেম লকে" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "ব্রাউজার পুনঃসূচনাই" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 74f47fac7df..611cb4f5f55 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 824f37f069e..01611cc0764 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "En bloquejar el sistema" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "En reiniciar el navegador" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 46f5f414a1a..5c0c8fbf524 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Zobrazit vše" }, + "viewLess": { + "message": "Zobrazit méně" + }, "viewLogin": { "message": "Zobrazit přihlašovací údaje" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Při uzamknutí systému" }, + "onIdle": { + "message": "Při nečinnosti systému" + }, + "onSleep": { + "message": "Při přechodu do režimu spánku" + }, "onRestart": { "message": "Při restartu prohlížeče" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Aktualizovat na Premium" }, + "upgradeCompleteSecurity": { + "message": "Aktualizujte pro úplné zabezpečení" + }, + "premiumGivesMoreTools": { + "message": "Verze Premium Vám poskytne více nástrojů k zabezpečení, efektivní práci a udržení kontroly." + }, + "explorePremium": { + "message": "Objevit Premium" + }, + "loadingVault": { + "message": "Načítání trezoru" + }, + "vaultLoaded": { + "message": "Trezor byl načten" + }, "settingDisabledByPolicy": { "message": "Toto nastavení je zakázáno zásadami Vaší organizace.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Číslo karty" + }, + "sessionTimeoutSettingsAction": { + "message": "Akce vypršení časového limitu" } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 07c5a68e3ec..3bd81cc8039 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "wrth ailgychwyn y porwr" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 93b311e158b..9a12f808f22 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Når systemet låses" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Ved genstart af browseren" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index d88c396bb80..517dc638aff 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -29,7 +29,7 @@ "message": "Mit Passkey anmelden" }, "useSingleSignOn": { - "message": "Single Sign-on verwenden" + "message": "Single Sign-On verwenden" }, "yourOrganizationRequiresSingleSignOn": { "message": "Deine Organisation erfordert Single Sign-On." @@ -44,7 +44,7 @@ "message": "Schließe die Erstellung deines Kontos ab, indem du ein Passwort festlegst" }, "enterpriseSingleSignOn": { - "message": "Enterprise Single-Sign-On" + "message": "Enterprise Single Sign-On" }, "cancel": { "message": "Abbrechen" @@ -594,6 +594,9 @@ "viewAll": { "message": "Alles anzeigen" }, + "viewLess": { + "message": "Weniger anzeigen" + }, "viewLogin": { "message": "Zugangsdaten anzeigen" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Wenn System gesperrt" }, + "onIdle": { + "message": "Bei Systeminaktivität" + }, + "onSleep": { + "message": "Im Standby" + }, "onRestart": { "message": "Bei Browser-Neustart" }, @@ -5789,7 +5798,7 @@ "message": "Notfallzugriff" }, "breachMonitoring": { - "message": "Datenpannen-Überwachung" + "message": "Datendiebstahl-Überwachung" }, "andMoreFeatures": { "message": "Und mehr!" @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade auf Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade für umfassende Sicherheit" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Tresor wird geladen" + }, + "vaultLoaded": { + "message": "Tresor geladen" + }, "settingDisabledByPolicy": { "message": "Diese Einstellung ist durch die Richtlinien deiner Organisation deaktiviert.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Kartennummer" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout-Aktion" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 7fb60530511..ae685fff651 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "Προβολή σύνδεσης" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Κατά το Κλείδωμα Συστήματος" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Κατά την Επανεκκίνηση του Browser" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 2384cf8c4ec..5cc7c30bfb4 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2436,6 +2436,9 @@ } } }, + "topLayerHijackWarning": { + "message": "This page is interfering with the Bitwarden experience. The Bitwarden inline menu has been temporarily disabled as a safety measure." + }, "setMasterPassword": { "message": "Set master password" }, @@ -5809,6 +5812,15 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, "loadingVault": { "message": "Loading vault" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 8ab541c569e..5079e1e6689 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organisation's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 68bf5497e37..2a34400ea58 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organisation's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 060da79a4ff..8100fa80435 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Al bloquear el sistema" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Al reiniciar el navegador" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index acb440b2aa6..0f69561df57 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Arvutist väljalogimisel" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Brauseri taaskäivitamisel" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 016381e17f8..c60ce5e4da7 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Sistema blokeatzean" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Nabigatzailea berrabiaraztean" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 4f8529b2710..5a03f8427fa 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "هنگام قفل سیستم" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "هنگام راه‌اندازی مجدد" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index e3a5b44ea91..db4649c33a7 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Näytä kaikki" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Kun järjestelmä lukitaan" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Kun selain käynnistetään uudelleen" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 2b58095d950..6239e9d4f97 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Sa pag-lock ng sistema" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Sa pag-restart ng browser" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index afb58afcc25..1cc04809a1b 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "Afficher l'Identifiant" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Au verrouillage" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Au redémarrage du navigateur" }, @@ -3283,7 +3292,7 @@ "message": "Bitwarden n’a pas pu déchiffrer le(s) élément(s) du coffre listé(s) ci-dessous." }, "contactCSToAvoidDataLossPart1": { - "message": "Contacter le service clientèle", + "message": "Contacter succès client", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "Ce paramètre est désactivé par la politique de sécurité de votre organisation.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Numéro de carte" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 6d0410f112c..090bc6b1493 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Ó bloquear o sistema" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Ó reiniciar o navegador" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index cc78e1a154a..2934d345600 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "הצג הכל" }, + "viewLess": { + "message": "הצג פחות" + }, "viewLogin": { "message": "הצג כניסה" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "בנעילת המערכת" }, + "onIdle": { + "message": "כשהמערכת מזהה חוסר פעילות" + }, + "onSleep": { + "message": "כשהמערכת נכנסת למצב שינה" + }, "onRestart": { "message": "בהפעלת הדפדפן מחדש" }, @@ -4975,7 +4984,7 @@ } }, "defaultLabelWithValue": { - "message": "Default ( $VALUE$ )", + "message": "ברירת מחדל ( $VALUE$ )", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -5777,28 +5786,43 @@ "message": "עבודה נהדרת באבטחת הכניסות בסיכון שלך!" }, "upgradeNow": { - "message": "Upgrade now" + "message": "שדרג עכשיו" }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "מאמת מובנה" }, "secureFileStorage": { - "message": "Secure file storage" + "message": "אחסון קבצים מאובטח" }, "emergencyAccess": { - "message": "Emergency access" + "message": "גישת חירום" }, "breachMonitoring": { - "message": "Breach monitoring" + "message": "ניטור פרצות" }, "andMoreFeatures": { - "message": "And more!" + "message": "ועוד!" }, "planDescPremium": { - "message": "Complete online security" + "message": "השלם אבטחה מקוונת" }, "upgradeToPremium": { - "message": "Upgrade to Premium" + "message": "שדרג לפרימיום" + }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "טוען כספת" + }, + "vaultLoaded": { + "message": "הכספת נטענה" }, "settingDisabledByPolicy": { "message": "הגדרה זו מושבתת על ידי מדיניות של הארגון שלך.", @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "מספר כרטיס" + }, + "sessionTimeoutSettingsAction": { + "message": "פעולת פסק זמן" } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index c27fa6f7eb7..75011ebf8e5 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On Locked" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On Restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 9d7539a9bd5..fb89e8940e3 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Vidi sve" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "Prikaži prijavu" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Pri zaključavanju sustava" }, + "onIdle": { + "message": "U stanju besposlenosti" + }, + "onSleep": { + "message": "U stanju mirovanja sustava" + }, "onRestart": { "message": "Pri pokretanju preglednika" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": " Nadogradi na Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Učitavanje trezora" + }, + "vaultLoaded": { + "message": "Trezor učitan" + }, "settingDisabledByPolicy": { "message": "Ova je postavka onemogućena pravilima tvoje organizacije.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Broj kartice" + }, + "sessionTimeoutSettingsAction": { + "message": "Radnja kod isteka" } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 9f10494258a..f9a2f9ff009 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Összes megtekintése" }, + "viewLess": { + "message": "kevesebb megjelenítése" + }, "viewLogin": { "message": "Bejelentkezés megtekintése" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Rendszerzároláskor" }, + "onIdle": { + "message": "Rendszer üresjárat esetén" + }, + "onSleep": { + "message": "Rendszer alvó mód esetén" + }, "onRestart": { "message": "Böngésző újraindításkor" }, @@ -4975,7 +4984,7 @@ } }, "defaultLabelWithValue": { - "message": "Default ( $VALUE$ )", + "message": "Alapértelmezett ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Áttérés Prémium csomagra" }, + "upgradeCompleteSecurity": { + "message": "Áttérés a teljes biztonságért" + }, + "premiumGivesMoreTools": { + "message": "A Premium több eszközt ad a biztonság megőrzéséhez, a hatékony munkavégzéshez és az irányítás megőrzéséhez." + }, + "explorePremium": { + "message": "Premium felfedezése" + }, + "loadingVault": { + "message": "Széf betöltése" + }, + "vaultLoaded": { + "message": "A széf betöltésre került." + }, "settingDisabledByPolicy": { "message": "Ezt a beállítást a szervezet házirendje letiltotta.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Kártya szám" + }, + "sessionTimeoutSettingsAction": { + "message": "Időkifutási művelet" } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 26a2b8dc6bd..a88e3cc2d37 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Saat Komputer Terkunci" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Saat Peramban Dimulai Ulang" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 60c97d7157a..f0e63d99ecf 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "Visualizza login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Al blocco del computer" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Al riavvio del browser" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "Questa impostazione è disabilitata dalle restrizioni della tua organizzazione.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 7c4420508a2..98ac152a08f 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3,7 +3,7 @@ "message": "Bitwarden" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "Bitwarden ロゴ" }, "extName": { "message": "Bitwarden パスワードマネージャー", @@ -32,7 +32,7 @@ "message": "シングルサインオンを使用する" }, "yourOrganizationRequiresSingleSignOn": { - "message": "Your organization requires single sign-on." + "message": "あなたの組織ではシングルサインオン (SSO) を使用する必要があります。" }, "welcomeBack": { "message": "ようこそ" @@ -554,15 +554,15 @@ "message": "Reset search" }, "archiveNoun": { - "message": "Archive", + "message": "アーカイブ", "description": "Noun" }, "archiveVerb": { - "message": "Archive", + "message": "アーカイブ", "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "アーカイブ解除" }, "itemsInArchive": { "message": "Items in archive" @@ -594,8 +594,11 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { - "message": "View login" + "message": "ログイン情報を表示" }, "noItemsInList": { "message": "表示するアイテムがありません" @@ -796,6 +799,12 @@ "onLocked": { "message": "ロック時" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "ブラウザ再起動時" }, @@ -1035,10 +1044,10 @@ "message": "編集されたアイテム" }, "savedWebsite": { - "message": "Saved website" + "message": "保存されたウェブサイト" }, "savedWebsites": { - "message": "Saved websites ( $COUNT$ )", + "message": "保存されたウェブサイト ($COUNT$ 件)", "placeholders": { "count": { "content": "$1", @@ -1145,10 +1154,10 @@ "description": "Tooltip and Aria label for edit button on cipher item" }, "newNotification": { - "message": "New notification" + "message": "新しい通知" }, "labelWithNotification": { - "message": "$LABEL$: New notification", + "message": "$LABEL$: 新しい通知", "description": "Label for the notification with a new login suggestion.", "placeholders": { "label": { @@ -1190,11 +1199,11 @@ "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { - "message": "Save login", + "message": "ログインを保存", "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { - "message": "Update existing login", + "message": "既存のログイン情報を更新", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { @@ -1561,13 +1570,13 @@ "message": "セキュリティキーの読み取り" }, "readingPasskeyLoading": { - "message": "Reading passkey..." + "message": "パスキーを読み込み中…" }, "passkeyAuthenticationFailed": { - "message": "Passkey authentication failed" + "message": "パスキー認証に失敗しました" }, "useADifferentLogInMethod": { - "message": "Use a different log in method" + "message": "別のログイン方法を使用する" }, "awaitingSecurityKeyInteraction": { "message": "セキュリティキーとの通信を待ち受け中…" @@ -1636,7 +1645,7 @@ "message": "ベース サーバー URL または少なくとも 1 つのカスタム環境を追加する必要があります。" }, "selfHostedEnvMustUseHttps": { - "message": "URLs must use HTTPS." + "message": "URL は HTTPS を使用する必要があります。" }, "customEnvironment": { "message": "カスタム環境" @@ -1689,7 +1698,7 @@ } }, "turnOffAutofill": { - "message": "Turn off autofill" + "message": "自動入力をオフにする" }, "confirmAutofill": { "message": "Confirm autofill" @@ -1713,7 +1722,7 @@ "message": "Autofill without adding" }, "doNotAutofill": { - "message": "Do not autofill" + "message": "自動入力しない" }, "showInlineMenuIdentitiesLabel": { "message": "ID を候補として表示する" @@ -1901,7 +1910,7 @@ "message": "セキュリティコード" }, "cardNumber": { - "message": "card number" + "message": "カード番号" }, "ex": { "message": "例:" @@ -2003,30 +2012,30 @@ "message": "SSH 鍵" }, "typeNote": { - "message": "Note" + "message": "メモ" }, "newItemHeaderLogin": { - "message": "New Login", + "message": "新規ログイン", "description": "Header for new login item type" }, "newItemHeaderCard": { - "message": "New Card", + "message": "新規カード", "description": "Header for new card item type" }, "newItemHeaderIdentity": { - "message": "New Identity", + "message": "新規身分証", "description": "Header for new identity item type" }, "newItemHeaderNote": { - "message": "New Note", + "message": "新規メモ", "description": "Header for new note item type" }, "newItemHeaderSshKey": { - "message": "New SSH key", + "message": "新しい SSH キー", "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "新しい Send テキスト", "description": "Header for new text send" }, "newItemHeaderFileSend": { @@ -2034,11 +2043,11 @@ "description": "Header for new file send" }, "editItemHeaderLogin": { - "message": "Edit Login", + "message": "ログインを編集", "description": "Header for edit login item type" }, "editItemHeaderCard": { - "message": "Edit Card", + "message": "カードを編集", "description": "Header for edit card item type" }, "editItemHeaderIdentity": { @@ -2046,15 +2055,15 @@ "description": "Header for edit identity item type" }, "editItemHeaderNote": { - "message": "Edit Note", + "message": "メモを編集", "description": "Header for edit note item type" }, "editItemHeaderSshKey": { - "message": "Edit SSH key", + "message": "SSH キーを編集", "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Send テキストを編集", "description": "Header for edit text send" }, "editItemHeaderFileSend": { @@ -2062,11 +2071,11 @@ "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "ログインを表示", "description": "Header for view login item type" }, "viewItemHeaderCard": { - "message": "View Card", + "message": "カードを表示", "description": "Header for view card item type" }, "viewItemHeaderIdentity": { @@ -2074,11 +2083,11 @@ "description": "Header for view identity item type" }, "viewItemHeaderNote": { - "message": "View Note", + "message": "メモを表示", "description": "Header for view note item type" }, "viewItemHeaderSshKey": { - "message": "View SSH key", + "message": "SSH キーを表示", "description": "Header for view SSH key item type" }, "passwordHistory": { @@ -2337,7 +2346,7 @@ "message": "このパスワードを使用する" }, "useThisPassphrase": { - "message": "Use this passphrase" + "message": "このパスフレーズを使用" }, "useThisUsername": { "message": "このユーザー名を使用する" @@ -2654,7 +2663,7 @@ "message": "変更" }, "changePassword": { - "message": "Change password", + "message": "パスワードを変更", "description": "Change password button for browser at risk notification on login." }, "changeButtonTitle": { @@ -2667,7 +2676,7 @@ } }, "atRiskPassword": { - "message": "At-risk password" + "message": "リスクがあるパスワード" }, "atRiskPasswords": { "message": "リスクがあるパスワード" @@ -2843,7 +2852,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCountReached": { - "message": "Max access count reached", + "message": "最大アクセス数に達しました", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "hideTextByDefault": { @@ -3193,7 +3202,7 @@ "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." }, "organizationName": { - "message": "Organization name" + "message": "組織名" }, "keyConnectorDomain": { "message": "Key Connector domain" @@ -3605,7 +3614,7 @@ "message": "リクエストが送信されました" }, "loginRequestApprovedForEmailOnDevice": { - "message": "Login request approved for $EMAIL$ on $DEVICE$", + "message": "$EMAIL$ に $DEVICE$ でのログインを承認しました", "placeholders": { "email": { "content": "$1", @@ -3618,16 +3627,16 @@ } }, "youDeniedLoginAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + "message": "別のデバイスからのログイン試行を拒否しました。自分自身である場合は、もう一度デバイスでログインしてください。" }, "device": { - "message": "Device" + "message": "デバイス" }, "loginStatus": { - "message": "Login status" + "message": "ログイン状態" }, "masterPasswordChanged": { - "message": "Master password saved" + "message": "マスターパスワードが保存されました" }, "exposedMasterPassword": { "message": "流出したマスターパスワード" @@ -3720,28 +3729,28 @@ "message": "このデバイスを記憶して今後のログインをシームレスにする" }, "manageDevices": { - "message": "Manage devices" + "message": "デバイスを管理" }, "currentSession": { - "message": "Current session" + "message": "現在のセッション" }, "mobile": { - "message": "Mobile", + "message": "モバイル", "description": "Mobile app" }, "extension": { - "message": "Extension", + "message": "拡張機能", "description": "Browser extension/addon" }, "desktop": { - "message": "Desktop", + "message": "デスクトップ", "description": "Desktop app" }, "webVault": { - "message": "Web vault" + "message": "ウェブ保管庫" }, "webApp": { - "message": "Web app" + "message": "Web アプリ" }, "cli": { "message": "CLI" @@ -3751,22 +3760,22 @@ "description": "Software Development Kit" }, "requestPending": { - "message": "Request pending" + "message": "保留中のリクエスト" }, "firstLogin": { - "message": "First login" + "message": "初回ログイン" }, "trusted": { - "message": "Trusted" + "message": "信頼済み" }, "needsApproval": { - "message": "Needs approval" + "message": "承認が必要" }, "devices": { - "message": "Devices" + "message": "デバイス" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "$EMAIL$ によるログインの試行", "placeholders": { "email": { "content": "$1", @@ -3775,31 +3784,31 @@ } }, "confirmAccess": { - "message": "Confirm access" + "message": "アクセスの確認" }, "denyAccess": { - "message": "Deny access" + "message": "アクセスを拒否" }, "time": { - "message": "Time" + "message": "時間" }, "deviceType": { - "message": "Device Type" + "message": "デバイス種別" }, "loginRequest": { - "message": "Login request" + "message": "ログインリクエスト" }, "thisRequestIsNoLongerValid": { - "message": "This request is no longer valid." + "message": "このリクエストは無効になりました。" }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "ログインリクエストの有効期限が切れています。" }, "justNow": { - "message": "Just now" + "message": "たった今" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "$MINUTES$ 分前に要求されました", "placeholders": { "minutes": { "content": "$1", @@ -3829,7 +3838,7 @@ "message": "管理者の承認を要求する" }, "unableToCompleteLogin": { - "message": "Unable to complete login" + "message": "ログインを完了できません" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { "message": "You need to log in on a trusted device or ask your administrator to assign you a password." @@ -3899,13 +3908,13 @@ "message": "Trust organization" }, "trust": { - "message": "Trust" + "message": "信頼する" }, "doNotTrust": { - "message": "Do not trust" + "message": "信頼しない" }, "organizationNotTrusted": { - "message": "Organization is not trusted" + "message": "組織は信頼されていません" }, "emergencyAccessTrustWarning": { "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" @@ -3920,11 +3929,11 @@ "message": "Trust user" }, "sendsTitleNoItems": { - "message": "Send sensitive information safely", + "message": "機密情報を安全に送信", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", + "message": "どのプラットフォームでも、誰とでも安全にファイルとデータを共有できます。流出を防止しながら、あなたの情報はエンドツーエンドで暗号化されます。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -4051,13 +4060,13 @@ "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "cannotAutofill": { - "message": "Cannot autofill" + "message": "自動入力できません" }, "cannotAutofillExactMatch": { "message": "Default matching is set to 'Exact Match'. The current website does not exactly match the saved login details for this item." }, "okay": { - "message": "Okay" + "message": "OK" }, "toggleSideNavigation": { "message": "サイドナビゲーションの切り替え" @@ -4268,10 +4277,10 @@ "message": "コレクションを選択" }, "importTargetHintCollection": { - "message": "Select this option if you want the imported file contents moved to a collection" + "message": "インポートしたファイルコンテンツをコレクションに移動したい場合は、このオプションを選択してください" }, "importTargetHintFolder": { - "message": "Select this option if you want the imported file contents moved to a folder" + "message": "インポートしたファイルコンテンツをフォルダーに移動したい場合は、このオプションを選択してください" }, "importUnassignedItemsError": { "message": "割り当てられていないアイテムがファイルに含まれています。" @@ -4524,7 +4533,7 @@ "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { - "message": "Advanced options", + "message": "高度な設定", "description": "Advanced option placeholder for uri option component" }, "confirmContinueToBrowserSettingsTitle": { @@ -4711,7 +4720,7 @@ } }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "$FIELD$ ($CIPHERNAME$) をコピー", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -4858,31 +4867,31 @@ } }, "downloadBitwarden": { - "message": "Download Bitwarden" + "message": "Bitwarden をダウンロード" }, "downloadBitwardenOnAllDevices": { - "message": "Download Bitwarden on all devices" + "message": "すべてのデバイスに Bitwarden をダウンロード" }, "getTheMobileApp": { - "message": "Get the mobile app" + "message": "モバイルアプリを入手" }, "getTheMobileAppDesc": { "message": "Access your passwords on the go with the Bitwarden mobile app." }, "getTheDesktopApp": { - "message": "Get the desktop app" + "message": "デスクトップアプリを入手" }, "getTheDesktopAppDesc": { "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." }, "downloadFromBitwardenNow": { - "message": "Download from bitwarden.com now" + "message": "bitwarden.com から今すぐダウンロード" }, "getItOnGooglePlay": { - "message": "Get it on Google Play" + "message": "Google Play で入手" }, "downloadOnTheAppStore": { - "message": "Download on the App Store" + "message": "App Store からダウンロード" }, "permanentlyDeleteAttachmentConfirmation": { "message": "この添付ファイルを完全に削除してもよろしいですか?" @@ -4975,7 +4984,7 @@ } }, "defaultLabelWithValue": { - "message": "Default ( $VALUE$ )", + "message": "既定 ( $VALUE$ )", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -5248,7 +5257,7 @@ "message": "拡張機能アイコンにログイン自動入力の候補の数を表示する" }, "accountAccessRequested": { - "message": "Account access requested" + "message": "アカウントへのアクセスが要求されました" }, "confirmAccessAttempt": { "message": "Confirm access attempt for $EMAIL$", @@ -5365,7 +5374,7 @@ "message": "Unlock PIN set" }, "unlockWithBiometricSet": { - "message": "Unlock with biometrics set" + "message": "生体認証でロック解除を設定しました" }, "authenticating": { "message": "認証中" @@ -5379,7 +5388,7 @@ "description": "Notification message for when a password has been regenerated" }, "saveToBitwarden": { - "message": "Save to Bitwarden", + "message": "Bitwarden へ保存", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { @@ -5587,13 +5596,13 @@ "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, "missingWebsite": { - "message": "Missing website" + "message": "ウェブサイトがありません" }, "settingsVaultOptions": { "message": "保管庫オプション" }, "emptyVaultDescription": { - "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + "message": "保管庫はパスワードだけではなく、ログイン情報、ID、カード、メモを安全に保管できます。" }, "introCarouselLabel": { "message": "Bitwarden へようこそ" @@ -5623,19 +5632,19 @@ "message": "Bitwarden のモバイル、ブラウザ、デスクトップアプリでは、保存できるパスワード数やデバイス数に制限はありません。" }, "nudgeBadgeAria": { - "message": "1 notification" + "message": "1件の通知" }, "emptyVaultNudgeTitle": { - "message": "Import existing passwords" + "message": "既存のパスワードをインポート" }, "emptyVaultNudgeBody": { "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." }, "emptyVaultNudgeButton": { - "message": "Import now" + "message": "今すぐインポート" }, "hasItemsVaultNudgeTitle": { - "message": "Welcome to your vault!" + "message": "保管庫へようこそ!" }, "phishingPageTitleV2": { "message": "Phishing attempt detected" @@ -5644,7 +5653,7 @@ "message": "The site you are attempting to visit is a known malicious site and a security risk." }, "phishingPageCloseTabV2": { - "message": "Close this tab" + "message": "このタブを閉じる" }, "phishingPageContinueV2": { "message": "Continue to this site (not recommended)" @@ -5661,7 +5670,7 @@ "message": "Learn more about phishing detection" }, "protectedBy": { - "message": "Protected by $PRODUCT$", + "message": "$PRODUCT$ によって保護されています", "placeholders": { "product": { "content": "$1", @@ -5687,7 +5696,7 @@ "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyBold": { - "message": "Website", + "message": "ウェブサイト", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, @@ -5715,20 +5724,20 @@ "message": "With notes, securely store sensitive data like banking or insurance details." }, "newSshNudgeTitle": { - "message": "Developer-friendly SSH access" + "message": "開発者フレンドリーの SSH アクセス" }, "newSshNudgeBodyOne": { - "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication.", + "message": "SSHエージェントにキーを登録することで、高速かつ暗号化された認証が可能になります。", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "newSshNudgeBodyTwo": { - "message": "Learn more about SSH agent", + "message": "SSH エージェントに関する詳細", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "パスワードをすばやく作成" }, "generatorNudgeBodyOne": { "message": "Easily create strong and unique passwords by clicking on", @@ -5745,7 +5754,7 @@ "description": "Aria label for the body content of the generator nudge" }, "aboutThisSetting": { - "message": "About this setting" + "message": "この設定について" }, "permitCipherDetailsDescription": { "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." @@ -5758,13 +5767,13 @@ "description": "'WebAssembly' is a technical term and should not be translated." }, "showMore": { - "message": "Show more" + "message": "もっと見る" }, "showLess": { - "message": "Show less" + "message": "隠す" }, "next": { - "message": "Next" + "message": "次へ" }, "moreBreadcrumbs": { "message": "More breadcrumbs", @@ -5777,16 +5786,16 @@ "message": "Great job securing your at-risk logins!" }, "upgradeNow": { - "message": "Upgrade now" + "message": "今すぐアップグレード" }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "認証機を内蔵" }, "secureFileStorage": { "message": "Secure file storage" }, "emergencyAccess": { - "message": "Emergency access" + "message": "緊急アクセス" }, "breachMonitoring": { "message": "Breach monitoring" @@ -5798,16 +5807,34 @@ "message": "Complete online security" }, "upgradeToPremium": { - "message": "Upgrade to Premium" + "message": "プレミアムにアップグレード" + }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." }, "zipPostalCodeLabel": { - "message": "ZIP / Postal code" + "message": "郵便番号" }, "cardNumberLabel": { - "message": "Card number" + "message": "カード番号" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index eaa5bc43021..e6e8becd50a 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 39e6c0be881..cb1a5c089fe 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index e5adcfce833..c83d63762d6 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "ಸಿಸ್ಟಮ್ ಲಾಕ್‌ನಲ್ಲಿ" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "ಬ್ರೌಸರ್ ಮರುಪ್ರಾರಂಭದಲ್ಲಿ" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 6037d208b42..50f49833d50 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "로그인 보기" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "시스템 잠금 시" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "브라우저 재시작 시" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 8e858de4f47..21cd9ca401c 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Užrakinant sistemą" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Paleidus iš naujo naršyklę" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index a4be22d433a..088d671a2b5 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Apskatīt visu" }, + "viewLess": { + "message": "Skatīt mazāk" + }, "viewLogin": { "message": "Apskatīt pieteikšanās vienumu" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Pēc sistēmas aizslēgšanas" }, + "onIdle": { + "message": "Sistēmas dīkstāvē" + }, + "onSleep": { + "message": "Pēc sistēmas iemigšanas" + }, "onRestart": { "message": "Pēc pārlūka pārsāknēšanas" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Uzlabot uz Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Ielādē glabātavu" + }, + "vaultLoaded": { + "message": "Glabātava ielādēta" + }, "settingDisabledByPolicy": { "message": "Šis iestatījums ir atspējots apvienības pamatnostādnēs.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Kartes numurs" + }, + "sessionTimeoutSettingsAction": { + "message": "Noildzes darbība" } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index cda9ec03923..d76f579fac4 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "സിസ്റ്റം ലോക്കിൽ" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "ബ്രൌസർ പുനരാരംഭിക്കുമ്പോൾ" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 57624a82381..acbbc97ceac 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 39e6c0be881..cb1a5c089fe 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 1268c960c8f..3933f66c541 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Ved maskinlåsing" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Ved nettleseromstart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 39e6c0be881..cb1a5c089fe 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 441ea71d840..a4d38f6c9ba 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Alles weergeven" }, + "viewLess": { + "message": "Minder weergeven" + }, "viewLogin": { "message": "Login bekijken" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Bij systeemvergrendeling" }, + "onIdle": { + "message": "Bij systeeminactiviteit" + }, + "onSleep": { + "message": "Bij slaapmodus" + }, "onRestart": { "message": "Bij herstart van de browser" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Opwaarderen naar Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade voor volledige beveiliging" + }, + "premiumGivesMoreTools": { + "message": "Premium geeft je meer tools om veilig te blijven, efficiënt te werken en in controle te blijven." + }, + "explorePremium": { + "message": "Premium verkennen" + }, + "loadingVault": { + "message": "Kluis laden" + }, + "vaultLoaded": { + "message": "Kluis geladen" + }, "settingDisabledByPolicy": { "message": "Deze instelling is uitgeschakeld door het beleid van uw organisatie.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Kaartnummer" + }, + "sessionTimeoutSettingsAction": { + "message": "Time-out actie" } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 39e6c0be881..cb1a5c089fe 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 39e6c0be881..cb1a5c089fe 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 1047ac9466e..0847126f33e 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "Pokaż dane logowania" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Po zablokowaniu urządzenia" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Po uruchomieniu przeglądarki" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Numer karty" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index a4da9025a8e..dbd7229db2a 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Ver tudo" }, + "viewLess": { + "message": "Ver menos" + }, "viewLogin": { "message": "Ver credencial" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Ao bloquear o sistema" }, + "onIdle": { + "message": "Quando o sistema ficar inativo" + }, + "onSleep": { + "message": "Quando o sistema hibernar" + }, "onRestart": { "message": "Ao reiniciar o navegador" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Faça upgrade para o Premium" }, + "upgradeCompleteSecurity": { + "message": "Faça upgrade para segurança completa" + }, + "premiumGivesMoreTools": { + "message": "O Premium te oferece mais ferramentas para se permanecer seguro, trabalhar eficientemente, e manter o controle." + }, + "explorePremium": { + "message": "Explorar o Premium" + }, + "loadingVault": { + "message": "Carregando cofre" + }, + "vaultLoaded": { + "message": "Cofre carregado" + }, "settingDisabledByPolicy": { "message": "Essa configuração está desativada pela política da sua organização.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Número do cartão" + }, + "sessionTimeoutSettingsAction": { + "message": "Ação do tempo limite" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 15c993ab768..55065838ec8 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Ver tudo" }, + "viewLess": { + "message": "Ver menos" + }, "viewLogin": { "message": "Ver credencial" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Ao bloquear o sistema" }, + "onIdle": { + "message": "Na inatividade do sistema" + }, + "onSleep": { + "message": "Na suspensão do sistema" + }, "onRestart": { "message": "Ao reiniciar o navegador" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Atualizar para o Premium" }, + "upgradeCompleteSecurity": { + "message": "Atualize para obter segurança total" + }, + "premiumGivesMoreTools": { + "message": "O Premium oferece mais ferramentas para manter a segurança, trabalhar com eficiência e manter o controlo." + }, + "explorePremium": { + "message": "Explorar o Premium" + }, + "loadingVault": { + "message": "A carregar o cofre" + }, + "vaultLoaded": { + "message": "Cofre carregado" + }, "settingDisabledByPolicy": { "message": "Esta configuração está desativada pela política da sua organização.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Número do cartão" + }, + "sessionTimeoutSettingsAction": { + "message": "Ação de tempo limite" } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 4e1ac8ae832..84a1937bf14 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "La blocarea sistemului" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "La repornirea browserului" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index d59fc34f736..22e9052fe43 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Посмотреть все" }, + "viewLess": { + "message": "Свернуть" + }, "viewLogin": { "message": "Просмотр логина" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Вместе с компьютером" }, + "onIdle": { + "message": "При бездействии" + }, + "onSleep": { + "message": "В режиме сна" + }, "onRestart": { "message": "При перезапуске браузера" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Обновить до Премиум" }, + "upgradeCompleteSecurity": { + "message": "Перейти для полной защищенности" + }, + "premiumGivesMoreTools": { + "message": "Премиум предоставит вам больше инструментов для обеспечения безопасности, эффективной работы и контроля над ситуацией." + }, + "explorePremium": { + "message": "Познакомиться с Премиум" + }, + "loadingVault": { + "message": "Загрузка хранилища" + }, + "vaultLoaded": { + "message": "Хранилище загружено" + }, "settingDisabledByPolicy": { "message": "Этот параметр отключен политикой вашей организации.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Номер карты" + }, + "sessionTimeoutSettingsAction": { + "message": "Тайм-аут действия" } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 8c5961153eb..f4c00449a2b 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "පද්ධතිය ලොක් මත" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "බ්රව්සරය නැවත ආරම්භ" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index b459c86c236..b8b820def35 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Zobraziť všetky" }, + "viewLess": { + "message": "Zobraziť menej" + }, "viewLogin": { "message": "Zobraziť prihlásenie" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Keď je systém uzamknutý" }, + "onIdle": { + "message": "Pri nečinnosti systému" + }, + "onSleep": { + "message": "V režime spánku" + }, "onRestart": { "message": "Po reštarte prehliadača" }, @@ -2186,7 +2195,7 @@ "description": "Default URI match detection for autofill." }, "toggleOptions": { - "message": "Voľby prepínača" + "message": "Zobraziť/skryť možnosti" }, "toggleCurrentUris": { "message": "Prepnúť zobrazenie aktuálnej URI", @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgradovať na Prémium" }, + "upgradeCompleteSecurity": { + "message": "Upgradovať pre úplné zabezpečenie" + }, + "premiumGivesMoreTools": { + "message": "Predplatné Prémium vám poskytuje viac nástrojov na zabezpečenie, efektívnu prácu a kontrolu." + }, + "explorePremium": { + "message": "Preskúmať Prémium" + }, + "loadingVault": { + "message": "Načítava sa trezor" + }, + "vaultLoaded": { + "message": "Trezor sa načítal" + }, "settingDisabledByPolicy": { "message": "Politika organizácie vypla toto nastavenie.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Číslo karty" + }, + "sessionTimeoutSettingsAction": { + "message": "Akcia pri vypršaní časového limitu" } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 0a6266636b3..70d9c5f70c2 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Ob zaklepu sistema" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Ob ponovnem zagonu brskalnika" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 0158ca6ba2b..97fcb37fb58 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -32,7 +32,7 @@ "message": "Употребити једнократну пријаву" }, "yourOrganizationRequiresSingleSignOn": { - "message": "Your organization requires single sign-on." + "message": "Ваша организација захтева јединствену пријаву." }, "welcomeBack": { "message": "Добродошли назад" @@ -592,7 +592,10 @@ "message": "Приказ" }, "viewAll": { - "message": "View all" + "message": "Прегледај све" + }, + "viewLess": { + "message": "View less" }, "viewLogin": { "message": "Преглед пријаве" @@ -796,6 +799,12 @@ "onLocked": { "message": "На закључавање система" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "На покретање прегледача" }, @@ -1035,10 +1044,10 @@ "message": "Ставка уређена" }, "savedWebsite": { - "message": "Saved website" + "message": "Сачувана веб локација" }, "savedWebsites": { - "message": "Saved websites ( $COUNT$ )", + "message": "Сачувана веб локација ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -1636,7 +1645,7 @@ "message": "Морате додати или основни УРЛ сервера или бар једно прилагођено окружење." }, "selfHostedEnvMustUseHttps": { - "message": "URLs must use HTTPS." + "message": "Везе морају да користе HTTPS." }, "customEnvironment": { "message": "Прилагођено окружење" @@ -1692,28 +1701,28 @@ "message": "Угасити ауто-пуњење" }, "confirmAutofill": { - "message": "Confirm autofill" + "message": "Потврди аутопуњење" }, "confirmAutofillDesc": { - "message": "This site doesn't match your saved login details. Before you fill in your login credentials, make sure it's a trusted site." + "message": "Овај сајт се не подудара са вашим сачуваним подацима за пријаву. Пре него што унесете своје акредитиве за пријаву, уверите се да је то поуздан сајт." }, "showInlineMenuLabel": { "message": "Прикажи предлоге за ауто-попуњавање у пољима обрасца" }, "howDoesBitwardenProtectFromPhishing": { - "message": "How does Bitwarden protect your data from phishing?" + "message": "Како Bitwarden штити ваше податке од фишинга?" }, "currentWebsite": { - "message": "Current website" + "message": "Тренутни сајт" }, "autofillAndAddWebsite": { - "message": "Autofill and add this website" + "message": "Ауто-попуни и додај овај сајт" }, "autofillWithoutAdding": { - "message": "Autofill without adding" + "message": "Ауто-попуни без додавања" }, "doNotAutofill": { - "message": "Do not autofill" + "message": "Не попуни" }, "showInlineMenuIdentitiesLabel": { "message": "Приказати идентитете као предлоге" @@ -3277,7 +3286,7 @@ "message": "Грешка при декрипцији" }, "errorGettingAutoFillData": { - "message": "Error getting autofill data" + "message": "Грешка при преузимању података за ауто-попуњавање" }, "couldNotDecryptVaultItemsBelow": { "message": "Bitwarden није могао да декриптује ставке из трезора наведене испод." @@ -4051,13 +4060,13 @@ "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "cannotAutofill": { - "message": "Cannot autofill" + "message": "Не може да се ауто-попуни" }, "cannotAutofillExactMatch": { - "message": "Default matching is set to 'Exact Match'. The current website does not exactly match the saved login details for this item." + "message": "Подразумевано подударање је подешено на „Тачно подударање“. Тренутна веб локација не одговара тачно сачуваним детаљима за пријаву за ову ставку." }, "okay": { - "message": "Okay" + "message": "У реду" }, "toggleSideNavigation": { "message": "Укључите бочну навигацију" @@ -4975,7 +4984,7 @@ } }, "defaultLabelWithValue": { - "message": "Default ( $VALUE$ )", + "message": "Подразумевано ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -5774,40 +5783,58 @@ "message": "Потврдите домен конектора кључа" }, "atRiskLoginsSecured": { - "message": "Great job securing your at-risk logins!" + "message": "Сјајан посао обезбеђивања ваших ризичних пријава!" }, "upgradeNow": { - "message": "Upgrade now" + "message": "Надогради сада" }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "Уграђени аутентификатор" }, "secureFileStorage": { - "message": "Secure file storage" + "message": "Сигурно складиштење датотека" }, "emergencyAccess": { - "message": "Emergency access" + "message": "Хитан приступ" }, "breachMonitoring": { - "message": "Breach monitoring" + "message": "Праћење повreda безбедности" }, "andMoreFeatures": { - "message": "And more!" + "message": "И још више!" }, "planDescPremium": { - "message": "Complete online security" + "message": "Потпуна онлајн безбедност" }, "upgradeToPremium": { - "message": "Upgrade to Premium" + "message": "Надоградите на Premium" + }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" }, "settingDisabledByPolicy": { - "message": "This setting is disabled by your organization's policy.", + "message": "Ово подешавање је онемогућено смерницама ваше организације.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." }, "zipPostalCodeLabel": { - "message": "ZIP / Postal code" + "message": "ZIP/Поштански број" }, "cardNumberLabel": { - "message": "Card number" + "message": "Број картице" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 057a7ca746c..ea7a4d80359 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Visa alla" }, + "viewLess": { + "message": "Visa mindre" + }, "viewLogin": { "message": "Visa inloggning" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Vid låsning av datorn" }, + "onIdle": { + "message": "När systemet är overksamt" + }, + "onSleep": { + "message": "När systemet är i strömsparläge" + }, "onRestart": { "message": "Vid omstart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Uppgradera till Premium" }, + "upgradeCompleteSecurity": { + "message": "Uppgradera för fullständig säkerhet" + }, + "premiumGivesMoreTools": { + "message": "Premium ger dig fler verktyg för att hålla dig säker, arbeta effektivt och ha kontroll." + }, + "explorePremium": { + "message": "Utforska Premium" + }, + "loadingVault": { + "message": "Läser in valv" + }, + "vaultLoaded": { + "message": "Valvet lästes in" + }, "settingDisabledByPolicy": { "message": "Denna inställning är inaktiverad enligt din organisations policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Kortnummer" + }, + "sessionTimeoutSettingsAction": { + "message": "Tidsgränsåtgärd" } } diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 43944875889..a95da4d2059 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "உள்நுழைவைக் காண்க" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "சிஸ்டம் பூட்டப்பட்டவுடன்" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "உலாவி மறுதொடக்கம் செய்யப்பட்டவுடன்" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 39e6c0be881..cb1a5c089fe 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On system lock" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On browser restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index e92192dafa0..9096067ce3b 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "View all" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "View login" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "On Locked" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "On Restart" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Upgrade to Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Card number" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 543560810fe..4c7ca6937a4 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Tümünü göster" }, + "viewLess": { + "message": "Daha az göster" + }, "viewLogin": { "message": "Hesabı göster" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Sistem kilitlenince" }, + "onIdle": { + "message": "Sistem boştayken" + }, + "onSleep": { + "message": "Sistem uyuyunca" + }, "onRestart": { "message": "Tarayıcı yeniden başlatılınca" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Premium'a yükselt" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Kasa yükleniyor" + }, + "vaultLoaded": { + "message": "Kasa yüklendi" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Kart numarası" + }, + "sessionTimeoutSettingsAction": { + "message": "Zaman aşımı eylemi" } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 2c6fa4eb15b..7f8b0f8b13b 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Переглянути все" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "Переглянути запис" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "З блокуванням системи" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "З перезапуском браузера" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Покращити до Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "Цей параметр вимкнено політикою вашої організації.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Номер картки" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 8029f5b2c46..414b4cc2cac 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "Xem tất cả" }, + "viewLess": { + "message": "View less" + }, "viewLogin": { "message": "Xem đăng nhập" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "Mỗi khi khóa máy" }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, "onRestart": { "message": "Mỗi khi khởi động lại trình duyệt" }, @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "Nâng cấp lên gói Cao cấp" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "Loading vault" + }, + "vaultLoaded": { + "message": "Vault loaded" + }, "settingDisabledByPolicy": { "message": "Cài đặt này bị vô hiệu hóa bởi chính sách tổ chức của bạn.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "Số thẻ" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index e59a74e358d..548bdd9e178 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "查看全部" }, + "viewLess": { + "message": "查看更少" + }, "viewLogin": { "message": "查看登录" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "系统锁定时" }, + "onIdle": { + "message": "系统空闲时" + }, + "onSleep": { + "message": "系统睡眠时" + }, "onRestart": { "message": "浏览器重启时" }, @@ -1476,7 +1485,7 @@ "message": "优先客户支持。" }, "ppremiumSignUpFuture": { - "message": "未来的更多高级功能。敬请期待!" + "message": "未来的更多高级版功能。敬请期待!" }, "premiumPurchase": { "message": "购买高级版" @@ -3726,7 +3735,7 @@ "message": "当前会话" }, "mobile": { - "message": "移动", + "message": "移动端", "description": "Mobile app" }, "extension": { @@ -4888,7 +4897,7 @@ "message": "确定要永久删除此附件吗?" }, "premium": { - "message": "高级会员" + "message": "高级版" }, "freeOrgsCannotUseAttachments": { "message": "免费组织无法使用附件" @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "升级为高级版" }, + "upgradeCompleteSecurity": { + "message": "升级以获得全面的安全防护" + }, + "premiumGivesMoreTools": { + "message": "高级版为您提供更多工具,助您保障安全、高效工作并掌控一切。" + }, + "explorePremium": { + "message": "探索高级版" + }, + "loadingVault": { + "message": "正在加载密码库" + }, + "vaultLoaded": { + "message": "密码库已加载" + }, "settingDisabledByPolicy": { "message": "此设置被您组织的策略禁用了。", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "卡号" + }, + "sessionTimeoutSettingsAction": { + "message": "超时动作" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 63f3ea59f60..600447a29f3 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -594,6 +594,9 @@ "viewAll": { "message": "檢視全部" }, + "viewLess": { + "message": "顯示較少" + }, "viewLogin": { "message": "檢視登入" }, @@ -796,6 +799,12 @@ "onLocked": { "message": "於系統鎖定時" }, + "onIdle": { + "message": "系統閒置時" + }, + "onSleep": { + "message": "系統睡眠時" + }, "onRestart": { "message": "於瀏覽器重新啟動時" }, @@ -4975,7 +4984,7 @@ } }, "defaultLabelWithValue": { - "message": "Default ( $VALUE$ )", + "message": "預設 ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -5800,6 +5809,21 @@ "upgradeToPremium": { "message": "升級到 Premium" }, + "upgradeCompleteSecurity": { + "message": "Upgrade for complete security" + }, + "premiumGivesMoreTools": { + "message": "Premium gives you more tools to stay secure, work efficiently, and stay in control." + }, + "explorePremium": { + "message": "Explore Premium" + }, + "loadingVault": { + "message": "正在載入密碼庫" + }, + "vaultLoaded": { + "message": "已載入密碼庫" + }, "settingDisabledByPolicy": { "message": "此設定已被你的組織原則停用。", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." @@ -5809,5 +5833,8 @@ }, "cardNumberLabel": { "message": "支付卡號碼" + }, + "sessionTimeoutSettingsAction": { + "message": "逾時後動作" } } diff --git a/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts index a64cea1ef3e..12cf669d89b 100644 --- a/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts +++ b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts @@ -1,7 +1,12 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +import { + ButtonModule, + CenterPositionStrategy, + DialogModule, + DialogService, +} from "@bitwarden/components"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -11,6 +16,8 @@ import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components }) export class AwaitDesktopDialogComponent { static open(dialogService: DialogService) { - return dialogService.open(AwaitDesktopDialogComponent); + return dialogService.open(AwaitDesktopDialogComponent, { + positionStrategy: new CenterPositionStrategy(), + }); } } diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index f9e2e1c534f..8df21bc66ef 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1530,5 +1530,63 @@ describe("NotificationBackground", () => { expect(environmentServiceSpy).toHaveBeenCalled(); }); }); + + describe("handleUnlockPopoutClosed", () => { + let onRemovedListeners: Array<(tabId: number, removeInfo: chrome.tabs.OnRemovedInfo) => void>; + let tabsQuerySpy: jest.SpyInstance; + + beforeEach(() => { + onRemovedListeners = []; + chrome.tabs.onRemoved.addListener = jest.fn((listener) => { + onRemovedListeners.push(listener); + }); + chrome.runtime.getURL = jest.fn().mockReturnValue("chrome-extension://id/popup/index.html"); + notificationBackground.init(); + }); + + const triggerTabRemoved = async (tabId: number) => { + onRemovedListeners[0](tabId, mock()); + await flushPromises(); + }; + + it("sends abandon message when unlock popout is closed and vault is locked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsQuerySpy = jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([]); + + await triggerTabRemoved(1); + + expect(tabsQuerySpy).toHaveBeenCalled(); + expect(messagingService.send).toHaveBeenCalledWith("abandonAutofillPendingNotifications"); + }); + + it("uses tracked tabId for fast lookup when available", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsQuerySpy = jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([ + { + id: 123, + url: "chrome-extension://id/popup/index.html?singleActionPopout=auth_unlockExtension", + } as chrome.tabs.Tab, + ]); + + await triggerTabRemoved(999); + tabsQuerySpy.mockClear(); + messagingService.send.mockClear(); + + await triggerTabRemoved(123); + + expect(tabsQuerySpy).not.toHaveBeenCalled(); + expect(messagingService.send).toHaveBeenCalledWith("abandonAutofillPendingNotifications"); + }); + + it("returns early when vault is unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + tabsQuerySpy = jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([]); + + await triggerTabRemoved(1); + + expect(tabsQuerySpy).not.toHaveBeenCalled(); + expect(messagingService.send).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index e27b50f13cd..de1514f0342 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -45,7 +45,7 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task // FIXME (PM-22628): Popup imports are forbidden in background // eslint-disable-next-line no-restricted-imports -import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; +import { AuthPopoutType, openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; // FIXME (PM-22628): Popup imports are forbidden in background // eslint-disable-next-line no-restricted-imports @@ -89,6 +89,7 @@ export default class NotificationBackground { ExtensionCommand.AutofillCard, ExtensionCommand.AutofillIdentity, ]); + private unlockPopoutTabId?: number; private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { bgAdjustNotificationBar: ({ message, sender }) => this.handleAdjustNotificationBarMessage(message, sender), @@ -146,6 +147,7 @@ export default class NotificationBackground { } this.setupExtensionMessageListener(); + this.setupUnlockPopoutCloseListener(); this.cleanupNotificationQueue(); } @@ -1163,6 +1165,7 @@ export default class NotificationBackground { message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ): Promise { + this.unlockPopoutTabId = undefined; const messageData = message.data as LockedVaultPendingNotificationsData; const retryCommand = messageData.commandToRetry.message.command as ExtensionCommandType; if (this.allowedRetryCommands.has(retryCommand)) { @@ -1313,4 +1316,43 @@ export default class NotificationBackground { const tabDomain = Utils.getDomain(tab.url); return tabDomain === queueMessage.domain || tabDomain === Utils.getDomain(queueMessage.tab.url); } + + private setupUnlockPopoutCloseListener() { + chrome.tabs.onRemoved.addListener(async (tabId: number) => { + await this.handleUnlockPopoutClosed(tabId); + }); + } + + /** + * If the unlock popout is closed while the vault + * is still locked and there are pending autofill notifications, abandon them. + */ + private async handleUnlockPopoutClosed(removedTabId: number) { + const authStatus = await this.getAuthStatus(); + if (authStatus >= AuthenticationStatus.Unlocked) { + this.unlockPopoutTabId = undefined; + return; + } + + if (this.unlockPopoutTabId === removedTabId) { + this.unlockPopoutTabId = undefined; + this.messagingService.send("abandonAutofillPendingNotifications"); + return; + } + + if (this.unlockPopoutTabId) { + return; + } + + const extensionUrl = chrome.runtime.getURL("popup/index.html"); + const unlockPopoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter( + (tab) => tab.url?.includes(`singleActionPopout=${AuthPopoutType.unlockExtension}`), + ); + + if (unlockPopoutTabs.length === 0) { + this.messagingService.send("abandonAutofillPendingNotifications"); + } else if (unlockPopoutTabs[0].id) { + this.unlockPopoutTabId = unlockPopoutTabs[0].id; + } + } } diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 80e453e9e83..50fb291b121 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -3286,6 +3286,9 @@ describe("OverlayBackground", () => { pageDetails: [pageDetailsForTab], fillNewPassword: true, allowTotpAutofill: true, + focusedFieldForm: undefined, + focusedFieldOpid: undefined, + inlineMenuFillType: undefined, }); expect(overlayBackground["inlineMenuCiphers"].entries()).toStrictEqual( new Map([ @@ -3680,6 +3683,9 @@ describe("OverlayBackground", () => { pageDetails: [overlayBackground["pageDetailsForTab"][sender.tab.id].get(sender.frameId)], fillNewPassword: true, allowTotpAutofill: false, + focusedFieldForm: undefined, + focusedFieldOpid: undefined, + inlineMenuFillType: InlineMenuFillTypes.PasswordGeneration, }); }); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index f3278fa6b07..225cbbe66ca 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1177,6 +1177,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { allowTotpAutofill: true, focusedFieldForm: this.focusedFieldData?.focusedFieldForm, focusedFieldOpid: this.focusedFieldData?.focusedFieldOpid, + inlineMenuFillType: this.focusedFieldData?.inlineMenuFillType, }); if (totpCode) { @@ -1863,6 +1864,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { allowTotpAutofill: false, focusedFieldForm: this.focusedFieldData?.focusedFieldForm, focusedFieldOpid: this.focusedFieldData?.focusedFieldOpid, + inlineMenuFillType: InlineMenuFillTypes.PasswordGeneration, }); globalThis.setTimeout(async () => { diff --git a/apps/browser/src/autofill/content/content-message-handler.ts b/apps/browser/src/autofill/content/content-message-handler.ts index c57b2d959f3..63afc215923 100644 --- a/apps/browser/src/autofill/content/content-message-handler.ts +++ b/apps/browser/src/autofill/content/content-message-handler.ts @@ -86,17 +86,30 @@ function handleOpenBrowserExtensionToUrlMessage({ url }: { url?: ExtensionPageUr } /** - * Handles the window message event. + * Handles window message events, validating source and extracting referrer for security. * * @param event - The window message event */ function handleWindowMessageEvent(event: MessageEvent) { - const { source, data } = event; + const { source, data, origin } = event; if (source !== window || !data?.command) { return; } - const referrer = source.location.hostname; + // Extract hostname from event.origin for secure referrer validation in background script + let referrer: string; + // Sandboxed iframe or opaque origin support + if (origin === "null") { + referrer = "null"; + } else { + try { + const originUrl = new URL(origin); + referrer = originUrl.hostname; + } catch { + return; + } + } + const handler = windowMessageHandlers[data.command]; if (handler) { handler({ data, referrer }); diff --git a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts index 6ad069ad56e..b341be28ebb 100644 --- a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts @@ -13,6 +13,7 @@ type SharedFido2ScriptRegistrationOptions = SharedFido2ScriptInjectionDetails & matches: string[]; excludeMatches: string[]; allFrames: true; + world?: "MAIN" | "ISOLATED"; }; type Fido2ExtensionMessage = { diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts index 752851b3d37..adb59b8f845 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts @@ -203,6 +203,7 @@ describe("Fido2Background", () => { { file: Fido2ContentScript.PageScriptDelayAppend }, { file: Fido2ContentScript.ContentScript }, ], + world: "MAIN", ...sharedRegistrationOptions, }); }); diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.ts b/apps/browser/src/autofill/fido2/background/fido2.background.ts index 22ee4a1822d..a8b016a14d6 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.ts @@ -176,6 +176,7 @@ export class Fido2Background implements Fido2BackgroundInterface { { file: await this.getFido2PageScriptAppendFileName() }, { file: Fido2ContentScript.ContentScript }, ], + world: "MAIN", ...this.sharedRegistrationOptions, }); } diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts index a147e0ba165..af60d1de77d 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts @@ -5,6 +5,7 @@ import { InlineMenuCipherData } from "../../../background/abstractions/overlay.b export type AutofillInlineMenuContainerMessage = { command: string; portKey: string; + token?: string; }; export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMessage & { diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts index f1a74556b24..b7bd24c537b 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts @@ -53,13 +53,35 @@ describe("AutofillInlineMenuContentService", () => { }); }); + describe("messageHandlers", () => { + it("returns the extension message handlers", () => { + const handlers = autofillInlineMenuContentService.messageHandlers; + + expect(handlers).toHaveProperty("closeAutofillInlineMenu"); + expect(handlers).toHaveProperty("appendAutofillInlineMenuToDom"); + }); + }); + describe("isElementInlineMenu", () => { - it("returns true if the passed element is the inline menu", () => { + it("returns true if the passed element is the inline menu list", () => { const element = document.createElement("div"); autofillInlineMenuContentService["listElement"] = element; expect(autofillInlineMenuContentService.isElementInlineMenu(element)).toBe(true); }); + + it("returns true if the passed element is the inline menu button", () => { + const element = document.createElement("div"); + autofillInlineMenuContentService["buttonElement"] = element; + + expect(autofillInlineMenuContentService.isElementInlineMenu(element)).toBe(true); + }); + + it("returns false if the passed element is not the inline menu", () => { + const element = document.createElement("div"); + + expect(autofillInlineMenuContentService.isElementInlineMenu(element)).toBe(false); + }); }); describe("extension message handlers", () => { @@ -388,7 +410,7 @@ describe("AutofillInlineMenuContentService", () => { }); it("closes the inline menu if the page body is not sufficiently opaque", async () => { - document.querySelector("html").style.opacity = "0.9"; + document.documentElement.style.opacity = "0.9"; document.body.style.opacity = "0"; await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); @@ -397,7 +419,7 @@ describe("AutofillInlineMenuContentService", () => { }); it("closes the inline menu if the page html is not sufficiently opaque", async () => { - document.querySelector("html").style.opacity = "0.3"; + document.documentElement.style.opacity = "0.3"; document.body.style.opacity = "0.7"; await autofillInlineMenuContentService["handlePageMutations"]([mockHTMLMutationRecord]); @@ -406,7 +428,7 @@ describe("AutofillInlineMenuContentService", () => { }); it("does not close the inline menu if the page html and body is sufficiently opaque", async () => { - document.querySelector("html").style.opacity = "0.9"; + document.documentElement.style.opacity = "0.9"; document.body.style.opacity = "1"; await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); await waitForIdleCallback(); @@ -599,5 +621,465 @@ describe("AutofillInlineMenuContentService", () => { overlayElement: AutofillOverlayElement.List, }); }); + + it("clears the persistent last child override timeout", () => { + jest.useFakeTimers(); + const clearTimeoutSpy = jest.spyOn(globalThis, "clearTimeout"); + autofillInlineMenuContentService["handlePersistentLastChildOverrideTimeout"] = setTimeout( + jest.fn(), + 500, + ); + + autofillInlineMenuContentService.destroy(); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + }); + + it("unobserves page attributes", () => { + const disconnectSpy = jest.spyOn( + autofillInlineMenuContentService["htmlMutationObserver"], + "disconnect", + ); + + autofillInlineMenuContentService.destroy(); + + expect(disconnectSpy).toHaveBeenCalled(); + }); + }); + + describe("getOwnedTagNames", () => { + it("returns an empty array when no elements are created", () => { + expect(autofillInlineMenuContentService.getOwnedTagNames()).toEqual([]); + }); + + it("returns the button element tag name", () => { + const buttonElement = document.createElement("div"); + autofillInlineMenuContentService["buttonElement"] = buttonElement; + + const tagNames = autofillInlineMenuContentService.getOwnedTagNames(); + + expect(tagNames).toContain("DIV"); + }); + + it("returns both button and list element tag names", () => { + const buttonElement = document.createElement("div"); + const listElement = document.createElement("span"); + autofillInlineMenuContentService["buttonElement"] = buttonElement; + autofillInlineMenuContentService["listElement"] = listElement; + + const tagNames = autofillInlineMenuContentService.getOwnedTagNames(); + + expect(tagNames).toEqual(["DIV", "SPAN"]); + }); + }); + + describe("getUnownedTopLayerItems", () => { + beforeEach(() => { + document.body.innerHTML = ""; + }); + + it("returns the tag names from button and list elements", () => { + const buttonElement = document.createElement("div"); + buttonElement.setAttribute("popover", "manual"); + autofillInlineMenuContentService["buttonElement"] = buttonElement; + + const listElement = document.createElement("span"); + listElement.setAttribute("popover", "manual"); + autofillInlineMenuContentService["listElement"] = listElement; + + /** Mock querySelectorAll to avoid :modal selector issues in jsdom */ + const querySelectorAllSpy = jest + .spyOn(globalThis.document, "querySelectorAll") + .mockReturnValue([] as any); + + const items = autofillInlineMenuContentService.getUnownedTopLayerItems(); + + expect(querySelectorAllSpy).toHaveBeenCalled(); + expect(items.length).toBe(0); + }); + + it("calls querySelectorAll with correct selector when includeCandidates is false", () => { + /** Mock querySelectorAll to avoid :modal selector issues in jsdom */ + const querySelectorAllSpy = jest + .spyOn(globalThis.document, "querySelectorAll") + .mockReturnValue([] as any); + + autofillInlineMenuContentService.getUnownedTopLayerItems(false); + + const calledSelector = querySelectorAllSpy.mock.calls[0][0]; + expect(calledSelector).toContain(":modal"); + expect(calledSelector).toContain(":popover-open"); + }); + + it("includes candidates selector when requested", () => { + /** Mock querySelectorAll to avoid :modal selector issues in jsdom */ + const querySelectorAllSpy = jest + .spyOn(globalThis.document, "querySelectorAll") + .mockReturnValue([] as any); + + autofillInlineMenuContentService.getUnownedTopLayerItems(true); + + const calledSelector = querySelectorAllSpy.mock.calls[0][0]; + expect(calledSelector).toContain("[popover], dialog"); + }); + }); + + describe("refreshTopLayerPosition", () => { + it("does nothing when inline menu is disabled", () => { + const getUnownedTopLayerItemsSpy = jest.spyOn( + autofillInlineMenuContentService, + "getUnownedTopLayerItems", + ); + + autofillInlineMenuContentService["inlineMenuEnabled"] = false; + const buttonElement = document.createElement("div"); + autofillInlineMenuContentService["buttonElement"] = buttonElement; + + autofillInlineMenuContentService.refreshTopLayerPosition(); + + // Should exit early and not call `getUnownedTopLayerItems` + expect(getUnownedTopLayerItemsSpy).not.toHaveBeenCalled(); + }); + + it("does nothing when no other top layer items exist", () => { + const buttonElement = document.createElement("div"); + autofillInlineMenuContentService["buttonElement"] = buttonElement; + jest + .spyOn(autofillInlineMenuContentService, "getUnownedTopLayerItems") + .mockReturnValue([] as any); + + const getElementsByTagSpy = jest.spyOn(globalThis.document, "getElementsByTagName"); + + autofillInlineMenuContentService.refreshTopLayerPosition(); + + // Should exit early and not get inline elements to refresh + expect(getElementsByTagSpy).not.toHaveBeenCalled(); + }); + + it("refreshes button popover when button is in document", () => { + jest + .spyOn(autofillInlineMenuContentService, "getUnownedTopLayerItems") + .mockReturnValue([document.createElement("div")] as any); + + const buttonElement = document.createElement("div"); + buttonElement.setAttribute("popover", "manual"); + buttonElement.showPopover = jest.fn(); + buttonElement.hidePopover = jest.fn(); + document.body.appendChild(buttonElement); + autofillInlineMenuContentService["buttonElement"] = buttonElement; + + autofillInlineMenuContentService.refreshTopLayerPosition(); + + expect(buttonElement.hidePopover).toHaveBeenCalled(); + expect(buttonElement.showPopover).toHaveBeenCalled(); + }); + + it("refreshes list popover when list is in document", () => { + jest + .spyOn(autofillInlineMenuContentService, "getUnownedTopLayerItems") + .mockReturnValue([document.createElement("div")] as any); + + const listElement = document.createElement("div"); + listElement.setAttribute("popover", "manual"); + listElement.showPopover = jest.fn(); + listElement.hidePopover = jest.fn(); + document.body.appendChild(listElement); + autofillInlineMenuContentService["listElement"] = listElement; + + autofillInlineMenuContentService.refreshTopLayerPosition(); + + expect(listElement.hidePopover).toHaveBeenCalled(); + expect(listElement.showPopover).toHaveBeenCalled(); + }); + }); + + describe("checkAndUpdateRefreshCount", () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date("2023-01-01T00:00:00.000Z")); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("does nothing when inline menu is disabled", () => { + autofillInlineMenuContentService["inlineMenuEnabled"] = false; + + autofillInlineMenuContentService["checkAndUpdateRefreshCount"]("topLayer"); + + expect(autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer).toBe(0); + }); + + it("increments refresh count when within time threshold", () => { + autofillInlineMenuContentService["lastTrackedTimestamp"].topLayer = Date.now() - 1000; + + autofillInlineMenuContentService["checkAndUpdateRefreshCount"]("topLayer"); + + expect(autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer).toBe(1); + }); + + it("resets count when outside time threshold", () => { + autofillInlineMenuContentService["lastTrackedTimestamp"].topLayer = Date.now() - 6000; + autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer = 5; + + autofillInlineMenuContentService["checkAndUpdateRefreshCount"]("topLayer"); + + expect(autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer).toBe(0); + }); + + it("disables inline menu and shows alert when count exceeds threshold", () => { + const alertSpy = jest.spyOn(globalThis.window, "alert").mockImplementation(); + const checkPageRisksSpy = jest.spyOn( + autofillInlineMenuContentService as any, + "checkPageRisks", + ); + autofillInlineMenuContentService["lastTrackedTimestamp"].topLayer = Date.now() - 1000; + autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer = 6; + + autofillInlineMenuContentService["checkAndUpdateRefreshCount"]("topLayer"); + + expect(autofillInlineMenuContentService["inlineMenuEnabled"]).toBe(false); + expect(alertSpy).toHaveBeenCalled(); + expect(checkPageRisksSpy).toHaveBeenCalled(); + }); + }); + + describe("refreshPopoverAttribute", () => { + it("calls checkAndUpdateRefreshCount with popoverAttribute type", () => { + const checkSpy = jest.spyOn( + autofillInlineMenuContentService as any, + "checkAndUpdateRefreshCount", + ); + const element = document.createElement("div"); + element.setAttribute("popover", "auto"); + element.showPopover = jest.fn(); + + autofillInlineMenuContentService["refreshPopoverAttribute"](element); + + expect(checkSpy).toHaveBeenCalledWith("popoverAttribute"); + expect(element.getAttribute("popover")).toBe("manual"); + expect(element.showPopover).toHaveBeenCalled(); + }); + }); + + describe("handleInlineMenuElementMutationObserverUpdate - popover attribute", () => { + it("refreshes popover attribute when changed from manual", () => { + const element = document.createElement("div"); + element.setAttribute("popover", "auto"); + element.showPopover = jest.fn(); + const refreshSpy = jest.spyOn( + autofillInlineMenuContentService as any, + "refreshPopoverAttribute", + ); + autofillInlineMenuContentService["buttonElement"] = element; + + const mockMutation = createMutationRecordMock({ + target: element, + type: "attributes", + attributeName: "popover", + }); + + autofillInlineMenuContentService["handleInlineMenuElementMutationObserverUpdate"]([ + mockMutation, + ]); + + expect(refreshSpy).toHaveBeenCalledWith(element); + }); + + it("does not refresh popover attribute when already manual", () => { + const element = document.createElement("div"); + element.setAttribute("popover", "manual"); + const refreshSpy = jest.spyOn( + autofillInlineMenuContentService as any, + "refreshPopoverAttribute", + ); + autofillInlineMenuContentService["buttonElement"] = element; + + const mockMutation = createMutationRecordMock({ + target: element, + type: "attributes", + attributeName: "popover", + }); + + autofillInlineMenuContentService["handleInlineMenuElementMutationObserverUpdate"]([ + mockMutation, + ]); + + expect(refreshSpy).not.toHaveBeenCalled(); + }); + }); + + describe("appendInlineMenuElements when disabled", () => { + beforeEach(() => { + observeContainerMutationsSpy.mockImplementation(); + }); + + it("does not append button when inline menu is disabled", async () => { + autofillInlineMenuContentService["inlineMenuEnabled"] = false; + jest.spyOn(globalThis.document.body, "appendChild"); + + sendMockExtensionMessage({ + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(globalThis.document.body.appendChild).not.toHaveBeenCalled(); + }); + + it("does not append list when inline menu is disabled", async () => { + autofillInlineMenuContentService["inlineMenuEnabled"] = false; + jest.spyOn(globalThis.document.body, "appendChild"); + + sendMockExtensionMessage({ + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }); + await flushPromises(); + + expect(globalThis.document.body.appendChild).not.toHaveBeenCalled(); + }); + }); + + describe("custom element creation for non-Firefox browsers", () => { + beforeEach(() => { + autofillInlineMenuContentService["isFirefoxBrowser"] = false; + observeContainerMutationsSpy.mockImplementation(); + }); + + it("creates a custom element for button in non-Firefox browsers", () => { + const definespy = jest.spyOn(globalThis.customElements, "define"); + + sendMockExtensionMessage({ + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(definespy).toHaveBeenCalled(); + expect(autofillInlineMenuContentService["buttonElement"]).toBeDefined(); + expect(autofillInlineMenuContentService["buttonElement"]?.tagName).not.toBe("DIV"); + }); + + it("creates a custom element for list in non-Firefox browsers", () => { + const defineSpy = jest.spyOn(globalThis.customElements, "define"); + + sendMockExtensionMessage({ + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }); + + expect(defineSpy).toHaveBeenCalled(); + expect(autofillInlineMenuContentService["listElement"]).toBeDefined(); + expect(autofillInlineMenuContentService["listElement"]?.tagName).not.toBe("DIV"); + }); + }); + + describe("getPageIsOpaque", () => { + it("returns false when no page elements exist", () => { + jest.spyOn(globalThis.document, "querySelectorAll").mockReturnValue([] as any); + + const result = autofillInlineMenuContentService["getPageIsOpaque"](); + + expect(result).toBe(false); + }); + + it("returns true when all html and body nodes have sufficient opacity", () => { + jest + .spyOn(globalThis.document, "querySelectorAll") + .mockReturnValue([document.documentElement, document.body] as any); + jest + .spyOn(globalThis.window, "getComputedStyle") + .mockImplementation(() => ({ opacity: "1" }) as CSSStyleDeclaration); + + const result = autofillInlineMenuContentService["getPageIsOpaque"](); + + expect(result).toBe(true); + }); + + it("returns false when html opacity is below threshold", () => { + jest + .spyOn(globalThis.document, "querySelectorAll") + .mockReturnValue([document.documentElement, document.body] as any); + let callCount = 0; + jest.spyOn(globalThis.window, "getComputedStyle").mockImplementation(() => { + callCount++; + return { opacity: callCount === 1 ? "0.5" : "1" } as CSSStyleDeclaration; + }); + + const result = autofillInlineMenuContentService["getPageIsOpaque"](); + + expect(result).toBe(false); + }); + + it("returns false when body opacity is below threshold", () => { + jest + .spyOn(globalThis.document, "querySelectorAll") + .mockReturnValue([document.documentElement, document.body] as any); + let callCount = 0; + jest.spyOn(globalThis.window, "getComputedStyle").mockImplementation(() => { + callCount++; + return { opacity: callCount === 1 ? "1" : "0.5" } as CSSStyleDeclaration; + }); + + const result = autofillInlineMenuContentService["getPageIsOpaque"](); + + expect(result).toBe(false); + }); + + it("returns false when opacity of at least one duplicate body is below threshold", () => { + const duplicateBody = document.createElement("body"); + jest + .spyOn(globalThis.document, "querySelectorAll") + .mockReturnValue([document.documentElement, document.body, duplicateBody] as any); + let callCount = 0; + jest.spyOn(globalThis.window, "getComputedStyle").mockImplementation(() => { + callCount++; + + let opacityValue = "0.5"; + switch (callCount) { + case 1: + opacityValue = "1"; + break; + case 2: + opacityValue = "0.7"; + break; + default: + break; + } + + return { opacity: opacityValue } as CSSStyleDeclaration; + }); + + const result = autofillInlineMenuContentService["getPageIsOpaque"](); + + expect(result).toBe(false); + }); + + it("returns true when opacity is above threshold", () => { + jest + .spyOn(globalThis.document, "querySelectorAll") + .mockReturnValue([document.documentElement, document.body] as any); + jest + .spyOn(globalThis.window, "getComputedStyle") + .mockImplementation(() => ({ opacity: "0.7" }) as CSSStyleDeclaration); + + const result = autofillInlineMenuContentService["getPageIsOpaque"](); + + expect(result).toBe(true); + }); + + it("returns false when opacity is at threshold", () => { + jest + .spyOn(globalThis.document, "querySelectorAll") + .mockReturnValue([document.documentElement, document.body] as any); + jest + .spyOn(globalThis.window, "getComputedStyle") + .mockImplementation(() => ({ opacity: "0.6" }) as CSSStyleDeclaration); + + const result = autofillInlineMenuContentService["getPageIsOpaque"](); + + expect(result).toBe(false); + }); }); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts index b550ae203d5..b61e5e19d53 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts @@ -22,6 +22,19 @@ import { import { AutofillInlineMenuButtonIframe } from "../iframe-content/autofill-inline-menu-button-iframe"; import { AutofillInlineMenuListIframe } from "../iframe-content/autofill-inline-menu-list-iframe"; +const experienceValidationBackoffThresholds = { + topLayer: { + countLimit: 5, + timeSpanLimit: 5000, + }, + popoverAttribute: { + countLimit: 10, + timeSpanLimit: 5000, + }, +}; + +type BackoffCheckType = keyof typeof experienceValidationBackoffThresholds; + export class AutofillInlineMenuContentService implements AutofillInlineMenuContentServiceInterface { private readonly sendExtensionMessage = sendExtensionMessage; private readonly generateRandomCustomElementName = generateRandomCustomElementName; @@ -35,6 +48,19 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private bodyMutationObserver: MutationObserver; private inlineMenuElementsMutationObserver: MutationObserver; private containerElementMutationObserver: MutationObserver; + private refreshCountWithinTimeThreshold: { [key in BackoffCheckType]: number } = { + topLayer: 0, + popoverAttribute: 0, + }; + private lastTrackedTimestamp = { + topLayer: Date.now(), + popoverAttribute: Date.now(), + }; + /** + * Distinct from preventing inline menu script injection, this is for cases + * where the page is subsequently determined to be risky. + */ + private inlineMenuEnabled = true; private mutationObserverIterations = 0; private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout; @@ -140,6 +166,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * Updates the position of both the inline menu button and inline menu list. */ private async appendInlineMenuElements({ overlayElement }: AutofillExtensionMessage) { + if (!this.inlineMenuEnabled) { + return; + } + if (overlayElement === AutofillOverlayElement.Button) { return this.appendButtonElement(); } @@ -151,6 +181,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * Updates the position of the inline menu button. */ private async appendButtonElement(): Promise { + if (!this.inlineMenuEnabled) { + return; + } + if (!this.buttonElement) { this.createButtonElement(); this.updateCustomElementDefaultStyles(this.buttonElement); @@ -167,6 +201,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * Updates the position of the inline menu list. */ private async appendListElement(): Promise { + if (!this.inlineMenuEnabled) { + return; + } + if (!this.listElement) { this.createListElement(); this.updateCustomElementDefaultStyles(this.listElement); @@ -219,6 +257,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * to create the element if it already exists in the DOM. */ private createButtonElement() { + if (!this.inlineMenuEnabled) { + return; + } + if (this.isFirefoxBrowser) { this.buttonElement = globalThis.document.createElement("div"); this.buttonElement.setAttribute("popover", "manual"); @@ -240,8 +282,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.buttonElement = globalThis.document.createElement(customElementName); this.buttonElement.setAttribute("popover", "manual"); - - this.createInternalStyleNode(this.buttonElement); } /** @@ -249,6 +289,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * to create the element if it already exists in the DOM. */ private createListElement() { + if (!this.inlineMenuEnabled) { + return; + } + if (this.isFirefoxBrowser) { this.listElement = globalThis.document.createElement("div"); this.listElement.setAttribute("popover", "manual"); @@ -270,30 +314,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.listElement = globalThis.document.createElement(customElementName); this.listElement.setAttribute("popover", "manual"); - - this.createInternalStyleNode(this.listElement); - } - - /** - * Builds and prepends an internal stylesheet to the container node with rules - * to prevent targeting by the host's global styling rules. This should only be - * used for pseudo elements such as `::backdrop` or `::before`. All other - * styles should be applied inline upon the parent container itself. - */ - private createInternalStyleNode(parent: HTMLElement) { - const css = document.createTextNode(` - ${parent.tagName}::backdrop { - background: none !important; - pointer-events: none !important; - } - ${parent.tagName}::before, ${parent.tagName}::after { - content:"" !important; - } - `); - const style = globalThis.document.createElement("style"); - style.setAttribute("type", "text/css"); - style.appendChild(css); - parent.prepend(style); } /** @@ -407,14 +427,23 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } const element = record.target as HTMLElement; - if (record.attributeName !== "style") { - this.removeModifiedElementAttributes(element); + if (record.attributeName === "popover" && this.inlineMenuEnabled) { + const attributeValue = element.getAttribute(record.attributeName); + if (attributeValue !== "manual") { + this.refreshPopoverAttribute(element); + } continue; } - element.removeAttribute("style"); - this.updateCustomElementDefaultStyles(element); + if (record.attributeName === "style") { + element.removeAttribute("style"); + this.updateCustomElementDefaultStyles(element); + + continue; + } + + this.removeModifiedElementAttributes(element); } }; @@ -428,7 +457,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte const attributes = Array.from(element.attributes); for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) { const attribute = attributes[attributeIndex]; - if (attribute.name === "style") { + if (attribute.name === "style" || attribute.name === "popover") { continue; } @@ -458,7 +487,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private checkPageRisks = async () => { const pageIsOpaque = await this.getPageIsOpaque(); - const risksFound = !pageIsOpaque; + const risksFound = !pageIsOpaque || !this.inlineMenuEnabled; if (risksFound) { this.closeInlineMenu(); @@ -509,7 +538,49 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte return otherTopLayeritems; }; + /** + * Internally track owned injected experience refreshes as a side-effect + * of host page interference. + */ + private checkAndUpdateRefreshCount = (countType: BackoffCheckType) => { + if (!this.inlineMenuEnabled) { + return; + } + + const { countLimit, timeSpanLimit } = experienceValidationBackoffThresholds[countType]; + const now = Date.now(); + const timeSinceLastTrackedRefresh = now - this.lastTrackedTimestamp[countType]; + const currentlyWithinTimeThreshold = timeSinceLastTrackedRefresh <= timeSpanLimit; + const withinCountThreshold = this.refreshCountWithinTimeThreshold[countType] <= countLimit; + + if (currentlyWithinTimeThreshold) { + if (withinCountThreshold) { + this.refreshCountWithinTimeThreshold[countType]++; + } else { + // Set inline menu to be off; page is aggressively trying to take top position of top layer + this.inlineMenuEnabled = false; + void this.checkPageRisks(); + + const warningMessage = chrome.i18n.getMessage("topLayerHijackWarning"); + globalThis.window.alert(warningMessage); + } + } else { + this.lastTrackedTimestamp[countType] = now; + this.refreshCountWithinTimeThreshold[countType] = 0; + } + }; + + private refreshPopoverAttribute = (element: HTMLElement) => { + this.checkAndUpdateRefreshCount("popoverAttribute"); + element.setAttribute("popover", "manual"); + element.showPopover(); + }; + refreshTopLayerPosition = () => { + if (!this.inlineMenuEnabled) { + return; + } + const otherTopLayerItems = this.getUnownedTopLayerItems(); // No need to refresh if there are no other top-layer items @@ -523,6 +594,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte const listInDocument = this.listElement && (globalThis.document.getElementsByTagName(this.listElement.tagName)[0] as HTMLElement); + if (buttonInDocument) { buttonInDocument.hidePopover(); buttonInDocument.showPopover(); @@ -532,6 +604,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte listInDocument.hidePopover(); listInDocument.showPopover(); } + + if (buttonInDocument || listInDocument) { + this.checkAndUpdateRefreshCount("topLayer"); + } }; /** @@ -541,24 +617,28 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * `body` (enforced elsewhere). */ private getPageIsOpaque = () => { - // These are computed style values, so we don't need to worry about non-float values - // for `opacity`, here // @TODO for definitive checks, traverse up the node tree from the inline menu container; // nodes can exist between `html` and `body` - const htmlElement = globalThis.document.querySelector("html"); - const bodyElement = globalThis.document.querySelector("body"); + /** + * `querySelectorAll` for (non-standard) cases where the page has additional copies of + * page nodes that should be unique + */ + const pageElements = globalThis.document.querySelectorAll("html, body"); - if (!htmlElement || !bodyElement) { + if (!pageElements.length) { return false; } - const htmlOpacity = globalThis.window.getComputedStyle(htmlElement)?.opacity || "0"; - const bodyOpacity = globalThis.window.getComputedStyle(bodyElement)?.opacity || "0"; + return [...pageElements].every((element) => { + // These are computed style values, so we don't need to worry about non-float values + // for `opacity`, here + const elementOpacity = globalThis.window.getComputedStyle(element)?.opacity || "0"; - // Any value above this is considered "opaque" for our purposes - const opacityThreshold = 0.6; + // Any value above this is considered "opaque" for our purposes + const opacityThreshold = 0.6; - return parseFloat(htmlOpacity) > opacityThreshold && parseFloat(bodyOpacity) > opacityThreshold; + return parseFloat(elementOpacity) > opacityThreshold; + }); }; /** diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.ts index 2fea65a7f01..3e2b364b17b 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.ts @@ -8,7 +8,10 @@ export class AutofillInlineMenuIframeElement { iframeTitle: string, ariaAlert?: string, ) { + const style = this.createInternalStyleNode(); const shadow: ShadowRoot = element.attachShadow({ mode: "closed" }); + shadow.prepend(style); + const autofillInlineMenuIframeService = new AutofillInlineMenuIframeService( shadow, portName, @@ -18,4 +21,50 @@ export class AutofillInlineMenuIframeElement { ); autofillInlineMenuIframeService.initMenuIframe(); } + + /** + * Builds and prepends an internal stylesheet to the container node with rules + * to prevent targeting by the host's global styling rules. This should only be + * used for pseudo elements such as `::backdrop` or `::before`. All other + * styles should be applied inline upon the parent container itself for improved + * specificity priority. + */ + private createInternalStyleNode() { + const css = document.createTextNode(` + :host::backdrop, + :host::before, + :host::after { + all: initial !important; + backdrop-filter: none !important; + filter: none !important; + inset: auto !important; + touch-action: auto !important; + user-select: text !important; + display: none !important; + position: relative !important; + top: auto !important; + right: auto !important; + bottom: auto !important; + left: auto !important; + transform: none !important; + transform-origin: 50% 50% !important; + opacity: 1 !important; + mix-blend-mode: normal !important; + isolation: isolate !important; + z-index: 0 !important; + background: none !important; + background-color: transparent !important; + background-image: none !important; + width: 0 !important; + height: 0 !important; + content: "" !important; + pointer-events: all !important; + } + `); + const style = globalThis.document.createElement("style"); + style.setAttribute("type", "text/css"); + style.appendChild(css); + + return style; + } } diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts index f7a5727e47f..d7a61bec61f 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts @@ -6,11 +6,13 @@ import { AutofillInlineMenuContainer } from "./autofill-inline-menu-container"; describe("AutofillInlineMenuContainer", () => { const portKey = "testPortKey"; - const iframeUrl = "https://example.com"; + const extensionOrigin = "chrome-extension://test-extension-id"; + const iframeUrl = `${extensionOrigin}/overlay/menu-list.html`; const pageTitle = "Example"; let autofillInlineMenuContainer: AutofillInlineMenuContainer; beforeEach(() => { + jest.spyOn(chrome.runtime, "getURL").mockReturnValue(`${extensionOrigin}/`); autofillInlineMenuContainer = new AutofillInlineMenuContainer(); }); @@ -28,7 +30,7 @@ describe("AutofillInlineMenuContainer", () => { portName: AutofillOverlayPort.List, }; - postWindowMessage(message); + postWindowMessage(message, extensionOrigin); expect(autofillInlineMenuContainer["defaultIframeAttributes"].src).toBe(message.iframeUrl); expect(autofillInlineMenuContainer["defaultIframeAttributes"].title).toBe(message.pageTitle); @@ -44,15 +46,48 @@ describe("AutofillInlineMenuContainer", () => { portName: AutofillOverlayPort.Button, }; - postWindowMessage(message); + postWindowMessage(message, extensionOrigin); jest.spyOn(autofillInlineMenuContainer["inlineMenuPageIframe"].contentWindow, "postMessage"); autofillInlineMenuContainer["inlineMenuPageIframe"].dispatchEvent(new Event("load")); expect(chrome.runtime.connect).toHaveBeenCalledWith({ name: message.portName }); + const expectedMessage = expect.objectContaining({ + ...message, + token: expect.any(String), + }); expect( autofillInlineMenuContainer["inlineMenuPageIframe"].contentWindow.postMessage, - ).toHaveBeenCalledWith(message, "*"); + ).toHaveBeenCalledWith(expectedMessage, "*"); + }); + + it("ignores initialization when URLs are not from extension origin", () => { + const invalidIframeUrlMessage = { + command: "initAutofillInlineMenuList", + iframeUrl: "https://malicious.com/overlay/menu-list.html", + pageTitle, + portKey, + portName: AutofillOverlayPort.List, + }; + + postWindowMessage(invalidIframeUrlMessage, extensionOrigin); + expect(autofillInlineMenuContainer["inlineMenuPageIframe"]).toBeUndefined(); + expect(autofillInlineMenuContainer["isInitialized"]).toBe(false); + + autofillInlineMenuContainer = new AutofillInlineMenuContainer(); + + const invalidStyleSheetUrlMessage = { + command: "initAutofillInlineMenuList", + iframeUrl, + pageTitle, + portKey, + portName: AutofillOverlayPort.List, + styleSheetUrl: "https://malicious.com/styles.css", + }; + + postWindowMessage(invalidStyleSheetUrlMessage, extensionOrigin); + expect(autofillInlineMenuContainer["inlineMenuPageIframe"]).toBeUndefined(); + expect(autofillInlineMenuContainer["isInitialized"]).toBe(false); }); }); @@ -69,7 +104,7 @@ describe("AutofillInlineMenuContainer", () => { portName: AutofillOverlayPort.Button, }; - postWindowMessage(message); + postWindowMessage(message, extensionOrigin); iframe = autofillInlineMenuContainer["inlineMenuPageIframe"]; jest.spyOn(iframe.contentWindow, "postMessage"); @@ -112,7 +147,8 @@ describe("AutofillInlineMenuContainer", () => { }); it("posts a message to the background from the inline menu iframe", () => { - const message = { command: "checkInlineMenuButtonFocused", portKey }; + const token = autofillInlineMenuContainer["token"]; + const message = { command: "checkInlineMenuButtonFocused", portKey, token }; postWindowMessage(message, "null", iframe.contentWindow as any); @@ -124,7 +160,28 @@ describe("AutofillInlineMenuContainer", () => { postWindowMessage(message); - expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(message, "*"); + const expectedMessage = expect.objectContaining({ + ...message, + token: expect.any(String), + }); + expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(expectedMessage, "*"); + }); + + it("ignores messages from iframe with invalid token", () => { + const message = { command: "checkInlineMenuButtonFocused", portKey, token: "invalid-token" }; + + postWindowMessage(message, "null", iframe.contentWindow as any); + + expect(port.postMessage).not.toHaveBeenCalled(); + }); + + it("ignores messages from iframe with commands not in the allowlist", () => { + const token = autofillInlineMenuContainer["token"]; + const message = { command: "maliciousCommand", portKey, token }; + + postWindowMessage(message, "null", iframe.contentWindow as any); + + expect(port.postMessage).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts index 6d85982a1ac..ad0b11f0bc6 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts @@ -1,6 +1,6 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; -import { setElementStyles } from "../../../../utils"; +import { generateRandomChars, setElementStyles } from "../../../../utils"; import { InitAutofillInlineMenuElementMessage, AutofillInlineMenuContainerWindowMessageHandlers, @@ -8,14 +8,37 @@ import { AutofillInlineMenuContainerPortMessage, } from "../../abstractions/autofill-inline-menu-container"; +/** + * Allowlist of commands that can be sent to the background script. + */ +const ALLOWED_BG_COMMANDS = new Set([ + "addNewVaultItem", + "autofillInlineMenuBlurred", + "autofillInlineMenuButtonClicked", + "checkAutofillInlineMenuButtonFocused", + "checkInlineMenuButtonFocused", + "fillAutofillInlineMenuCipher", + "fillGeneratedPassword", + "redirectAutofillInlineMenuFocusOut", + "refreshGeneratedPassword", + "refreshOverlayCiphers", + "triggerDelayedAutofillInlineMenuClosure", + "updateAutofillInlineMenuColorScheme", + "updateAutofillInlineMenuListHeight", + "unlockVault", + "viewSelectedCipher", +]); + export class AutofillInlineMenuContainer { private readonly setElementStyles = setElementStyles; - private readonly extensionOriginsSet: Set; private port: chrome.runtime.Port | null = null; /** Non-null asserted. */ private portName!: string; /** Non-null asserted. */ private inlineMenuPageIframe!: HTMLIFrameElement; + private token: string; + private isInitialized: boolean = false; + private readonly extensionOrigin: string; private readonly iframeStyles: Partial = { all: "initial", position: "fixed", @@ -49,11 +72,8 @@ export class AutofillInlineMenuContainer { }; constructor() { - this.extensionOriginsSet = new Set([ - chrome.runtime.getURL("").slice(0, -1).toLowerCase(), // Remove the trailing slash and normalize the extension url to lowercase - "null", - ]); - + this.token = generateRandomChars(32); + this.extensionOrigin = chrome.runtime.getURL("").slice(0, -1); globalThis.addEventListener("message", this.handleWindowMessage); } @@ -63,9 +83,22 @@ export class AutofillInlineMenuContainer { * @param message - The message containing the iframe url and page title. */ private handleInitInlineMenuIframe(message: InitAutofillInlineMenuElementMessage) { + if (this.isInitialized) { + return; + } + + if (!this.isExtensionUrl(message.iframeUrl)) { + return; + } + + if (message.styleSheetUrl && !this.isExtensionUrl(message.styleSheetUrl)) { + return; + } + this.defaultIframeAttributes.src = message.iframeUrl; this.defaultIframeAttributes.title = message.pageTitle; this.portName = message.portName; + this.isInitialized = true; this.inlineMenuPageIframe = globalThis.document.createElement("iframe"); this.setElementStyles(this.inlineMenuPageIframe, this.iframeStyles, true); @@ -81,6 +114,26 @@ export class AutofillInlineMenuContainer { globalThis.document.body.appendChild(this.inlineMenuPageIframe); } + /** + * validates that a URL is from the extension origin. + * prevents loading arbitrary URLs in the iframe. + * + * @param url - The URL to validate. + */ + private isExtensionUrl(url: string): boolean { + if (!url) { + return false; + } + try { + const urlObj = new URL(url); + return ( + urlObj.origin === this.extensionOrigin || urlObj.href.startsWith(this.extensionOrigin + "/") + ); + } catch { + return false; + } + } + /** * Sets up the port message listener for the inline menu page. * @@ -88,7 +141,8 @@ export class AutofillInlineMenuContainer { */ private setupPortMessageListener = (message: InitAutofillInlineMenuElementMessage) => { this.port = chrome.runtime.connect({ name: this.portName }); - this.postMessageToInlineMenuPage(message); + const initMessage = { ...message, token: this.token }; + this.postMessageToInlineMenuPageUnsafe(initMessage); }; /** @@ -97,6 +151,22 @@ export class AutofillInlineMenuContainer { * @param message - The message to post. */ private postMessageToInlineMenuPage(message: AutofillInlineMenuContainerWindowMessage) { + if (this.inlineMenuPageIframe?.contentWindow) { + const messageWithToken = { ...message, token: this.token }; + this.postMessageToInlineMenuPageUnsafe(messageWithToken); + } + } + + /** + * Posts a message to the inline menu page iframe without token validation. + * + * UNSAFE: Bypasses token authentication and sends raw messages. Only use internally + * when sending trusted messages (e.g., initialization) or when token validation + * would create circular dependencies. External callers should use postMessageToInlineMenuPage(). + * + * @param message - The message to post. + */ + private postMessageToInlineMenuPageUnsafe(message: Record) { if (this.inlineMenuPageIframe?.contentWindow) { this.inlineMenuPageIframe.contentWindow.postMessage(message, "*"); } @@ -108,9 +178,15 @@ export class AutofillInlineMenuContainer { * @param message - The message to post. */ private postMessageToBackground(message: AutofillInlineMenuContainerPortMessage) { - if (this.port) { - this.port.postMessage(message); + if (!this.port) { + return; } + + if (message.command && !ALLOWED_BG_COMMANDS.has(message.command)) { + return; + } + + this.port.postMessage(message); } /** @@ -124,23 +200,33 @@ export class AutofillInlineMenuContainer { return; } - if ( - this.windowMessageHandlers[ - message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers - ] - ) { - this.windowMessageHandlers[ - message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers - ](message); + if (this.windowMessageHandlers[message.command]) { + // only accept init messages from extension origin or parent window + if ( + (message.command === "initAutofillInlineMenuButton" || + message.command === "initAutofillInlineMenuList") && + !this.isMessageFromExtensionOrigin(event) && + !this.isMessageFromParentWindow(event) + ) { + return; + } + this.windowMessageHandlers[message.command](message); return; } if (this.isMessageFromParentWindow(event)) { + // messages from parent window are trusted and forwarded to iframe this.postMessageToInlineMenuPage(message); return; } - this.postMessageToBackground(message); + // messages from iframe to background require object identity verification with a contentWindow check and token auth + if (this.isMessageFromInlineMenuPageIframe(event)) { + if (this.isValidSessionToken(message)) { + this.postMessageToBackground(message); + } + return; + } }; /** @@ -184,10 +270,34 @@ export class AutofillInlineMenuContainer { if (!this.inlineMenuPageIframe) { return false; } + // only trust the specific iframe we created + return this.inlineMenuPageIframe.contentWindow === event.source; + } - return ( - this.inlineMenuPageIframe.contentWindow === event.source && - this.extensionOriginsSet.has(event.origin.toLowerCase()) - ); + /** + * Validates that the message contains a valid session token. + * The session token is generated when the container is created and is refreshed + * every time the inline menu container is recreated. + * + */ + private isValidSessionToken(message: { token?: string }): boolean { + return message.token === this.token; + } + + /** + * Validates that a message event originates from the extension. + * + * @param event - The message event to validate. + * @returns True if the message is from the extension origin. + */ + private isMessageFromExtensionOrigin(event: MessageEvent): boolean { + try { + if (event.origin === "null") { + return false; + } + return event.origin === this.extensionOrigin; + } catch { + return false; + } } } diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts index 89f44a6a80d..ea77e3e434d 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts @@ -16,6 +16,7 @@ export class AutofillInlineMenuPageElement extends HTMLElement { private portKey!: string; /** Non-null asserted. */ protected windowMessageHandlers!: AutofillInlineMenuPageElementWindowMessageHandlers; + private token?: string; constructor() { super(); @@ -37,8 +38,12 @@ export class AutofillInlineMenuPageElement extends HTMLElement { styleSheetUrl: string, translations: Record, portKey: string, + token?: string, ): Promise { this.portKey = portKey; + if (token) { + this.token = token; + } this.translations = translations; globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale")); @@ -58,7 +63,11 @@ export class AutofillInlineMenuPageElement extends HTMLElement { * @param message - The message to post */ protected postMessageToParent(message: AutofillInlineMenuPageElementWindowMessage) { - globalThis.parent.postMessage({ portKey: this.portKey, ...message }, "*"); + const messageWithAuth: Record = { portKey: this.portKey, ...message }; + if (this.token) { + messageWithAuth.token = this.token; + } + globalThis.parent.postMessage(messageWithAuth, "*"); } /** @@ -105,6 +114,15 @@ export class AutofillInlineMenuPageElement extends HTMLElement { } const message = event?.data; + + if ( + message?.token && + (message?.command === "initAutofillInlineMenuButton" || + message?.command === "initAutofillInlineMenuList") + ) { + this.token = message.token; + } + const handler = this.windowMessageHandlers[message?.command]; if (!handler) { return; diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts index 85bf8c16610..05bfbf378a8 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts @@ -6,6 +6,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { AutofillMessageCommand } from "../../enums/autofill-message.enums"; +import { InlineMenuFillType } from "../../enums/autofill-overlay.enum"; import AutofillField from "../../models/autofill-field"; import AutofillForm from "../../models/autofill-form"; import AutofillPageDetails from "../../models/autofill-page-details"; @@ -30,6 +31,7 @@ export interface AutoFillOptions { autoSubmitLogin?: boolean; focusedFieldForm?: string; focusedFieldOpid?: string; + inlineMenuFillType?: InlineMenuFillType; } export interface FormData { @@ -49,6 +51,7 @@ export interface GenerateFillScriptOptions { tabUrl: string; defaultUriMatch: UriMatchStrategySetting; focusedFieldOpid?: string; + inlineMenuFillType?: InlineMenuFillType; } export type CollectPageDetailsResponseMessage = { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 7854dc8e161..817a7cca43c 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -1118,6 +1118,12 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * @param autofillFieldData - Autofill field data captured from the form field element. */ private async setQualifiedLoginFillType(autofillFieldData: AutofillField) { + // Check if this is a current password field in a password change form + if (this.inlineMenuFieldQualificationService.isUpdateCurrentPasswordField(autofillFieldData)) { + autofillFieldData.inlineMenuFillType = InlineMenuFillTypes.CurrentPasswordUpdate; + return; + } + autofillFieldData.inlineMenuFillType = CipherType.Login; autofillFieldData.showPasskeys = autofillFieldData.autoCompleteType.includes("webauthn"); diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index b436214f327..13e97766594 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -44,6 +44,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums"; +import { InlineMenuFillTypes } from "../enums/autofill-overlay.enum"; import { AutofillPort } from "../enums/autofill-port.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; @@ -103,6 +104,15 @@ describe("AutofillService", () => { beforeEach(() => { configService = mock(); configService.getFeatureFlag$.mockImplementation(() => of(false)); + + // Initialize domainSettingsService BEFORE it's used + domainSettingsService = new DefaultDomainSettingsService( + fakeStateProvider, + policyService, + accountService, + ); + domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); + scriptInjectorService = new BrowserScriptInjectorService( domainSettingsService, platformUtilsService, @@ -141,12 +151,6 @@ describe("AutofillService", () => { userNotificationsSettings, messageListener, ); - domainSettingsService = new DefaultDomainSettingsService( - fakeStateProvider, - policyService, - accountService, - ); - domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); jest.spyOn(BrowserApi, "tabSendMessage"); }); @@ -2077,6 +2081,193 @@ describe("AutofillService", () => { }); }); + describe("given password generation with inlineMenuFillType", () => { + beforeEach(() => { + pageDetails.forms = undefined; + pageDetails.fields = []; // Clear fields to start fresh + options.inlineMenuFillType = InlineMenuFillTypes.PasswordGeneration; + options.cipher.login.totp = null; // Disable TOTP for these tests + }); + + it("includes all password fields from the same form when filling with password generation", async () => { + const newPasswordField = createAutofillFieldMock({ + opid: "new-password", + type: "password", + form: "validFormId", + elementNumber: 2, + }); + const confirmPasswordField = createAutofillFieldMock({ + opid: "confirm-password", + type: "password", + form: "validFormId", + elementNumber: 3, + }); + pageDetails.fields.push(newPasswordField, confirmPasswordField); + options.focusedFieldOpid = newPasswordField.opid; + + await autofillService["generateLoginFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(filledFields[newPasswordField.opid]).toBeDefined(); + expect(filledFields[confirmPasswordField.opid]).toBeDefined(); + }); + + it("finds username field for the first password field when generating passwords", async () => { + const newPasswordField = createAutofillFieldMock({ + opid: "new-password", + type: "password", + form: "validFormId", + elementNumber: 2, + }); + pageDetails.fields.push(newPasswordField); + options.focusedFieldOpid = newPasswordField.opid; + jest.spyOn(autofillService as any, "findUsernameField"); + + await autofillService["generateLoginFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["findUsernameField"]).toHaveBeenCalledWith( + pageDetails, + expect.objectContaining({ opid: newPasswordField.opid }), + false, + false, + true, + ); + }); + + it("does not include password fields from different forms", async () => { + const formAPasswordField = createAutofillFieldMock({ + opid: "form-a-password", + type: "password", + form: "formA", + elementNumber: 1, + }); + const formBPasswordField = createAutofillFieldMock({ + opid: "form-b-password", + type: "password", + form: "formB", + elementNumber: 2, + }); + pageDetails.fields = [formAPasswordField, formBPasswordField]; + options.focusedFieldOpid = formAPasswordField.opid; + + await autofillService["generateLoginFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(filledFields[formAPasswordField.opid]).toBeDefined(); + expect(filledFields[formBPasswordField.opid]).toBeUndefined(); + }); + }); + + describe("given current password update with inlineMenuFillType", () => { + beforeEach(() => { + pageDetails.forms = undefined; + pageDetails.fields = []; // Clear fields to start fresh + options.inlineMenuFillType = InlineMenuFillTypes.CurrentPasswordUpdate; + options.cipher.login.totp = null; // Disable TOTP for these tests + }); + + it("includes all password fields from the same form when updating current password", async () => { + const currentPasswordField = createAutofillFieldMock({ + opid: "current-password", + type: "password", + form: "validFormId", + elementNumber: 1, + }); + const newPasswordField = createAutofillFieldMock({ + opid: "new-password", + type: "password", + form: "validFormId", + elementNumber: 2, + }); + const confirmPasswordField = createAutofillFieldMock({ + opid: "confirm-password", + type: "password", + form: "validFormId", + elementNumber: 3, + }); + pageDetails.fields.push(currentPasswordField, newPasswordField, confirmPasswordField); + options.focusedFieldOpid = currentPasswordField.opid; + + await autofillService["generateLoginFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(filledFields[currentPasswordField.opid]).toBeDefined(); + expect(filledFields[newPasswordField.opid]).toBeDefined(); + expect(filledFields[confirmPasswordField.opid]).toBeDefined(); + }); + + it("includes all password fields from the same form without TOTP", async () => { + const currentPasswordField = createAutofillFieldMock({ + opid: "current-password", + type: "password", + form: "validFormId", + elementNumber: 1, + }); + const newPasswordField = createAutofillFieldMock({ + opid: "new-password", + type: "password", + form: "validFormId", + elementNumber: 2, + }); + pageDetails.fields.push(currentPasswordField, newPasswordField); + options.focusedFieldOpid = currentPasswordField.opid; + + await autofillService["generateLoginFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(filledFields[currentPasswordField.opid]).toBeDefined(); + expect(filledFields[newPasswordField.opid]).toBeDefined(); + }); + + it("does not include password fields from different forms during password update", async () => { + const formAPasswordField = createAutofillFieldMock({ + opid: "form-a-password", + type: "password", + form: "formA", + elementNumber: 1, + }); + const formBPasswordField = createAutofillFieldMock({ + opid: "form-b-password", + type: "password", + form: "formB", + elementNumber: 2, + }); + pageDetails.fields = [formAPasswordField, formBPasswordField]; + options.focusedFieldOpid = formAPasswordField.opid; + + await autofillService["generateLoginFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(filledFields[formAPasswordField.opid]).toBeDefined(); + expect(filledFields[formBPasswordField.opid]).toBeUndefined(); + }); + }); + describe("given a set of page details that does not contain a password field", () => { let emailField: AutofillField; let emailFieldView: FieldView; @@ -3140,12 +3331,16 @@ describe("AutofillService", () => { "example.com", "exampleapp.com", ]); - domainSettingsService.equivalentDomains$ = of([["not-example.com"]]); const pageUrl = "https://subdomain.example.com"; const tabUrl = "https://www.not-example.com"; const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false); + // Mock getUrlEquivalentDomains to return the expected domains + jest + .spyOn(domainSettingsService, "getUrlEquivalentDomains") + .mockReturnValue(of(equivalentDomains)); + const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith( diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index fcc8861228b..010f5ea0f27 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -52,6 +52,7 @@ import { ScriptInjectorService } from "../../platform/services/abstractions/scri // eslint-disable-next-line no-restricted-imports import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window"; import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums"; +import { InlineMenuFillTypes } from "../enums/autofill-overlay.enum"; import { AutofillPort } from "../enums/autofill-port.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; @@ -452,6 +453,7 @@ export default class AutofillService implements AutofillServiceInterface { tabUrl: tab.url, defaultUriMatch: defaultUriMatch, focusedFieldOpid: options.focusedFieldOpid, + inlineMenuFillType: options.inlineMenuFillType, }); if (!fillScript || !fillScript.script || !fillScript.script.length) { @@ -971,26 +973,53 @@ export default class AutofillService implements AutofillServiceInterface { if (passwordFields.length && !passwords.length) { // in the event that password fields exist but weren't processed within form elements. - // select matching password if focused, otherwise first in prioritized list. for username, use focused field if it matches, otherwise find field before password. - const passwordFieldToUse = focusedField - ? prioritizedPasswordFields.find(passwordMatchesFocused) || prioritizedPasswordFields[0] - : prioritizedPasswordFields[0]; + const isPasswordGeneration = + options.inlineMenuFillType === InlineMenuFillTypes.PasswordGeneration; + const isCurrentPasswordUpdate = + options.inlineMenuFillType === InlineMenuFillTypes.CurrentPasswordUpdate; - if (passwordFieldToUse) { - passwords.push(passwordFieldToUse); + // For password generation or current password update, include all password fields from the same form + // This ensures we have access to all fields regardless of their login/registration classification + if ((isPasswordGeneration || isCurrentPasswordUpdate) && focusedField) { + // Add all password fields from the same form as the focused field + const focusedFieldForm = focusedField.form; - if (login.username && passwordFieldToUse.elementNumber > 0) { - username = getUsernameForPassword(passwordFieldToUse, true); + // Check both login and registration fields to ensure we get all password fields + const allPasswordFields = [...loginPasswordFields, ...registrationPasswordFields]; + allPasswordFields.forEach((passField) => { + if (passField.form === focusedFieldForm) { + passwords.push(passField); + } + }); + } + + // If we didn't add any passwords above (either not password generation/update or no matching fields), + // select matching password if focused, otherwise first in prioritized list. + if (!passwords.length) { + const passwordFieldToUse = focusedField + ? prioritizedPasswordFields.find(passwordMatchesFocused) || prioritizedPasswordFields[0] + : prioritizedPasswordFields[0]; + + if (passwordFieldToUse) { + passwords.push(passwordFieldToUse); + } + } + + // Handle username and TOTP for the first password field + const firstPasswordField = passwords[0]; + if (firstPasswordField) { + if (login.username && firstPasswordField.elementNumber > 0) { + username = getUsernameForPassword(firstPasswordField, true); if (username) { usernames.set(username.opid, username); } } - if (options.allowTotpAutofill && login.totp && passwordFieldToUse.elementNumber > 0) { + if (options.allowTotpAutofill && login.totp && firstPasswordField.elementNumber > 0) { totp = - isFocusedTotpField && passwordMatchesFocused(passwordFieldToUse) + isFocusedTotpField && passwordMatchesFocused(firstPasswordField) ? focusedField - : this.findTotpField(pageDetails, passwordFieldToUse, false, false, true); + : this.findTotpField(pageDetails, firstPasswordField, false, false, true); if (totp) { totps.push(totp); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index baa45c312b8..920f0f02caa 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -130,7 +130,7 @@ import { } from "@bitwarden/common/platform/abstractions/storage.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { ActionsService } from "@bitwarden/common/platform/actions/actions-service"; -import { IpcService } from "@bitwarden/common/platform/ipc"; +import { IpcService, IpcSessionRepository } from "@bitwarden/common/platform/ipc"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; @@ -548,7 +548,7 @@ export default class MainBackground { this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session this.memoryStorageService = this.memoryStorageForStateProviders; } else { - this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory + this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(this.logService); // mv2 stores to memory this.memoryStorageService = this.memoryStorageForStateProviders; } @@ -1477,7 +1477,12 @@ export default class MainBackground { ); this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService); - this.ipcService = new IpcBackgroundService(this.platformUtilsService, this.logService); + const ipcSessionRepository = new IpcSessionRepository(this.stateProvider); + this.ipcService = new IpcBackgroundService( + this.platformUtilsService, + this.logService, + ipcSessionRepository, + ); this.endUserNotificationService = new DefaultEndUserNotificationService( this.stateProvider, diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index de0d79a89db..798a7583f85 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -256,6 +256,9 @@ export default class RuntimeBackground { case "addToLockedVaultPendingNotifications": this.lockedVaultPendingNotifications.push(msg.data); break; + case "abandonAutofillPendingNotifications": + this.lockedVaultPendingNotifications = []; + break; case "lockVault": await this.lockService.lock(msg.userId); break; diff --git a/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts b/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts index 2b91a28122c..589b880b206 100644 --- a/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts +++ b/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts @@ -47,6 +47,8 @@ import { TypographyModule, ], }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28231): Use Component suffix +// eslint-disable-next-line @angular-eslint/component-class-suffix export class PhishingWarning { private activatedRoute = inject(ActivatedRoute); private messageSender = inject(MessageSender); diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts index cb76a1cc354..6e1bf07c647 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts @@ -58,6 +58,7 @@ export class PhishingDataService { new Set( (state?.domains?.filter((line) => line.trim().length > 0) ?? []).concat( this._testDomains, + "phishing.testcategory.com", // Included for QA to test in prod ), ), ), diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index d44a3d2a2e7..3d8f648daca 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.11.0", + "version": "2025.11.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index b6381201c7d..2b2aa0f117b 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.11.0", + "version": "2025.11.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -164,7 +164,8 @@ "overlay/menu.html", "popup/fonts/*" ], - "matches": [""] + "matches": [""], + "use_dynamic_url": true } ], "__firefox__browser_specific_settings": { diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 76ec18f496f..cfc39fa18a1 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -60,8 +60,8 @@ export class BrowserApi { } // Normalize both URLs by removing trailing slashes - const normalizedOrigin = sender.origin.replace(/\/$/, ""); - const normalizedExtensionUrl = extensionUrl.replace(/\/$/, ""); + const normalizedOrigin = sender.origin.replace(/\/$/, "").toLowerCase(); + const normalizedExtensionUrl = extensionUrl.replace(/\/$/, "").toLowerCase(); if (!normalizedOrigin.startsWith(normalizedExtensionUrl)) { logger?.warning( @@ -268,11 +268,11 @@ export class BrowserApi { static async closeTab(tabId: number): Promise { if (tabId) { if (BrowserApi.isWebExtensionsApi) { - browser.tabs.remove(tabId).catch((error) => { + await browser.tabs.remove(tabId).catch((error) => { throw new Error("[BrowserApi] Failed to remove current tab: " + error.message); }); } else if (BrowserApi.isChromeApi) { - chrome.tabs.remove(tabId).catch((error) => { + await chrome.tabs.remove(tabId).catch((error) => { throw new Error("[BrowserApi] Failed to remove current tab: " + error.message); }); } @@ -288,7 +288,7 @@ export class BrowserApi { static async navigateTabToUrl(tabId: number, url: URL): Promise { if (tabId) { if (BrowserApi.isWebExtensionsApi) { - browser.tabs.update(tabId, { url: url.href }).catch((error) => { + await browser.tabs.update(tabId, { url: url.href }).catch((error) => { throw new Error("Failed to navigate tab to URL: " + error.message); }); } else if (BrowserApi.isChromeApi) { diff --git a/apps/browser/src/platform/ipc/ipc-background.service.ts b/apps/browser/src/platform/ipc/ipc-background.service.ts index 911ca931c70..9fc2ca24b6a 100644 --- a/apps/browser/src/platform/ipc/ipc-background.service.ts +++ b/apps/browser/src/platform/ipc/ipc-background.service.ts @@ -8,6 +8,7 @@ import { OutgoingMessage, ipcRegisterDiscoverHandler, IpcClient, + IpcSessionRepository, } from "@bitwarden/sdk-internal"; import { BrowserApi } from "../browser/browser-api"; @@ -18,6 +19,7 @@ export class IpcBackgroundService extends IpcService { constructor( private platformUtilsService: PlatformUtilsService, private logService: LogService, + private sessionRepository: IpcSessionRepository, ) { super(); } @@ -70,7 +72,9 @@ export class IpcBackgroundService extends IpcService { ); }); - await super.initWithClient(new IpcClient(this.communicationBackend)); + await super.initWithClient( + IpcClient.newWithClientManagedSessions(this.communicationBackend, this.sessionRepository), + ); if (this.platformUtilsService.isDev()) { await ipcRegisterDiscoverHandler(this.client, { diff --git a/apps/browser/src/platform/storage/background-memory-storage.service.ts b/apps/browser/src/platform/storage/background-memory-storage.service.ts index 5e1bff99c39..e4431c30db6 100644 --- a/apps/browser/src/platform/storage/background-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/background-memory-storage.service.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LogService } from "@bitwarden/logging"; import { SerializedMemoryStorageService } from "@bitwarden/storage-core"; import { BrowserApi } from "../browser/browser-api"; @@ -11,14 +12,14 @@ import { portName } from "./port-name"; export class BackgroundMemoryStorageService extends SerializedMemoryStorageService { private _ports: chrome.runtime.Port[] = []; - constructor() { + constructor(private readonly logService: LogService) { super(); BrowserApi.addListener(chrome.runtime.onConnect, (port) => { if (port.name !== portName(chrome.storage.session)) { return; } - if (!BrowserApi.senderIsInternal(port.sender)) { + if (!BrowserApi.senderIsInternal(port.sender, this.logService)) { return; } diff --git a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts index 4a8f5d3f2ff..8004559f57c 100644 --- a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts +++ b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts @@ -4,6 +4,9 @@ */ import { trackEmissions } from "@bitwarden/common/../spec/utils"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { LogService } from "@bitwarden/logging"; import { mockPorts } from "../../../spec/mock-port.spec-util"; @@ -14,11 +17,13 @@ import { ForegroundMemoryStorageService } from "./foreground-memory-storage.serv describe.skip("foreground background memory storage interaction", () => { let foreground: ForegroundMemoryStorageService; let background: BackgroundMemoryStorageService; + let logService: MockProxy; beforeEach(() => { mockPorts(); + logService = mock(); - background = new BackgroundMemoryStorageService(); + background = new BackgroundMemoryStorageService(logService); foreground = new ForegroundMemoryStorageService(); }); diff --git a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts index 510348927ce..e1774dbbddd 100644 --- a/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts +++ b/apps/browser/src/popup/components/desktop-sync-verification-dialog.component.ts @@ -9,6 +9,7 @@ import { ButtonModule, DialogModule, DialogService, + CenterPositionStrategy, } from "@bitwarden/components"; export type DesktopSyncVerificationDialogParams = { @@ -49,6 +50,7 @@ export class DesktopSyncVerificationDialogComponent implements OnDestroy, OnInit static open(dialogService: DialogService, data: DesktopSyncVerificationDialogParams) { return dialogService.open(DesktopSyncVerificationDialogComponent, { data, + positionStrategy: new CenterPositionStrategy(), }); } } diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html index c6ea52aff62..a72847a5bf2 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html @@ -16,7 +16,7 @@ - - diff --git a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts index f81bccc760c..1b83c316f41 100644 --- a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts @@ -7,6 +7,7 @@ import { DialogModule, DialogService, TypographyModule, + CenterPositionStrategy, } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; import { DarkImageSourceDirective, VaultCarouselModule } from "@bitwarden/vault"; @@ -52,6 +53,7 @@ export class AtRiskCarouselDialogComponent { static open(dialogService: DialogService) { return dialogService.open(AtRiskCarouselDialogComponent, { disableClose: true, + positionStrategy: new CenterPositionStrategy(), }); } } diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts index 96c597113a5..9b972fd0f3e 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts @@ -61,6 +61,8 @@ class MockPopupPageComponent { template: ``, changeDetection: ChangeDetectionStrategy.OnPush, }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28231): Use Component suffix +// eslint-disable-next-line @angular-eslint/component-class-suffix class MockAppIcon { readonly cipher = input(undefined); } diff --git a/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.html b/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.html new file mode 100644 index 00000000000..6dbc7430638 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.html @@ -0,0 +1 @@ + diff --git a/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.ts b/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.ts new file mode 100644 index 00000000000..a30a447833b --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.ts @@ -0,0 +1,20 @@ +import { animate, style, transition, trigger } from "@angular/animations"; +import { ChangeDetectionStrategy, Component, HostBinding } from "@angular/core"; + +@Component({ + selector: "vault-fade-in-out", + templateUrl: "./vault-fade-in-out.component.html", + animations: [ + trigger("fadeInOut", [ + transition(":enter", [ + style({ opacity: 0 }), + animate("100ms ease-in", style({ opacity: 1 })), + ]), + transition(":leave", [animate("300ms ease-out", style({ opacity: 0 }))]), + ]), + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VaultFadeInOutComponent { + @HostBinding("@fadeInOut") fadeInOut = true; +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts index b314c48fecd..546a352111e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -49,6 +49,8 @@ import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-p PopOutComponent, ], }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28231): Use Component suffix +// eslint-disable-next-line @angular-eslint/component-class-suffix export class AssignCollections { /** Params needed to populate the assign collections component */ params: CollectionAssignmentParams; diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts index 2125af289a2..44a033137de 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts @@ -30,6 +30,8 @@ const blockedURISettingsRoute = "/blocked-domains"; selector: "blocked-injection-banner", templateUrl: "blocked-injection-banner.component.html", }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28231): Use Component suffix +// eslint-disable-next-line @angular-eslint/component-class-suffix export class BlockedInjectionBanner implements OnInit { /** * Flag indicating that the banner should be shown diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts index 2139b6d9a4f..8fa48dc5d79 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts @@ -24,6 +24,8 @@ import { selector: "vault-cipher-form-generator", template: "", }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28231): Use Component suffix +// eslint-disable-next-line @angular-eslint/component-class-suffix class MockCipherFormGenerator { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 6850a474af5..469247f9692 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -428,7 +428,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit { await this.vaultPopupSectionService.updateSectionOpenStoredState( this.collapsibleKey()!, - this.disclosure.open, + this.disclosure.open(), ); } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 5bca9cddd4f..7a5a99c8100 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -8,20 +8,32 @@ -
- - {{ "yourVaultIsEmpty" | i18n }} - -

{{ "emptyVaultDescription" | i18n }}

-
- - {{ "newLogin" | i18n }} - -
-
+ +
+ + {{ "yourVaultIsEmpty" | i18n }} + +

+ {{ "emptyVaultDescription" | i18n }} +

+
+ + {{ "newLogin" | i18n }} + +
+
+
+ + @if (skeletonFeatureFlag$ | async) { + + + + } @else { + + } + + + - - - - - + + + + + + + + + @if (skeletonFeatureFlag$ | async) { + + + + } @else { + + } @if (showSkeletonsLoaders$ | async) { diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts new file mode 100644 index 00000000000..5563cd3033b --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts @@ -0,0 +1,569 @@ +import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling"; +import { ChangeDetectionStrategy, Component, input, NO_ERRORS_SCHEMA } from "@angular/core"; +import { TestBed, fakeAsync, flush, tick } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { ActivatedRoute, Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject, Observable, Subject, of } from "rxjs"; + +import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components"; +import { NudgeType, NudgesService } from "@bitwarden/angular/vault"; +import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { CurrentAccountComponent } from "@bitwarden/browser/auth/popup/account-switching/current-account.component"; +import AutofillService from "@bitwarden/browser/autofill/services/autofill.service"; +import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; +import { PopupHeaderComponent } from "@bitwarden/browser/platform/popup/layout/popup-header.component"; +import { PopupRouterCacheService } from "@bitwarden/browser/platform/popup/view-cache/popup-router-cache.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { TaskService } from "@bitwarden/common/vault/tasks"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; +import { DecryptionFailureDialogComponent } from "@bitwarden/vault"; + +import { BrowserApi } from "../../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../../platform/browser/browser-popup-utils"; +import { IntroCarouselService } from "../../services/intro-carousel.service"; +import { VaultPopupAutofillService } from "../../services/vault-popup-autofill.service"; +import { VaultPopupCopyButtonsService } from "../../services/vault-popup-copy-buttons.service"; +import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; +import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service"; +import { VaultPopupScrollPositionService } from "../../services/vault-popup-scroll-position.service"; +import { AtRiskPasswordCalloutComponent } from "../at-risk-callout/at-risk-password-callout.component"; + +import { AutofillVaultListItemsComponent } from "./autofill-vault-list-items/autofill-vault-list-items.component"; +import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component"; +import { NewItemDropdownV2Component } from "./new-item-dropdown/new-item-dropdown-v2.component"; +import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component"; +import { VaultListItemsContainerComponent } from "./vault-list-items-container/vault-list-items-container.component"; +import { VaultV2Component } from "./vault-v2.component"; + +@Component({ + selector: "popup-header", + standalone: true, + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PopupHeaderStubComponent { + readonly pageTitle = input(""); +} + +@Component({ + selector: "app-vault-header-v2", + standalone: true, + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VaultHeaderV2StubComponent {} + +@Component({ + selector: "app-current-account", + standalone: true, + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class CurrentAccountStubComponent {} + +@Component({ + selector: "app-new-item-dropdown", + standalone: true, + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class NewItemDropdownStubComponent { + readonly initialValues = input(); +} + +@Component({ + selector: "app-pop-out", + standalone: true, + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class PopOutStubComponent {} + +@Component({ + selector: "blocked-injection-banner", + standalone: true, + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class BlockedInjectionBannerStubComponent {} + +@Component({ + selector: "vault-at-risk-password-callout", + standalone: true, + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class VaultAtRiskCalloutStubComponent {} + +@Component({ + selector: "app-autofill-vault-list-items", + standalone: true, + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class AutofillVaultListItemsStubComponent {} + +@Component({ + selector: "app-vault-list-items-container", + standalone: true, + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class VaultListItemsContainerStubComponent { + readonly title = input(); + readonly ciphers = input(); + readonly id = input(); + readonly disableSectionMargin = input(); + readonly collapsibleKey = input(); +} + +const mockDialogRef = { + close: jest.fn(), + afterClosed: jest.fn().mockReturnValue(of(undefined)), +} as unknown as import("@bitwarden/components").DialogRef; + +jest + .spyOn(PremiumUpgradeDialogComponent, "open") + .mockImplementation((_: DialogService) => mockDialogRef as any); + +jest + .spyOn(DecryptionFailureDialogComponent, "open") + .mockImplementation((_: DialogService, _params: any) => mockDialogRef as any); +jest.spyOn(BrowserApi, "isPopupOpen").mockResolvedValue(false); +jest.spyOn(BrowserPopupUtils, "openCurrentPagePopout").mockResolvedValue(); + +describe("VaultV2Component", () => { + let component: VaultV2Component; + + interface FakeAccount { + id: string; + } + + function queryAllSpotlights(fixture: any): HTMLElement[] { + return Array.from(fixture.nativeElement.querySelectorAll("bit-spotlight")) as HTMLElement[]; + } + + const itemsSvc: any = { + emptyVault$: new BehaviorSubject(false), + noFilteredResults$: new BehaviorSubject(false), + showDeactivatedOrg$: new BehaviorSubject(false), + favoriteCiphers$: new BehaviorSubject([]), + remainingCiphers$: new BehaviorSubject([]), + cipherCount$: new BehaviorSubject(0), + loading$: new BehaviorSubject(true), + } as Partial; + + const filtersSvc = { + allFilters$: new Subject(), + filters$: new BehaviorSubject({}), + filterVisibilityState$: new BehaviorSubject({}), + } as Partial; + + const accountActive$ = new BehaviorSubject({ id: "user-1" }); + + const cipherSvc = { + failedToDecryptCiphers$: jest.fn().mockReturnValue(of([])), + } as Partial; + + const nudgesSvc = { + showNudgeSpotlight$: jest.fn().mockImplementation((_type: NudgeType) => of(false)), + dismissNudge: jest.fn().mockResolvedValue(undefined), + } as Partial; + + const dialogSvc = {} as Partial; + + const introSvc = { + setIntroCarouselDismissed: jest.fn().mockResolvedValue(undefined), + } as Partial; + + const scrollSvc = { + start: jest.fn(), + stop: jest.fn(), + } as Partial; + + function getObs(cmp: any, key: string): Observable { + return cmp[key] as Observable; + } + + const hasPremiumFromAnySource$ = new BehaviorSubject(false); + + const billingSvc = { + hasPremiumFromAnySource$: (_: string) => hasPremiumFromAnySource$, + }; + + const vaultProfileSvc = { + getProfileCreationDate: jest + .fn() + .mockResolvedValue(new Date(Date.now() - 8 * 24 * 60 * 60 * 1000)), // 8 days ago + }; + + beforeEach(async () => { + jest.clearAllMocks(); + await TestBed.configureTestingModule({ + imports: [VaultV2Component, RouterTestingModule], + providers: [ + { provide: VaultPopupItemsService, useValue: itemsSvc }, + { provide: VaultPopupListFiltersService, useValue: filtersSvc }, + { provide: VaultPopupScrollPositionService, useValue: scrollSvc }, + { + provide: AccountService, + useValue: { activeAccount$: accountActive$ }, + }, + { provide: CipherService, useValue: cipherSvc }, + { provide: DialogService, useValue: dialogSvc }, + { provide: IntroCarouselService, useValue: introSvc }, + { provide: NudgesService, useValue: nudgesSvc }, + { + provide: VaultProfileService, + useValue: vaultProfileSvc, + }, + { + provide: VaultPopupCopyButtonsService, + useValue: { showQuickCopyActions$: new BehaviorSubject(false) }, + }, + { + provide: BillingAccountProfileStateService, + useValue: billingSvc, + }, + { + provide: I18nService, + useValue: { translate: (key: string) => key, t: (key: string) => key }, + }, + { provide: PopupRouterCacheService, useValue: mock() }, + { provide: RestrictedItemTypesService, useValue: { restricted$: new BehaviorSubject([]) } }, + { provide: PlatformUtilsService, useValue: mock() }, + { provide: AvatarService, useValue: mock() }, + { provide: ActivatedRoute, useValue: mock() }, + { provide: AuthService, useValue: mock() }, + { provide: AutofillService, useValue: mock() }, + { + provide: VaultPopupAutofillService, + useValue: mock(), + }, + { provide: TaskService, useValue: mock() }, + { provide: StateProvider, useValue: mock() }, + { + provide: ConfigService, + useValue: { + getFeatureFlag$: (_: string) => of(false), + }, + }, + { + provide: SearchService, + useValue: { isCipherSearching$: of(false) }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + TestBed.overrideComponent(VaultV2Component, { + remove: { + imports: [ + PopupHeaderComponent, + VaultHeaderV2Component, + CurrentAccountComponent, + NewItemDropdownV2Component, + PopOutComponent, + BlockedInjectionBanner, + AtRiskPasswordCalloutComponent, + AutofillVaultListItemsComponent, + VaultListItemsContainerComponent, + ], + }, + add: { + imports: [ + PopupHeaderStubComponent, + VaultHeaderV2StubComponent, + CurrentAccountStubComponent, + NewItemDropdownStubComponent, + PopOutStubComponent, + BlockedInjectionBannerStubComponent, + VaultAtRiskCalloutStubComponent, + AutofillVaultListItemsStubComponent, + VaultListItemsContainerStubComponent, + ], + }, + }); + + const fixture = TestBed.createComponent(VaultV2Component); + component = fixture.componentInstance; + }); + + describe("vaultState", () => { + type ExpectedKey = "Empty" | "DeactivatedOrg" | "NoResults" | null; + + const cases: [string, boolean, boolean, boolean, ExpectedKey][] = [ + ["null when none true", false, false, false, null], + ["Empty when empty true only", true, false, false, "Empty"], + ["DeactivatedOrg when only deactivated true", false, false, true, "DeactivatedOrg"], + ["NoResults when only noResults true", false, true, false, "NoResults"], + ]; + + it.each(cases)( + "%s", + fakeAsync( + ( + _label: string, + empty: boolean, + noResults: boolean, + deactivated: boolean, + expectedKey: ExpectedKey, + ) => { + const empty$ = itemsSvc.emptyVault$ as BehaviorSubject; + const noResults$ = itemsSvc.noFilteredResults$ as BehaviorSubject; + const deactivated$ = itemsSvc.showDeactivatedOrg$ as BehaviorSubject; + + empty$.next(empty); + noResults$.next(noResults); + deactivated$.next(deactivated); + tick(); + + const expectedValue = + expectedKey === null ? null : (component as any).VaultStateEnum[expectedKey]; + + expect((component as any).vaultState).toBe(expectedValue); + }, + ), + ); + }); + + it("loading$ is true when items loading or filters missing; false when both ready", () => { + const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject; + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + const values: boolean[] = []; + getObs(component, "loading$").subscribe((v) => values.push(!!v)); + + itemsLoading$.next(true); + + allFilters$.next({}); + + itemsLoading$.next(false); + + expect(values[values.length - 1]).toBe(false); + }); + + it("ngAfterViewInit waits for allFilters$ then starts scroll position service", fakeAsync(() => { + const allFilters$ = filtersSvc.allFilters$ as unknown as Subject; + + (component as any).virtualScrollElement = {} as CdkVirtualScrollableElement; + + component.ngAfterViewInit(); + expect(scrollSvc.start).not.toHaveBeenCalled(); + + allFilters$.next({ any: true }); + tick(); + + expect(scrollSvc.start).toHaveBeenCalledTimes(1); + expect(scrollSvc.start).toHaveBeenCalledWith((component as any).virtualScrollElement); + + flush(); + })); + + it("showPremiumDialog opens PremiumUpgradeDialogComponent", () => { + component["showPremiumDialog"](); + expect(PremiumUpgradeDialogComponent.open).toHaveBeenCalledTimes(1); + }); + + it("navigateToImport navigates and opens popout if popup is open", fakeAsync(async () => { + (BrowserApi.isPopupOpen as jest.Mock).mockResolvedValueOnce(true); + + const ngRouter = TestBed.inject(Router); + jest.spyOn(ngRouter, "navigate").mockResolvedValue(true as any); + + await component["navigateToImport"](); + + expect(ngRouter.navigate).toHaveBeenCalledWith(["/import"]); + + expect(BrowserPopupUtils.openCurrentPagePopout).toHaveBeenCalled(); + })); + + it("navigateToImport does not popout when popup is not open", fakeAsync(async () => { + (BrowserApi.isPopupOpen as jest.Mock).mockResolvedValueOnce(false); + + const ngRouter = TestBed.inject(Router); + jest.spyOn(ngRouter, "navigate").mockResolvedValue(true as any); + + await component["navigateToImport"](); + + expect(ngRouter.navigate).toHaveBeenCalledWith(["/import"]); + expect(BrowserPopupUtils.openCurrentPagePopout).not.toHaveBeenCalled(); + })); + + it("ngOnInit dismisses intro carousel and opens decryption dialog for non-deleted failures", fakeAsync(() => { + (cipherSvc.failedToDecryptCiphers$ as any).mockReturnValue( + of([ + { id: "a", isDeleted: false }, + { id: "b", isDeleted: true }, + { id: "c", isDeleted: false }, + ]), + ); + + void component.ngOnInit(); + tick(); + + expect(introSvc.setIntroCarouselDismissed).toHaveBeenCalled(); + + expect(DecryptionFailureDialogComponent.open).toHaveBeenCalledWith(expect.any(Object), { + cipherIds: ["a", "c"], + }); + + flush(); + })); + + it("dismissVaultNudgeSpotlight forwards to NudgesService with active user id", fakeAsync(() => { + const spy = jest.spyOn(nudgesSvc, "dismissNudge").mockResolvedValue(undefined); + + accountActive$.next({ id: "user-xyz" }); + + void component.ngOnInit(); + tick(); + + void component["dismissVaultNudgeSpotlight"](NudgeType.HasVaultItems); + tick(); + + expect(spy).toHaveBeenCalledWith(NudgeType.HasVaultItems, "user-xyz"); + })); + + it("accountAgeInDays$ computes integer days since creation", (done) => { + getObs(component, "accountAgeInDays$").subscribe((days) => { + if (days !== null) { + expect(days).toBeGreaterThanOrEqual(7); + done(); + } + }); + + void component.ngOnInit(); + }); + + it("renders Premium spotlight when eligible and opens dialog on click", fakeAsync(() => { + itemsSvc.cipherCount$.next(10); + + hasPremiumFromAnySource$.next(false); + + (nudgesSvc.showNudgeSpotlight$ as jest.Mock).mockImplementation((type: NudgeType) => + of(type === NudgeType.PremiumUpgrade), + ); + + const fixture = TestBed.createComponent(VaultV2Component); + const component = fixture.componentInstance; + + void component.ngOnInit(); + + fixture.detectChanges(); + tick(); + + fixture.detectChanges(); + + const spotlights = Array.from( + fixture.nativeElement.querySelectorAll("bit-spotlight"), + ) as HTMLElement[]; + expect(spotlights.length).toBe(1); + + const spotDe = fixture.debugElement.query(By.css("bit-spotlight")); + expect(spotDe).toBeTruthy(); + + spotDe.triggerEventHandler("onButtonClick", undefined); + fixture.detectChanges(); + + expect(PremiumUpgradeDialogComponent.open).toHaveBeenCalledTimes(1); + })); + + it("renders Empty-Vault spotlight when vaultState is Empty and nudge is on", fakeAsync(() => { + itemsSvc.emptyVault$.next(true); + + (nudgesSvc.showNudgeSpotlight$ as jest.Mock).mockImplementation((type: NudgeType) => { + return of(type === NudgeType.EmptyVaultNudge); + }); + + const fixture = TestBed.createComponent(VaultV2Component); + fixture.detectChanges(); + tick(); + + const spotlights = queryAllSpotlights(fixture); + expect(spotlights.length).toBe(1); + + expect(fixture.nativeElement.textContent).toContain("emptyVaultNudgeTitle"); + })); + + it("renders Has-Items spotlight when vault has items and nudge is on", fakeAsync(() => { + itemsSvc.emptyVault$.next(false); + + (nudgesSvc.showNudgeSpotlight$ as jest.Mock).mockImplementation((type: NudgeType) => { + return of(type === NudgeType.HasVaultItems); + }); + + const fixture = TestBed.createComponent(VaultV2Component); + fixture.detectChanges(); + tick(); + + const spotlights = queryAllSpotlights(fixture); + expect(spotlights.length).toBe(1); + + expect(fixture.nativeElement.textContent).toContain("hasItemsVaultNudgeTitle"); + })); + + it("does not render Premium spotlight when account is less than a week old", fakeAsync(() => { + itemsSvc.cipherCount$.next(10); + hasPremiumFromAnySource$.next(false); + + vaultProfileSvc.getProfileCreationDate = jest + .fn() + .mockResolvedValue(new Date(Date.now() - 3 * 24 * 60 * 60 * 1000)); // 3 days ago + + (nudgesSvc.showNudgeSpotlight$ as jest.Mock).mockImplementation((type: NudgeType) => { + return of(type === NudgeType.PremiumUpgrade); + }); + + const fixture = TestBed.createComponent(VaultV2Component); + fixture.detectChanges(); + tick(); + + const spotlights = queryAllSpotlights(fixture); + expect(spotlights.length).toBe(0); + })); + + it("does not render Premium spotlight when vault has less than 5 items", fakeAsync(() => { + itemsSvc.cipherCount$.next(3); + hasPremiumFromAnySource$.next(false); + + (nudgesSvc.showNudgeSpotlight$ as jest.Mock).mockImplementation((type: NudgeType) => { + return of(type === NudgeType.PremiumUpgrade); + }); + + const fixture = TestBed.createComponent(VaultV2Component); + fixture.detectChanges(); + tick(); + + const spotlights = queryAllSpotlights(fixture); + expect(spotlights.length).toBe(0); + })); + + it("does not render Premium spotlight when user already has premium", fakeAsync(() => { + itemsSvc.cipherCount$.next(10); + hasPremiumFromAnySource$.next(true); + + (nudgesSvc.showNudgeSpotlight$ as jest.Mock).mockImplementation((type: NudgeType) => { + return of(type === NudgeType.PremiumUpgrade); + }); + + const fixture = TestBed.createComponent(VaultV2Component); + fixture.detectChanges(); + tick(); + + const spotlights = queryAllSpotlights(fixture); + expect(spotlights.length).toBe(0); + })); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index e55a702d350..471e6e70601 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -9,6 +9,7 @@ import { distinctUntilChanged, filter, firstValueFrom, + from, map, Observable, shareReplay, @@ -17,19 +18,24 @@ import { tap, } from "rxjs"; +import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; +import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; import { DeactivatedOrg, NoResults, VaultOpen } from "@bitwarden/assets/svg"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; +import { skeletonLoadingDelay } from "@bitwarden/common/vault/utils/skeleton-loading.operator"; import { ButtonModule, DialogService, @@ -50,6 +56,7 @@ import { VaultPopupListFiltersService } from "../../services/vault-popup-list-fi import { VaultPopupLoadingService } from "../../services/vault-popup-loading.service"; import { VaultPopupScrollPositionService } from "../../services/vault-popup-scroll-position.service"; import { AtRiskPasswordCalloutComponent } from "../at-risk-callout/at-risk-password-callout.component"; +import { VaultFadeInOutComponent } from "../vault-fade-in-out/vault-fade-in-out.component"; import { VaultFadeInOutSkeletonComponent } from "../vault-fade-in-out-skeleton/vault-fade-in-out-skeleton.component"; import { VaultLoadingSkeletonComponent } from "../vault-loading-skeleton/vault-loading-skeleton.component"; @@ -96,6 +103,7 @@ type VaultState = UnionOfValues; TypographyModule, VaultLoadingSkeletonComponent, VaultFadeInOutSkeletonComponent, + VaultFadeInOutComponent, ], }) export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { @@ -124,22 +132,73 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { void this.liveAnnouncer.announce(this.i18nService.translate(key), "polite"); }), ); - private skeletonFeatureFlag$ = this.configService.getFeatureFlag$( + + protected skeletonFeatureFlag$ = this.configService.getFeatureFlag$( FeatureFlag.VaultLoadingSkeletons, ); + private showPremiumNudgeSpotlight$ = this.activeUserId$.pipe( + switchMap((userId) => this.nudgesService.showNudgeSpotlight$(NudgeType.PremiumUpgrade, userId)), + ); + protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$; protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$; protected allFilters$ = this.vaultPopupListFiltersService.allFilters$; + protected cipherCount$ = this.vaultPopupItemsService.cipherCount$; + protected hasPremium$ = this.activeUserId$.pipe( + switchMap((userId) => this.billingAccountService.hasPremiumFromAnySource$(userId)), + ); + protected accountAgeInDays$ = this.activeUserId$.pipe( + switchMap((userId) => { + const creationDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)); + return creationDate$.pipe( + map((creationDate) => { + if (!creationDate) { + return 0; + } + const ageInMilliseconds = Date.now() - creationDate.getTime(); + return Math.floor(ageInMilliseconds / (1000 * 60 * 60 * 24)); + }), + ); + }), + ); + + protected showPremiumSpotlight$ = combineLatest([ + this.showPremiumNudgeSpotlight$, + this.showEmptyVaultSpotlight$, + this.showHasItemsVaultSpotlight$, + this.hasPremium$, + this.cipherCount$, + this.accountAgeInDays$, + ]).pipe( + map( + ([showNudge, emptyVault, hasItems, hasPremium, count, age]) => + showNudge && !emptyVault && !hasItems && !hasPremium && count >= 5 && age >= 7, + ), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + showPremiumDialog() { + PremiumUpgradeDialogComponent.open(this.dialogService); + } /** When true, show spinner loading state */ protected showSpinnerLoaders$ = combineLatest([this.loading$, this.skeletonFeatureFlag$]).pipe( map(([loading, skeletonsEnabled]) => loading && !skeletonsEnabled), ); - /** When true, show skeleton loading state */ - protected showSkeletonsLoaders$ = combineLatest([this.loading$, this.skeletonFeatureFlag$]).pipe( - map(([loading, skeletonsEnabled]) => loading && skeletonsEnabled), + /** When true, show skeleton loading state with debouncing to prevent flicker */ + protected showSkeletonsLoaders$ = combineLatest([ + this.loading$, + this.searchService.isCipherSearching$, + this.skeletonFeatureFlag$, + ]).pipe( + map( + ([loading, cipherSearching, skeletonsEnabled]) => + (loading || cipherSearching) && skeletonsEnabled, + ), + distinctUntilChanged(), + skeletonLoadingDelay(), ); protected newItemItemValues$: Observable = @@ -177,9 +236,12 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { private introCarouselService: IntroCarouselService, private nudgesService: NudgesService, private router: Router, + private vaultProfileService: VaultProfileService, + private billingAccountService: BillingAccountProfileStateService, private liveAnnouncer: LiveAnnouncer, private i18nService: I18nService, private configService: ConfigService, + private searchService: SearchService, ) { combineLatest([ this.vaultPopupItemsService.emptyVault$, diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index afe9d61d5af..321d7936806 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -288,6 +288,11 @@ export class VaultPopupItemsService { map((ciphers) => !ciphers.length), ); + /** + * Observable that contains the count of ciphers in the active filtered list. + */ + cipherCount$: Observable = this._activeCipherList$.pipe(map((ciphers) => ciphers.length)); + /** * Observable that indicates whether there are no ciphers to show with the current filter. */ diff --git a/apps/cli/package.json b/apps/cli/package.json index 26e1183004a..fc38440b70f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -75,7 +75,7 @@ "inquirer": "8.2.6", "jsdom": "26.1.0", "jszip": "3.10.1", - "koa": "2.16.1", + "koa": "2.16.2", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", @@ -87,7 +87,7 @@ "papaparse": "5.5.3", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "semver": "7.7.2", + "semver": "7.7.3", "tldts": "7.0.1", "zxcvbn": "4.4.2" } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index c9f1d11210b..ebfb76eab2f 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -495,6 +495,7 @@ export class ServiceContainer { this.masterPasswordUnlockService = new DefaultMasterPasswordUnlockService( this.masterPasswordService, this.keyService, + this.logService, ); this.appIdService = new AppIdService(this.storageService, this.logService); diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 4da82144305..e8cc9385bb2 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -684,17 +684,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - [[package]] name = "colorchoice" version = "1.0.3" @@ -841,65 +830,6 @@ dependencies = [ "syn", ] -[[package]] -name = "cxx" -version = "1.0.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a71ea7f29c73f7ffa64c50b83c9fe4d3a6d4be89a86b009eb80d5a6d3429d741" -dependencies = [ - "cc", - "cxxbridge-cmd", - "cxxbridge-flags", - "cxxbridge-macro", - "foldhash", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a8232661d66dcf713394726157d3cfe0a89bfc85f52d6e9f9bbc2306797fe7" -dependencies = [ - "cc", - "codespan-reporting", - "proc-macro2", - "quote", - "scratch", - "syn", -] - -[[package]] -name = "cxxbridge-cmd" -version = "1.0.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f44296c8693e9ea226a48f6a122727f77aa9e9e338380cb021accaeeb7ee279" -dependencies = [ - "clap", - "codespan-reporting", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f69c181c176981ae44ba9876e2ea41ce8e574c296b38d06925ce9214fb8e4" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8faff5d4467e0709448187df29ccbf3b0982cc426ee444a193f87b11afb565a8" -dependencies = [ - "proc-macro2", - "quote", - "rustversion", - "syn", -] - [[package]] name = "der" version = "0.7.10" @@ -921,27 +851,21 @@ dependencies = [ "ashpd", "base64", "bitwarden-russh", - "byteorder", "bytes", "cbc", "chacha20poly1305", "core-foundation", "desktop_objc", "dirs", - "ed25519", "futures", "homedir", "interprocess", - "keytar", "libc", "linux-keyutils", "memsec", "oo7", "pin-project", - "pkcs8", "rand 0.9.1", - "rsa", - "russh-cryptovec", "scopeguard", "secmem-proc", "security-framework", @@ -949,12 +873,10 @@ dependencies = [ "serde", "serde_json", "sha2", - "ssh-encoding", "ssh-key", "sysinfo", "thiserror 2.0.12", "tokio", - "tokio-stream", "tokio-util", "tracing", "typenum", @@ -972,18 +894,14 @@ version = "0.0.0" dependencies = [ "anyhow", "autotype", - "base64", "chromium_importer", "desktop_core", - "hex", "napi", "napi-build", "napi-derive", "serde", "serde_json", "tokio", - "tokio-stream", - "tokio-util", "tracing", "tracing-subscriber", "windows-registry", @@ -996,9 +914,7 @@ version = "0.0.0" dependencies = [ "anyhow", "cc", - "core-foundation", "glob", - "thiserror 2.0.12", "tokio", "tracing", ] @@ -1007,7 +923,6 @@ dependencies = [ name = "desktop_proxy" version = "0.0.0" dependencies = [ - "anyhow", "desktop_core", "embed_plist", "futures", @@ -1740,27 +1655,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "keytar" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d361c55fba09829ac620b040f5425bf239b1030c3d6820a84acac8da867dca4d" -dependencies = [ - "keytar-sys", -] - -[[package]] -name = "keytar-sys" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe908c6896705a1cb516cd6a5d956c63f08d95ace81b93253a98cd93e1e6a65a" -dependencies = [ - "cc", - "cxx", - "cxx-build", - "pkg-config", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1813,15 +1707,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" -dependencies = [ - "cc", -] - [[package]] name = "linux-keyutils" version = "0.2.4" @@ -1875,7 +1760,6 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-util", "tracing", "tracing-oslog", "tracing-subscriber", @@ -2521,21 +2405,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "aes", - "cbc", - "der", - "pbkdf2", - "scrypt", - "sha2", - "spki", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -2543,8 +2412,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", - "pkcs5", - "rand_core 0.6.4", "spki", ] @@ -2923,27 +2790,12 @@ dependencies = [ "rustix 1.0.7", ] -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "scc" version = "2.4.0" @@ -2959,12 +2811,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scratch" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52" - [[package]] name = "scroll" version = "0.12.0" @@ -2985,17 +2831,6 @@ dependencies = [ "syn", ] -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "pbkdf2", - "salsa20", - "sha2", -] - [[package]] name = "sdd" version = "3.0.10" @@ -3370,15 +3205,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "termtree" version = "0.5.1" @@ -3483,17 +3309,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-stream" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.13" @@ -3693,12 +3508,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - [[package]] name = "uniffi" version = "0.28.3" @@ -4029,15 +3838,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index ccf7c1f3796..d7afd44e9cd 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -39,7 +39,6 @@ futures = "=0.3.31" hex = "=0.4.3" homedir = "=0.3.4" interprocess = "=2.2.1" -keytar = "=0.1.6" libc = "=0.2.172" linux-keyutils = "=0.2.4" memsec = "=0.7.0" @@ -64,7 +63,6 @@ ssh-key = { version = "=0.6.7", default-features = false } sysinfo = "=0.35.0" thiserror = "=2.0.12" tokio = "=1.45.0" -tokio-stream = "=0.1.15" tokio-util = "=0.7.13" tracing = "=0.1.41" tracing-subscriber = { version = "=0.3.20", features = [ diff --git a/apps/desktop/desktop_native/autotype/src/windows/type_input.rs b/apps/desktop/desktop_native/autotype/src/windows/type_input.rs index b757cf7752f..10f30f5ee4f 100644 --- a/apps/desktop/desktop_native/autotype/src/windows/type_input.rs +++ b/apps/desktop/desktop_native/autotype/src/windows/type_input.rs @@ -33,7 +33,8 @@ impl InputOperations for Win32InputOperations { /// Attempts to type the input text wherever the user's cursor is. /// /// `input` must be a vector of utf-16 encoded characters to insert. -/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super, Shift, letters a - Z +/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super, +/// Shift, letters a - Z /// /// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput pub(super) fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> { @@ -234,16 +235,16 @@ where #[cfg(test)] mod tests { - //! For the mocking of the traits that are static methods, we need to use the `serial_test` crate - //! in order to mock those, since the mock expectations set have to be global in absence of a `self`. - //! More info: https://docs.rs/mockall/latest/mockall/#static-methods + //! For the mocking of the traits that are static methods, we need to use the `serial_test` + //! crate in order to mock those, since the mock expectations set have to be global in + //! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods - use super::*; - - use crate::windowing::MockErrorOperations; use serial_test::serial; use windows::Win32::Foundation::WIN32_ERROR; + use super::*; + use crate::windowing::MockErrorOperations; + #[test] fn get_alphabetic_hot_key_succeeds() { for c in ('a'..='z').chain('A'..='Z') { diff --git a/apps/desktop/desktop_native/autotype/src/windows/window_title.rs b/apps/desktop/desktop_native/autotype/src/windows/window_title.rs index 58f06eb54c1..d56a811ab5c 100644 --- a/apps/desktop/desktop_native/autotype/src/windows/window_title.rs +++ b/apps/desktop/desktop_native/autotype/src/windows/window_title.rs @@ -127,8 +127,8 @@ where /// /// # Errors /// -/// - If the actual window title length (what the win32 API declares was written into the -/// buffer), is length zero and GetLastError() != 0 , return the GetLastError() message. +/// - If the actual window title length (what the win32 API declares was written into the buffer), +/// is length zero and GetLastError() != 0 , return the GetLastError() message. fn get_window_title(window_handle: &H, expected_title_length: usize) -> Result where H: WindowHandleOperations, @@ -169,17 +169,17 @@ where #[cfg(test)] mod tests { - //! For the mocking of the traits that are static methods, we need to use the `serial_test` crate - //! in order to mock those, since the mock expectations set have to be global in absence of a `self`. - //! More info: https://docs.rs/mockall/latest/mockall/#static-methods + //! For the mocking of the traits that are static methods, we need to use the `serial_test` + //! crate in order to mock those, since the mock expectations set have to be global in + //! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods - use super::*; - - use crate::windowing::MockErrorOperations; use mockall::predicate; use serial_test::serial; use windows::Win32::Foundation::WIN32_ERROR; + use super::*; + use crate::windowing::MockErrorOperations; + #[test] #[serial] fn get_window_title_length_can_be_zero() { diff --git a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/crypto.rs b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/crypto.rs index 094dbf94a67..c335a4b296a 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/crypto.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/crypto.rs @@ -95,7 +95,8 @@ pub(crate) fn decode_abe_key_blob(blob_data: &[u8]) -> Result> { let content_offset = content_len_offset + 4; let content = get_safe(blob_data, content_offset, content_len)?; - // When the size is exactly 32 bytes, it's a plain key. It's used in unbranded Chromium builds, Brave, possibly Edge + // When the size is exactly 32 bytes, it's a plain key. It's used in unbranded Chromium builds, + // Brave, possibly Edge if content_len == 32 { return Ok(content.to_vec()); } diff --git a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/impersonate.rs b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/impersonate.rs index 5a5109b9d32..22006b8db14 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/impersonate.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/impersonate.rs @@ -30,7 +30,8 @@ pub(crate) fn start_impersonating() -> Result { // Need to enable SE_DEBUG_PRIVILEGE to enumerate and open SYSTEM processes enable_debug_privilege()?; - // Find a SYSTEM process and get its token. Not every SYSTEM process allows token duplication, so try several. + // Find a SYSTEM process and get its token. Not every SYSTEM process allows token duplication, + // so try several. let (token, pid, name) = find_system_process_with_token(get_system_pid_list())?; // Impersonate the SYSTEM process diff --git a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/log.rs b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/log.rs index 7ee34a4160e..aa00a2f61b7 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/log.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/log.rs @@ -1,13 +1,13 @@ +use chromium_importer::config::{ENABLE_DEVELOPER_LOGGING, LOG_FILENAME}; use tracing::{error, level_filters::LevelFilter}; use tracing_subscriber::{ fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter, Layer as _, }; -use chromium_importer::config::{ENABLE_DEVELOPER_LOGGING, LOG_FILENAME}; - pub(crate) fn init_logging() { if ENABLE_DEVELOPER_LOGGING { - // We only log to a file. It's impossible to see stdout/stderr when this exe is launched from ShellExecuteW. + // We only log to a file. It's impossible to see stdout/stderr when this exe is launched + // from ShellExecuteW. match std::fs::File::create(LOG_FILENAME) { Ok(file) => { let file_filter = EnvFilter::builder() diff --git a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/main.rs b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/main.rs index e178a8accf7..560135b8ce4 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/main.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/src/windows/main.rs @@ -1,12 +1,14 @@ -use anyhow::{anyhow, Result}; -use clap::Parser; -use scopeguard::defer; use std::{ ffi::OsString, os::windows::{ffi::OsStringExt as _, io::AsRawHandle}, path::PathBuf, time::Duration, }; + +use anyhow::{anyhow, Result}; +use chromium_importer::chromium::{verify_signature, ADMIN_TO_USER_PIPE_NAME}; +use clap::Parser; +use scopeguard::defer; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::windows::named_pipe::{ClientOptions, NamedPipeClient}, @@ -25,8 +27,6 @@ use windows::Win32::{ UI::Shell::IsUserAnAdmin, }; -use chromium_importer::chromium::{verify_signature, ADMIN_TO_USER_PIPE_NAME}; - use super::{ crypto::{ decode_abe_key_blob, decode_base64, decrypt_with_dpapi_as_system, diff --git a/apps/desktop/desktop_native/chromium_importer/Cargo.toml b/apps/desktop/desktop_native/chromium_importer/Cargo.toml index 933b0a8dac3..4b02079bfdb 100644 --- a/apps/desktop/desktop_native/chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/chromium_importer/Cargo.toml @@ -7,35 +7,38 @@ publish = { workspace = true } [dependencies] aes = { workspace = true } -aes-gcm = { workspace = true } anyhow = { workspace = true } async-trait = "=0.1.88" -base64 = { workspace = true } -cbc = { workspace = true, features = ["alloc"] } dirs = { workspace = true } hex = { workspace = true } -pbkdf2 = "=0.12.2" rand = { workspace = true } rusqlite = { version = "=0.37.0", features = ["bundled"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -sha1 = "=0.10.6" -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] +cbc = { workspace = true, features = ["alloc"] } +pbkdf2 = "=0.12.2" security-framework = { workspace = true } +sha1 = "=0.10.6" [target.'cfg(target_os = "windows")'.dependencies] +aes-gcm = { workspace = true } +base64 = { workspace = true } windows = { workspace = true, features = [ "Win32_Security_Cryptography", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", ] } verifysign = "=0.2.4" +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] +cbc = { workspace = true, features = ["alloc"] } oo7 = { workspace = true } +pbkdf2 = "=0.12.2" +sha1 = "=0.10.6" [lints] workspace = true diff --git a/apps/desktop/desktop_native/chromium_importer/src/chromium/mod.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/mod.rs index aec8a84b5c1..e57b40b5778 100644 --- a/apps/desktop/desktop_native/chromium_importer/src/chromium/mod.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/mod.rs @@ -1,5 +1,8 @@ -use std::path::{Path, PathBuf}; -use std::sync::LazyLock; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::LazyLock, +}; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -9,11 +12,10 @@ use rusqlite::{params, Connection}; mod platform; +pub(crate) use platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS; #[cfg(target_os = "windows")] pub use platform::*; -pub(crate) use platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS; - // // Public API // @@ -86,14 +88,15 @@ pub async fn import_logins( let local_logins = get_logins(&data_dir, profile_id, "Login Data") .map_err(|e| anyhow!("Failed to query logins: {}", e))?; - // This is not available in all browsers, but there's no harm in trying. If the file doesn't exist we just get an empty vector. + // This is not available in all browsers, but there's no harm in trying. If the file doesn't + // exist we just get an empty vector. let account_logins = get_logins(&data_dir, profile_id, "Login Data For Account") .map_err(|e| anyhow!("Failed to query logins: {}", e))?; // TODO: Do we need a better merge strategy? Maybe ignore duplicates at least? - // TODO: Should we also ignore an error from one of the two imports? If one is successful and the other fails, - // should we still return the successful ones? At the moment it doesn't fail for a missing file, only when - // something goes really wrong. + // TODO: Should we also ignore an error from one of the two imports? If one is successful and + // the other fails, should we still return the successful ones? At the moment it + // doesn't fail for a missing file, only when something goes really wrong. let all_logins = local_logins .into_iter() .chain(account_logins.into_iter()) @@ -148,13 +151,13 @@ pub(crate) struct LocalState { #[derive(serde::Deserialize, Clone)] struct AllProfiles { - info_cache: std::collections::HashMap, + info_cache: HashMap, } #[derive(serde::Deserialize, Clone)] struct OneProfile { name: String, - gaia_name: Option, + gaia_id: Option, user_name: Option, } @@ -197,10 +200,14 @@ fn get_profile_info(local_state: &LocalState) -> Vec { .profile .info_cache .iter() - .map(|(name, info)| ProfileInfo { - name: info.name.clone(), - folder: name.clone(), - account_name: info.gaia_name.clone(), + .map(|(folder, info)| ProfileInfo { + name: if !info.name.trim().is_empty() { + info.name.clone() + } else { + folder.clone() + }, + folder: folder.clone(), + account_name: info.gaia_id.clone(), account_email: info.user_name.clone(), }) .collect() @@ -348,3 +355,111 @@ async fn decrypt_login( }), } } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_local_state(profiles: Vec<(&str, &str, Option<&str>, Option<&str>)>) -> LocalState { + let info_cache = profiles + .into_iter() + .map(|(folder, name, gaia_id, user_name)| { + ( + folder.to_string(), + OneProfile { + name: name.to_string(), + gaia_id: gaia_id.map(|s| s.to_string()), + user_name: user_name.map(|s| s.to_string()), + }, + ) + }) + .collect::>(); + + LocalState { + profile: AllProfiles { info_cache }, + os_crypt: None, + } + } + + #[test] + fn test_get_profile_info_basic() { + let local_state = make_local_state(vec![ + ( + "Profile 1", + "User 1", + Some("Account 1"), + Some("email1@example.com"), + ), + ( + "Profile 2", + "User 2", + Some("Account 2"), + Some("email2@example.com"), + ), + ]); + let infos = get_profile_info(&local_state); + assert_eq!(infos.len(), 2); + + let profile1 = infos.iter().find(|p| p.folder == "Profile 1").unwrap(); + assert_eq!(profile1.name, "User 1"); + assert_eq!(profile1.account_name.as_deref(), Some("Account 1")); + assert_eq!( + profile1.account_email.as_deref(), + Some("email1@example.com") + ); + + let profile2 = infos.iter().find(|p| p.folder == "Profile 2").unwrap(); + assert_eq!(profile2.name, "User 2"); + assert_eq!(profile2.account_name.as_deref(), Some("Account 2")); + assert_eq!( + profile2.account_email.as_deref(), + Some("email2@example.com") + ); + } + + #[test] + fn test_get_profile_info_empty_name() { + let local_state = make_local_state(vec![( + "ProfileX", + "", + Some("AccountX"), + Some("emailx@example.com"), + )]); + let infos = get_profile_info(&local_state); + assert_eq!(infos.len(), 1); + assert_eq!(infos[0].name, "ProfileX"); + assert_eq!(infos[0].folder, "ProfileX"); + } + + #[test] + fn test_get_profile_info_none_fields() { + let local_state = make_local_state(vec![("ProfileY", "NameY", None, None)]); + let infos = get_profile_info(&local_state); + assert_eq!(infos.len(), 1); + assert_eq!(infos[0].name, "NameY"); + assert_eq!(infos[0].account_name, None); + assert_eq!(infos[0].account_email, None); + } + + #[test] + fn test_get_profile_info_multiple_profiles() { + let local_state = make_local_state(vec![ + ("P1", "N1", Some("A1"), Some("E1")), + ("P2", "", None, None), + ("P3", "N3", Some("A3"), None), + ]); + let infos = get_profile_info(&local_state); + assert_eq!(infos.len(), 3); + + let p1 = infos.iter().find(|p| p.folder == "P1").unwrap(); + assert_eq!(p1.name, "N1"); + + let p2 = infos.iter().find(|p| p.folder == "P2").unwrap(); + assert_eq!(p2.name, "P2"); + + let p3 = infos.iter().find(|p| p.folder == "P3").unwrap(); + assert_eq!(p3.name, "N3"); + assert_eq!(p3.account_name.as_deref(), Some("A3")); + assert_eq!(p3.account_email, None); + } +} diff --git a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/linux.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/linux.rs index 227dffdcca7..14e38797640 100644 --- a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/linux.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/linux.rs @@ -4,15 +4,17 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use oo7::XDG_SCHEMA_ATTRIBUTE; -use crate::chromium::{BrowserConfig, CryptoService, LocalState}; - -use crate::util; +use crate::{ + chromium::{BrowserConfig, CryptoService, LocalState}, + util, +}; // // Public API // -// TODO: It's possible that there might be multiple possible data directories, depending on the installation method (e.g., snap, flatpak, etc.). +// TODO: It's possible that there might be multiple possible data directories, depending on the +// installation method (e.g., snap, flatpak, etc.). pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[ BrowserConfig { name: "Chrome", diff --git a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/macos.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/macos.rs index c0e770c161b..5d0b4f0c75c 100644 --- a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/macos.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/macos.rs @@ -2,9 +2,10 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use security_framework::passwords::get_generic_password; -use crate::chromium::{BrowserConfig, CryptoService, LocalState}; - -use crate::util; +use crate::{ + chromium::{BrowserConfig, CryptoService, LocalState}, + util, +}; // // Public API diff --git a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/abe.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/abe.rs index 943727690f2..a76f7b95e5c 100644 --- a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/abe.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/abe.rs @@ -1,6 +1,6 @@ -use super::abe_config; -use anyhow::{anyhow, Result}; use std::{ffi::OsStr, os::windows::ffi::OsStrExt}; + +use anyhow::{anyhow, Result}; use tokio::{ io::{self, AsyncReadExt, AsyncWriteExt}, net::windows::named_pipe::{NamedPipeServer, ServerOptions}, @@ -14,6 +14,8 @@ use windows::{ Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_HIDE}, }; +use super::abe_config; + const WAIT_FOR_ADMIN_MESSAGE_TIMEOUT_SECS: u64 = 30; fn start_tokio_named_pipe_server( diff --git a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/mod.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/mod.rs index 867104d9bfd..9cc89ed2161 100644 --- a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/mod.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/mod.rs @@ -1,11 +1,14 @@ +use std::path::{Path, PathBuf}; + use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit, Nonce}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; -use std::path::{Path, PathBuf}; -use crate::chromium::{BrowserConfig, CryptoService, LocalState}; -use crate::util; +use crate::{ + chromium::{BrowserConfig, CryptoService, LocalState}, + util, +}; mod abe; mod abe_config; mod crypto; @@ -95,7 +98,8 @@ impl CryptoService for WindowsCryptoService { let (version, no_prefix) = util::split_encrypted_string_and_validate(encrypted, &["v10", "v20"])?; - // v10 is already stripped; Windows Chrome uses AES-GCM: [12 bytes IV][ciphertext][16 bytes auth tag] + // v10 is already stripped; Windows Chrome uses AES-GCM: [12 bytes IV][ciphertext][16 bytes + // auth tag] const IV_SIZE: usize = 12; const TAG_SIZE: usize = 16; const MIN_LENGTH: usize = IV_SIZE + TAG_SIZE; @@ -242,8 +246,8 @@ fn get_dist_admin_exe_path(current_exe_full_path: &Path) -> Result { Ok(admin_exe) } -// Try to find bitwarden_chromium_import_helper.exe in debug build folders. This might not cover all the cases. -// Tested on `npm run electron` from apps/desktop and apps/desktop/desktop_native. +// Try to find bitwarden_chromium_import_helper.exe in debug build folders. This might not cover all +// the cases. Tested on `npm run electron` from apps/desktop and apps/desktop/desktop_native. fn get_debug_admin_exe_path() -> Result { let current_dir = std::env::current_dir()?; let folder_name = current_dir diff --git a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/signature.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/signature.rs index d5d6c5d6d15..97cf57935b2 100644 --- a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/signature.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows/signature.rs @@ -1,5 +1,6 @@ -use anyhow::{anyhow, Result}; use std::path::Path; + +use anyhow::{anyhow, Result}; use tracing::{debug, info}; use verifysign::CodeSignVerifier; diff --git a/apps/desktop/desktop_native/chromium_importer/src/metadata.rs b/apps/desktop/desktop_native/chromium_importer/src/metadata.rs index bfd7f184621..114c9f8df84 100644 --- a/apps/desktop/desktop_native/chromium_importer/src/metadata.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/metadata.rs @@ -59,9 +59,9 @@ pub fn get_supported_importers( // Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage #[cfg(test)] mod tests { - use super::*; use std::collections::HashSet; + use super::*; use crate::chromium::{InstalledBrowserRetriever, SUPPORTED_BROWSER_MAP}; pub struct MockInstalledBrowserRetriever {} diff --git a/apps/desktop/desktop_native/chromium_importer/src/util.rs b/apps/desktop/desktop_native/chromium_importer/src/util.rs index f346d7e6dd0..2dbc6ed005b 100644 --- a/apps/desktop/desktop_native/chromium_importer/src/util.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/util.rs @@ -32,7 +32,7 @@ pub(crate) fn split_encrypted_string_and_validate<'a>( } /// Decrypt using AES-128 in CBC mode. -#[cfg(any(target_os = "linux", target_os = "macos", test))] +#[cfg(any(target_os = "linux", target_os = "macos"))] pub(crate) fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result> { use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}; @@ -41,7 +41,8 @@ pub(crate) fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> R .map_err(|e| anyhow!("Failed to decrypt: {}", e)) } -/// Derives a PBKDF2 key from the static "saltysalt" salt with the given password and iteration count. +/// Derives a PBKDF2 key from the static "saltysalt" salt with the given password and iteration +/// count. #[cfg(any(target_os = "linux", target_os = "macos"))] pub(crate) fn derive_saltysalt(password: &[u8], iterations: u32) -> Result> { use pbkdf2::{hmac::Hmac, pbkdf2}; @@ -55,27 +56,9 @@ pub(crate) fn derive_saltysalt(password: &[u8], iterations: u32) -> Result Vec { - (0..length).map(|i| offset + i as u8 * increment).collect() - } - - fn generate_generic_array>( - offset: u8, - increment: u8, - ) -> GenericArray { - GenericArray::generate(|i| offset + i as u8 * increment) - } - fn run_split_encrypted_string_test<'a, const N: usize>( successfully_split: bool, plaintext_to_encrypt: &'a str, @@ -144,8 +127,28 @@ mod tests { run_split_encrypted_string_and_validate_test(false, "v10EncryptMe!", &[]); } + #[cfg(any(target_os = "linux", target_os = "macos"))] #[test] fn test_decrypt_aes_128_cbc() { + use aes::cipher::{ + block_padding::Pkcs7, + generic_array::{sequence::GenericSequence, GenericArray}, + ArrayLength, BlockEncryptMut, KeyIvInit, + }; + + const LENGTH16: usize = 16; + + fn generate_generic_array>( + offset: u8, + increment: u8, + ) -> GenericArray { + GenericArray::generate(|i| offset + i as u8 * increment) + } + + fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec { + (0..length).map(|i| offset + i as u8 * increment).collect() + } + let offset = 0; let increment = 1; diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index f6c9d669df6..dc9246f55c6 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -23,27 +23,15 @@ anyhow = { workspace = true } arboard = { workspace = true, features = ["wayland-data-control"] } base64 = { workspace = true } bitwarden-russh = { workspace = true } -byteorder = { workspace = true } bytes = { workspace = true } cbc = { workspace = true, features = ["alloc"] } chacha20poly1305 = { workspace = true } dirs = { workspace = true } -ed25519 = { workspace = true, features = ["pkcs8"] } futures = { workspace = true } -homedir = { workspace = true } interprocess = { workspace = true, features = ["tokio"] } memsec = { workspace = true, features = ["alloc_ext"] } -pin-project = { workspace = true } -pkcs8 = { workspace = true, features = ["alloc", "encryption", "pem"] } rand = { workspace = true } -rsa = { workspace = true } -russh-cryptovec = { workspace = true } -scopeguard = { workspace = true } -secmem-proc = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } sha2 = { workspace = true } -ssh-encoding = { workspace = true } ssh-key = { workspace = true, features = [ "encryption", "ed25519", @@ -53,13 +41,17 @@ ssh-key = { workspace = true, features = [ sysinfo = { workspace = true, features = ["windows"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] } -tokio-stream = { workspace = true, features = ["net"] } tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } typenum = { workspace = true } zeroizing-alloc = { workspace = true } [target.'cfg(windows)'.dependencies] +pin-project = { workspace = true } +scopeguard = { workspace = true } +secmem-proc = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } widestring = { workspace = true, optional = true } windows = { workspace = true, features = [ "Foundation", @@ -76,21 +68,20 @@ windows = { workspace = true, features = [ ], optional = true } windows-future = { workspace = true } -[target.'cfg(windows)'.dev-dependencies] -keytar = { workspace = true } - [target.'cfg(target_os = "macos")'.dependencies] core-foundation = { workspace = true, optional = true } +homedir = { workspace = true } +secmem-proc = { workspace = true } security-framework = { workspace = true, optional = true } security-framework-sys = { workspace = true, optional = true } desktop_objc = { path = "../objc" } [target.'cfg(target_os = "linux")'.dependencies] -oo7 = { workspace = true } +ashpd = { workspace = true } +homedir = { workspace = true } libc = { workspace = true } linux-keyutils = { workspace = true } -ashpd = { workspace = true } - +oo7 = { workspace = true } zbus = { workspace = true, optional = true } zbus_polkit = { workspace = true, optional = true } diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index e4d51f5da9a..937c67ff30a 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -86,11 +86,15 @@ impl KeyMaterial { #[cfg(test)] mod tests { - use crate::biometric::{decrypt, encrypt, KeyMaterial}; - use crate::crypto::CipherString; - use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; use std::str::FromStr; + use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; + + use crate::{ + biometric::{decrypt, encrypt, KeyMaterial}, + crypto::CipherString, + }; + fn key_material() -> KeyMaterial { KeyMaterial { os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index 0f6ff8f33dc..3f4f10a1fcf 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -1,18 +1,18 @@ use std::str::FromStr; -use anyhow::Result; +use anyhow::{anyhow, Result}; use base64::Engine; use rand::RngCore; use sha2::{Digest, Sha256}; use tracing::error; - -use crate::biometric::{base64_engine, KeyMaterial, OsDerivedKey}; use zbus::Connection; use zbus_polkit::policykit1::*; use super::{decrypt, encrypt}; -use crate::crypto::CipherString; -use anyhow::anyhow; +use crate::{ + biometric::{base64_engine, KeyMaterial, OsDerivedKey}, + crypto::CipherString, +}; /// The Unix implementation of the biometric trait. pub struct Biometric {} diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index 8013c21bf9a..f72282d9284 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -16,13 +16,12 @@ use windows::{ }; use windows_future::IAsyncOperation; +use super::{decrypt, encrypt, windows_focus::set_focus}; use crate::{ biometric::{KeyMaterial, OsDerivedKey}, crypto::CipherString, }; -use super::{decrypt, encrypt, windows_focus::set_focus}; - /// The Windows OS implementation of the biometric trait. pub struct Biometric {} @@ -61,7 +60,8 @@ impl super::BiometricTrait for Biometric { match ucv_available { UserConsentVerifierAvailability::Available => Ok(true), - UserConsentVerifierAvailability::DeviceBusy => Ok(true), // TODO: Look into removing this and making the check more ad-hoc + // TODO: look into removing this and making the check more ad-hoc + UserConsentVerifierAvailability::DeviceBusy => Ok(true), _ => Ok(false), } } @@ -133,7 +133,6 @@ fn random_challenge() -> [u8; 16] { #[cfg(test)] mod tests { use super::*; - use crate::biometric::BiometricTrait; #[test] diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs index 44cba4a9e5b..ff2abc0686b 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs +++ b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs @@ -1,17 +1,19 @@ //! This file implements Polkit based system unlock. //! //! # Security -//! This section describes the assumed security model and security guarantees achieved. In the required security -//! guarantee is that a locked vault - a running app - cannot be unlocked when the device (user-space) -//! is compromised in this state. +//! This section describes the assumed security model and security guarantees achieved. In the +//! required security guarantee is that a locked vault - a running app - cannot be unlocked when the +//! device (user-space) is compromised in this state. //! -//! When first unlocking the app, the app sends the user-key to this module, which holds it in secure memory, -//! protected by memfd_secret. This makes it inaccessible to other processes, even if they compromise root, a kernel compromise -//! has circumventable best-effort protections. While the app is running this key is held in memory, even if locked. -//! When unlocking, the app will prompt the user via `polkit` to get a yes/no decision on whether to release the key to the app. +//! When first unlocking the app, the app sends the user-key to this module, which holds it in +//! secure memory, protected by memfd_secret. This makes it inaccessible to other processes, even if +//! they compromise root, a kernel compromise has circumventable best-effort protections. While the +//! app is running this key is held in memory, even if locked. When unlocking, the app will prompt +//! the user via `polkit` to get a yes/no decision on whether to release the key to the app. + +use std::sync::Arc; use anyhow::{anyhow, Result}; -use std::sync::Arc; use tokio::sync::Mutex; use tracing::{debug, warn}; use zbus::Connection; @@ -20,8 +22,8 @@ use zbus_polkit::policykit1::{AuthorityProxy, CheckAuthorizationFlags, Subject}; use crate::secure_memory::*; pub struct BiometricLockSystem { - // The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure - // locked vaults cannot be unlocked + // The userkeys that are held in memory MUST be protected from memory dumping attacks, to + // ensure locked vaults cannot be unlocked secure_memory: Arc>, } @@ -88,8 +90,9 @@ impl super::BiometricTrait for BiometricLockSystem { } } -/// Perform a polkit authorization against the bitwarden unlock policy. Note: This relies on no custom -/// rules in the system skipping the authorization check, in which case this counts as UV / authentication. +/// Perform a polkit authorization against the bitwarden unlock policy. Note: This relies on no +/// custom rules in the system skipping the authorization check, in which case this counts as UV / +/// authentication. async fn polkit_authenticate_bitwarden_policy() -> Result { debug!("[Polkit] Authenticating / performing UV"); diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs b/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs index 669267b7829..55aee27dd33 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric_v2/mod.rs @@ -17,8 +17,8 @@ pub trait BiometricTrait: Send + Sync { async fn authenticate(&self, hwnd: Vec, message: String) -> Result; /// Check if biometric authentication is available async fn authenticate_available(&self) -> Result; - /// Enroll a key for persistent unlock. If the implementation does not support persistent enrollment, - /// this function should do nothing. + /// Enroll a key for persistent unlock. If the implementation does not support persistent + /// enrollment, this function should do nothing. async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()>; /// Clear the persistent and ephemeral keys async fn unenroll(&self, user_id: &str) -> Result<()>; @@ -28,6 +28,7 @@ pub trait BiometricTrait: Send + Sync { async fn provide_key(&self, user_id: &str, key: &[u8]); /// Perform biometric unlock and return the key async fn unlock(&self, user_id: &str, hwnd: Vec) -> Result>; - /// Check if biometric unlock is available based on whether a key is present and whether authentication is possible + /// Check if biometric unlock is available based on whether a key is present and whether + /// authentication is possible async fn unlock_available(&self, user_id: &str) -> Result; } diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs b/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs index 043c2453cd0..32d2eb7e6e6 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs @@ -2,38 +2,40 @@ //! //! There are two paths implemented here. //! The former via UV + ephemerally (but protected) keys. This only works after first unlock. -//! The latter via a signing API, that deterministically signs a challenge, from which a windows hello key is derived. This key -//! is used to encrypt the protected key. +//! The latter via a signing API, that deterministically signs a challenge, from which a windows +//! hello key is derived. This key is used to encrypt the protected key. //! //! # Security -//! The security goal is that a locked vault - a running app - cannot be unlocked when the device (user-space) -//! is compromised in this state. +//! The security goal is that a locked vault - a running app - cannot be unlocked when the device +//! (user-space) is compromised in this state. //! //! ## UV path -//! When first unlocking the app, the app sends the user-key to this module, which holds it in secure memory, -//! protected by DPAPI. This makes it inaccessible to other processes, unless they compromise the system administrator, or kernel. -//! While the app is running this key is held in memory, even if locked. When unlocking, the app will prompt the user via +//! When first unlocking the app, the app sends the user-key to this module, which holds it in +//! secure memory, protected by DPAPI. This makes it inaccessible to other processes, unless they +//! compromise the system administrator, or kernel. While the app is running this key is held in +//! memory, even if locked. When unlocking, the app will prompt the user via //! `windows_hello_authenticate` to get a yes/no decision on whether to release the key to the app. -//! Note: Further process isolation is needed here so that code cannot be injected into the running process, which may -//! circumvent DPAPI. +//! Note: Further process isolation is needed here so that code cannot be injected into the running +//! process, which may circumvent DPAPI. //! //! ## Sign path -//! In this scenario, when enrolling, the app sends the user-key to this module, which derives the windows hello key -//! with the Windows Hello prompt. This is done by signing a per-user challenge, which produces a deterministic -//! signature which is hashed to obtain a key. This key is used to encrypt and persist the vault unlock key (user key). +//! In this scenario, when enrolling, the app sends the user-key to this module, which derives the +//! windows hello key with the Windows Hello prompt. This is done by signing a per-user challenge, +//! which produces a deterministic signature which is hashed to obtain a key. This key is used to +//! encrypt and persist the vault unlock key (user key). //! -//! Since the keychain can be accessed by all user-space processes, the challenge is known to all userspace processes. -//! Therefore, to circumvent the security measure, the attacker would need to create a fake Windows-Hello prompt, and -//! get the user to confirm it. +//! Since the keychain can be accessed by all user-space processes, the challenge is known to all +//! userspace processes. Therefore, to circumvent the security measure, the attacker would need to +//! create a fake Windows-Hello prompt, and get the user to confirm it. use std::sync::{atomic::AtomicBool, Arc}; -use tracing::{debug, warn}; use aes::cipher::KeyInit; use anyhow::{anyhow, Result}; use chacha20poly1305::{aead::Aead, XChaCha20Poly1305, XNonce}; use sha2::{Digest, Sha256}; use tokio::sync::Mutex; +use tracing::{debug, warn}; use windows::{ core::{factory, h, Interface, HSTRING}, Security::{ @@ -74,8 +76,8 @@ struct WindowsHelloKeychainEntry { /// The Windows OS implementation of the biometric trait. pub struct BiometricLockSystem { - // The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure - // locked vaults cannot be unlocked + // The userkeys that are held in memory MUST be protected from memory dumping attacks, to + // ensure locked vaults cannot be unlocked secure_memory: Arc>, } @@ -114,12 +116,14 @@ impl super::BiometricTrait for BiometricLockSystem { } async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()> { - // Enrollment works by first generating a random challenge unique to the user / enrollment. Then, - // with the challenge and a Windows-Hello prompt, the "windows hello key" is derived. The windows - // hello key is used to encrypt the key to store with XChaCha20Poly1305. The bundle of nonce, - // challenge and wrapped-key are stored to the keychain + // Enrollment works by first generating a random challenge unique to the user / enrollment. + // Then, with the challenge and a Windows-Hello prompt, the "windows hello key" is + // derived. The windows hello key is used to encrypt the key to store with + // XChaCha20Poly1305. The bundle of nonce, challenge and wrapped-key are stored to + // the keychain - // Each enrollment (per user) has a unique challenge, so that the windows-hello key is unique + // Each enrollment (per user) has a unique challenge, so that the windows-hello key is + // unique let challenge: [u8; CHALLENGE_LENGTH] = rand::random(); // This key is unique to the challenge @@ -155,8 +159,8 @@ impl super::BiometricTrait for BiometricLockSystem { }); let mut secure_memory = self.secure_memory.lock().await; - // If the key is held ephemerally, always use UV API. Only use signing API if the key is not held - // ephemerally but the keychain holds it persistently. + // If the key is held ephemerally, always use UV API. Only use signing API if the key is not + // held ephemerally but the keychain holds it persistently. if secure_memory.has(user_id) { if windows_hello_authenticate("Unlock your vault".to_string()).await? { secure_memory @@ -175,7 +179,8 @@ impl super::BiometricTrait for BiometricLockSystem { &keychain_entry.wrapped_key, &keychain_entry.nonce, )?; - // The first unlock already sets the key for subsequent unlocks. The key may again be set externally after unlock finishes. + // The first unlock already sets the key for subsequent unlocks. The key may again be + // set externally after unlock finishes. secure_memory.put(user_id.to_string(), &decrypted_key.clone()); Ok(decrypted_key) } @@ -231,8 +236,8 @@ async fn windows_hello_authenticate_with_crypto( ) -> Result<[u8; XCHACHA20POLY1305_KEY_LENGTH]> { debug!("[Windows Hello] Authenticating to sign challenge"); - // Ugly hack: We need to focus the window via window focusing APIs until Microsoft releases a new API. - // This is unreliable, and if it does not work, the operation may fail + // Ugly hack: We need to focus the window via window focusing APIs until Microsoft releases a + // new API. This is unreliable, and if it does not work, the operation may fail let stop_focusing = Arc::new(AtomicBool::new(false)); let stop_focusing_clone = stop_focusing.clone(); let _ = std::thread::spawn(move || loop { @@ -243,8 +248,8 @@ async fn windows_hello_authenticate_with_crypto( break; } }); - // Only stop focusing once this function exits. The focus MUST run both during the initial creation - // with RequestCreateAsync, and also with the subsequent use with RequestSignAsync. + // Only stop focusing once this function exits. The focus MUST run both during the initial + // creation with RequestCreateAsync, and also with the subsequent use with RequestSignAsync. let _guard = scopeguard::guard((), |_| { stop_focusing.store(true, std::sync::atomic::Ordering::Relaxed); }); @@ -283,8 +288,8 @@ async fn windows_hello_authenticate_with_crypto( let signature_buffer = signature.Result()?; let signature_value = unsafe { as_mut_bytes(&signature_buffer)? }; - // The signature is deterministic based on the challenge and keychain key. Thus, it can be hashed to a key. - // It is unclear what entropy this key provides. + // The signature is deterministic based on the challenge and keychain key. Thus, it can be + // hashed to a key. It is unclear what entropy this key provides. let windows_hello_key = Sha256::digest(signature_value).into(); Ok(windows_hello_key) } diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/windows_focus.rs b/apps/desktop/desktop_native/core/src/biometric_v2/windows_focus.rs index f3ffb6e4ebe..bf303c88e01 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/windows_focus.rs +++ b/apps/desktop/desktop_native/core/src/biometric_v2/windows_focus.rs @@ -34,23 +34,25 @@ pub fn focus_security_prompt() { /// Sets focus to a window using a few unstable methods fn set_focus(hwnd: HWND) { unsafe { - // Windows REALLY does not like apps stealing focus, even if it is for fixing Windows-Hello bugs. - // The windows hello signing prompt NEEDS to be focused instantly, or it will error, but it does - // not focus itself. + // Windows REALLY does not like apps stealing focus, even if it is for fixing Windows-Hello + // bugs. The windows hello signing prompt NEEDS to be focused instantly, or it will + // error, but it does not focus itself. // This function implements forced focusing of windows using a few hacks. // The conditions to successfully foreground a window are: // All of the following conditions are true: - // The calling process belongs to a desktop application, not a UWP app or a Windows Store app designed for Windows 8 or 8.1. - // The foreground process has not disabled calls to SetForegroundWindow by a previous call to the LockSetForegroundWindow function. - // The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo). - // No menus are active. + // - The calling process belongs to a desktop application, not a UWP app or a Windows + // Store app designed for Windows 8 or 8.1. + // - The foreground process has not disabled calls to SetForegroundWindow by a previous + // call to the LockSetForegroundWindow function. + // - The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in + // SystemParametersInfo). No menus are active. // Additionally, at least one of the following conditions is true: - // The calling process is the foreground process. - // The calling process was started by the foreground process. - // There is currently no foreground window, and thus no foreground process. - // The calling process received the last input event. - // Either the foreground process or the calling process is being debugged. + // - The calling process is the foreground process. + // - The calling process was started by the foreground process. + // - There is currently no foreground window, and thus no foreground process. + // - The calling process received the last input event. + // - Either the foreground process or the calling process is being debugged. // Update the foreground lock timeout temporarily let mut old_timeout = 0; @@ -75,7 +77,8 @@ fn set_focus(hwnd: HWND) { ); }); - // Attach to the foreground thread once attached, we can foreground, even if in the background + // Attach to the foreground thread once attached, we can foreground, even if in the + // background let dw_current_thread = GetCurrentThreadId(); let dw_fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None); @@ -91,7 +94,8 @@ fn set_focus(hwnd: HWND) { } } -/// When restoring focus to the application window, we need a less aggressive method so the electron window doesn't get frozen. +/// When restoring focus to the application window, we need a less aggressive method so the electron +/// window doesn't get frozen. pub(crate) fn restore_focus(hwnd: HWND) { unsafe { let _ = SetForegroundWindow(hwnd); diff --git a/apps/desktop/desktop_native/core/src/crypto/crypto.rs b/apps/desktop/desktop_native/core/src/crypto/crypto.rs index d9e2aec3046..7991c87ca28 100644 --- a/apps/desktop/desktop_native/core/src/crypto/crypto.rs +++ b/apps/desktop/desktop_native/core/src/crypto/crypto.rs @@ -5,9 +5,8 @@ use aes::cipher::{ BlockEncryptMut, KeyIvInit, }; -use crate::error::{CryptoError, Result}; - use super::CipherString; +use crate::error::{CryptoError, Result}; pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray) -> Result> { let iv = GenericArray::from_slice(iv); @@ -16,7 +15,8 @@ pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray) -> .decrypt_padded_mut::(&mut data) .map_err(|_| CryptoError::KeyDecrypt)?; - // Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it, we truncate to the subslice length + // Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it, + // we truncate to the subslice length let decrypted_len = decrypted_key_slice.len(); data.truncate(decrypted_len); diff --git a/apps/desktop/desktop_native/core/src/error.rs b/apps/desktop/desktop_native/core/src/error.rs index d70d8624018..c8d3ec02332 100644 --- a/apps/desktop/desktop_native/core/src/error.rs +++ b/apps/desktop/desktop_native/core/src/error.rs @@ -35,15 +35,4 @@ pub enum KdfParamError { InvalidParams(String), } -// Ensure that the error messages implement Send and Sync -#[cfg(test)] -const _: () = { - fn assert_send() {} - fn assert_sync() {} - fn assert_all() { - assert_send::(); - assert_sync::(); - } -}; - pub type Result = std::result::Result; diff --git a/apps/desktop/desktop_native/core/src/ipc/mod.rs b/apps/desktop/desktop_native/core/src/ipc/mod.rs index 5d4cc9e27f7..f806e395d10 100644 --- a/apps/desktop/desktop_native/core/src/ipc/mod.rs +++ b/apps/desktop/desktop_native/core/src/ipc/mod.rs @@ -49,7 +49,8 @@ pub fn path(name: &str) -> std::path::PathBuf { #[cfg(target_os = "macos")] { // When running in an unsandboxed environment, path is: /Users// - // While running sandboxed, it's different: /Users//Library/Containers/com.bitwarden.desktop/Data + // While running sandboxed, it's different: + // /Users//Library/Containers/com.bitwarden.desktop/Data let mut home = dirs::home_dir().unwrap(); // Check if the app is sandboxed by looking for the Containers directory @@ -59,8 +60,9 @@ pub fn path(name: &str) -> std::path::PathBuf { // If the app is sanboxed, we need to use the App Group directory if let Some(position) = containers_position { - // We want to use App Groups in /Users//Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop, - // so we need to remove all the components after the user. We can use the previous position to do this. + // We want to use App Groups in /Users//Library/Group + // Containers/LTZ2PFU5D6.com.bitwarden.desktop, so we need to remove all the + // components after the user. We can use the previous position to do this. while home.components().count() > position - 1 { home.pop(); } diff --git a/apps/desktop/desktop_native/core/src/ipc/server.rs b/apps/desktop/desktop_native/core/src/ipc/server.rs index 2762a832ac6..a65638303f1 100644 --- a/apps/desktop/desktop_native/core/src/ipc/server.rs +++ b/apps/desktop/desktop_native/core/src/ipc/server.rs @@ -3,9 +3,8 @@ use std::{ path::{Path, PathBuf}, }; -use futures::{SinkExt, StreamExt, TryFutureExt}; - use anyhow::Result; +use futures::{SinkExt, StreamExt, TryFutureExt}; use interprocess::local_socket::{tokio::prelude::*, GenericFilePath, ListenerOptions}; use tokio::{ io::{AsyncRead, AsyncWrite}, @@ -42,14 +41,17 @@ impl Server { /// /// # Parameters /// - /// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. - /// - `client_to_server_send`: This [`mpsc::Sender`] will receive all the [`Message`]'s that the clients send to this server. + /// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection + /// and must be the same for both the server and client. + /// - `client_to_server_send`: This [`mpsc::Sender`] will receive all the [`Message`]'s + /// that the clients send to this server. pub fn start( path: &Path, client_to_server_send: mpsc::Sender, ) -> Result> { - // If the unix socket file already exists, we get an error when trying to bind to it. So we remove it first. - // Any processes that were using the old socket should remain connected to it but any new connections will use the new socket. + // If the unix socket file already exists, we get an error when trying to bind to it. So we + // remove it first. Any processes that were using the old socket should remain + // connected to it but any new connections will use the new socket. if !cfg!(windows) { let _ = std::fs::remove_file(path); } @@ -58,8 +60,9 @@ impl Server { let opts = ListenerOptions::new().name(name); let listener = opts.create_tokio()?; - // This broadcast channel is used for sending messages to all connected clients, and so the sender - // will be stored in the server while the receiver will be cloned and passed to each client handler. + // This broadcast channel is used for sending messages to all connected clients, and so the + // sender will be stored in the server while the receiver will be cloned and passed + // to each client handler. let (server_to_clients_send, server_to_clients_recv) = broadcast::channel::(MESSAGE_CHANNEL_BUFFER); diff --git a/apps/desktop/desktop_native/core/src/password/macos.rs b/apps/desktop/desktop_native/core/src/password/macos.rs index 4f3a16ba4be..72d8ebeb425 100644 --- a/apps/desktop/desktop_native/core/src/password/macos.rs +++ b/apps/desktop/desktop_native/core/src/password/macos.rs @@ -1,9 +1,10 @@ -use crate::password::PASSWORD_NOT_FOUND; use anyhow::Result; use security_framework::passwords::{ delete_generic_password, get_generic_password, set_generic_password, }; +use crate::password::PASSWORD_NOT_FOUND; + #[allow(clippy::unused_async)] pub async fn get_password(service: &str, account: &str) -> Result { let password = get_generic_password(service, account).map_err(convert_error)?; diff --git a/apps/desktop/desktop_native/core/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs index b7595dca287..57b71adefed 100644 --- a/apps/desktop/desktop_native/core/src/password/unix.rs +++ b/apps/desktop/desktop_native/core/src/password/unix.rs @@ -1,9 +1,11 @@ -use crate::password::PASSWORD_NOT_FOUND; +use std::collections::HashMap; + use anyhow::{anyhow, Result}; use oo7::dbus::{self}; -use std::collections::HashMap; use tracing::info; +use crate::password::PASSWORD_NOT_FOUND; + pub async fn get_password(service: &str, account: &str) -> Result { match get_password_new(service, account).await { Ok(res) => Ok(res), diff --git a/apps/desktop/desktop_native/core/src/password/windows.rs b/apps/desktop/desktop_native/core/src/password/windows.rs index ad09019f014..645620b444e 100644 --- a/apps/desktop/desktop_native/core/src/password/windows.rs +++ b/apps/desktop/desktop_native/core/src/password/windows.rs @@ -1,4 +1,3 @@ -use crate::password::PASSWORD_NOT_FOUND; use anyhow::{anyhow, Result}; use widestring::{U16CString, U16String}; use windows::{ @@ -12,6 +11,8 @@ use windows::{ }, }; +use crate::password::PASSWORD_NOT_FOUND; + const CRED_FLAGS_NONE: u32 = 0; #[allow(clippy::unused_async)] diff --git a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs index bad348c93e2..263cc10b716 100644 --- a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs +++ b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs @@ -4,15 +4,15 @@ use libc::c_uint; use libc::{self, c_int}; use tracing::info; -// RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on crashes -// https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20 +// RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on +// crashes https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20 #[cfg(target_env = "musl")] const RLIMIT_CORE: c_int = 4; #[cfg(target_env = "gnu")] const RLIMIT_CORE: c_uint = 4; -// PR_SET_DUMPABLE makes it so no other running process (root or same user) can dump the memory of this process -// or attach a debugger to it. +// PR_SET_DUMPABLE makes it so no other running process (root or same user) can dump the memory of +// this process or attach a debugger to it. // https://github.com/torvalds/linux/blob/a38297e3fb012ddfa7ce0321a7e5a8daeb1872b6/include/uapi/linux/prctl.h#L14 const PR_SET_DUMPABLE: c_int = 4; diff --git a/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs b/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs index 3ff8a6d3d83..8d8e10d92c4 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs @@ -29,8 +29,9 @@ impl SecureMemoryStore for DpapiSecretKVStore { fn put(&mut self, key: String, value: &[u8]) { let length_header_len = std::mem::size_of::(); - // The allocated data has to be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE, so we pad it and write the length in front - // We are storing LENGTH|DATA|00..00, where LENGTH is the length of DATA, the total length is a multiple + // The allocated data has to be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE, so we pad it + // and write the length in front We are storing LENGTH|DATA|00..00, where LENGTH is + // the length of DATA, the total length is a multiple // of CRYPTPROTECTMEMORY_BLOCK_SIZE, and the padding is filled with zeros. let data_len = value.len(); diff --git a/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs b/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs index a8952d8f55a..d116e564bc8 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs @@ -10,8 +10,8 @@ use crate::secure_memory::{ /// allows circumventing length and amount limitations on platform specific secure memory APIs since /// only a single short item needs to be protected. /// -/// The key is briefly in process memory during encryption and decryption, in memory that is protected -/// from swapping to disk via mlock, and then zeroed out immediately after use. +/// The key is briefly in process memory during encryption and decryption, in memory that is +/// protected from swapping to disk via mlock, and then zeroed out immediately after use. #[allow(unused)] pub(crate) struct EncryptedMemoryStore { map: std::collections::HashMap, diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs index 1ee6c4cdf40..7e2917ade6d 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs @@ -6,9 +6,9 @@ use rand::{rng, Rng}; pub(super) const KEY_SIZE: usize = 32; pub(super) const NONCE_SIZE: usize = 24; -/// The encryption performed here is xchacha-poly1305. Any tampering with the key or the ciphertexts will result -/// in a decryption failure and panic. The key's memory contents are protected from being swapped to disk -/// via mlock. +/// The encryption performed here is xchacha-poly1305. Any tampering with the key or the ciphertexts +/// will result in a decryption failure and panic. The key's memory contents are protected from +/// being swapped to disk via mlock. pub(super) struct MemoryEncryptionKey(NonNull<[u8]>); /// An encrypted memory blob that must be decrypted using the same key that it was encrypted with. diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs index 0975b542877..52b75d94a09 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs @@ -1,10 +1,13 @@ -use super::crypto::{MemoryEncryptionKey, KEY_SIZE}; -use super::SecureKeyContainer; use windows::Win32::Security::Cryptography::{ CryptProtectMemory, CryptUnprotectMemory, CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, }; +use super::{ + crypto::{MemoryEncryptionKey, KEY_SIZE}, + SecureKeyContainer, +}; + /// https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata /// The DPAPI store encrypts data using the Windows Data Protection API (DPAPI). The key is bound /// to the current process, and cannot be decrypted by other user-mode processes. diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs index a738d964671..29c62759740 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs @@ -1,9 +1,8 @@ -use crate::secure_memory::secure_key::crypto::MemoryEncryptionKey; - -use super::crypto::KEY_SIZE; -use super::SecureKeyContainer; use linux_keyutils::{KeyRing, KeyRingIdentifier}; +use super::{crypto::KEY_SIZE, SecureKeyContainer}; +use crate::secure_memory::secure_key::crypto::MemoryEncryptionKey; + /// The keys are bound to the process keyring. const KEY_RING_IDENTIFIER: KeyRingIdentifier = KeyRingIdentifier::Process; /// This is an atomic global counter used to help generate unique key IDs @@ -26,9 +25,9 @@ pub(super) struct KeyctlSecureKeyContainer { id: String, } -// SAFETY: The key id is fully owned by this struct and not exposed or cloned, and cleaned up on drop. -// Further, since we use `KeyRingIdentifier::Process` and not `KeyRingIdentifier::Thread`, the key -// is accessible across threads within the same process bound. +// SAFETY: The key id is fully owned by this struct and not exposed or cloned, and cleaned up on +// drop. Further, since we use `KeyRingIdentifier::Process` and not `KeyRingIdentifier::Thread`, the +// key is accessible across threads within the same process bound. unsafe impl Send for KeyctlSecureKeyContainer {} // SAFETY: The container is non-mutable and thus safe to share between threads. unsafe impl Sync for KeyctlSecureKeyContainer {} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs index 4e6a2c4d7ac..e9f96db3148 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs @@ -1,8 +1,9 @@ use std::{ptr::NonNull, sync::LazyLock}; -use super::crypto::MemoryEncryptionKey; -use super::crypto::KEY_SIZE; -use super::SecureKeyContainer; +use super::{ + crypto::{MemoryEncryptionKey, KEY_SIZE}, + SecureKeyContainer, +}; /// https://man.archlinux.org/man/memfd_secret.2.en /// The memfd_secret store protects the data using the `memfd_secret` syscall. The @@ -15,8 +16,8 @@ pub(super) struct MemfdSecretSecureKeyContainer { // SAFETY: The pointers in this struct are allocated by `memfd_secret`, and we have full ownership. // They are never exposed outside or cloned, and are cleaned up by drop. unsafe impl Send for MemfdSecretSecureKeyContainer {} -// SAFETY: The container is non-mutable and thus safe to share between threads. Further, memfd-secret -// is accessible across threads within the same process bound. +// SAFETY: The container is non-mutable and thus safe to share between threads. Further, +// memfd-secret is accessible across threads within the same process bound. unsafe impl Sync for MemfdSecretSecureKeyContainer {} impl SecureKeyContainer for MemfdSecretSecureKeyContainer { diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs index db21cd7fedc..961988c1d40 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs @@ -1,8 +1,9 @@ use std::ptr::NonNull; -use super::crypto::MemoryEncryptionKey; -use super::crypto::KEY_SIZE; -use super::SecureKeyContainer; +use super::{ + crypto::{MemoryEncryptionKey, KEY_SIZE}, + SecureKeyContainer, +}; /// A SecureKeyContainer that uses mlock to prevent the memory from being swapped to disk. /// This does not provide as strong protections as other methods, but is always supported. diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs index 6c3b53117a5..26e72f7d581 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs @@ -1,9 +1,12 @@ -//! This module provides hardened storage for single cryptographic keys. These are meant for encrypting large amounts of memory. -//! Some platforms restrict how many keys can be protected by their APIs, which necessitates this layer of indirection. This significantly -//! reduces the complexity of each platform specific implementation, since all that's needed is implementing protecting a single fixed sized key -//! instead of protecting many arbitrarily sized secrets. This significantly lowers the effort to maintain each implementation. +//! This module provides hardened storage for single cryptographic keys. These are meant for +//! encrypting large amounts of memory. Some platforms restrict how many keys can be protected by +//! their APIs, which necessitates this layer of indirection. This significantly reduces the +//! complexity of each platform specific implementation, since all that's needed is implementing +//! protecting a single fixed sized key instead of protecting many arbitrarily sized secrets. This +//! significantly lowers the effort to maintain each implementation. //! -//! The implementations include DPAPI on Windows, `keyctl` on Linux, and `memfd_secret` on Linux, and a fallback implementation using mlock. +//! The implementations include DPAPI on Windows, `keyctl` on Linux, and `memfd_secret` on Linux, +//! and a fallback implementation using mlock. use tracing::info; @@ -20,12 +23,13 @@ pub use crypto::EncryptedMemory; use crate::secure_memory::secure_key::crypto::DecryptionError; -/// An ephemeral key that is protected using a platform mechanism. It is generated on construction freshly, and can be used -/// to encrypt and decrypt segments of memory. Since the key is ephemeral, persistent data cannot be encrypted with this key. -/// On Linux and Windows, in most cases the protection mechanisms prevent memory dumps/debuggers from reading the key. +/// An ephemeral key that is protected using a platform mechanism. It is generated on construction +/// freshly, and can be used to encrypt and decrypt segments of memory. Since the key is ephemeral, +/// persistent data cannot be encrypted with this key. On Linux and Windows, in most cases the +/// protection mechanisms prevent memory dumps/debuggers from reading the key. /// -/// Note: This can be circumvented if code can be injected into the process and is only effective in combination with the -/// memory isolation provided in `process_isolation`. +/// Note: This can be circumvented if code can be injected into the process and is only effective in +/// combination with the memory isolation provided in `process_isolation`. /// - https://github.com/zer1t0/keydump #[allow(unused)] pub(crate) struct SecureMemoryEncryptionKey(CrossPlatformSecureKeyContainer); @@ -55,7 +59,8 @@ impl SecureMemoryEncryptionKey { /// from memory attacks. #[allow(unused)] trait SecureKeyContainer: Sync + Send { - /// Returns the key as a byte slice. This slice does not have additional memory protections applied. + /// Returns the key as a byte slice. This slice does not have additional memory protections + /// applied. fn as_key(&self) -> crypto::MemoryEncryptionKey; /// Creates a new SecureKeyContainer from the provided key. fn from_key(key: crypto::MemoryEncryptionKey) -> Self; diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs index 61cb8fc187d..8ba64618ffa 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -7,13 +7,12 @@ use std::{ }; use base64::{engine::general_purpose::STANDARD, Engine as _}; -use tokio::sync::Mutex; -use tokio_util::sync::CancellationToken; - use bitwarden_russh::{ session_bind::SessionBindResult, ssh_agent::{self, SshKey}, }; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; use tracing::{error, info}; #[cfg_attr(target_os = "windows", path = "windows.rs")] @@ -34,7 +33,8 @@ pub struct BitwardenDesktopAgent { show_ui_request_tx: tokio::sync::mpsc::Sender, get_ui_response_rx: Arc>>, request_id: Arc, - /// before first unlock, or after account switching, listing keys should require an unlock to get a list of public keys + /// before first unlock, or after account switching, listing keys should require an unlock to + /// get a list of public keys needs_unlock: Arc, is_running: Arc, } diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs index cb10e873a33..38b2193faf5 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs @@ -1,7 +1,6 @@ -use futures::Stream; -use std::os::windows::prelude::AsRawHandle as _; use std::{ io, + os::windows::prelude::AsRawHandle as _, pin::Pin, sync::{ atomic::{AtomicBool, Ordering}, @@ -9,6 +8,8 @@ use std::{ }, task::{Context, Poll}, }; + +use futures::Stream; use tokio::{ net::windows::named_pipe::{NamedPipeServer, ServerOptions}, select, diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs index 77eec5e35c7..5b6b1d8f36b 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs @@ -1,11 +1,13 @@ +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; + use futures::Stream; -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; use tokio::net::{UnixListener, UnixStream}; -use super::peerinfo; -use super::peerinfo::models::PeerInfo; +use super::{peerinfo, peerinfo::models::PeerInfo}; #[derive(Debug)] pub struct PeercredUnixListenerStream { diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs index fad535cb80e..74b909f5ce7 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs @@ -1,9 +1,10 @@ use std::sync::{atomic::AtomicBool, Arc, Mutex}; /** -* Peerinfo represents the information of a peer process connecting over a socket. -* This can be later extended to include more information (icon, app name) for the corresponding application. -*/ + * Peerinfo represents the information of a peer process connecting over a socket. + * This can be later extended to include more information (icon, app name) for the corresponding + * application. + */ #[derive(Debug, Clone)] pub struct PeerInfo { uid: u32, diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs index a45c2f6c0bf..8623df13776 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs @@ -6,9 +6,8 @@ use homedir::my_home; use tokio::{net::UnixListener, sync::Mutex}; use tracing::{error, info}; -use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream; - use super::{BitwardenDesktopAgent, SshAgentUIRequest}; +use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream; /// User can override the default socket path with this env var const ENV_BITWARDEN_SSH_AUTH_SOCK: &str = "BITWARDEN_SSH_AUTH_SOCK"; diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs index 662a4658ede..2012dab2d77 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs @@ -2,6 +2,7 @@ use bitwarden_russh::ssh_agent; pub mod named_pipe_listener_stream; use std::sync::Arc; + use tokio::sync::Mutex; use super::{BitwardenDesktopAgent, SshAgentUIRequest}; diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml index ea44f3d9a27..50f1834851d 100644 --- a/apps/desktop/desktop_native/macos_provider/Cargo.toml +++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml @@ -14,17 +14,16 @@ crate-type = ["staticlib", "cdylib"] bench = false [dependencies] +uniffi = { workspace = true, features = ["cli"] } + +[target.'cfg(target_os = "macos")'.dependencies] desktop_core = { path = "../core" } futures = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["sync"] } -tokio-util = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -uniffi = { workspace = true, features = ["cli"] } - -[target.'cfg(target_os = "macos")'.dependencies] tracing-oslog = "0.3.0" [build-dependencies] diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 4198baa4b5a..b5847a602d5 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -16,17 +16,13 @@ manual_test = [] [dependencies] anyhow = { workspace = true } autotype = { path = "../autotype" } -base64 = { workspace = true } chromium_importer = { path = "../chromium_importer" } desktop_core = { path = "../core" } -hex = { workspace = true } napi = { workspace = true, features = ["async"] } napi-derive = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true } -tokio-stream = { workspace = true } -tokio-util = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 0a8beb8c427..01bfa65d571 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -11,7 +11,10 @@ export declare namespace passwords { * Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. */ export function getPassword(service: string, account: string): Promise - /** Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry. */ + /** + * Save the password to the keychain. Adds an entry if none exists otherwise updates the + * existing entry. + */ export function setPassword(service: string, account: string, password: string): Promise /** * Delete the stored password from the keychain. @@ -35,7 +38,8 @@ export declare namespace biometrics { * base64 encoded key and the base64 encoded challenge used to create it * separated by a `|` character. * - * If the iv is provided, it will be used as the challenge. Otherwise a random challenge will be generated. + * If the iv is provided, it will be used as the challenge. Otherwise a random challenge will + * be generated. * * `format!("|")` */ @@ -119,8 +123,9 @@ export declare namespace ipc { /** * Create and start the IPC server without blocking. * - * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. - * @param callback This function will be called whenever a message is received from a client. + * @param name The endpoint name to listen on. This name uniquely identifies the IPC + * connection and must be the same for both the server and client. @param callback + * This function will be called whenever a message is received from a client. */ static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise /** Return the path to the IPC server. */ @@ -130,8 +135,9 @@ export declare namespace ipc { /** * Send a message over the IPC server to all the connected clients * - * @return The number of clients that the message was sent to. Note that the number of messages - * actually received may be less, as some clients could disconnect before receiving the message. + * @return The number of clients that the message was sent to. Note that the number of + * messages actually received may be less, as some clients could disconnect before + * receiving the message. */ send(message: string): number } @@ -194,8 +200,9 @@ export declare namespace autofill { /** * Create and start the IPC server without blocking. * - * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. - * @param callback This function will be called whenever a message is received from a client. + * @param name The endpoint name to listen on. This name uniquely identifies the IPC + * connection and must be the same for both the server and client. @param callback + * This function will be called whenever a message is received from a client. */ static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void): Promise /** Return the path to the IPC server. */ diff --git a/apps/desktop/desktop_native/napi/index.js b/apps/desktop/desktop_native/napi/index.js index acfd0dffb89..64819be4405 100644 --- a/apps/desktop/desktop_native/napi/index.js +++ b/apps/desktop/desktop_native/napi/index.js @@ -78,12 +78,6 @@ switch (platform) { throw new Error(`Unsupported architecture on macOS: ${arch}`); } break; - case "freebsd": - nativeBinding = loadFirstAvailable( - ["desktop_napi.freebsd-x64.node"], - "@bitwarden/desktop-napi-freebsd-x64", - ); - break; case "linux": switch (arch) { case "x64": diff --git a/apps/desktop/desktop_native/napi/package.json b/apps/desktop/desktop_native/napi/package.json index d557ccfd259..ca17377c9f2 100644 --- a/apps/desktop/desktop_native/napi/package.json +++ b/apps/desktop/desktop_native/napi/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "description": "", "scripts": { - "build": "napi build --platform --js false", + "build": "node scripts/build.js", "test": "cargo test" }, "author": "", diff --git a/apps/desktop/desktop_native/napi/scripts/build.js b/apps/desktop/desktop_native/napi/scripts/build.js new file mode 100644 index 00000000000..a6680f5d311 --- /dev/null +++ b/apps/desktop/desktop_native/napi/scripts/build.js @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { execSync } = require('child_process'); + +const args = process.argv.slice(2); +const isRelease = args.includes('--release'); + +if (isRelease) { + console.log('Building release mode.'); +} else { + console.log('Building debug mode.'); + process.env.RUST_LOG = 'debug'; +} + +execSync(`napi build --platform --js false`, { stdio: 'inherit', env: process.env }); diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 39e57bd0bb5..c34e7574f68 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -19,7 +19,8 @@ pub mod passwords { .map_err(|e| napi::Error::from_reason(e.to_string())) } - /// Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry. + /// Save the password to the keychain. Adds an entry if none exists otherwise updates the + /// existing entry. #[napi] pub async fn set_password( service: String, @@ -107,7 +108,8 @@ pub mod biometrics { /// base64 encoded key and the base64 encoded challenge used to create it /// separated by a `|` character. /// - /// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will be generated. + /// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will + /// be generated. /// /// `format!("|")` #[allow(clippy::unused_async)] // FIXME: Remove unused async! @@ -556,8 +558,9 @@ pub mod ipc { impl IpcServer { /// Create and start the IPC server without blocking. /// - /// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. - /// @param callback This function will be called whenever a message is received from a client. + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC + /// connection and must be the same for both the server and client. @param callback + /// This function will be called whenever a message is received from a client. #[allow(clippy::unused_async)] // FIXME: Remove unused async! #[napi(factory)] pub async fn listen( @@ -598,8 +601,9 @@ pub mod ipc { /// Send a message over the IPC server to all the connected clients /// - /// @return The number of clients that the message was sent to. Note that the number of messages - /// actually received may be less, as some clients could disconnect before receiving the message. + /// @return The number of clients that the message was sent to. Note that the number of + /// messages actually received may be less, as some clients could disconnect before + /// receiving the message. #[napi] pub fn send(&self, message: String) -> napi::Result { self.server @@ -743,8 +747,9 @@ pub mod autofill { impl IpcServer { /// Create and start the IPC server without blocking. /// - /// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. - /// @param callback This function will be called whenever a message is received from a client. + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC + /// connection and must be the same for both the server and client. @param callback + /// This function will be called whenever a message is received from a client. #[allow(clippy::unused_async)] // FIXME: Remove unused async! #[napi(factory)] pub async fn listen( @@ -946,18 +951,18 @@ pub mod logging { //! //! # Example //! - //! [Elec] 14:34:03.517 › [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock} + //! [Elec] 14:34:03.517 › [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting + //! SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock} - use std::fmt::Write; - use std::sync::OnceLock; + use std::{fmt::Write, sync::OnceLock}; use napi::threadsafe_function::{ ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode, }; use tracing::Level; - use tracing_subscriber::fmt::format::{DefaultVisitor, Writer}; use tracing_subscriber::{ - filter::{EnvFilter, LevelFilter}, + filter::EnvFilter, + fmt::format::{DefaultVisitor, Writer}, layer::SubscriberExt, util::SubscriberInitExt, Layer, @@ -1044,9 +1049,17 @@ pub mod logging { pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) { let _ = JS_LOGGER.0.set(js_log_fn); + // the log level hierarchy is determined by: + // - if RUST_LOG is detected at runtime + // - if RUST_LOG is provided at compile time + // - default to INFO let filter = EnvFilter::builder() - // set the default log level to INFO. - .with_default_directive(LevelFilter::INFO.into()) + .with_default_directive( + option_env!("RUST_LOG") + .unwrap_or("info") + .parse() + .expect("should provide valid log level at compile time."), + ) // parse directives from the RUST_LOG environment variable, // overriding the default directive for matching targets. .from_env_lossy(); @@ -1064,6 +1077,8 @@ pub mod logging { #[napi] pub mod chromium_importer { + use std::collections::HashMap; + use chromium_importer::{ chromium::{ DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult, @@ -1071,7 +1086,6 @@ pub mod chromium_importer { }, metadata::NativeImporterMetadata as _NativeImporterMetadata, }; - use std::collections::HashMap; #[napi(object)] pub struct ProfileInfo { diff --git a/apps/desktop/desktop_native/objc/Cargo.toml b/apps/desktop/desktop_native/objc/Cargo.toml index fc8910bddd3..c161b8226ba 100644 --- a/apps/desktop/desktop_native/objc/Cargo.toml +++ b/apps/desktop/desktop_native/objc/Cargo.toml @@ -8,16 +8,12 @@ publish = { workspace = true } [features] default = [] -[dependencies] +[target.'cfg(target_os = "macos")'.dependencies] anyhow = { workspace = true } -thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -[target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "=0.10.1" - -[build-dependencies] +[target.'cfg(target_os = "macos")'.build-dependencies] cc = "=1.2.4" glob = "=0.3.2" diff --git a/apps/desktop/desktop_native/process_isolation/Cargo.toml b/apps/desktop/desktop_native/process_isolation/Cargo.toml index 170832c2fde..d8c6c7a618c 100644 --- a/apps/desktop/desktop_native/process_isolation/Cargo.toml +++ b/apps/desktop/desktop_native/process_isolation/Cargo.toml @@ -8,7 +8,7 @@ publish = { workspace = true } [lib] crate-type = ["cdylib"] -[dependencies] +[target.'cfg(target_os = "linux")'.dependencies] ctor = { workspace = true } desktop_core = { path = "../core" } libc = { workspace = true } diff --git a/apps/desktop/desktop_native/process_isolation/src/lib.rs b/apps/desktop/desktop_native/process_isolation/src/lib.rs index 850ffac841e..55c5d7fafae 100644 --- a/apps/desktop/desktop_native/process_isolation/src/lib.rs +++ b/apps/desktop/desktop_native/process_isolation/src/lib.rs @@ -5,8 +5,9 @@ //! On Linux, this is PR_SET_DUMPABLE to prevent debuggers from attaching, the env //! from being read and the memory from being stolen. -use desktop_core::process_isolation; use std::{ffi::c_char, sync::LazyLock}; + +use desktop_core::process_isolation; use tracing::info; static ORIGINAL_UNSETENV: LazyLock i32> = diff --git a/apps/desktop/desktop_native/proxy/Cargo.toml b/apps/desktop/desktop_native/proxy/Cargo.toml index c672f57543d..25682fe2aa3 100644 --- a/apps/desktop/desktop_native/proxy/Cargo.toml +++ b/apps/desktop/desktop_native/proxy/Cargo.toml @@ -6,7 +6,6 @@ version = { workspace = true } publish = { workspace = true } [dependencies] -anyhow = { workspace = true } desktop_core = { path = "../core" } futures = { workspace = true } tokio = { workspace = true, features = ["io-std", "io-util", "macros", "rt"] } diff --git a/apps/desktop/desktop_native/proxy/src/main.rs b/apps/desktop/desktop_native/proxy/src/main.rs index c2c525b865a..21957d8ba32 100644 --- a/apps/desktop/desktop_native/proxy/src/main.rs +++ b/apps/desktop/desktop_native/proxy/src/main.rs @@ -60,7 +60,6 @@ fn init_logging(log_path: &Path, console_level: LevelFilter, file_level: LevelFi /// a stable communication channel between the proxy and the running desktop application. /// /// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop -/// // FIXME: Remove unwraps! They panic and terminate the whole application. #[allow(clippy::unwrap_used)] #[tokio::main(flavor = "current_thread")] @@ -83,8 +82,10 @@ async fn main() { // Different browsers send different arguments when the app starts: // // Firefox: - // - The complete path to the app manifest. (in the form `/Users//Library/.../Mozilla/NativeMessagingHosts/com.8bit.bitwarden.json`) - // - (in Firefox 55+) the ID (as given in the manifest.json) of the add-on that started it (in the form `{[UUID]}`). + // - The complete path to the app manifest. (in the form + // `/Users//Library/.../Mozilla/NativeMessagingHosts/com.8bit.bitwarden.json`) + // - (in Firefox 55+) the ID (as given in the manifest.json) of the add-on that started it (in + // the form `{[UUID]}`). // // Chrome on Windows: // - Origin of the extension that started it (in the form `chrome-extension://[ID]`). @@ -96,7 +97,8 @@ async fn main() { let args: Vec<_> = std::env::args().skip(1).collect(); info!(?args, "Process args"); - // Setup two channels, one for sending messages to the desktop application (`out`) and one for receiving messages from the desktop application (`in`) + // Setup two channels, one for sending messages to the desktop application (`out`) and one for + // receiving messages from the desktop application (`in`) let (in_send, in_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER); let (out_send, mut out_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER); diff --git a/apps/desktop/desktop_native/rustfmt.toml b/apps/desktop/desktop_native/rustfmt.toml new file mode 100644 index 00000000000..bb3baeccd76 --- /dev/null +++ b/apps/desktop/desktop_native/rustfmt.toml @@ -0,0 +1,7 @@ +# Wrap comments and increase the width of comments to 100 +comment_width = 100 +wrap_comments = true + +# Sort and group imports +group_imports = "StdExternalCrate" +imports_granularity = "Crate" diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs index 2e4f453d8f0..893fdf765fc 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -2,11 +2,12 @@ #![allow(non_snake_case)] #![allow(non_camel_case_types)] -use std::ffi::c_uchar; -use std::ptr; -use windows::Win32::Foundation::*; -use windows::Win32::System::Com::*; -use windows::Win32::System::LibraryLoader::*; +use std::{ffi::c_uchar, ptr}; + +use windows::Win32::{ + Foundation::*, + System::{Com::*, LibraryLoader::*}, +}; use windows_core::*; mod pluginauthenticator; diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index eaa299db508..6e89799e9c4 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -116,7 +116,7 @@ "to": "libprocess_isolation.so" } ], - "target": ["deb", "freebsd", "rpm", "AppImage", "snap"], + "target": ["deb", "rpm", "AppImage", "snap"], "desktop": { "entry": { "Name": "Bitwarden", @@ -252,9 +252,6 @@ "artifactName": "${productName}-${version}-${arch}.${ext}", "fpm": ["--rpm-rpmbuild-define", "_build_id_links none"] }, - "freebsd": { - "artifactName": "${productName}-${version}-${arch}.${ext}" - }, "snap": { "summary": "Bitwarden is a secure and free password manager for all of your devices.", "description": "Password Manager\n**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation. See https://btwrdn.com/install-snap for details.", diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index a4286aabed9..9ad1ffb3ec0 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -19,7 +19,7 @@ "yargs": "18.0.0" }, "devDependencies": { - "@types/node": "22.19.0", + "@types/node": "22.19.1", "typescript": "5.4.2" } }, @@ -117,9 +117,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz", - "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "license": "MIT", "peer": true, "dependencies": { diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 55699af47dd..21a6ba3626a 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -24,7 +24,7 @@ "yargs": "18.0.0" }, "devDependencies": { - "@types/node": "22.19.0", + "@types/node": "22.19.1", "typescript": "5.4.2" }, "_moduleAliases": { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 519aae2c6b8..2633f3d5909 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2025.11.0", + "version": "2025.11.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts b/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts index 5d3c777f333..d65df60a8ce 100644 --- a/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts +++ b/apps/desktop/src/app/components/browser-sync-verification-dialog.component.ts @@ -1,7 +1,13 @@ import { Component, Inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { DIALOG_DATA, ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +import { + DIALOG_DATA, + ButtonModule, + DialogModule, + DialogService, + CenterPositionStrategy, +} from "@bitwarden/components"; export type BrowserSyncVerificationDialogParams = { fingerprint: string[]; @@ -19,6 +25,7 @@ export class BrowserSyncVerificationDialogComponent { static open(dialogService: DialogService, data: BrowserSyncVerificationDialogParams) { return dialogService.open(BrowserSyncVerificationDialogComponent, { data, + positionStrategy: new CenterPositionStrategy(), }); } } diff --git a/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts b/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts index 14c2b137d73..6f9695f856a 100644 --- a/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts +++ b/apps/desktop/src/app/components/verify-native-messaging-dialog.component.ts @@ -1,7 +1,13 @@ import { Component, Inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { DIALOG_DATA, ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +import { + DIALOG_DATA, + ButtonModule, + DialogModule, + DialogService, + CenterPositionStrategy, +} from "@bitwarden/components"; export type VerifyNativeMessagingDialogData = { applicationName: string; @@ -19,6 +25,7 @@ export class VerifyNativeMessagingDialogComponent { static open(dialogService: DialogService, data: VerifyNativeMessagingDialogData) { return dialogService.open(VerifyNativeMessagingDialogComponent, { data, + positionStrategy: new CenterPositionStrategy(), }); } } diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 6da1c7e9c8b..1c6a2bc49c9 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Ongelukkig word blaaierintegrasie tans slegs in die weergawe vir die Mac-toepwinkel ondersteun." - }, "browserIntegrationWindowsStoreDesc": { "message": "Ongelukkig word blaaierintegrasie tans nie in die weergawe vir die Windows-winkel ondersteun nie." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index a6a7e881db9..ca404f4e179 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "حدث خطأ أثناء تمكين دمج المتصفح." }, - "browserIntegrationMasOnlyDesc": { - "message": "للأسف، لا يتم دعم تكامل المتصفح إلا في إصدار متجر تطبيقات ماك في الوقت الحالي." - }, "browserIntegrationWindowsStoreDesc": { "message": "للأسف، لا يتم دعم تكامل المتصفح في إصدار متجر ويندوز في الوقت الحالي." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 81b24cadfe8..55c2bdcd677 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -511,7 +511,7 @@ "description": "This describes a value that is 'linked' (related) to another value." }, "remove": { - "message": "Çıxart" + "message": "Xaric et" }, "nameRequired": { "message": "Ad lazımdır." @@ -1659,7 +1659,7 @@ } }, "passwordSafe": { - "message": "Bu parol, veri pozuntularında qeydə alınmayıb. Rahatlıqla istifadə edə bilərsiniz." + "message": "Bu parol, veri pozuntularında qeydə alınmayıb. Əmniyyətlə istifadə edə bilərsiniz." }, "baseDomain": { "message": "Baza domeni", @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Brauzer inteqrasiyasını fəallaşdırarkən bir xəta baş verdi." }, - "browserIntegrationMasOnlyDesc": { - "message": "Təəssüf ki, brauzer inteqrasiyası indilik yalnız Mac App Store versiyasında dəstəklənir." - }, "browserIntegrationWindowsStoreDesc": { "message": "Təəssüf ki, brauzer inteqrasiyası hal-hazırda Windows Store versiyasında dəstəklənmir." }, @@ -3906,7 +3903,7 @@ "message": "Ana qovluğun adından sonra \"/\" əlavə edərək qovluğu ardıcıl yerləşdirin. Nümunə: Social/Forums" }, "sendsTitleNoItems": { - "message": "Send, həssas məlumatlar təhlükəsizdir", + "message": "Send ilə həssas məlumatlar əmniyyətdədir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { @@ -3961,7 +3958,7 @@ "message": "Kimliklərinizlə, uzun qeydiyyat və ya əlaqə xanalarını daha tez avtomatik doldurun." }, "newNoteNudgeTitle": { - "message": "Həssas verilərinizi güvənli şəkildə saxlayın" + "message": "Həssas verilərinizi əmniyyətdə saxlayın" }, "newNoteNudgeBody": { "message": "Notlarla, bankçılıq və ya sığorta təfsilatları kimi həssas veriləri təhlükəsiz saxlayın." @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "\"Premium\"a yüksəlt" + }, + "sessionTimeoutSettingsAction": { + "message": "Vaxt bitmə əməliyyatı" + }, + "sessionTimeoutHeader": { + "message": "Sessiya vaxt bitməsi" } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index cead61915ca..b2e4db47b32 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "На жаль, інтэграцыя з браўзерам зараз падтрымліваецца толькі ў версіі для Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "На жаль, інтэграцыя з браўзерам у цяперашні час не падтрымліваецца ў версіі для Microsoft Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index c2c2e236d37..ad03c2cc023 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Възникна грешка при включването на интеграцията с браузъра." }, - "browserIntegrationMasOnlyDesc": { - "message": "За жалост в момента интеграцията с браузър не се поддържа във версията за магазина на Mac." - }, "browserIntegrationWindowsStoreDesc": { "message": "За жалост в момента интеграцията с браузър не се поддържа във версията за магазина на Windows." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Надградете до Платения план" + }, + "sessionTimeoutSettingsAction": { + "message": "Действие при изтичането на времето за достъп" + }, + "sessionTimeoutHeader": { + "message": "Изтичане на времето за сесията" } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 5c932d4ed21..d6c61c1ab51 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 08793959da6..569f1072c4b 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Nažalost, za sada je integracija sa preglednikom podržana samo u Mac App Store verziji aplikacije." - }, "browserIntegrationWindowsStoreDesc": { "message": "Nažalost, integracija sa preglednikom nije podržana u Windows Store verziji aplikacije." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 8ee8f7030bc..de468f1e8b3 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -42,7 +42,7 @@ "message": "Cerca en la caixa forta" }, "resetSearch": { - "message": "Reset search" + "message": "Restableix la cerca" }, "addItem": { "message": "Afegeix element" @@ -70,7 +70,7 @@ } }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "No teniu permisos per editar aquest element" }, "welcomeBack": { "message": "Benvingut/da de nou" @@ -706,10 +706,10 @@ "message": "S'ha guardat el fitxer adjunt." }, "addAttachment": { - "message": "Add attachment" + "message": "Afig adjunt" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "La mida màxima del fitxer és de 500 MB" }, "file": { "message": "Fitxer" @@ -757,7 +757,7 @@ "message": "Inicia sessió a Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Introduïu el codi que us hem enviat al correu electrònic" }, "enterTheCodeFromYourAuthenticatorApp": { "message": "Introduïu el codi de la vostra aplicació d'autenticació" @@ -954,14 +954,14 @@ } }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "No ho torneu a preguntar en aquest dispositiu durant 30 dies" }, "selectAnotherMethod": { "message": "Select another method", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "Utilitzeu el codi de recuperació" }, "insertU2f": { "message": "Introduïu la vostra clau de seguretat al port USB de l'ordinador. Si té un botó, premeu-lo." @@ -1467,7 +1467,7 @@ "description": "Copy credit card security code (CVV)" }, "cardNumber": { - "message": "card number" + "message": "núm. targeta de crèdit" }, "premiumMembership": { "message": "Subscripció Premium" @@ -1862,10 +1862,10 @@ "message": "Bloqueja amb la contrasenya mestra en reiniciar" }, "requireMasterPasswordOrPinOnAppRestart": { - "message": "Require master password or PIN on app restart" + "message": "Sol·licita la contrasenya mestra o el PIN en reiniciar l'aplicació" }, "requireMasterPasswordOnAppRestart": { - "message": "Require master password on app restart" + "message": "Sol·licita la contrasenya mestra en reiniciar l'aplicació" }, "deleteAccount": { "message": "Suprimeix el compte" @@ -2023,7 +2023,7 @@ "message": "Make 2-step verification seamless" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden pot emmagatzemar i omplir codis de verificació en dos passos. Copieu i enganxeu la clau en aquest camp." }, "totpHelperWithCapture": { "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." @@ -2048,7 +2048,7 @@ } }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Targeta de crèdit caducada" }, "cardExpiredMessage": { "message": "If you've renewed it, update the card's information" @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "S'ha produït un error en activar la integració del navegador." }, - "browserIntegrationMasOnlyDesc": { - "message": "Malauradament, la integració del navegador només és compatible amb la versió de Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "Malauradament, la integració del navegador només és compatible amb la versió de Microsoft Store." }, @@ -3092,18 +3089,18 @@ "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." }, "webApp": { - "message": "Web app" + "message": "Aplicació web" }, "mobile": { - "message": "Mobile", + "message": "Mòbil", "description": "Mobile app" }, "extension": { - "message": "Extension", + "message": "Extensió", "description": "Browser extension/addon" }, "desktop": { - "message": "Desktop", + "message": "Escriptori", "description": "Desktop app" }, "cli": { @@ -3114,10 +3111,10 @@ "description": "Software Development Kit" }, "server": { - "message": "Server" + "message": "Servidor" }, "loginRequest": { - "message": "Login request" + "message": "Petició d'inici de sessió" }, "deviceType": { "message": "Tipus de dispositiu" @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 578d0607cc2..c02dbabbc93 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Vyskytla se chyba při povolování integrace prohlížeče." }, - "browserIntegrationMasOnlyDesc": { - "message": "Integrace prohlížeče je podporována jen ve verzi pro Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "Integrace prohlížeče není ve verzi pro Windows Store podporována." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Aktualizovat na Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Akce vypršení časového limitu" + }, + "sessionTimeoutHeader": { + "message": "Časový limit relace" } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 278196f9d04..25b52fcc101 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index fad9b9c1af4..1d135a533f2 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "En fejl opstod under aktivering af webbrowserintegration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Desværre understøttes browserintegration indtil videre kun i Mac App Store-versionen." - }, "browserIntegrationWindowsStoreDesc": { "message": "Desværre understøttes browserintegration pt. ikke i Microsoft Store-versionen." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 3f5caa00c4c..2f8daec5b68 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -772,7 +772,7 @@ "message": "Anmelden mit einem anderen Gerät" }, "useSingleSignOn": { - "message": "Single Sign-on verwenden" + "message": "Single Sign-On verwenden" }, "yourOrganizationRequiresSingleSignOn": { "message": "Deine Organisation erfordert Single Sign-On." @@ -1979,7 +1979,7 @@ "message": "Timeout-Aktion bestätigen" }, "enterpriseSingleSignOn": { - "message": "Enterprise Single-Sign-On" + "message": "Enterprise Single Sign-On" }, "setMasterPassword": { "message": "Master-Passwort festlegen" @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Beim Aktivieren der Browser-Integration ist ein Fehler aufgetreten." }, - "browserIntegrationMasOnlyDesc": { - "message": "Leider wird die Browser-Integration derzeit nur in der Mac App Store Version unterstützt." - }, "browserIntegrationWindowsStoreDesc": { "message": "Leider wird die Browser-Integration derzeit nicht in der Microsoft Store Version unterstützt." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade auf Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout-Aktion" + }, + "sessionTimeoutHeader": { + "message": "Sitzungs-Timeout" } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 55a3c4fe170..0b869c1e02f 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Παρουσιάστηκε σφάλμα κατά την ενεργοποίηση ενσωμάτωσης του περιηγητή." }, - "browserIntegrationMasOnlyDesc": { - "message": "Δυστυχώς η ενσωμάτωση του προγράμματος περιήγησης υποστηρίζεται μόνο στην έκδοση Mac App Store για τώρα." - }, "browserIntegrationWindowsStoreDesc": { "message": "Δυστυχώς η ενσωμάτωση του περιηγητή, δεν υποστηρίζεται προς το παρόν στην έκδοση Windows Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 63e0cf96742..16af69361c6 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 832025c8c0e..c6f1253bb59 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Windows Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index a3a8643a8f6..28a9f3b8bce 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 01163c7ad29..9966fa1064c 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Se ha producido un error mientras se habilitaba la integración del navegador." }, - "browserIntegrationMasOnlyDesc": { - "message": "Por desgracia la integración del navegador sólo está soportada por ahora en la versión de la Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "Lamentablemente, la integración del navegador no está actualmente soportada en la versión de Microsoft Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 25489871b3b..d85c52bb763 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Midagi läks valesti brauseriga ühendamisel." }, - "browserIntegrationMasOnlyDesc": { - "message": "Paraku on brauseri integratsioon hetkel toetatud ainult Mac App Store'i versioonis." - }, "browserIntegrationWindowsStoreDesc": { "message": "Paraku ei ole brauseri integratsioon hetkel Microsoft Store versioonis toetatud." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index c1007d7d71c..36401df0078 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Zoritxarrez, Mac App Storeren bertsioan soilik onartzen da oraingoz nabigatzailearen integrazioa." - }, "browserIntegrationWindowsStoreDesc": { "message": "Zoritxarrez, nabigatzailearen integrazioa ez da onartzen Windows Storen bertsioan." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index eb62c711628..caa241eb036 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "خطایی هنگام فعال‌سازی یکپارچه سازی مرورگر رخ داده است." }, - "browserIntegrationMasOnlyDesc": { - "message": "متأسفانه در حال حاضر ادغام مرورگر فقط در نسخه Mac App Store پشتیبانی می‌شود." - }, "browserIntegrationWindowsStoreDesc": { "message": "متأسفانه در حال حاضر ادغام مرورگر در نسخه فروشگاه ویندوز پشتیبانی نمی‌شود." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 06f0338e1f3..e2952659d03 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Otettaessa selainintegraatiota käyttöön tapahtui virhe." }, - "browserIntegrationMasOnlyDesc": { - "message": "Valitettavasti selainintegraatiota tuetaan toistaiseksi vain Mac App Store -versiossa." - }, "browserIntegrationWindowsStoreDesc": { "message": "Valitettavasti selainintegraatiota ei toistaiseksi tueta Microsoft Store -versiossa." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 6a32df33ecb..6eaa5577807 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Sa kasamaang palad ang pagsasama ng browser ay suportado lamang sa bersyon ng Mac App Store para sa ngayon." - }, "browserIntegrationWindowsStoreDesc": { "message": "Sa kasamaang palad ang pagsasama ng browser ay kasalukuyang hindi suportado sa bersyon ng Microsoft Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 0cd10d0582c..acd5037bb6d 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -283,7 +283,7 @@ "message": "Bitwarden n'a pas pu déchiffrer le(s) élément(s) du coffre listé(s) ci-dessous." }, "contactCSToAvoidDataLossPart1": { - "message": "Contacter le service clientèle", + "message": "Contacter succès client", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Une erreur s'est produite lors de l'action de l'intégration du navigateur." }, - "browserIntegrationMasOnlyDesc": { - "message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment." - }, "browserIntegrationWindowsStoreDesc": { "message": "Malheureusement l'intégration avec le navigateur n'est pas supportée dans la version Windows Store pour le moment." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 70d4c7cb494..d607bb8d097 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index abe445f83d5..868cd9ccbc5 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "אירעה שגיאה בעת אפשור שילוב דפדפן." }, - "browserIntegrationMasOnlyDesc": { - "message": "למרבה הצער שילוב דפדפן נתמך רק בגרסת Mac App Store לעת עתה." - }, "browserIntegrationWindowsStoreDesc": { "message": "למרבה הצער שילוב דפדפן אינו נתמך כרגע בגרסת ה־Microsoft Store." }, @@ -4198,27 +4195,33 @@ "message": "מספר כרטיס" }, "upgradeNow": { - "message": "Upgrade now" + "message": "שדרג עכשיו" }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "מאמת מובנה" }, "secureFileStorage": { - "message": "Secure file storage" + "message": "אחסון קבצים מאובטח" }, "emergencyAccess": { - "message": "Emergency access" + "message": "גישת חירום" }, "breachMonitoring": { - "message": "Breach monitoring" + "message": "ניטור פרצות" }, "andMoreFeatures": { - "message": "And more!" + "message": "ועוד!" }, "planDescPremium": { - "message": "Complete online security" + "message": "השלם אבטחה מקוונת" }, "upgradeToPremium": { - "message": "Upgrade to Premium" + "message": "שדרג לפרימיום" + }, + "sessionTimeoutSettingsAction": { + "message": "פעולת פסק זמן" + }, + "sessionTimeoutHeader": { + "message": "פסק זמן להפעלה" } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 84676c4d941..2ab323eedc9 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index d1c2ba68779..0f7a8185118 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Pogreška prillikom integracije s preglednikom." }, - "browserIntegrationMasOnlyDesc": { - "message": "Nažalost, za sada je integracija s preglednikom podržana samo u Mac App Store verziji aplikacije." - }, "browserIntegrationWindowsStoreDesc": { "message": "Nažalost, integracija s preglednikom trenutno nije podržana u Windows Store verziji aplikacije." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": " Nadogradi na Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Radnja nakon isteka" + }, + "sessionTimeoutHeader": { + "message": "Istek sesije" } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 9d296a7d2cc..9a6dd787f8c 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Hiba történt a böngésző integrációjának engedélyezése közben." }, - "browserIntegrationMasOnlyDesc": { - "message": "Sajnos a böngésző integrációt egyelőre csak a Mac App Store verzió támogatja." - }, "browserIntegrationWindowsStoreDesc": { "message": "A böngésző integrációt egyelőre csak a Windows Store verzió támogatja." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Áttérés Prémium csomagra" + }, + "sessionTimeoutSettingsAction": { + "message": "Időkifutási művelet" + }, + "sessionTimeoutHeader": { + "message": "Munkamenet időkifutás" } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 03da4bbd030..188ee153da1 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Sayangnya integrasi browser hanya didukung di versi Mac App Store untuk saat ini." - }, "browserIntegrationWindowsStoreDesc": { "message": "Sayangnya integrasi browser saat ini tidak didukung di versi Windows Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 4881d96b44a..1656a301b42 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Si è verificato un errore durante l'attivazione dell'integrazione del browser." }, - "browserIntegrationMasOnlyDesc": { - "message": "Purtroppo l'integrazione del browser è supportata solo nella versione nell'App Store per ora." - }, "browserIntegrationWindowsStoreDesc": { "message": "Purtroppo l'integrazione del browser non è supportata nella versione del Microsoft Store per ora." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 1dfb5a42ead..ca50828b12c 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "ブラウザー統合の有効化中にエラーが発生しました。" }, - "browserIntegrationMasOnlyDesc": { - "message": "残念ながら、ブラウザ統合は、Mac App Storeのバージョンでのみサポートされています。" - }, "browserIntegrationWindowsStoreDesc": { "message": "残念ながらお使いの Microsoft Store のバージョンではブラウザの統合に対応していません。" }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index edaa68e7302..9337286d3fd 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 70d4c7cb494..d607bb8d097 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index b880c845f4f..d1375efee8c 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "ದುರದೃಷ್ಟವಶಾತ್ ಬ್ರೌಸರ್ ಏಕೀಕರಣವನ್ನು ಇದೀಗ ಮ್ಯಾಕ್ ಆಪ್ ಸ್ಟೋರ್ ಆವೃತ್ತಿಯಲ್ಲಿ ಮಾತ್ರ ಬೆಂಬಲಿಸಲಾಗುತ್ತದೆ." - }, "browserIntegrationWindowsStoreDesc": { "message": "ದುರದೃಷ್ಟವಶಾತ್ ವಿಂಡೋಸ್ ಸ್ಟೋರ್ ಆವೃತ್ತಿಯಲ್ಲಿ ಬ್ರೌಸರ್ ಏಕೀಕರಣವನ್ನು ಪ್ರಸ್ತುತ ಬೆಂಬಲಿಸುವುದಿಲ್ಲ." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 7ef1645febf..2e40b8d7f23 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "브라우저와 연결은 현재 Mac App Store 버전에서만 지원됩니다." - }, "browserIntegrationWindowsStoreDesc": { "message": "현재 Microsoft Store 버전에서는 브라우저와 연결이 지원되지 않습니다." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 471bde7b410..16f328d6240 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Deja, bet naršyklės integravimas kol kas palaikomas tik Mac App Store versijoje." - }, "browserIntegrationWindowsStoreDesc": { "message": "Deja, bet naršyklės integravimas nepalaikomas Microsoft Store versijoje." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index cc6cad0fd40..7800a4e9024 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Atgadījās kļūda pārlūka saistīšanas iespējošanas laikā." }, - "browserIntegrationMasOnlyDesc": { - "message": "Diemžēl sasaistīšāna ar pārlūku pagaidām ir nodrošināta tikai Mac App Store laidienā." - }, "browserIntegrationWindowsStoreDesc": { "message": "Diemžēl sasaistīšana ar pārlūku pagaidām nav nodrošināta Windows veikala laidienā." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Uzlabot uz Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Noildzes darbība" + }, + "sessionTimeoutHeader": { + "message": "Sesijas noildze" } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index a67fa99079b..29e3cefee0c 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 4eee4cb0c0d..662ce9a1fc6 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 70d4c7cb494..d607bb8d097 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 047f272b564..bcbd26cede3 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 9bede02bfcf..42fb6d479c0 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Nettleserintegrasjon støttes dessverre bare i Mac App Store-versjonen for øyeblikket." - }, "browserIntegrationWindowsStoreDesc": { "message": "Nettleserintegrasjon er for øyeblikket dessverre ikke støttet i Windows Store-versjonen." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 093896ca17a..cce8f6a2ba5 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index de0820ac5ed..82b51b018c5 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Er is iets misgegaan bij het tijdens het inschakelen van de browserintegratie." }, - "browserIntegrationMasOnlyDesc": { - "message": "Helaas wordt browserintegratie momenteel alleen ondersteund in de Mac App Store-versie." - }, "browserIntegrationWindowsStoreDesc": { "message": "Helaas wordt browserintegratie momenteel niet ondersteund in de Windows Store-versie." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Opwaarderen naar Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Time-out actie" + }, + "sessionTimeoutHeader": { + "message": "Sessietime-out" } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index c582356f115..08567979e8b 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 89489e1db87..4ca05acaac5 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index b9eb4b6232b..c05e7f05cb1 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Wystąpił błąd podczas włączania połączenia z przeglądarką." }, - "browserIntegrationMasOnlyDesc": { - "message": "Połączenie z przeglądarką jest obsługiwane tylko z wersją aplikacji ze sklepu Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "Połączenie z przeglądarką nie jest obecnie obsługiwane w aplikacji w wersji Windows Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 9749d280516..7871ac72533 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -70,7 +70,7 @@ } }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "Você não tem permissão para editar este item" }, "welcomeBack": { "message": "Boas-vindas de volta" @@ -283,7 +283,7 @@ "message": "O Bitwarden não pôde descriptografar o(s) item(ns) listados abaixo do cofre." }, "contactCSToAvoidDataLossPart1": { - "message": "Contate o sucesso do consumidor", + "message": "Contate o costumer success", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { @@ -473,7 +473,7 @@ "message": "Use caixas de seleção se gostaria de preencher automaticamente a caixa de seleção de um formulário, como um lembrar e-mail" }, "linkedHelpText": { - "message": "Use um campo vinculado quando você estiver experienciando problemas de preenchimento automático em um site específico." + "message": "Use um campo vinculado quando você estiver experienciando problemas com o preenchimento automático em um site específico." }, "linkedLabelHelpText": { "message": "Digite o ID html, nome, aria-label, ou placeholder do campo." @@ -1635,7 +1635,7 @@ "message": "Copiado com sucesso" }, "errorRefreshingAccessToken": { - "message": "Erro ao acessar token de recarregamento" + "message": "Erro de atualização do token de acesso" }, "errorRefreshingAccessTokenDesc": { "message": "Nenhum token de recarregamento ou chave de API foi encontrado. Tente desconectar-se e conectar-se novamente." @@ -1749,10 +1749,10 @@ "message": "Use a chave de criptografia da sua conta, derivada do nome de usuário e senha principal da sua conta, para criptografar a exportação e restringir a importação para apenas a conta atual do Bitwarden." }, "passwordProtected": { - "message": "Protegido por senha" + "message": "Protegida por senha" }, "passwordProtectedOptionDescription": { - "message": "Defina uma senha de arquivo para criptografar a exportação e importá-la para qualquer conta do Bitwarden usando a senha para descriptografia." + "message": "Configure uma senha de arquivo para criptografar a exportação e importá-la para qualquer conta do Bitwarden usando a senha para descriptografá-la." }, "exportTypeHeading": { "message": "Tipo da exportação" @@ -1761,10 +1761,10 @@ "message": "Restrita à conta" }, "restrictCardTypeImport": { - "message": "Não é possível importar tipos de item de cartão" + "message": "Não é possível importar itens do tipo de cartão" }, "restrictCardTypeImportDesc": { - "message": "Uma política definida por 1 ou mais organizações impedem que você importe cartões em seus cofres." + "message": "Uma política configurada por uma ou mais organizações impedem que você importe cartões em seus cofres." }, "filePasswordAndConfirmFilePasswordDoNotMatch": { "message": "\"Senha do arquivo\" e \"Confirmar senha do arquivo\" não correspondem." @@ -1780,7 +1780,7 @@ "message": "Confirmar exportação do cofre" }, "exportWarningDesc": { - "message": "Esta exportação contém os dados do seu cofre em um formato não criptografado. Você não deve armazenar ou enviar o arquivo exportado por canais inseguros (como e-mail). Exclua o arquivo imediatamente após terminar de usá-lo." + "message": "Esta exportação contém os dados do seu cofre em um formato não criptografado. Você não deve armazenar ou enviar o arquivo exportado por canais inseguros (como e-mail). Apague o arquivo imediatamente após terminar de usá-lo." }, "encExportKeyWarningDesc": { "message": "Esta exportação criptografa seus dados usando a chave de criptografia da sua conta. Se você rotacionar a chave de criptografia da sua conta, você deve exportar novamente, já que você não será capaz de descriptografar este arquivo de exportação." @@ -1789,7 +1789,7 @@ "message": "As chaves de criptografia de conta são únicas para cada conta de usuário do Bitwarden, então você não pode importar uma exportação criptografada para uma conta diferente." }, "noOrganizationsList": { - "message": "Você não pertence a nenhuma organização. Organizações permitem-lhe compartilhar itens em segurança com outros usuários." + "message": "Você não faz parte de uma organização. Organizações permitem-lhe compartilhar itens com segurança com outros usuários." }, "noCollectionsInList": { "message": "Não há coleções para listar." @@ -1798,7 +1798,7 @@ "message": "Propriedade" }, "whoOwnsThisItem": { - "message": "Quem possui este item?" + "message": "Quem é o proprietário deste item?" }, "strong": { "message": "Forte", @@ -1816,17 +1816,17 @@ "message": "Senha principal fraca" }, "weakMasterPasswordDesc": { - "message": "A senha principal que você selecionou está fraca. Você deve usar uma senha principal forte (ou uma frase-passe) para proteger a sua conta Bitwarden adequadamente. Tem certeza que deseja usar esta senha principal?" + "message": "A senha principal que você escolheu é fraca. Você deve usar uma senha principal forte (ou uma frase secreta) para proteger a sua conta Bitwarden adequadamente. Tem certeza que deseja usar esta senha principal?" }, "pin": { "message": "PIN", "description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device." }, "unlockWithPin": { - "message": "Desbloquear com o PIN" + "message": "Desbloquear com PIN" }, "setYourPinCode": { - "message": "Defina o seu código PIN para desbloquear o Bitwarden. Suas configurações de PIN serão redefinidas se alguma vez você encerrar completamente toda a sessão do aplicativo." + "message": "Configure o seu código PIN para desbloquear o Bitwarden. Suas configurações de PIN serão redefinidas se você desconectar-se totalmente do aplicativo." }, "pinRequired": { "message": "O código PIN é necessário." @@ -1841,7 +1841,7 @@ "message": "Desbloquear com o Windows Hello" }, "unlockWithPolkit": { - "message": "Desbloquear com autenticação de sistema" + "message": "Desbloquear com a autenticação do sistema" }, "windowsHelloConsentMessage": { "message": "Verifique para o Bitwarden." @@ -1850,16 +1850,16 @@ "message": "Desbloquear com o Touch ID" }, "additionalTouchIdSettings": { - "message": "Configurações adicionais de Touch ID" + "message": "Configurações adicionais do Touch ID" }, "touchIdConsentMessage": { "message": "desbloquear o seu cofre" }, "autoPromptTouchId": { - "message": "Pedir pelo Touch ID ao iniciar" + "message": "Pedir o Touch ID ao abrir" }, "lockWithMasterPassOnRestart1": { - "message": "Bloquear com senha principal ao reiniciar" + "message": "Bloquear com a senha principal ao reiniciar" }, "requireMasterPasswordOrPinOnAppRestart": { "message": "Exigir senha principal ou PIN ao reiniciar o app" @@ -1886,7 +1886,7 @@ "message": "Conta apagada" }, "accountDeletedDesc": { - "message": "A sua conta foi fechada e todos os dados associados foram excluídos." + "message": "A sua conta foi fechada e todos os dados associados foram apagados." }, "preferences": { "message": "Preferências" @@ -1898,7 +1898,7 @@ "message": "Sempre mostrar um ícone na barra de menu." }, "hideToMenuBar": { - "message": "Ocultar para a barra de menu" + "message": "Esconder na barra de menu" }, "selectOneCollection": { "message": "Você deve selecionar pelo menos uma coleção." @@ -1910,10 +1910,10 @@ "message": "Restaurar" }, "premiumManageAlertAppStore": { - "message": "Você pode gerenciar sua assinatura da App Store. Você quer visitar a App Store agora?" + "message": "Você pode gerenciar sua assinatura pela App Store. Você quer visitar a App Store agora?" }, "legal": { - "message": "Aspectos Legais", + "message": "Jurídico", "description": "Noun. As in 'legal documents', like our terms of service and privacy policy." }, "termsOfService": { @@ -1923,7 +1923,7 @@ "message": "Política de Privacidade" }, "unsavedChangesConfirmation": { - "message": "Você tem certeza que deseja sair? Se sair agora, as suas informações atuais não serão salvas." + "message": "Tem certeza que quer sair? Se sair agora, as suas informações atuais não serão salvas." }, "unsavedChangesTitle": { "message": "Alterações não salvas" @@ -1932,7 +1932,7 @@ "message": "Clonar" }, "passwordGeneratorPolicyInEffect": { - "message": "Uma ou mais políticas da organização estão afetando as suas configurações do gerador." + "message": "Uma ou mais políticas da organização estão afetando as configurações do seu gerador." }, "vaultTimeoutAction": { "message": "Ação do tempo limite do cofre" @@ -1961,7 +1961,7 @@ "message": "Apagar item para sempre" }, "permanentlyDeleteItemConfirmation": { - "message": "Você tem certeza que deseja excluir permanentemente esse item?" + "message": "Tem certeza que quer apagar este item para sempre?" }, "permanentlyDeletedItem": { "message": "Item apagado para sempre" @@ -1985,11 +1985,11 @@ "message": "Configurar senha principal" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "As permissões da sua organização foram atualizadas, exigindo que você defina uma senha principal.", + "message": "As permissões da sua organização foram atualizadas, exigindo que você configure uma senha principal.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Sua organização requer que você defina uma senha principal.", + "message": "Sua organização requer que você configure uma senha principal.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { @@ -2023,10 +2023,10 @@ "message": "Torne a verificação em 2 etapas mais simples" }, "totpHelper": { - "message": "Bitwarden pode armazenar e preencher códigos de verificação de 2 etapas. Copie e cole a chave nesse campo." + "message": "O Bitwarden pode armazenar e preencher códigos de verificação de 2 etapas. Copie e cole a chave nesse campo." }, "totpHelperWithCapture": { - "message": "Bitwarden pode armazenar e preencher códigos de verificação de 2 etapas. Selecione o ícone da câmera e tire uma captura de tela do código QR de autenticação desse site ou copie e cole a chave nesse campo." + "message": "O Bitwarden pode armazenar e preencher códigos de verificação de 2 etapas. Selecione o ícone da câmera e tire uma captura de tela do código QR do autenticador nesse site ou copie e cole a chave nesse campo." }, "premium": { "message": "Premium", @@ -2048,10 +2048,10 @@ } }, "cardExpiredTitle": { - "message": "Cartão expirado" + "message": "Cartão vencido" }, "cardExpiredMessage": { - "message": "Se você fez uma renovação recente, atualize as informações do cartão" + "message": "Se você o renovou, atualize as informações do cartão" }, "verificationRequired": { "message": "Verificação necessária", @@ -2133,7 +2133,7 @@ "message": "Permitir integração com o navegador" }, "enableBrowserIntegrationDesc1": { - "message": "Usado para permitir desbloqueio biométrico em navegadores que não são o Safari." + "message": "Usado para permitir o desbloqueio biométrico em navegadores que não são o Safari." }, "enableDuckDuckGoBrowserIntegration": { "message": "Permitir integração com o navegador DuckDuckGo" @@ -2145,16 +2145,13 @@ "message": "Integração com o navegador não suportada" }, "browserIntegrationErrorTitle": { - "message": "Erro ao ativar a integração do navegador" + "message": "Erro ao ativar a integração com o navegador" }, "browserIntegrationErrorDesc": { - "message": "Ocorreu um erro ao ativar a integração do navegador." - }, - "browserIntegrationMasOnlyDesc": { - "message": "Infelizmente, por ora, a integração do navegador só é suportada na versão da Mac App Store." + "message": "Ocorreu um erro ao ativar a integração com o navegador." }, "browserIntegrationWindowsStoreDesc": { - "message": "Infelizmente, a integração do navegador não é suportada na versão da Microsoft Store." + "message": "Infelizmente, a integração com o navegador não é suportada na versão da Microsoft Store no momento." }, "browserIntegrationLinuxDesc": { "message": "Infelizmente, a integração do navegador não é suportada na versão linux no momento." @@ -2163,22 +2160,22 @@ "message": "Exigir verificação para integração com o navegador" }, "enableBrowserIntegrationFingerprintDesc": { - "message": "Adicione uma camada adicional de segurança, exigindo validação de frase biométrica ao estabelecer uma ligação entre o computador e o navegador. Quando ativado, isto requer intervenção do usuário e verificação cada vez que uma conexão é estabelecida." + "message": "Adicione uma camada adicional de segurança, exigindo a validação da frase biométrica ao estabelecer uma ligação entre o computador e o navegador. Requer intervenção do usuário e verificação cada vez que uma conexão é estabelecida." }, "enableHardwareAcceleration": { - "message": "Utilizar aceleração de hardware" + "message": "Usar aceleração de hardware" }, "enableHardwareAccelerationDesc": { - "message": "Por padrão esta configuração está ativada. Desligue apenas se tiver problemas gráficos. Reiniciar é necessário." + "message": "Por padrão esta configuração está ativada. Desative apenas se tiver problemas gráficos. Reiniciar é necessário." }, "approve": { "message": "Aprovar" }, "verifyBrowserTitle": { - "message": "Verificar conexão do navegador" + "message": "Verifique a conexão com o navegador" }, "verifyBrowserDesc": { - "message": "Por favor, certifique-se que a impressão digital mostrada é idêntica à impressão digital exibida na extensão do navegador." + "message": "Certifique-se que a frase biométrica mostrada é idêntica à exibida na extensão do navegador." }, "verifyNativeMessagingConnectionTitle": { "message": "$APPID$ quer se conectar ao Bitwarden", @@ -2190,7 +2187,7 @@ } }, "verifyNativeMessagingConnectionDesc": { - "message": "Gostaria de aprovar este pedido?" + "message": "Gostaria de aprovar esta solicitação?" }, "verifyNativeMessagingConnectionWarning": { "message": "Se não iniciou esta solicitação, não a aprove." @@ -2214,19 +2211,19 @@ "message": "Sua senha nova não pode ser a mesma que a sua atual." }, "hintEqualsPassword": { - "message": "Sua dica de senha não pode ser o mesmo que sua senha." + "message": "A dica da sua senha não pode ser a mesma que a sua senha." }, "personalOwnershipPolicyInEffect": { "message": "Uma política de organização está afetando suas opções de propriedade." }, "personalOwnershipPolicyInEffectImports": { - "message": "Uma política da organização bloqueou a importação de itens em seu cofre pessoal." + "message": "Uma política da organização bloqueou a importação de itens em seu cofre individual." }, "personalDetails": { "message": "Detalhes pessoais" }, "identification": { - "message": "Identificação" + "message": "Identidade" }, "contactInfo": { "message": "Informações de contato" @@ -2259,14 +2256,14 @@ "message": "Data de apagamento" }, "deletionDateDesc": { - "message": "O Send será eliminado permanentemente na data e hora especificadas.", + "message": "O Send será apagado para sempre no horário especificado.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { "message": "Data de validade" }, "expirationDateDesc": { - "message": "Se definido, o acesso a este Send expirará na data e hora especificadas.", + "message": "Se configurado, o acesso a este Send acabará no horário especificado.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCount": { @@ -2274,7 +2271,7 @@ "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "maxAccessCountDesc": { - "message": "Se atribuído, usuários não poderão mais acessar este Send assim que o número máximo de acessos for atingido.", + "message": "Se configurado, usuários não poderão mais acessar este Send assim que o número máximo de acessos for atingido.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "currentAccessCount": { @@ -2285,11 +2282,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { - "message": "Opcionalmente exigir uma senha para os usuários acessarem este Send.", + "message": "Opcionalmente exija uma senha para os usuários acessarem este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { - "message": "Notas privadas sobre este Send.", + "message": "Anotações privadas sobre este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLink": { @@ -2349,7 +2346,7 @@ "message": "Personalizado" }, "deleteSendConfirmation": { - "message": "Você tem certeza que deseja excluir este Send?", + "message": "Tem certeza que quer apagar este Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copySendLinkToClipboard": { @@ -2364,7 +2361,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { - "message": "Devido a uma política corporativa, você só pode excluir um Send existente.", + "message": "Devido a uma política corporativa, você só pode apagar um Send existente.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copyLink": { @@ -2386,10 +2383,10 @@ "message": "Número máximo de acessos atingido" }, "expired": { - "message": "Expirado" + "message": "Vencido" }, "pendingDeletion": { - "message": "Exclusão pendente" + "message": "Apagamento pendente" }, "webAuthnAuthenticate": { "message": "Autenticar WebAuthn" @@ -2416,16 +2413,16 @@ "message": "Você precisa verificar o seu e-mail para usar este recurso." }, "passwordPrompt": { - "message": "Solicitação nova de senha principal" + "message": "Resolicitar senha principal" }, "passwordConfirmation": { "message": "Confirmação de senha principal" }, "passwordConfirmationDesc": { - "message": "Esta ação está protegida. Para continuar, por favor, reinsira a sua senha principal para verificar sua identidade." + "message": "Esta ação está protegida. Redigite a sua senha principal para verificar sua identidade." }, "masterPasswordSuccessfullySet": { - "message": "Senha principal definida com sucesso" + "message": "Senha principal configurada com sucesso" }, "updatedMasterPassword": { "message": "Senha principal atualizada" @@ -2434,31 +2431,31 @@ "message": "Atualizar senha principal" }, "updateMasterPasswordWarning": { - "message": "Sua senha principal foi alterada recentemente por um administrador de sua organização. Para acessar o cofre, você precisa atualizá-la agora. O processo desconectará você da sessão atual, exigindo que você entre novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." + "message": "Sua senha principal foi alterada recentemente por um administrador de sua organização. Para acessar o cofre, você precisa atualizá-la agora. O processo desconectará você da sessão atual, exigindo que você conecte-se novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, "updateWeakMasterPasswordWarning": { - "message": "A sua senha principal não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha principal agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." + "message": "A sua senha principal não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha principal agora. O processo desconectará você da sessão atual, exigindo que você se conecte novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, "changePasswordWarning": { - "message": "Após mudar a sua senha, será necessário entrar novamente com a sua nova senha. Sessões ativas em outros dispositivos serão encerradas em até uma hora." + "message": "Após mudar a sua senha, será necessário conectar-se novamente com a sua nova senha. Sessões ativas em outros dispositivos serão desconectadas em até uma hora." }, "accountRecoveryUpdateMasterPasswordSubtitle": { - "message": "Mude a sua senha principal para completar a recuperação de conta." + "message": "Altere a sua senha principal para concluir a recuperação da conta." }, "updateMasterPasswordSubtitle": { "message": "Sua senha principal não corresponde aos requisitos da organização. Mude a sua senha principal para continuar." }, "tdeDisabledMasterPasswordRequired": { - "message": "Sua organização desativou a criptografia confiável do dispositivo. Defina uma senha principal para acessar o seu cofre." + "message": "Sua organização desativou a criptografia de dispositivo confiado. Configure uma senha principal para acessar o seu cofre." }, "tryAgain": { - "message": "Repetir" + "message": "Tentar novamente" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verificação necessária para esta ação. Defina um PIN para continuar." + "message": "Verificação necessária para esta ação. Configure um PIN para continuar." }, "setPin": { - "message": "Definir PIN" + "message": "Configurar PIN" }, "verifyWithBiometrics": { "message": "Verificar com biometria" @@ -2467,13 +2464,13 @@ "message": "Aguardando confirmação" }, "couldNotCompleteBiometrics": { - "message": "Não foi possível completar a biometria." + "message": "Não foi possível concluir a biometria." }, "needADifferentMethod": { "message": "Precisa de um método diferente?" }, "useMasterPassword": { - "message": "Usar a senha principal" + "message": "Usar senha principal" }, "usePin": { "message": "Usar PIN" @@ -2482,7 +2479,7 @@ "message": "Usar biometria" }, "enterVerificationCodeSentToEmail": { - "message": "Digite o código de verificação que foi enviado para o seu e-mail." + "message": "Digite o código de verificação enviado para o seu e-mail." }, "resendCode": { "message": "Reenviar código" @@ -2507,7 +2504,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "As políticas da sua organização estão afetando o tempo limite do seu cofre. O tempo limite máximo permitido para cofre é $HOURS$ hora(s) e $MINUTES$ minuto(s). A ação de tempo limite do seu cofre é definida como $ACTION$.", + "message": "As políticas da sua organização estão afetando o tempo limite do seu cofre. O máximo permitido do tempo limite do cofre é $HOURS$ hora(s) e $MINUTES$ minuto(s). A ação de tempo limite do seu cofre está configurada para $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -2524,7 +2521,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "As políticas da sua organização definiram a ação do tempo limite do seu cofre para $ACTION$.", + "message": "As políticas da sua organização configuraram a ação do tempo limite do seu cofre para $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2536,10 +2533,10 @@ "message": "O tempo limite do seu cofre excede as restrições definidas por sua organização." }, "vaultTimeoutPolicyAffectingOptions": { - "message": "Requisitos de políticas corporativas foram adicionadas as suas opções de tempo limite" + "message": "Os requisitos das políticas corporativas foram aplicados às suas opções de tempo limite" }, "vaultTimeoutPolicyInEffect": { - "message": "As políticas da sua organização definiram o seu tempo limite máximo permitido no cofre para $HOURS$ hora(s) e $MINUTES$ minuto(s).", + "message": "As políticas da sua organização configuraram o seu máximo permitido do tempo limite do cofre para $HOURS$ hora(s) e $MINUTES$ minuto(s).", "placeholders": { "hours": { "content": "$1", @@ -2565,7 +2562,7 @@ } }, "vaultCustomTimeoutMinimum": { - "message": "O tempo limite personalizado mínimo é de 1 minuto." + "message": "O mínimo do tempo limite personalizado é de 1 minuto." }, "inviteAccepted": { "message": "Convite aceito" @@ -2580,7 +2577,7 @@ "message": "Exportação de cofre removida" }, "personalVaultExportPolicyInEffect": { - "message": "Uma ou mais políticas da organização impdem que você exporte seu cofre pessoal." + "message": "Uma ou mais políticas da organização impedem que você exporte seu cofre pessoal." }, "addAccount": { "message": "Adicionar conta" @@ -2592,13 +2589,13 @@ "message": "Senha principal removida" }, "removeMasterPasswordForOrganizationUserKeyConnector": { - "message": "Uma senha principal não é mais necessária para membros da seguinte organização. Por favor, confirme o domínio abaixo com o administrador da sua organização." + "message": "Uma senha principal não é mais necessária para membros da seguinte organização. Confirme o domínio abaixo com o administrador da sua organização." }, "organizationName": { "message": "Nome da organização" }, "keyConnectorDomain": { - "message": "Domínio do Conector de Chave" + "message": "Domínio do Key Connector" }, "leaveOrganization": { "message": "Sair da organização" @@ -2610,25 +2607,25 @@ "message": "Você saiu da organização." }, "ssoKeyConnectorError": { - "message": "Erro do conector de chave: certifique-se de que o conector de chave está disponível e funcionando corretamente." + "message": "Erro do Key Connector: certifique-se de que o Key Connector está disponível e funcionando corretamente." }, "lockAllVaults": { "message": "Bloquear todos os cofres" }, "accountLimitReached": { - "message": "Não mais do que 5 contas podem estar logadas ao mesmo tempo." + "message": "Não mais do que 5 contas podem estar conectadas ao mesmo tempo." }, "accountPreferences": { "message": "Preferências" }, "appPreferences": { - "message": "Configurações do Aplicativo (Todas as Contas)" + "message": "Configurações do aplicativo (todas as contas)" }, "accountSwitcherLimitReached": { - "message": "Limite de Contas atingido. Saia de uma conta para adicionar outra." + "message": "Limite de contas atingido. Desconecte uma conta para adicionar outra." }, "settingsTitle": { - "message": "Configurações do Aplicativo para $EMAIL$", + "message": "Configurações do aplicativo para $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -2646,13 +2643,13 @@ "message": "Opções" }, "sessionTimeout": { - "message": "Sua sessão expirou. Volte e tente entrar novamente." + "message": "Sua sessão expirou. Volte e tente se conectar novamente." }, "exportingPersonalVaultTitle": { "message": "Exportando cofre individual" }, "exportingIndividualVaultDescription": { - "message": "Apenas os itens do cofre individual associados com $EMAIL$ serão exportados. Os itens do cofre da organização não serão incluídos. Apenas as informações dos itens do cofre serão exportadas e não incluirão anexos associados.", + "message": "Apenas os itens do cofre individual associados a $EMAIL$ serão exportados. Os itens do cofre de organizações não serão incluídos. Apenas as informações dos itens do cofre serão exportadas e não incluirão anexos associados.", "placeholders": { "email": { "content": "$1", @@ -2661,7 +2658,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Apenas os itens do cofre individual, incluindo anexos associados com $EMAIL$ serão exportados. Os itens do cofre da organização não serão incluídos", + "message": "Apenas os itens do cofre individual, incluindo anexos associados com $EMAIL$ serão exportados. Os itens do cofre de organizações não serão incluídos", "placeholders": { "email": { "content": "$1", @@ -2673,7 +2670,7 @@ "message": "Exportando cofre da organização" }, "exportingOrganizationVaultDesc": { - "message": "Apenas o cofre da organização associado com $ORGANIZATION$ será exportado. Itens do cofre individual e itens de outras organizações não serão incluídos.", + "message": "Apenas o cofre da organização associado a $ORGANIZATION$ será exportado. Itens de cofres individuais e de outras organizações não serão incluídos.", "placeholders": { "organization": { "content": "$1", @@ -2691,7 +2688,7 @@ } }, "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { - "message": "Apenas o cofre da organização associado com $ORGANIZATION$ será exportado. Os itens da minhas coleções não serão incluídos.", + "message": "Apenas o cofre da organização associado com $ORGANIZATION$ será exportado. As coleções de itens não serão incluídos.", "placeholders": { "organization": { "content": "$1", @@ -2713,13 +2710,13 @@ "description": "Short for 'credential generator'." }, "whatWouldYouLikeToGenerate": { - "message": "O que você gostaria de gerar?" + "message": "O que gostaria de gerar?" }, "passwordType": { - "message": "Tipo de senha" + "message": "Tipo da senha" }, "regenerateUsername": { - "message": "Regenerar nome de usuário" + "message": "Regerar nome de usuário" }, "generateUsername": { "message": "Gerar nome de usuário" @@ -2740,7 +2737,7 @@ "message": "Senha gerada" }, "passphraseGenerated": { - "message": "Gerador de frase secreta" + "message": "Frase secreta gerada" }, "usernameGenerated": { "message": "Nome de usuário gerado" @@ -2796,19 +2793,19 @@ "message": "E-mail pega-tudo" }, "catchallEmailDesc": { - "message": "Use o catch-all configurado no seu domínio." + "message": "Use a caixa de entrada pega-tudo configurada no seu domínio." }, "useThisEmail": { "message": "Usar este e-mail" }, "useThisPassword": { - "message": "Use esta senha" + "message": "Usar esta senha" }, "useThisPassphrase": { - "message": "Use esta frase secreta" + "message": "Usar esta frase secreta" }, "useThisUsername": { - "message": "Use este nome de usuário" + "message": "Usar este nome de usuário" }, "random": { "message": "Aleatório" @@ -2826,13 +2823,13 @@ "message": "Todos os cofres" }, "searchOrganization": { - "message": "Pesquisar organização" + "message": "Buscar na organização" }, "searchMyVault": { - "message": "Pesquisar meu cofre" + "message": "Buscar no meu cofre" }, "forwardedEmail": { - "message": "Alias de Encaminhamento de E-mail" + "message": "Alias de encaminhamento de e-mail" }, "forwardedEmailDesc": { "message": "Gere um alias de e-mail com um serviço externo de encaminhamento." @@ -2846,7 +2843,7 @@ "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "Erro $SERVICENAME$: $ERRORMESSAGE$", + "message": "Erro do $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2874,7 +2871,7 @@ } }, "forwaderInvalidToken": { - "message": "Token de API $SERVICENAME$ inválido", + "message": "Token de API do $SERVICENAME$ inválido", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2884,7 +2881,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Token de API $SERVICENAME$ inválido: $ERRORMESSAGE$", + "message": "Token de API da $SERVICENAME$ inválido: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2922,7 +2919,7 @@ } }, "forwarderNoAccountId": { - "message": "Não foi possível obter o ID da conta de e-mail mascarado $SERVICENAME$.", + "message": "Não é possível obter o ID da conta de e-mail mascarado do $SERVICENAME$.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2932,7 +2929,7 @@ } }, "forwarderNoDomain": { - "message": "Domínio $SERVICENAME$ inválido.", + "message": "Domínio inválido do $SERVICENAME$.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2942,7 +2939,7 @@ } }, "forwarderNoUrl": { - "message": "URL $SERVICENAME$ inválido.", + "message": "URL inválido do $SERVICENAME$.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2952,7 +2949,7 @@ } }, "forwarderUnknownError": { - "message": "Ocorreu um erro $SERVICENAME$ desconhecido.", + "message": "Ocorreu um erro desconhecido do $SERVICENAME$.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2972,7 +2969,7 @@ } }, "hostname": { - "message": "Servidor", + "message": "Nome do servidor", "description": "Part of a URL." }, "apiAccessToken": { @@ -2997,16 +2994,16 @@ "message": "Cofre" }, "loginWithMasterPassword": { - "message": "Entrar com senha principal" + "message": "Conectar-se com senha principal" }, "rememberEmail": { - "message": "Lembrar e-mail" + "message": "Lembrar do e-mail" }, "newAroundHere": { "message": "Novo por aqui?" }, "loggingInTo": { - "message": "Entrando em $DOMAIN$", + "message": "Conectando-se a $DOMAIN$", "placeholders": { "domain": { "content": "$1", @@ -3015,7 +3012,7 @@ } }, "logInWithAnotherDevice": { - "message": "Entrar com outro dispositivo" + "message": "Conectar-se com outro dispositivo" }, "loginInitiated": { "message": "Autenticação iniciada" @@ -3033,7 +3030,7 @@ "message": "Desbloqueie o Bitwarden no seu dispositivo ou no " }, "notificationSentDeviceAnchor": { - "message": "app web" + "message": "aplicativo web" }, "notificationSentDevicePart2": { "message": "Certifique-se de que a frase biométrica corresponde à frase abaixo antes de aprovar." @@ -3051,7 +3048,7 @@ "message": "Você será notificado assim que a solicitação for aprovada" }, "needAnotherOption": { - "message": "A entrada com dispositivo deve ser ativada nas configurações do aplicativo móvel do Bitwarden. Precisa de outra opção?" + "message": "A autenticação com dispositivo deve ser ativada nas configurações do aplicativo móvel do Bitwarden. Precisa de outra opção?" }, "viewAllLogInOptions": { "message": "Ver todas as opções de autenticação" @@ -3089,10 +3086,10 @@ } }, "youDeniedLoginAttemptFromAnotherDevice": { - "message": "Você negou uma tentativa de autenticação por outro dispositivo. Se foi você, tente entrar com o dispositivo novamente." + "message": "Você negou uma tentativa de autenticação por outro dispositivo. Se foi você, tente conectar-se com o dispositivo novamente." }, "webApp": { - "message": "App web" + "message": "Aplicativo web" }, "mobile": { "message": "Móvel", @@ -3120,7 +3117,7 @@ "message": "Solicitação de autenticação" }, "deviceType": { - "message": "Tipo de dispositivo" + "message": "Tipo do dispositivo" }, "ipAddress": { "message": "Endereço de IP" @@ -3183,7 +3180,7 @@ "message": "Nenhum e-mail?" }, "goBack": { - "message": "Voltar" + "message": "Volte" }, "toEditYourEmailAddress": { "message": "para editar o seu endereço de e-mail." @@ -3213,10 +3210,10 @@ "message": "Acessando" }, "accessTokenUnableToBeDecrypted": { - "message": "Você foi desconectado porque seu token de acesso não pôde ser descriptografado. Por favor, entre novamente para resolver esse problema." + "message": "Você foi desconectado porque seu token de acesso não pôde ser descriptografado. Conecte-se novamente para resolver esse problema." }, "refreshTokenSecureStorageRetrievalFailure": { - "message": "Você foi desconectado porque seu token de recarregamento não pôde ser recuperado. Por favor, entre novamente para resolver esse problema." + "message": "Você foi desconectado porque seu token de recarregamento não pôde ser recuperado. Conecte-se novamente para resolver esse problema." }, "masterPasswordHint": { "message": "A sua senha principal não pode ser recuperada se você esquecê-la!" @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Faça upgrade para o Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Ação do tempo limite" + }, + "sessionTimeoutHeader": { + "message": "Tempo limite da sessão" } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index d19c0075873..de0427ddab0 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -70,7 +70,7 @@ } }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "Não tem permissão para editar este item" }, "welcomeBack": { "message": "Bem-vindo de volta" @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Ocorreu um erro ao ativar a integração do navegador." }, - "browserIntegrationMasOnlyDesc": { - "message": "Infelizmente, a integração do navegador só é suportada na versão da Mac App Store por enquanto." - }, "browserIntegrationWindowsStoreDesc": { "message": "Infelizmente, a integração do navegador não é atualmente suportada na versão da Microsoft Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Atualizar para o Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Ação de tempo limite" + }, + "sessionTimeoutHeader": { + "message": "Tempo limite da sessão" } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index fbb1d018b60..a72ce3547e9 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Din păcate, integrarea browserului este acceptată numai în versiunea Mac App Store pentru moment." - }, "browserIntegrationWindowsStoreDesc": { "message": "Din păcate, integrarea browserului nu este susținută în prezent în versiunea Microsoft Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 0170bd57310..914bb603630 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Произошла ошибка при включении интеграции с браузером." }, - "browserIntegrationMasOnlyDesc": { - "message": "К сожалению, интеграция браузера пока поддерживается только в версии Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "К сожалению, интеграция браузера в версии для Microsoft Store в настоящее время не поддерживается." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Обновить до Премиум" + }, + "sessionTimeoutSettingsAction": { + "message": "Тайм-аут действия" + }, + "sessionTimeoutHeader": { + "message": "Тайм-аут сеанса" } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 4193867ead5..a83b2cbf536 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 8fecc5de1b9..0b14b961bbb 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1692,7 +1692,7 @@ "description": "Default URI match detection for auto-fill." }, "toggleOptions": { - "message": "Voľby prepínača" + "message": "Zobraziť/skryť možnosti" }, "organization": { "message": "Organizácia", @@ -1856,7 +1856,7 @@ "message": "odomknúť svoj trezor" }, "autoPromptTouchId": { - "message": "Pri spustení požiadať o Touch ID" + "message": "Pri spustení aplikácie požiadať o Touch ID" }, "lockWithMasterPassOnRestart1": { "message": "Pri reštarte zamknúť hlavným heslom" @@ -1904,7 +1904,7 @@ "message": "Musíte vybrať aspoň jednu zbierku." }, "premiumUpdated": { - "message": "Povýšili ste na prémium." + "message": "Upgradovali ste na Prémium." }, "restore": { "message": "Obnoviť" @@ -1964,10 +1964,10 @@ "message": "Naozaj chcete natrvalo odstrániť túto položku?" }, "permanentlyDeletedItem": { - "message": "Položka natrvalo odstránená" + "message": "Položka bola natrvalo odstránená" }, "restoredItem": { - "message": "Obnovená položka" + "message": "Položka bola obnovená" }, "permanentlyDelete": { "message": "Natrvalo odstrániť" @@ -2139,7 +2139,7 @@ "message": "Povoliť integráciu prehliadača DuckDuckGo" }, "enableDuckDuckGoBrowserIntegrationDesc": { - "message": "Používajte svoj trezor Bitwarden pri prehliadaní pomocou DuckDuckGo." + "message": "Používajte svoj trezor v Bitwardene pri prehliadaní pomocou DuckDuckGo." }, "browserIntegrationUnsupportedTitle": { "message": "Integrácia v prehliadači nie je podporovaná" @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Pri povoľovaní integrácie v prehliadači sa vyskytla chyba." }, - "browserIntegrationMasOnlyDesc": { - "message": "Bohužiaľ, integrácia v prehliadači je zatiaľ podporovaná iba vo verzii Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "Bohužiaľ, integrácia v prehliadači zatiaľ nie je podporovaná, ak je aplikácia nainštalovaná cez Microsoft Store." }, @@ -2181,7 +2178,7 @@ "message": "Uistite sa, že zobrazený odtlačok prsta je identický s odtlačkom prsta zobrazeným v rozšírení prehliadača." }, "verifyNativeMessagingConnectionTitle": { - "message": "$APPID$ sa chce pripojiť k Bitwarden", + "message": "$APPID$ sa chce pripojiť k Bitwardenu", "placeholders": { "appid": { "content": "$1", @@ -2208,7 +2205,7 @@ "message": "Vzhľadom na spôsob inštalácie nebolo možné automaticky povoliť podporu biometrie. Chcete otvoriť dokumentáciu, ako to urobiť manuálne?" }, "personalOwnershipSubmitError": { - "message": "Z dôvodu podnikovej politiky máte obmedzené ukladanie položiek do osobného trezora. Zmeňte možnosť vlastníctvo na organizáciu a vyberte si z dostupných zbierok." + "message": "Z dôvodu podnikových pravidiel máte obmedzené ukladanie položiek do osobného trezora. Zmeňte možnosť vlastníctvo na organizáciu a vyberte si z dostupných zbierok." }, "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { "message": "Nové heslo nemôže byť rovnaké ako súčasné heslo." @@ -2305,15 +2302,15 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { - "message": "Send vytvorený", + "message": "Send bol vytvorený", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Send upravený", + "message": "Send bol upravený", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletedSend": { - "message": "Send odstránený", + "message": "Send bol odstránený", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newPassword": { @@ -2324,7 +2321,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { - "message": "Vytvoriť Send", + "message": "Nový Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTextDesc": { @@ -2360,7 +2357,7 @@ "message": "Kopírovať odkaz na zdieľanie tohto Sendu do schránky počas ukladania." }, "sendDisabled": { - "message": "Funkcia Send zakázaná", + "message": "Send bol odstránený", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { @@ -2428,7 +2425,7 @@ "message": "Hlavné heslo bolo úspešne nastavené" }, "updatedMasterPassword": { - "message": "Hlavné heslo aktualizované" + "message": "Hlavné heslo bolo aktualizované" }, "updateMasterPassword": { "message": "Aktualizovať hlavné heslo" @@ -2479,7 +2476,7 @@ "message": "Použiť PIN kód" }, "useBiometrics": { - "message": "Použiť biometrické údaje" + "message": "Použiť biometriu" }, "enterVerificationCodeSentToEmail": { "message": "Zadajte overovací kód, ktorý vám bol zaslaný na e-mail." @@ -2577,7 +2574,7 @@ "message": "Táto organizácia má podnikovú politiku, ktorá vás automaticky zaregistruje na obnovenie hesla. Registrácia umožní správcom organizácie zmeniť vaše hlavné heslo." }, "vaultExportDisabled": { - "message": "Export trezoru je zakázaný" + "message": "Export trezoru bol odstránený" }, "personalVaultExportPolicyInEffect": { "message": "Jedna alebo viacero zásad organizácie vám bráni exportovať váš osobný trezor." @@ -2622,7 +2619,7 @@ "message": "Predvoľby" }, "appPreferences": { - "message": "Nastavenia aplikácie (Všetky účty)" + "message": "Nastavenia aplikácie (všetky účty)" }, "accountSwitcherLimitReached": { "message": "Dosiahnutý limit počtu účtov. Odhláste sa z účtu aby ste mohli pridať ďalší." @@ -2793,7 +2790,7 @@ "message": "Použiť možnosti subadresovania svojho poskytovateľa e-mailu." }, "catchallEmail": { - "message": "Catch-all Email" + "message": "E-mail Catch-all" }, "catchallEmailDesc": { "message": "Použiť doručenú poštu typu catch-all nastavenú na doméne." @@ -2846,7 +2843,7 @@ "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "$SERVICENAME$ chyba: $ERRORMESSAGE$", + "message": "Chyba $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2962,7 +2959,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Nepodporovaná služba: '$SERVICENAME$'.", + "message": "Neznáme presmerovanie: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2979,16 +2976,16 @@ "message": "Prístupový token API" }, "apiKey": { - "message": "API kľúč" + "message": "Kľúč API" }, "premiumSubcriptionRequired": { "message": "Vyžaduje sa predplatné Prémium" }, "organizationIsDisabled": { - "message": "Organizácia je vypnutá." + "message": "Organizácia je pozastavená" }, "disabledOrganizationFilterError": { - "message": "K položkám vo vypnutej organizácii nie je možné pristupovať. Požiadajte o pomoc vlastníka organizácie." + "message": "K položkám pozastavenej organizácii nie je možné pristupovať. Požiadajte o pomoc vlastníka organizácie." }, "neverLockWarning": { "message": "Ste si istí, že chcete použiť možnosť \"Nikdy\"? Táto predvoľba ukladá šifrovací kľúč od trezora priamo na zariadení. Ak použijete túto možnosť, mali by ste svoje zariadenie náležite zabezpečiť." @@ -3189,16 +3186,16 @@ "message": "na úpravu e-mailovej adresy." }, "exposedMasterPassword": { - "message": "Odhalené hlavné heslo" + "message": "Uniknuté hlavné heslo" }, "exposedMasterPasswordDesc": { - "message": "Nájdené heslo v uniknuných údajoch. Na ochranu svojho účtu používajte jedinečné heslo. Naozaj chcete používať odhalené heslo?" + "message": "Heslo bolo nájdené v uniknutých údajoch. Na ochranu svojho účtu používajte jedinečné heslo. Naozaj chcete používať uniknuté heslo?" }, "weakAndExposedMasterPassword": { - "message": "Slabé a odhalené hlavné heslo" + "message": "Slabé a uniknuté hlavné heslo" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Nájdené slabé heslo v uniknuných údajoch. Na ochranu svojho účtu používajte silné a jedinečné heslo. Naozaj chcete používať toto heslo?" + "message": "Nájdené slabé heslo v uniknutých údajoch. Na ochranu svojho účtu používajte silné a jedinečné heslo. Naozaj chcete používať toto heslo?" }, "checkForBreaches": { "message": "Skontrolovať známe úniky údajov pre toto heslo" @@ -3222,7 +3219,7 @@ "message": "Vaše hlavné heslo sa nebude dať obnoviť, ak ho zabudnete!" }, "characterMinimum": { - "message": "Minimálny počet znakov $LENGTH$", + "message": "Minimálny počet znakov: $LENGTH$", "placeholders": { "length": { "content": "$1", @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgradovať na Prémium" + }, + "sessionTimeoutSettingsAction": { + "message": "Akcia pri vypršaní časového limitu" + }, + "sessionTimeoutHeader": { + "message": "Časový limit relácie" } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 329403e43fc..353c6858afa 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index b7df1855552..1bc4a0ed016 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -70,7 +70,7 @@ } }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "Немате дозволу да уређујете ову ставку" }, "welcomeBack": { "message": "Добродошли назад" @@ -775,7 +775,7 @@ "message": "Употребити једнократну пријаву" }, "yourOrganizationRequiresSingleSignOn": { - "message": "Your organization requires single sign-on." + "message": "Ваша организација захтева јединствену пријаву." }, "submit": { "message": "Пошаљи" @@ -1039,7 +1039,7 @@ "message": "Морате додати или основни УРЛ сервера или бар једно прилагођено окружење." }, "selfHostedEnvMustUseHttps": { - "message": "URLs must use HTTPS." + "message": "Везе морају да користе HTTPS." }, "customEnvironment": { "message": "Прилагођено окружење" @@ -1862,10 +1862,10 @@ "message": "Закључајте са главном лозинком при поновном покретању" }, "requireMasterPasswordOrPinOnAppRestart": { - "message": "Require master password or PIN on app restart" + "message": "Потражити главну лозинку или ПИН при поновном покретању" }, "requireMasterPasswordOnAppRestart": { - "message": "Require master password on app restart" + "message": "Потражити главну лозинку при поновном покретању апликације" }, "deleteAccount": { "message": "Брисање налога" @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Дошло је до грешке при омогућавању интеграције прегледача." }, - "browserIntegrationMasOnlyDesc": { - "message": "Нажалост, интеграција прегледача за сада је подржана само у верзији Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "Нажалост, интеграција прегледача није за сада подржана у Windows Store." }, @@ -4183,7 +4180,7 @@ "message": "Ставка је послата у архиву" }, "itemWasUnarchived": { - "message": "Item was unarchived" + "message": "Ставка враћена из архиве" }, "archiveItem": { "message": "Архивирај ставку" @@ -4192,33 +4189,39 @@ "message": "Архивиране ставке су искључене из општих резултата претраге и предлога за ауто попуњавање. Јесте ли сигурни да желите да архивирате ову ставку?" }, "zipPostalCodeLabel": { - "message": "ZIP / Postal code" + "message": "ZIP/Поштански број" }, "cardNumberLabel": { - "message": "Card number" + "message": "Број картице" }, "upgradeNow": { - "message": "Upgrade now" + "message": "Надогради сада" }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "Уграђени аутентификатор" }, "secureFileStorage": { - "message": "Secure file storage" + "message": "Сигурно складиштење датотека" }, "emergencyAccess": { - "message": "Emergency access" + "message": "Хитан приступ" }, "breachMonitoring": { - "message": "Breach monitoring" + "message": "Праћење повreda безбедности" }, "andMoreFeatures": { - "message": "And more!" + "message": "И још више!" }, "planDescPremium": { - "message": "Complete online security" + "message": "Потпуна онлајн безбедност" }, "upgradeToPremium": { - "message": "Upgrade to Premium" + "message": "Надоградите на Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index e06f78efbf6..93d56419ae3 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Ett fel uppstod vid aktivering av webbläsarintegration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Tyvärr stöds webbläsarintegration för tillfället endast i versionen från Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "Tyvärr stöds webbläsarintegration för tillfället inte i versionen från Windows Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Uppgradera till Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Tidsgränsåtgärd" + }, + "sessionTimeoutHeader": { + "message": "Sessionstidsgräns" } } diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index de574204738..2f9d12917d6 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "உலாவி ஒருங்கிணைப்பை இயக்கும்போது ஒரு பிழை ஏற்பட்டது." }, - "browserIntegrationMasOnlyDesc": { - "message": "துரதிர்ஷ்டவசமாக உலாவி ஒருங்கிணைப்பு தற்போது Mac App Store பதிப்பில் மட்டுமே ஆதரிக்கப்படுகிறது." - }, "browserIntegrationWindowsStoreDesc": { "message": "துரதிர்ஷ்டவசமாக உலாவி ஒருங்கிணைப்பு தற்போது Microsoft Store பதிப்பில் ஆதரிக்கப்படவில்லை." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 70d4c7cb494..d607bb8d097 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index a9b197f946c..d794ace629c 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." }, - "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." - }, "browserIntegrationWindowsStoreDesc": { "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Upgrade to Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 94f64b31811..ac67b177cbf 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Tarayıcı entegrasyonu etkinleştirilirken bir hata oluştu." }, - "browserIntegrationMasOnlyDesc": { - "message": "Ne yazık ki tarayıcı entegrasyonu şu anda sadece Mac App Store sürümünde destekleniyor." - }, "browserIntegrationWindowsStoreDesc": { "message": "Maalesef tarayıcı entegrasyonu şimdilik Windows Store sürümünde desteklenmiyor." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Premium'a yükselt" + }, + "sessionTimeoutSettingsAction": { + "message": "Zaman aşımı eylemi" + }, + "sessionTimeoutHeader": { + "message": "Oturum zaman aşımı" } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index b6e0b18a981..7ed0710ca74 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Під час увімкнення інтеграції з браузером сталася помилка." }, - "browserIntegrationMasOnlyDesc": { - "message": "На жаль, зараз інтеграція з браузером підтримується лише у версії для Mac з App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "На жаль, зараз інтеграція з браузером не підтримується у версії з Microsoft Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Покращити до Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index db945022070..8bf88aba458 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "Đã xảy ra lỗi khi bật tích hợp với trình duyệt." }, - "browserIntegrationMasOnlyDesc": { - "message": "Rất tiếc, tính năng tích hợp trình duyệt hiện chỉ được hỗ trợ trong phiên bản Mac App Store." - }, "browserIntegrationWindowsStoreDesc": { "message": "Rất tiếc, tính năng tích hợp trình duyệt hiện không được hỗ trợ trong phiên bản Microsoft Store." }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "Nâng cấp lên gói Cao cấp" + }, + "sessionTimeoutSettingsAction": { + "message": "Timeout action" + }, + "sessionTimeoutHeader": { + "message": "Session timeout" } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 93c907743d9..b5e68b83bde 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1298,7 +1298,7 @@ "message": "系统空闲时" }, "onSleep": { - "message": "系统休眠时" + "message": "系统睡眠时" }, "onLocked": { "message": "系统锁定时" @@ -1503,7 +1503,7 @@ "message": "优先客户支持。" }, "premiumSignUpFuture": { - "message": "所有未来的高级功能。即将推出!" + "message": "未来的更多高级版功能。敬请期待!" }, "premiumPurchase": { "message": "购买高级版" @@ -2029,7 +2029,7 @@ "message": "Bitwarden 可以存储并填充两步验证码。选择相机图标来拍摄此网站的验证器二维码,或将密钥复制并粘贴到此字段。" }, "premium": { - "message": "高级会员", + "message": "高级版", "description": "Premium membership" }, "freeOrgsCannotUseAttachments": { @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "启用浏览器集成时出错。" }, - "browserIntegrationMasOnlyDesc": { - "message": "很遗憾,目前仅 Mac App Store 版本支持浏览器集成。" - }, "browserIntegrationWindowsStoreDesc": { "message": "很遗憾,Microsoft Store 版本目前不支持浏览器集成。" }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "升级为高级版" + }, + "sessionTimeoutSettingsAction": { + "message": "超时动作" + }, + "sessionTimeoutHeader": { + "message": "会话超时" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 0dc4d0911fc..61fc00543ed 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -70,7 +70,7 @@ } }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "你沒有權限編輯這個項目" }, "welcomeBack": { "message": "歡迎回來" @@ -2150,9 +2150,6 @@ "browserIntegrationErrorDesc": { "message": "啟用瀏覽器整合時發生錯誤。" }, - "browserIntegrationMasOnlyDesc": { - "message": "很遺憾,目前僅 Mac App Store 版本支援瀏覽器整合功能。" - }, "browserIntegrationWindowsStoreDesc": { "message": "很遺憾,Microsoft Store 版本目前尚不支援瀏覽器整合功能。" }, @@ -4220,5 +4217,11 @@ }, "upgradeToPremium": { "message": "升級到 Premium" + }, + "sessionTimeoutSettingsAction": { + "message": "逾時後動作" + }, + "sessionTimeoutHeader": { + "message": "工作階段逾時" } } diff --git a/apps/desktop/src/main/menu/menu.view.ts b/apps/desktop/src/main/menu/menu.view.ts index 962c57fdb60..d24128730cc 100644 --- a/apps/desktop/src/main/menu/menu.view.ts +++ b/apps/desktop/src/main/menu/menu.view.ts @@ -6,6 +6,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { isDev } from "../../utils"; +import { WindowMain } from "../window.main"; import { IMenubarMenu } from "./menubar"; @@ -42,11 +43,18 @@ export class ViewMenu implements IMenubarMenu { private readonly _i18nService: I18nService; private readonly _messagingService: MessagingService; private readonly _isLocked: boolean; + private readonly _windowMain: WindowMain; - constructor(i18nService: I18nService, messagingService: MessagingService, isLocked: boolean) { + constructor( + i18nService: I18nService, + messagingService: MessagingService, + isLocked: boolean, + windowMain: WindowMain, + ) { this._i18nService = i18nService; this._messagingService = messagingService; this._isLocked = isLocked; + this._windowMain = windowMain; } private get searchVault(): MenuItemConstructorOptions { @@ -86,7 +94,12 @@ export class ViewMenu implements IMenubarMenu { return { id: "zoomIn", label: this.localize("zoomIn"), - role: "zoomIn", + click: async () => { + const currentZoom = this._windowMain.win.webContents.zoomFactor; + const newZoom = currentZoom + 0.1; + this._windowMain.win.webContents.zoomFactor = newZoom; + await this._windowMain.saveZoomFactor(newZoom); + }, accelerator: "CmdOrCtrl+=", }; } @@ -95,7 +108,12 @@ export class ViewMenu implements IMenubarMenu { return { id: "zoomOut", label: this.localize("zoomOut"), - role: "zoomOut", + click: async () => { + const currentZoom = this._windowMain.win.webContents.zoomFactor; + const newZoom = Math.max(0.2, currentZoom - 0.1); + this._windowMain.win.webContents.zoomFactor = newZoom; + await this._windowMain.saveZoomFactor(newZoom); + }, accelerator: "CmdOrCtrl+-", }; } @@ -104,7 +122,11 @@ export class ViewMenu implements IMenubarMenu { return { id: "resetZoom", label: this.localize("resetZoom"), - role: "resetZoom", + click: async () => { + const newZoom = 1.0; + this._windowMain.win.webContents.zoomFactor = newZoom; + await this._windowMain.saveZoomFactor(newZoom); + }, accelerator: "CmdOrCtrl+0", }; } diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts index 8ac3a084d95..0a00a67b84a 100644 --- a/apps/desktop/src/main/menu/menubar.ts +++ b/apps/desktop/src/main/menu/menubar.ts @@ -86,7 +86,7 @@ export class Menubar { updateRequest?.restrictedCipherTypes, ), new EditMenu(i18nService, messagingService, isLocked), - new ViewMenu(i18nService, messagingService, isLocked), + new ViewMenu(i18nService, messagingService, isLocked, windowMain), new AccountMenu( i18nService, messagingService, diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index f8ea7551c47..d148a1a35f8 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -303,7 +303,9 @@ export class WindowMain { this.win.webContents.zoomFactor = this.windowStates[mainWindowSizeKey].zoomFactor ?? 1.0; }); - // Persist zoom changes immediately when user zooms in/out or resets zoom + // Persist zoom changes from mouse wheel and programmatic zoom operations + // NOTE: This event does NOT fire for keyboard shortcuts (Ctrl+/-/0, Cmd+/-/0) + // which are handled by custom menu click handlers in ViewMenu // We can't depend on higher level web events (like close) to do this // because locking the vault resets window state. this.win.webContents.on("zoom-changed", async () => { @@ -432,6 +434,11 @@ export class WindowMain { await this.desktopSettingsService.setAlwaysOnTop(this.enableAlwaysOnTop); } + async saveZoomFactor(zoomFactor: number) { + this.windowStates[mainWindowSizeKey].zoomFactor = zoomFactor; + await this.desktopSettingsService.setWindow(this.windowStates[mainWindowSizeKey]); + } + private windowStateChangeHandler(configKey: string, win: BrowserWindow) { global.clearTimeout(this.windowStateChangeTimer); this.windowStateChangeTimer = global.setTimeout(async () => { diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 512f8c638ef..0a4204bf233 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.11.0", + "version": "2025.11.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.11.0", + "version": "2025.11.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index a24bd703248..765bc771b9e 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2025.11.0", + "version": "2025.11.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/platform/popup-modal-styles.ts b/apps/desktop/src/platform/popup-modal-styles.ts index ae46ebb5c76..5c5619bd463 100644 --- a/apps/desktop/src/platform/popup-modal-styles.ts +++ b/apps/desktop/src/platform/popup-modal-styles.ts @@ -45,11 +45,11 @@ export function applyMainWindowStyles(window: BrowserWindow, existingWindowState // need to guard against null/undefined values if (existingWindowState?.width && existingWindowState?.height) { - window.setSize(existingWindowState.width, existingWindowState.height); + window.setSize(Math.floor(existingWindowState.width), Math.floor(existingWindowState.height)); } if (existingWindowState?.x && existingWindowState?.y) { - window.setPosition(existingWindowState.x, existingWindowState.y); + window.setPosition(Math.floor(existingWindowState.x), Math.floor(existingWindowState.y)); } window.setWindowButtonVisibility?.(true); diff --git a/apps/web/package.json b/apps/web/package.json index b95d3e6aba5..c2db376e5da 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2025.11.2", + "version": "2025.11.3", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.html b/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.html index 9ddc9897a31..a8021e82c39 100644 --- a/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.html +++ b/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.html @@ -1 +1 @@ - + diff --git a/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts b/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts index 8a58f5b92d7..3c1d0d2b691 100644 --- a/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts @@ -1,36 +1,33 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input, OnChanges } from "@angular/core"; +import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { GroupView } from "../../core"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-group-badge", templateUrl: "group-name-badge.component.html", standalone: false, + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class GroupNameBadgeComponent implements OnChanges { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() selectedGroups: SelectionReadOnlyRequest[]; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() allGroups: GroupView[]; +export class GroupNameBadgeComponent { + readonly selectedGroups = input([]); + readonly allGroups = input([]); - protected groupNames: string[] = []; + protected readonly groupNames = computed(() => { + const allGroups = this.allGroups(); + if (!allGroups) { + return []; + } + + return this.selectedGroups() + .map((g) => { + return allGroups.find((o) => o.id === g.id)?.name; + }) + .filter((name): name is string => name !== undefined) + .sort(this.i18nService.collator.compare); + }); constructor(private i18nService: I18nService) {} - - ngOnChanges() { - this.groupNames = this.selectedGroups - .map((g) => { - return this.allGroups.find((o) => o.id === g.id)?.name; - }) - .sort(this.i18nService.collator.compare); - } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html index 0abc40da683..bd2237bc2fd 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html @@ -1,5 +1,5 @@

- {{ "organizationDataOwnershipContent" | i18n }} + {{ "organizationDataOwnershipDescContent" | i18n }} { if (this.policyResponse?.enabled && !this.enabled.value) { - const dialogRef = this.dialogService.open(this.warningContent); + const dialogRef = this.dialogService.open(this.warningContent, { + positionStrategy: new CenterPositionStrategy(), + }); const result = await lastValueFrom(dialogRef.closed); return Boolean(result); } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts index 5cb61197b99..3e23eff13a9 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts @@ -25,7 +25,7 @@ const render: Story["render"] = (args) => ({ ...args, }, template: ` - + Access selector (); + async ngOnInit(): Promise { const milestone3FeatureEnabled = await this.configService.getFeatureFlag( FeatureFlag.PM26462_Milestone_3, @@ -37,7 +39,7 @@ export class CreateOrganizationComponent implements OnInit { ? PlanType.FamiliesAnnually : PlanType.FamiliesAnnually2025; - this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => { + this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => { if (qParams.plan === "families" || qParams.productTier == ProductTierType.Families) { this.plan = familyPlan; this.productTier = ProductTierType.Families; @@ -61,4 +63,9 @@ export class CreateOrganizationComponent implements OnInit { this.secretsManager = qParams.product == ProductType.SecretsManager; }); } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html index 172646f5d4d..8baf304969f 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html @@ -25,23 +25,21 @@

  • {{ "twoFactorYubikeySaveForm" | i18n }}

  • -
    -
    -
    - {{ "yubikeyX" | i18n: (i + 1).toString() }} - - - -
    - {{ keys[i].existingKey }} - -
    +
    +
    + {{ "yubikeyX" | i18n: (i + 1).toString() }} + + + +
    + {{ keys[i].existingKey }} +
    diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index 7ee5891e8a9..255e1ef544c 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -23,6 +23,8 @@ import { ToastService } from "@bitwarden/components"; templateUrl: "adjust-subscription.component.html", standalone: false, }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28231): Use Component suffix +// eslint-disable-next-line @angular-eslint/component-class-suffix export class AdjustSubscription implements OnInit, OnDestroy { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 70e16ad3037..e0c1a12a80f 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -344,6 +344,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy data: { type: "Organization", id: this.organizationId, + plan: this.sub.plan.type, }, }); diff --git a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts index 5448f03aa56..9e7b870579d 100644 --- a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts @@ -1,9 +1,10 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { ChangeDetectionStrategy, Component, input, OnDestroy, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { map, Observable, of, startWith, Subject, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PopoverModule, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -34,18 +35,17 @@ type PaymentMethodFormGroup = FormGroup<{ }>; }>; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-enter-payment-method", + changeDetection: ChangeDetectionStrategy.OnPush, template: ` - @let showBillingDetails = includeBillingAddress && selected !== "payPal"; -
    + @let showBillingDetails = includeBillingAddress() && selected !== "payPal"; + @if (showBillingDetails) {
    {{ "paymentMethod" | i18n }}
    }
    - + @@ -60,7 +60,7 @@ type PaymentMethodFormGroup = FormGroup<{ } - @if (showPayPal) { + @if (showPayPal()) { @@ -68,7 +68,7 @@ type PaymentMethodFormGroup = FormGroup<{ } - @if (showAccountCredit) { + @if (showAccountCredit()) { @@ -82,10 +82,10 @@ type PaymentMethodFormGroup = FormGroup<{ @case ("card") {
    - + {{ "cardNumberLabel" | i18n }} -
    +
    - + {{ "expiration" | i18n }} -
    +
    - + {{ "securityCodeSlashCVV" | i18n }}
    } @@ -131,7 +131,7 @@ type PaymentMethodFormGroup = FormGroup<{ bitInput id="routingNumber" type="text" - [formControl]="group.controls.bankAccount.controls.routingNumber" + [formControl]="group().controls.bankAccount.controls.routingNumber" required /> @@ -141,7 +141,7 @@ type PaymentMethodFormGroup = FormGroup<{ bitInput id="accountNumber" type="text" - [formControl]="group.controls.bankAccount.controls.accountNumber" + [formControl]="group().controls.bankAccount.controls.accountNumber" required /> @@ -151,7 +151,7 @@ type PaymentMethodFormGroup = FormGroup<{ id="accountHolderName" bitInput type="text" - [formControl]="group.controls.bankAccount.controls.accountHolderName" + [formControl]="group().controls.bankAccount.controls.accountHolderName" required /> @@ -159,7 +159,7 @@ type PaymentMethodFormGroup = FormGroup<{ {{ "bankAccountType" | i18n }} @@ -186,7 +186,7 @@ type PaymentMethodFormGroup = FormGroup<{ } @case ("accountCredit") { - @if (hasEnoughAccountCredit) { + @if (hasEnoughAccountCredit()) { {{ "makeSureEnoughCredit" | i18n }} @@ -204,7 +204,7 @@ type PaymentMethodFormGroup = FormGroup<{
    {{ "country" | i18n }} - + @for (selectableCountry of selectableCountries; track selectableCountry.value) { @@ -233,26 +233,15 @@ type PaymentMethodFormGroup = FormGroup<{ standalone: true, imports: [BillingServicesModule, PaymentLabelComponent, PopoverModule, SharedModule], }) -export class EnterPaymentMethodComponent implements OnInit { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input({ required: true }) group!: PaymentMethodFormGroup; +export class EnterPaymentMethodComponent implements OnInit, OnDestroy { + protected readonly instanceId = Utils.newGuid(); - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() private showBankAccount = true; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() showPayPal = true; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() showAccountCredit = false; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() hasEnoughAccountCredit = true; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() includeBillingAddress = false; + readonly group = input.required(); + protected readonly showBankAccount = input(true); + readonly showPayPal = input(true); + readonly showAccountCredit = input(false); + readonly hasEnoughAccountCredit = input(true); + readonly includeBillingAddress = input(false); protected showBankAccount$!: Observable; protected selectableCountries = selectableCountries; @@ -269,57 +258,62 @@ export class EnterPaymentMethodComponent implements OnInit { ngOnInit() { this.stripeService.loadStripe( + this.instanceId, { - cardNumber: "#stripe-card-number", - cardExpiry: "#stripe-card-expiry", - cardCvc: "#stripe-card-cvc", + cardNumber: `#stripe-card-number-${this.instanceId}`, + cardExpiry: `#stripe-card-expiry-${this.instanceId}`, + cardCvc: `#stripe-card-cvc-${this.instanceId}`, }, true, ); - if (this.showPayPal) { + if (this.showPayPal()) { this.braintreeService.loadBraintree("#braintree-container", false); } - if (!this.includeBillingAddress) { - this.showBankAccount$ = of(this.showBankAccount); - this.group.controls.billingAddress.disable(); + if (!this.includeBillingAddress()) { + this.showBankAccount$ = of(this.showBankAccount()); + this.group().controls.billingAddress.disable(); } else { - this.group.controls.billingAddress.patchValue({ + this.group().controls.billingAddress.patchValue({ country: "US", }); - this.showBankAccount$ = this.group.controls.billingAddress.controls.country.valueChanges.pipe( - startWith(this.group.controls.billingAddress.controls.country.value), - map((country) => this.showBankAccount && country === "US"), - ); + this.showBankAccount$ = + this.group().controls.billingAddress.controls.country.valueChanges.pipe( + startWith(this.group().controls.billingAddress.controls.country.value), + map((country) => this.showBankAccount() && country === "US"), + ); } - this.group.controls.type.valueChanges - .pipe(startWith(this.group.controls.type.value), takeUntil(this.destroy$)) + this.group() + .controls.type.valueChanges.pipe( + startWith(this.group().controls.type.value), + takeUntil(this.destroy$), + ) .subscribe((selected) => { if (selected === "bankAccount") { - this.group.controls.bankAccount.enable(); - if (this.includeBillingAddress) { - this.group.controls.billingAddress.enable(); + this.group().controls.bankAccount.enable(); + if (this.includeBillingAddress()) { + this.group().controls.billingAddress.enable(); } } else { switch (selected) { case "card": { - this.stripeService.mountElements(); - if (this.includeBillingAddress) { - this.group.controls.billingAddress.enable(); + this.stripeService.mountElements(this.instanceId); + if (this.includeBillingAddress()) { + this.group().controls.billingAddress.enable(); } break; } case "payPal": { this.braintreeService.createDropin(); - if (this.includeBillingAddress) { - this.group.controls.billingAddress.disable(); + if (this.includeBillingAddress()) { + this.group().controls.billingAddress.disable(); } break; } } - this.group.controls.bankAccount.disable(); + this.group().controls.bankAccount.disable(); } }); @@ -330,22 +324,28 @@ export class EnterPaymentMethodComponent implements OnInit { }); } + ngOnDestroy() { + this.stripeService.unloadStripe(this.instanceId); + this.destroy$.next(); + this.destroy$.complete(); + } + select = (paymentMethod: PaymentMethodOption) => - this.group.controls.type.patchValue(paymentMethod); + this.group().controls.type.patchValue(paymentMethod); tokenize = async (): Promise => { const exchange = async (paymentMethod: TokenizablePaymentMethod) => { switch (paymentMethod) { case "bankAccount": { - this.group.controls.bankAccount.markAllAsTouched(); - if (!this.group.controls.bankAccount.valid) { + this.group().controls.bankAccount.markAllAsTouched(); + if (!this.group().controls.bankAccount.valid) { throw new Error("Attempted to tokenize invalid bank account information."); } - const bankAccount = this.group.controls.bankAccount.getRawValue(); + const bankAccount = this.group().controls.bankAccount.getRawValue(); const clientSecret = await this.stripeService.createSetupIntent("bankAccount"); - const billingDetails = this.group.controls.billingAddress.enabled - ? this.group.controls.billingAddress.getRawValue() + const billingDetails = this.group().controls.billingAddress.enabled + ? this.group().controls.billingAddress.getRawValue() : undefined; return await this.stripeService.setupBankAccountPaymentMethod( clientSecret, @@ -355,10 +355,14 @@ export class EnterPaymentMethodComponent implements OnInit { } case "card": { const clientSecret = await this.stripeService.createSetupIntent("card"); - const billingDetails = this.group.controls.billingAddress.enabled - ? this.group.controls.billingAddress.getRawValue() + const billingDetails = this.group().controls.billingAddress.enabled + ? this.group().controls.billingAddress.getRawValue() : undefined; - return this.stripeService.setupCardPaymentMethod(clientSecret, billingDetails); + return this.stripeService.setupCardPaymentMethod( + this.instanceId, + clientSecret, + billingDetails, + ); } case "payPal": { return this.braintreeService.requestPaymentMethod(); @@ -410,15 +414,15 @@ export class EnterPaymentMethodComponent implements OnInit { validate = (): boolean => { if (this.selected === "bankAccount") { - this.group.controls.bankAccount.markAllAsTouched(); - return this.group.controls.bankAccount.valid; + this.group().controls.bankAccount.markAllAsTouched(); + return this.group().controls.bankAccount.valid; } return true; }; get selected(): PaymentMethodOption { - return this.group.value.type!; + return this.group().value.type!; } static getFormGroup = (): PaymentMethodFormGroup => diff --git a/apps/web/src/app/billing/services/stripe.service.spec.ts b/apps/web/src/app/billing/services/stripe.service.spec.ts new file mode 100644 index 00000000000..983aeb266ae --- /dev/null +++ b/apps/web/src/app/billing/services/stripe.service.spec.ts @@ -0,0 +1,797 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BankAccount } from "@bitwarden/common/billing/models/domain"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { StripeService } from "./stripe.service"; + +// Extend Window interface to include Stripe +declare global { + interface Window { + Stripe: any; + } +} + +describe("StripeService", () => { + let service: StripeService; + let apiService: MockProxy; + let logService: MockProxy; + + // Stripe SDK mocks + let mockStripeInstance: any; + let mockElements: any; + let mockCardNumber: any; + let mockCardExpiry: any; + let mockCardCvc: any; + + // DOM mocks + let mockScript: HTMLScriptElement; + let mockIframe: HTMLIFrameElement; + + beforeEach(() => { + jest.useFakeTimers(); + + // Setup service dependency mocks + apiService = mock(); + logService = mock(); + + // Setup Stripe element mocks + mockCardNumber = { + mount: jest.fn(), + unmount: jest.fn(), + }; + mockCardExpiry = { + mount: jest.fn(), + unmount: jest.fn(), + }; + mockCardCvc = { + mount: jest.fn(), + unmount: jest.fn(), + }; + + // Setup Stripe Elements mock + mockElements = { + create: jest.fn((type: string) => { + switch (type) { + case "cardNumber": + return mockCardNumber; + case "cardExpiry": + return mockCardExpiry; + case "cardCvc": + return mockCardCvc; + default: + return null; + } + }), + getElement: jest.fn((type: string) => { + switch (type) { + case "cardNumber": + return mockCardNumber; + case "cardExpiry": + return mockCardExpiry; + case "cardCvc": + return mockCardCvc; + default: + return null; + } + }), + }; + + // Setup Stripe instance mock + mockStripeInstance = { + elements: jest.fn(() => mockElements), + confirmCardSetup: jest.fn(), + confirmUsBankAccountSetup: jest.fn(), + }; + + // Setup window.Stripe mock + window.Stripe = jest.fn(() => mockStripeInstance); + + // Setup DOM mocks + mockScript = { + id: "", + src: "", + onload: null, + onerror: null, + } as any; + + mockIframe = { + src: "https://js.stripe.com/v3/", + remove: jest.fn(), + } as any; + + jest.spyOn(window.document, "createElement").mockReturnValue(mockScript); + jest.spyOn(window.document, "getElementById").mockReturnValue(null); + jest.spyOn(window.document.head, "appendChild").mockReturnValue(mockScript); + jest.spyOn(window.document.head, "removeChild").mockImplementation(() => mockScript); + jest.spyOn(window.document, "querySelectorAll").mockReturnValue([mockIframe] as any); + + // Mock getComputedStyle + jest.spyOn(window, "getComputedStyle").mockReturnValue({ + getPropertyValue: (prop: string) => { + const props: Record = { + "--color-text-main": "0, 0, 0", + "--color-text-muted": "128, 128, 128", + "--color-danger-600": "220, 38, 38", + }; + return props[prop] || ""; + }, + } as any); + + // Create service instance + service = new StripeService(apiService, logService); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + jest.restoreAllMocks(); + }); + + // Helper function to trigger script load + const triggerScriptLoad = () => { + if (mockScript.onload) { + mockScript.onload(new Event("load")); + } + }; + + // Helper function to advance timers and flush promises + const advanceTimersAndFlush = async (ms: number) => { + jest.advanceTimersByTime(ms); + await Promise.resolve(); + }; + + describe("createSetupIntent", () => { + it("should call API with correct path for card payment", async () => { + apiService.send.mockResolvedValue("client_secret_card_123"); + + const result = await service.createSetupIntent("card"); + + expect(apiService.send).toHaveBeenCalledWith("POST", "/setup-intent/card", null, true, true); + expect(result).toBe("client_secret_card_123"); + }); + + it("should call API with correct path for bank account payment", async () => { + apiService.send.mockResolvedValue("client_secret_bank_456"); + + const result = await service.createSetupIntent("bankAccount"); + + expect(apiService.send).toHaveBeenCalledWith( + "POST", + "/setup-intent/bank-account", + null, + true, + true, + ); + expect(result).toBe("client_secret_bank_456"); + }); + + it("should return client secret from API response", async () => { + const expectedSecret = "seti_1234567890_secret_abcdefg"; + apiService.send.mockResolvedValue(expectedSecret); + + const result = await service.createSetupIntent("card"); + + expect(result).toBe(expectedSecret); + }); + + it("should propagate API errors", async () => { + const error = new Error("API error"); + apiService.send.mockRejectedValue(error); + + await expect(service.createSetupIntent("card")).rejects.toThrow("API error"); + }); + }); + + describe("loadStripe - initial load", () => { + const instanceId = "test-instance-1"; + const elementIds = { + cardNumber: "#card-number", + cardExpiry: "#card-expiry", + cardCvc: "#card-cvc", + }; + + it("should create script element with correct attributes", () => { + service.loadStripe(instanceId, elementIds, false); + + expect(window.document.createElement).toHaveBeenCalledWith("script"); + expect(mockScript.id).toBe("stripe-script"); + expect(mockScript.src).toBe("https://js.stripe.com/v3?advancedFraudSignals=false"); + }); + + it("should append script to document head", () => { + service.loadStripe(instanceId, elementIds, false); + + expect(window.document.head.appendChild).toHaveBeenCalledWith(mockScript); + }); + + it("should initialize Stripe client on script load", async () => { + service.loadStripe(instanceId, elementIds, false); + + triggerScriptLoad(); + await advanceTimersAndFlush(0); + + expect(window.Stripe).toHaveBeenCalledWith(process.env.STRIPE_KEY); + }); + + it("should create Elements instance and store in Map", async () => { + service.loadStripe(instanceId, elementIds, false); + + triggerScriptLoad(); + await advanceTimersAndFlush(50); + + expect(mockStripeInstance.elements).toHaveBeenCalled(); + expect(service["instances"].size).toBe(1); + expect(service["instances"].get(instanceId)).toBeDefined(); + }); + + it("should increment instanceCount", async () => { + service.loadStripe(instanceId, elementIds, false); + + triggerScriptLoad(); + await advanceTimersAndFlush(50); + + expect(service["instanceCount"]).toBe(1); + }); + }); + + describe("loadStripe - already loaded", () => { + const instanceId1 = "instance-1"; + const instanceId2 = "instance-2"; + const elementIds = { + cardNumber: "#card-number", + cardExpiry: "#card-expiry", + cardCvc: "#card-cvc", + }; + + beforeEach(async () => { + // Load first instance to initialize Stripe + service.loadStripe(instanceId1, elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + }); + + it("should not create new script if already loaded", () => { + jest.clearAllMocks(); + + service.loadStripe(instanceId2, elementIds, false); + + expect(window.document.createElement).not.toHaveBeenCalled(); + expect(window.document.head.appendChild).not.toHaveBeenCalled(); + }); + + it("should immediately initialize instance when script loaded", async () => { + service.loadStripe(instanceId2, elementIds, false); + await advanceTimersAndFlush(50); + + expect(service["instances"].size).toBe(2); + expect(service["instances"].get(instanceId2)).toBeDefined(); + }); + + it("should increment instanceCount correctly", async () => { + expect(service["instanceCount"]).toBe(1); + + service.loadStripe(instanceId2, elementIds, false); + await advanceTimersAndFlush(50); + + expect(service["instanceCount"]).toBe(2); + }); + }); + + describe("loadStripe - concurrent calls", () => { + const elementIds = { + cardNumber: "#card-number", + cardExpiry: "#card-expiry", + cardCvc: "#card-cvc", + }; + + it("should handle multiple loadStripe calls sequentially", async () => { + // Test practical scenario: load instances one after another + service.loadStripe("instance-1", elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + + service.loadStripe("instance-2", elementIds, false); + await advanceTimersAndFlush(100); + + service.loadStripe("instance-3", elementIds, false); + await advanceTimersAndFlush(100); + + // All instances should be initialized + expect(service["instances"].size).toBe(3); + expect(service["instanceCount"]).toBe(3); + expect(service["instances"].get("instance-1")).toBeDefined(); + expect(service["instances"].get("instance-2")).toBeDefined(); + expect(service["instances"].get("instance-3")).toBeDefined(); + }); + + it("should share Stripe client across instances", async () => { + // Load first instance + service.loadStripe("instance-1", elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + + const stripeClientAfterFirst = service["stripe"]; + expect(stripeClientAfterFirst).toBeDefined(); + + // Load second instance + service.loadStripe("instance-2", elementIds, false); + await advanceTimersAndFlush(100); + + // Should reuse the same Stripe client + expect(service["stripe"]).toBe(stripeClientAfterFirst); + expect(service["instances"].size).toBe(2); + }); + }); + + describe("mountElements - success path", () => { + const instanceId = "mount-test-instance"; + const elementIds = { + cardNumber: "#card-number-mount", + cardExpiry: "#card-expiry-mount", + cardCvc: "#card-cvc-mount", + }; + + beforeEach(async () => { + service.loadStripe(instanceId, elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + }); + + it("should mount all three card elements to DOM", async () => { + service.mountElements(instanceId); + await advanceTimersAndFlush(100); + + expect(mockCardNumber.mount).toHaveBeenCalledWith("#card-number-mount"); + expect(mockCardExpiry.mount).toHaveBeenCalledWith("#card-expiry-mount"); + expect(mockCardCvc.mount).toHaveBeenCalledWith("#card-cvc-mount"); + }); + + it("should use correct element IDs from instance", async () => { + const customIds = { + cardNumber: "#custom-card", + cardExpiry: "#custom-expiry", + cardCvc: "#custom-cvc", + }; + + service.loadStripe("custom-instance", customIds, false); + await advanceTimersAndFlush(100); + + service.mountElements("custom-instance"); + await advanceTimersAndFlush(100); + + expect(mockCardNumber.mount).toHaveBeenCalledWith("#custom-card"); + expect(mockCardExpiry.mount).toHaveBeenCalledWith("#custom-expiry"); + expect(mockCardCvc.mount).toHaveBeenCalledWith("#custom-cvc"); + }); + + it("should handle autoMount flag correctly", async () => { + const autoMountId = "auto-mount-instance"; + jest.clearAllMocks(); + + service.loadStripe(autoMountId, elementIds, true); + triggerScriptLoad(); + await advanceTimersAndFlush(150); + + // Should auto-mount without explicit call + expect(mockCardNumber.mount).toHaveBeenCalled(); + expect(mockCardExpiry.mount).toHaveBeenCalled(); + expect(mockCardCvc.mount).toHaveBeenCalled(); + }); + }); + + describe("mountElements - retry logic", () => { + const elementIds = { + cardNumber: "#card-number", + cardExpiry: "#card-expiry", + cardCvc: "#card-cvc", + }; + + it("should retry if instance not found", async () => { + service.mountElements("non-existent-instance"); + await advanceTimersAndFlush(100); + + expect(logService.warning).toHaveBeenCalledWith( + expect.stringContaining("Stripe instance non-existent-instance not found"), + ); + }); + + it("should log error after 10 failed attempts", async () => { + service.mountElements("non-existent-instance"); + + for (let i = 0; i < 10; i++) { + await advanceTimersAndFlush(100); + } + + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("not found after 10 attempts"), + ); + }); + + it("should retry if elements not ready", async () => { + const instanceId = "retry-elements-instance"; + service.loadStripe(instanceId, elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + + // Make elements temporarily unavailable + mockElements.getElement.mockReturnValueOnce(null); + mockElements.getElement.mockReturnValueOnce(null); + mockElements.getElement.mockReturnValueOnce(null); + + service.mountElements(instanceId); + await advanceTimersAndFlush(100); + + expect(logService.warning).toHaveBeenCalledWith( + expect.stringContaining("Some Stripe card elements"), + ); + }); + }); + + describe("setupCardPaymentMethod", () => { + const instanceId = "card-setup-instance"; + const clientSecret = "seti_card_secret_123"; + const elementIds = { + cardNumber: "#card-number", + cardExpiry: "#card-expiry", + cardCvc: "#card-cvc", + }; + + beforeEach(async () => { + service.loadStripe(instanceId, elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + }); + + it("should call Stripe confirmCardSetup with correct parameters", async () => { + mockStripeInstance.confirmCardSetup.mockResolvedValue({ + setupIntent: { status: "succeeded", payment_method: "pm_card_123" }, + }); + + await service.setupCardPaymentMethod(instanceId, clientSecret); + + expect(mockStripeInstance.confirmCardSetup).toHaveBeenCalledWith(clientSecret, { + payment_method: { + card: mockCardNumber, + }, + }); + }); + + it("should include billing details when provided", async () => { + mockStripeInstance.confirmCardSetup.mockResolvedValue({ + setupIntent: { status: "succeeded", payment_method: "pm_card_123" }, + }); + + const billingDetails = { country: "US", postalCode: "12345" }; + await service.setupCardPaymentMethod(instanceId, clientSecret, billingDetails); + + expect(mockStripeInstance.confirmCardSetup).toHaveBeenCalledWith(clientSecret, { + payment_method: { + card: mockCardNumber, + billing_details: { + address: { + country: "US", + postal_code: "12345", + }, + }, + }, + }); + }); + + it("should throw error if instance not found", async () => { + await expect(service.setupCardPaymentMethod("non-existent", clientSecret)).rejects.toThrow( + "Payment method initialization failed. Please try again.", + ); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Stripe instance non-existent not found"), + ); + }); + + it("should throw error if setup fails", async () => { + const error = { message: "Card declined" }; + mockStripeInstance.confirmCardSetup.mockResolvedValue({ error }); + + await expect(service.setupCardPaymentMethod(instanceId, clientSecret)).rejects.toEqual(error); + expect(logService.error).toHaveBeenCalledWith(error); + }); + + it("should throw error if status is not succeeded", async () => { + const error = { message: "Invalid status" }; + mockStripeInstance.confirmCardSetup.mockResolvedValue({ + setupIntent: { status: "requires_action" }, + error, + }); + + await expect(service.setupCardPaymentMethod(instanceId, clientSecret)).rejects.toEqual(error); + }); + + it("should return payment method ID on success", async () => { + mockStripeInstance.confirmCardSetup.mockResolvedValue({ + setupIntent: { status: "succeeded", payment_method: "pm_card_success_123" }, + }); + + const result = await service.setupCardPaymentMethod(instanceId, clientSecret); + + expect(result).toBe("pm_card_success_123"); + }); + }); + + describe("setupBankAccountPaymentMethod", () => { + const clientSecret = "seti_bank_secret_456"; + const bankAccount: BankAccount = { + accountHolderName: "John Doe", + routingNumber: "110000000", + accountNumber: "000123456789", + accountHolderType: "individual", + }; + + beforeEach(async () => { + // Initialize Stripe instance for bank account tests + service.loadStripe( + "bank-test-instance", + { + cardNumber: "#card", + cardExpiry: "#expiry", + cardCvc: "#cvc", + }, + false, + ); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + }); + + it("should call Stripe confirmUsBankAccountSetup with bank details", async () => { + mockStripeInstance.confirmUsBankAccountSetup.mockResolvedValue({ + setupIntent: { status: "requires_action", payment_method: "pm_bank_123" }, + }); + + await service.setupBankAccountPaymentMethod(clientSecret, bankAccount); + + expect(mockStripeInstance.confirmUsBankAccountSetup).toHaveBeenCalledWith(clientSecret, { + payment_method: { + us_bank_account: { + routing_number: "110000000", + account_number: "000123456789", + account_holder_type: "individual", + }, + billing_details: { + name: "John Doe", + }, + }, + }); + }); + + it("should include billing address when provided", async () => { + mockStripeInstance.confirmUsBankAccountSetup.mockResolvedValue({ + setupIntent: { status: "requires_action", payment_method: "pm_bank_123" }, + }); + + const billingDetails = { country: "US", postalCode: "90210" }; + await service.setupBankAccountPaymentMethod(clientSecret, bankAccount, billingDetails); + + expect(mockStripeInstance.confirmUsBankAccountSetup).toHaveBeenCalledWith(clientSecret, { + payment_method: { + us_bank_account: { + routing_number: "110000000", + account_number: "000123456789", + account_holder_type: "individual", + }, + billing_details: { + name: "John Doe", + address: { + country: "US", + postal_code: "90210", + }, + }, + }, + }); + }); + + it("should omit billing address when not provided", async () => { + mockStripeInstance.confirmUsBankAccountSetup.mockResolvedValue({ + setupIntent: { status: "requires_action", payment_method: "pm_bank_123" }, + }); + + await service.setupBankAccountPaymentMethod(clientSecret, bankAccount); + + const call = mockStripeInstance.confirmUsBankAccountSetup.mock.calls[0][1]; + expect(call.payment_method.billing_details.address).toBeUndefined(); + }); + + it("should validate status is requires_action", async () => { + const error = { message: "Invalid status" }; + mockStripeInstance.confirmUsBankAccountSetup.mockResolvedValue({ + setupIntent: { status: "succeeded" }, + error, + }); + + await expect( + service.setupBankAccountPaymentMethod(clientSecret, bankAccount), + ).rejects.toEqual(error); + }); + + it("should return payment method ID on success", async () => { + mockStripeInstance.confirmUsBankAccountSetup.mockResolvedValue({ + setupIntent: { status: "requires_action", payment_method: "pm_bank_success_456" }, + }); + + const result = await service.setupBankAccountPaymentMethod(clientSecret, bankAccount); + + expect(result).toBe("pm_bank_success_456"); + }); + }); + + describe("unloadStripe - single instance", () => { + const instanceId = "unload-test-instance"; + const elementIds = { + cardNumber: "#card-number", + cardExpiry: "#card-expiry", + cardCvc: "#card-cvc", + }; + + beforeEach(async () => { + service.loadStripe(instanceId, elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + }); + + it("should unmount all card elements", () => { + service.unloadStripe(instanceId); + + expect(mockCardNumber.unmount).toHaveBeenCalled(); + expect(mockCardExpiry.unmount).toHaveBeenCalled(); + expect(mockCardCvc.unmount).toHaveBeenCalled(); + }); + + it("should remove instance from Map", () => { + expect(service["instances"].has(instanceId)).toBe(true); + + service.unloadStripe(instanceId); + + expect(service["instances"].has(instanceId)).toBe(false); + }); + + it("should decrement instanceCount", () => { + expect(service["instanceCount"]).toBe(1); + + service.unloadStripe(instanceId); + + expect(service["instanceCount"]).toBe(0); + }); + + it("should remove script when last instance unloaded", () => { + jest.spyOn(window.document, "getElementById").mockReturnValue(mockScript); + + service.unloadStripe(instanceId); + + expect(window.document.head.removeChild).toHaveBeenCalledWith(mockScript); + }); + + it("should remove Stripe iframes after cleanup delay", async () => { + service.unloadStripe(instanceId); + + await advanceTimersAndFlush(500); + + expect(window.document.querySelectorAll).toHaveBeenCalledWith("iframe"); + expect(mockIframe.remove).toHaveBeenCalled(); + }); + }); + + describe("unloadStripe - multiple instances", () => { + const elementIds = { + cardNumber: "#card-number", + cardExpiry: "#card-expiry", + cardCvc: "#card-cvc", + }; + + beforeEach(async () => { + // Load first instance + service.loadStripe("instance-1", elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + + // Load second instance (script already loaded) + service.loadStripe("instance-2", elementIds, false); + await advanceTimersAndFlush(100); + }); + + it("should not remove script when other instances exist", () => { + expect(service["instanceCount"]).toBe(2); + + service.unloadStripe("instance-1"); + + expect(service["instanceCount"]).toBe(1); + expect(window.document.head.removeChild).not.toHaveBeenCalled(); + }); + + it("should only cleanup specific instance", () => { + service.unloadStripe("instance-1"); + + expect(service["instances"].has("instance-1")).toBe(false); + expect(service["instances"].has("instance-2")).toBe(true); + }); + + it("should handle reference counting correctly", () => { + expect(service["instanceCount"]).toBe(2); + + service.unloadStripe("instance-1"); + expect(service["instanceCount"]).toBe(1); + + service.unloadStripe("instance-2"); + expect(service["instanceCount"]).toBe(0); + }); + }); + + describe("unloadStripe - edge cases", () => { + it("should handle unload of non-existent instance gracefully", () => { + expect(() => service.unloadStripe("non-existent")).not.toThrow(); + expect(service["instanceCount"]).toBe(0); + }); + + it("should handle duplicate unload calls", async () => { + const instanceId = "duplicate-unload"; + const elementIds = { + cardNumber: "#card-number", + cardExpiry: "#card-expiry", + cardCvc: "#card-cvc", + }; + + service.loadStripe(instanceId, elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + + service.unloadStripe(instanceId); + expect(service["instanceCount"]).toBe(0); + + service.unloadStripe(instanceId); + expect(service["instanceCount"]).toBe(0); // Should not go negative + }); + + it("should catch and log element unmount errors", async () => { + const instanceId = "error-unmount"; + const elementIds = { + cardNumber: "#card-number", + cardExpiry: "#card-expiry", + cardCvc: "#card-cvc", + }; + + service.loadStripe(instanceId, elementIds, false); + triggerScriptLoad(); + await advanceTimersAndFlush(100); + + const unmountError = new Error("Unmount failed"); + mockCardNumber.unmount.mockImplementation(() => { + throw unmountError; + }); + + service.unloadStripe(instanceId); + + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Error unmounting Stripe elements"), + unmountError, + ); + }); + }); + + describe("element styling", () => { + it("should apply correct CSS custom properties", () => { + const options = service["getElementOptions"]("cardNumber"); + + expect(options.style.base.color).toBe("rgb(0, 0, 0)"); + expect(options.style.base["::placeholder"].color).toBe("rgb(128, 128, 128)"); + expect(options.style.invalid.color).toBe("rgb(0, 0, 0)"); + expect(options.style.invalid.borderColor).toBe("rgb(220, 38, 38)"); + }); + + it("should remove placeholder for cardNumber and cardCvc", () => { + const cardNumberOptions = service["getElementOptions"]("cardNumber"); + const cardCvcOptions = service["getElementOptions"]("cardCvc"); + const cardExpiryOptions = service["getElementOptions"]("cardExpiry"); + + expect(cardNumberOptions.placeholder).toBe(""); + expect(cardCvcOptions.placeholder).toBe(""); + expect(cardExpiryOptions.placeholder).toBeUndefined(); + }); + }); +}); diff --git a/apps/web/src/app/billing/services/stripe.service.ts b/apps/web/src/app/billing/services/stripe.service.ts index a2eb7cd98f2..9aabab9beb0 100644 --- a/apps/web/src/app/billing/services/stripe.service.ts +++ b/apps/web/src/app/billing/services/stripe.service.ts @@ -8,8 +8,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { BankAccountPaymentMethod, CardPaymentMethod } from "../payment/types"; -import { BillingServicesModule } from "./billing-services.module"; - type SetupBankAccountRequest = { payment_method: { us_bank_account: { @@ -39,15 +37,21 @@ type SetupCardRequest = { }; }; -@Injectable({ providedIn: BillingServicesModule }) +@Injectable({ providedIn: "root" }) export class StripeService { - private stripe: any; - private elements: any; - private elementIds: { - cardNumber: string; - cardExpiry: string; - cardCvc: string; - }; + // Shared/Global - One Stripe client for entire application + private stripe: any = null; + private stripeScriptLoaded = false; + private instanceCount = 0; + + // Per-Instance - Isolated Elements for each component + private instances = new Map< + string, + { + elements: any; + elementIds: { cardNumber: string; cardExpiry: string; cardCvc: string }; + } + >(); constructor( private apiService: ApiService, @@ -76,53 +80,121 @@ export class StripeService { * Loads [Stripe JS]{@link https://docs.stripe.com/js} in the element of the current page and mounts * Stripe credit card [elements]{@link https://docs.stripe.com/js/elements_object/create} into the HTML elements with the provided element IDS. * We do this to avoid having to load the Stripe JS SDK on every page of the Web Vault given many pages contain sensitive information. + * @param instanceId - Unique identifier for this component instance. * @param elementIds - The ID attributes of the HTML elements used to load the Stripe JS credit card elements. * @param autoMount - A flag indicating whether you want to immediately mount the Stripe credit card elements. */ loadStripe( + instanceId: string, elementIds: { cardNumber: string; cardExpiry: string; cardCvc: string }, autoMount: boolean, ) { - this.elementIds = elementIds; - const script = window.document.createElement("script"); - script.id = "stripe-script"; - script.src = "https://js.stripe.com/v3?advancedFraudSignals=false"; - script.onload = async () => { - const window$ = window as any; - this.stripe = window$.Stripe(process.env.STRIPE_KEY); - this.elements = this.stripe.elements(); - setTimeout(() => { - this.elements.create("cardNumber", this.getElementOptions("cardNumber")); - this.elements.create("cardExpiry", this.getElementOptions("cardExpiry")); - this.elements.create("cardCvc", this.getElementOptions("cardCvc")); - if (autoMount) { - this.mountElements(); - } - }, 50); - }; + // Check if script is already loaded + if (this.stripeScriptLoaded) { + // Script already loaded, initialize this instance immediately + this.initializeInstance(instanceId, elementIds, autoMount); + } else if (!window.document.getElementById("stripe-script")) { + // Script not loaded and not loading, start loading it + const script = window.document.createElement("script"); + script.id = "stripe-script"; + script.src = "https://js.stripe.com/v3?advancedFraudSignals=false"; + script.onload = async () => { + const window$ = window as any; + this.stripe = window$.Stripe(process.env.STRIPE_KEY); + this.stripeScriptLoaded = true; // Mark as loaded after script loads - window.document.head.appendChild(script); + // Initialize this instance after script loads + this.initializeInstance(instanceId, elementIds, autoMount); + }; + window.document.head.appendChild(script); + } else { + // Script is currently loading, wait for it + this.initializeInstance(instanceId, elementIds, autoMount); + } } - mountElements(attempt: number = 1) { - setTimeout(() => { - if (!this.elements) { - this.logService.warning(`Stripe elements are missing, retrying for attempt ${attempt}...`); - this.mountElements(attempt + 1); + private initializeInstance( + instanceId: string, + elementIds: { cardNumber: string; cardExpiry: string; cardCvc: string }, + autoMount: boolean, + attempt: number = 1, + ) { + // Wait for stripe to be available if script just loaded + if (!this.stripe) { + if (attempt < 10) { + this.logService.warning( + `Stripe not yet loaded for instance ${instanceId}, retrying attempt ${attempt}...`, + ); + setTimeout( + () => this.initializeInstance(instanceId, elementIds, autoMount, attempt + 1), + 50, + ); } else { - const cardNumber = this.elements.getElement("cardNumber"); - const cardExpiry = this.elements.getElement("cardExpiry"); - const cardCVC = this.elements.getElement("cardCvc"); + this.logService.error( + `Stripe failed to load for instance ${instanceId} after ${attempt} attempts`, + ); + } + return; + } + + // Create a new Elements instance for this component + const elements = this.stripe.elements(); + + // Store instance data + this.instances.set(instanceId, { elements, elementIds }); + + // Increment instance count now that instance is successfully initialized + this.instanceCount++; + + // Create the card elements + setTimeout(() => { + elements.create("cardNumber", this.getElementOptions("cardNumber")); + elements.create("cardExpiry", this.getElementOptions("cardExpiry")); + elements.create("cardCvc", this.getElementOptions("cardCvc")); + + if (autoMount) { + this.mountElements(instanceId); + } + }, 50); + } + + mountElements(instanceId: string, attempt: number = 1) { + setTimeout(() => { + const instance = this.instances.get(instanceId); + + if (!instance) { + if (attempt < 10) { + this.logService.warning( + `Stripe instance ${instanceId} not found, retrying for attempt ${attempt}...`, + ); + this.mountElements(instanceId, attempt + 1); + } else { + this.logService.error( + `Stripe instance ${instanceId} not found after ${attempt} attempts`, + ); + } + return; + } + + if (!instance.elements) { + this.logService.warning( + `Stripe elements for instance ${instanceId} are missing, retrying for attempt ${attempt}...`, + ); + this.mountElements(instanceId, attempt + 1); + } else { + const cardNumber = instance.elements.getElement("cardNumber"); + const cardExpiry = instance.elements.getElement("cardExpiry"); + const cardCVC = instance.elements.getElement("cardCvc"); if ([cardNumber, cardExpiry, cardCVC].some((element) => !element)) { this.logService.warning( - `Some Stripe card elements are missing, retrying for attempt ${attempt}...`, + `Some Stripe card elements for instance ${instanceId} are missing, retrying for attempt ${attempt}...`, ); - this.mountElements(attempt + 1); + this.mountElements(instanceId, attempt + 1); } else { - cardNumber.mount(this.elementIds.cardNumber); - cardExpiry.mount(this.elementIds.cardExpiry); - cardCVC.mount(this.elementIds.cardCvc); + cardNumber.mount(instance.elementIds.cardNumber); + cardExpiry.mount(instance.elementIds.cardExpiry); + cardCVC.mount(instance.elementIds.cardCvc); } } }, 100); @@ -132,6 +204,9 @@ export class StripeService { * Creates a Stripe [SetupIntent]{@link https://docs.stripe.com/api/setup_intents} and uses the resulting client secret * to invoke the Stripe JS [confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} method, * thereby creating and storing a Stripe [PaymentMethod]{@link https://docs.stripe.com/api/payment_methods}. + * @param clientSecret - The client secret from the SetupIntent. + * @param bankAccount - The bank account details. + * @param billingDetails - Optional billing details. * @returns The ID of the newly created PaymentMethod. */ async setupBankAccountPaymentMethod( @@ -171,13 +246,28 @@ export class StripeService { * Creates a Stripe [SetupIntent]{@link https://docs.stripe.com/api/setup_intents} and uses the resulting client secret * to invoke the Stripe JS [confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} method, * thereby creating and storing a Stripe [PaymentMethod]{@link https://docs.stripe.com/api/payment_methods}. + * @param instanceId - Unique identifier for the component instance. + * @param clientSecret - The client secret from the SetupIntent. + * @param billingDetails - Optional billing details. * @returns The ID of the newly created PaymentMethod. */ async setupCardPaymentMethod( + instanceId: string, clientSecret: string, billingDetails?: { country: string; postalCode: string }, ): Promise { - const cardNumber = this.elements.getElement("cardNumber"); + const instance = this.instances.get(instanceId); + if (!instance) { + const availableInstances = Array.from(this.instances.keys()); + this.logService.error( + `Stripe instance ${instanceId} not found. ` + + `Available instances: [${availableInstances.join(", ")}]. ` + + `This may occur if the component was destroyed during the payment flow.`, + ); + throw new Error("Payment method initialization failed. Please try again."); + } + + const cardNumber = instance.elements.getElement("cardNumber"); const request: SetupCardRequest = { payment_method: { card: cardNumber, @@ -200,24 +290,77 @@ export class StripeService { } /** - * Removes {@link https://docs.stripe.com/js} from the element of the current page as well as all - * Stripe-managed